diff --git a/core/fpdfapi/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp index f27215caf..78ded83b6 100644 --- a/core/fpdfapi/parser/cpdf_document.cpp +++ b/core/fpdfapi/parser/cpdf_document.cpp @@ -738,6 +738,15 @@ RetainPtr CPDF_Document::CreateNewPageAt(int iPage, return dict; } +// EmbedPDF(epdf-api): backs EPDFPage_ImportRaw(). +bool CPDF_Document::InsertExistingPage(int iPage, + RetainPtr page_dict) { + if (!page_dict || !page_dict->GetObjNum()) { + return false; + } + return InsertNewPage(iPage, std::move(page_dict)); +} + bool CPDF_Document::InsertDeletePDFPage( RetainPtr pages_dict, int pages_to_go, diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index a51fbf245..346705d96 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -312,6 +312,10 @@ class CPDF_Document : public Observable, // when the number can't take it or the page can't be inserted; nothing // changes then. RetainPtr CreateNewPageAt(int iPage, uint32_t objnum); + // EmbedPDF(epdf-api): puts |page_dict|, a page out of the page tree, back + // into it at |iPage|, for EPDFPage_ImportRaw(). False when |iPage| is out of + // range or the page tree can't take it. + bool InsertExistingPage(int iPage, RetainPtr page_dict); void IncrementParsedPageCount() { ++parsed_page_count_; } uint32_t GetParsedPageCountForTesting() { return parsed_page_count_; } diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 39f6de523..27b819d9d 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -40,8 +40,10 @@ source_set("fpdfsdk") { "epdf_outline.cpp", "epdf_outline_helpers.cpp", "epdf_outline_helpers.h", - # EmbedPDF: the catalog's /Names /Pages and /Templates name trees. + # EmbedPDF: the catalog's /Names /Pages and /Templates name trees, and + # the lookups page captures share. "epdf_named_pages.cpp", + "epdf_named_pages_helpers.h", # EmbedPDF: digests (EPDF_DigestBuffer) and the hasher signatures share. "epdf_digest.cpp", "epdf_hasher.h", diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 60458bab5..aecddb9de 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -224,7 +224,9 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFOutline_ExportItemRawToOwnedBuffer); CHK(EPDFOutline_ImportItemRaw); CHK(EPDFPage_ExportAnnotsRawToOwnedBuffer); + CHK(EPDFPage_ExportRawToOwnedBuffer); CHK(EPDFPage_ImportAnnotsRaw); + CHK(EPDFPage_ImportRaw); // epdf_catalog.h CHK(EPDFCatalog_GetLanguage); diff --git a/fpdfsdk/epdf_capture.cpp b/fpdfsdk/epdf_capture.cpp index d503a96e2..467098186 100644 --- a/fpdfsdk/epdf_capture.cpp +++ b/fpdfsdk/epdf_capture.cpp @@ -21,6 +21,7 @@ #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/cpdf_syntax_parser.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/cfx_read_only_span_stream.h" @@ -29,8 +30,10 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/span.h" +#include "core/fxcrt/widestring.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/epdf_form_helpers.h" +#include "fpdfsdk/epdf_named_pages_helpers.h" #include "fpdfsdk/epdf_object_helpers.h" #include "fpdfsdk/epdf_outline_helpers.h" #include "fpdfsdk/epdf_owned_buffer.h" @@ -57,7 +60,8 @@ bool IsOutlineLinkKey(ByteStringView key) { key == "SE"; } -// Objects a capture never holds: the catalog, the page tree and its pages. +// Objects a capture never holds: the catalog, the page tree and its pages, +// save the page a page capture is of (CaptureWriter::AddPage()). bool IsBoundaryObject(const CPDF_Object* object) { RetainPtr dict = object->GetDict(); if (!dict) { @@ -133,6 +137,13 @@ class CaptureWriter { AddObject(objnum); } + // Captures the page |objnum| as AddObject() does: a page is a boundary + // object, but not the page the capture is of. + void AddPage(uint32_t objnum) { + pages_.insert(objnum); + AddObject(objnum); + } + // The header, |manifest|, then every captured object in number order. DataVector Write(const CPDF_Dictionary* manifest) const { fxcrt::ostringstream text; @@ -198,7 +209,8 @@ class CaptureWriter { pending_.pop_front(); RetainPtr object = layer_->GetOrParseIndirectObject(objnum); - if (!object || IsBoundaryObject(object.Get())) { + if (!object || + (IsBoundaryObject(object.Get()) && !pages_.contains(objnum))) { continue; } if (RetainPtr version = layer_->FindLayerVersion(objnum)) { @@ -251,6 +263,7 @@ class CaptureWriter { UnownedPtr const layer_; std::set outline_items_; + std::set pages_; std::set visited_; std::deque pending_; std::map> objects_; @@ -867,3 +880,109 @@ EPDFOutline_ImportItemRaw(FPDF_DOCUMENT document, return epdf::WriteOutlineChildren(layer, node, children) && epdf::RecountOutline(layer, *root, item); } + +FPDF_EXPORT void* FPDF_CALLCONV +EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, + uint32_t page_objnum, + unsigned long* out_size) { + if (out_size) { + *out_size = 0; + } + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !page_objnum) { + return nullptr; + } + CPDF_DocumentViewScope view(layer); + const int page_index = layer->GetPageIndex(page_objnum); + if (page_index < 0) { + return nullptr; + } + RetainPtr before = + page_index > 0 ? layer->GetPageDictionary(page_index - 1) : nullptr; + + // The page, where it was (After: the page it followed, 0 when it was the + // first), and the keys that name it. + auto manifest = pdfium::MakeRetain(); + manifest->SetNewFor("Kind", "Page"); + manifest->SetNewFor("Page", + pdfium::checked_cast(page_objnum)); + manifest->SetNewFor( + "After", before ? pdfium::checked_cast(before->GetObjNum()) : 0); + auto names = manifest->SetNewFor("Names"); + for (const WideString& key : epdf::NamedPageKeysOf(layer, page_objnum)) { + names->AppendNew(key.AsStringView()); + } + CaptureWriter writer(layer); + writer.AddPage(page_objnum); + return ToOwnedBuffer(writer.Write(manifest.Get()), out_size); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_ImportRaw(FPDF_DOCUMENT document, + const void* capture, + unsigned long size) { + CPDF_LayerDocument* layer = LayerOf(document); + if (!layer || !layer->InTransaction() || !capture || !size) { + return false; + } + CPDF_DocumentViewScope view(layer); + // SAFETY: required from caller. + std::optional read = ReadCapture( + layer, + UNSAFE_BUFFERS(pdfium::span(static_cast(capture), size)), + "Page"); + if (!read) { + return false; + } + const int page_number = read->manifest->GetIntegerFor("Page"); + const int after = read->manifest->GetIntegerFor("After"); + if (page_number <= 0 || after < 0 || + static_cast(page_number) > layer->GetLastObjNum()) { + return false; + } + const uint32_t page = static_cast(page_number); + // It must not be back already. + if (layer->GetPageIndex(page) >= 0) { + return false; + } + // It comes back from the capture, or as the page the layer reads now (the + // file's original, when the layer never edited it). + auto captured = read->objects.find(page); + const bool in_capture = captured != read->objects.end(); + RetainPtr reads_now = + ToDictionary(layer->GetOrParseIndirectObject(page)); + const CPDF_Dictionary* comes_back = + in_capture ? captured->second->AsDictionary() : reads_now.Get(); + if (!comes_back || comes_back->GetNameFor("Type") != "Page") { + return false; + } + // After the page it followed while that is still in the page tree, else + // first. + const int anchor = + after > 0 ? layer->GetPageIndex(static_cast(after)) : -1; + const int slot = anchor >= 0 ? anchor + 1 : 0; + + // A page the layer made reads as null once deleted (SetPageToNullObject(), + // so nothing that still points at it saves it): it comes back over that + // null. + if (in_capture) { + RetainPtr held = layer->FindLayerVersion(page); + if (held && held->IsNull()) { + layer->DeleteIndirectObject(page); + } + } + if (!InstallObjects(layer, *read)) { + return false; + } + RetainPtr page_dict = + ToDictionary(layer->GetMutableIndirectObject(page)); + if (!page_dict || !layer->InsertExistingPage(slot, std::move(page_dict))) { + return false; + } + // A key another page holds now stays with it. + RetainPtr names = read->manifest->GetArrayFor("Names"); + for (size_t i = 0; names && i < names->size(); ++i) { + epdf::AddNamedPageIfFree(layer, names->GetUnicodeTextAt(i), page); + } + return true; +} diff --git a/fpdfsdk/epdf_capture_embeddertest.cpp b/fpdfsdk/epdf_capture_embeddertest.cpp index db3accf0e..2d2597463 100644 --- a/fpdfsdk/epdf_capture_embeddertest.cpp +++ b/fpdfsdk/epdf_capture_embeddertest.cpp @@ -10,6 +10,7 @@ #include +#include #include #include @@ -30,10 +31,14 @@ #include "public/cpp/fpdf_scopers.h" #include "public/epdf_annot.h" #include "public/epdf_capture.h" +#include "public/epdf_edit.h" #include "public/epdf_form.h" #include "public/epdf_layer.h" +#include "public/epdf_named_pages.h" #include "public/epdf_save.h" +#include "public/epdf_view.h" #include "public/fpdf_annot.h" +#include "public/fpdf_edit.h" #include "public/fpdf_save.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" @@ -266,6 +271,89 @@ uint32_t CreateSquare(FPDF_DOCUMENT doc, int page_index, uint32_t objnum) { return Recolor(doc, objnum, 0, 1, 0); } +// The objects of the document of pages below. +constexpr uint32_t kPageTree = 2; +constexpr uint32_t kCover = 3; +constexpr uint32_t kIntro = 4; +constexpr uint32_t kBody = 5; +constexpr uint32_t kCoverLink = 8; +constexpr uint32_t kIntroSquare = 9; +constexpr uint32_t kSignField = 10; +constexpr uint32_t kSignWidget = 11; + +// Three pages, two of them named. The cover (3) links to the body; the +// intro (4) holds a blue square (9, its appearance 12), and nothing points at +// it but the page tree and its name; the body (5) holds the widget (11) of the +// field `sign` (10). +std::string MakePagesDocument() { + return MakePdf({ + "<< /Type /Catalog /Pages 2 0 R /Names << /Pages << /Names [(Cover) 3 0 " + "R (Intro) 4 0 R] >> >> /AcroForm << /Fields [10 0 R] >> >>", + "<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 6 0 R " + "/Resources << >> /Annots [8 0 R] >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 7 0 R " + "/Resources << >> /Annots [9 0 R] >>", + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 300] /Contents 6 0 R " + "/Resources << >> /Annots [11 0 R] >>", + Stream("0 0 1 rg 0 0 10 10 re f"), + Stream("1 0 0 rg 0 0 20 20 re f"), + "<< /Type /Annot /Subtype /Link /Rect [10 10 50 50] /P 3 0 R " + "/Dest [5 0 R /Fit] >>", + "<< /Type /Annot /Subtype /Square /Rect [20 200 80 260] /C [0 0 1] " + "/P 4 0 R /AP << /N 12 0 R >> >>", + "<< /T (sign) /FT /Tx /Kids [11 0 R] >>", + "<< /Type /Annot /Subtype /Widget /Parent 10 0 R /Rect [20 20 180 44] " + "/P 5 0 R >>", + "<< /Type /XObject /Subtype /Form /BBox [0 0 60 60] /Length 23 >>\n" + "stream\n0 0 1 rg 0 0 60 60 re f\nendstream", + }); +} + +// The pages of |doc| by object number, in page order. +std::vector PageOrder(FPDF_DOCUMENT doc) { + std::vector pages; + for (int i = 0; i < FPDF_GetPageCount(doc); ++i) { + pages.push_back(EPDFDoc_GetPageObjectNumberByIndex(doc, i)); + } + return pages; +} + +// The /Names /Pages registrations of |doc|, as "key=page", in tree order. +std::vector NamedPages(FPDF_DOCUMENT doc) { + std::vector entries; + const int count = EPDFDoc_GetNamedPageCount(doc, EPDF_NAMED_PAGE_TREE_PAGES); + for (int i = 0; i < count; ++i) { + const unsigned long length = EPDFDoc_GetNamedPageAt( + doc, EPDF_NAMED_PAGE_TREE_PAGES, i, nullptr, 0, nullptr, nullptr); + std::vector buffer = GetFPDFWideStringBuffer(length); + unsigned int page = 0; + EPDFDoc_GetNamedPageAt(doc, EPDF_NAMED_PAGE_TREE_PAGES, i, buffer.data(), + length, &page, nullptr); + entries.push_back(GetPlatformWString(buffer.data()) + L"=" + + std::to_wstring(page)); + } + return entries; +} + +// Whether |capture| holds the object |objnum|. +bool HoldsObject(const std::vector& capture, uint32_t objnum) { + const std::string text(capture.begin(), capture.end()); + return text.find("\n" + std::to_string(objnum) + " 0 obj\n") != + std::string::npos; +} + +std::vector ExportPage(FPDF_DOCUMENT doc, uint32_t page_objnum) { + unsigned long size = 0; + void* buffer = EPDFPage_ExportRawToOwnedBuffer(doc, page_objnum, &size); + return Take(buffer, size); +} + +bool ImportPage(FPDF_DOCUMENT doc, const std::vector& capture) { + return EPDFPage_ImportRaw(doc, capture.data(), + static_cast(capture.size())); +} + } // namespace class EPDFCaptureEmbedderTest : public EmbedderTest { @@ -732,3 +820,304 @@ TEST_F(EPDFCaptureEmbedderTest, PlainDocumentHasNoCaptures) { EXPECT_TRUE(ExportDict(doc.get(), kFileSquare, "AP").empty()); EXPECT_TRUE(ExportField(doc.get(), kMergedField).empty()); } + +// Pages (EPDFPage_ExportRawToOwnedBuffer and EPDFPage_ImportRaw), over the +// document of pages above. +class EPDFPageCaptureEmbedderTest : public EmbedderTest { + protected: + void SetUp() override { + EmbedderTest::SetUp(); + input_ = MakePagesDocument(); + base_ = EPDF_LoadMemBaseDocument(input_.data(), input_.size(), nullptr); + ASSERT_TRUE(base_); + } + + void TearDown() override { + EPDF_ReleaseBaseDocument(base_); + EmbedderTest::TearDown(); + } + + ScopedFPDFDocument OpenLayer() { + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base_, nullptr, nullptr, nullptr)); + if (layer && !EPDFLayer_RaiseLastObjectNumber(layer.get(), kRaised)) { + return nullptr; + } + return layer; + } + + // The layer saved and opened again: what nothing reaches is gone. Each + // artifact stays alive as long as the test, for the documents reading it. + ScopedFPDFDocument Reopen(FPDF_DOCUMENT layer) { + ClearString(); + EXPECT_TRUE(EPDFLayer_SaveLayerArtifact(layer, this, nullptr)); + std::string& artifact = artifacts_.emplace_back(GetString()); + FPDF_FILEACCESS access = {}; + access.m_FileLen = artifact.size(); + access.m_GetBlock = GetBlockFromString; + access.m_Param = &artifact; + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument reopened( + EPDFLayer_OpenLayerArtifact(base_, &access, nullptr, &status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + return reopened; + } + + // The page tree's /Count. + int CountOf(FPDF_DOCUMENT doc) { + RetainPtr pages = DictOf(doc, kPageTree); + return pages ? pages->GetIntegerFor("Count") : -1; + } + + std::string input_; + std::list artifacts_; + EPDF_BASE_DOCUMENT base_ = nullptr; +}; + +// A page of the file comes back at its place, with its square and its name. +// The layer turned the page and recoloured the square, so the capture holds +// those edits: they come back also when a save and reopen dropped the +// layer's versions, which nothing reached once the page was gone. +TEST_F(EPDFPageCaptureEmbedderTest, DeletedFilePageComesBackWithItsEdits) { + for (bool reopen : {false, true}) { + SCOPED_TRACE(reopen ? "reopened between delete and import" + : "same session"); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + const std::string blue = ColorOf(layer.get(), kIntroSquare); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kIntro, 1)); + Recolor(layer.get(), kIntroSquare, 1, 0, 0); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::string red = ColorOf(layer.get(), kIntroSquare); + ASSERT_NE(blue, red); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), kIntro); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kBody)); + EXPECT_THAT(NamedPages(layer.get()), ElementsAre(L"Cover=3")); + + ScopedFPDFDocument target = reopen ? Reopen(layer.get()) : std::move(layer); + ASSERT_TRUE(target); + if (reopen) { + EXPECT_EQ(0, DictOf(target.get(), kIntro)->GetIntegerFor("Rotate")); + EXPECT_EQ(blue, ColorOf(target.get(), kIntroSquare)); + } + ASSERT_TRUE(EPDFLayer_BeginTransaction(target.get())); + ASSERT_TRUE(ImportPage(target.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(target.get())); + EXPECT_THAT(PageOrder(target.get()), ElementsAre(kCover, kIntro, kBody)); + EXPECT_EQ(3, CountOf(target.get())); + EXPECT_EQ(kPageTree, RefOf(target.get(), kIntro, "Parent")); + EXPECT_EQ(90, DictOf(target.get(), kIntro)->GetIntegerFor("Rotate")); + EXPECT_THAT(Annots(target.get(), 1), ElementsAre(kIntroSquare)); + EXPECT_EQ(red, ColorOf(target.get(), kIntroSquare)); + EXPECT_THAT(NamedPages(target.get()), ElementsAre(L"Cover=3", L"Intro=4")); + ScopedFPDFPage page(FPDF_LoadPage(target.get(), 1)); + ASSERT_TRUE(page); + EXPECT_EQ(1, FPDFPage_GetRotation(page.get())); + } +} + +// A page the layer made comes back at its number, over the null its delete +// left, with its square, the square's appearance and its name, also when a +// save and reopen dropped all of them; and it saves as itself afterwards. +TEST_F(EPDFPageCaptureEmbedderTest, DeletedLayerPageComesBack) { + for (bool reopen : {false, true}) { + SCOPED_TRACE(reopen ? "reopened between delete and import" + : "same session"); + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFPage_InsertBlankRaw(layer.get(), 3, 300, 300, 20)); + const uint32_t appearance = CreateSquare(layer.get(), 3, 21); + ASSERT_TRUE(appearance); + ASSERT_TRUE(EPDFDoc_SetNamedPage(layer.get(), + GetFPDFWideString(L"Appendix").get(), 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::string appearance_bytes = RawBytesOf(layer.get(), appearance); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), 20); + ASSERT_FALSE(capture.empty()); + EXPECT_TRUE(HoldsObject(capture, 20)); + EXPECT_TRUE(HoldsObject(capture, 21)); + EXPECT_TRUE(HoldsObject(capture, appearance)); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), 20)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kIntro, kBody)); + EXPECT_FALSE(DictOf(layer.get(), 20)); // the null the delete left + + ScopedFPDFDocument target = reopen ? Reopen(layer.get()) : std::move(layer); + ASSERT_TRUE(target); + if (reopen) { + EXPECT_FALSE(DictOf(target.get(), 21)); + EXPECT_TRUE(RawBytesOf(target.get(), appearance).empty()); + } + ASSERT_TRUE(EPDFLayer_BeginTransaction(target.get())); + ASSERT_TRUE(ImportPage(target.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(target.get())); + EXPECT_THAT(PageOrder(target.get()), + ElementsAre(kCover, kIntro, kBody, 20u)); + EXPECT_EQ(4, CountOf(target.get())); + EXPECT_THAT(Annots(target.get(), 3), ElementsAre(21u)); + EXPECT_EQ(appearance, NormalAppearanceOf(target.get(), 21)); + EXPECT_EQ(appearance_bytes, RawBytesOf(target.get(), appearance)); + EXPECT_THAT(NamedPages(target.get()), + ElementsAre(L"Appendix=20", L"Cover=3", L"Intro=4")); + + ScopedFPDFDocument saved = Reopen(target.get()); + ASSERT_TRUE(saved); + EXPECT_THAT(PageOrder(saved.get()), + ElementsAre(kCover, kIntro, kBody, 20u)); + EXPECT_THAT(Annots(saved.get(), 3), ElementsAre(21u)); + EXPECT_EQ(appearance, NormalAppearanceOf(saved.get(), 21)); + } +} + +// A delete leaves the form alone, so the page brings its widget back into +// its /Annots, still the kid of its field; the cover's link still names it. +TEST_F(EPDFPageCaptureEmbedderTest, PageComesBackWithItsFieldsWidget) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), kBody); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kBody)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + RetainPtr field = DictOf(layer.get(), kSignField); + ASSERT_TRUE(field); + EXPECT_THAT(RefsIn(field->GetArrayFor("Kids").Get()), + ElementsAre(kSignWidget)); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(ImportPage(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kIntro, kBody)); + EXPECT_THAT(Annots(layer.get(), 2), ElementsAre(kSignWidget)); + EXPECT_EQ(kSignField, RefOf(layer.get(), kSignWidget, "Parent")); + EXPECT_EQ(kBody, RefOf(layer.get(), kSignWidget, "P")); + RetainPtr link = DictOf(layer.get(), kCoverLink); + ASSERT_TRUE(link); + EXPECT_THAT(RefsIn(link->GetArrayFor("Dest").Get()), ElementsAre(kBody, 0u)); +} + +// Its place: after the page it followed while that is still in the page +// tree, wherever that page went since, else first; never twice. +TEST_F(EPDFPageCaptureEmbedderTest, ImportFindsItsPlace) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector cover = ExportPage(layer.get(), kCover); + const std::vector intro = ExportPage(layer.get(), kIntro); + const std::vector body = ExportPage(layer.get(), kBody); + ASSERT_FALSE(cover.empty()); + ASSERT_FALSE(intro.empty()); + ASSERT_FALSE(body.empty()); + EXPECT_FALSE(ImportPage(layer.get(), intro)); // it's there + + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kCover)); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + ASSERT_TRUE(ImportPage(layer.get(), intro)); // the cover is gone: first + EXPECT_FALSE(ImportPage(layer.get(), intro)); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kIntro, kBody)); + ASSERT_TRUE(ImportPage(layer.get(), cover)); // it was first + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kIntro, kBody)); + + // The body follows the intro, wherever the intro is now. + const int intro_index = 1; + ASSERT_TRUE(FPDF_MovePages(layer.get(), &intro_index, 1, 0)); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kBody)); + ASSERT_TRUE(ImportPage(layer.get(), body)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kIntro, kBody, kCover)); + EXPECT_EQ(3, CountOf(layer.get())); + EXPECT_THAT(NamedPages(layer.get()), ElementsAre(L"Cover=3", L"Intro=4")); +} + +// A key another page took in the meantime stays with that page. +TEST_F(EPDFPageCaptureEmbedderTest, ImportLeavesAKeyAnotherPageTook) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), kIntro); + ASSERT_FALSE(capture.empty()); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + ASSERT_TRUE(EPDFDoc_SetNamedPage(layer.get(), + GetFPDFWideString(L"Intro").get(), kBody)); + ASSERT_TRUE(ImportPage(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kIntro, kBody)); + EXPECT_THAT(NamedPages(layer.get()), ElementsAre(L"Cover=3", L"Intro=5")); +} + +// A capture holds its page, never another: the cover links to the body, +// which the layer turned, and the page tree is its parent. +TEST_F(EPDFPageCaptureEmbedderTest, CaptureHoldsNoOtherPage) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kCover, 1)); + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kBody, 2)); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), kCover); + ASSERT_FALSE(capture.empty()); + EXPECT_TRUE(HoldsObject(capture, kCover)); + EXPECT_FALSE(HoldsObject(capture, kBody)); + EXPECT_FALSE(HoldsObject(capture, kPageTree)); + EXPECT_FALSE(HoldsObject(capture, 1)); // the catalog +} + +// An import that the transaction then drops leaves the page out. +TEST_F(EPDFPageCaptureEmbedderTest, AbortedImportLeavesThePageOut) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + const std::vector capture = ExportPage(layer.get(), kIntro); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(ImportPage(layer.get(), capture)); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kBody)); + EXPECT_EQ(2, CountOf(layer.get())); + EXPECT_THAT(NamedPages(layer.get()), ElementsAre(L"Cover=3")); +} + +TEST_F(EPDFPageCaptureEmbedderTest, CapturesNeedAPageAndALayer) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + EXPECT_TRUE(ExportPage(layer.get(), 0).empty()); + EXPECT_TRUE(ExportPage(layer.get(), 1).empty()); // the catalog + EXPECT_TRUE(ExportPage(layer.get(), kPageTree).empty()); + EXPECT_TRUE(ExportPage(layer.get(), kIntroSquare).empty()); + EXPECT_TRUE(ExportPage(layer.get(), 99).empty()); + // Turned, so the capture holds the page: cutting its end leaves an object + // without `endobj`. + ASSERT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(layer.get(), kIntro, 1)); + const std::vector capture = ExportPage(layer.get(), kIntro); + ASSERT_TRUE(HoldsObject(capture, kIntro)); + const std::vector annots = ExportAnnots(layer.get(), 1, {0}); + ASSERT_FALSE(annots.empty()); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(EPDFDoc_DeletePageByObjectNumber(layer.get(), kIntro)); + EXPECT_TRUE(ExportPage(layer.get(), kIntro).empty()); // out of the tree + EXPECT_FALSE(ImportPage(layer.get(), annots)); // another kind + const std::vector cut(capture.begin(), capture.end() - 4); + EXPECT_FALSE(ImportPage(layer.get(), cut)); + EXPECT_FALSE(ImportPage(layer.get(), {})); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_FALSE(ImportPage(layer.get(), capture)); // outside a transaction + EXPECT_THAT(PageOrder(layer.get()), ElementsAre(kCover, kBody)); + + ScopedFPDFDocument plain( + FPDF_LoadMemDocument(input_.data(), input_.size(), nullptr)); + ASSERT_TRUE(plain); + EXPECT_TRUE(ExportPage(plain.get(), kIntro).empty()); +} diff --git a/fpdfsdk/epdf_named_pages.cpp b/fpdfsdk/epdf_named_pages.cpp index d0f0e177e..77a2184c8 100644 --- a/fpdfsdk/epdf_named_pages.cpp +++ b/fpdfsdk/epdf_named_pages.cpp @@ -20,6 +20,7 @@ #include "core/fxcrt/span.h" #include "core/fxcrt/widestring.h" #include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_named_pages_helpers.h" namespace { @@ -96,6 +97,43 @@ int RemoveAllByKey(CPDF_NameTree* tree, const WideString& key) { } // namespace +namespace epdf { + +std::vector NamedPageKeysOf(CPDF_Document* doc, + uint32_t page_objnum) { + std::vector keys; + auto name_tree = CPDF_NameTree::CreateForReading(doc, "Pages"); + const size_t count = name_tree ? name_tree->GetCount() : 0; + for (size_t i = 0; i < count; ++i) { + WideString key; + RetainPtr value = name_tree->LookupValueAndName(i, &key); + const CPDF_Dictionary* dict = value ? value->AsDictionary() : nullptr; + if (dict && dict->GetObjNum() == page_objnum) { + keys.push_back(std::move(key)); + } + } + return keys; +} + +bool AddNamedPageIfFree(CPDF_Document* doc, + const WideString& key, + uint32_t page_objnum) { + if (key.IsEmpty() || doc->GetPageIndex(page_objnum) < 0) { + return false; + } + if (auto existing = CPDF_NameTree::CreateForReading(doc, "Pages"); + existing && FindIndexByKey(*existing, key).has_value()) { + return false; + } + // Mutable catalog path, as EPDFDoc_SetNamedPage() takes. + auto name_tree = CPDF_NameTree::CreateWithRootNameArray(doc, "Pages"); + return name_tree && + name_tree->AddValueAndName( + pdfium::MakeRetain(doc, page_objnum), key); +} + +} // namespace epdf + FPDF_EXPORT int FPDF_CALLCONV EPDFDoc_GetNamedPageCount(FPDF_DOCUMENT document, int tree) { CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); diff --git a/fpdfsdk/epdf_named_pages_helpers.h b/fpdfsdk/epdf_named_pages_helpers.h new file mode 100644 index 000000000..2cb6997dc --- /dev/null +++ b/fpdfsdk/epdf_named_pages_helpers.h @@ -0,0 +1,32 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#ifndef FPDFSDK_EPDF_NAMED_PAGES_HELPERS_H_ +#define FPDFSDK_EPDF_NAMED_PAGES_HELPERS_H_ + +#include + +#include + +#include "core/fxcrt/widestring.h" + +class CPDF_Document; + +// The catalog's /Names /Pages tree (public/epdf_named_pages.h), for the fork's +// own callers. Callers open a CPDF_DocumentViewScope. +namespace epdf { + +// The keys whose value is the page |page_objnum|, in tree order. +std::vector NamedPageKeysOf(CPDF_Document* doc, + uint32_t page_objnum); + +// Registers |key| -> the page |page_objnum| unless a registration holds |key| +// already, which then keeps it. False when one does, when |key| is empty, or +// when the page isn't in the page tree. +bool AddNamedPageIfFree(CPDF_Document* doc, + const WideString& key, + uint32_t page_objnum); + +} // namespace epdf + +#endif // FPDFSDK_EPDF_NAMED_PAGES_HELPERS_H_ diff --git a/public/epdf_capture.h b/public/epdf_capture.h index 097b8be46..c144a1fbb 100644 --- a/public/epdf_capture.h +++ b/public/epdf_capture.h @@ -131,6 +131,30 @@ EPDFOutline_ImportItemRaw(FPDF_DOCUMENT document, const void* capture, unsigned long size); +// Experimental EmbedPDF Extension API. +// The page |page_objnum| as EPDFDoc_DeletePageByObjectNumber() removes it: +// the page and every object the layer holds a version of that it reaches +// (its contents, resources and annotations, with their appearances; never +// another page or the form's fields), the /Names /Pages keys that name it, +// and its place: the page it followed. NULL when |page_objnum| is not a page +// in the page tree. +FPDF_EXPORT void* FPDF_CALLCONV +EPDFPage_ExportRawToOwnedBuffer(FPDF_DOCUMENT document, + uint32_t page_objnum, + unsigned long* out_size); + +// Experimental EmbedPDF Extension API. +// Puts the page of |capture| back: its objects where the layer holds no +// version, the page itself also over the null a delete leaves for a page the +// layer made; then the page into the page tree right after the page it +// followed while that is still in it, else first; then each of its keys that +// no registration holds now. False outside a transaction, for a malformed +// capture, or when the page is in the page tree already. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_ImportRaw(FPDF_DOCUMENT document, + const void* capture, + unsigned long size); + #ifdef __cplusplus } // extern "C" #endif