Skip to content

Commit a3f16c2

Browse files
committed
Prune unreachable new PDF objects during save
Make `CPDF_Creator::WriteNewObjs()` reachability-aware so newly allocated indirect objects that are no longer referenced do not get written into saved PDFs. The save reachability set now includes normal document graph references plus trailer-owned roots such as `/Info` and non-inline `/Encrypt`, avoiding dangling trailer references while still pruning true orphans. Also refactor `EPDF_SetEncryption()` to use an inline encryption dictionary so it follows CPDF_Creator’s existing trailer-owned encrypt path, and update Bug1206 to assert render-only saves remain stable while reopened rendering still matches.
1 parent efe597b commit a3f16c2

4 files changed

Lines changed: 165 additions & 13 deletions

File tree

core/fpdfapi/edit/cpdf_creator.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <array>
1414
#include <set>
1515
#include <utility>
16+
#include <vector>
1617

1718
#include "core/fpdfapi/parser/cpdf_array.h"
1819
#include "core/fpdfapi/parser/cpdf_crypto_handler.h"
@@ -136,6 +137,28 @@ ByteString FormatXrefOffset10(FX_FILESIZE offset) {
136137
return ByteString::Format("%010" PRId64, static_cast<int64_t>(offset));
137138
}
138139

140+
std::set<uint32_t> CollectSaveReachableObjects(
141+
CPDF_Document* document,
142+
const CPDF_Dictionary* encrypt_dict) {
143+
std::set<uint32_t> objects = GetObjectsWithReferences(document);
144+
145+
// `GetObjectsWithReferences()` covers the normal document graph rooted at
146+
// /Root. The save trailer may also reference dictionaries outside that graph.
147+
// Keep those roots in sync with the trailer entries emitted in
148+
// WriteDoc_Stage4().
149+
RetainPtr<CPDF_Dictionary> info = document->GetInfo();
150+
if (info && info->GetObjNum() != 0) {
151+
objects.insert(info->GetObjNum());
152+
}
153+
154+
if (encrypt_dict && !encrypt_dict->IsInline() &&
155+
encrypt_dict->GetObjNum() != 0) {
156+
objects.insert(encrypt_dict->GetObjNum());
157+
}
158+
159+
return objects;
160+
}
161+
139162
} // namespace
140163

141164
CPDF_Creator::CPDF_Creator(CPDF_Document* doc,
@@ -223,8 +246,15 @@ bool CPDF_Creator::WriteOldObjs() {
223246
}
224247

225248
bool CPDF_Creator::WriteNewObjs() {
249+
const std::set<uint32_t> objects_with_refs =
250+
CollectSaveReachableObjects(document_, encrypt_dict_.Get());
251+
std::vector<uint32_t> written_new_obj_nums;
226252
for (size_t i = cur_obj_num_; i < new_obj_num_array_.size(); ++i) {
227253
uint32_t objnum = new_obj_num_array_[i];
254+
if (!pdfium::Contains(objects_with_refs, objnum)) {
255+
continue;
256+
}
257+
228258
RetainPtr<const CPDF_Object> pObj = document_->GetIndirectObject(objnum);
229259
if (!pObj) {
230260
continue;
@@ -234,7 +264,9 @@ bool CPDF_Creator::WriteNewObjs() {
234264
if (!WriteIndirectObj(pObj->GetObjNum(), pObj.Get())) {
235265
return false;
236266
}
267+
written_new_obj_nums.push_back(objnum);
237268
}
269+
new_obj_num_array_ = std::move(written_new_obj_nums);
238270
return true;
239271
}
240272

fpdfsdk/fpdf_annot_embeddertest.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,26 +1997,31 @@ TEST_F(FPDFAnnotEmbedderTest, GetFormAnnotAndCheckFlagsComboBox) {
19971997
}
19981998

19991999
TEST_F(FPDFAnnotEmbedderTest, Bug1206) {
2000-
static constexpr size_t kExpectedMinimumOriginalSize = 1601;
2001-
20022000
ASSERT_TRUE(OpenDocument("bug_1206.pdf"));
20032001

20042002
ScopedPage page = LoadScopedPage(0);
20052003
ASSERT_TRUE(page);
20062004

20072005
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
20082006
const size_t original_size = GetString().size();
2009-
EXPECT_LE(kExpectedMinimumOriginalSize, original_size); // Sanity check.
2007+
ASSERT_GT(original_size, 0u);
20102008
ClearString();
20112009

20122010
for (size_t i = 0; i < 10; ++i) {
20132011
ScopedFPDFBitmap bitmap = RenderLoadedPageWithFlags(page.get(), FPDF_ANNOT);
20142012
CompareBitmapWithExpectationSuffix(bitmap.get(), "bug_1206");
20152013

20162014
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
2017-
// TODO(https://crbug.com/42270200): This is wrong. The size should be
2018-
// equal, not bigger.
2019-
EXPECT_GT(GetString().size(), original_size);
2015+
EXPECT_EQ(original_size, GetString().size());
2016+
2017+
ScopedSavedDoc saved_doc = OpenScopedSavedDocument();
2018+
ASSERT_TRUE(saved_doc);
2019+
ScopedSavedPage saved_page = LoadScopedSavedPage(0);
2020+
ASSERT_TRUE(saved_page);
2021+
ScopedFPDFBitmap saved_bitmap =
2022+
RenderSavedPageWithFlags(saved_page.get(), FPDF_ANNOT);
2023+
CompareBitmapWithExpectationSuffix(saved_bitmap.get(), "bug_1206");
2024+
20202025
ClearString();
20212026
}
20222027
}
@@ -2585,7 +2590,8 @@ TEST_F(FPDFAnnotEmbedderTest, GetFontSizeNegative) {
25852590
}
25862591
}
25872592

2588-
TEST_F(FPDFAnnotEmbedderTest, DirectAnnotationObjectNumberStaysZeroAfterRender) {
2593+
TEST_F(FPDFAnnotEmbedderTest,
2594+
DirectAnnotationObjectNumberStaysZeroAfterRender) {
25892595
ASSERT_TRUE(OpenDocument("freetext_annotation_without_da.pdf"));
25902596
ScopedPage page = LoadScopedPage(0);
25912597
ASSERT_TRUE(page);
@@ -2600,8 +2606,8 @@ TEST_F(FPDFAnnotEmbedderTest, DirectAnnotationObjectNumberStaysZeroAfterRender)
26002606
ASSERT_TRUE(bitmap);
26012607
EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get()));
26022608

2603-
ASSERT_TRUE(EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 10,
2604-
20, 30));
2609+
ASSERT_TRUE(
2610+
EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 10, 20, 30));
26052611
EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get()));
26062612
}
26072613

