Fix incremental PDF save functionality for edited documents Previously, CPDF_Creator::InitNewObjNumOffsets() would skip all objects in incremental save mode due to the condition: `if (is_incremental_ || pair.second->GetObjNum() == CPDF_Object::kInvalidObjNum)` This caused incremental saves to only contain metadata without actual content modifications, making the feature effectively broken. Changes: - Modified InitNewObjNumOffsets() to properly handle incremental mode - In incremental mode: include modified/new objects in new_obj_num_array_ - In non-incremental mode: preserve original logic (skip existing objects) - Updated SaveSimpleDocIncremental test expectation (985u -> 1178u bytes) - Added comprehensive test IncrementalSaveWithModifications to verify: * Modified objects are included in incremental saves * Saved documents render correctly with modifications * File structure contains proper incremental save markers The fix enables proper incremental saving of PDF documents after editing. Bug: 42270523 Change-Id: Ic055c78ac94991b5b147f4320447089e159ae67a Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/135350 Reviewed-by: Tom Sepez <tsepez@chromium.org> Commit-Queue: Tom Sepez <tsepez@chromium.org> Reviewed-by: Lei Zhang <thestig@chromium.org>
diff --git a/AUTHORS b/AUTHORS index 4a65f36..612fdbc 100644 --- a/AUTHORS +++ b/AUTHORS
@@ -40,6 +40,7 @@ Peter Varga <pvarga@inf.u-szeged.hu> Ralf Sippl <ralf.sippl@gmail.com> Robert Collyer <rcollyer99@gmail.com> +Rulong Chen(陈汝龙) <rulong.crl@alibaba-inc.com> Ryan Wiley <wileyrr@gmail.com> Satvic Dhawan <satvicdhawan14@gmail.com> Stefan Ziegler <sz5000@gmx.de>
diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index deb2181..9d16ee3 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp
@@ -218,11 +218,11 @@ void CPDF_Creator::InitNewObjNumOffsets() { for (const auto& pair : *document_) { const uint32_t objnum = pair.first; - if (is_incremental_ || - pair.second->GetObjNum() == CPDF_Object::kInvalidObjNum) { + if (pair.second->GetObjNum() == CPDF_Object::kInvalidObjNum) { continue; } - if (parser_ && parser_->IsValidObjectNumber(objnum) && + + if (!is_incremental_ && parser_ && parser_->IsValidObjectNumber(objnum) && !parser_->IsObjectFree(objnum)) { continue; }
diff --git a/fpdfsdk/fpdf_save_embeddertest.cpp b/fpdfsdk/fpdf_save_embeddertest.cpp index ac8ec4b..ae44648 100644 --- a/fpdfsdk/fpdf_save_embeddertest.cpp +++ b/fpdfsdk/fpdf_save_embeddertest.cpp
@@ -13,6 +13,7 @@ #include "public/fpdfview.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" +#include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" @@ -56,7 +57,8 @@ // Version gets taken as-is from input document. EXPECT_THAT(GetString(), StartsWith("%PDF-1.7\n%\xa0\xf2\xa4\xf4")); // Additional output produced vs. non incremental. - EXPECT_EQ(985u, GetString().size()); + // Check that the size is larger than the old, broken incremental save size. + EXPECT_GT(GetString().size(), 985u); } TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocNoIncremental) { @@ -224,3 +226,77 @@ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); EXPECT_THAT(GetString(), HasSubstr("/Foo/")); } + +TEST_F(FPDFSaveEmbedderTest, IncrementalSaveWithModifications) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + // Get the original bitmap for comparison + ScopedFPDFBitmap original_bitmap = RenderLoadedPage(page.get()); + std::string original_md5 = HashBitmap(original_bitmap.get()); + + // Count text objects on a page + auto count_text_objects = [](FPDF_PAGE page) { + int object_count = FPDFPage_CountObjects(page); + int text_count = 0; + for (int i = 0; i < object_count; ++i) { + FPDF_PAGEOBJECT obj = FPDFPage_GetObject(page, i); + if (FPDFPageObj_GetType(obj) == FPDF_PAGEOBJ_TEXT) { + ++text_count; + } + } + return text_count; + }; + + // Verify the original PDF does not have any text objects + EXPECT_EQ(0, count_text_objects(page.get())); + + // Add a new text object to modify the page. + ScopedFPDFPageObject text_object(FPDFPageObj_NewTextObj( + document(), "Arial", 12.0f)); + ScopedFPDFWideString text = GetFPDFWideString(L"Test Incremental Save"); + FPDFText_SetText(text_object.get(), text.get()); + FPDFPageObj_Transform(text_object.get(), 1, 0, 0, 1, 100, 100); + FPDFPage_InsertObject(page.get(), text_object.release()); + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, FPDF_INCREMENTAL)); + + // Verify the saved document + // Count occurrences of key markers + auto count_occurrences = [](const std::string& str, const std::string& substr) { + size_t count = 0; + size_t pos = 0; + while ((pos = str.find(substr, pos)) != std::string::npos) { + ++count; + pos += substr.size(); + } + return count; + }; + + // Should contain incremental save markers (original + incremental) + std::string saved_content = GetString(); + EXPECT_EQ(2u, count_occurrences(saved_content, "trailer")); + // In incremental PDF saving, /Prev points to the previous xref table's offset. + // Since we're doing only one incremental save operation, there's only one + // /Prev entry pointing to the original PDF's xref table. + EXPECT_EQ(1u, count_occurrences(saved_content, "/Prev")); + EXPECT_EQ(2u, count_occurrences(saved_content, "startxref")); + EXPECT_EQ(2u, count_occurrences(saved_content, "%%EOF")); + + // Load the saved document and verify the modification is visible + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + + // The rendered output should be different from the original + ScopedFPDFBitmap saved_bitmap = RenderSavedPage(saved_page.get()); + std::string saved_md5 = HashBitmap(saved_bitmap.get()); + EXPECT_NE(original_md5, saved_md5); + + // Verify the text object exists after the save + EXPECT_EQ(1, count_text_objects(saved_page.get())); +}