diff --git a/core/fpdfapi/edit/cpdf_pageexporter.cpp b/core/fpdfapi/edit/cpdf_pageexporter.cpp index 692efd003..1eb2d9b7e 100644 --- a/core/fpdfapi/edit/cpdf_pageexporter.cpp +++ b/core/fpdfapi/edit/cpdf_pageexporter.cpp @@ -18,14 +18,24 @@ CPDF_PageExporter::CPDF_PageExporter(CPDF_Document* dest_doc, CPDF_PageExporter::~CPDF_PageExporter() = default; bool CPDF_PageExporter::ExportPages(pdfium::span page_indices, - int index) { + int index, + pdfium::span page_objnums) { + // EmbedPDF(chosen-object-numbers): one number per page, or none. + if (!page_objnums.empty() && page_objnums.size() != page_indices.size()) { + return false; + } if (!Init()) { return false; } int curpage = index; - for (uint32_t pageIndex : page_indices) { - RetainPtr dest_page_dict = dest()->CreateNewPage(curpage); + for (size_t i = 0; i < page_indices.size(); ++i) { + const uint32_t pageIndex = page_indices[i]; + // EmbedPDF(chosen-object-numbers): the copy at the caller's number. + const uint32_t objnum = page_objnums.empty() ? 0 : page_objnums[i]; + RetainPtr dest_page_dict = + objnum ? dest()->CreateNewPageAt(curpage, objnum) + : dest()->CreateNewPage(curpage); RetainPtr src_page_dict = src()->GetPageDictionary(pageIndex); if (!src_page_dict || !dest_page_dict) { diff --git a/core/fpdfapi/edit/cpdf_pageexporter.h b/core/fpdfapi/edit/cpdf_pageexporter.h index 7574c0627..f23505ce5 100644 --- a/core/fpdfapi/edit/cpdf_pageexporter.h +++ b/core/fpdfapi/edit/cpdf_pageexporter.h @@ -24,7 +24,14 @@ class CPDF_PageExporter final : public CPDF_PageOrganizer { // For the pages from the source document with `page_indices` as their page // indices, insert them into the destination document at page `index`. // `page_indices` and `index` are 0-based. - bool ExportPages(pdfium::span page_indices, int index); + // + // EmbedPDF(chosen-object-numbers): `page_objnums`, when not empty, has one + // number per page: the copy of `page_indices[i]` is written at + // `page_objnums[i]` (see CPDF_Document::CreateNewPageAt()), or at the next + // free number when it is 0. + bool ExportPages(pdfium::span page_indices, + int index, + pdfium::span page_objnums = {}); }; #endif // CORE_FPDFAPI_EDIT_CPDF_PAGEEXPORTER_H_ diff --git a/docs/embedpdf/fork-ledger.md b/docs/embedpdf/fork-ledger.md index e89de2b69..dd328ff76 100644 --- a/docs/embedpdf/fork-ledger.md +++ b/docs/embedpdf/fork-ledger.md @@ -83,7 +83,8 @@ layer sees. **Where.** `cpdf_document_view_scope.{h,cpp}`, `CPDF_BaseDocument`, read entry points in `fpdfsdk/`. Every function in `epdf_annot.cpp` opens one before it walks the document's objects. **Guarded by.** `cpdf_base_document_unittest.cpp`, `cpdf_layer_document_unittest.cpp`, -`EPDFAnnotEmbedderTest.ReadsResolveThroughTheLayer`. +`EPDFAnnotEmbedderTest.ReadsResolveThroughTheLayer`, +`EPDFPageLabelsEmbedderTest.WritesInALayerTransaction` (`FPDF_GetPageLabel`). **When upstream touches this.** A new entry point that walks objects beyond its own page opens a scope, whether it reads or writes: `FPDFPage_Flatten()` writes one page but reads every page's `/Annots`. Without a scope, a base page with layer `/Annots` crashes the view detector. @@ -141,9 +142,12 @@ promote before any structural write. and the last object number only rises. **Why.** Optimistic edits name objects before the engine applies them. **Where.** `CPDF_LayerDocument::RaiseLastObjectNumber`, `CanAddIndirectObjectAt`, -`AddIndirectObjectAt`, the explicit-number APIs in `fpdf_editpage.cpp`. -**Guarded by.** `epdf_object_number_embeddertest.cpp`. -**When upstream touches this.** The object number allocator: keep the free-number check. +`AddIndirectObjectAt`, the explicit-number APIs in `fpdf_editpage.cpp`, the page numbers +`CPDF_PageExporter::ExportPages` takes for `EPDFDoc_ImportPages`. +**Guarded by.** `epdf_object_number_embeddertest.cpp`, +`EPDFPpoEmbedderTest.CopiesTakeTheChosenNumbers`. +**When upstream touches this.** The object number allocator: keep the free-number check. The page +exporter: keep creating each page at its chosen number. ### layer-open **Rule.** A layer opens over a base file plus a saved delta: the parser reads the base bytes diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 1ede4810c..37cf77bac 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -46,6 +46,8 @@ source_set("fpdfsdk") { # the lookups page captures share. "epdf_named_pages.cpp", "epdf_named_pages_helpers.h", + # EmbedPDF: page label sections (public/epdf_doc.h). + "epdf_page_labels.cpp", # EmbedPDF: digests (EPDF_DigestBuffer) and the hasher signatures share. "epdf_digest.cpp", "epdf_hasher.h", @@ -285,6 +287,8 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_sliced_load_embeddertest.cpp", # EmbedPDF: covers the /Names /Pages and /Templates name-tree APIs. "epdf_named_pages_embeddertest.cpp", + # EmbedPDF: covers page label sections (EPDFDoc_*PageLabelSection*). + "epdf_page_labels_embeddertest.cpp", # EmbedPDF: covers pages copied with their form fields (public/epdf_ppo.h). "epdf_ppo_embeddertest.cpp", # EmbedPDF: a content rewrite keeps what it did not change. diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 3eb663307..eb28c82c2 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -259,6 +259,9 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFDest_CreateXYZByObjectNumber); CHK(EPDFDest_GetPageObjectNumber); CHK(EPDFDest_GetViewNullParams); + CHK(EPDFDoc_GetPageLabelSectionAt); + CHK(EPDFDoc_GetPageLabelSectionCount); + CHK(EPDFDoc_SetPageLabelSections); CHK(EPDFNamedDest_Remove); CHK(EPDFNamedDest_SetDest); CHK(EPDF_GetMetaKeyCount); diff --git a/fpdfsdk/epdf_page_labels.cpp b/fpdfsdk/epdf_page_labels.cpp new file mode 100644 index 000000000..9ad0ed1f0 --- /dev/null +++ b/fpdfsdk/epdf_page_labels.cpp @@ -0,0 +1,265 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +// Page label sections (public/epdf_doc.h): /PageLabels read as sections, and +// written from them. + +#include +#include +#include +#include +#include + +#include "constants/catalog.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/widestring.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/epdf_doc.h" + +namespace { + +// As deep as PDFium reads a name tree. +constexpr int kMaxTreeDepth = 32; + +// /S of each EPDF_PAGE_LABEL_STYLE_*, by value; NONE writes none. +constexpr std::array kStyleNames = {nullptr, "D", "R", + "r", "A", "a"}; + +struct Section { + int first_page = 0; + int style = EPDF_PAGE_LABEL_STYLE_DECIMAL; + int first_number = 1; + WideString prefix; +}; + +struct Entry { + int key = 0; + RetainPtr value; +}; + +int StyleOf(const ByteString& name) { + for (size_t i = 1; i < kStyleNames.size(); ++i) { + if (name == kStyleNames[i]) { + return static_cast(i); + } + } + return EPDF_PAGE_LABEL_STYLE_NONE; +} + +// The entries of the number tree at |node|, in tree order. A node with /Nums +// is a leaf, as PDFium reads it: its /Kids, if any, are not read. +void CollectEntries(const CPDF_Dictionary* node, + int depth, + std::set* visited, + std::vector* entries) { + if (!node || depth > kMaxTreeDepth || !visited->insert(node).second) { + return; + } + RetainPtr nums = node->GetArrayFor("Nums"); + if (nums) { + for (size_t i = 0; i + 1 < nums->size(); i += 2) { + entries->push_back( + {nums->GetIntegerAt(i), nums->GetDirectObjectAt(i + 1)}); + } + return; + } + RetainPtr kids = node->GetArrayFor("Kids"); + if (!kids) { + return; + } + for (size_t i = 0; i < kids->size(); ++i) { + CollectEntries(kids->GetDictAt(i).Get(), depth + 1, visited, entries); + } +} + +// The page label sections of |doc|, as EPDFDoc_GetPageLabelSectionCount() +// describes them. Read through the document's view. +std::vector
ReadSections(CPDF_Document* doc) { + const CPDF_Dictionary* catalog = doc->GetRoot(); + RetainPtr labels = + catalog ? catalog->GetDictFor(pdfium::catalog::kPageLabels) : nullptr; + const int page_count = doc->GetPageCount(); + if (!labels || page_count <= 0) { + return {}; + } + + std::vector entries; + std::set visited; + CollectEntries(labels.Get(), 0, &visited, &entries); + std::erase_if(entries, [page_count](const Entry& entry) { + return entry.key < 0 || entry.key >= page_count; + }); + std::stable_sort( + entries.begin(), entries.end(), + [](const Entry& a, const Entry& b) { return a.key < b.key; }); + + std::vector
sections; + if (entries.empty() || entries.front().key > 0) { + sections.push_back(Section()); + } + for (size_t i = 0; i < entries.size(); ++i) { + // Of two entries with one key, the later one counts. + if (i + 1 < entries.size() && entries[i + 1].key == entries[i].key) { + continue; + } + const Entry& entry = entries[i]; + const CPDF_Dictionary* label = + entry.value ? entry.value->AsDictionary() : nullptr; + Section section; + section.first_page = entry.key; + if (!label) { + section.first_number = entry.key + 1; + sections.push_back(std::move(section)); + continue; + } + section.style = StyleOf(label->GetByteStringFor("S")); + section.first_number = label->GetIntegerFor("St", 1); + if (label->KeyExist("P")) { + section.prefix = label->GetUnicodeTextFor("P"); + } + sections.push_back(std::move(section)); + } + return sections; +} + +} // namespace + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPageLabelSectionCount(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return -1; + } + CPDF_DocumentViewScope document_view(doc); + return pdfium::checked_cast(ReadSections(doc).size()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPageLabelSectionAt(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen, + int* first_page, + int* style, + int* first_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || index < 0) { + return 0; + } + CPDF_DocumentViewScope document_view(doc); + const std::vector
sections = ReadSections(doc); + if (static_cast(index) >= sections.size()) { + return 0; + } + const Section& section = sections[index]; + if (first_page) { + *first_page = section.first_page; + } + if (style) { + *style = section.style; + } + if (first_number) { + *first_number = section.first_number; + } + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + section.prefix, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPageLabelSections(FPDF_DOCUMENT document, + const int* first_pages, + const int* styles, + const int* first_numbers, + const FPDF_WIDESTRING* prefixes, + unsigned long count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || (count > 0 && (!first_pages || !styles || !first_numbers))) { + return false; + } + CPDF_DocumentViewScope document_view(doc); + const CPDF_Dictionary* catalog = doc->GetRoot(); + if (!catalog) { + return false; + } + if (count == 0) { + if (catalog->KeyExist(pdfium::catalog::kPageLabels)) { + RetainPtr writable = doc->GetMutableRoot(); + if (!writable) { + return false; + } + writable->RemoveFor(pdfium::catalog::kPageLabels); + } + return true; + } + + const size_t size = pdfium::checked_cast(count); + // SAFETY: required from caller, |count| of each. + const auto pages = UNSAFE_BUFFERS(pdfium::span(first_pages, size)); + const auto style_values = UNSAFE_BUFFERS(pdfium::span(styles, size)); + const auto numbers = UNSAFE_BUFFERS(pdfium::span(first_numbers, size)); + const pdfium::span prefix_values = + prefixes ? UNSAFE_BUFFERS(pdfium::span(prefixes, size)) + : pdfium::span(); + const int page_count = doc->GetPageCount(); + for (size_t i = 0; i < size; ++i) { + const int previous = i == 0 ? -1 : pages[i - 1]; + if ((i == 0 && pages[i] != 0) || pages[i] <= previous || + pages[i] >= page_count || style_values[i] < 0 || + static_cast(style_values[i]) >= kStyleNames.size()) { + return false; + } + } + + // Its own object keeps its number; on a layer the write lands in the + // layer's copy of it. + RetainPtr reference = + ToReference(catalog->GetObjectFor(pdfium::catalog::kPageLabels)); + RetainPtr labels = + reference ? ToDictionary( + doc->GetMutableIndirectObject(reference->GetRefObjNum())) + : nullptr; + if (labels) { + labels->RemoveFor("Kids"); + labels->RemoveFor("Limits"); + } else { + RetainPtr writable = doc->GetMutableRoot(); + if (!writable) { + return false; + } + labels = writable->SetNewFor(pdfium::catalog::kPageLabels); + } + + auto nums = labels->SetNewFor("Nums"); + for (size_t i = 0; i < size; ++i) { + nums->AppendNew(pages[i]); + auto label = nums->AppendNew(); + if (const char* name = kStyleNames[style_values[i]]) { + label->SetNewFor("S", name); + } + const WideString prefix = + i < prefix_values.size() + ? WideStringFromFPDFWideString(prefix_values[i]) + : WideString(); + if (!prefix.IsEmpty()) { + label->SetNewFor("P", prefix.AsStringView()); + } + if (numbers[i] != 1) { + label->SetNewFor("St", numbers[i]); + } + } + return true; +} diff --git a/fpdfsdk/epdf_page_labels_embeddertest.cpp b/fpdfsdk/epdf_page_labels_embeddertest.cpp new file mode 100644 index 000000000..3722a302e --- /dev/null +++ b/fpdfsdk/epdf_page_labels_embeddertest.cpp @@ -0,0 +1,361 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_doc.h" +#include "public/epdf_layer.h" +#include "public/epdf_save.h" +#include "public/fpdf_doc.h" +#include "public/fpdf_edit.h" +#include "public/fpdf_save.h" +#include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +struct Section { + int first_page = 0; + int style = EPDF_PAGE_LABEL_STYLE_DECIMAL; + int first_number = 1; + std::wstring prefix; + + bool operator==(const Section& other) const { + return first_page == other.first_page && style == other.style && + first_number == other.first_number && prefix == other.prefix; + } +}; + +void PrintTo(const Section& section, std::ostream* os) { + *os << "{" << section.first_page << ", style " << section.style << ", from " + << section.first_number << ", " << testing::PrintToString(section.prefix) + << "}"; +} + +std::vector
SectionsOf(FPDF_DOCUMENT document) { + std::vector
sections; + const int count = EPDFDoc_GetPageLabelSectionCount(document); + for (int i = 0; i < count; ++i) { + Section section; + const unsigned long length = EPDFDoc_GetPageLabelSectionAt( + document, i, nullptr, 0, §ion.first_page, §ion.style, + §ion.first_number); + EXPECT_GE(length, 2u); + std::vector prefix = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, + EPDFDoc_GetPageLabelSectionAt(document, i, prefix.data(), length, + nullptr, nullptr, nullptr)); + section.prefix = GetPlatformWString(prefix.data()); + sections.push_back(section); + } + return sections; +} + +bool WriteSections(FPDF_DOCUMENT document, + const std::vector
& sections) { + std::vector first_pages; + std::vector styles; + std::vector first_numbers; + std::vector owned; + std::vector prefixes; + for (const Section& section : sections) { + first_pages.push_back(section.first_page); + styles.push_back(section.style); + first_numbers.push_back(section.first_number); + owned.push_back(GetFPDFWideString(section.prefix)); + prefixes.push_back(owned.back().get()); + } + return EPDFDoc_SetPageLabelSections(document, first_pages.data(), + styles.data(), first_numbers.data(), + prefixes.data(), sections.size()); +} + +// Each page's label as FPDF_GetPageLabel() gives it, "(none)" for none. +std::vector LabelsOf(FPDF_DOCUMENT document) { + std::vector labels; + for (int i = 0; i < FPDF_GetPageCount(document); ++i) { + const unsigned long length = FPDF_GetPageLabel(document, i, nullptr, 0); + if (length == 0) { + labels.push_back(L"(none)"); + continue; + } + std::vector label = GetFPDFWideStringBuffer(length); + FPDF_GetPageLabel(document, i, label.data(), length); + labels.push_back(GetPlatformWString(label.data())); + } + return labels; +} + +// The object /PageLabels names in the catalog, 0 when it is direct or +// missing. +uint32_t PageLabelsObject(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + CPDF_DocumentViewScope document_view(doc); + RetainPtr reference = + ToReference(doc->GetRoot()->GetObjectFor("PageLabels")); + return reference ? reference->GetRefObjNum() : 0; +} + +ScopedFPDFDocument NewDocument(int page_count) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + for (int i = 0; i < page_count; ++i) { + FPDF_ClosePage(FPDFPage_New(document.get(), i, 612, 792)); + } + return document; +} + +// page_labels.pdf's sections: front matter in roman numerals, a body from 1, +// an appendix as zzA, zzB, and a last page labelled "". +const std::vector
kFileSections = { + {0, EPDF_PAGE_LABEL_STYLE_LOWER_ROMAN, 1, L""}, + {2, EPDF_PAGE_LABEL_STYLE_DECIMAL, 1, L""}, + {4, EPDF_PAGE_LABEL_STYLE_UPPER_LETTERS, 1, L"zz"}, + {6, EPDF_PAGE_LABEL_STYLE_NONE, 1, L""}, +}; +const std::vector kFileLabels = {L"i", L"ii", L"1", L"2", + L"zzA", L"zzB", L""}; + +} // namespace + +class EPDFPageLabelsEmbedderTest : public EmbedderTest { + protected: + ScopedFPDFDocument SaveAndReopen(FPDF_DOCUMENT document) { + ClearString(); + EXPECT_TRUE(FPDF_SaveAsCopy(document, this, 0)); + saved_.push_back(GetString()); + const std::string& bytes = saved_.back(); + return ScopedFPDFDocument(FPDF_LoadMemDocument( + bytes.data(), static_cast(bytes.size()), nullptr)); + } + + // page_labels.pdf as a layer over a frozen base. + ScopedFPDFDocument OpenLayer() { + if (!base_) { + const std::string path = PathService::GetTestFilePath("page_labels.pdf"); + bytes_ = GetFileContents(path.c_str()); + base_ = EPDF_LoadMemBaseDocument( + bytes_.data(), static_cast(bytes_.size()), nullptr); + } + return ScopedFPDFDocument( + EPDFLayer_OpenLayer(base_, nullptr, nullptr, nullptr)); + } + + ScopedFPDFDocument SaveAndReopenArtifact(FPDF_DOCUMENT layer) { + ClearString(); + EXPECT_TRUE(EPDFLayer_SaveLayerArtifact(layer, this, nullptr)); + saved_.push_back(GetString()); + FPDF_FILEACCESS access = {}; + access.m_FileLen = saved_.back().size(); + access.m_GetBlock = GetBlockFromString; + access.m_Param = &saved_.back(); + return ScopedFPDFDocument( + EPDFLayer_OpenLayerArtifact(base_, &access, nullptr, nullptr)); + } + + void TearDown() override { + if (base_) { + EPDF_ReleaseBaseDocument(base_); + } + EmbedderTest::TearDown(); + } + + private: + std::vector saved_; + std::vector bytes_; + EPDF_BASE_DOCUMENT base_ = nullptr; +}; + +TEST_F(EPDFPageLabelsEmbedderTest, ReadsTheSectionsOfAFile) { + ASSERT_TRUE(OpenDocument("page_labels.pdf")); + SetWholeFileAvailable(); + EXPECT_EQ(kFileSections, SectionsOf(document())); + EXPECT_EQ(kFileLabels, LabelsOf(document())); + + int first_page = -1; + EXPECT_EQ(0u, EPDFDoc_GetPageLabelSectionAt(document(), 4, nullptr, 0, + &first_page, nullptr, nullptr)); + EXPECT_EQ(0u, EPDFDoc_GetPageLabelSectionAt(document(), -1, nullptr, 0, + &first_page, nullptr, nullptr)); + EXPECT_EQ(-1, first_page); + EXPECT_EQ(-1, EPDFDoc_GetPageLabelSectionCount(nullptr)); +} + +TEST_F(EPDFPageLabelsEmbedderTest, AFileWithoutPageLabelsHasNoSections) { + ASSERT_TRUE(OpenDocument("about_blank.pdf")); + EXPECT_EQ(0, EPDFDoc_GetPageLabelSectionCount(document())); + EXPECT_EQ(std::vector{L"(none)"}, LabelsOf(document())); +} + +// A tree of two leaves, the way FPDF_GetPageLabel() reads it: the pages before +// the first entry and the entry that isn't a dictionary number by their +// place, and an entry past the last page is left out. +TEST_F(EPDFPageLabelsEmbedderTest, ReadsATreeAsPageLabelsDo) { + ScopedFPDFDocument document = NewDocument(7); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document.get()); + auto labels = doc->GetMutableRoot()->SetNewFor("PageLabels"); + auto kids = labels->SetNewFor("Kids"); + + auto front = doc->NewIndirect(); + auto front_limits = front->SetNewFor("Limits"); + front_limits->AppendNew(2); + front_limits->AppendNew(3); + auto front_nums = front->SetNewFor("Nums"); + front_nums->AppendNew(2); + front_nums->AppendNew()->SetNewFor("S", "r"); + front_nums->AppendNew(3); + front_nums->AppendNew(5); + kids->AppendNew(doc, front->GetObjNum()); + + auto back = doc->NewIndirect(); + auto back_limits = back->SetNewFor("Limits"); + back_limits->AppendNew(5); + back_limits->AppendNew(9); + auto back_nums = back->SetNewFor("Nums"); + back_nums->AppendNew(5); + auto appendix = back_nums->AppendNew(); + appendix->SetNewFor("S", "A"); + appendix->SetNewFor("P", "x-"); + appendix->SetNewFor("St", 3); + back_nums->AppendNew(9); + back_nums->AppendNew()->SetNewFor("S", "D"); + kids->AppendNew(doc, back->GetObjNum()); + + const std::vector
expected = { + {0, EPDF_PAGE_LABEL_STYLE_DECIMAL, 1, L""}, + {2, EPDF_PAGE_LABEL_STYLE_LOWER_ROMAN, 1, L""}, + {3, EPDF_PAGE_LABEL_STYLE_DECIMAL, 4, L""}, + {5, EPDF_PAGE_LABEL_STYLE_UPPER_LETTERS, 3, L"x-"}, + }; + EXPECT_EQ(expected, SectionsOf(document.get())); + const std::vector labels_read = {L"1", L"2", L"i", L"4", + L"5", L"x-C", L"x-D"}; + EXPECT_EQ(labels_read, LabelsOf(document.get())); + + // Written back as they read, the pages keep every label. + ASSERT_TRUE(WriteSections(document.get(), expected)); + EXPECT_EQ(expected, SectionsOf(document.get())); + EXPECT_EQ(labels_read, LabelsOf(document.get())); +} + +TEST_F(EPDFPageLabelsEmbedderTest, WritesSectionsThatLabelThePages) { + ScopedFPDFDocument document = NewDocument(6); + const std::vector
sections = { + {0, EPDF_PAGE_LABEL_STYLE_LOWER_ROMAN, 1, L""}, + {2, EPDF_PAGE_LABEL_STYLE_DECIMAL, 1, L""}, + {4, EPDF_PAGE_LABEL_STYLE_DECIMAL, 7, L"B-\u00e9"}, + }; + ASSERT_TRUE(WriteSections(document.get(), sections)); + EXPECT_EQ(sections, SectionsOf(document.get())); + const std::vector labels = {L"i", L"ii", L"1", + L"2", L"B-\u00e97", L"B-\u00e98"}; + EXPECT_EQ(labels, LabelsOf(document.get())); + + // /S, /P and /St only when they say something. + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document.get()); + RetainPtr nums = + doc->GetRoot()->GetDictFor("PageLabels")->GetArrayFor("Nums"); + ASSERT_EQ(6u, nums->size()); + EXPECT_FALSE(nums->GetDictAt(1)->KeyExist("St")); + EXPECT_FALSE(nums->GetDictAt(1)->KeyExist("P")); + EXPECT_EQ(7, nums->GetDictAt(5)->GetIntegerFor("St")); + + ScopedFPDFDocument reopened = SaveAndReopen(document.get()); + ASSERT_TRUE(reopened); + EXPECT_EQ(sections, SectionsOf(reopened.get())); + EXPECT_EQ(labels, LabelsOf(reopened.get())); +} + +TEST_F(EPDFPageLabelsEmbedderTest, NoSectionsRemovesTheLabels) { + ASSERT_TRUE(OpenDocument("page_labels.pdf")); + SetWholeFileAvailable(); + ASSERT_TRUE(EPDFDoc_SetPageLabelSections(document(), nullptr, nullptr, + nullptr, nullptr, 0)); + EXPECT_EQ(0, EPDFDoc_GetPageLabelSectionCount(document())); + EXPECT_EQ(std::vector(7, L"(none)"), LabelsOf(document())); + // Removing none is fine too. + EXPECT_TRUE(EPDFDoc_SetPageLabelSections(document(), nullptr, nullptr, + nullptr, nullptr, 0)); +} + +TEST_F(EPDFPageLabelsEmbedderTest, RefusesSectionsThatBreakTheRules) { + ASSERT_TRUE(OpenDocument("page_labels.pdf")); + SetWholeFileAvailable(); + const int d = EPDF_PAGE_LABEL_STYLE_DECIMAL; + // Not starting at page 0, not in order, past the last page, a style that + // isn't one. + EXPECT_FALSE(WriteSections(document(), {{1, d, 1, L""}})); + EXPECT_FALSE(WriteSections(document(), + {{0, d, 1, L""}, {3, d, 1, L""}, {3, d, 1, L""}})); + EXPECT_FALSE(WriteSections(document(), + {{0, d, 1, L""}, {2, d, 1, L""}, {1, d, 1, L""}})); + EXPECT_FALSE(WriteSections(document(), {{0, d, 1, L""}, {7, d, 1, L""}})); + EXPECT_FALSE(WriteSections(document(), {{0, 6, 1, L""}})); + EXPECT_FALSE(WriteSections(document(), {{0, -1, 1, L""}})); + const int zero = 0; + EXPECT_FALSE(EPDFDoc_SetPageLabelSections(document(), &zero, nullptr, &zero, + nullptr, 1)); + EXPECT_FALSE(WriteSections(nullptr, {{0, d, 1, L""}})); + // Nothing was written. + EXPECT_EQ(kFileSections, SectionsOf(document())); + EXPECT_EQ(kFileLabels, LabelsOf(document())); + + // Without prefixes at all, and with a first number below 1, as written. + const int first_pages[] = {0, 5}; + const int styles[] = {d, EPDF_PAGE_LABEL_STYLE_NONE}; + const int first_numbers[] = {0, 1}; + ASSERT_TRUE(EPDFDoc_SetPageLabelSections(document(), first_pages, styles, + first_numbers, nullptr, 2)); + const std::vector
written = { + {0, d, 0, L""}, {5, EPDF_PAGE_LABEL_STYLE_NONE, 1, L""}}; + EXPECT_EQ(written, SectionsOf(document())); +} + +// On a layer, inside a transaction: /PageLabels keeps its object, an abort +// leaves the sections as they were, and the layer's artifact keeps a commit. +TEST_F(EPDFPageLabelsEmbedderTest, WritesInALayerTransaction) { + ScopedFPDFDocument layer = OpenLayer(); + ASSERT_TRUE(layer); + const uint32_t labels_object = PageLabelsObject(layer.get()); + ASSERT_NE(0u, labels_object); + EXPECT_EQ(kFileSections, SectionsOf(layer.get())); + + const std::vector
moved = { + {0, EPDF_PAGE_LABEL_STYLE_LOWER_ROMAN, 1, L""}, + {1, EPDF_PAGE_LABEL_STYLE_DECIMAL, 1, L""}, + {5, EPDF_PAGE_LABEL_STYLE_UPPER_LETTERS, 1, L"zz"}, + }; + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(WriteSections(layer.get(), moved)); + EXPECT_EQ(moved, SectionsOf(layer.get())); + ASSERT_TRUE(EPDFLayer_AbortTransaction(layer.get())); + EXPECT_EQ(kFileSections, SectionsOf(layer.get())); + EXPECT_EQ(kFileLabels, LabelsOf(layer.get())); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(layer.get())); + ASSERT_TRUE(WriteSections(layer.get(), moved)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(layer.get())); + EXPECT_EQ(labels_object, PageLabelsObject(layer.get())); + const std::vector labels = {L"i", L"1", L"2", L"3", + L"4", L"zzA", L"zzB"}; + EXPECT_EQ(labels, LabelsOf(layer.get())); + + ScopedFPDFDocument reopened = SaveAndReopenArtifact(layer.get()); + ASSERT_TRUE(reopened); + EXPECT_EQ(moved, SectionsOf(reopened.get())); + EXPECT_EQ(labels, LabelsOf(reopened.get())); +} diff --git a/fpdfsdk/epdf_ppo.cpp b/fpdfsdk/epdf_ppo.cpp index 468f02e6f..abd372910 100644 --- a/fpdfsdk/epdf_ppo.cpp +++ b/fpdfsdk/epdf_ppo.cpp @@ -676,13 +676,32 @@ std::vector PageIndices(const CPDF_Document* doc, return indices; } +// Whether |dest| can write a page at each chosen number of |objnums|: free, +// and no number chosen twice. 0 takes the next free number. +bool CanTakePageNumbers(const CPDF_Document* dest, + pdfium::span objnums) { + std::set chosen; + for (uint32_t objnum : objnums) { + if (objnum == 0) { + continue; + } + if (!chosen.insert(objnum).second || + !dest->CanAddIndirectObjectAt(objnum)) { + return false; + } + } + return true; +} + } // namespace -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, - FPDF_DOCUMENT src_doc, - const int* page_indices, - unsigned long length, - int index) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, + FPDF_DOCUMENT src_doc, + const int* page_indices, + unsigned long length, + int index, + const uint32_t* page_objnums) { CPDF_Document* dest = CPDFDocumentFromFPDFDocument(dest_doc); CPDF_Document* src = CPDFDocumentFromFPDFDocument(src_doc); if (!dest || !src || dest == src || (page_indices && length == 0)) { @@ -698,6 +717,13 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, if (!valid || indices.empty()) { return false; } + // SAFETY: required from caller, one number per copied page. + const pdfium::span objnums = + page_objnums ? UNSAFE_BUFFERS(pdfium::span(page_objnums, indices.size())) + : pdfium::span(); + if (!CanTakePageNumbers(dest, objnums)) { + return false; + } const FieldPlan plan = PlanFields(src, indices); CPDF_PageExporter exporter(dest, src); @@ -707,7 +733,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, exporter.MapObjectNumber(field, dest->NewIndirect()->GetObjNum()); } - if (!exporter.ExportPages(indices, index)) { + if (!exporter.ExportPages(indices, index, objnums)) { return false; } if (CPDF_Document::Extension* extension = dest->GetExtension()) { diff --git a/fpdfsdk/epdf_ppo_embeddertest.cpp b/fpdfsdk/epdf_ppo_embeddertest.cpp index 6d41b2209..a5cafe06f 100644 --- a/fpdfsdk/epdf_ppo_embeddertest.cpp +++ b/fpdfsdk/epdf_ppo_embeddertest.cpp @@ -10,6 +10,7 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" @@ -19,6 +20,7 @@ #include "public/epdf_form.h" #include "public/epdf_layer.h" #include "public/epdf_save.h" +#include "public/epdf_view.h" #include "public/fpdf_edit.h" #include "public/fpdf_save.h" #include "public/fpdfview.h" @@ -113,6 +115,18 @@ RetainPtr AcroFormOf(FPDF_DOCUMENT document) { return doc->GetRoot()->GetDictFor("AcroForm"); } +// The page the first annotation of page |page_index| names in its /P, by +// object number. +uint32_t PageOfFirstAnnot(FPDF_DOCUMENT document, int page_index) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + CPDF_DocumentViewScope document_view(doc); + RetainPtr annots = + doc->GetPageDictionary(page_index)->GetArrayFor("Annots"); + RetainPtr page = + ToReference(annots->GetDictAt(0)->GetObjectFor("P")); + return page ? page->GetRefObjNum() : 0; +} + // A Type 1 font of |doc| named |base_font|. RetainPtr NewFont(CPDF_Document* doc, const ByteString& base_font) { @@ -238,7 +252,8 @@ TEST_F(EPDFPpoEmbedderTest, NamesTheTopLevelFieldsThePagesBring) { TEST_F(EPDFPpoEmbedderTest, BringsTheFieldsOfEveryPage) { ASSERT_TRUE(OpenDocument("form_page_import.pdf")); ScopedFPDFDocument dest(FPDF_CreateNewDocument()); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 0)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 0, nullptr)); EXPECT_EQ(2, FPDF_GetPageCount(dest.get())); EXPECT_EQ(AllPagesFields(), ReadFields(dest.get())); @@ -273,7 +288,8 @@ TEST_F(EPDFPpoEmbedderTest, BringsOnlyWhatIsAboveThePagesWidgets) { ASSERT_TRUE(OpenDocument("form_page_import.pdf")); ScopedFPDFDocument dest(FPDF_CreateNewDocument()); const int second_page = 1; - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), &second_page, 1, 0)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), &second_page, 1, 0, nullptr)); const std::map expected = { {L"person.email", {L"ann@example.com", 1, true}}, {L"plan", {L"yearly", 1, true}}, @@ -299,7 +315,8 @@ TEST_F(EPDFPpoEmbedderTest, PointsCopiedParentsAtTheCopies) { // A page first, so the copies' numbers differ from the source's. FPDF_ClosePage(FPDFPage_New(dest.get(), 0, 300, 300)); const int first_page = 0; - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), &first_page, 1, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), &first_page, 1, 1, nullptr)); CPDF_Document* doc = CPDFDocumentFromFPDFDocument(dest.get()); RetainPtr annots = @@ -329,7 +346,8 @@ TEST_F(EPDFPpoEmbedderTest, AddsToAFormAndRenamesAFontItUses) { GetFPDFWideString(L"plan").get())); ASSERT_TRUE(EPDFForm_SetFieldName(document(), kPerson, GetFPDFWideString(L"person_2").get())); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1, nullptr)); std::map expected = { {L"person", {L"", 0, true}}, @@ -370,7 +388,8 @@ TEST_F(EPDFPpoEmbedderTest, CopiesIntoALayerInATransaction) { LayerDoc dest; ASSERT_TRUE(OpenLayer("hello_world.pdf", &dest)); ASSERT_TRUE(EPDFLayer_BeginTransaction(dest.layer)); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 1, nullptr)); ASSERT_TRUE(EPDFLayer_CommitTransaction(dest.layer)); EXPECT_EQ(3, FPDF_GetPageCount(dest.layer)); EXPECT_EQ(AllPagesFields(), ReadFields(dest.layer)); @@ -390,7 +409,8 @@ TEST_F(EPDFPpoEmbedderTest, CopiesOutOfALayerAsTheLayerReadsIt) { ScopedFPDFDocument dest(FPDF_CreateNewDocument()); const int first_page = 0; - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), src.layer, &first_page, 1, 0)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), src.layer, &first_page, 1, 0, nullptr)); const std::map expected = { {L"applicant.name", {L"Ann", 1, true}}, {L"plan", {L"yearly", 1, true}}, @@ -401,16 +421,86 @@ TEST_F(EPDFPpoEmbedderTest, CopiesOutOfALayerAsTheLayerReadsIt) { EXPECT_EQ(expected, ReadFields(reopened.get())); } +// The copies' pages are written at the numbers the caller chose, which their +// annotations name as their page and a save keeps; 0 takes the next free +// number, above the ones handed out. +TEST_F(EPDFPpoEmbedderTest, CopiesTakeTheChosenNumbers) { + ASSERT_TRUE(OpenDocument("form_page_import.pdf")); + LayerDoc dest; + ASSERT_TRUE(OpenLayer("hello_world.pdf", &dest)); + const uint32_t last = EPDFLayer_GetLastObjectNumber(dest.layer); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(dest.layer, last + 2)); + ASSERT_TRUE(EPDFLayer_BeginTransaction(dest.layer)); + const uint32_t numbers[] = {last + 2, 0}; + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 0, numbers)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(dest.layer)); + EXPECT_EQ(3, FPDF_GetPageCount(dest.layer)); + EXPECT_EQ(last + 2, EPDFDoc_GetPageObjectNumberByIndex(dest.layer, 0)); + EXPECT_GT(EPDFDoc_GetPageObjectNumberByIndex(dest.layer, 1), last + 2); + EXPECT_EQ(last + 2, PageOfFirstAnnot(dest.layer, 0)); + EXPECT_EQ(AllPagesFields(), ReadFields(dest.layer)); + + ScopedFPDFDocument reopened = SaveAndReopen(dest.layer); + ASSERT_TRUE(reopened); + EXPECT_EQ(last + 2, EPDFDoc_GetPageObjectNumberByIndex(reopened.get(), 0)); + EXPECT_EQ(last + 2, PageOfFirstAnnot(reopened.get(), 0)); + EXPECT_EQ(AllPagesFields(), ReadFields(reopened.get())); +} + +// A number that isn't free, or one chosen twice, refuses the import before +// anything is copied. A chosen number needs a layer transaction. +TEST_F(EPDFPpoEmbedderTest, RefusesNumbersItCannotTake) { + ASSERT_TRUE(OpenDocument("form_page_import.pdf")); + LayerDoc dest; + ASSERT_TRUE(OpenLayer("hello_world.pdf", &dest)); + const uint32_t last = EPDFLayer_GetLastObjectNumber(dest.layer); + ASSERT_TRUE(EPDFLayer_RaiseLastObjectNumber(dest.layer, last + 2)); + const uint32_t page = EPDFDoc_GetPageObjectNumberByIndex(dest.layer, 0); + const uint32_t free_number[] = {last + 1, 0}; + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 0, free_number)); + + ASSERT_TRUE(EPDFLayer_BeginTransaction(dest.layer)); + const uint32_t twice[] = {last + 1, last + 1}; + const uint32_t taken[] = {page, 0}; + const uint32_t beyond[] = {0, last + 3}; + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 0, twice)); + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 0, taken)); + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 0, beyond)); + // Nothing was copied, not even a field: no number went past the raise. + EXPECT_EQ(1, FPDF_GetPageCount(dest.layer)); + EXPECT_EQ(last + 2, EPDFLayer_GetLastObjectNumber(dest.layer)); + EXPECT_TRUE( + EPDFDoc_ImportPages(dest.layer, document(), nullptr, 0, 1, free_number)); + ASSERT_TRUE(EPDFLayer_CommitTransaction(dest.layer)); + EXPECT_EQ(last + 1, EPDFDoc_GetPageObjectNumberByIndex(dest.layer, 1)); + + ScopedFPDFDocument plain(FPDF_CreateNewDocument()); + const uint32_t chosen[] = {5, 0}; + EXPECT_FALSE( + EPDFDoc_ImportPages(plain.get(), document(), nullptr, 0, 0, chosen)); + EXPECT_EQ(0, FPDF_GetPageCount(plain.get())); +} + TEST_F(EPDFPpoEmbedderTest, RefusesBadArguments) { ASSERT_TRUE(OpenDocument("form_page_import.pdf")); ScopedFPDFDocument dest(FPDF_CreateNewDocument()); const int out_of_range[] = {0, 2}; - EXPECT_FALSE(EPDFDoc_ImportPages(dest.get(), document(), out_of_range, 2, 0)); + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.get(), document(), out_of_range, 2, 0, nullptr)); const int negative = -1; - EXPECT_FALSE(EPDFDoc_ImportPages(dest.get(), document(), &negative, 1, 0)); - EXPECT_FALSE(EPDFDoc_ImportPages(dest.get(), document(), &negative, 0, 0)); - EXPECT_FALSE(EPDFDoc_ImportPages(document(), document(), nullptr, 0, 0)); - EXPECT_FALSE(EPDFDoc_ImportPages(nullptr, document(), nullptr, 0, 0)); + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.get(), document(), &negative, 1, 0, nullptr)); + EXPECT_FALSE( + EPDFDoc_ImportPages(dest.get(), document(), &negative, 0, 0, nullptr)); + EXPECT_FALSE( + EPDFDoc_ImportPages(document(), document(), nullptr, 0, 0, nullptr)); + EXPECT_FALSE( + EPDFDoc_ImportPages(nullptr, document(), nullptr, 0, 0, nullptr)); // Nothing was copied. EXPECT_EQ(0, FPDF_GetPageCount(dest.get())); EXPECT_FALSE(AcroFormOf(dest.get())); @@ -424,7 +514,8 @@ TEST_F(EPDFPpoEmbedderTest, SharesAFontDefinedTheSameWay) { ScopedFPDFDocument dest = NewTargetForm("Times-Roman"); ASSERT_TRUE(EPDFForm_SetFieldName(document(), kPerson, GetFPDFWideString(L"person_2").get())); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1, nullptr)); RetainPtr fonts = AcroFormOf(dest.get())->GetDictFor("DR")->GetDictFor("Font"); @@ -440,7 +531,8 @@ TEST_F(EPDFPpoEmbedderTest, RenamesAFontWithOtherWidths) { ScopedFPDFDocument dest = NewTargetForm("Times-Roman", 200); ASSERT_TRUE(EPDFForm_SetFieldName(document(), kPerson, GetFPDFWideString(L"person_2").get())); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1, nullptr)); RetainPtr fonts = AcroFormOf(dest.get())->GetDictFor("DR")->GetDictFor("Font"); @@ -460,7 +552,8 @@ TEST_F(EPDFPpoEmbedderTest, RenamesTheFontOfAnAppearanceWithAComment) { ScopedFPDFDocument dest = NewTargetForm("Courier"); ASSERT_TRUE(EPDFForm_SetFieldName(document(), kPerson, GetFPDFWideString(L"person_2").get())); - ASSERT_TRUE(EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1)); + ASSERT_TRUE( + EPDFDoc_ImportPages(dest.get(), document(), nullptr, 0, 1, nullptr)); EXPECT_EQ("/F1_1 % the field's font\n10 Tf 0 g", CopiedEmailAppearance(dest.get())); diff --git a/fpdfsdk/fpdf_doc.cpp b/fpdfsdk/fpdf_doc.cpp index c6ad47ef1..2508a88e0 100644 --- a/fpdfsdk/fpdf_doc.cpp +++ b/fpdfsdk/fpdf_doc.cpp @@ -719,8 +719,11 @@ FPDF_GetPageLabel(FPDF_DOCUMENT document, return 0; } + // EmbedPDF(view-scope): read through the document's view scope, so a + // layer's own /PageLabels counts. + ScopedFPDFDocumentView document_view(document); // CPDF_PageLabel can deal with NULL |document|. - CPDF_PageLabel label(CPDFDocumentFromFPDFDocument(document)); + CPDF_PageLabel label(document_view.Get()); std::optional str = label.GetLabel(page_index); if (!str.has_value()) { return 0; diff --git a/public/epdf_doc.h b/public/epdf_doc.h index 6dff288f3..ab513d40a 100644 --- a/public/epdf_doc.h +++ b/public/epdf_doc.h @@ -12,8 +12,8 @@ extern "C" { #endif // __cplusplus // EmbedPDF's additions to public/fpdf_doc.h: document metadata and its trapped -// status, destinations, named destinations and actions. The outline's -// writers are in public/epdf_outline.h. +// status, destinations, named destinations, actions and page label sections. +// The outline's writers are in public/epdf_outline.h. // The trapped status of the document. See section 14.10.2.4 "Trapped" of the // ISO 32000-1:2008 spec. @@ -391,6 +391,103 @@ EPDFAction_CreateURI(FPDF_DOCUMENT document, FPDF_BYTESTRING uri); FPDF_EXPORT FPDF_ACTION FPDF_CALLCONV EPDFAction_CreateNamed(FPDF_DOCUMENT document, FPDF_BYTESTRING name); +// How a page label section numbers its pages (/S, ISO 32000-1 12.4.2). +// No number: the label is the prefix alone. Also for a style PDFium +// doesn't know, which it labels the same way. +#define EPDF_PAGE_LABEL_STYLE_NONE 0 +// /D: 1, 2, 3. +#define EPDF_PAGE_LABEL_STYLE_DECIMAL 1 +// /R: I, II, III. +#define EPDF_PAGE_LABEL_STYLE_UPPER_ROMAN 2 +// /r: i, ii, iii. +#define EPDF_PAGE_LABEL_STYLE_LOWER_ROMAN 3 +// /A: A to Z, then AA to ZZ. +#define EPDF_PAGE_LABEL_STYLE_UPPER_LETTERS 4 +// /a: a to z, then aa to zz. +#define EPDF_PAGE_LABEL_STYLE_LOWER_LETTERS 5 + +// Experimental EmbedPDF Extension API. +// The number of page label sections of |document| (/PageLabels). A section +// labels the pages from its first page up to the next section's: each page +// gets the section's prefix followed by its number in the section's style, +// counting from the section's first number. The sections are read as +// FPDF_GetPageLabel() labels the pages: +// +// - an entry of the number tree starts a section at its key, the page +// index; keys out of order are taken in order, and of two entries with +// one key the later one counts; +// - an entry whose key isn't a page of the document is left out; +// - pages before the first entry, and an entry that isn't a dictionary, +// are numbered 1, 2, 3 by their place in the document, without a +// prefix. +// +// So the first section always starts at page 0. (For a tree whose keys are +// out of order, or whose /Limits are wrong, FPDF_GetPageLabel() reads +// differently; for a tree as ISO 32000 describes it, the readings are the +// same.) +// +// document - handle to the document. +// +// Returns the number of sections: 0 when the document has no /PageLabels, +// and its pages no labels, or -1 for an invalid |document|. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPageLabelSectionCount(FPDF_DOCUMENT document); + +// Experimental EmbedPDF Extension API. +// Read page label section |index| of |document|. +// +// document - handle to the document. +// index - the section, 0 <= index < +// EPDFDoc_GetPageLabelSectionCount(), in page order. +// buffer - receives the section's prefix (/P) as UTF-16LE with a NUL +// terminator; may be NULL to query the size. +// buflen - size of |buffer| in bytes. +// first_page - receives the index of the section's first page; may be +// NULL. +// style - receives one of EPDF_PAGE_LABEL_STYLE_*; may be NULL. +// first_number - receives the number of the section's first page (/St, +// 1 when the entry gives none, as written even below 1); +// may be NULL. +// +// Returns the number of bytes the prefix needs including the terminator (2 +// for no prefix; it is copied only when |buflen| holds it), or 0 for an +// invalid |document| or |index|. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPageLabelSectionAt(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen, + int* first_page, + int* style, + int* first_number); + +// Experimental EmbedPDF Extension API. +// Replace the page label sections of |document| with |count| sections: the +// i-th starts at page |first_pages[i]|, numbers in style |styles[i]| (one of +// EPDF_PAGE_LABEL_STYLE_*) from |first_numbers[i]|, and has the prefix +// |prefixes[i]| (UTF-16LE, NUL-terminated; NULL or empty for none). NULL +// |prefixes| gives no section a prefix. +// +// The first section starts at page 0, each one after the one before, and +// every one at a page of the document. 0 |count| removes /PageLabels, and +// with it the pages' labels. +// +// /PageLabels is written as one number tree of the sections, in that order: +// into its own object when it is one, which keeps its number, else into the +// catalog. A section's /S, /P and /St are written when they say something: +// /St only when the first number isn't 1. Where the sections go is the +// caller's: pages that move, come or go take no section along by themselves. +// +// Returns true when the sections were written; false, writing nothing, for an +// invalid |document| or sections that break the rules above. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPageLabelSections(FPDF_DOCUMENT document, + const int* first_pages, + const int* styles, + const int* first_numbers, + const FPDF_WIDESTRING* prefixes, + unsigned long count); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/public/epdf_ppo.h b/public/epdf_ppo.h index 2a06a7154..c5283e9bb 100644 --- a/public/epdf_ppo.h +++ b/public/epdf_ppo.h @@ -55,13 +55,24 @@ extern "C" { // As with FPDF_ImportPagesByIndex(), the copies may still read streams from // |src_doc|: keep it open while |dest_doc| is in use. // -// Returns TRUE on success. On FALSE, pages may already have been copied: on a -// layer document, the caller aborts its transaction. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, - FPDF_DOCUMENT src_doc, - const int* page_indices, - unsigned long length, - int index); +// |page_objnums|, when not NULL, holds one object number per copied page, in +// the order they are copied: the copy's page dictionary is written at it, so +// the caller names the pages before the import (EPDFPage_InsertBlankRaw() +// does the same for a blank page). 0 takes the next free number. A number +// other than 0 needs a layer transaction on |dest_doc| and must be free +// (EPDFLayer_RaiseLastObjectNumber()), and no two may be the same. Everything +// else the pages bring gets the next free numbers. +// +// Returns TRUE on success. A number that can't be taken refuses the import +// before anything is copied. On another FALSE, pages may already have been +// copied: on a layer document, the caller aborts its transaction. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ImportPages(FPDF_DOCUMENT dest_doc, + FPDF_DOCUMENT src_doc, + const int* page_indices, + unsigned long length, + int index, + const uint32_t* page_objnums); // Experimental EmbedPDF Extension API. // The top-level form fields EPDFDoc_ImportPages() brings with the pages at