fpdfsdk/fpdf_save_embeddertest.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@
66
#include <string>
77
#include <vector>
88

9+
#include "core/fpdfapi/parser/cpdf_cross_ref_table.h"
10+
#include "core/fpdfapi/parser/cpdf_document.h"
11+
#include "core/fpdfapi/parser/cpdf_parser.h"
912
#include "core/fxcrt/fx_string.h"
13+
#include "fpdfsdk/cpdfsdk_helpers.h"
1014
#include "public/cpp/fpdf_scopers.h"
15+
#include "public/fpdf_annot.h"
1116
#include "public/fpdf_edit.h"
1217
#include "public/fpdf_ppo.h"
1318
#include "public/fpdf_save.h"
@@ -24,6 +29,23 @@ using testing::HasSubstr;
2429
using testing::Not;
2530
using testing::StartsWith;
2631

32+
namespace {
33+
34+
bool HasSavedXRefEntryForObject(FPDF_DOCUMENT document, uint32_t objnum) {
35+
CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(document);
36+
if (!cpdf_doc || !cpdf_doc->GetParser() ||
37+
!cpdf_doc->GetParser()->GetCrossRefTable()) {
38+
return false;
39+
}
40+
41+
const CPDF_CrossRefTable::ObjectInfo* info =
42+
cpdf_doc->GetParser()->GetCrossRefTable()->GetObjectInfo(objnum);
43+
return info && (info->type == CPDF_CrossRefTable::ObjectType::kNormal ||
44+
info->type == CPDF_CrossRefTable::ObjectType::kCompressed);
45+
}
46+
47+
} // namespace
48+
2749
class FPDFSaveEmbedderTest : public EmbedderTest {};
2850

2951
TEST_F(FPDFSaveEmbedderTest, SaveSimpleDoc) {
@@ -85,6 +107,98 @@ TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocIncremental) {
85107
EXPECT_GT(GetString().size(), 985u);
86108
}
87109

110+
TEST_F(FPDFSaveEmbedderTest, SaveAsCopyPrunesUnlinkedNewAnnotation) {
111+
ASSERT_TRUE(OpenDocument("rectangles.pdf"));
112+
ScopedPage page = LoadScopedPage(0);
113+
ASSERT_TRUE(page);
114+
ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get()));
115+
116+
ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT));
117+
ASSERT_TRUE(annot);
118+
const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get());
119+
ASSERT_GT(annot_objnum, 0u);
120+
ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get()));
121+
122+
annot.reset();
123+
ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 0));
124+
ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get()));
125+
126+
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
127+
ScopedSavedDoc saved_doc = OpenScopedSavedDocument();
128+
ASSERT_TRUE(saved_doc);
129+
EXPECT_FALSE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum));
130+
131+
ScopedSavedPage saved_page = LoadScopedSavedPage(0);
132+
ASSERT_TRUE(saved_page);
133+
EXPECT_EQ(0, FPDFPage_GetAnnotCount(saved_page.get()));
134+
}
135+
136+
TEST_F(FPDFSaveEmbedderTest, IncrementalSavePrunesUnlinkedNewAnnotation) {
137+
ASSERT_TRUE(OpenDocument("rectangles.pdf"));
138+
ScopedPage page = LoadScopedPage(0);
139+
ASSERT_TRUE(page);
140+
ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get()));
141+
142+
ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT));
143+
ASSERT_TRUE(annot);
144+
const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get());
145+
ASSERT_GT(annot_objnum, 0u);
146+
147+
annot.reset();
148+
ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 0));
149+
ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get()));
150+
151+
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, FPDF_INCREMENTAL));
152+
ScopedSavedDoc saved_doc = OpenScopedSavedDocument();
153+
ASSERT_TRUE(saved_doc);
154+
EXPECT_FALSE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum));
155+
156+
ScopedSavedPage saved_page = LoadScopedSavedPage(0);
157+
ASSERT_TRUE(saved_page);
158+
EXPECT_EQ(0, FPDFPage_GetAnnotCount(saved_page.get()));
159+
}
160+
161+
TEST_F(FPDFSaveEmbedderTest, SaveAsCopyKeepsLinkedNewAnnotation) {
162+
ASSERT_TRUE(OpenDocument("rectangles.pdf"));
163+
ScopedPage page = LoadScopedPage(0);
164+
ASSERT_TRUE(page);
165+
ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get()));
166+
167+
ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT));
168+
ASSERT_TRUE(annot);
169+
const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get());
170+
ASSERT_GT(annot_objnum, 0u);
171+
ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get()));
172+
173+
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
174+
ScopedSavedDoc saved_doc = OpenScopedSavedDocument();
175+
ASSERT_TRUE(saved_doc);
176+
EXPECT_TRUE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum));
177+
178+
ScopedSavedPage saved_page = LoadScopedSavedPage(0);
179+
ASSERT_TRUE(saved_page);
180+
ASSERT_EQ(1, FPDFPage_GetAnnotCount(saved_page.get()));
181+
ScopedFPDFAnnotation saved_annot(FPDFPage_GetAnnot(saved_page.get(), 0));
182+
ASSERT_TRUE(saved_annot);
183+
EXPECT_EQ(FPDF_ANNOT_TEXT, FPDFAnnot_GetSubtype(saved_annot.get()));
184+
}
185+
186+
TEST_F(FPDFSaveEmbedderTest, SetEncryptionRoundTripsWithInlineEncryptDict) {
187+
ASSERT_TRUE(OpenDocument("hello_world.pdf"));
188+
ASSERT_TRUE(EPDF_SetEncryption(document(), "user", "owner",
189+
EPDF_PERM_PRINT | EPDF_PERM_COPY));
190+
191+
ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
192+
ScopedSavedDoc saved_doc = OpenScopedSavedDocumentWithPassword("user");
193+
ASSERT_TRUE(saved_doc);
194+
EXPECT_TRUE(EPDF_IsEncrypted(saved_doc.get()));
195+
196+
ScopedSavedPage saved_page = LoadScopedSavedPage(0);
197+
ASSERT_TRUE(saved_page);
198+
ScopedFPDFBitmap bitmap = RenderSavedPage(saved_page.get());
199+
ASSERT_TRUE(bitmap);
200+
}
201+
88202
TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocNoIncremental) {
89203
ASSERT_TRUE(OpenDocument("hello_world.pdf"));
90204
EXPECT_TRUE(FPDF_SaveWithVersion(document(), this, FPDF_NO_INCREMENTAL, 14));

fpdfsdk/fpdf_view.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,10 @@ EPDF_SetEncryption(FPDF_DOCUMENT document,
524524
int32_t permissions =
525525
static_cast<int32_t>(BuildPermissionsForRevision(allowed_flags_32));
526526

527-
// Create encrypt dictionary as indirect object
528-
auto pEncryptDict = pDoc->NewIndirect<CPDF_Dictionary>();
527+
// Create the encrypt dictionary inline. CPDF_Creator::SetEncryption() owns
528+
// the trailer-only reference and writes inline encrypt dictionaries as
529+
// indirect objects during save.
530+
auto pEncryptDict = pDoc->New<CPDF_Dictionary>();
529531
pEncryptDict->SetNewFor<CPDF_Name>("Filter", "Standard");
530532
pEncryptDict->SetNewFor<CPDF_Number>("V", 5);
531533
pEncryptDict->SetNewFor<CPDF_Number>("R", 6);
@@ -552,8 +554,6 @@ EPDF_SetEncryption(FPDF_DOCUMENT document,
552554
// Returns false if LoadDict fails or R != 6
553555
if (!pSecurityHandler->OnCreateWithPasswords(pEncryptDict.Get(), user_pwd,
554556
owner_pwd)) {
555-
// Cleanup the indirect object we created
556-
pDoc->DeleteIndirectObject(pEncryptDict->GetObjNum());
557557
return false;
558558
}
559559

0 commit comments

Comments
 (0)