diff --git a/BUILD.gn b/BUILD.gn index 3ba3799196..3b58698a15 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -198,6 +198,15 @@ source_set("pdfium_public_headers_impl") { sources = [ "public/cpp/fpdf_deleters.h", "public/cpp/fpdf_scopers.h", + # EmbedPDF: detached, read-only PDF action models. + "public/epdf_action.h", + # EmbedPDF: public runtime font registration API used by page fallback + # rendering and annotation authoring. + "public/epdf_font.h", + # EmbedPDF: session-free AcroForm model, write transactions, FDF/XFDF. + "public/epdf_form.h", + # EmbedPDF: generic namespace-scoped document/page /PieceInfo metadata. + "public/epdf_pieceinfo.h", "public/epdf_redact.h", "public/fpdf_annot.h", "public/fpdf_attachment.h", diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp index 2a4428e25a..fcba2105a4 100644 --- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp +++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp @@ -379,8 +379,42 @@ void CPDF_PageContentGenerator::GenerateContent() { return; } + // EmbedPDF: did this pass rewrite EVERY existing content stream? Computed + // before the move — an append-only pass (a lone kNoContentStream bucket) + // leaves the existing streams' ops out of `page_objects_`' bookkeeping, so + // resource pruning must not run (see UpdateResourcesDict). + const int32_t existing_streams = CountExistingContentStreams(); + bool regenerated_all_streams = true; + for (int32_t i = 0; i < existing_streams; ++i) { + if (!pdfium::Contains(new_stream_data, i)) { + regenerated_all_streams = false; + break; + } + } + UpdateContentStreams(std::move(new_stream_data)); - UpdateResourcesDict(); + UpdateResourcesDict(regenerated_all_streams); +} + +int32_t CPDF_PageContentGenerator::CountExistingContentStreams() { + if (obj_holder_->GetMutableFormStream()) { + return 1; + } + RetainPtr contents = + obj_holder_->GetDict()->GetObjectFor(pdfium::page_object::kContents); + if (!contents) { + return 0; + } + // Resolve indirection: /Contents is commonly an indirect reference to a + // stream or to an array of streams. + RetainPtr direct = contents->GetDirect(); + if (!direct) { + return 0; + } + if (const CPDF_Array* arr = direct->AsArray()) { + return pdfium::checked_cast(arr->size()); + } + return direct->IsStream() ? 1 : 0; } std::map @@ -540,7 +574,7 @@ void CPDF_PageContentGenerator::UpdateContentStreams( } } -void CPDF_PageContentGenerator::UpdateResourcesDict() { +void CPDF_PageContentGenerator::UpdateResourcesDict(bool regenerated_all_streams) { RetainPtr resources = obj_holder_->GetMutableResources(); if (!resources) { return; @@ -560,6 +594,17 @@ void CPDF_PageContentGenerator::UpdateResourcesDict() { // shared. Checked for that and clone those as well. CloneResourcesDictEntries(document_, resources); + // EmbedPDF: pruning is only sound when THIS pass rewrote every content + // stream. `page_objects_` / `seen_resources` describe the SERIALIZED + // output; an untouched stream's raw ops can reference resources no page + // object records — e.g. a page-level `/C1 cs` prolog whose colorspace is + // inherited by a bare-`scn` Form XObject. Pruning after an append-only + // pass orphans those references (the classic symptom: the whole page + // collapses to grayscale after applying a redaction that touched nothing). + if (!regenerated_all_streams) { + return; + } + ResourcesMap seen_resources; for (auto& page_object : page_objects_) { if (!page_object->IsActive()) { diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.h b/core/fpdfapi/edit/cpdf_pagecontentgenerator.h index 05e2821554..fe2ecbd4b2 100644 --- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.h +++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.h @@ -81,7 +81,14 @@ class CPDF_PageContentGenerator { // Updates the resource dictionary for `obj_holder_` to account for all the // changes. - void UpdateResourcesDict(); + // EmbedPDF: `regenerated_all_streams` reports whether THIS pass rewrote + // every existing content stream. Resource pruning is only sound then — + // see the guard in the implementation. + void UpdateResourcesDict(bool regenerated_all_streams); + + // EmbedPDF: the holder's current content-stream count (form = its single + // stream; page = resolved /Contents array size, or 1 for a lone stream). + int32_t CountExistingContentStreams(); UnownedPtr const obj_holder_; UnownedPtr const document_; diff --git a/core/fpdfapi/font/cpdf_font.cpp b/core/fpdfapi/font/cpdf_font.cpp index c0de42a8de..16923148ea 100644 --- a/core/fpdfapi/font/cpdf_font.cpp +++ b/core/fpdfapi/font/cpdf_font.cpp @@ -30,8 +30,10 @@ #include "core/fxcrt/check.h" #include "core/fxcrt/fx_codepage.h" #include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/stl_util.h" #include "core/fxge/cfx_fontmapper.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_substfont.h" #include "core/fxge/fx_font.h" #include "core/fxge/fx_fontencoding.h" @@ -395,14 +397,38 @@ const char* CPDF_Font::GetAdobeCharName( } uint32_t CPDF_Font::FallbackFontFromCharcode(uint32_t charcode) { + // EmbedPDF: prefer fonts registered through EPDFFont_* before falling back to + // PDFium's hard-coded Arial substitute. This lets broken PDFs with missing + // glyph coverage render through the same runtime fallback registry used by + // annotation authoring, without repairing or mutating the source PDF. + WideString str = UnicodeFromCharCode(charcode); + uint32_t unicode = !str.IsEmpty() ? str[0] : charcode; + for (size_t i = 0; i < font_fallbacks_.size(); ++i) { + if (font_fallbacks_[i]->GetFace() && + font_fallbacks_[i]->GetFace()->GetCharIndex(unicode) != 0) { + return pdfium::checked_cast(i); + } + } + + FX_SAFE_INT32 safe_weight = stem_v_; + safe_weight *= 5; + const int weight = safe_weight.ValueOrDefault(pdfium::kFontWeightNormal); + if (auto font_id = CFX_FontRegistry::FindFallbackFont(unicode, weight, + italic_angle_ != 0)) { + std::unique_ptr fallback_font = + CFX_FontRegistry::CreateFont(*font_id); + if (fallback_font) { + font_fallbacks_.push_back(std::move(fallback_font)); + return pdfium::checked_cast(font_fallbacks_.size() - 1); + } + } + if (font_fallbacks_.empty()) { - font_fallbacks_.push_back(std::make_unique()); - FX_SAFE_INT32 safe_weight = stem_v_; - safe_weight *= 5; - font_fallbacks_[0]->LoadSubst( - "Arial", IsTrueTypeFont(), flags_, - safe_weight.ValueOrDefault(pdfium::kFontWeightNormal), italic_angle_, - FX_CodePage::kDefANSI, IsVertWriting()); + auto fallback_font = std::make_unique(); + fallback_font->LoadSubst("Arial", IsTrueTypeFont(), flags_, weight, + italic_angle_, FX_CodePage::kDefANSI, + IsVertWriting()); + font_fallbacks_.push_back(std::move(fallback_font)); } return 0; } diff --git a/core/fpdfapi/page/cpdf_annotcontext.h b/core/fpdfapi/page/cpdf_annotcontext.h index ebc16c97a1..b28c9948c7 100644 --- a/core/fpdfapi/page/cpdf_annotcontext.h +++ b/core/fpdfapi/page/cpdf_annotcontext.h @@ -35,6 +35,10 @@ class CPDF_AnnotContext { // Never nullptr. IPDF_Page* GetPage() const { return page_; } + // Index at the time the annotation handle was created, or -1 when the + // handle was not created from a page annotation lookup. + int GetAnnotIndex() const { return annot_index_; } + private: void EnsureMutableBackingForAnnotDict(); diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index f258efae31..912e46daba 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -13,6 +13,12 @@ source_set("fpdfdoc") { "cpdf_action.h", "cpdf_annot.cpp", "cpdf_annot.h", + # EmbedPDF: registered FreeText annotation fonts and per-layer subset + # embedding. Keep these fork-owned files when rebasing from upstream PDFium. + "cpdf_annotfontmap.cpp", + "cpdf_annotfontmap.h", + "cpdf_annotfontsubset.cpp", + "cpdf_annotfontsubset.h", "cpdf_annotlist.cpp", "cpdf_annotlist.h", "cpdf_apsettings.cpp", @@ -92,6 +98,9 @@ source_set("fpdfdoc") { "../fpdfapi/render", "../fxcrt", "../fxge", + # EmbedPDF: CPDF_AnnotFontSubset uses HarfBuzz subsetting to embed only the + # glyphs used by each saved annotation/layer. + "../../third_party/harfbuzz-ng", ] visibility = [ "../../*" ] } diff --git a/core/fpdfdoc/cpdf_annot_unittest.cpp b/core/fpdfdoc/cpdf_annot_unittest.cpp index 547c025ff3..9c554cad64 100644 --- a/core/fpdfdoc/cpdf_annot_unittest.cpp +++ b/core/fpdfdoc/cpdf_annot_unittest.cpp @@ -189,5 +189,7 @@ TEST_F(CPDFAnnotWithPageModuleTest, EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); EXPECT_EQ(CFX_FloatRect(0, 0, 10, 10), annot_dict->GetRectFor(pdfium::annotation::kRect)); - EXPECT_EQ(CFX_FloatRect(-2, -2, 12, 12), annot.GetRect()); + // The drawing rect is the minimal union of the authored /Rect and the + // stroked ink bounds: points 1..9 inflated by half the width (2). + EXPECT_EQ(CFX_FloatRect(-1, -1, 11, 11), annot.GetRect()); } diff --git a/core/fpdfdoc/cpdf_annotfontmap.cpp b/core/fpdfdoc/cpdf_annotfontmap.cpp new file mode 100644 index 0000000000..4a9c88b270 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontmap.cpp @@ -0,0 +1,308 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: annotation font map for registered runtime fonts. This lets +// FreeText appearance generation pick per-glyph fallback fonts and later embed +// only the glyph subset used by the annotation/layer. + +#include "core/fpdfdoc/cpdf_annotfontmap.h" + +#include +#include +#include + +#include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfdoc/cpdf_annotfontsubset.h" +#include "core/fpdfdoc/cpdf_interactiveform.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/fx_codepage.h" +#include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxge/cfx_font.h" + +namespace { + +constexpr char kRegisteredFontResourcePrefix[] = "ERegF"; + +ByteString ResourceKeyForRegisteredFont(CFX_FontRegistry::FontId font_id) { + return ByteString::Format("%s%u", kRegisteredFontResourcePrefix, font_id); +} + +bool PDFontSupportsUnicode(const RetainPtr& font, uint16_t word) { + if (!font) { + return false; + } + + uint32_t charcode = font->CharCodeFromUnicode(word); + if (charcode == CPDF_Font::kInvalidCharCode || (charcode == 0 && word != 0)) { + return false; + } + + bool vert_glyph = false; + return font->GlyphFromCharCode(charcode, &vert_glyph) > 0; +} + +} // namespace + +CPDF_AnnotFontMap::CPDF_AnnotFontMap(CPDF_Document* doc, + RetainPtr default_font, + const ByteString& default_font_alias, + bool allow_registered_fallbacks) + : doc_(doc), allow_registered_fallbacks_(allow_registered_fallbacks) { + FontEntry entry; + entry.font = std::move(default_font); + entry.alias = default_font_alias; + RetainPtr default_font_dict = + entry.font ? entry.font->GetFontDict() : nullptr; + if (auto font_id = + CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + default_font_dict.Get())) { + RetainPtr registered_font = CreateRegisteredLayoutFont(*font_id); + if (registered_font) { + entry.font = std::move(registered_font); + entry.registered_font_id = *font_id; + } + } + fonts_.push_back(std::move(entry)); +} + +CPDF_AnnotFontMap::~CPDF_AnnotFontMap() { + DeleteTemporaryLayoutObjects(); +} + +// static +bool CPDF_AnnotFontMap::EnsureRegisteredFontMarkerInDocument( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + ByteString* resource_key) { + if (!doc || !resource_key || !CFX_FontRegistry::IsValidFont(font_id)) { + return false; + } + + RetainPtr root_dict = doc->GetMutableRoot(); + if (!root_dict) { + return false; + } + + RetainPtr acroform_dict = + root_dict->GetMutableDictFor("AcroForm"); + if (!acroform_dict) { + acroform_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); + CHECK(acroform_dict); + } + + RetainPtr font_res = + acroform_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor("Font"); + + ByteString key = ResourceKeyForRegisteredFont(font_id); + if (RetainPtr existing_font_dict = + font_res->GetMutableDictFor(key.AsStringView())) { + // EmbedPDF: registered-font identity is stored in the marker dictionary, + // not inferred from the resource alias. This survives alias collisions, + // suffixes, and resource renaming during save/merge. + if (CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + existing_font_dict.Get()) == font_id) { + *resource_key = key; + return true; + } + } + + const ByteString base_key = key; + for (int suffix = 1; font_res->KeyExist(key.AsStringView()); ++suffix) { + key = ByteString::Format("%s_%d", base_key.c_str(), suffix); + } + + RetainPtr marker_font_dict = + CPDF_AnnotFontSubset::CreateMarkerFontDict(doc, font_id); + if (!marker_font_dict) { + return false; + } + + font_res->SetNewFor(key, doc, marker_font_dict->GetObjNum()); + *resource_key = key; + return true; +} + +RetainPtr CPDF_AnnotFontMap::CreateFontResourceDict() { + if (!doc_) { + return nullptr; + } + + auto resource_font_dict = doc_->New(); + for (FontEntry& entry : fonts_) { + if (!entry.font || entry.alias.IsEmpty()) { + continue; + } + + if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { + RetainPtr subset_font_dict = + CPDF_AnnotFontSubset::CreateSubsetFontDict( + doc_, entry.registered_font_id, entry.glyph_to_unicode); + if (subset_font_dict) { + resource_font_dict->SetNewFor( + entry.alias, doc_, subset_font_dict->GetObjNum()); + } + continue; + } + + RetainPtr font_dict = entry.font->GetFontDict(); + if (!font_dict) { + continue; + } + + const uint32_t font_obj_num = font_dict->GetObjNum(); + if (font_obj_num != 0) { + resource_font_dict->SetNewFor(entry.alias, doc_, + font_obj_num); + } else { + resource_font_dict->SetFor(entry.alias, font_dict->Clone()); + } + } + return resource_font_dict; +} + +RetainPtr CPDF_AnnotFontMap::GetPDFFont(int32_t font_index) { + return fxcrt::IndexInBounds(fonts_, font_index) ? fonts_[font_index].font + : nullptr; +} + +ByteString CPDF_AnnotFontMap::GetPDFFontAlias(int32_t font_index) { + return fxcrt::IndexInBounds(fonts_, font_index) ? fonts_[font_index].alias + : ByteString(); +} + +int32_t CPDF_AnnotFontMap::GetWordFontIndex(uint16_t word, + FX_Charset charset, + int32_t font_index) { + if (SupportsWord(font_index, word)) { + return font_index; + } + if (SupportsWord(0, word)) { + return 0; + } + + for (size_t i = 1; i < fonts_.size(); ++i) { + if (SupportsWord(pdfium::checked_cast(i), word)) { + return pdfium::checked_cast(i); + } + } + + if (!allow_registered_fallbacks_ || !fonts_.front().font) { + return -1; + } + + const int weight = + fonts_.front().font->GetFontWeight().value_or(pdfium::kFontWeightNormal); + const bool italic = fonts_.front().font->GetItalicAngle() != 0; + std::optional font_id = + CFX_FontRegistry::FindFallbackFont(word, weight, italic); + if (!font_id.has_value()) { + return -1; + } + + int32_t existing_font_index = FindExistingRegisteredFont(*font_id); + if (existing_font_index >= 0) { + return existing_font_index; + } + + return AddRegisteredFallbackFont(*font_id); +} + +int32_t CPDF_AnnotFontMap::CharCodeFromUnicode(int32_t font_index, + uint16_t word) { + RetainPtr font = GetPDFFont(font_index); + if (!font) { + return -1; + } + + uint32_t charcode = font->CharCodeFromUnicode(word); + if (charcode == CPDF_Font::kInvalidCharCode || (charcode == 0 && word != 0)) { + return -1; + } + if (fxcrt::IndexInBounds(fonts_, font_index)) { + FontEntry& entry = fonts_[font_index]; + if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { + entry.glyph_to_unicode.emplace(charcode, word); + } + } + return pdfium::checked_cast(charcode); +} + +FX_Charset CPDF_AnnotFontMap::CharSetFromUnicode(uint16_t word, + FX_Charset old_charset) { + if (word < 0x7F) { + return FX_Charset::kANSI; + } + if (old_charset != FX_Charset::kDefault) { + return old_charset; + } + return CFX_Font::GetCharSetFromUnicode(word); +} + +bool CPDF_AnnotFontMap::SupportsWord(int32_t font_index, uint16_t word) const { + if (!fxcrt::IndexInBounds(fonts_, font_index)) { + return false; + } + const FontEntry& entry = fonts_[font_index]; + if (!entry.font) { + return false; + } + if (entry.registered_font_id != CFX_FontRegistry::kInvalidFontId) { + return CFX_FontRegistry::SupportsUnicode(entry.registered_font_id, word); + } + return PDFontSupportsUnicode(entry.font, word); +} + +void CPDF_AnnotFontMap::DeleteTemporaryLayoutObjects() { + fonts_.clear(); + if (!doc_) { + temporary_layout_object_numbers_.clear(); + return; + } + + for (uint32_t obj_num : temporary_layout_object_numbers_) { + doc_->DeleteIndirectObject(obj_num); + } + temporary_layout_object_numbers_.clear(); +} + +RetainPtr CPDF_AnnotFontMap::CreateRegisteredLayoutFont( + CFX_FontRegistry::FontId font_id) { + CPDF_AnnotFontSubset::LayoutFont layout_font = + CPDF_AnnotFontSubset::CreateLayoutFont(doc_, font_id); + temporary_layout_object_numbers_.insert( + temporary_layout_object_numbers_.end(), + layout_font.temporary_object_numbers.begin(), + layout_font.temporary_object_numbers.end()); + return std::move(layout_font.font); +} + +int32_t CPDF_AnnotFontMap::FindExistingRegisteredFont( + CFX_FontRegistry::FontId font_id) const { + for (size_t i = 0; i < fonts_.size(); ++i) { + if (fonts_[i].registered_font_id == font_id) { + return pdfium::checked_cast(i); + } + } + return -1; +} + +int32_t CPDF_AnnotFontMap::AddRegisteredFallbackFont( + CFX_FontRegistry::FontId font_id) { + RetainPtr font = CreateRegisteredLayoutFont(font_id); + if (!font) { + return -1; + } + + FontEntry entry; + entry.font = std::move(font); + entry.alias = ResourceKeyForRegisteredFont(font_id); + entry.registered_font_id = font_id; + fonts_.push_back(std::move(entry)); + return pdfium::checked_cast(fonts_.size() - 1); +} diff --git a/core/fpdfdoc/cpdf_annotfontmap.h b/core/fpdfdoc/cpdf_annotfontmap.h new file mode 100644 index 0000000000..19193dea09 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontmap.h @@ -0,0 +1,72 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: annotation font map for registered runtime fonts. This file is +// fork-owned and supports FreeText fallback font routing/subsetting. + +#ifndef CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ +#define CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ + +#include + +#include +#include + +#include "core/fpdfdoc/ipvt_fontmap.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; +class CPDF_Font; + +class CPDF_AnnotFontMap final : public IPVT_FontMap { + public: + CPDF_AnnotFontMap(CPDF_Document* doc, + RetainPtr default_font, + const ByteString& default_font_alias, + bool allow_registered_fallbacks); + ~CPDF_AnnotFontMap() override; + + static bool EnsureRegisteredFontMarkerInDocument( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + ByteString* resource_key); + + RetainPtr CreateFontResourceDict(); + + // IPVT_FontMap: + RetainPtr GetPDFFont(int32_t font_index) override; + ByteString GetPDFFontAlias(int32_t font_index) override; + int32_t GetWordFontIndex(uint16_t word, + FX_Charset charset, + int32_t font_index) override; + int32_t CharCodeFromUnicode(int32_t font_index, uint16_t word) override; + FX_Charset CharSetFromUnicode(uint16_t word, FX_Charset old_charset) override; + + private: + struct FontEntry { + RetainPtr font; + ByteString alias; + CFX_FontRegistry::FontId registered_font_id = + CFX_FontRegistry::kInvalidFontId; + std::map glyph_to_unicode; + }; + + bool SupportsWord(int32_t font_index, uint16_t word) const; + void DeleteTemporaryLayoutObjects(); + RetainPtr CreateRegisteredLayoutFont( + CFX_FontRegistry::FontId font_id); + int32_t FindExistingRegisteredFont(CFX_FontRegistry::FontId font_id) const; + int32_t AddRegisteredFallbackFont(CFX_FontRegistry::FontId font_id); + + UnownedPtr const doc_; + const bool allow_registered_fallbacks_; + std::vector fonts_; + std::vector temporary_layout_object_numbers_; +}; + +#endif // CORE_FPDFDOC_CPDF_ANNOTFONTMAP_H_ diff --git a/core/fpdfdoc/cpdf_annotfontsubset.cpp b/core/fpdfdoc/cpdf_annotfontsubset.cpp new file mode 100644 index 0000000000..bec45b0be7 --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontsubset.cpp @@ -0,0 +1,708 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: builds PDF font dictionaries for registered annotation fonts, +// including per-annotation/layer subsets so large fallback fonts are not fully +// embedded into saved PDFs. + +#include "core/fpdfdoc/cpdf_annotfontsubset.h" + +#include +#include +#include +#include +#include + +#include "constants/font_encodings.h" +#include "core/fpdfapi/font/cpdf_font.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_name.h" +#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/fxcrt/check.h" +#include "core/fxcrt/check_op.h" +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/fx_extension.h" +#include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/fx_string.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_font.h" +#include "hb-subset.h" // nogncheck + +namespace { + +constexpr char kRegisteredFontIdKey[] = "EmbedPDFRegisteredFontId"; +constexpr uint32_t kMaxBfCharBfRangeEntries = 100; +constexpr uint32_t kMaxPdfCid = 0xffff; + +enum class ObjectStorage { + kDirect, + kIndirect, +}; + +ByteString NormalizeBaseFontName(ByteString name) { + name.Remove(' '); + return name.IsEmpty() ? ByteString(CFX_Font::kUntitledFontName) : name; +} + +ByteString BaseFontNameForRegisteredFont(CFX_FontRegistry::FontId font_id, + const CFX_Font* font) { + ByteString name = CFX_FontRegistry::GetBaseFontName(font_id); + if (name.IsEmpty() && font) { + name = font->GetBaseFontName(); + } + return NormalizeBaseFontName(std::move(name)); +} + +RetainPtr NewDictionary(CPDF_Document* doc, + ObjectStorage storage) { + return storage == ObjectStorage::kIndirect + ? doc->NewIndirect() + : pdfium::MakeRetain(); +} + +RetainPtr NewArray(CPDF_Document* doc, ObjectStorage storage) { + return storage == ObjectStorage::kIndirect ? doc->NewIndirect() + : pdfium::MakeRetain(); +} + +RetainPtr NewStream(CPDF_Document* doc, + pdfium::span data, + ObjectStorage storage) { + return storage == ObjectStorage::kIndirect + ? doc->NewIndirect(data) + : pdfium::MakeRetain(data); +} + +template +void SetReferenceOrDirect(CPDF_Dictionary* dict, + const ByteString& key, + CPDF_Document* doc, + RetainPtr object) { + if (!object) { + return; + } + + const uint32_t obj_num = object->GetObjNum(); + if (obj_num != 0) { + dict->SetNewFor(key, doc, obj_num); + return; + } + + dict->SetFor(key, RetainPtr(std::move(object))); +} + +template +void AppendReferenceOrDirect(CPDF_Array* array, + CPDF_Document* doc, + RetainPtr object) { + if (!object) { + return; + } + + const uint32_t obj_num = object->GetObjNum(); + if (obj_num != 0) { + array->AppendNew(doc, obj_num); + return; + } + + array->Append(RetainPtr(std::move(object))); +} + +ByteString MakeSubsetBaseFontName( + const ByteString& base_font_name, + const CPDF_AnnotFontSubset::GlyphUnicodeMap& glyph_to_unicode) { + // EmbedPDF: a deterministic six-letter PDF subset tag is enough to keep + // subsets distinguishable for readers/debugging. A theoretical hash collision + // is harmless because each AP resource dictionary still points at its own + // embedded subset font object. + uint32_t hash = 2166136261u; + auto mix = [&hash](uint32_t value) { + for (int i = 0; i < 4; ++i) { + hash ^= (value >> (i * 8)) & 0xff; + hash *= 16777619u; + } + }; + + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + mix(glyph_id); + mix(unicode); + } + + char prefix[7] = {}; + for (int i = 0; i < 6; ++i) { + prefix[i] = static_cast('A' + (hash % 26)); + hash = hash / 26 + 1; + } + return ByteString(prefix) + "+" + base_font_name; +} + +RetainPtr CreateCompositeFontDict(CPDF_Document* doc, + const ByteString& name, + ObjectStorage storage) { + auto font_dict = NewDictionary(doc, storage); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "Type0"); + font_dict->SetNewFor("Encoding", "Identity-H"); + font_dict->SetNewFor("BaseFont", name); + return font_dict; +} + +RetainPtr CreateCidFontDict(CPDF_Document* doc, + const ByteString& name, + ObjectStorage storage) { + auto cid_font_dict = NewDictionary(doc, storage); + cid_font_dict->SetNewFor("Type", "Font"); + cid_font_dict->SetNewFor("Subtype", "CIDFontType2"); + cid_font_dict->SetNewFor("BaseFont", name); + cid_font_dict->SetNewFor("CIDToGIDMap", "Identity"); + + auto cid_system_info_dict = pdfium::MakeRetain(); + cid_system_info_dict->SetNewFor("Registry", "Adobe"); + cid_system_info_dict->SetNewFor("Ordering", "Identity"); + cid_system_info_dict->SetNewFor("Supplement", 0); + cid_font_dict->SetFor("CIDSystemInfo", std::move(cid_system_info_dict)); + return cid_font_dict; +} + +RetainPtr LoadFontDesc( + CPDF_Document* doc, + const ByteString& font_name, + CFX_Font* font, + pdfium::span font_data, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + auto font_descriptor_dict = NewDictionary(doc, storage); + font_descriptor_dict->SetNewFor("Type", "FontDescriptor"); + font_descriptor_dict->SetNewFor("FontName", font_name); + + int flags = pdfium::kFontStyleNonSymbolic; + if (font->IsFixedWidth()) { + flags |= pdfium::kFontStyleFixedPitch; + } + if (font_name.Contains("Serif")) { + flags |= pdfium::kFontStyleSerif; + } + if (font->IsItalic()) { + flags |= pdfium::kFontStyleItalic; + } + if (font->IsBold()) { + flags |= pdfium::kFontStyleForceBold; + } + font_descriptor_dict->SetNewFor("Flags", flags); + + FX_RECT bbox = font->GetBBox().value_or(FX_RECT()); + font_descriptor_dict->SetRectFor("FontBBox", CFX_FloatRect(bbox)); + font_descriptor_dict->SetNewFor("ItalicAngle", + font->IsItalic() ? -12 : 0); + font_descriptor_dict->SetNewFor("Ascent", font->GetAscent()); + font_descriptor_dict->SetNewFor("Descent", font->GetDescent()); + font_descriptor_dict->SetNewFor("CapHeight", font->GetAscent()); + font_descriptor_dict->SetNewFor("StemV", + font->IsBold() ? 120 : 70); + + RetainPtr stream = + storage == ObjectStorage::kDirect + ? doc->NewIndirect(font_data) + : NewStream(doc, font_data, ObjectStorage::kIndirect); + stream->GetMutableDict()->SetNewFor( + "Length1", pdfium::checked_cast(font_data.size())); + if (temporary_object_numbers && storage == ObjectStorage::kDirect) { + temporary_object_numbers->push_back(stream->GetObjNum()); + } + font_descriptor_dict->SetNewFor("FontFile2", doc, + stream->GetObjNum()); + return font_descriptor_dict; +} + +RetainPtr CreateWidthsArray( + CPDF_Document* doc, + const std::map& widths, + ObjectStorage storage) { + auto widths_array = NewArray(doc, storage); + for (auto it = widths.begin(); it != widths.end(); ++it) { + auto next_it = std::next(it); + + if (next_it != widths.end() && next_it->first == it->first + 1 && + next_it->second == it->second) { + widths_array->AppendNew(static_cast(it->first)); + + while (next_it != widths.end() && next_it->first == it->first + 1 && + next_it->second == it->second) { + it = next_it; + next_it = std::next(it); + } + widths_array->AppendNew(static_cast(it->first)); + widths_array->AppendNew(static_cast(it->second)); + continue; + } + + widths_array->AppendNew(static_cast(it->first)); + auto current_width_array = pdfium::MakeRetain(); + current_width_array->AppendNew(static_cast(it->second)); + + while (next_it != widths.end() && next_it->first == it->first + 1) { + it = next_it; + next_it = std::next(it); + current_width_array->AppendNew(static_cast(it->second)); + } + widths_array->Append(std::move(current_width_array)); + } + return widths_array; +} + +const char kToUnicodeStart[] = + "/CIDInit /ProcSet findresource begin\n" + "12 dict begin\n" + "begincmap\n" + "/CIDSystemInfo\n" + "<> def\n" + "/CMapName /Adobe-Identity-H def\n" + "/CMapType 2 def\n" + "1 begincodespacerange\n" + "<0000> \n" + "endcodespacerange\n"; + +const char kToUnicodeEnd[] = + "endcmap\n" + "CMapName currentdict /CMap defineresource pop\n" + "end\n" + "end\n"; + +void AddCharcode(fxcrt::ostringstream& buffer, uint32_t number) { + CHECK_LE(number, kMaxPdfCid); + buffer << "<"; + char ans[4]; + FXSYS_IntToFourHexChars(number, ans); + for (char c : ans) { + buffer << c; + } + buffer << ">"; +} + +void AddUnicode(fxcrt::ostringstream& buffer, uint32_t unicode) { + if (pdfium::IsHighSurrogate(unicode) || pdfium::IsLowSurrogate(unicode)) { + unicode = 0; + } + + char unicode_buf[8]; + pdfium::span unicode_span = FXSYS_ToUTF16BE(unicode, unicode_buf); + CHECK(!unicode_span.empty()); + buffer << "<"; + for (char c : unicode_span) { + buffer << c; + } + buffer << ">"; +} + +RetainPtr LoadUnicode( + CPDF_Document* doc, + const std::multimap& to_unicode, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + std::map char_to_unicode_map; + std::map, std::vector> + char_range_to_unicodes_map; + std::map, uint32_t> + char_range_to_consecutive_unicodes_map; + + for (auto it = to_unicode.begin(); it != to_unicode.end(); ++it) { + uint32_t first_charcode = it->first; + uint32_t first_unicode = it->second; + { + auto next_it = std::next(it); + if (next_it == to_unicode.end() || first_charcode + 1 != next_it->first) { + char_to_unicode_map[first_charcode] = first_unicode; + continue; + } + } + + ++it; + uint32_t current_charcode = it->first; + uint32_t current_unicode = it->second; + if (current_charcode % 256 == 0) { + char_to_unicode_map[first_charcode] = first_unicode; + char_to_unicode_map[current_charcode] = current_unicode; + continue; + } + + const size_t max_extra = 255 - (current_charcode % 256); + auto next_it = std::next(it); + if (first_unicode + 1 != current_unicode) { + std::vector unicodes = {first_unicode, current_unicode}; + for (size_t i = 0; i < max_extra; ++i) { + if (next_it == to_unicode.end() || + current_charcode + 1 != next_it->first) { + break; + } + ++it; + ++current_charcode; + unicodes.push_back(it->second); + next_it = std::next(it); + } + CHECK_EQ(it->first - first_charcode + 1, unicodes.size()); + char_range_to_unicodes_map[std::make_pair(first_charcode, it->first)] = + std::move(unicodes); + continue; + } + + for (size_t i = 0; i < max_extra; ++i) { + if (next_it == to_unicode.end() || + current_charcode + 1 != next_it->first || + current_unicode + 1 != next_it->second) { + break; + } + ++it; + ++current_charcode; + ++current_unicode; + next_it = std::next(it); + } + char_range_to_consecutive_unicodes_map[std::make_pair( + first_charcode, current_charcode)] = first_unicode; + } + + fxcrt::ostringstream buffer; + buffer << kToUnicodeStart; + + uint32_t to_process = + pdfium::checked_cast(char_to_unicode_map.size()); + auto char_it = char_to_unicode_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfchar\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(char_it != char_to_unicode_map.end()); + AddCharcode(buffer, char_it->first); + buffer << " "; + AddUnicode(buffer, char_it->second); + buffer << "\n"; + ++char_it; + } + buffer << "endbfchar\n"; + to_process -= count; + } + + to_process = + pdfium::checked_cast(char_range_to_unicodes_map.size()); + auto range_it = char_range_to_unicodes_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfrange\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(range_it != char_range_to_unicodes_map.end()); + AddCharcode(buffer, range_it->first.first); + buffer << " "; + AddCharcode(buffer, range_it->first.second); + buffer << " ["; + auto unicodes = pdfium::span(range_it->second); + AddUnicode(buffer, unicodes[0]); + for (uint32_t code : unicodes.subspan(1u)) { + buffer << " "; + AddUnicode(buffer, code); + } + buffer << "]\n"; + ++range_it; + } + buffer << "endbfrange\n"; + to_process -= count; + } + + to_process = pdfium::checked_cast( + char_range_to_consecutive_unicodes_map.size()); + auto consecutive_it = char_range_to_consecutive_unicodes_map.begin(); + while (to_process) { + const uint32_t count = std::min(to_process, kMaxBfCharBfRangeEntries); + buffer << count << " beginbfrange\n"; + for (uint32_t i = 0; i < count; ++i) { + CHECK(consecutive_it != char_range_to_consecutive_unicodes_map.end()); + AddCharcode(buffer, consecutive_it->first.first); + buffer << " "; + AddCharcode(buffer, consecutive_it->first.second); + buffer << " "; + AddUnicode(buffer, consecutive_it->second); + buffer << "\n"; + ++consecutive_it; + } + buffer << "endbfrange\n"; + to_process -= count; + } + + buffer << kToUnicodeEnd; + RetainPtr stream = doc->NewIndirect(&buffer); + if (temporary_object_numbers && storage == ObjectStorage::kDirect) { + temporary_object_numbers->push_back(stream->GetObjNum()); + } + return stream; +} + +void CreateDescendantFontsArray(CPDF_Document* doc, + CPDF_Dictionary* font_dict, + RetainPtr cid_font_dict) { + auto descendant_fonts_array = + font_dict->SetNewFor("DescendantFonts"); + AppendReferenceOrDirect(descendant_fonts_array.Get(), doc, + std::move(cid_font_dict)); +} + +DataVector SubsetFontDataRetainGids( + pdfium::span font_data, + const CPDF_AnnotFontSubset::GlyphUnicodeMap& glyph_to_unicode) { + if (font_data.empty() || glyph_to_unicode.empty()) { + return DataVector(); + } + + hb_blob_t* source_blob = + hb_blob_create(reinterpret_cast(font_data.data()), + pdfium::checked_cast(font_data.size()), + HB_MEMORY_MODE_READONLY, nullptr, nullptr); + if (!source_blob) { + return DataVector(); + } + + hb_face_t* source_face = hb_face_create(source_blob, 0); + hb_blob_destroy(source_blob); + if (!source_face) { + return DataVector(); + } + + hb_subset_input_t* input = hb_subset_input_create_or_fail(); + if (!input) { + hb_face_destroy(source_face); + return DataVector(); + } + + hb_set_t* glyph_set = hb_subset_input_glyph_set(input); + hb_set_add(glyph_set, 0); + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + if (glyph_id <= kMaxPdfCid) { + hb_set_add(glyph_set, glyph_id); + } + } + + hb_subset_input_set_flags( + input, HB_SUBSET_FLAGS_RETAIN_GIDS | HB_SUBSET_FLAGS_NO_HINTING); + + hb_face_t* subset_face = hb_subset_or_fail(source_face, input); + hb_subset_input_destroy(input); + hb_face_destroy(source_face); + if (!subset_face) { + return DataVector(); + } + + hb_blob_t* subset_blob = hb_face_reference_blob(subset_face); + hb_face_destroy(subset_face); + if (!subset_blob) { + return DataVector(); + } + + unsigned int subset_length = 0; + const char* subset_data = hb_blob_get_data(subset_blob, &subset_length); + DataVector result; + if (subset_data && subset_length > 0) { + result = DataVector( + reinterpret_cast(subset_data), + reinterpret_cast(subset_data) + subset_length); + } + hb_blob_destroy(subset_blob); + return result; +} + +RetainPtr BuildCompositeFont( + CPDF_Document* doc, + CFX_Font* font, + const ByteString& base_font_name, + pdfium::span font_data, + const std::map& widths, + const std::multimap& to_unicode, + ObjectStorage storage, + std::vector* temporary_object_numbers) { + if (!doc || !font || widths.empty() || to_unicode.empty()) { + return nullptr; + } + + RetainPtr font_dict = + CreateCompositeFontDict(doc, base_font_name, storage); + RetainPtr cid_font_dict = + CreateCidFontDict(doc, base_font_name, storage); + + RetainPtr font_descriptor_dict = LoadFontDesc( + doc, base_font_name, font, font_data, storage, temporary_object_numbers); + SetReferenceOrDirect(cid_font_dict.Get(), "FontDescriptor", doc, + std::move(font_descriptor_dict)); + + RetainPtr widths_array = CreateWidthsArray(doc, widths, storage); + SetReferenceOrDirect(cid_font_dict.Get(), "W", doc, std::move(widths_array)); + + CreateDescendantFontsArray(doc, font_dict.Get(), std::move(cid_font_dict)); + + RetainPtr to_unicode_stream = + LoadUnicode(doc, to_unicode, storage, temporary_object_numbers); + SetReferenceOrDirect(font_dict.Get(), "ToUnicode", doc, + std::move(to_unicode_stream)); + return font_dict; +} + +} // namespace + +CPDF_AnnotFontSubset::LayoutFont::LayoutFont() = default; + +CPDF_AnnotFontSubset::LayoutFont::LayoutFont(LayoutFont&& that) noexcept = + default; + +CPDF_AnnotFontSubset::LayoutFont& CPDF_AnnotFontSubset::LayoutFont::operator=( + LayoutFont&& that) noexcept = default; + +CPDF_AnnotFontSubset::LayoutFont::~LayoutFont() = default; + +// static +CPDF_AnnotFontSubset::LayoutFont CPDF_AnnotFontSubset::CreateLayoutFont( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id) { + LayoutFont result; + if (!doc || !CFX_FontRegistry::IsValidFont(font_id)) { + return result; + } + + std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); + if (!font || !font->HasAnyGlyphs()) { + return result; + } + + auto char_codes_and_indices = + font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); + if (char_codes_and_indices.empty()) { + return result; + } + + std::multimap to_unicode; + std::map widths; + for (const auto& item : char_codes_and_indices) { + if (item.glyph_index > kMaxPdfCid) { + continue; + } + if (!pdfium::Contains(widths, item.glyph_index)) { + widths[item.glyph_index] = font->GetGlyphWidth(item.glyph_index); + } + to_unicode.emplace(item.glyph_index, item.char_code); + } + if (widths.empty() || to_unicode.empty()) { + return result; + } + + const ByteString base_font_name = + BaseFontNameForRegisteredFont(font_id, font.get()); + RetainPtr font_dict = BuildCompositeFont( + doc, font.get(), base_font_name, font->GetFontSpan(), widths, to_unicode, + ObjectStorage::kDirect, &result.temporary_object_numbers); + result.font = CPDF_Font::Create(doc, std::move(font_dict), nullptr); + return result; +} + +// static +RetainPtr CPDF_AnnotFontSubset::CreateSubsetFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + const GlyphUnicodeMap& glyph_to_unicode) { + if (!doc || glyph_to_unicode.empty() || + !CFX_FontRegistry::IsValidFont(font_id)) { + return nullptr; + } + + std::unique_ptr font = CFX_FontRegistry::CreateFont(font_id); + if (!font || !font->HasAnyGlyphs()) { + return nullptr; + } + + GlyphUnicodeMap filtered_glyph_to_unicode; + std::map widths; + std::multimap to_unicode; + for (const auto& [glyph_id, unicode] : glyph_to_unicode) { + if (glyph_id == 0 || glyph_id > kMaxPdfCid) { + continue; + } + filtered_glyph_to_unicode.emplace(glyph_id, unicode); + widths[glyph_id] = font->GetGlyphWidth(glyph_id); + to_unicode.emplace(glyph_id, unicode); + } + if (filtered_glyph_to_unicode.empty()) { + return nullptr; + } + + DataVector subset_font_data = + SubsetFontDataRetainGids(font->GetFontSpan(), filtered_glyph_to_unicode); + pdfium::span font_data = subset_font_data.empty() + ? font->GetFontSpan() + : pdfium::span(subset_font_data); + + const ByteString base_font_name = + BaseFontNameForRegisteredFont(font_id, font.get()); + const ByteString subset_font_name = + MakeSubsetBaseFontName(base_font_name, filtered_glyph_to_unicode); + return BuildCompositeFont(doc, font.get(), subset_font_name, font_data, + widths, to_unicode, ObjectStorage::kIndirect, + /*temporary_object_numbers=*/nullptr); +} + +// static +RetainPtr CPDF_AnnotFontSubset::CreateMarkerFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id) { + if (!doc || !CFX_FontRegistry::IsValidFont(font_id)) { + return nullptr; + } + + auto font_dict = doc->NewIndirect(); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "Type1"); + font_dict->SetNewFor( + "BaseFont", BaseFontNameForRegisteredFont(font_id, nullptr)); + font_dict->SetNewFor("Encoding", + pdfium::font_encodings::kWinAnsiEncoding); + font_dict->SetNewFor(kRegisteredFontIdKey, + ByteString::Format("%u", font_id)); + return font_dict; +} + +// static +std::optional +CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + const CPDF_Dictionary* font_dict) { + if (!font_dict) { + return std::nullopt; + } + + ByteString id_string = font_dict->GetByteStringFor(kRegisteredFontIdKey); + if (id_string.IsEmpty()) { + return std::nullopt; + } + + uint32_t font_id = 0; + for (char ch : id_string.AsStringView()) { + if (ch < '0' || ch > '9') { + return std::nullopt; + } + FX_SAFE_UINT32 safe_font_id = font_id; + safe_font_id *= 10; + safe_font_id += ch - '0'; + if (!safe_font_id.IsValid()) { + return std::nullopt; + } + font_id = safe_font_id.ValueOrDie(); + } + + if (!CFX_FontRegistry::IsValidFont(font_id)) { + return std::nullopt; + } + return font_id; +} diff --git a/core/fpdfdoc/cpdf_annotfontsubset.h b/core/fpdfdoc/cpdf_annotfontsubset.h new file mode 100644 index 0000000000..a1118be63d --- /dev/null +++ b/core/fpdfdoc/cpdf_annotfontsubset.h @@ -0,0 +1,57 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: fork-owned helper for registered annotation font layout and +// per-annotation/layer subset embedding. + +#ifndef CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ +#define CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ + +#include + +#include +#include +#include + +#include "core/fxcrt/retain_ptr.h" +#include "core/fxge/cfx_fontregistry.h" + +class CPDF_Dictionary; +class CPDF_Document; +class CPDF_Font; + +class CPDF_AnnotFontSubset final { + public: + using GlyphUnicodeMap = std::map; + + struct LayoutFont { + LayoutFont(); + LayoutFont(LayoutFont&& that) noexcept; + LayoutFont& operator=(LayoutFont&& that) noexcept; + ~LayoutFont(); + + LayoutFont(const LayoutFont&) = delete; + LayoutFont& operator=(const LayoutFont&) = delete; + + RetainPtr font; + std::vector temporary_object_numbers; + }; + + static LayoutFont CreateLayoutFont(CPDF_Document* doc, + CFX_FontRegistry::FontId font_id); + + static RetainPtr CreateSubsetFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id, + const GlyphUnicodeMap& glyph_to_unicode); + + static RetainPtr CreateMarkerFontDict( + CPDF_Document* doc, + CFX_FontRegistry::FontId font_id); + + static std::optional + GetRegisteredFontIdFromMarkerFontDict(const CPDF_Dictionary* font_dict); +}; + +#endif // CORE_FPDFDOC_CPDF_ANNOTFONTSUBSET_H_ diff --git a/core/fpdfdoc/cpdf_dest.cpp b/core/fpdfdoc/cpdf_dest.cpp index ac99bbcc95..bf12f00e6c 100644 --- a/core/fpdfdoc/cpdf_dest.cpp +++ b/core/fpdfdoc/cpdf_dest.cpp @@ -12,6 +12,7 @@ #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_name.h" #include "core/fpdfapi/parser/cpdf_number.h" @@ -72,6 +73,37 @@ int CPDF_Dest::GetDestPageIndex(CPDF_Document* doc) const { return doc->GetPageIndex(pPage->GetObjNum()); } +uint32_t CPDF_Dest::GetPageObjectNumber(CPDF_Document* doc) const { + if (!doc || !array_) { + return 0; + } + + RetainPtr pPage = array_->GetDirectObjectAt(0); + if (!pPage) { + return 0; + } + + if (pPage->IsNumber()) { + const int page_index = pPage->GetInteger(); + if (page_index < 0 || page_index >= doc->GetPageCount()) { + return 0; + } + + RetainPtr page = doc->GetPageDictionary(page_index); + return page ? page->GetObjNum() : 0; + } + + if (!pPage->IsDictionary()) { + return 0; + } + + const uint32_t page_object_number = pPage->GetObjNum(); + if (page_object_number == 0 || doc->GetPageIndex(page_object_number) < 0) { + return 0; + } + return page_object_number; +} + std::vector CPDF_Dest::GetScrollPositionArray() const { std::vector result; if (array_) { diff --git a/core/fpdfdoc/cpdf_dest.h b/core/fpdfdoc/cpdf_dest.h index c79010d3e6..a0b496fe61 100644 --- a/core/fpdfdoc/cpdf_dest.h +++ b/core/fpdfdoc/cpdf_dest.h @@ -7,6 +7,7 @@ #ifndef CORE_FPDFDOC_CPDF_DEST_H_ #define CORE_FPDFDOC_CPDF_DEST_H_ +#include #include #include "core/fpdfapi/parser/cpdf_array.h" @@ -28,6 +29,7 @@ class CPDF_Dest { const CPDF_Array* GetArray() const { return array_.Get(); } int GetDestPageIndex(CPDF_Document* doc) const; + uint32_t GetPageObjectNumber(CPDF_Document* doc) const; std::vector GetScrollPositionArray() const; // Returns the zoom mode, as one of the PDFDEST_VIEW_* values in fpdf_doc.h. diff --git a/core/fpdfdoc/cpdf_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp index e399eb0635..d2be5aa2a1 100644 --- a/core/fpdfdoc/cpdf_generateap.cpp +++ b/core/fpdfdoc/cpdf_generateap.cpp @@ -34,6 +34,8 @@ #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" #include "core/fpdfdoc/cpdf_annot.h" +#include "core/fpdfdoc/cpdf_annotfontmap.h" +#include "core/fpdfdoc/cpdf_annotfontsubset.h" #include "core/fpdfdoc/cpdf_cloudy_border.h" #include "core/fpdfdoc/cpdf_color_utils.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" @@ -45,6 +47,7 @@ #include "core/fxcrt/fx_string_wrappers.h" #include "core/fxcrt/fx_system.h" #include "core/fxcrt/notreached.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_renderdevice.h" namespace { @@ -111,8 +114,12 @@ void EmitEndingWithAngle(fxcrt::ostringstream& out, const float cos_a = cos(final_angle_rad); const float sin_a = sin(final_angle_rad); - out << "q " << cos_a << " " << sin_a << " " << -sin_a << " " << cos_a << " " - << pos.x << " " << pos.y << " cm\n"; + // WriteMatrix, never raw `<<`: an axis-aligned segment has cos ≈ ±4.4e-8 + // and default ostream float formatting would emit it in scientific + // notation — not legal PDF number syntax (Acrobat rejects the file). + out << "q "; + WriteMatrix(out, CFX_Matrix(cos_a, sin_a, -sin_a, cos_a, pos.x, pos.y)) + << " cm\n"; emitter(); out << "Q\n"; } @@ -262,9 +269,12 @@ ByteString GetPDFWordString(IPVT_FontMap* font_map, } ByteString word_string; - uint32_t char_code = pdf_font->CharCodeFromUnicode(word); - if (char_code != CPDF_Font::kInvalidCharCode) { - pdf_font->AppendChar(&word_string, char_code); + // EmbedPDF: route unicode-to-charcode mapping through IPVT_FontMap so + // CPDF_AnnotFontMap can use registered fallback fonts and subset-local glyph + // ids when generating FreeText appearance streams. + int32_t char_code = font_map->CharCodeFromUnicode(font_index, word); + if (char_code >= 0) { + pdf_font->AppendChar(&word_string, static_cast(char_code)); } return word_string; } @@ -496,8 +506,23 @@ ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict) { info.bbox = unrotated; const float theta = rotate_deg * 3.14159265358979323846f / 180.0f; - const float cos_t = cosf(theta); - const float sin_t = sinf(theta); + // Snap the trig to exact 0/±1 near quarter turns (the values `upright` + // authoring produces): float cos(90°) is ~-4.4e-8, which would otherwise + // leak near-zero noise into the emitted matrix numbers. + auto snap = [](float v) { + if (fabsf(v) < 1e-6f) { + return 0.0f; + } + if (fabsf(v - 1.0f) < 1e-6f) { + return 1.0f; + } + if (fabsf(v + 1.0f) < 1e-6f) { + return -1.0f; + } + return v; + }; + const float cos_t = snap(cosf(theta)); + const float sin_t = snap(sinf(theta)); const float cx = (unrotated.left + unrotated.right) / 2.0f; const float cy = (unrotated.bottom + unrotated.top) / 2.0f; @@ -1140,6 +1165,153 @@ ByteString GenerateTextSymbolAP(const CFX_FloatRect& rect, return ByteString(app_stream); } +// Appends a closed ellipse path inscribed in |bounds| (same four-bezier +// construction as GenerateCircleAP). +void AppendEllipsePath(fxcrt::ostringstream& app_stream, + const CFX_FloatRect& bounds) { + const float middle_x = (bounds.left + bounds.right) / 2; + const float middle_y = (bounds.top + bounds.bottom) / 2; + + static constexpr float kL = 0.5523f; + const float delta_x = kL * bounds.Width() / 2.0f; + const float delta_y = kL * bounds.Height() / 2.0f; + + app_stream << middle_x << " " << bounds.top << " m\n"; + app_stream << middle_x + delta_x << " " << bounds.top << " " << bounds.right + << " " << middle_y + delta_y << " " << bounds.right << " " + << middle_y << " c\n"; + app_stream << bounds.right << " " << middle_y - delta_y << " " + << middle_x + delta_x << " " << bounds.bottom << " " << middle_x + << " " << bounds.bottom << " c\n"; + app_stream << middle_x - delta_x << " " << bounds.bottom << " " + << bounds.left << " " << middle_y - delta_y << " " << bounds.left + << " " << middle_y << " c\n"; + app_stream << bounds.left << " " << middle_y + delta_y << " " + << middle_x - delta_x << " " << bounds.top << " " << middle_x + << " " << bounds.top << " c\nh\n"; +} + +ByteString GenerateFileAttachmentSymbolAP(const CFX_FloatRect& rect, + const CPDF_Dictionary& annot_dict) { + fxcrt::ostringstream app_stream; + + // Read fill color from /C array; default to yellow (the note-icon + // default in GenerateTextSymbolAP). + CFX_Color fill_color(CFX_Color::Type::kRGB, 1, 1, 0); + RetainPtr color_array = + annot_dict.GetArrayFor(pdfium::annotation::kC); + if (color_array) { + fill_color = fpdfdoc::CFXColorFromArray(*color_array); + } + + // Same luminance-based contrast stroke as GenerateTextSymbolAP. + float luminance = 0.299f * fill_color.fColor1 + 0.587f * fill_color.fColor2 + + 0.114f * fill_color.fColor3; + CFX_Color stroke_color = luminance < 0.45f + ? CFX_Color(CFX_Color::Type::kRGB, 1, 1, 1) + : CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0); + + // /Name picks the glyph. Absent or foreign names mean PushPin, the + // ISO 32000 default icon for file attachment annotations. + CPDF_Annot::Icon icon = + CPDF_Annot::StringToIcon(annot_dict.GetNameFor("Name")); + if (icon != CPDF_Annot::Icon::kFile_Graph && + icon != CPDF_Annot::Icon::kFile_Paperclip && + icon != CPDF_Annot::Icon::kFile_Tag) { + icon = CPDF_Annot::Icon::kFile_PushPin; + } + + static constexpr int kBorderWidth = 1; + static constexpr float kHalfWidth = kBorderWidth / 2.0f; + CFX_FloatRect box = rect; + box.Deflate(kHalfWidth, kHalfWidth); + const float w = box.Width(); + const float h = box.Height(); + // Glyph coordinates below are fractions of the icon box. + auto px = [&](float fx) { return box.left + fx * w; }; + auto py = [&](float fy) { return box.bottom + fy * h; }; + + if (icon == CPDF_Annot::Icon::kFile_Paperclip) { + // A paperclip is a wire, not a closed region, so /C colours the + // stroked wire itself; a wider contrast pass underneath is the wire + // counterpart of the note icon's contrast border. + fxcrt::ostringstream path; + WritePoint(path, {px(0.32f), py(0.28f)}) << " m\n"; + WritePoint(path, {px(0.32f), py(0.72f)}) << " l\n"; + path << px(0.32f) << " " << py(0.85f) << " " << px(0.68f) << " " + << py(0.85f) << " " << px(0.68f) << " " << py(0.72f) << " c\n"; + WritePoint(path, {px(0.68f), py(0.20f)}) << " l\n"; + path << px(0.68f) << " " << py(0.10f) << " " << px(0.50f) << " " + << py(0.10f) << " " << px(0.50f) << " " << py(0.20f) << " c\n"; + WritePoint(path, {px(0.50f), py(0.65f)}) << " l\n"; + path << px(0.50f) << " " << py(0.72f) << " " << px(0.41f) << " " + << py(0.72f) << " " << px(0.41f) << " " << py(0.65f) << " c\n"; + WritePoint(path, {px(0.41f), py(0.30f)}) << " l\n"; + const ByteString wire(path); + + app_stream << "1 J\n1 j\n"; + app_stream << GenerateColorAP(stroke_color, PaintOperation::kStroke); + WriteFloat(app_stream, 2.6f) << " w\n" << wire << "S\n"; + app_stream << GenerateColorAP(fill_color, PaintOperation::kStroke); + WriteFloat(app_stream, 1.4f) << " w\n" << wire << "S\n"; + return ByteString(app_stream); + } + + app_stream << GenerateColorAP(fill_color, PaintOperation::kFill); + app_stream << GenerateColorAP(stroke_color, PaintOperation::kStroke); + app_stream << kBorderWidth << " w\n"; + + switch (icon) { + case CPDF_Annot::Icon::kFile_PushPin: { + // Round head, collar, and a tapering needle. Painted with the + // nonzero rule (`B`) so touching subpaths merge instead of + // punching even-odd holes. + AppendEllipsePath(app_stream, CFX_FloatRect(px(0.33f), py(0.53f), + px(0.67f), py(0.87f))); + app_stream << px(0.37f) << " " << py(0.46f) << " " << px(0.63f) - px(0.37f) + << " " << py(0.525f) - py(0.46f) << " re\n"; + WritePoint(app_stream, {px(0.47f), py(0.455f)}) << " m\n"; + WritePoint(app_stream, {px(0.53f), py(0.455f)}) << " l\n"; + WritePoint(app_stream, {px(0.50f), py(0.10f)}) << " l\nh\n"; + app_stream << "B\n"; + break; + } + case CPDF_Annot::Icon::kFile_Graph: { + // Even-odd turns the outer+inner rectangles into a frame ring; the + // three bars sit inside the ring on its bottom edge. + app_stream << px(0.10f) << " " << py(0.10f) << " " << px(0.90f) - px(0.10f) + << " " << py(0.90f) - py(0.10f) << " re\n"; + app_stream << px(0.16f) << " " << py(0.16f) << " " << px(0.84f) - px(0.16f) + << " " << py(0.84f) - py(0.16f) << " re\n"; + app_stream << px(0.22f) << " " << py(0.16f) << " " << px(0.36f) - px(0.22f) + << " " << py(0.40f) - py(0.16f) << " re\n"; + app_stream << px(0.43f) << " " << py(0.16f) << " " << px(0.57f) - px(0.43f) + << " " << py(0.56f) - py(0.16f) << " re\n"; + app_stream << px(0.64f) << " " << py(0.16f) << " " << px(0.78f) - px(0.64f) + << " " << py(0.76f) - py(0.16f) << " re\n"; + app_stream << "B*\n"; + break; + } + case CPDF_Annot::Icon::kFile_Tag: { + // Label pentagon pointing left; even-odd punches the eyelet hole. + WritePoint(app_stream, {px(0.10f), py(0.50f)}) << " m\n"; + WritePoint(app_stream, {px(0.34f), py(0.80f)}) << " l\n"; + WritePoint(app_stream, {px(0.90f), py(0.80f)}) << " l\n"; + WritePoint(app_stream, {px(0.90f), py(0.20f)}) << " l\n"; + WritePoint(app_stream, {px(0.34f), py(0.20f)}) << " l\nh\n"; + AppendEllipsePath(app_stream, CFX_FloatRect(px(0.305f), py(0.445f), + px(0.415f), py(0.555f))); + app_stream << "B*\n"; + break; + } + default: { + NOTREACHED(); + } + } + + return ByteString(app_stream); +} + RetainPtr GenerateExtGStateDict( const CPDF_Dictionary& annot_dict, const ByteString& blend_mode) { @@ -1383,10 +1555,13 @@ void GenerateLineEndings(fxcrt::ostringstream& ap, ByteString GenerateTextFieldAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, float font_size, - CPVT_VariableText& vt) { + CPVT_VariableText& vt, + const WideString* value_override) { RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = v_field ? v_field->GetUnicodeText() : WideString(); + WideString value = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); RetainPtr q_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, "Q"); const int32_t align = q_field ? q_field->GetInteger() : 0; @@ -1433,12 +1608,15 @@ ByteString GenerateComboBoxAP(const CPDF_Dictionary* annot_dict, const CFX_FloatRect& body_rect, const CFX_Color& text_color, float font_size, - CPVT_VariableText::Provider& provider) { + CPVT_VariableText::Provider& provider, + const WideString* value_override) { fxcrt::ostringstream body_stream; RetainPtr v_field = CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kV); - WideString value = v_field ? v_field->GetUnicodeText() : WideString(); + WideString value = value_override + ? *value_override + : (v_field ? v_field->GetUnicodeText() : WideString()); CPVT_VariableText vt(&provider); CFX_FloatRect button_rect = body_rect; button_rect.left = button_rect.right - 13; @@ -1728,6 +1906,18 @@ bool GenerateFreeTextAP(APGenerationTarget* target, CFX_FloatRect text_box(rect.left + rd.left, rect.bottom + rd.bottom, rect.right - rd.right, rect.top - rd.top); + // (b') EmbedPDF upright tilt: for a callout the /EMBD_Metadata pair means + // the TEXT BOX only — `UnrotatedRect` is the logical text box, `Rotation` + // its tilt about the box centre. The /CL leader stays page-space, so the + // rotation is baked INLINE (a `q cm … Q` around the box + text below), + // never as the form /Matrix — /Rect keeps placing the whole appearance + // (RD then recovers the rotated box's AABB, the best axis-aligned box a + // viewer regenerating this AP can draw). + const ShapeRotationInfo box_rot = GetShapeRotationInfo(annot_dict); + if (box_rot.is_rotated) { + text_box = box_rot.bbox; + } + // (c) Border width and colors. const float border_w = GetBorderWidth(annot_dict); @@ -1819,6 +2009,16 @@ bool GenerateFreeTextAP(APGenerationTarget* target, // (g) Draw text box rectangle. Pick the paint operator dynamically so a // missing /C means "no fill" (stroke-only) rather than falling back to // PDF's default black fill. Mirrors GenerateCircleAP / GenerateSquareAP. + // An upright-tilted box (see (b')) authors in the logical box frame and + // spins it about its centre via an inline `cm` — box + text only; the + // leader/arrow above already drew in page space. WriteMatrix, never raw + // `<<`: default ostream float formatting uses scientific notation for tiny + // magnitudes (cos of a right angle ≈ -4.4e-8), which is not legal PDF + // number syntax — Acrobat rejects the whole file as corrupt. + if (box_rot.is_rotated) { + appearance_stream << "q "; + WriteMatrix(appearance_stream, box_rot.matrix) << " cm\n"; + } const bool is_fill_rect = color_array != nullptr; const bool is_stroke_rect = border_w > 0; CFX_FloatRect text_box_stroke = text_box; @@ -1839,7 +2039,11 @@ bool GenerateFreeTextAP(APGenerationTarget* target, actual_text_color = fpdfdoc::CFXColorFromArray(*tc); } - CPVT_FontMap map(doc, nullptr, std::move(default_font), font_name); + // EmbedPDF: use the annotation font map instead of CPVT_FontMap so + // FreeText AP generation can fall back to registered fonts and produce + // persistent, per-annotation subsets when saving. + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + target->IsPersistent()); CPVT_VariableText::Provider provider(&map); CPVT_VariableText vt(&provider); @@ -1879,11 +2083,15 @@ bool GenerateFreeTextAP(APGenerationTarget* target, PaintOperation::kFill) << body << "ET\nQ\n"; } + if (box_rot.is_rotated) { + appearance_stream << "Q\n"; // close the (g) inline box rotation + } // Finalize AP dict. auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); + // EmbedPDF: collect both the original DA font and any registered fallback + // fonts actually used by this annotation into the AP resource dictionary. + auto resource_font_dict = map.CreateFontResourceDict(); auto resource_dict = GenerateResourcesDict( doc, std::move(graphics_state_dict), std::move(resource_font_dict)); GenerateAndSetAPDict(target, annot_dict, &appearance_stream, @@ -1916,7 +2124,9 @@ bool GenerateFreeTextAP(APGenerationTarget* target, appearance_stream << "q\n" << border_stream << "Q\n"; } - CPVT_FontMap map(doc, nullptr, std::move(default_font), font_name); + // EmbedPDF: same registered-font/subset path as the callout branch above. + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + target->IsPersistent()); CPVT_VariableText::Provider provider(&map); CPVT_VariableText vt(&provider); @@ -1961,8 +2171,9 @@ bool GenerateFreeTextAP(APGenerationTarget* target, } auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict.Get()); + // EmbedPDF: include registered fallback subset fonts used by this FreeText + // appearance, scoped to this annotation/layer. + auto resource_font_dict = map.CreateFontResourceDict(); auto resource_dict = GenerateResourcesDict( doc, std::move(graphics_state_dict), std::move(resource_font_dict)); if (rot_info.is_rotated) { @@ -2198,13 +2409,13 @@ bool GenerateInkAP(APGenerationTarget* target, app_stream << GetDashPatternString(annot_dict); - // Set inflated rect as a new rect because paths near the border with large - // width should not be clipped to the original rect. - CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - rect.Inflate(border_width / 2, border_width / 2); - if (target->IsPersistent()) { - annot_dict->SetRectFor(pdfium::annotation::kRect, rect); - } + // Track the stroked ink's true bounds while writing the path: the union of + // every /InkList point, inflated by half the border width below (the round + // caps/joins set above — `1 J 1 j` — extend exactly border_width / 2 past a + // point). This is what the appearance actually PAINTS, independent of what + // /Rect currently claims. + CFX_FloatRect ink_bounds; + bool has_ink_point = false; for (size_t i = 0; i < ink_list->size(); i++) { RetainPtr coordinates_array = ink_list->GetArrayAt(i); @@ -2213,19 +2424,47 @@ bool GenerateInkAP(APGenerationTarget* target, continue; } - app_stream << coordinates_array->GetFloatAt(0) << " " - << coordinates_array->GetFloatAt(1) << " m "; + const float x0 = coordinates_array->GetFloatAt(0); + const float y0 = coordinates_array->GetFloatAt(1); + app_stream << x0 << " " << y0 << " m "; + if (has_ink_point) { + ink_bounds.UpdateRect(CFX_PointF(x0, y0)); + } else { + ink_bounds = CFX_FloatRect(x0, y0, x0, y0); + has_ink_point = true; + } // Start loop at the second point (index 2) --- // The 'm' command already moves to the first point. for (size_t j = 2; j < coordinates_array->size(); j += 2) { - app_stream << coordinates_array->GetFloatAt(j) << " " - << coordinates_array->GetFloatAt(j + 1) << " l "; + const float x = coordinates_array->GetFloatAt(j); + const float y = coordinates_array->GetFloatAt(j + 1); + app_stream << x << " " << y << " l "; + ink_bounds.UpdateRect(CFX_PointF(x, y)); } app_stream << "S\n"; } + // ENSURE-FIT, never blind-inflate. The caller owns /Rect (EmbedPDF's + // writers author it as the stroked visual bounds already); grow it only + // when the painted ink would actually be clipped, by the minimal union — + // so regeneration is IDEMPOTENT. Upstream PDFium instead inflated /Rect by + // border_width / 2 unconditionally on every call: harmless on its one-shot + // "synthesize a missing /AP at load" path, but unbounded growth once the + // appearance is re-baked after each edit. + CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); + rect.Normalize(); + if (has_ink_point) { + ink_bounds.Inflate(border_width / 2, border_width / 2); + if (!rect.Contains(ink_bounds)) { + rect.Union(ink_bounds); + if (target->IsPersistent()) { + annot_dict->SetRectFor(pdfium::annotation::kRect, rect); + } + } + } + auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); auto resources_dict = GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); @@ -2261,6 +2500,29 @@ bool GenerateTextAP(CPDF_Document* doc, return true; } +bool GenerateFileAttachmentAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { + fxcrt::ostringstream app_stream; + app_stream << "/" << kGSDictName << " gs "; + + // Like the note icon, a file attachment renders at a fixed icon size + // anchored at the /Rect's bottom-left corner. + CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); + const float icon_length = 20; + CFX_FloatRect icon_rect(rect.left, rect.bottom, rect.left + icon_length, + rect.bottom + icon_length); + annot_dict->SetRectFor(pdfium::annotation::kRect, icon_rect); + + app_stream << GenerateFileAttachmentSymbolAP(icon_rect, *annot_dict); + + auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); + auto resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(doc, annot_dict, &app_stream, std::move(resources_dict), + false /*IsTextMarkupAnnotation*/); + return true; +} + bool GenerateUnderlineAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { @@ -2546,151 +2808,278 @@ bool GenerateLinkAP(CPDF_Document* doc, return true; } -void GenerateRedactAPDicts(CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - fxcrt::ostringstream* normal_stream, - fxcrt::ostringstream* rollover_stream, - RetainPtr resource_dict, - bool is_text_markup) { - CFX_FloatRect rect = is_text_markup - ? CPDF_Annot::BoundingRectFromQuadPoints(annot_dict) - : annot_dict->GetRectFor(pdfium::annotation::kRect); +// EmbedPDF: the regions a /Redact annotation targets — /QuadPoints quads when +// present (text redactions), else the annotation /Rect (area redactions). +std::vector GetRedactOverlayRegions( + const CPDF_Dictionary* annot_dict) { + std::vector regions; + RetainPtr quad_points_array = + annot_dict->GetArrayFor("QuadPoints"); + if (quad_points_array && quad_points_array->size() >= 8) { + const size_t quad_count = + CPDF_Annot::QuadPointCount(quad_points_array.Get()); + for (size_t i = 0; i < quad_count; ++i) { + CFX_FloatRect rect = CPDF_Annot::RectFromQuadPoints(annot_dict, i); + rect.Normalize(); + if (!rect.IsEmpty()) { + regions.push_back(rect); + } + } + if (!regions.empty()) { + return regions; + } + } + CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); + rect.Normalize(); + if (!rect.IsEmpty()) { + regions.push_back(rect); + } + return regions; +} - // Create Normal appearance stream (border only) - auto normal_stream_dict = pdfium::MakeRetain(); - normal_stream_dict->SetNewFor("FormType", 1); - normal_stream_dict->SetNewFor("Type", "XObject"); - normal_stream_dict->SetNewFor("Subtype", "Form"); - normal_stream_dict->SetMatrixFor("Matrix", CFX_Matrix()); - normal_stream_dict->SetRectFor("BBox", rect); - normal_stream_dict->SetFor("Resources", resource_dict->Clone()); - - auto normal_pdf_stream = - doc->NewIndirect(std::move(normal_stream_dict)); - normal_pdf_stream->SetDataFromStringstream(normal_stream); - - // Create Rollover/Down/RO appearance stream (filled preview) - // This single stream is shared by R, D, and RO - auto rollover_stream_dict = pdfium::MakeRetain(); - rollover_stream_dict->SetNewFor("FormType", 1); - rollover_stream_dict->SetNewFor("Type", "XObject"); - rollover_stream_dict->SetNewFor("Subtype", "Form"); - rollover_stream_dict->SetMatrixFor("Matrix", CFX_Matrix()); - rollover_stream_dict->SetRectFor("BBox", rect); - rollover_stream_dict->SetFor("Resources", resource_dict->Clone()); - - auto rollover_pdf_stream = - doc->NewIndirect(std::move(rollover_stream_dict)); - rollover_pdf_stream->SetDataFromStringstream(rollover_stream); - - // Get the object number for the shared rollover stream - uint32_t rollover_obj_num = rollover_pdf_stream->GetObjNum(); - - // Set all entries in AP dictionary - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - ap_dict->SetNewFor("N", doc, normal_pdf_stream->GetObjNum()); - ap_dict->SetNewFor("R", doc, rollover_obj_num); // Rollover - ap_dict->SetNewFor("D", doc, rollover_obj_num); // Down +// EmbedPDF: the marking-stage /AP BBox and the overlay BBox share this rule: +// quad bounding box for text redactions, /Rect for area redactions. +CFX_FloatRect GetRedactOverlayBBox(const CPDF_Dictionary* annot_dict) { + RetainPtr quad_points_array = + annot_dict->GetArrayFor("QuadPoints"); + if (quad_points_array && quad_points_array->size() >= 8) { + return CPDF_Annot::BoundingRectFromQuadPoints(annot_dict); + } + CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); + rect.Normalize(); + return rect; +} - // Set RO (Redact Overlay) - this is what gets applied when redaction is - // finalized RO is stored directly on the annotation dict, not inside AP - annot_dict->SetNewFor("RO", doc, rollover_obj_num); +RetainPtr MakeRedactFormStream( + CPDF_Document* doc, + const CFX_FloatRect& bbox, + RetainPtr resources, + fxcrt::ostringstream* ops) { + auto stream_dict = pdfium::MakeRetain(); + stream_dict->SetNewFor("FormType", 1); + stream_dict->SetNewFor("Type", "XObject"); + stream_dict->SetNewFor("Subtype", "Form"); + stream_dict->SetMatrixFor("Matrix", CFX_Matrix()); + stream_dict->SetRectFor("BBox", bbox); + if (resources) { + stream_dict->SetFor("Resources", std::move(resources)); + } + auto stream = doc->NewIndirect(std::move(stream_dict)); + stream->SetDataFromStringstream(ops); + return stream; } -bool GenerateRedactAP(CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - const ByteString& blend_name) { - fxcrt::ostringstream normal_stream; - fxcrt::ostringstream rollover_stream; - normal_stream << "/" << kGSDictName << " gs "; - rollover_stream << "/" << kGSDictName << " gs "; +constexpr float kRedactRepeatFallbackFontSize = 12.0f; +constexpr int kRedactMaxRepeatDoublings = 10; // 2^10 = 1024 label instances + +// EmbedPDF: lay out the /OverlayText label for one redacted region with the +// same CPVT + annotation-font-map stack as FreeText appearances, so /DA fonts +// (standard, DR-resolved, or registered runtime fonts) shape and subset +// identically. Top-aligned in reading order; ISO 32000-2 prescribes neither +// the vertical placement nor the /Repeat tiling, so /Repeat is expressed as +// "repeat the label, space-joined, wrapped to the region, clipped". +void AppendRedactLabelForRegion(CPDF_AnnotFontMap& map, + const CPDF_Dictionary* annot_dict, + const WideString& overlay_text, + float da_font_size, + const CFX_Color& label_color, + const CFX_FloatRect& region, + fxcrt::ostringstream& stream) { + CPVT_VariableText::Provider provider(&map); + CPVT_VariableText vt(&provider); + vt.SetPlateRect(region); + vt.SetAlignment(annot_dict->GetIntegerFor("Q")); + vt.SetMultiLine(true); + vt.SetAutoReturn(true); - // Get colors from annotation dictionary - // C - stroke/border color (default: red for redact) - // IC - interior color (fill when redaction applied, default: black) - RetainPtr stroke_color = - annot_dict->GetArrayFor(pdfium::annotation::kC); + const bool repeat = annot_dict->GetBooleanFor("Repeat", false); + // CPVT auto-sizing fits ALL text into the plate, so it cannot combine with + // /Repeat (more repetitions would only shrink the font); pin a concrete + // size for the repeat case when /DA asks for auto (size 0). + float font_size = da_font_size; + if (repeat && FXSYS_IsFloatZero(font_size)) { + font_size = kRedactRepeatFallbackFontSize; + } + SetVtFontSize(font_size, vt); + vt.Initialize(); + vt.SetText(overlay_text); + vt.RearrangeAll(); + + if (repeat) { + // Double the space-joined text until the wrapped layout covers the region + // vertically; the last row's surplus is removed by the region clip below. + // Bounded to keep tiny-font/huge-region combinations sane. + WideString tiled = overlay_text; + for (int i = 0; i < kRedactMaxRepeatDoublings && + vt.GetContentRect().Height() < region.Height(); + ++i) { + WideString doubled = tiled; + doubled += L' '; + doubled += tiled; + tiled = std::move(doubled); + vt.SetText(tiled); + vt.RearrangeAll(); + } + } + + const ByteString body = + GenerateEditAP(vt.GetProvider()->GetFontMap(), vt.GetIterator(), + CFX_PointF(0.0f, 0.0f), /*continuous=*/true, + /*sub_word=*/0); + if (body.IsEmpty()) { + return; + } + stream << "q\n"; + WriteRect(stream, region) << " re W n\n"; + stream << "BT\n" + << GenerateColorAP(label_color, PaintOperation::kFill) << body + << "ET\nQ\n"; +} + +// EmbedPDF: emit the final ("post-apply") overlay ops for a /Redact +// annotation: opaque /IC fill of every region, then the /OverlayText label. +// The marking-stage /CA opacity is deliberately not carried over — the +// content underneath is destroyed, so the replacement marking paints opaque, +// matching Acrobat. Returns false when the annotation defines neither a fill +// nor a label. +bool AppendRedactOverlayOps(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + fxcrt::ostringstream& stream, + RetainPtr* out_font_resources) { + const WideString overlay_text = annot_dict->GetUnicodeTextFor("OverlayText"); RetainPtr interior_color = annot_dict->GetArrayFor("IC"); const bool has_fill = interior_color && !interior_color->IsEmpty(); + if (!has_fill && overlay_text.IsEmpty()) { + return false; + } - // Normal appearance: stroke color for border - normal_stream << GetColorStringWithDefault( - stroke_color.Get(), - CFX_Color(CFX_Color::Type::kRGB, 1, 0, 0), // default: red - PaintOperation::kStroke); + const std::vector regions = + GetRedactOverlayRegions(annot_dict); + if (regions.empty()) { + return false; + } - // Rollover appearance: interior color for fill - rollover_stream << GetColorStringWithDefault( - interior_color.Get(), - CFX_Color(CFX_Color::Type::kTransparent), // default: no fill - PaintOperation::kFill); + if (has_fill) { + stream << GetColorStringWithDefault( + interior_color.Get(), CFX_Color(CFX_Color::Type::kTransparent), + PaintOperation::kFill); + for (const CFX_FloatRect& region : regions) { + WriteRect(stream, region) << " re f\n"; + } + } - float border_width = GetBorderWidth(annot_dict); - if (border_width > 0) { - normal_stream << border_width << " w "; - normal_stream << GetDashPatternString(annot_dict); + if (overlay_text.IsEmpty()) { + return true; } - // Check for QuadPoints (text-based redaction) - RetainPtr quad_points_array = - annot_dict->GetArrayFor("QuadPoints"); + // /DA resolution mirrors the FreeText persistent path, but is forgiving: + // redact annotations marked by other producers can lack /DA (ISO requires + // it alongside /OverlayText, but such files exist) or an AcroForm /DR — + // fall back to Helvetica rather than dropping the label. + RetainPtr root_dict = doc->GetMutableRoot(); + RetainPtr form_dict; + if (root_dict) { + form_dict = root_dict->GetMutableDictFor("AcroForm"); + if (!form_dict) { + form_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); + } + } - if (quad_points_array && quad_points_array->size() >= 8) { - // QuadPoints present - iterate through each quad - const size_t quad_point_count = - CPDF_Annot::QuadPointCount(quad_points_array.Get()); - for (size_t i = 0; i < quad_point_count; ++i) { - CFX_FloatRect rect = CPDF_Annot::RectFromQuadPoints(annot_dict, i); - rect.Normalize(); + std::optional da_info = + form_dict ? GetDefaultAppearanceInfo(annot_dict, form_dict.Get()) + : std::nullopt; + const ByteString font_name = + da_info.has_value() ? da_info.value().font_name : ByteString("Helv"); + const float da_font_size = + da_info.has_value() ? da_info.value().font_size : 0.0f; - // Normal: stroke the rectangle (border only) - if (border_width > 0) { - CFX_FloatRect stroke_rect = rect; - stroke_rect.Deflate(border_width / 2, border_width / 2); - normal_stream << stroke_rect.left << " " << stroke_rect.bottom << " " - << stroke_rect.Width() << " " << stroke_rect.Height() - << " re S\n"; - } + CFX_Color label_color = + da_info.has_value() ? da_info.value().text_color : CFX_Color(); + if (label_color.nColorType == CFX_Color::Type::kTransparent) { + // Legacy EmbedPDF v2 files carry the label colour in /OC; ISO keeps it + // in the /DA string. Default to black when neither is present. + RetainPtr oc = annot_dict->GetArrayFor("OC"); + label_color = (oc && oc->size() >= 3) + ? fpdfdoc::CFXColorFromArray(*oc) + : CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0); + } - // Rollover: fill the rectangle (only if interior color is set) - if (has_fill) { - rollover_stream << rect.left << " " << rect.top << " m " << rect.right - << " " << rect.top << " l " << rect.right << " " - << rect.bottom << " l " << rect.left << " " - << rect.bottom << " l h f\n"; - } - } + RetainPtr font_dict; + if (form_dict) { + RetainPtr dr_font_dict = + form_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor("Font"); + font_dict = GetFontFromDrFontDictOrGenerateFallback(doc, dr_font_dict.Get(), + font_name); } else { - // No QuadPoints - use the annotation Rect - CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - rect.Normalize(); + font_dict = GenerateFallbackFontDict(doc); + } + RetainPtr default_font = + CPDF_DocPageData::FromDocument(doc)->GetFont(font_dict); + if (!default_font) { + return has_fill; + } - // Normal: stroke the rectangle (border only) - if (border_width > 0) { - CFX_FloatRect stroke_rect = rect; + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true); + for (const CFX_FloatRect& region : regions) { + AppendRedactLabelForRegion(map, annot_dict, overlay_text, da_font_size, + label_color, region, stream); + } + *out_font_resources = map.CreateFontResourceDict(); + return true; +} + +bool GenerateRedactAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { + // Normal (marking-stage) appearance: border-only outline in /C, default + // red. The filled preview is NOT drawn here — R/D and /RO all share the + // final overlay from BuildRedactOverlayForm, so hovering a marked + // redaction previews exactly what apply will paint. + fxcrt::ostringstream normal_stream; + normal_stream << "/" << kGSDictName << " gs "; + normal_stream << GetColorStringWithDefault( + annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), + CFX_Color(CFX_Color::Type::kRGB, 1, 0, 0), // default: red + PaintOperation::kStroke); + + const float border_width = GetBorderWidth(annot_dict); + if (border_width > 0) { + normal_stream << border_width << " w "; + normal_stream << GetDashPatternString(annot_dict); + for (const CFX_FloatRect& region : GetRedactOverlayRegions(annot_dict)) { + CFX_FloatRect stroke_rect = region; stroke_rect.Deflate(border_width / 2, border_width / 2); normal_stream << stroke_rect.left << " " << stroke_rect.bottom << " " << stroke_rect.Width() << " " << stroke_rect.Height() << " re S\n"; } - - // Rollover: fill the rectangle (only if interior color is set) - if (has_fill) { - rollover_stream << rect.left << " " << rect.bottom << " " << rect.Width() - << " " << rect.Height() << " re f\n"; - } } - // Build resources auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); auto resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); + const CFX_FloatRect bbox = GetRedactOverlayBBox(annot_dict); + RetainPtr normal_pdf_stream = MakeRedactFormStream( + doc, bbox, std::move(resources_dict), &normal_stream); - // Generate both Normal and Rollover appearance streams - bool has_quad_points = quad_points_array && quad_points_array->size() >= 8; - GenerateRedactAPDicts(doc, annot_dict, &normal_stream, &rollover_stream, - resources_dict, has_quad_points); + // Rollover/Down and /RO share the final overlay (fill + label). + RetainPtr overlay = + CPDF_GenerateAP::BuildRedactOverlayForm(doc, annot_dict); + if (!overlay) { + // Neither /IC nor /OverlayText: keep the AP structure (and the baked /RO + // that pre-v3 clients flatten on apply) with an empty overlay. + fxcrt::ostringstream empty_stream; + overlay = MakeRedactFormStream(doc, bbox, nullptr, &empty_stream); + } + RetainPtr ap_dict = + annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); + ap_dict->SetNewFor("N", doc, normal_pdf_stream->GetObjNum()); + ap_dict->SetNewFor("R", doc, overlay->GetObjNum()); + ap_dict->SetNewFor("D", doc, overlay->GetObjNum()); + + // /RO lives on the annotation dict, not inside /AP. + annot_dict->SetNewFor("RO", doc, overlay->GetObjNum()); return true; } @@ -2698,7 +3087,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPVT_VariableText::Provider& provider, + const WideString* value_override) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); const bool has_bg = @@ -2739,8 +3129,8 @@ void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, WriteRect(app_stream, clip_rect) << " re W n\n"; CPVT_VariableText vt(&provider); - ByteString body = - GenerateTextFieldAP(annot_dict, body_rect, da_info.font_size, vt); + ByteString body = GenerateTextFieldAP(annot_dict, body_rect, + da_info.font_size, vt, value_override); app_stream << "BT\n"; app_stream << GenerateColorAP(da_info.text_color, PaintOperation::kStroke); @@ -2759,7 +3149,8 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, const CPDF_Dictionary* annot_dict, const CFX_FloatRect& bbox, const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { + CPVT_VariableText::Provider& provider, + const WideString* value_override) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); const BorderStyleInfo border_info = @@ -2782,7 +3173,7 @@ void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, body_rect.Deflate(border_info.width, border_info.width); app_stream << GenerateComboBoxAP(annot_dict, body_rect, da_info.text_color, - da_info.font_size, provider); + da_info.font_size, provider, value_override); } void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, @@ -2939,7 +3330,8 @@ std::optional GetWidgetFormType( bool GenerateFormAPToTarget(APGenerationTarget* target, CPDF_Dictionary* annot_dict, - CPDF_GenerateAP::FormType type) { + CPDF_GenerateAP::FormType type, + const WideString* value_override) { CPDF_Document* const doc = target->doc; const CPDF_Dictionary* root_dict = doc->GetRoot(); if (!root_dict) { @@ -2948,8 +3340,15 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, RetainPtr form_dict = root_dict->GetDictFor("AcroForm"); + RetainPtr ephemeral_form_dict; if (!form_dict) { - return false; + if (target->IsPersistent()) { + form_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); + CHECK(form_dict); + } else { + ephemeral_form_dict = GenerateEphemeralDefaultAcroFormDict(); + form_dict = ephemeral_form_dict; + } } std::optional default_appearance_info = @@ -2958,32 +3357,55 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, return false; } + // A missing or font-less /DR must not veto appearance generation — the + // widget's own /DA names the font it wants, and DR-less AcroForms are + // common in flattened government forms (the IRS f1040 class). Persistent + // targets seed /DR/Font with a fallback the same way redaction overlays + // do; ephemeral targets fall back without mutating the document. RetainPtr dr_dict = form_dict->GetDictFor("DR"); - if (!dr_dict) { - return false; - } - - RetainPtr dr_font_dict = dr_dict->GetDictFor("Font"); + RetainPtr dr_font_dict = + dr_dict ? dr_dict->GetDictFor("Font") : nullptr; if (!ValidateFontResourceDict(dr_font_dict.Get())) { - return false; + dr_font_dict.Reset(); } const ByteString& font_name = default_appearance_info.value().font_name; RetainPtr font_dict; if (target->IsPersistent()) { + RetainPtr mutable_dr_font_dict; + if (dr_font_dict) { + mutable_dr_font_dict = + pdfium::WrapRetain(const_cast(dr_font_dict.Get())); + } else { + RetainPtr mutable_root = doc->GetMutableRoot(); + RetainPtr mutable_form_dict = + mutable_root ? mutable_root->GetMutableDictFor("AcroForm") : nullptr; + if (!mutable_form_dict) { + return false; + } + mutable_dr_font_dict = + mutable_form_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor( + "Font"); + dr_dict = mutable_form_dict->GetDictFor("DR"); + } font_dict = GetFontFromDrFontDictOrGenerateFallback( - doc, - pdfium::WrapRetain(const_cast(dr_font_dict.Get())), - font_name); + doc, mutable_dr_font_dict.Get(), font_name); } else { - font_dict = - GetFontFromDrFontDictOrDirectFallback(dr_font_dict.Get(), font_name); + font_dict = dr_font_dict ? GetFontFromDrFontDictOrDirectFallback( + dr_font_dict.Get(), font_name) + : GenerateDirectFallbackFontDict(); } auto* doc_page_data = CPDF_DocPageData::FromDocument(doc); RetainPtr default_font = doc_page_data->GetFont(font_dict); if (!default_font) { return false; } + const bool use_registered_font_map = + target->IsPersistent() && + (CFX_FontRegistry::HasFallbackFonts() || + CPDF_AnnotFontSubset::GetRegisteredFontIdFromMarkerFontDict( + font_dict.Get()) + .has_value()); const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); @@ -3018,26 +3440,55 @@ bool GenerateFormAPToTarget(APGenerationTarget* target, std::move(resource_font_dict)); } + auto generate_form_stream = [&](CPVT_VariableText::Provider& provider, + fxcrt::ostringstream& app_stream) { + switch (type) { + case CPDF_GenerateAP::kTextField: + GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), provider, + value_override); + break; + case CPDF_GenerateAP::kComboBox: + GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), provider, + value_override); + break; + case CPDF_GenerateAP::kListBox: + GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, + default_appearance_info.value(), provider); + break; + } + }; + + if (use_registered_font_map) { + // EmbedPDF: form widgets need the same registered fallback/subset path as + // FreeText when their value/options contain glyphs outside the DA font. + // Keep the old CPVT_FontMap path unless a registered font is actually + // involved so existing form AP output remains stable by default. + CPDF_AnnotFontMap map(doc, std::move(default_font), font_name, + /*allow_registered_fallbacks=*/true); + CPVT_VariableText::Provider provider(&map); + + fxcrt::ostringstream app_stream; + generate_form_stream(provider, app_stream); + + normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); + RetainPtr stream_dict = normal_stream->GetMutableDict(); + stream_dict->SetMatrixFor("Matrix", dims.matrix); + stream_dict->SetRectFor("BBox", dims.bbox); + RetainPtr stream_resources = + stream_dict->GetOrCreateDictFor("Resources"); + stream_resources->SetFor("Font", map.CreateFontResourceDict()); + return true; + } + RetainPtr ephemeral_resources_dict = resources_dict; CPVT_FontMap map(doc, std::move(resources_dict), std::move(default_font), font_name); CPVT_VariableText::Provider provider(&map); fxcrt::ostringstream app_stream; - switch (type) { - case CPDF_GenerateAP::kTextField: - GenerateTextFieldFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_GenerateAP::kComboBox: - GenerateComboBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - case CPDF_GenerateAP::kListBox: - GenerateListBoxFormAP(app_stream, annot_dict, dims.bbox, - default_appearance_info.value(), provider); - break; - } + generate_form_stream(provider, app_stream); if (!target->IsPersistent()) { return GenerateAPDict( @@ -3067,7 +3518,17 @@ void CPDF_GenerateAP::GenerateFormAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, FormType type) { APGenerationTarget target{doc, annot_dict}; - GenerateFormAPToTarget(&target, annot_dict, type); + GenerateFormAPToTarget(&target, annot_dict, type, nullptr); +} + +// static +bool CPDF_GenerateAP::GenerateFormAPWithValueOverride( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + FormType type, + const WideString& value_override) { + APGenerationTarget target{doc, annot_dict}; + return GenerateFormAPToTarget(&target, annot_dict, type, &value_override); } // static @@ -3077,7 +3538,7 @@ CPDF_GenerateAP::GenerateEphemeralFormAP(CPDF_Document* doc, FormType type) { APGenerationTarget target{doc, nullptr}; if (!GenerateFormAPToTarget(&target, const_cast(annot_dict), - type)) { + type, nullptr)) { return std::nullopt; } return GeneratedAP{std::move(target.normal_stream)}; @@ -3426,6 +3887,8 @@ bool CPDF_GenerateAP::GenerateAnnotAP(CPDF_Document* doc, return GeneratePopupAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::TEXT: return GenerateTextAP(doc, annot_dict, blend_name); + case CPDF_Annot::Subtype::FILEATTACHMENT: + return GenerateFileAttachmentAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::LINK: return GenerateLinkAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::REDACT: @@ -3487,6 +3950,26 @@ bool CPDF_GenerateAP::CanGenerateEphemeralAnnotAP(CPDF_Annot::Subtype subtype) { return SupportsEphemeralAnnotAP(subtype); } +// static +RetainPtr CPDF_GenerateAP::BuildRedactOverlayForm( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict) { + if (!doc || !annot_dict) { + return nullptr; + } + fxcrt::ostringstream ops; + RetainPtr font_resources; + if (!AppendRedactOverlayOps(doc, annot_dict, ops, &font_resources)) { + return nullptr; + } + RetainPtr resources; + if (font_resources) { + resources = GenerateResourcesDict(doc, nullptr, std::move(font_resources)); + } + return MakeRedactFormStream(doc, GetRedactOverlayBBox(annot_dict), + std::move(resources), &ops); +} + // static bool CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( CPDF_Document* doc, @@ -3597,3 +4080,32 @@ bool CPDF_GenerateAP::UpdateDefaultAppearance(CPDF_Document* doc, annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); return true; } + +bool CPDF_GenerateAP::UpdateDefaultAppearanceRegisteredFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + CFX_FontRegistry::FontId font_id, + float font_size, + const CFX_Color& color) { + // EmbedPDF: allow FreeText DA to reference a registered runtime font. The DA + // stores a lightweight marker resource; actual subset embedding happens when + // AP generation knows the characters used by this annotation/layer. + if (!doc || !annot_dict || !CFX_FontRegistry::IsValidFont(font_id)) { + return false; + } + + ByteString resource_key; + if (!CPDF_AnnotFontMap::EnsureRegisteredFontMarkerInDocument(doc, font_id, + &resource_key) || + resource_key.IsEmpty()) { + return false; + } + + ByteString da_font_part = StringFromFontNameAndSize(resource_key, font_size); + ByteString da_color_part = GenerateColorAP(color, PaintOperation::kFill); + da_color_part.TrimBack('\n'); + da_font_part.TrimBack('\n'); + + annot_dict->SetNewFor("DA", da_color_part + " " + da_font_part); + return true; +} diff --git a/core/fpdfdoc/cpdf_generateap.h b/core/fpdfdoc/cpdf_generateap.h index c0b4cc1f4c..250fc6b372 100644 --- a/core/fpdfdoc/cpdf_generateap.h +++ b/core/fpdfdoc/cpdf_generateap.h @@ -10,9 +10,12 @@ #include #include "core/fpdfdoc/cpdf_annot.h" +#include "core/fxcrt/widestring.h" +#include "core/fxge/cfx_fontregistry.h" class CPDF_Dictionary; class CPDF_Document; +class CPDF_Stream; struct CFX_Color; enum class BlendMode; @@ -24,6 +27,13 @@ class CPDF_GenerateAP { CPDF_Dictionary* pAnnotDict, FormType type); + // EmbedPDF: regenerate a text/combo widget appearance from display text + // without changing the field's semantic /V value. + static bool GenerateFormAPWithValueOverride(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + FormType type, + const WideString& value_override); + static void GenerateCheckboxFormAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict); @@ -63,6 +73,17 @@ class CPDF_GenerateAP { static bool CanGenerateEphemeralAnnotAP(CPDF_Annot::Subtype subtype); + // EmbedPDF: build the final redaction overlay for a /Redact annotation as + // an indirect Form XObject: opaque /IC fill plus the /OverlayText label per + // /DA, /Q and /Repeat. The single source of truth for what an applied + // redaction looks like — marking-stage AP generation bakes it as the R/D + // and /RO streams, and the apply path synthesizes it when a file carries no + // pre-baked /RO. Returns null when the annotation defines neither fill nor + // label. + static RetainPtr BuildRedactOverlayForm( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict); + static bool GenerateDefaultAppearanceWithColor(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const CFX_Color& color); @@ -73,6 +94,15 @@ class CPDF_GenerateAP { float font_size, const CFX_Color& color); + // EmbedPDF: Set FreeText DA to a registered runtime font. The actual AP path + // later embeds a subset for only the characters used by the annotation/layer. + static bool UpdateDefaultAppearanceRegisteredFont( + CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + CFX_FontRegistry::FontId font_id, + float font_size, + const CFX_Color& color); + CPDF_GenerateAP() = delete; CPDF_GenerateAP(const CPDF_GenerateAP&) = delete; CPDF_GenerateAP& operator=(const CPDF_GenerateAP&) = delete; diff --git a/core/fpdfdoc/cpdf_generateap_unittest.cpp b/core/fpdfdoc/cpdf_generateap_unittest.cpp index 635e1533c3..10028c5336 100644 --- a/core/fpdfdoc/cpdf_generateap_unittest.cpp +++ b/core/fpdfdoc/cpdf_generateap_unittest.cpp @@ -270,7 +270,9 @@ TEST_F(CPDFGenerateAPTest, GenerateEphemeralInkAPDoesNotInflateAnnotRect) { EXPECT_EQ(0u, generated->normal_stream->GetObjNum()); EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); EXPECT_EQ(original_rect, annot_dict->GetRectFor(pdfium::annotation::kRect)); - EXPECT_EQ(CFX_FloatRect(-2, -2, 12, 12), + // The ephemeral BBox minimally encloses both the authored /Rect and the + // stroked ink: points 1..9 inflated by half the width (2). + EXPECT_EQ(CFX_FloatRect(-1, -1, 11, 11), generated->normal_stream->GetDict()->GetRectFor("BBox")); EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); } diff --git a/core/fpdfdoc/cpdf_interactiveform.cpp b/core/fpdfdoc/cpdf_interactiveform.cpp index 4f85fde2db..22078d81d3 100644 --- a/core/fpdfdoc/cpdf_interactiveform.cpp +++ b/core/fpdfdoc/cpdf_interactiveform.cpp @@ -856,6 +856,20 @@ CPDF_InteractiveForm::GetControlsForField(const CPDF_FormField* field) { return control_lists_[pdfium::WrapUnowned(field)]; } +// EmbedPDF: layer documents resolve references held by frozen base objects +// through the base holder, which returns stale instances once an object has +// been promoted into the layer. Rebinding by object number restores the +// document's current view. Identity on plain documents. See header. +RetainPtr CPDF_InteractiveForm::ResolveCurrentDict( + RetainPtr dict) const { + if (!dict || dict->GetObjNum() == 0) { + return dict; + } + RetainPtr current = + ToDictionary(document_->GetIndirectObject(dict->GetObjNum())); + return current ? current : dict; +} + void CPDF_InteractiveForm::LoadField( RetainPtr field_dict, int nLevel) { @@ -865,6 +879,9 @@ void CPDF_InteractiveForm::LoadField( if (!field_dict) { return; } + // EmbedPDF: bind to the document's current view so layer promotions win + // over frozen base instances reached through base-held references. + field_dict = ResolveCurrentDict(std::move(field_dict)); uint32_t dwParentObjNum = field_dict->GetObjNum(); RetainPtr kids = @@ -907,6 +924,51 @@ void CPDF_InteractiveForm::FixPageFields(CPDF_Page* page) { } } +// EmbedPDF: recover form fields that are reachable from a page's /Annots +// array but missing from the /AcroForm /Fields tree (a common producer +// bug); used by the EPDFForm_* model build instead of the page-load hook +// FixPageFields(), which requires an expensive CPDF_Page. See header. +void CPDF_InteractiveForm::ReconcileWidget( + RetainPtr widget_dict) { + widget_dict = ResolveCurrentDict(std::move(widget_dict)); + if (!widget_dict) { + return; + } + if (GetControlByDict(widget_dict.Get())) { + return; + } + + // A widget that carries no field type anywhere on its /Parent chain is + // not a form control; leave it alone. + if (!CPDF_FormField::GetFieldAttrForDict(widget_dict.Get(), + pdfium::form_fields::kFT)) { + return; + } + + // Climb to the field root, cycle-guarded, so every sibling widget of the + // same field lands on one logical field. + RetainPtr root = widget_dict; + std::vector visited = {root.Get()}; + for (int i = 0; i < kMaxRecursion; ++i) { + RetainPtr parent = + ResolveCurrentDict(root->GetDictFor(pdfium::form_fields::kParent)); + if (!parent || pdfium::Contains(visited, parent.Get())) { + break; + } + visited.push_back(parent.Get()); + root = std::move(parent); + } + LoadField(root, 0); + + // If the widget is still unlinked (e.g. its parent does not list it in + // /Kids), load it directly. AddTerminalField() resolves the owning field + // through the inheritance-aware attribute lookup, so the control still + // attaches to the field with the correct fully qualified name. + if (!GetControlByDict(widget_dict.Get())) { + LoadField(std::move(widget_dict), 0); + } +} + void CPDF_InteractiveForm::AddTerminalField( RetainPtr field_dict) { RetainPtr field_storage_dict = field_dict; @@ -917,7 +979,10 @@ void CPDF_InteractiveForm::AddTerminalField( field_storage_dict.Reset(); if (kids) { for (size_t i = 0; i < kids->size(); ++i) { - RetainPtr kid = kids->GetDictAt(i); + // EmbedPDF: rebind to the layer's current view (see + // ResolveCurrentDict). + RetainPtr kid = + ResolveCurrentDict(kids->GetDictAt(i)); if (CPDF_FormField::GetFieldAttrForDict(kid.Get(), pdfium::form_fields::kFT)) { field_storage_dict = std::move(kid); @@ -957,7 +1022,9 @@ void CPDF_InteractiveForm::AddTerminalField( return; } for (size_t i = 0; i < kids->size(); i++) { - RetainPtr kid = kids->GetDictAt(i); + // EmbedPDF: rebind to the layer's current view (see ResolveCurrentDict). + RetainPtr kid = + ResolveCurrentDict(kids->GetDictAt(i)); if (kid && kid->GetNameFor("Subtype") == "Widget") { AddControl(field, pdfium::WrapRetain(const_cast(kid.Get()))); @@ -1019,20 +1086,22 @@ bool CPDF_InteractiveForm::CheckRequiredFields( } std::unique_ptr CPDF_InteractiveForm::ExportToFDF( - const WideString& pdf_path) const { + const WideString& pdf_path, + bool skip_empty_required) const { std::vector fields; CFieldTree::Node* pRoot = field_tree_->GetRoot(); const size_t nCount = pRoot->CountFields(); for (size_t i = 0; i < nCount; ++i) { fields.push_back(pRoot->GetFieldAtIndex(i)); } - return ExportToFDF(pdf_path, fields, true); + return ExportToFDF(pdf_path, fields, true, skip_empty_required); } std::unique_ptr CPDF_InteractiveForm::ExportToFDF( const WideString& pdf_path, const std::vector& fields, - bool bIncludeOrExclude) const { + bool bIncludeOrExclude, + bool skip_empty_required) const { std::unique_ptr doc = CFDF_Document::CreateNewDoc(); if (!doc) { return nullptr; @@ -1067,11 +1136,16 @@ std::unique_ptr CPDF_InteractiveForm::ExportToFDF( continue; } - if ((dwFlags & pdfium::form_flags::kRequired) != 0 && - field->GetFieldDict() - ->GetByteStringFor(pdfium::form_fields::kV) - .IsEmpty()) { - continue; + // EmbedPDF: |skip_empty_required| makes the historic omit-empty-required + // submission behavior optional so interchange exports stay faithful. + if (skip_empty_required && (dwFlags & pdfium::form_flags::kRequired) != 0) { + RetainPtr value = + field->GetFieldAttr(pdfium::form_fields::kV); + if (!value || value->IsNull() || + (value->IsArray() ? value->AsArray()->IsEmpty() + : value->GetString().IsEmpty())) { + continue; + } } WideString fullname = diff --git a/core/fpdfdoc/cpdf_interactiveform.h b/core/fpdfdoc/cpdf_interactiveform.h index 3de7645fd2..8e05e56ef7 100644 --- a/core/fpdfdoc/cpdf_interactiveform.h +++ b/core/fpdfdoc/cpdf_interactiveform.h @@ -82,11 +82,17 @@ class CPDF_InteractiveForm { bool CheckRequiredFields(const std::vector* fields, bool bIncludeOrExclude) const; - std::unique_ptr ExportToFDF(const WideString& pdf_path) const; + // EmbedPDF: |skip_empty_required| preserves the historic submission + // behavior of omitting required fields whose value is empty; pass false + // for faithful interchange exports. + std::unique_ptr ExportToFDF( + const WideString& pdf_path, + bool skip_empty_required = true) const; std::unique_ptr ExportToFDF( const WideString& pdf_path, const std::vector& fields, - bool bIncludeOrExclude) const; + bool bIncludeOrExclude, + bool skip_empty_required = true) const; void ResetForm(); void ResetForm(pdfium::span fields, bool bIncludeOrExclude); @@ -94,6 +100,13 @@ class CPDF_InteractiveForm { void SetNotifierIface(NotifierIface* notify); void FixPageFields(CPDF_Page* page); + // EmbedPDF: Load a widget annotation that is reachable from a page's + // /Annots array but not from the /AcroForm /Fields tree. Climbs the + // /Parent chain to the field root first, so sibling widgets of a + // partially linked field reconcile onto a single logical field. + // In-memory reconciliation only; never writes to the document. + void ReconcileWidget(RetainPtr widget_dict); + // Wrap callbacks thru NotifierIface. bool NotifyBeforeValueChange(CPDF_FormField* field, const WideString& value); void NotifyAfterValueChange(CPDF_FormField* field); @@ -108,6 +121,13 @@ class CPDF_InteractiveForm { CPDF_Document* document() { return document_; } private: + // EmbedPDF: Rebind an indirect dictionary to the document's current view + // of that object number. On layer documents this returns the promoted + // clone when one exists; references held by frozen base objects resolve + // to stale frozen instances otherwise. Identity on plain documents. + RetainPtr ResolveCurrentDict( + RetainPtr dict) const; + void LoadField(RetainPtr field_dict, int nLevel); void AddTerminalField(RetainPtr field_dict); CPDF_FormControl* AddControl(CPDF_FormField* field, diff --git a/core/fpdfdoc/cpvt_fontmap.cpp b/core/fpdfdoc/cpvt_fontmap.cpp index 7f3ef705cc..cb44b89b34 100644 --- a/core/fpdfdoc/cpvt_fontmap.cpp +++ b/core/fpdfdoc/cpvt_fontmap.cpp @@ -16,7 +16,7 @@ #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fxcrt/check.h" #include "core/fxcrt/fx_codepage.h" -#include "core/fxcrt/notreached.h" +#include "core/fxcrt/numerics/safe_conversions.h" CPVT_FontMap::CPVT_FontMap(CPDF_Document* doc, RetainPtr pResDict, @@ -81,14 +81,48 @@ ByteString CPVT_FontMap::GetPDFFontAlias(int32_t nFontIndex) { int32_t CPVT_FontMap::GetWordFontIndex(uint16_t word, FX_Charset charset, int32_t nFontIndex) { - NOTREACHED(); + // EmbedPDF: preserve upstream shared AP behavior for the default form/popup + // font map: choose the DA font first, then PDFium's native annotation font, + // based only on CharCodeFromUnicode(). Stricter glyph checks live in + // CPDF_AnnotFontMap, which is used only by registered FreeText fonts. + if (RetainPtr pDefFont = GetPDFFont(0)) { + if (pDefFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { + return 0; + } + } + if (RetainPtr pSysFont = GetPDFFont(1)) { + if (pSysFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { + return 1; + } + } + return -1; } int32_t CPVT_FontMap::CharCodeFromUnicode(int32_t nFontIndex, uint16_t word) { - NOTREACHED(); + // EmbedPDF: default implementation is equivalent to the old shared AP path's + // direct pdf_font->CharCodeFromUnicode() call. The hook exists so specialized + // font maps can return registered-font subset glyph ids. + RetainPtr font = GetPDFFont(nFontIndex); + if (!font) { + return -1; + } + uint32_t charcode = font->CharCodeFromUnicode(word); + if (charcode == CPDF_Font::kInvalidCharCode || + !pdfium::IsValueInRangeForNumericType(charcode)) { + return -1; + } + return static_cast(charcode); } FX_Charset CPVT_FontMap::CharSetFromUnicode(uint16_t word, FX_Charset nOldCharset) { - NOTREACHED(); + // EmbedPDF: provide a conservative default implementation for the PVT + // provider hook; registered annotation maps may override as needed. + if (word < 0x7F) { + return FX_Charset::kANSI; + } + if (nOldCharset != FX_Charset::kDefault) { + return nOldCharset; + } + return CFX_Font::GetCharSetFromUnicode(word); } diff --git a/core/fpdfdoc/cpvt_variabletext.cpp b/core/fpdfdoc/cpvt_variabletext.cpp index 60cb93a41d..63b0be70f3 100644 --- a/core/fpdfdoc/cpvt_variabletext.cpp +++ b/core/fpdfdoc/cpvt_variabletext.cpp @@ -68,17 +68,9 @@ int32_t CPVT_VariableText::Provider::GetTypeDescent(int32_t nFontIndex) { int32_t CPVT_VariableText::Provider::GetWordFontIndex(uint16_t word, FX_Charset charset, int32_t nFontIndex) { - if (RetainPtr pDefFont = font_map_->GetPDFFont(0)) { - if (pDefFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { - return 0; - } - } - if (RetainPtr pSysFont = font_map_->GetPDFFont(1)) { - if (pSysFont->CharCodeFromUnicode(word) != CPDF_Font::kInvalidCharCode) { - return 1; - } - } - return -1; + // EmbedPDF: delegate font choice to IPVT_FontMap so CPDF_AnnotFontMap can + // route individual FreeText glyphs to registered fallback fonts. + return font_map_->GetWordFontIndex(word, charset, nFontIndex); } int32_t CPVT_VariableText::Provider::GetDefaultFontIndex() { diff --git a/core/fxcodec/flate/flatemodule.cpp b/core/fxcodec/flate/flatemodule.cpp index 8ceb2dd72c..737fd54d83 100644 --- a/core/fxcodec/flate/flatemodule.cpp +++ b/core/fxcodec/flate/flatemodule.cpp @@ -842,6 +842,48 @@ DataAndBytesConsumed FlateModule::FlateOrLZWDecode( } } +// static +FlateModule::SinkDecodeStatus FlateModule::FlateDecodeToSink( + pdfium::span src_span, + uint64_t max_decoded_bytes, + const std::function)>& sink, + uint64_t* total_out) { + *total_out = 0; + std::unique_ptr context(FlateInit()); + if (!context) { + return SinkDecodeStatus::kSinkError; + } + + FlateInput(context.get(), src_span); + + // One reusable chunk keeps peak memory bounded no matter how large the + // decoded output is. + static constexpr uint32_t kChunkSize = 1 << 20; // 1 MiB + DataVector chunk(kChunkSize); + uint64_t total = 0; + while (true) { + const bool ret = FlateOutput(context.get(), chunk); + const uint32_t avail_buf_size = FlateGetAvailOut(context.get()); + const uint32_t produced = kChunkSize - avail_buf_size; + if (produced > 0) { + total += produced; + if (max_decoded_bytes && total > max_decoded_bytes) { + *total_out = total - produced; + return SinkDecodeStatus::kLimitExceeded; + } + if (!sink(pdfium::span(chunk).first(produced))) { + *total_out = total - produced; + return SinkDecodeStatus::kSinkError; + } + } + if (!ret || avail_buf_size != 0) { + break; + } + } + *total_out = total; + return SinkDecodeStatus::kSuccess; +} + // static DataVector FlateModule::Encode(pdfium::span src_span) { FX_SAFE_SIZE_T safe_dest_size = src_span.size(); diff --git a/core/fxcodec/flate/flatemodule.h b/core/fxcodec/flate/flatemodule.h index cf56ed5fc9..3e85480c03 100644 --- a/core/fxcodec/flate/flatemodule.h +++ b/core/fxcodec/flate/flatemodule.h @@ -9,6 +9,7 @@ #include +#include #include #include "core/fxcodec/data_and_bytes_consumed.h" @@ -42,6 +43,28 @@ class FlateModule { int Columns, uint32_t estimated_size); + // EmbedPDF: outcome of FlateDecodeToSink(). + enum class SinkDecodeStatus : uint8_t { + kSuccess, + kLimitExceeded, + kSinkError, + }; + + // EmbedPDF: inflates |src_span| into |sink| one bounded chunk at a time, + // without materializing the full decoded output. Peak memory is one chunk + // regardless of the decoded size. |sink| returns false to abort. + // |max_decoded_bytes| of 0 means unlimited; when the decoded output would + // exceed it, decoding stops with kLimitExceeded (|sink| may already have + // received earlier chunks). On return, |*total_out| holds the number of + // bytes handed to |sink|. Termination semantics match FlateUncompress(): + // corrupt trailing data yields the successfully inflated prefix rather + // than an error, and an empty decoded stream is a valid kSuccess result. + static SinkDecodeStatus FlateDecodeToSink( + pdfium::span src_span, + uint64_t max_decoded_bytes, + const std::function)>& sink, + uint64_t* total_out); + static DataVector Encode(pdfium::span src_span); FlateModule() = delete; diff --git a/core/fxcodec/flate/flatemodule_unittest.cpp b/core/fxcodec/flate/flatemodule_unittest.cpp index 480b8ba9a5..fa6161ad0f 100644 --- a/core/fxcodec/flate/flatemodule_unittest.cpp +++ b/core/fxcodec/flate/flatemodule_unittest.cpp @@ -72,3 +72,117 @@ TEST(FlateModule, Encode) { ++i; } } + +namespace { + +DataVector MakePatternedData(size_t size) { + DataVector data(size); + for (size_t i = 0; i < size; ++i) { + data[i] = static_cast((i * 31 + i / 997) & 0xff); + } + return data; +} + +} // namespace + +TEST(FlateModule, DecodeToSinkRoundTrip) { + // Larger than the sink chunk size (1 MiB) so multiple chunks are emitted. + const DataVector original = + MakePatternedData(3 * 1024 * 1024 + 123); + const DataVector compressed = FlateModule::Encode(original); + ASSERT_FALSE(compressed.empty()); + + DataVector decoded; + size_t sink_calls = 0; + uint64_t total = 0; + FlateModule::SinkDecodeStatus status = FlateModule::FlateDecodeToSink( + compressed, /*max_decoded_bytes=*/0, + [&](pdfium::span chunk) { + ++sink_calls; + decoded.insert(decoded.end(), chunk.begin(), chunk.end()); + return true; + }, + &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kSuccess, status); + EXPECT_EQ(original.size(), total); + EXPECT_GE(sink_calls, 3u); + EXPECT_TRUE(decoded == original); +} + +TEST(FlateModule, DecodeToSinkEmptyStream) { + const DataVector compressed = FlateModule::Encode({}); + size_t sink_calls = 0; + uint64_t total = 1; // Poison; must be reset to 0. + FlateModule::SinkDecodeStatus status = FlateModule::FlateDecodeToSink( + compressed, /*max_decoded_bytes=*/0, + [&](pdfium::span) { + ++sink_calls; + return true; + }, + &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kSuccess, status); + EXPECT_EQ(0u, total); + EXPECT_EQ(0u, sink_calls); +} + +TEST(FlateModule, DecodeToSinkLimitExceeded) { + const DataVector original = MakePatternedData(3 * 1024 * 1024); + const DataVector compressed = FlateModule::Encode(original); + + // A tiny cap trips before the first full chunk can be delivered. + size_t sink_calls = 0; + uint64_t total = 0; + FlateModule::SinkDecodeStatus status = FlateModule::FlateDecodeToSink( + compressed, /*max_decoded_bytes=*/100, + [&](pdfium::span) { + ++sink_calls; + return true; + }, + &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kLimitExceeded, status); + EXPECT_EQ(0u, total); + EXPECT_EQ(0u, sink_calls); + + // A one-chunk cap delivers the first chunk, then trips on the second. + uint64_t delivered = 0; + status = FlateModule::FlateDecodeToSink( + compressed, /*max_decoded_bytes=*/1024 * 1024, + [&](pdfium::span chunk) { + delivered += chunk.size(); + return true; + }, + &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kLimitExceeded, status); + EXPECT_EQ(delivered, total); + EXPECT_LE(total, 1024u * 1024u); +} + +TEST(FlateModule, DecodeToSinkSinkAbort) { + const DataVector original = MakePatternedData(64); + const DataVector compressed = FlateModule::Encode(original); + + uint64_t total = 0; + FlateModule::SinkDecodeStatus status = FlateModule::FlateDecodeToSink( + compressed, /*max_decoded_bytes=*/0, + [](pdfium::span) { return false; }, &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kSinkError, status); + EXPECT_EQ(0u, total); +} + +TEST(FlateModule, DecodeToSinkGarbageInput) { + // Matches FlateOrLZWDecode(): undecodable input yields the successfully + // inflated prefix — here, nothing — rather than a distinct error. + static const char kGarbage[] = "preposterous nonsense"; + size_t sink_calls = 0; + uint64_t total = 0; + FlateModule::SinkDecodeStatus status = FlateModule::FlateDecodeToSink( + pdfium::as_bytes(pdfium::span(kGarbage)), /*max_decoded_bytes=*/0, + [&](pdfium::span) { + ++sink_calls; + return true; + }, + &total); + EXPECT_EQ(FlateModule::SinkDecodeStatus::kSuccess, status); + EXPECT_EQ(0u, total); + EXPECT_EQ(0u, sink_calls); +} diff --git a/core/fxcrt/BUILD.gn b/core/fxcrt/BUILD.gn index 45eec12371..09c2f00b33 100644 --- a/core/fxcrt/BUILD.gn +++ b/core/fxcrt/BUILD.gn @@ -179,12 +179,13 @@ source_set("fxcrt") { "win/win_util.h", ] } - if (pdf_enable_xfa) { - sources += [ - "cfx_memorystream.cpp", - "cfx_memorystream.h", - ] - } + # EmbedPDF: CFX_MemoryStream was XFA-gated upstream because XFA was its + # only consumer; the EPDFForm_* XFDF serializer (fpdfsdk/epdf_form.cpp) + # uses it as the in-memory write target, so build it unconditionally. + sources += [ + "cfx_memorystream.cpp", + "cfx_memorystream.h", + ] } source_set("test_support") { diff --git a/core/fxcrt/xml/cfx_xmlelement.cpp b/core/fxcrt/xml/cfx_xmlelement.cpp index 185bd93927..bedd7e9315 100644 --- a/core/fxcrt/xml/cfx_xmlelement.cpp +++ b/core/fxcrt/xml/cfx_xmlelement.cpp @@ -109,6 +109,37 @@ void CFX_XMLElement::Save( pXMLStream->WriteString(">\n"); } +// EmbedPDF: identical to Save() minus the injected newlines, so text +// content round-trips byte-exact under xml:space="preserve" (XFDF form +// data values). See CFX_XMLNode::SaveCompact(). +void CFX_XMLElement::SaveCompact( + const RetainPtr& pXMLStream) { + ByteString bsNameEncoded = name_.ToUTF8(); + + pXMLStream->WriteString("<"); + pXMLStream->WriteString(bsNameEncoded.AsStringView()); + + for (const auto& it : attrs_) { + pXMLStream->WriteString( + AttributeToString(it.first, it.second).ToUTF8().AsStringView()); + } + + if (!GetFirstChild()) { + pXMLStream->WriteString(" />"); + return; + } + + pXMLStream->WriteString(">"); + + for (CFX_XMLNode* pChild = GetFirstChild(); pChild; + pChild = pChild->GetNextSibling()) { + pChild->SaveCompact(pXMLStream); + } + pXMLStream->WriteString("WriteString(bsNameEncoded.AsStringView()); + pXMLStream->WriteString(">"); +} + CFX_XMLElement* CFX_XMLElement::GetFirstChildNamed(WideStringView name) const { return GetNthChildNamed(name, 0); } diff --git a/core/fxcrt/xml/cfx_xmlelement.h b/core/fxcrt/xml/cfx_xmlelement.h index d48bf9fe01..e7f1d6a1f3 100644 --- a/core/fxcrt/xml/cfx_xmlelement.h +++ b/core/fxcrt/xml/cfx_xmlelement.h @@ -23,6 +23,9 @@ class CFX_XMLElement final : public CFX_XMLNode { Type GetType() const override; CFX_XMLNode* Clone(CFX_XMLDocument* doc) override; void Save(const RetainPtr& pXMLStream) override; + // EmbedPDF: element serialization without formatting whitespace. + void SaveCompact( + const RetainPtr& pXMLStream) override; const WideString& GetName() const { return name_; } diff --git a/core/fxcrt/xml/cfx_xmlnode.cpp b/core/fxcrt/xml/cfx_xmlnode.cpp index c7a4f6641a..97741b73d0 100644 --- a/core/fxcrt/xml/cfx_xmlnode.cpp +++ b/core/fxcrt/xml/cfx_xmlnode.cpp @@ -14,6 +14,12 @@ void CFX_XMLNode::InsertChildNode(CFX_XMLNode* pNode, int32_t index) { InsertBefore(pNode, GetNthChild(index)); } +// EmbedPDF: see header - whitespace-exact serialization entry point. +void CFX_XMLNode::SaveCompact( + const RetainPtr& pXMLStream) { + Save(pXMLStream); +} + CFX_XMLNode* CFX_XMLNode::GetRoot() { CFX_XMLNode* pParent = this; while (pParent->GetParent()) { diff --git a/core/fxcrt/xml/cfx_xmlnode.h b/core/fxcrt/xml/cfx_xmlnode.h index 5934bc7036..8f0b961ab5 100644 --- a/core/fxcrt/xml/cfx_xmlnode.h +++ b/core/fxcrt/xml/cfx_xmlnode.h @@ -29,6 +29,14 @@ class CFX_XMLNode : public TreeNode { virtual CFX_XMLNode* Clone(CFX_XMLDocument* doc) = 0; virtual void Save(const RetainPtr& pXMLStream) = 0; + // EmbedPDF: whitespace-exact serialization for xml:space="preserve" + // payloads (XFDF form data). Save() pretty-prints by injecting newlines + // into element content, which corrupts text values on round-trip. Kept + // as a separate method so existing Save() output (XFA checksummed XML) + // is untouched. Default forwards to Save(); CFX_XMLElement overrides. + virtual void SaveCompact( + const RetainPtr& pXMLStream); + CFX_XMLNode* GetRoot(); void InsertChildNode(CFX_XMLNode* pNode, int32_t index); }; diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn index 9604143bd5..4763ea406f 100644 --- a/core/fxge/BUILD.gn +++ b/core/fxge/BUILD.gn @@ -33,6 +33,10 @@ source_set("fxge") { "cfx_font.h", "cfx_fontmapper.cpp", "cfx_fontmapper.h", + # EmbedPDF: runtime font registry shared by page fallback rendering and + # annotation authoring/subset embedding. + "cfx_fontregistry.cpp", + "cfx_fontregistry.h", "cfx_fontmgr.cpp", "cfx_fontmgr.h", "cfx_gemodule.cpp", diff --git a/core/fxge/cfx_fontregistry.cpp b/core/fxge/cfx_fontregistry.cpp new file mode 100644 index 0000000000..36185b0f7b --- /dev/null +++ b/core/fxge/cfx_fontregistry.cpp @@ -0,0 +1,326 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: process/thread-local registry for runtime fonts. Registered fonts +// are used both for page-rendering fallback and for annotation authoring. + +#include "core/fxge/cfx_fontregistry.h" + +#include +#include +#include +#include +#include + +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/epdf_tls.h" +#include "core/fxcrt/fx_codepage.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxcrt/utf16.h" +#include "core/fxge/cfx_face.h" +#include "core/fxge/cfx_font.h" + +namespace { + +struct RegisteredFont { + CFX_FontRegistry::FontId id = CFX_FontRegistry::kInvalidFontId; + ByteString base_font_name; + int weight = pdfium::kFontWeightNormal; + bool italic = false; + std::vector supported_unicodes; + DataVector memory_data; + RetainPtr stream; +}; + +struct RegistryState { + CFX_FontRegistry::FontId next_font_id = 1; + std::vector> fonts; + std::vector fallback_order; +}; + +EPDF_TLS RegistryState* g_registry = nullptr; + +RegistryState* GetRegistry() { + if (!g_registry) { + g_registry = new RegistryState(); + } + return g_registry; +} + +RegisteredFont* GetRegisteredFont(CFX_FontRegistry::FontId font_id) { + if (font_id == CFX_FontRegistry::kInvalidFontId || !g_registry) { + return nullptr; + } + + for (const auto& font : g_registry->fonts) { + if (font && font->id == font_id) { + return font.get(); + } + } + return nullptr; +} + +ByteString NormalizeBaseFontName(ByteString name) { + name.Remove(' '); + return name.IsEmpty() ? ByteString(CFX_Font::kUntitledFontName) : name; +} + +int NormalizeWeight(int weight, const CFX_Font& font) { + if (weight >= 100 && weight <= 900) { + return weight; + } + return font.IsBold() ? pdfium::kFontWeightBold : pdfium::kFontWeightNormal; +} + +bool NormalizeItalic(int italic, const CFX_Font& font) { + if (italic == 0 || italic == 1) { + return italic == 1; + } + return font.IsItalic(); +} + +DataVector ReadStreamToData(IFX_SeekableReadStream* stream) { + if (!stream || stream->GetSize() <= 0 || + !pdfium::IsValueInRangeForNumericType(stream->GetSize())) { + return {}; + } + + DataVector data(pdfium::checked_cast(stream->GetSize())); + if (!stream->ReadBlockAtOffset(pdfium::span(data), /*offset=*/0)) { + return {}; + } + return data; +} + +std::unique_ptr LoadFont(pdfium::span data) { + if (data.empty()) { + return nullptr; + } + + auto font = std::make_unique(); + if (!font->LoadEmbedded(data, /*force_vertical=*/false, /*object_tag=*/0)) { + return nullptr; + } + return font; +} + +std::vector CollectSupportedUnicodes(CFX_Font* font) { + if (!font) { + return {}; + } + + auto char_codes_and_indices = + font->GetCharCodesAndIndices(pdfium::kMaximumSupplementaryCodePoint); + std::vector supported_unicodes; + supported_unicodes.reserve(char_codes_and_indices.size()); + for (const auto& item : char_codes_and_indices) { + if (item.glyph_index != 0) { + supported_unicodes.push_back(item.char_code); + } + } + + std::ranges::sort(supported_unicodes); + supported_unicodes.erase( + std::unique(supported_unicodes.begin(), supported_unicodes.end()), + supported_unicodes.end()); + return supported_unicodes; +} + +CFX_FontRegistry::FontId RegisterLoadedFontSource( + const ByteString& family_name, + int weight, + int italic, + pdfium::span data, + DataVector memory_data, + RetainPtr stream) { + if (data.empty()) { + return CFX_FontRegistry::kInvalidFontId; + } + + RegistryState* registry = GetRegistry(); + if (registry->next_font_id == + std::numeric_limits::max()) { + return CFX_FontRegistry::kInvalidFontId; + } + + std::unique_ptr font = LoadFont(data); + if (!font || !font->HasAnyGlyphs()) { + return CFX_FontRegistry::kInvalidFontId; + } + + std::vector supported_unicodes = + CollectSupportedUnicodes(font.get()); + if (supported_unicodes.empty()) { + return CFX_FontRegistry::kInvalidFontId; + } + + auto registered_font = std::make_unique(); + registered_font->id = registry->next_font_id++; + registered_font->base_font_name = NormalizeBaseFontName( + family_name.IsEmpty() ? font->GetBaseFontName() : family_name); + registered_font->weight = NormalizeWeight(weight, *font); + registered_font->italic = NormalizeItalic(italic, *font); + registered_font->supported_unicodes = std::move(supported_unicodes); + registered_font->memory_data = std::move(memory_data); + registered_font->stream = std::move(stream); + + const CFX_FontRegistry::FontId id = registered_font->id; + registry->fonts.push_back(std::move(registered_font)); + return id; +} + +int StyleScore(const RegisteredFont& font, int weight, bool italic) { + const int weight_score = std::abs(font.weight - weight); + const int italic_score = font.italic == italic ? 0 : 1000; + return weight_score + italic_score; +} + +} // namespace + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::RegisterMemoryFont( + const ByteString& family_name, + int weight, + int italic, + pdfium::span data) { + if (data.empty()) { + return kInvalidFontId; + } + + DataVector memory_data(data.begin(), data.end()); + pdfium::span font_data(memory_data); + return RegisterLoadedFontSource(family_name, weight, italic, font_data, + std::move(memory_data), nullptr); +} + +// static +CFX_FontRegistry::FontId CFX_FontRegistry::RegisterFont( + const ByteString& family_name, + int weight, + int italic, + RetainPtr stream) { + DataVector data = ReadStreamToData(stream.Get()); + return RegisterLoadedFontSource(family_name, weight, italic, + pdfium::span(data), {}, std::move(stream)); +} + +// static +void CFX_FontRegistry::ClearRegisteredFonts() { + if (!g_registry) { + return; + } + g_registry->fallback_order.clear(); + g_registry->fonts.clear(); + // EmbedPDF: do not reset next_font_id. Documents can keep registered-font + // marker resources after ClearRegisteredFonts(); reusing ids could make an + // old marker resolve to a different font registered later in the same + // runtime/thread. +} + +// static +bool CFX_FontRegistry::AddFallbackFont(FontId font_id) { + if (!IsValidFont(font_id)) { + return false; + } + + RegistryState* registry = GetRegistry(); + if (pdfium::Contains(registry->fallback_order, font_id)) { + return true; + } + registry->fallback_order.push_back(font_id); + return true; +} + +// static +void CFX_FontRegistry::ClearFallbackFonts() { + if (!g_registry) { + return; + } + g_registry->fallback_order.clear(); +} + +// static +bool CFX_FontRegistry::HasFallbackFonts() { + return g_registry && !g_registry->fallback_order.empty(); +} + +// static +bool CFX_FontRegistry::IsValidFont(FontId font_id) { + return GetRegisteredFont(font_id) != nullptr; +} + +// static +ByteString CFX_FontRegistry::GetBaseFontName(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? font->base_font_name : ByteString(); +} + +// static +int CFX_FontRegistry::GetStyleWeight(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font ? font->weight : pdfium::kFontWeightNormal; +} + +// static +bool CFX_FontRegistry::IsStyleItalic(FontId font_id) { + RegisteredFont* font = GetRegisteredFont(font_id); + return font && font->italic; +} + +// static +bool CFX_FontRegistry::SupportsUnicode(FontId font_id, uint32_t unicode) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font) { + return false; + } + return std::ranges::binary_search(font->supported_unicodes, unicode); +} + +// static +std::optional +CFX_FontRegistry::FindFallbackFont(uint32_t unicode, int weight, bool italic) { + if (!g_registry) { + return std::nullopt; + } + + std::optional best_font_id; + int best_score = std::numeric_limits::max(); + for (FontId font_id : g_registry->fallback_order) { + RegisteredFont* font = GetRegisteredFont(font_id); + if (!font || !SupportsUnicode(font_id, unicode)) { + continue; + } + + const int score = StyleScore(*font, weight, italic); + if (!best_font_id.has_value() || score < best_score) { + best_font_id = font_id; + best_score = score; + } + } + return best_font_id; +} + +// static +std::unique_ptr CFX_FontRegistry::CreateFont(FontId font_id) { + RegisteredFont* registered_font = GetRegisteredFont(font_id); + if (!registered_font) { + return nullptr; + } + + if (!registered_font->memory_data.empty()) { + return LoadFont(pdfium::span(registered_font->memory_data)); + } + + DataVector data = ReadStreamToData(registered_font->stream.Get()); + return LoadFont(pdfium::span(data)); +} + +// static +void CFX_FontRegistry::DestroyGlobals() { + delete g_registry; + g_registry = nullptr; +} diff --git a/core/fxge/cfx_fontregistry.h b/core/fxge/cfx_fontregistry.h new file mode 100644 index 0000000000..736934d662 --- /dev/null +++ b/core/fxge/cfx_fontregistry.h @@ -0,0 +1,56 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// EmbedPDF: fork-owned runtime font registry shared by page fallback rendering +// and annotation appearance/subset embedding. + +#ifndef CORE_FXGE_CFX_FONTREGISTRY_H_ +#define CORE_FXGE_CFX_FONTREGISTRY_H_ + +#include + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" + +class CFX_Font; +class IFX_SeekableReadStream; + +class CFX_FontRegistry { + public: + using FontId = uint32_t; + + static constexpr FontId kInvalidFontId = 0; + + static FontId RegisterMemoryFont(const ByteString& family_name, + int weight, + int italic, + pdfium::span data); + static FontId RegisterFont(const ByteString& family_name, + int weight, + int italic, + RetainPtr stream); + static void ClearRegisteredFonts(); + + static bool AddFallbackFont(FontId font_id); + static void ClearFallbackFonts(); + static bool HasFallbackFonts(); + + static bool IsValidFont(FontId font_id); + static ByteString GetBaseFontName(FontId font_id); + static int GetStyleWeight(FontId font_id); + static bool IsStyleItalic(FontId font_id); + static bool SupportsUnicode(FontId font_id, uint32_t unicode); + static std::optional FindFallbackFont(uint32_t unicode, + int weight, + bool italic); + static std::unique_ptr CreateFont(FontId font_id); + + static void DestroyGlobals(); +}; + +#endif // CORE_FXGE_CFX_FONTREGISTRY_H_ diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 6c8aa9f80a..d387a009ef 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -7,9 +7,20 @@ import("../testing/test.gni") source_set("fpdfsdk") { sources = [ + # EmbedPDF: detached, read-only PDF action models. + "epdf_action.cpp", + "epdf_action_helpers.h", "epdf_base_document.cpp", + # EmbedPDF: implements EPDFFont_* runtime font registration APIs. + "epdf_font.cpp", + # EmbedPDF: session-free AcroForm model snapshot (EPDFForm_* APIs). + "epdf_form.cpp", + # EmbedPDF: layer-safe selective page and annotation flattening. + "epdf_flatten.cpp", "epdf_layer.cpp", "epdf_outline.cpp", + # EmbedPDF: generic namespace-scoped document/page /PieceInfo metadata. + "epdf_pieceinfo.cpp", "epdf_page_content_helpers.cpp", "epdf_page_content_helpers.h", "epdf_png_shim.cpp", @@ -137,6 +148,12 @@ pdfium_embeddertest_source_set("embeddertests") { sources = [ "cpdfsdk_annotiterator_embeddertest.cpp", "cpdfsdk_baannot_embeddertest.cpp", + # EmbedPDF: covers detached action models and owner-specific readers. + "epdf_action_embeddertest.cpp", + # EmbedPDF: covers the EPDFForm_* session-free form APIs. + "epdf_form_embeddertest.cpp", + # EmbedPDF: covers generic document/page /PieceInfo metadata APIs. + "epdf_pieceinfo_embeddertest.cpp", "fpdf_annot_embeddertest.cpp", "fpdf_attachment_embeddertest.cpp", "fpdf_catalog_embeddertest.cpp", diff --git a/fpdfsdk/epdf_action.cpp b/fpdfsdk/epdf_action.cpp new file mode 100644 index 0000000000..45bf73133a --- /dev/null +++ b/fpdfsdk/epdf_action.cpp @@ -0,0 +1,473 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_action.h" + +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfdoc/cpdf_action.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/fx_string_wrappers.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_action_helpers.h" + +namespace epdf { + +namespace { + +constexpr size_t kMaxActionDepth = 64; +constexpr size_t kMaxActionNodes = 1024; +constexpr size_t kMaxJavaScriptCodeUnits = 8 * 1024 * 1024; + +struct ActionNodeRecord { + ByteString subtype; + int type = EPDF_ACTION_TYPE_UNKNOWN; + std::optional javascript; + std::vector next; + // Keeps the node's action dictionary alive for the model's lifetime so + // the EPDFAction_GetNode{Dest,URI,FilePath,Name} payload getters can + // rehydrate a CPDF_Action on demand. Nothing is extracted eagerly: + // building a model costs the same as before this field existed. + RetainPtr dict; +}; + +int NormalizeActionType(CPDF_Action::Type type) { + switch (type) { + case CPDF_Action::Type::kUnknown: + return EPDF_ACTION_TYPE_UNKNOWN; + case CPDF_Action::Type::kGoTo: + return EPDF_ACTION_TYPE_GOTO; + case CPDF_Action::Type::kGoToR: + return EPDF_ACTION_TYPE_GOTO_REMOTE; + case CPDF_Action::Type::kGoToE: + return EPDF_ACTION_TYPE_GOTO_EMBEDDED; + case CPDF_Action::Type::kLaunch: + return EPDF_ACTION_TYPE_LAUNCH; + case CPDF_Action::Type::kThread: + return EPDF_ACTION_TYPE_THREAD; + case CPDF_Action::Type::kURI: + return EPDF_ACTION_TYPE_URI; + case CPDF_Action::Type::kSound: + return EPDF_ACTION_TYPE_SOUND; + case CPDF_Action::Type::kMovie: + return EPDF_ACTION_TYPE_MOVIE; + case CPDF_Action::Type::kHide: + return EPDF_ACTION_TYPE_HIDE; + case CPDF_Action::Type::kNamed: + return EPDF_ACTION_TYPE_NAMED; + case CPDF_Action::Type::kSubmitForm: + return EPDF_ACTION_TYPE_SUBMIT_FORM; + case CPDF_Action::Type::kResetForm: + return EPDF_ACTION_TYPE_RESET_FORM; + case CPDF_Action::Type::kImportData: + return EPDF_ACTION_TYPE_IMPORT_DATA; + case CPDF_Action::Type::kJavaScript: + return EPDF_ACTION_TYPE_JAVASCRIPT; + case CPDF_Action::Type::kSetOCGState: + return EPDF_ACTION_TYPE_SET_OCG_STATE; + case CPDF_Action::Type::kRendition: + return EPDF_ACTION_TYPE_RENDITION; + case CPDF_Action::Type::kTrans: + return EPDF_ACTION_TYPE_TRANSITION; + case CPDF_Action::Type::kGoTo3DView: + return EPDF_ACTION_TYPE_GOTO_3D_VIEW; + } + return EPDF_ACTION_TYPE_UNKNOWN; +} + +} // namespace + +struct ActionModelData { + std::vector nodes; + uint32_t warning_flags = 0; +}; + +namespace { + +std::optional AppendAction( + const CPDF_Action& action, + size_t depth, + size_t* javascript_code_units, + std::set* active_path, + ActionModelData* model) { + const CPDF_Dictionary* dict = action.GetDict(); + if (!dict) { + model->warning_flags |= EPDF_ACTION_WARNING_MALFORMED_NEXT; + return std::nullopt; + } + if (depth >= kMaxActionDepth || model->nodes.size() >= kMaxActionNodes) { + model->warning_flags |= EPDF_ACTION_WARNING_INCOMPLETE; + return std::nullopt; + } + + ActionNodeRecord node; + node.subtype = dict->GetNameFor("S"); + node.type = NormalizeActionType(action.GetType()); + node.dict = pdfium::WrapRetain(dict); + if (node.type == EPDF_ACTION_TYPE_JAVASCRIPT || + node.type == EPDF_ACTION_TYPE_RENDITION) { + node.javascript = action.MaybeGetJavaScript(); + if (node.javascript.has_value()) { + const size_t length = node.javascript->GetLength(); + if (length > kMaxJavaScriptCodeUnits - *javascript_code_units) { + model->warning_flags |= EPDF_ACTION_WARNING_INCOMPLETE; + return std::nullopt; + } + *javascript_code_units += length; + } + } + + const EPDF_ACTION_NODE_ID node_id = + pdfium::checked_cast(model->nodes.size()); + model->nodes.push_back(std::move(node)); + active_path->insert(dict); + + if (dict->KeyExist("Next")) { + RetainPtr next = dict->GetDirectObjectFor("Next"); + if (!next || (!next->IsDictionary() && !next->IsArray())) { + model->warning_flags |= EPDF_ACTION_WARNING_MALFORMED_NEXT; + } + } + const size_t child_count = action.GetSubActionsCount(); + for (size_t i = 0; i < child_count; ++i) { + CPDF_Action child = action.GetSubAction(i); + const CPDF_Dictionary* child_dict = child.GetDict(); + if (!child_dict) { + model->warning_flags |= EPDF_ACTION_WARNING_MALFORMED_NEXT; + continue; + } + if (active_path->contains(child_dict)) { + model->warning_flags |= EPDF_ACTION_WARNING_CYCLE_DROPPED; + continue; + } + std::optional child_id = AppendAction( + child, depth + 1, javascript_code_units, active_path, model); + if (child_id.has_value()) { + model->nodes[node_id].next.push_back(child_id.value()); + } + } + + active_path->erase(dict); + return node_id; +} + +const ActionModelData* DataFromHandle(EPDF_ACTION_MODEL model); + +} // namespace + +ActionModelDataPtr BuildActionModel(const CPDF_Action& action) { + if (!action.HasDict()) { + return nullptr; + } + auto model = std::make_shared(); + size_t javascript_code_units = 0; + std::set active_path; + AppendAction(action, 0, &javascript_code_units, &active_path, model.get()); + return model; +} + +} // namespace epdf + +struct epdf_action_model_t__ { + explicit epdf_action_model_t__(epdf::ActionModelDataPtr data) + : data(std::move(data)) {} + + epdf::ActionModelDataPtr data; +}; + +namespace epdf { + +namespace { + +const ActionModelData* DataFromHandle(EPDF_ACTION_MODEL model) { + return model && model->data ? model->data.get() : nullptr; +} + +const ActionNodeRecord* GetNode(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + const ActionModelData* data = DataFromHandle(model); + return data && node < data->nodes.size() ? &data->nodes[node] : nullptr; +} + +EPDF_ACTION_MODEL MakeModelFromDictionary( + RetainPtr dictionary) { + return dictionary ? MakeActionModelHandle( + BuildActionModel(CPDF_Action(std::move(dictionary)))) + : nullptr; +} + +const CPDF_Dictionary* GetDocumentRoot(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? doc->GetRoot() : nullptr; +} + +RetainPtr GetPageDictionaryByObjectNumber( + FPDF_DOCUMENT document, + uint32_t page_object_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || page_object_number == 0) { + return nullptr; + } +#ifdef PDF_ENABLE_XFA + if (doc->GetExtension()) { + return nullptr; + } +#endif // PDF_ENABLE_XFA + const int page_index = doc->GetPageIndex(page_object_number); + return page_index >= 0 ? doc->GetPageDictionary(page_index) : nullptr; +} + +} // namespace + +EPDF_ACTION_MODEL MakeActionModelHandle(ActionModelDataPtr data) { + return data ? new epdf_action_model_t__(std::move(data)) : nullptr; +} + +} // namespace epdf + +FPDF_EXPORT void FPDF_CALLCONV EPDFAction_CloseModel(EPDF_ACTION_MODEL model) { + delete model; +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFAction_LoadModel(FPDF_ACTION action) { + CPDF_Dictionary* dictionary = CPDFDictionaryFromFPDFAction(action); + return dictionary ? epdf::MakeActionModelHandle(epdf::BuildActionModel( + CPDF_Action(pdfium::WrapRetain(dictionary)))) + : nullptr; +} + +FPDF_EXPORT EPDF_ACTION_NODE_ID FPDF_CALLCONV +EPDFAction_GetRootNode(EPDF_ACTION_MODEL model) { + const epdf::ActionModelData* data = epdf::DataFromHandle(model); + return data && !data->nodes.empty() ? 0 : EPDF_ACTION_NODE_INVALID; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFAction_GetNodeCount(EPDF_ACTION_MODEL model) { + const epdf::ActionModelData* data = epdf::DataFromHandle(model); + return data ? pdfium::checked_cast(data->nodes.size()) : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFAction_GetNodeType(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + return record ? record->type : EPDF_ACTION_TYPE_UNKNOWN; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeSubtype(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + char* buffer, + unsigned long buflen) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!record) { + return 0; + } + return NulTerminateMaybeCopyAndReturnLength( + record->subtype, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAction_NodeHasJavaScript(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + return record && record->javascript.has_value(); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeJavaScript(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!record || !record->javascript.has_value()) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + record->javascript.value(), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_DEST FPDF_CALLCONV +EPDFAction_GetNodeDest(FPDF_DOCUMENT document, + EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!doc || !record || !record->dict) { + return nullptr; + } + // Same type gate as FPDFAction_GetDest. + if (record->type != EPDF_ACTION_TYPE_GOTO && + record->type != EPDF_ACTION_TYPE_GOTO_REMOTE && + record->type != EPDF_ACTION_TYPE_GOTO_EMBEDDED) { + return nullptr; + } + CPDF_Action cAction(record->dict); + return FPDFDestFromCPDFArray(cAction.GetDest(doc).GetArray()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeURI(FPDF_DOCUMENT document, + EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!doc || !record || !record->dict || + record->type != EPDF_ACTION_TYPE_URI) { + return 0; + } + CPDF_Action cAction(record->dict); + ByteString uri = cAction.GetURI(doc); + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + uri, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeFilePath(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!record || !record->dict) { + return 0; + } + // Same type gate as FPDFAction_GetFilePath. + if (record->type != EPDF_ACTION_TYPE_GOTO_REMOTE && + record->type != EPDF_ACTION_TYPE_GOTO_EMBEDDED && + record->type != EPDF_ACTION_TYPE_LAUNCH) { + return 0; + } + CPDF_Action cAction(record->dict); + ByteString path = cAction.GetFilePath().ToUTF8(); + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + path, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeName(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!record || !record->dict || + record->type != EPDF_ACTION_TYPE_NAMED) { + return 0; + } + ByteString name = record->dict->GetNameFor("N"); + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + name, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFAction_GetNextCount(EPDF_ACTION_MODEL model, EPDF_ACTION_NODE_ID node) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + return record ? pdfium::checked_cast(record->next.size()) : 0; +} + +FPDF_EXPORT EPDF_ACTION_NODE_ID FPDF_CALLCONV +EPDFAction_GetNextAt(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + int index) { + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!record || index < 0 || + static_cast(index) >= record->next.size()) { + return EPDF_ACTION_NODE_INVALID; + } + return record->next[static_cast(index)]; +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFAction_GetWarningFlags(EPDF_ACTION_MODEL model) { + const epdf::ActionModelData* data = epdf::DataFromHandle(model); + return data ? data->warning_flags : EPDF_ACTION_WARNING_INCOMPLETE; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAction_IsComplete(EPDF_ACTION_MODEL model) { + const epdf::ActionModelData* data = epdf::DataFromHandle(model); + return data && !data->nodes.empty() && + !(data->warning_flags & EPDF_ACTION_WARNING_INCOMPLETE); +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetOpenActionModel(FPDF_DOCUMENT document) { + const CPDF_Dictionary* root = epdf::GetDocumentRoot(document); + return root ? epdf::MakeModelFromDictionary(root->GetDictFor("OpenAction")) + : nullptr; +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetAdditionalActionModel(FPDF_DOCUMENT document, int event) { + static constexpr std::array kKeys = {"WC", "WS", "DS", "WP", + "DP"}; + if (event < 0 || event >= static_cast(kKeys.size())) { + return nullptr; + } + const CPDF_Dictionary* root = epdf::GetDocumentRoot(document); + RetainPtr additional = + root ? root->GetDictFor("AA") : nullptr; + return additional ? epdf::MakeModelFromDictionary( + additional->GetDictFor(kKeys[event])) + : nullptr; +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetPageActionModel(FPDF_DOCUMENT document, + uint32_t page_object_number, + int event) { + if (event != EPDF_PAGE_ACTION_OPEN && event != EPDF_PAGE_ACTION_CLOSE) { + return nullptr; + } + RetainPtr page = + epdf::GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr additional = + page ? page->GetDictFor("AA") : nullptr; + const char* key = event == EPDF_PAGE_ACTION_OPEN ? "O" : "C"; + return additional ? epdf::MakeModelFromDictionary(additional->GetDictFor(key)) + : nullptr; +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFAnnot_GetActionModel(FPDF_ANNOTATION annotation, int event) { + static constexpr std::array kAdditionalKeys = { + "E", "X", "D", "U", "Fo", "Bl", "PO", "PC", "PV", "PI"}; + if (event < EPDF_ANNOT_ACTION_ACTIVATE || + event > EPDF_ANNOT_ACTION_PAGE_INVISIBLE) { + return nullptr; + } + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annotation); + const CPDF_Dictionary* annotation_dict = + context ? context->GetAnnotDict() : nullptr; + if (!annotation_dict) { + return nullptr; + } + if (event == EPDF_ANNOT_ACTION_ACTIVATE) { + return epdf::MakeModelFromDictionary(annotation_dict->GetDictFor("A")); + } + RetainPtr additional = + annotation_dict->GetDictFor("AA"); + const int additional_index = event - EPDF_ANNOT_ACTION_CURSOR_ENTER; + return additional ? epdf::MakeModelFromDictionary(additional->GetDictFor( + kAdditionalKeys[additional_index])) + : nullptr; +} diff --git a/fpdfsdk/epdf_action_embeddertest.cpp b/fpdfsdk/epdf_action_embeddertest.cpp new file mode 100644 index 0000000000..3f582d0d47 --- /dev/null +++ b/fpdfsdk/epdf_action_embeddertest.cpp @@ -0,0 +1,526 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_action.h" + +#include +#include +#include + +#include "core/fpdfapi/page/cpdf_page.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_name.h" +#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/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/epdf_form.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_doc.h" +#include "public/fpdf_edit.h" +#include "public/fpdf_javascript.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 ActionModelDeleter { + void operator()(EPDF_ACTION_MODEL model) const { + EPDFAction_CloseModel(model); + } +}; + +using ScopedEPDFActionModel = + std::unique_ptr; + +std::wstring GetActionJavaScript(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + const unsigned long length = + EPDFAction_GetNodeJavaScript(model, node, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, + EPDFAction_GetNodeJavaScript(model, node, buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetLegacyJavaScript(FPDF_JAVASCRIPT_ACTION action) { + const unsigned long length = + FPDFJavaScriptAction_GetScript(action, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, + FPDFJavaScriptAction_GetScript(action, buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::string GetActionSubtype(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node) { + const unsigned long length = + EPDFAction_GetNodeSubtype(model, node, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, + EPDFAction_GetNodeSubtype(model, node, buffer.data(), length)); + return std::string(buffer.data()); +} + +RetainPtr MakeJavaScriptAction(const wchar_t* script) { + auto action = pdfium::MakeRetain(); + action->SetNewFor("S", "JavaScript"); + action->SetNewFor("JS", script); + return action; +} + +} // namespace + +class EPDFActionEmbedderTest : public EmbedderTest {}; + +TEST_F(EPDFActionEmbedderTest, InvalidArguments) { + EXPECT_EQ(EPDF_ACTION_NODE_INVALID, EPDFAction_GetRootNode(nullptr)); + EXPECT_EQ(0, EPDFAction_GetNodeCount(nullptr)); + EXPECT_FALSE(EPDFAction_IsComplete(nullptr)); + EXPECT_FALSE(EPDFDoc_GetOpenActionModel(nullptr)); + EXPECT_FALSE(EPDFDoc_GetAdditionalActionModel( + nullptr, EPDF_DOCUMENT_ACTION_WILL_CLOSE)); + EXPECT_FALSE(EPDFDoc_GetPageActionModel(nullptr, 1, EPDF_PAGE_ACTION_OPEN)); + EXPECT_FALSE(EPDFAnnot_GetActionModel(nullptr, EPDF_ANNOT_ACTION_ACTIVATE)); +} + +TEST_F(EPDFActionEmbedderTest, NamedJavaScriptIndexPairing) { + ASSERT_TRUE(OpenDocument("js.pdf")); + ASSERT_EQ(5, FPDFDoc_GetJavaScriptActionCount(document())); + + for (int index = 0; index < 5; ++index) { + ScopedFPDFJavaScriptAction legacy( + FPDFDoc_GetJavaScriptAction(document(), index)); + ScopedEPDFActionModel model( + EPDFDoc_GetNamedJavaScriptActionModel(document(), index)); + ASSERT_EQ(!!legacy, !!model) << "index " << index; + if (!legacy) { + continue; + } + const EPDF_ACTION_NODE_ID root = EPDFAction_GetRootNode(model.get()); + ASSERT_NE(EPDF_ACTION_NODE_INVALID, root); + EXPECT_EQ(EPDF_ACTION_TYPE_JAVASCRIPT, + EPDFAction_GetNodeType(model.get(), root)); + EXPECT_TRUE(EPDFAction_NodeHasJavaScript(model.get(), root)); + EXPECT_EQ(GetLegacyJavaScript(legacy.get()), + GetActionJavaScript(model.get(), root)); + } + + EXPECT_FALSE(EPDFDoc_GetNamedJavaScriptActionModel(document(), -1)); + EXPECT_FALSE(EPDFDoc_GetNamedJavaScriptActionModel(document(), 5)); +} + +TEST_F(EPDFActionEmbedderTest, NextRenditionCycleAndMalformedEntry) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document.get()); + ASSERT_TRUE(doc); + + RetainPtr root = doc->NewIndirect(); + root->SetNewFor("S", "JavaScript"); + root->SetNewFor("JS", L"root();"); + + RetainPtr rendition = doc->NewIndirect(); + rendition->SetNewFor("S", "Rendition"); + const ByteString rendition_script = "rendition();"; + RetainPtr stream = + doc->NewIndirect(rendition_script.unsigned_span()); + rendition->SetNewFor("JS", doc, stream->GetObjNum()); + rendition->SetNewFor("Next", doc, root->GetObjNum()); + + RetainPtr future = doc->NewIndirect(); + future->SetNewFor("S", "FutureAction"); + RetainPtr malformed_next = future->SetNewFor("Next"); + malformed_next->AppendNew(7); + + RetainPtr next = root->SetNewFor("Next"); + next->AppendNew(doc, rendition->GetObjNum()); + next->AppendNew(doc, future->GetObjNum()); + + ScopedEPDFActionModel model( + EPDFAction_LoadModel(FPDFActionFromCPDFDictionary(root.Get()))); + ASSERT_TRUE(model); + ASSERT_EQ(3, EPDFAction_GetNodeCount(model.get())); + const EPDF_ACTION_NODE_ID model_root = EPDFAction_GetRootNode(model.get()); + ASSERT_EQ(0u, model_root); + ASSERT_EQ(2, EPDFAction_GetNextCount(model.get(), model_root)); + + const EPDF_ACTION_NODE_ID rendition_node = + EPDFAction_GetNextAt(model.get(), model_root, 0); + ASSERT_NE(EPDF_ACTION_NODE_INVALID, rendition_node); + EXPECT_EQ(EPDF_ACTION_TYPE_RENDITION, + EPDFAction_GetNodeType(model.get(), rendition_node)); + EXPECT_TRUE(EPDFAction_NodeHasJavaScript(model.get(), rendition_node)); + EXPECT_EQ(L"rendition();", GetActionJavaScript(model.get(), rendition_node)); + EXPECT_EQ(0, EPDFAction_GetNextCount(model.get(), rendition_node)); + + const EPDF_ACTION_NODE_ID future_node = + EPDFAction_GetNextAt(model.get(), model_root, 1); + ASSERT_NE(EPDF_ACTION_NODE_INVALID, future_node); + EXPECT_EQ(EPDF_ACTION_TYPE_UNKNOWN, + EPDFAction_GetNodeType(model.get(), future_node)); + EXPECT_EQ("FutureAction", GetActionSubtype(model.get(), future_node)); + EXPECT_FALSE(EPDFAction_NodeHasJavaScript(model.get(), future_node)); + + const uint32_t warnings = EPDFAction_GetWarningFlags(model.get()); + EXPECT_TRUE(warnings & EPDF_ACTION_WARNING_CYCLE_DROPPED); + EXPECT_TRUE(warnings & EPDF_ACTION_WARNING_MALFORMED_NEXT); + EXPECT_FALSE(warnings & EPDF_ACTION_WARNING_INCOMPLETE); + EXPECT_TRUE(EPDFAction_IsComplete(model.get())); +} + +TEST_F(EPDFActionEmbedderTest, DepthLimitMarksModelIncomplete) { + RetainPtr root = MakeJavaScriptAction(L"root();"); + RetainPtr current = root; + for (int i = 0; i < 64; ++i) { + RetainPtr child = MakeJavaScriptAction(L"next();"); + current->SetFor("Next", child); + current = std::move(child); + } + + ScopedEPDFActionModel model( + EPDFAction_LoadModel(FPDFActionFromCPDFDictionary(root.Get()))); + ASSERT_TRUE(model); + EXPECT_EQ(64, EPDFAction_GetNodeCount(model.get())); + EXPECT_TRUE(EPDFAction_GetWarningFlags(model.get()) & + EPDF_ACTION_WARNING_INCOMPLETE); + EXPECT_FALSE(EPDFAction_IsComplete(model.get())); +} + +TEST_F(EPDFActionEmbedderTest, DocumentAndPageModelsAreDetached) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + ScopedFPDFPage page(FPDFPage_New(document.get(), 0, 300, 300)); + ASSERT_TRUE(page); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document.get()); + ASSERT_TRUE(doc); + RetainPtr root = doc->GetMutableRoot(); + ASSERT_TRUE(root); + root->SetFor("OpenAction", MakeJavaScriptAction(L"open();")); + RetainPtr document_aa = + root->SetNewFor("AA"); + document_aa->SetFor("WS", MakeJavaScriptAction(L"willSave();")); + + CPDF_Page* cpdf_page = CPDFPageFromFPDFPage(page.get()); + ASSERT_TRUE(cpdf_page); + RetainPtr page_dict = cpdf_page->GetMutableDict(); + ASSERT_TRUE(page_dict); + RetainPtr page_aa = + page_dict->SetNewFor("AA"); + page_aa->SetFor("O", MakeJavaScriptAction(L"pageOpen();")); + const uint32_t page_objnum = EPDFPage_GetObjectNumber(page.get()); + ASSERT_GT(page_objnum, 0u); + + ScopedEPDFActionModel open(EPDFDoc_GetOpenActionModel(document.get())); + ScopedEPDFActionModel will_save(EPDFDoc_GetAdditionalActionModel( + document.get(), EPDF_DOCUMENT_ACTION_WILL_SAVE)); + ScopedEPDFActionModel page_open(EPDFDoc_GetPageActionModel( + document.get(), page_objnum, EPDF_PAGE_ACTION_OPEN)); + ASSERT_TRUE(open); + ASSERT_TRUE(will_save); + ASSERT_TRUE(page_open); + EXPECT_FALSE(EPDFDoc_GetAdditionalActionModel(document.get(), -1)); + EXPECT_FALSE(EPDFDoc_GetPageActionModel(document.get(), page_objnum, 9)); + + page.reset(); + document.reset(); + + EXPECT_EQ(L"open();", GetActionJavaScript(open.get(), 0)); + EXPECT_EQ(L"willSave();", GetActionJavaScript(will_save.get(), 0)); + EXPECT_EQ(L"pageOpen();", GetActionJavaScript(page_open.get(), 0)); +} + +TEST_F(EPDFActionEmbedderTest, LayerActionReadsDoNotPromote) { + const std::string path = + PathService::GetTestFilePath("annots_action_handling.pdf"); + ASSERT_FALSE(path.empty()); + const std::vector bytes = GetFileContents(path.c_str()); + ASSERT_FALSE(bytes.empty()); + EPDF_BASE_DOCUMENT base = EPDF_LoadMemBaseDocument( + bytes.data(), static_cast(bytes.size()), nullptr); + ASSERT_TRUE(base); + EPDFLayerOpenStatus status; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, status); + ASSERT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(layer.get())); + + bool found_action = false; + const int script_count = FPDFDoc_GetJavaScriptActionCount(layer.get()); + ASSERT_GE(script_count, 0); + for (int index = 0; index < script_count; ++index) { + ScopedEPDFActionModel action( + EPDFDoc_GetNamedJavaScriptActionModel(layer.get(), index)); + found_action = found_action || !!action; + } + for (int event = EPDF_DOCUMENT_ACTION_WILL_CLOSE; + event <= EPDF_DOCUMENT_ACTION_DID_PRINT; ++event) { + ScopedEPDFActionModel action( + EPDFDoc_GetAdditionalActionModel(layer.get(), event)); + found_action = found_action || !!action; + } + ScopedEPDFActionModel open(EPDFDoc_GetOpenActionModel(layer.get())); + found_action = found_action || !!open; + + const int page_count = FPDF_GetPageCount(layer.get()); + for (int page_index = 0; page_index < page_count; ++page_index) { + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), page_index)); + ASSERT_TRUE(page); + const uint32_t page_objnum = EPDFPage_GetObjectNumber(page.get()); + for (int event = EPDF_PAGE_ACTION_OPEN; event <= EPDF_PAGE_ACTION_CLOSE; + ++event) { + ScopedEPDFActionModel action( + EPDFDoc_GetPageActionModel(layer.get(), page_objnum, event)); + found_action = found_action || !!action; + } + const int annotation_count = FPDFPage_GetAnnotCount(page.get()); + for (int annotation_index = 0; annotation_index < annotation_count; + ++annotation_index) { + ScopedFPDFAnnotation annotation( + FPDFPage_GetAnnot(page.get(), annotation_index)); + ASSERT_TRUE(annotation); + for (int event = EPDF_ANNOT_ACTION_ACTIVATE; + event <= EPDF_ANNOT_ACTION_PAGE_INVISIBLE; ++event) { + ScopedEPDFActionModel action( + EPDFAnnot_GetActionModel(annotation.get(), event)); + found_action = found_action || !!action; + } + } + } + EXPECT_TRUE(found_action); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(layer.get())); + + layer.reset(); + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(EPDFActionEmbedderTest, + MergedFieldAndWidgetAdditionalActionsStaySeparate) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + RetainPtr merged = + ToDictionary(doc->GetMutableIndirectObject(4)); + ASSERT_TRUE(merged); + merged->SetNewFor("DV", L"default text"); + RetainPtr aa = merged->SetNewFor("AA"); + aa->SetFor("V", MakeJavaScriptAction(L"validate();")); + aa->SetFor("C", MakeJavaScriptAction(L"calculate();")); + aa->SetFor("Fo", MakeJavaScriptAction(L"focus();")); + aa->SetFor("E", MakeJavaScriptAction(L"enter();")); + + RetainPtr acro_form = + doc->GetMutableRoot()->GetMutableDictFor("AcroForm"); + ASSERT_TRUE(acro_form); + RetainPtr calculation_order = + acro_form->SetNewFor("CO"); + calculation_order->AppendNew(doc, merged->GetObjNum()); + + EPDF_FORM_MODEL form = EPDFForm_LoadModel(document()); + ASSERT_TRUE(form); + ASSERT_EQ(1, EPDFForm_CountFields(form)); + EXPECT_EQ(L"default text", [&]() { + const unsigned long length = + EPDFForm_GetFieldDefaultValueAt(form, 0, 0, nullptr, 0); + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFForm_GetFieldDefaultValueAt(form, 0, 0, buffer.data(), + length)); + return GetPlatformWString(buffer.data()); + }()); + ASSERT_EQ(1, EPDFForm_CountCalculationOrder(form)); + EXPECT_EQ(0, EPDFForm_GetCalculationOrderFieldIndex(form, 0)); + + ScopedEPDFActionModel validate( + EPDFForm_GetFieldActionModel(form, 0, EPDF_FORM_ACTION_VALIDATE)); + ScopedEPDFActionModel calculate( + EPDFForm_GetFieldActionModel(form, 0, EPDF_FORM_ACTION_CALCULATE)); + EXPECT_FALSE( + EPDFForm_GetFieldActionModel(form, 0, EPDF_FORM_ACTION_KEYSTROKE)); + EXPECT_FALSE(EPDFForm_GetFieldActionModel(form, 0, EPDF_FORM_ACTION_FORMAT)); + ASSERT_TRUE(validate); + ASSERT_TRUE(calculate); + EPDFForm_CloseModel(form); + + ScopedEPDFActionModel focus; + ScopedEPDFActionModel enter; + { + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + { + ScopedFPDFAnnotation widget(FPDFPage_GetAnnot(page, 0)); + ASSERT_TRUE(widget); + focus.reset( + EPDFAnnot_GetActionModel(widget.get(), EPDF_ANNOT_ACTION_FOCUS)); + enter.reset(EPDFAnnot_GetActionModel(widget.get(), + EPDF_ANNOT_ACTION_CURSOR_ENTER)); + // /X is absent. The field /V at the same numeric event position must + // never leak through the annotation event mapping. + EXPECT_FALSE(EPDFAnnot_GetActionModel(widget.get(), + EPDF_ANNOT_ACTION_CURSOR_EXIT)); + } + UnloadPage(page); + } + ASSERT_TRUE(focus); + ASSERT_TRUE(enter); + + CloseDocument(); + EXPECT_EQ(L"validate();", GetActionJavaScript(validate.get(), 0)); + EXPECT_EQ(L"calculate();", GetActionJavaScript(calculate.get(), 0)); + EXPECT_EQ(L"focus();", GetActionJavaScript(focus.get(), 0)); + EXPECT_EQ(L"enter();", GetActionJavaScript(enter.get(), 0)); +} + +TEST_F(EPDFActionEmbedderTest, NodeUriPayloadFromRealDocument) { + ASSERT_TRUE(OpenDocument("annots_action_handling.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + bool checked_link = false; + const int annotation_count = FPDFPage_GetAnnotCount(page.get()); + for (int i = 0; i < annotation_count; ++i) { + ScopedFPDFAnnotation annotation(FPDFPage_GetAnnot(page.get(), i)); + ASSERT_TRUE(annotation); + if (FPDFAnnot_GetSubtype(annotation.get()) != FPDF_ANNOT_LINK) { + continue; + } + ScopedEPDFActionModel model( + EPDFAnnot_GetActionModel(annotation.get(), EPDF_ANNOT_ACTION_ACTIVATE)); + // This fixture also contains destination-only links. They correctly have + // no activate action model; keep looking for its URI-action link. + if (!model) { + continue; + } + const EPDF_ACTION_NODE_ID root = EPDFAction_GetRootNode(model.get()); + ASSERT_NE(EPDF_ACTION_NODE_INVALID, root); + ASSERT_EQ(EPDF_ACTION_TYPE_URI, EPDFAction_GetNodeType(model.get(), root)); + + const unsigned long len = + EPDFAction_GetNodeURI(document(), model.get(), root, nullptr, 0); + ASSERT_GT(len, 1ul); + std::vector buffer(len); + ASSERT_EQ(len, EPDFAction_GetNodeURI(document(), model.get(), root, + buffer.data(), len)); + const ByteString uri(buffer.data()); + EXPECT_EQ(0u, uri.Find("https://").value_or(1u)); + + // Wrong-type payload getters answer empty rather than lying. + EXPECT_FALSE(EPDFAction_GetNodeDest(document(), model.get(), root)); + EXPECT_EQ(0ul, EPDFAction_GetNodeFilePath(model.get(), root, nullptr, 0)); + EXPECT_EQ(0ul, EPDFAction_GetNodeName(model.get(), root, nullptr, 0)); + checked_link = true; + } + EXPECT_TRUE(checked_link); +} + +TEST_F(EPDFActionEmbedderTest, NodeDestPayloadFromCreatedGoTo) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + ScopedFPDFPage page(FPDFPage_New(document.get(), 0, 612, 792)); + ASSERT_TRUE(page); + + FPDF_DEST dest = EPDFDest_CreateXYZ(page.get(), /*has_left=*/true, 30.0f, + /*has_top=*/true, 500.0f, + /*has_zoom=*/false, 0.0f); + ASSERT_TRUE(dest); + FPDF_ACTION action = EPDFAction_CreateGoTo(document.get(), dest); + ASSERT_TRUE(action); + + ScopedEPDFActionModel model(EPDFAction_LoadModel(action)); + ASSERT_TRUE(model); + const EPDF_ACTION_NODE_ID root = EPDFAction_GetRootNode(model.get()); + ASSERT_NE(EPDF_ACTION_NODE_INVALID, root); + ASSERT_EQ(EPDF_ACTION_TYPE_GOTO, EPDFAction_GetNodeType(model.get(), root)); + + FPDF_DEST node_dest = + EPDFAction_GetNodeDest(document.get(), model.get(), root); + ASSERT_TRUE(node_dest); + FPDF_BOOL has_x = false; + FPDF_BOOL has_y = false; + FPDF_BOOL has_zoom = false; + FS_FLOAT x = 0; + FS_FLOAT y = 0; + FS_FLOAT zoom = 0; + ASSERT_TRUE(FPDFDest_GetLocationInPage(node_dest, &has_x, &has_y, &has_zoom, + &x, &y, &zoom)); + EXPECT_TRUE(has_x); + EXPECT_TRUE(has_y); + EXPECT_FALSE(has_zoom); + EXPECT_FLOAT_EQ(30.0f, x); + EXPECT_FLOAT_EQ(500.0f, y); + + // A goto node has no URI/file/name payload. + EXPECT_EQ(0ul, EPDFAction_GetNodeURI(document.get(), model.get(), root, + nullptr, 0)); + EXPECT_EQ(0ul, EPDFAction_GetNodeName(model.get(), root, nullptr, 0)); +} + +TEST_F(EPDFActionEmbedderTest, NodeFilePathAndNamePayloadsFromSyntheticDicts) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document.get()); + ASSERT_TRUE(doc); + + RetainPtr launch = doc->NewIndirect(); + launch->SetNewFor("S", "Launch"); + launch->SetNewFor("F", "app.exe"); + ScopedEPDFActionModel launch_model( + EPDFAction_LoadModel(FPDFActionFromCPDFDictionary(launch.Get()))); + ASSERT_TRUE(launch_model); + const EPDF_ACTION_NODE_ID launch_root = + EPDFAction_GetRootNode(launch_model.get()); + ASSERT_EQ(EPDF_ACTION_TYPE_LAUNCH, + EPDFAction_GetNodeType(launch_model.get(), launch_root)); + unsigned long len = + EPDFAction_GetNodeFilePath(launch_model.get(), launch_root, nullptr, 0); + ASSERT_GT(len, 1ul); + std::vector path(len); + ASSERT_EQ(len, EPDFAction_GetNodeFilePath(launch_model.get(), launch_root, + path.data(), len)); + EXPECT_STREQ("app.exe", path.data()); + + RetainPtr named = doc->NewIndirect(); + named->SetNewFor("S", "Named"); + named->SetNewFor("N", "NextPage"); + ScopedEPDFActionModel named_model( + EPDFAction_LoadModel(FPDFActionFromCPDFDictionary(named.Get()))); + ASSERT_TRUE(named_model); + const EPDF_ACTION_NODE_ID named_root = + EPDFAction_GetRootNode(named_model.get()); + ASSERT_EQ(EPDF_ACTION_TYPE_NAMED, + EPDFAction_GetNodeType(named_model.get(), named_root)); + len = EPDFAction_GetNodeName(named_model.get(), named_root, nullptr, 0); + ASSERT_GT(len, 1ul); + std::vector name(len); + ASSERT_EQ(len, EPDFAction_GetNodeName(named_model.get(), named_root, + name.data(), len)); + EXPECT_STREQ("NextPage", name.data()); + + // Cross-type checks: a launch node answers nothing for uri/name and a + // named node nothing for file paths. + EXPECT_EQ(0ul, EPDFAction_GetNodeURI(document.get(), launch_model.get(), + launch_root, nullptr, 0)); + EXPECT_EQ(0ul, EPDFAction_GetNodeName(launch_model.get(), launch_root, + nullptr, 0)); + EXPECT_EQ(0ul, EPDFAction_GetNodeFilePath(named_model.get(), named_root, + nullptr, 0)); +} diff --git a/fpdfsdk/epdf_action_helpers.h b/fpdfsdk/epdf_action_helpers.h new file mode 100644 index 0000000000..19b12487cb --- /dev/null +++ b/fpdfsdk/epdf_action_helpers.h @@ -0,0 +1,24 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FPDFSDK_EPDF_ACTION_HELPERS_H_ +#define FPDFSDK_EPDF_ACTION_HELPERS_H_ + +#include + +#include "public/epdf_action.h" + +class CPDF_Action; + +namespace epdf { + +struct ActionModelData; +using ActionModelDataPtr = std::shared_ptr; + +ActionModelDataPtr BuildActionModel(const CPDF_Action& action); +EPDF_ACTION_MODEL MakeActionModelHandle(ActionModelDataPtr data); + +} // namespace epdf + +#endif // FPDFSDK_EPDF_ACTION_HELPERS_H_ diff --git a/fpdfsdk/epdf_flatten.cpp b/fpdfsdk/epdf_flatten.cpp new file mode 100644 index 0000000000..d06714026b --- /dev/null +++ b/fpdfsdk/epdf_flatten.cpp @@ -0,0 +1,813 @@ +// Copyright 2014 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com + +#include "public/fpdf_flatten.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "constants/annotation_common.h" +#include "constants/annotation_flags.h" +#include "constants/font_encodings.h" +#include "constants/page_object.h" +#include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" +#include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/page/cpdf_page.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_name.h" +#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/fpdf_parser_utility.h" +#include "core/fpdfdoc/cpdf_annot.h" +#include "core/fxcrt/fx_string_wrappers.h" +#include "fpdfsdk/cpdfsdk_helpers.h" + +namespace { + +struct FlattenCandidate { + size_t annotation_index = 0; + uint32_t annotation_object_number = 0; + RetainPtr annotation; + RetainPtr appearance; + CFX_FloatRect annotation_rect; + CFX_FloatRect appearance_rect; + CFX_Matrix appearance_matrix; +}; + +struct FlattenPlan { + bool target_found = false; + std::vector candidates; +}; + +struct FlattenTarget { + uint32_t annotation_object_number = 0; + int annotation_index = -1; + const CPDF_Dictionary* annotation = nullptr; +}; + +bool IsValidUsage(int usage) { + return usage == FLAT_NORMALDISPLAY || usage == FLAT_PRINT; +} + +uint32_t GetObjectNumber(const CPDF_Object* entry, + const CPDF_Dictionary* dictionary) { + const CPDF_Reference* reference = ToReference(entry); + return reference ? reference->GetRefObjNum() + : (dictionary ? dictionary->GetObjNum() : 0); +} + +bool IsEligibleForUsage(const CPDF_Dictionary* annotation, int usage) { + if (!annotation || + annotation->GetByteStringFor(pdfium::annotation::kSubtype) == "Popup") { + return false; + } + + const int flags = annotation->GetIntegerFor("F"); + if (flags & pdfium::annotation_flags::kHidden) { + return false; + } + return usage == FLAT_NORMALDISPLAY + ? !(flags & pdfium::annotation_flags::kInvisible) + : !!(flags & pdfium::annotation_flags::kPrint); +} + +RetainPtr GetEffectiveObject(CPDF_Document* document, + const CPDF_Object* entry) { + if (!entry) { + return nullptr; + } + const CPDF_Reference* reference = ToReference(entry); + return reference && document + ? document->GetOrParseIndirectObject(reference->GetRefObjNum()) + : pdfium::WrapRetain(entry); +} + +RetainPtr GetNormalAppearance( + CPDF_Document* document, + const CPDF_Dictionary* annotation) { + RetainPtr appearance_entry = + annotation ? annotation->GetObjectFor(pdfium::annotation::kAP) : nullptr; + RetainPtr appearance = + ToDictionary(GetEffectiveObject(document, appearance_entry.Get())); + if (!appearance) { + return nullptr; + } + + RetainPtr normal_entry = appearance->GetObjectFor("N"); + RetainPtr normal = + GetEffectiveObject(document, normal_entry.Get()); + if (!normal) { + return nullptr; + } + if (const CPDF_Stream* stream = normal->AsStream()) { + return pdfium::WrapRetain(stream); + } + + const CPDF_Dictionary* states = normal->AsDictionary(); + if (!states) { + return nullptr; + } + + const ByteString state = annotation->GetByteStringFor("AS"); + if (!state.IsEmpty()) { + RetainPtr state_entry = + states->GetObjectFor(state.AsStringView()); + return ToStream(GetEffectiveObject(document, state_entry.Get())); + } + + CPDF_DictionaryLocker locker(states); + for (const auto& item : locker) { + RetainPtr direct = + GetEffectiveObject(document, item.second.Get()); + if (direct && direct->IsStream()) { + return pdfium::WrapRetain(direct->AsStream()); + } + } + return nullptr; +} + +std::optional MakeCandidate( + CPDF_Document* document, + size_t annotation_index, + const CPDF_Object* entry, + RetainPtr annotation, + int usage) { + if (!IsEligibleForUsage(annotation.Get(), usage)) { + return std::nullopt; + } + + RetainPtr appearance = + GetNormalAppearance(document, annotation.Get()); + if (!appearance) { + return std::nullopt; + } + + CFX_FloatRect annotation_rect = + annotation->GetRectFor(pdfium::annotation::kRect); + annotation_rect.Normalize(); + if (annotation_rect.IsEmpty()) { + return std::nullopt; + } + + RetainPtr appearance_dict = appearance->GetDict(); + CFX_FloatRect appearance_rect; + if (appearance_dict->KeyExist("Rect")) { + appearance_rect = appearance_dict->GetRectFor("Rect"); + } else { + appearance_rect = appearance_dict->GetRectFor("BBox"); + } + appearance_rect.Normalize(); + if (appearance_rect.IsEmpty()) { + return std::nullopt; + } + const CFX_Matrix appearance_matrix = appearance_dict->GetMatrixFor("Matrix"); + CFX_FloatRect transformed_appearance_rect = + appearance_matrix.TransformRect(appearance_rect); + transformed_appearance_rect.Normalize(); + if (transformed_appearance_rect.IsEmpty()) { + return std::nullopt; + } + + FlattenCandidate candidate; + candidate.annotation_index = annotation_index; + candidate.annotation_object_number = GetObjectNumber(entry, annotation.Get()); + candidate.annotation = std::move(annotation); + candidate.appearance = std::move(appearance); + candidate.annotation_rect = annotation_rect; + candidate.appearance_rect = appearance_rect; + candidate.appearance_matrix = appearance_matrix; + return candidate; +} + +FlattenPlan BuildFlattenPlan(CPDF_Document* document, + const CPDF_Dictionary* page, + const FlattenTarget* target, + int usage) { + FlattenPlan plan; + RetainPtr annotations_entry = + page ? page->GetObjectFor("Annots") : nullptr; + RetainPtr annotations = + ToArray(GetEffectiveObject(document, annotations_entry.Get())); + if (!annotations) { + return plan; + } + + for (size_t i = 0; i < annotations->size(); ++i) { + RetainPtr entry = annotations->GetObjectAt(i); + RetainPtr annotation = + ToDictionary(GetEffectiveObject(document, entry.Get())); + if (!annotation) { + continue; + } + + const uint32_t object_number = + GetObjectNumber(entry.Get(), annotation.Get()); + if (target) { + const bool matches = + target->annotation_object_number != 0 + ? object_number == target->annotation_object_number + : annotation.Get() == target->annotation || + (target->annotation_index >= 0 && + i == static_cast(target->annotation_index)); + if (!matches) { + continue; + } + plan.target_found = true; + } + + std::optional candidate = + MakeCandidate(document, i, entry.Get(), std::move(annotation), usage); + if (candidate) { + plan.candidates.push_back(std::move(*candidate)); + } + if (target) { + break; + } + } + return plan; +} + +bool IsSamePage(CPDF_Page* page, CPDF_AnnotContext* annotation) { + CPDF_Page* annotation_page = annotation && annotation->GetPage() + ? annotation->GetPage()->AsPDFPage() + : nullptr; + if (!page || !annotation_page || + page->GetDocument() != annotation_page->GetDocument()) { + return false; + } + + RetainPtr page_dictionary = page->GetDict(); + RetainPtr annotation_page_dictionary = + annotation_page->GetDict(); + if (!page_dictionary || !annotation_page_dictionary) { + return false; + } + + const uint32_t page_object_number = page_dictionary->GetObjNum(); + const uint32_t annotation_page_object_number = + annotation_page_dictionary->GetObjNum(); + return page_object_number != 0 && annotation_page_object_number != 0 + ? page_object_number == annotation_page_object_number + : page_dictionary.Get() == annotation_page_dictionary.Get(); +} + +ByteString GenerateFlattenedContent(const ByteString& key) { + return "q 1 0 0 1 0 0 cm /" + key + " Do Q"; +} + +RetainPtr NewIndirectContentsStreamReference( + CPDF_Document* document, + const ByteString& contents) { + auto pNewContents = + document->NewIndirect(document->New()); + pNewContents->SetData(contents.unsigned_span()); + return pNewContents->MakeReference(document); +} + +void AppendExistingContentStream(CPDF_Document* document, + const CPDF_Object* entry, + CPDF_Array* destination) { + if (!document || !entry || !destination) { + return; + } + + RetainPtr direct = entry->GetDirect(); + const CPDF_Stream* stream = ToStream(direct.Get()); + if (!stream) { + return; + } + + const CPDF_Reference* reference = ToReference(entry); + const uint32_t object_number = + reference ? reference->GetRefObjNum() : stream->GetObjNum(); + if (object_number != 0) { + destination->AppendNew(document, object_number); + return; + } + + RetainPtr clone = ToStream(stream->CloneForHolder(document)); + if (!clone) { + return; + } + const uint32_t clone_object_number = document->AddIndirectObject(clone); + destination->AppendNew(document, clone_object_number); +} + +void AppendExistingContents(CPDF_Document* document, + const CPDF_Object* contents, + CPDF_Array* destination) { + if (!contents) { + return; + } + + RetainPtr direct = contents->GetDirect(); + const CPDF_Array* array = ToArray(direct.Get()); + if (!array) { + AppendExistingContentStream(document, contents, destination); + return; + } + + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr entry = array->GetObjectAt(i); + AppendExistingContentStream(document, entry.Get(), destination); + } +} + +void SetPageContents(const ByteString& key, + CPDF_Dictionary* page, + CPDF_Document* document) { + RetainPtr existing = + page->GetObjectFor(pdfium::page_object::kContents); + if (!existing) { + page->SetFor(pdfium::page_object::kContents, + NewIndirectContentsStreamReference( + document, GenerateFlattenedContent(key))); + return; + } + + auto contents = document->NewIndirect(); + contents->Append(NewIndirectContentsStreamReference(document, "q")); + AppendExistingContents(document, existing.Get(), contents.Get()); + contents->Append(NewIndirectContentsStreamReference(document, "Q")); + contents->Append(NewIndirectContentsStreamReference( + document, GenerateFlattenedContent(key))); + page->SetNewFor(pdfium::page_object::kContents, document, + contents->GetObjNum()); +} + +CFX_Matrix GetMatrix(const CFX_FloatRect& rcAnnot, + const CFX_FloatRect& rcStream, + const CFX_Matrix& matrix) { + if (rcStream.IsEmpty()) { + return CFX_Matrix(); + } + + CFX_FloatRect rcTransformed = matrix.TransformRect(rcStream); + rcTransformed.Normalize(); + + float a = rcAnnot.Width() / rcTransformed.Width(); + float d = rcAnnot.Height() / rcTransformed.Height(); + + float e = rcAnnot.left - rcTransformed.left * a; + float f = rcAnnot.bottom - rcTransformed.bottom * d; + return CFX_Matrix(a, 0.0f, 0.0f, d, e, f); +} + +bool IsValidBaseEncoding(ByteString base_encoding) { + // ISO 32000-1:2008 spec, table 114. + // ISO 32000-2:2020 spec, table 112. + // + // Since /BaseEncoding is optional, `base_encoding` can be empty. + return base_encoding.IsEmpty() || + base_encoding == pdfium::font_encodings::kWinAnsiEncoding || + base_encoding == pdfium::font_encodings::kMacRomanEncoding || + base_encoding == pdfium::font_encodings::kMacExpertEncoding; +} + +void SanitizeFont(RetainPtr font_dict) { + if (!font_dict) { + return; + } + + RetainPtr encoding_dict = + font_dict->GetMutableDictFor("Encoding"); + if (encoding_dict) { + if (!IsValidBaseEncoding(encoding_dict->GetNameFor("BaseEncoding"))) { + font_dict->RemoveFor("Encoding"); + } + } +} + +void SanitizeFontResources(RetainPtr font_resource_dict) { + if (!font_resource_dict) { + return; + } + + CPDF_DictionaryLocker locker(font_resource_dict); + for (auto it : locker) { + SanitizeFont(ToDictionary(it.second->GetMutableDirect())); + } +} + +void SanitizeResources(RetainPtr resources_dict) { + if (!resources_dict) { + return; + } + + SanitizeFontResources(resources_dict->GetMutableDictFor("Font")); +} + +RetainPtr GetInheritedDictionary( + const CPDF_Dictionary* page, + ByteStringView key) { + std::set visited; + const CPDF_Dictionary* current = page; + while (current && !visited.contains(current)) { + RetainPtr value = current->GetDictFor(key); + if (value) { + return value; + } + visited.insert(current); + current = current->GetDictFor(pdfium::page_object::kParent).Get(); + } + return nullptr; +} + +CFX_FloatRect GetInheritedRect(const CPDF_Dictionary* page, + ByteStringView key) { + std::set visited; + const CPDF_Dictionary* current = page; + while (current && !visited.contains(current)) { + if (current->KeyExist(key)) { + CFX_FloatRect rect = current->GetRectFor(key); + rect.Normalize(); + return rect; + } + visited.insert(current); + current = current->GetDictFor(pdfium::page_object::kParent).Get(); + } + return CFX_FloatRect(); +} + +// Give the page a private resource dictionary and a private /XObject child. +// This both preserves inherited resources and avoids mutating a resource +// dictionary shared by the base document or another page. +RetainPtr CreateLocalPageXObjects(CPDF_Document* document, + CPDF_Dictionary* page) { + if (!document || !page) { + return nullptr; + } + + RetainPtr effective_resources = + GetInheritedDictionary(page, pdfium::page_object::kResources); + RetainPtr local_resources = + effective_resources + ? ToDictionary(effective_resources->CloneForHolder(document)) + : document->New(); + if (!local_resources) { + return nullptr; + } + + RetainPtr existing_xobjects = + local_resources->GetDictFor("XObject"); + RetainPtr local_xobjects = + existing_xobjects + ? ToDictionary(existing_xobjects->CloneForHolder(document)) + : document->New(); + if (!local_xobjects) { + return nullptr; + } + + local_resources->SetFor("XObject", local_xobjects); + page->SetFor(pdfium::page_object::kResources, local_resources); + return local_xobjects; +} + +RetainPtr GetMutableArrayMember(CPDF_Document* document, + CPDF_Dictionary* dictionary, + ByteStringView key) { + if (!document || !dictionary) { + return nullptr; + } + RetainPtr entry = dictionary->GetObjectFor(key); + if (const CPDF_Reference* reference = ToReference(entry.Get())) { + return ToArray( + document->GetMutableIndirectObject(reference->GetRefObjNum())); + } + return dictionary->GetMutableArrayFor(key); +} + +bool RemoveObjectFromArray(CPDF_Array* array, + uint32_t object_number, + const CPDF_Dictionary* dictionary) { + if (!array) { + return false; + } + + bool removed = false; + for (size_t i = array->size(); i > 0; --i) { + RetainPtr entry = array->GetObjectAt(i - 1); + const CPDF_Reference* reference = ToReference(entry.Get()); + if ((reference && object_number != 0 && + reference->GetRefObjNum() == object_number) || + (!reference && entry.Get() == dictionary)) { + array->RemoveAt(i - 1); + removed = true; + } + } + return removed; +} + +RetainPtr GetMutableAcroForm(CPDF_Document* document) { + const CPDF_Dictionary* root = document ? document->GetRoot() : nullptr; + RetainPtr entry = + root ? root->GetObjectFor("AcroForm") : nullptr; + if (const CPDF_Reference* reference = ToReference(entry.Get())) { + return ToDictionary( + document->GetMutableIndirectObject(reference->GetRefObjNum())); + } + if (!entry) { + return nullptr; + } + RetainPtr mutable_root = document->GetMutableRoot(); + return mutable_root ? mutable_root->GetMutableDictFor("AcroForm") : nullptr; +} + +void UnlinkMergedFieldAndPruneAncestors(CPDF_Document* document, + uint32_t field_object_number) { + uint32_t current = field_object_number; + for (int depth = 0; current != 0 && depth < 32; ++depth) { + RetainPtr node = + ToDictionary(document->GetOrParseIndirectObject(current)); + if (!node) { + return; + } + + RetainPtr parent = node->GetDictFor("Parent"); + if (parent && parent->GetObjNum() != 0) { + RetainPtr mutable_parent = + ToDictionary(document->GetMutableIndirectObject(parent->GetObjNum())); + RetainPtr parent_kids = + GetMutableArrayMember(document, mutable_parent.Get(), "Kids"); + if (!mutable_parent || + !RemoveObjectFromArray(parent_kids.Get(), current, node.Get())) { + return; + } + if (!parent_kids->IsEmpty() || mutable_parent->KeyExist("FT")) { + return; + } + mutable_parent->RemoveFor("Kids"); + current = parent->GetObjNum(); + continue; + } + + RetainPtr acro_form = GetMutableAcroForm(document); + RetainPtr fields = + GetMutableArrayMember(document, acro_form.Get(), "Fields"); + RemoveObjectFromArray(fields.Get(), current, node.Get()); + return; + } +} + +// A flattened widget must no longer remain reachable through the AcroForm +// field tree. Separate widgets leave their logical field behind as an +// unplaced field. A merged field/widget is removed from its parent field array. +void DetachFlattenedWidget(CPDF_Document* document, + const FlattenCandidate& candidate, + CPDF_Dictionary* mutable_annotation) { + if (!document || !mutable_annotation || + candidate.annotation->GetNameFor(pdfium::annotation::kSubtype) != + "Widget") { + return; + } + + RetainPtr original_parent = + candidate.annotation->GetDictFor("Parent"); + RetainPtr mutable_parent; + if (original_parent && original_parent->GetObjNum() != 0) { + mutable_parent = ToDictionary( + document->GetMutableIndirectObject(original_parent->GetObjNum())); + } else if (original_parent) { + mutable_parent = mutable_annotation->GetMutableDictFor("Parent"); + } + + const bool is_merged_field = candidate.annotation->KeyExist("FT"); + if (is_merged_field && candidate.annotation_object_number != 0) { + UnlinkMergedFieldAndPruneAncestors(document, + candidate.annotation_object_number); + mutable_annotation->RemoveFor("Parent"); + return; + } + + if (mutable_parent) { + RetainPtr kids = + GetMutableArrayMember(document, mutable_parent.Get(), "Kids"); + RemoveObjectFromArray(kids.Get(), candidate.annotation_object_number, + mutable_annotation); + + if (kids && kids->IsEmpty()) { + // A separate widget's terminal field remains visible as an unplaced + // field. + mutable_parent->RemoveFor("Kids"); + } + mutable_annotation->RemoveFor("Parent"); + return; + } + + if (!is_merged_field) { + return; // An orphan widget was never part of the AcroForm field tree. + } + + RetainPtr acro_form = GetMutableAcroForm(document); + RetainPtr fields = + GetMutableArrayMember(document, acro_form.Get(), "Fields"); + RemoveObjectFromArray(fields.Get(), candidate.annotation_object_number, + mutable_annotation); +} + +int ApplyFlattenPlan(CPDF_Document* document, + RetainPtr page, + std::vector candidates) { + if (!document || !page || candidates.empty()) { + return FLATTEN_FAIL; + } + + struct PreparedAppearance { + FlattenCandidate candidate; + RetainPtr stream; + }; + std::vector prepared; + prepared.reserve(candidates.size()); + for (FlattenCandidate& candidate : candidates) { + RetainPtr stream = + ToStream(candidate.appearance->CloneForHolder(document)); + if (!stream) { + continue; + } + prepared.push_back({std::move(candidate), std::move(stream)}); + } + if (prepared.empty()) { + return FLATTEN_FAIL; + } + + CFX_FloatRect media_box = + GetInheritedRect(page.Get(), pdfium::page_object::kMediaBox); + if (media_box.IsEmpty()) { + media_box = CFX_FloatRect(0.0f, 0.0f, 612.0f, 792.0f); + } + + CFX_FloatRect crop_box = + GetInheritedRect(page.Get(), pdfium::page_object::kCropBox); + if (crop_box.IsEmpty()) { + crop_box = media_box; + } + + page->SetRectFor(pdfium::page_object::kMediaBox, media_box); + page->SetRectFor(pdfium::page_object::kCropBox, crop_box); + + RetainPtr page_xobjects = + CreateLocalPageXObjects(document, page.Get()); + if (!page_xobjects) { + return FLATTEN_FAIL; + } + + ByteString page_form_name; + for (int i = 0; i < INT_MAX; ++i) { + ByteString candidate_name = ByteString::Format("FFT%d", i); + if (!page_xobjects->KeyExist(candidate_name.AsStringView())) { + page_form_name = std::move(candidate_name); + break; + } + } + if (page_form_name.IsEmpty()) { + return FLATTEN_FAIL; + } + + auto page_form = + document->NewIndirect(document->New()); + RetainPtr page_form_dict = page_form->GetMutableDict(); + RetainPtr page_form_resources = + page_form_dict->SetNewFor("Resources"); + RetainPtr form_xobjects = + page_form_resources->SetNewFor("XObject"); + page_form_dict->SetNewFor("Type", "XObject"); + page_form_dict->SetNewFor("Subtype", "Form"); + page_form_dict->SetNewFor("FormType", 1); + page_form_dict->SetRectFor("BBox", crop_box); + + ByteString form_content; + for (size_t i = 0; i < prepared.size(); ++i) { + PreparedAppearance& item = prepared[i]; + RetainPtr appearance_dict = item.stream->GetMutableDict(); + appearance_dict->SetNewFor("Type", "XObject"); + appearance_dict->SetNewFor("Subtype", "Form"); + SanitizeResources(appearance_dict->GetMutableDictFor("Resources")); + + const uint32_t appearance_object_number = + document->AddIndirectObject(item.stream); + const ByteString form_name = ByteString::Format("F%zu", i); + form_xobjects->SetNewFor(form_name, document, + appearance_object_number); + + CFX_Matrix matrix = GetMatrix(item.candidate.annotation_rect, + item.candidate.appearance_rect, + item.candidate.appearance_matrix); + matrix.b = 0; + matrix.c = 0; + fxcrt::ostringstream buffer; + WriteMatrix(buffer, matrix); + form_content += ByteString::Format( + "q %s cm /%s Do Q\n", ByteString(buffer).c_str(), form_name.c_str()); + } + page_form->SetDataAndRemoveFilter(form_content.unsigned_span()); + page_xobjects->SetNewFor(page_form_name, document, + page_form->GetObjNum()); + SetPageContents(page_form_name, page.Get(), document); + + RetainPtr annotations = + GetMutableArrayMember(document, page.Get(), "Annots"); + if (!annotations) { + return FLATTEN_FAIL; + } + + // Resolve widget dictionaries while their page-array entries still exist. + for (PreparedAppearance& item : prepared) { + if (item.candidate.annotation->GetNameFor(pdfium::annotation::kSubtype) != + "Widget") { + continue; + } + RetainPtr mutable_annotation; + if (item.candidate.annotation_object_number != 0) { + mutable_annotation = ToDictionary(document->GetMutableIndirectObject( + item.candidate.annotation_object_number)); + } else if (item.candidate.annotation_index < annotations->size()) { + mutable_annotation = + annotations->GetMutableDictAt(item.candidate.annotation_index); + } + if (mutable_annotation) { + DetachFlattenedWidget(document, item.candidate, mutable_annotation.Get()); + } + } + + std::sort(prepared.begin(), prepared.end(), + [](const PreparedAppearance& lhs, const PreparedAppearance& rhs) { + return lhs.candidate.annotation_index > + rhs.candidate.annotation_index; + }); + for (const PreparedAppearance& item : prepared) { + if (item.candidate.annotation_index < annotations->size()) { + annotations->RemoveAt(item.candidate.annotation_index); + } + } + if (annotations->IsEmpty()) { + page->RemoveFor("Annots"); + } + return FLATTEN_SUCCESS; +} + +int FlattenPage(CPDF_Page* page, const FlattenTarget* target, int usage) { + CPDF_Document* document = page ? page->GetDocument() : nullptr; + RetainPtr const_page = + page ? page->GetDict() : nullptr; + if (!document || !const_page || !IsValidUsage(usage)) { + return FLATTEN_FAIL; + } + + FlattenPlan plan = + BuildFlattenPlan(document, const_page.Get(), target, usage); + if (target && !plan.target_found) { + return FLATTEN_FAIL; + } + if (plan.candidates.empty()) { + return FLATTEN_NOTHINGTODO; + } + RetainPtr mutable_page = page->GetMutableDict(); + if (!mutable_page) { + return FLATTEN_FAIL; + } + return ApplyFlattenPlan(document, std::move(mutable_page), + std::move(plan.candidates)); +} + +} // namespace + +FPDF_EXPORT int FPDF_CALLCONV EPDFPage_Flatten(FPDF_PAGE page, int usage) { + CPDF_Page* pdf_page = CPDFPageFromFPDFPage(page); + if (!pdf_page || !IsValidUsage(usage)) { + return FLATTEN_FAIL; + } + return FlattenPage(pdf_page, nullptr, usage); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, + FPDF_ANNOTATION annot, + int usage) { + CPDF_Page* pdf_page = CPDFPageFromFPDFPage(page); + CPDF_AnnotContext* annotation = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!pdf_page || !annotation || !IsValidUsage(usage) || + !IsSamePage(pdf_page, annotation)) { + return FLATTEN_FAIL; + } + + const CPDF_Dictionary* annotation_dictionary = annotation->GetAnnotDict(); + if (!annotation_dictionary) { + return FLATTEN_FAIL; + } + + const FlattenTarget target = {annotation_dictionary->GetObjNum(), + annotation->GetAnnotIndex(), + annotation_dictionary}; + return FlattenPage(pdf_page, &target, usage); +} diff --git a/fpdfsdk/epdf_font.cpp b/fpdfsdk/epdf_font.cpp new file mode 100644 index 0000000000..efe9c04db5 --- /dev/null +++ b/fpdfsdk/epdf_font.cpp @@ -0,0 +1,73 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_font.h" + +#include +#include + +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "core/fxge/cfx_fontregistry.h" +#include "fpdfsdk/cpdfsdk_customaccess.h" + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + FPDF_FILEACCESS* file_access) { + if (!file_access) { + return CFX_FontRegistry::kInvalidFontId; + } + + ByteString font_family_name(family_name ? family_name : ""); + return CFX_FontRegistry::RegisterFont( + font_family_name, weight, italic, + pdfium::MakeRetain(file_access)); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + int size) { + if (size < 0) { + return CFX_FontRegistry::kInvalidFontId; + } + return EPDFFont_RegisterMemFont64(family_name, weight, italic, data_buf, + static_cast(size)); +} + +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + size_t size) { + if (!data_buf || size == 0) { + return CFX_FontRegistry::kInvalidFontId; + } + + ByteString font_family_name(family_name ? family_name : ""); + // SAFETY: required from caller. + auto font_data = + UNSAFE_BUFFERS(pdfium::span(static_cast(data_buf), size)); + return CFX_FontRegistry::RegisterMemoryFont(font_family_name, weight, italic, + font_data); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearRegisteredFonts(void) { + CFX_FontRegistry::ClearRegisteredFonts(); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id) { + return CFX_FontRegistry::AddFallbackFont(font_id); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void) { + CFX_FontRegistry::ClearFallbackFonts(); +} diff --git a/fpdfsdk/epdf_form.cpp b/fpdfsdk/epdf_form.cpp new file mode 100644 index 0000000000..e83071c183 --- /dev/null +++ b/fpdfsdk/epdf_form.cpp @@ -0,0 +1,3758 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_form.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "constants/annotation_flags.h" +#include "constants/form_fields.h" +#include "constants/form_flags.h" +#include "core/fpdfapi/parser/cfdf_document.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_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "core/fpdfapi/parser/fpdf_parser_decode.h" +#include "core/fpdfdoc/cpdf_aaction.h" +#include "core/fpdfdoc/cpdf_action.h" +#include "core/fpdfdoc/cpdf_formcontrol.h" +#include "core/fpdfdoc/cpdf_formfield.h" +#include "core/fpdfdoc/cpdf_generateap.h" +#include "core/fpdfdoc/cpdf_interactiveform.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/cfx_memorystream.h" +#include "core/fxcrt/cfx_read_only_span_stream.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/span.h" +#include "core/fxcrt/span_util.h" +#include "core/fxcrt/stl_util.h" +#include "core/fxcrt/widestring.h" +#include "core/fxcrt/xml/cfx_xmldocument.h" +#include "core/fxcrt/xml/cfx_xmlelement.h" +#include "core/fxcrt/xml/cfx_xmlnode.h" +#include "core/fxcrt/xml/cfx_xmlparser.h" +#include "core/fxcrt/xml/cfx_xmltext.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_action_helpers.h" + +namespace { + +struct WidgetRecord { + uint32_t objnum = 0; + uint32_t page_objnum = 0; + ByteString on_state; + WideString export_value; + bool checked = false; +}; + +struct OptionRecord { + WideString label; + WideString value; + bool selected = false; +}; + +struct FieldValueRecord { + int kind = EPDF_FORM_VALUE_NONE; + std::vector values; +}; + +struct FieldRecord { + uint32_t objnum = 0; + int family = EPDF_FORMFIELD_FAMILY_UNKNOWN; + uint32_t flags = 0; + int origin = EPDF_FORMFIELD_ORIGIN_ACROFORM; + int max_len = 0; + WideString fqn; + WideString alternate_name; + WideString mapping_name; + FieldValueRecord value; + FieldValueRecord default_value; + std::vector options; + std::vector widgets; + std::array actions; +}; + +// A detached, immutable snapshot. Holds no pointers into the document, so +// it stays valid after the document is closed and can never dangle or +// observe stale pre-promotion objects. +struct FormModel { + int kind = EPDF_FORMKIND_NONE; + bool need_appearances = false; + std::vector fields; + std::map field_index_by_objnum; + std::map field_index_by_widget_objnum; + std::vector calculation_order; +}; + +FormModel* FormModelFromHandle(EPDF_FORM_MODEL model) { + return reinterpret_cast(model); +} + +EPDF_FORM_MODEL HandleFromFormModel(FormModel* model) { + return reinterpret_cast(model); +} + +const FieldRecord* GetFieldRecord(EPDF_FORM_MODEL model, int field_index) { + FormModel* form = FormModelFromHandle(model); + if (!form || field_index < 0 || + field_index >= fxcrt::CollectionSize(form->fields)) { + return nullptr; + } + return &form->fields[field_index]; +} + +const WidgetRecord* GetWidgetRecord(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field || widget_index < 0 || + widget_index >= fxcrt::CollectionSize(field->widgets)) { + return nullptr; + } + return &field->widgets[widget_index]; +} + +const OptionRecord* GetOptionRecord(EPDF_FORM_MODEL model, + int field_index, + int option_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field || option_index < 0 || + option_index >= fxcrt::CollectionSize(field->options)) { + return nullptr; + } + return &field->options[option_index]; +} + +int FamilyFromFieldType(CPDF_FormField::Type type) { + switch (type) { + case CPDF_FormField::kPushButton: + return EPDF_FORMFIELD_FAMILY_PUSHBUTTON; + case CPDF_FormField::kRadioButton: + return EPDF_FORMFIELD_FAMILY_RADIO; + case CPDF_FormField::kCheckBox: + return EPDF_FORMFIELD_FAMILY_CHECKBOX; + case CPDF_FormField::kText: + case CPDF_FormField::kRichText: + case CPDF_FormField::kFile: + return EPDF_FORMFIELD_FAMILY_TEXT; + case CPDF_FormField::kListBox: + return EPDF_FORMFIELD_FAMILY_LISTBOX; + case CPDF_FormField::kComboBox: + return EPDF_FORMFIELD_FAMILY_COMBOBOX; + case CPDF_FormField::kSign: + return EPDF_FORMFIELD_FAMILY_SIGNATURE; + case CPDF_FormField::kUnknown: + return EPDF_FORMFIELD_FAMILY_UNKNOWN; + } + return EPDF_FORMFIELD_FAMILY_UNKNOWN; +} + +bool IsToggleFamily(int family) { + return family == EPDF_FORMFIELD_FAMILY_CHECKBOX || + family == EPDF_FORMFIELD_FAMILY_RADIO; +} + +bool IsChoiceFamily(int family) { + return family == EPDF_FORMFIELD_FAMILY_COMBOBOX || + family == EPDF_FORMFIELD_FAMILY_LISTBOX; +} + +FieldValueRecord SnapshotFieldValue(RetainPtr object) { + FieldValueRecord record; + if (!object || object->IsNull()) { + return record; + } + if (object->IsString() || object->IsName()) { + record.kind = EPDF_FORM_VALUE_SCALAR; + record.values.push_back(object->GetUnicodeText()); + return record; + } + const CPDF_Array* array = object->AsArray(); + if (!array) { + record.kind = EPDF_FORM_VALUE_UNSUPPORTED; + return record; + } + + record.kind = EPDF_FORM_VALUE_ARRAY; + record.values.reserve(array->size()); + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr element = array->GetDirectObjectAt(i); + if (!element || !element->IsString()) { + record.kind = EPDF_FORM_VALUE_UNSUPPORTED; + record.values.clear(); + return record; + } + record.values.push_back(element->GetUnicodeText()); + } + return record; +} + +size_t CountFormFields(const CPDF_InteractiveForm& form) { + return form.CountFields(WideString()); +} + +// Collect the set of field dictionaries currently known to |form|. Used to +// tell recovered fields (found only by the page sweep) apart from fields +// reachable through /AcroForm /Fields. +std::set CollectFieldDicts( + const CPDF_InteractiveForm& form) { + std::set dicts; + const size_t count = CountFormFields(form); + for (size_t i = 0; i < count; ++i) { + CPDF_FormField* field = form.GetField(i, WideString()); + if (field) { + dicts.insert(field->GetFieldDict().Get()); + } + } + return dicts; +} + +// Walk every page dictionary (page-tree traversal only - no CPDF_Page, no +// content parsing) and reconcile widget annotations that the /AcroForm +// /Fields walk did not reach. Also records which page references each +// widget, which the snapshot uses as the widget's placement. +std::map SweepPageWidgets( + CPDF_Document* doc, + CPDF_InteractiveForm* form) { + std::map widget_pages; + const int page_count = doc->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = doc->GetPageDictionary(i); + if (!page) { + continue; + } + RetainPtr annots = page->GetArrayFor("Annots"); + if (!annots) { + continue; + } + for (size_t j = 0; j < annots->size(); ++j) { + // Resolve each annotation by object number through the document so + // layer promotions win over the frozen instances that references + // held by frozen base objects would yield. + RetainPtr element = annots->GetObjectAt(j); + if (!element) { + continue; + } + RetainPtr annot; + if (const CPDF_Reference* ref = element->AsReference()) { + annot = + ToDictionary(doc->GetOrParseIndirectObject(ref->GetRefObjNum())); + } else { + annot = ToDictionary(std::move(element)); + } + if (!annot || annot->GetNameFor("Subtype") != "Widget") { + continue; + } + widget_pages.try_emplace(annot.Get(), page->GetObjNum()); + if (!form->GetControlByDict(annot.Get())) { + form->ReconcileWidget(annot); + } + } + } + return widget_pages; +} + +// The reconciled view of the form: the /AcroForm tree merged by fully +// qualified name and reconciled with the page sweep, so recovered fields +// participate and promoted values win. This is the ONE lens both reads +// (model snapshot, interchange export) and write transactions look through; +// a write planned against the raw field dictionary alone would miss +// same-FQN twin widgets that only the reconciliation knows about. +std::unique_ptr BuildReconciledForm(CPDF_Document* doc) { + auto form = std::make_unique(doc); + SweepPageWidgets(doc, form.get()); + return form; +} + +uint32_t PageObjNumForWidget( + const std::map& widget_pages, + const CPDF_Dictionary* widget_dict) { + const auto it = widget_pages.find(widget_dict); + if (it != widget_pages.end()) { + return it->second; + } + // Fall back to the widget's /P entry for widgets that no swept page + // references (e.g. pages outside a layer's page list). + RetainPtr page = widget_dict->GetDictFor("P"); + return page ? page->GetObjNum() : 0; +} + +FieldRecord SnapshotField( + CPDF_FormField* field, + const std::set& initial_fields, + const std::map& widget_pages) { + FieldRecord record; + const CPDF_Dictionary* field_dict = field->GetFieldDict().Get(); + record.objnum = field_dict->GetObjNum(); + record.family = FamilyFromFieldType(field->GetType()); + record.flags = field->GetFieldFlags(); + record.origin = pdfium::Contains(initial_fields, field_dict) + ? EPDF_FORMFIELD_ORIGIN_ACROFORM + : EPDF_FORMFIELD_ORIGIN_RECOVERED; + record.fqn = field->GetFullName(); + record.alternate_name = field->GetAlternateName(); + record.mapping_name = field->GetMappingName(); + record.value = SnapshotFieldValue( + CPDF_FormField::GetFieldAttrForDict(field_dict, pdfium::form_fields::kV)); + record.default_value = SnapshotFieldValue(CPDF_FormField::GetFieldAttrForDict( + field_dict, pdfium::form_fields::kDV)); + + static constexpr std::array kActionTypes = { + CPDF_AAction::kKeyStroke, CPDF_AAction::kFormat, CPDF_AAction::kValidate, + CPDF_AAction::kCalculate}; + CPDF_AAction additional_actions = field->GetAdditionalAction(); + for (size_t i = 0; i < kActionTypes.size(); ++i) { + if (additional_actions.ActionExist(kActionTypes[i])) { + record.actions[i] = + epdf::BuildActionModel(additional_actions.GetAction(kActionTypes[i])); + } + } + if (record.family == EPDF_FORMFIELD_FAMILY_TEXT) { + record.max_len = field->GetMaxLen(); + } + + if (IsChoiceFamily(record.family)) { + const int option_count = field->CountOptions(); + record.options.reserve(option_count); + for (int i = 0; i < option_count; ++i) { + OptionRecord option; + option.label = field->GetOptionLabel(i); + option.value = field->GetOptionValue(i); + option.selected = field->IsItemSelected(i); + record.options.push_back(std::move(option)); + } + } + + const int control_count = field->CountControls(); + record.widgets.reserve(control_count); + for (int i = 0; i < control_count; ++i) { + const CPDF_FormControl* control = field->GetControl(i); + if (!control) { + continue; + } + const CPDF_Dictionary* widget_dict = control->GetWidgetDict().Get(); + WidgetRecord widget; + widget.objnum = widget_dict->GetObjNum(); + widget.page_objnum = PageObjNumForWidget(widget_pages, widget_dict); + if (IsToggleFamily(record.family)) { + widget.on_state = control->GetOnStateName(); + widget.export_value = control->GetExportValue(); + widget.checked = control->IsChecked(); + } + record.widgets.push_back(std::move(widget)); + } + return record; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Write transactions. +// +// Layer-correctness rules, load-bearing on CPDF_LayerDocument: +// 1. Plan with const reads resolved per object number through +// doc->GetIndirectObject() (layer-first lookup), never through cached +// references captured from frozen base objects. +// 2. Validate fully BEFORE the first mutable access: a failed transaction +// must promote nothing. +// 3. Mutate ONLY objects obtained from doc->GetMutableIndirectObject() +// (which promotes) or reached through such a promoted clone. Never +// mutate an object reached by resolving a reference held by a frozen +// base object - that would corrupt the shared base. +// --------------------------------------------------------------------------- + +namespace { + +constexpr char kOffState[] = "Off"; + +// One widget of a terminal field, resolved for a transaction. +struct TxnControl { + uint32_t objnum = 0; // 0 for direct (spec-violating) kid dictionaries. + size_t kids_index = 0; + bool merged = false; // The control IS the field dictionary. + RetainPtr dict; // Planning-phase resolution. + ByteString on_state; + WideString export_value; + ByteString current_as; +}; + +// GetOrParseIndirectObject parses on demand on plain documents (the const +// GetIndirectObject is a map-only lookup) and is the promoted-first lookup +// on layer documents, where it never promotes - safe for planning reads. +RetainPtr ResolveFieldDict(CPDF_Document* doc, + uint32_t field_objnum) { + if (!doc || field_objnum == 0) { + return nullptr; + } + return ToDictionary(doc->GetOrParseIndirectObject(field_objnum)); +} + +RetainPtr ResolveParentFieldDict( + CPDF_Document* doc, + const CPDF_Dictionary* field) { + RetainPtr parent_object = + field ? field->GetObjectFor(pdfium::form_fields::kParent) : nullptr; + if (!parent_object) { + return nullptr; + } + if (const CPDF_Reference* reference = parent_object->AsReference()) { + return ToDictionary( + doc->GetOrParseIndirectObject(reference->GetRefObjNum())); + } + return ToDictionary(parent_object->GetDirect()); +} + +bool HasInheritedFieldAttribute(CPDF_Document* doc, + const CPDF_Dictionary* field, + ByteStringView key) { + RetainPtr current = ResolveParentFieldDict(doc, field); + for (int depth = 0; current && depth < 32; ++depth) { + if (current->KeyExist(key)) { + return true; + } + current = ResolveParentFieldDict(doc, current.Get()); + } + return false; +} + +ByteString InheritedFieldType(const CPDF_Dictionary* field_dict) { + RetainPtr ft = + CPDF_FormField::GetFieldAttrForDict(field_dict, pdfium::form_fields::kFT); + return ft ? ft->GetString() : ByteString(); +} + +uint32_t InheritedFieldFlags(const CPDF_Dictionary* field_dict) { + RetainPtr ff = + CPDF_FormField::GetFieldAttrForDict(field_dict, pdfium::form_fields::kFf); + return ff ? static_cast(ff->GetInteger()) : 0; +} + +ByteString ReadWidgetOnState(const CPDF_Dictionary* widget_dict) { + RetainPtr ap = widget_dict->GetDictFor("AP"); + if (!ap) { + return ByteString(); + } + RetainPtr normal = ap->GetDictFor("N"); + if (!normal) { + return ByteString(); + } + CPDF_DictionaryLocker locker(normal); + for (const auto& it : locker) { + if (it.first != kOffState) { + return it.first; + } + } + return ByteString(); +} + +WideString OptExportAt(const CPDF_Array* opt, size_t index) { + RetainPtr element = opt->GetDirectObjectAt(index); + if (!element) { + return WideString(); + } + const CPDF_Array* pair = element->AsArray(); + return pair ? pair->GetUnicodeTextAt(0) : element->GetUnicodeText(); +} + +// Populate the planning info of one resolved control and append it. +// Mirrors CPDF_FormControl::GetExportValue(): toggle /Opt entries are +// plain strings indexed by control ordinal, with a "Yes" fallback. +void FinishTxnControl(const CPDF_Array* opt_array, + bool want_toggle_info, + std::vector* out, + TxnControl control) { + if (want_toggle_info && control.dict) { + control.on_state = ReadWidgetOnState(control.dict.Get()); + control.current_as = control.dict->GetNameFor("AS"); + const size_t ordinal = out->size(); + ByteString export_bytes = control.on_state; + if (opt_array && ordinal < opt_array->size()) { + export_bytes = opt_array->GetByteStringAt(ordinal); + } + if (export_bytes.IsEmpty()) { + export_bytes = "Yes"; + } + control.export_value = PDF_DecodeText(export_bytes.unsigned_span()); + } + out->push_back(std::move(control)); +} + +// Resolve the widgets of a terminal field from its own dictionary, each +// through the document so layer promotions win. Fails when a kid carries +// /T: the target is a non-terminal field and value transactions must +// address terminal fields. +bool CollectRawTxnControls(CPDF_Document* doc, + const CPDF_Dictionary* field_dict, + uint32_t field_objnum, + bool want_toggle_info, + std::vector* out) { + RetainPtr opt_array; + if (want_toggle_info) { + opt_array = ToArray(CPDF_FormField::GetFieldAttrForDict(field_dict, "Opt")); + } + + RetainPtr kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); + if (!kids) { + TxnControl control; + control.merged = true; + control.objnum = field_objnum; + control.dict = pdfium::WrapRetain(field_dict); + FinishTxnControl(opt_array.Get(), want_toggle_info, out, + std::move(control)); + return true; + } + + for (size_t i = 0; i < kids->size(); ++i) { + RetainPtr element = kids->GetObjectAt(i); + if (!element) { + continue; + } + TxnControl control; + control.kids_index = i; + if (const CPDF_Reference* ref = element->AsReference()) { + control.objnum = ref->GetRefObjNum(); + control.dict = + ToDictionary(doc->GetOrParseIndirectObject(control.objnum)); + } else { + control.dict = ToDictionary(std::move(element)); + } + if (!control.dict) { + continue; + } + if (control.dict->KeyExist(pdfium::form_fields::kT)) { + return false; // Child field: |field_dict| is not terminal. + } + FinishTxnControl(opt_array.Get(), want_toggle_info, out, + std::move(control)); + } + return !out->empty(); +} + +// Locate the reconciled field owning |field_objnum|: the merged +// CPDF_FormField whose storage dictionary carries that object number. +CPDF_FormField* ReconciledFieldByObjNum(const CPDF_InteractiveForm* form, + uint32_t field_objnum) { + const size_t count = form->CountFields(WideString()); + for (size_t i = 0; i < count; ++i) { + CPDF_FormField* field = form->GetField(i, WideString()); + if (field && field->GetFieldDict() && + field->GetFieldDict()->GetObjNum() == field_objnum) { + return field; + } + } + return nullptr; +} + +// Resolve the widgets of a terminal field from the reconciled form view. +// Two-plane documents (the IRS f1040 class: an orphaned /AcroForm twin plus +// a standalone page-annot twin sharing one fully qualified name) fill +// correctly only when a write covers every twin — the raw /Kids walk cannot +// see across planes, but the reconciled control list is exactly the widget +// set the model snapshot reported to the caller. This mirrors what stock +// CPDF_FormField::CheckControl gets for free from its in-memory state. +bool CollectReconciledTxnControls(CPDF_Document* doc, + const CPDF_InteractiveForm* form, + const CPDF_Dictionary* field_dict, + uint32_t field_objnum, + bool want_toggle_info, + std::vector* out) { + const CPDF_FormField* field = ReconciledFieldByObjNum(form, field_objnum); + if (!field) { + return false; + } + + RetainPtr opt_array; + if (want_toggle_info) { + opt_array = ToArray(CPDF_FormField::GetFieldAttrForDict(field_dict, "Opt")); + } + + const CPDF_Dictionary* storage_dict = field->GetFieldDict().Get(); + const int count = field->CountControls(); + for (int i = 0; i < count; ++i) { + const CPDF_FormControl* form_control = field->GetControl(i); + if (!form_control) { + continue; + } + RetainPtr control_dict = + form_control->GetWidgetDict(); + if (!control_dict) { + continue; + } + + TxnControl control; + const uint32_t objnum = control_dict->GetObjNum(); + if (objnum == field_objnum || control_dict.Get() == storage_dict) { + // The merged control: the field dictionary itself is the widget. + control.merged = true; + control.objnum = field_objnum; + control.dict = pdfium::WrapRetain(field_dict); + } else if (objnum != 0) { + control.objnum = objnum; + // Re-resolve through the document so layer promotions win over the + // instance the form captured at build time. + control.dict = ToDictionary(doc->GetOrParseIndirectObject(objnum)); + } else { + // Direct (spec-violating) kid: recover its /Kids index from the + // form-held storage dictionary, then plan against the current view. + RetainPtr storage_kids = + storage_dict->GetArrayFor(pdfium::form_fields::kKids); + RetainPtr current_kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); + if (!storage_kids || !current_kids) { + continue; + } + for (size_t k = 0; k < storage_kids->size(); ++k) { + if (storage_kids->GetDictAt(k).Get() == control_dict.Get()) { + control.kids_index = k; + control.dict = current_kids->GetDictAt(k); + break; + } + } + if (!control.dict) { + continue; + } + } + if (!control.dict) { + continue; + } + FinishTxnControl(opt_array.Get(), want_toggle_info, out, + std::move(control)); + } + return !out->empty(); +} + +// Resolve the widgets of a terminal field for a transaction. The reconciled +// view is authoritative — reads and writes must see the SAME widget set. +// Falls back to the raw /Kids walk for fields the interactive form cannot +// represent (unnamed, type-less, or unplaced authoring drafts). |reconciled| +// may be null; batch callers (interchange import) pass their own so the +// form is built once per batch instead of once per field. +bool CollectTxnControls(CPDF_Document* doc, + const CPDF_Dictionary* field_dict, + uint32_t field_objnum, + bool want_toggle_info, + const CPDF_InteractiveForm* reconciled, + std::vector* out) { + std::unique_ptr owned_form; + if (!reconciled) { + owned_form = BuildReconciledForm(doc); + reconciled = owned_form.get(); + } + if (CollectReconciledTxnControls(doc, reconciled, field_dict, field_objnum, + want_toggle_info, out)) { + return true; + } + out->clear(); + return CollectRawTxnControls(doc, field_dict, field_objnum, want_toggle_info, + out); +} + +// Resolve a control for mutation. Everything routes through promotion: +// indirect widgets promote themselves; direct kids are reached through the +// already-promoted field clone. +RetainPtr MutableControlDict( + CPDF_Document* doc, + const TxnControl& control, + const RetainPtr& promoted_field) { + if (control.merged) { + return promoted_field; + } + if (control.objnum != 0) { + return ToDictionary(doc->GetMutableIndirectObject(control.objnum)); + } + RetainPtr kids = + promoted_field->GetMutableArrayFor(pdfium::form_fields::kKids); + return kids ? kids->GetMutableDictAt(control.kids_index) : nullptr; +} + +void ReportChangedWidgets(const std::vector& changed_objnums, + unsigned long total_changed, + uint32_t* buffer, + unsigned long buffer_size, + unsigned long* out_changed_count) { + if (buffer && buffer_size > 0) { + pdfium::span out_span = + UNSAFE_BUFFERS(pdfium::span(buffer, static_cast(buffer_size))); + const size_t n = std::min(out_span.size(), changed_objnums.size()); + fxcrt::Copy(pdfium::span(changed_objnums).first(n), out_span); + } + if (out_changed_count) { + *out_changed_count = total_changed; + } +} + +CPDF_GenerateAP::FormType ChoiceFormType(uint32_t flags) { + return (flags & pdfium::form_flags::kChoiceCombo) ? CPDF_GenerateAP::kComboBox + : CPDF_GenerateAP::kListBox; +} + +struct NormalizedChoiceValues { + bool free_text = false; + std::vector> matched; +}; + +std::optional> ReadChoiceValues( + const CPDF_Object* object) { + std::vector values; + if (!object || object->IsNull()) { + return values; + } + if (object->IsString()) { + values.push_back(object->GetUnicodeText()); + return values; + } + const CPDF_Array* array = object->AsArray(); + if (!array) { + return std::nullopt; + } + values.reserve(array->size()); + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr element = array->GetDirectObjectAt(i); + if (!element || !element->IsString()) { + return std::nullopt; + } + values.push_back(element->GetUnicodeText()); + } + return values; +} + +std::vector FilterChoiceValues( + const std::vector& values, + const std::vector& available_exports, + bool preserve_free_text) { + std::vector kept; + for (const WideString& value : values) { + if (pdfium::Contains(available_exports, value)) { + kept.push_back(value); + } + } + if (kept.empty() && preserve_free_text && !values.empty()) { + kept = values; + } + return kept; +} + +std::optional NormalizeChoiceValues( + const CPDF_Dictionary* field, + uint32_t flags, + const std::vector& values) { + const bool is_combo = flags & pdfium::form_flags::kChoiceCombo; + const bool is_edit = flags & pdfium::form_flags::kChoiceEdit; + const bool is_multi = flags & pdfium::form_flags::kChoiceMultiSelect; + if (values.size() > 1 && (is_combo || !is_multi)) { + return std::nullopt; + } + + RetainPtr opt_array = + ToArray(CPDF_FormField::GetFieldAttrForDict(field, "Opt")); + NormalizedChoiceValues normalized; + bool all_matched = true; + for (const WideString& value : values) { + bool found = false; + if (opt_array) { + for (size_t i = 0; i < opt_array->size(); ++i) { + if (OptExportAt(opt_array.Get(), i) == value) { + normalized.matched.emplace_back(i, value); + found = true; + break; + } + } + } + all_matched = all_matched && found; + } + normalized.free_text = !all_matched; + if (normalized.free_text && !(is_combo && is_edit && values.size() == 1)) { + return std::nullopt; + } + + std::sort(normalized.matched.begin(), normalized.matched.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + normalized.matched.erase( + std::unique( + normalized.matched.begin(), normalized.matched.end(), + [](const auto& a, const auto& b) { return a.first == b.first; }), + normalized.matched.end()); + return normalized; +} + +void WriteChoiceDefaultValues(CPDF_Dictionary* field, + const std::vector& requested_values, + const NormalizedChoiceValues& normalized) { + if (normalized.free_text) { + field->SetNewFor(pdfium::form_fields::kDV, + requested_values[0].AsStringView()); + return; + } + if (normalized.matched.size() == 1) { + field->SetNewFor(pdfium::form_fields::kDV, + normalized.matched[0].second.AsStringView()); + return; + } + auto defaults = field->SetNewFor(pdfium::form_fields::kDV); + for (const auto& entry : normalized.matched) { + defaults->AppendNew(entry.second.AsStringView()); + } +} + +// Toggle transactions mirror CPDF_FormField::CheckControl semantics: +// checkboxes are always in unison; radios only with the RadiosInUnison +// flag; /V holds the export value name, or the control index when the +// field carries /Opt. +struct ToggleContext { + RetainPtr field; + uint32_t flags = 0; + bool is_radio = false; + std::vector controls; +}; + +bool PrepareToggle(CPDF_Document* doc, + const CPDF_InteractiveForm* reconciled, + uint32_t field_objnum, + ToggleContext* ctx) { + ctx->field = ResolveFieldDict(doc, field_objnum); + if (!ctx->field || + InheritedFieldType(ctx->field.Get()) != pdfium::form_fields::kBtn) { + return false; + } + ctx->flags = InheritedFieldFlags(ctx->field.Get()); + if (ctx->flags & pdfium::form_flags::kButtonPushbutton) { + return false; + } + ctx->is_radio = ctx->flags & pdfium::form_flags::kButtonRadio; + return CollectTxnControls(doc, ctx->field.Get(), field_objnum, + /*want_toggle_info=*/true, reconciled, + &ctx->controls); +} + +bool RejectClearForNoToggleToOff(const ToggleContext& ctx) { + return ctx.is_radio && (ctx.flags & pdfium::form_flags::kButtonNoToggleToOff); +} + +bool ExecuteToggle(CPDF_Document* doc, + uint32_t field_objnum, + const ToggleContext& ctx, + const TxnControl* target, + size_t target_ordinal, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + const bool unison = + !ctx.is_radio || (ctx.flags & pdfium::form_flags::kButtonRadiosInUnison); + + // Plan: new /AS per control, new /V for the field. + struct Step { + size_t control_index; + ByteString new_as; + }; + std::vector steps; + for (size_t i = 0; i < ctx.controls.size(); ++i) { + const TxnControl& control = ctx.controls[i]; + bool checked = false; + if (target) { + checked = unison ? control.export_value == target->export_value && + control.on_state == target->on_state + : i == target_ordinal; + } + ByteString new_as = checked ? control.on_state : ByteString(kOffState); + if (new_as != control.current_as) { + steps.push_back({i, std::move(new_as)}); + } + } + + RetainPtr opt_array = + ToArray(CPDF_FormField::GetFieldAttrForDict(ctx.field.Get(), "Opt")); + ByteString new_v = kOffState; + if (target) { + new_v = opt_array ? ByteString::FormatInteger( + pdfium::checked_cast(target_ordinal)) + : PDF_EncodeText(target->export_value.AsStringView()); + } + RetainPtr current_v = CPDF_FormField::GetFieldAttrForDict( + ctx.field.Get(), pdfium::form_fields::kV); + const bool v_changes = !current_v || current_v->GetString() != new_v; + + if (steps.empty() && !v_changes) { + ReportChangedWidgets({}, 0, changed_widget_objnums, buffer_size, + out_changed_count); + return true; + } + + // Apply. First mutable access happens here; promotion is now safe. + const bool need_promoted_field = + v_changes || std::any_of(steps.begin(), steps.end(), [&](const Step& s) { + const TxnControl& c = ctx.controls[s.control_index]; + return c.merged || c.objnum == 0; + }); + RetainPtr promoted_field; + if (need_promoted_field) { + promoted_field = ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + } + if (v_changes) { + promoted_field->SetNewFor(pdfium::form_fields::kV, new_v); + } + + std::vector changed; + unsigned long total_changed = 0; + for (const Step& step : steps) { + const TxnControl& control = ctx.controls[step.control_index]; + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (!widget) { + continue; + } + widget->SetNewFor("AS", step.new_as); + ++total_changed; + if (control.objnum != 0) { + changed.push_back(control.objnum); + } + } + ReportChangedWidgets(changed, total_changed, changed_widget_objnums, + buffer_size, out_changed_count); + return true; +} + +// Select the target widget by appearance state name ("Off"/empty clears). +bool ApplyToggle(CPDF_Document* doc, + const CPDF_InteractiveForm* reconciled, + uint32_t field_objnum, + const ByteString& requested_state, + bool lenient_unknown_state, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + ToggleContext ctx; + if (!PrepareToggle(doc, reconciled, field_objnum, &ctx)) { + return false; + } + const bool clearing = + requested_state.IsEmpty() || requested_state == kOffState; + if (clearing && RejectClearForNoToggleToOff(ctx)) { + return false; + } + const TxnControl* target = nullptr; + size_t target_ordinal = 0; + if (!clearing) { + for (size_t i = 0; i < ctx.controls.size(); ++i) { + if (ctx.controls[i].on_state == requested_state) { + target = &ctx.controls[i]; + target_ordinal = i; + break; + } + } + if (!target && !lenient_unknown_state) { + return false; + } + } + return ExecuteToggle(doc, field_objnum, ctx, target, target_ordinal, + changed_widget_objnums, buffer_size, out_changed_count); +} + +// Select the target widget by export value - the identity FDF/XFDF carry. +bool ApplyToggleByExport(CPDF_Document* doc, + const CPDF_InteractiveForm* reconciled, + uint32_t field_objnum, + const WideString& export_value, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + ToggleContext ctx; + if (!PrepareToggle(doc, reconciled, field_objnum, &ctx)) { + return false; + } + const bool clearing = export_value.IsEmpty() || export_value == L"Off"; + if (clearing && RejectClearForNoToggleToOff(ctx)) { + return false; + } + const TxnControl* target = nullptr; + size_t target_ordinal = 0; + if (!clearing) { + for (size_t i = 0; i < ctx.controls.size(); ++i) { + if (ctx.controls[i].export_value == export_value) { + target = &ctx.controls[i]; + target_ordinal = i; + break; + } + } + if (!target) { + return false; + } + } + return ExecuteToggle(doc, field_objnum, ctx, target, target_ordinal, + changed_widget_objnums, buffer_size, out_changed_count); +} + +bool ResolveToggleDefault(const ToggleContext& ctx, + const CPDF_Object* default_value, + const TxnControl** out_target, + size_t* out_target_ordinal) { + *out_target = nullptr; + *out_target_ordinal = 0; + if (!default_value || default_value->IsNull()) { + return true; + } + if (!default_value->IsName()) { + return false; + } + + const ByteString raw_default = default_value->GetString(); + if (raw_default == kOffState) { + return true; + } + RetainPtr opt_array = + ToArray(CPDF_FormField::GetFieldAttrForDict(ctx.field.Get(), "Opt")); + for (size_t i = 0; i < ctx.controls.size(); ++i) { + const ByteString checked_value = + opt_array ? ByteString::FormatInteger(pdfium::checked_cast(i)) + : ctx.controls[i].on_state; + if (checked_value == raw_default) { + *out_target = &ctx.controls[i]; + *out_target_ordinal = i; + return true; + } + } + return false; +} + +bool ApplyToggleDefault(CPDF_Document* doc, + uint32_t field_objnum, + const CPDF_Object* default_value, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + ToggleContext ctx; + if (!PrepareToggle(doc, /*reconciled=*/nullptr, field_objnum, &ctx)) { + return false; + } + const TxnControl* target = nullptr; + size_t target_ordinal = 0; + if (!ResolveToggleDefault(ctx, default_value, &target, &target_ordinal)) { + return false; + } + // NoToggleToOff governs interactive changes, not restoring the declared + // default. A missing or explicit /Off default must still reset to Off. + return ExecuteToggle(doc, field_objnum, ctx, target, target_ordinal, + changed_widget_objnums, buffer_size, out_changed_count); +} + +// Same-FQN twin controls are field roots in their own plane (they carry +// /T). Value state must land on them too: appearance generation re-reads +// the value by climbing the control's own dictionary chain — which never +// crosses planes — and per-widget readers in other viewers do the same. +// Runs after the field-level value write and before appearance regen. +void MirrorFieldValueToTwinControls( + CPDF_Document* doc, + const RetainPtr& promoted_field, + const std::vector& controls) { + for (const TxnControl& control : controls) { + if (control.merged || !control.dict || + !control.dict->KeyExist(pdfium::form_fields::kT)) { + continue; + } + RetainPtr twin = + MutableControlDict(doc, control, promoted_field); + if (!twin || twin == promoted_field) { + continue; + } + for (const char* key : {pdfium::form_fields::kV, "I", "RV"}) { + RetainPtr value = promoted_field->GetObjectFor(key); + if (value) { + twin->SetFor(key, value->Clone()); + } else { + twin->RemoveFor(key); + } + } + } +} + +// Regenerate the /AP of every control and report them all as changed. +bool RegenerateControlAppearances( + CPDF_Document* doc, + const std::vector& controls, + const RetainPtr& promoted_field, + CPDF_GenerateAP::FormType type, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + std::vector changed; + unsigned long total_changed = 0; + for (const TxnControl& control : controls) { + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (!widget) { + continue; + } + CPDF_GenerateAP::GenerateFormAP(doc, widget.Get(), type); + ++total_changed; + if (control.objnum != 0) { + changed.push_back(control.objnum); + } + } + ReportChangedWidgets(changed, total_changed, changed_widget_objnums, + buffer_size, out_changed_count); + return true; +} + +// Internal text transaction; the public wrapper converts the wire string. +bool ApplyTextValue(CPDF_Document* doc, + const CPDF_InteractiveForm* reconciled, + uint32_t field_objnum, + const WideString& new_value, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()) != pdfium::form_fields::kTx) { + return false; + } + + RetainPtr max_len_obj = + CPDF_FormField::GetFieldAttrForDict(field.Get(), "MaxLen"); + const int max_len = max_len_obj ? max_len_obj->GetInteger() : 0; + const WideString normalized_value = + max_len > 0 && new_value.GetLength() > static_cast(max_len) + ? new_value.First(static_cast(max_len)) + : new_value; + + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, reconciled, &controls)) { + return false; + } + + RetainPtr current_v = + CPDF_FormField::GetFieldAttrForDict(field.Get(), pdfium::form_fields::kV); + const WideString current_value = + current_v ? current_v->GetUnicodeText() : WideString(); + if (current_value == normalized_value && !field->KeyExist("RV")) { + ReportChangedWidgets({}, 0, changed_widget_objnums, buffer_size, + out_changed_count); + return true; + } + + RetainPtr promoted_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + promoted_field->SetNewFor(pdfium::form_fields::kV, + normalized_value.AsStringView()); + // A rich text value would now contradict /V; drop it rather than lie. + promoted_field->RemoveFor("RV"); + MirrorFieldValueToTwinControls(doc, promoted_field, controls); + + return RegenerateControlAppearances( + doc, controls, promoted_field, CPDF_GenerateAP::kTextField, + changed_widget_objnums, buffer_size, out_changed_count); +} + +// Internal choice transaction; the public wrapper converts the wire strings. +bool ApplyChoiceValues(CPDF_Document* doc, + const CPDF_InteractiveForm* reconciled, + uint32_t field_objnum, + const std::vector& new_values, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()) != pdfium::form_fields::kCh) { + return false; + } + const uint32_t flags = InheritedFieldFlags(field.Get()); + std::optional normalized = + NormalizeChoiceValues(field.Get(), flags, new_values); + if (!normalized.has_value()) { + return false; + } + + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, reconciled, &controls)) { + return false; + } + + RetainPtr promoted_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + + if (new_values.empty()) { + if (HasInheritedFieldAttribute(doc, field.Get(), pdfium::form_fields::kV)) { + if (!(flags & pdfium::form_flags::kChoiceCombo) && + (flags & pdfium::form_flags::kChoiceMultiSelect)) { + promoted_field->SetNewFor(pdfium::form_fields::kV); + } else { + promoted_field->SetNewFor(pdfium::form_fields::kV, + WideStringView()); + } + } else { + promoted_field->RemoveFor(pdfium::form_fields::kV); + } + if (HasInheritedFieldAttribute(doc, field.Get(), "I")) { + promoted_field->SetNewFor("I"); + } else { + promoted_field->RemoveFor("I"); + } + } else if (normalized->free_text) { + promoted_field->SetNewFor(pdfium::form_fields::kV, + new_values[0].AsStringView()); + if (HasInheritedFieldAttribute(doc, field.Get(), "I")) { + promoted_field->SetNewFor("I"); + } else { + promoted_field->RemoveFor("I"); + } + } else { + if (normalized->matched.size() == 1) { + promoted_field->SetNewFor( + pdfium::form_fields::kV, + normalized->matched[0].second.AsStringView()); + } else { + auto value_array = + promoted_field->SetNewFor(pdfium::form_fields::kV); + for (const auto& entry : normalized->matched) { + value_array->AppendNew(entry.second.AsStringView()); + } + } + auto index_array = promoted_field->SetNewFor("I"); + for (const auto& entry : normalized->matched) { + index_array->AppendNew( + pdfium::checked_cast(entry.first)); + } + } + promoted_field->RemoveFor("RV"); + MirrorFieldValueToTwinControls(doc, promoted_field, controls); + + return RegenerateControlAppearances( + doc, controls, promoted_field, ChoiceFormType(flags), + changed_widget_objnums, buffer_size, out_changed_count); +} + +uint32_t DisplayFlags(uint32_t current_flags, int display) { + switch (display) { + case EPDF_FORM_DISPLAY_VISIBLE: + return (current_flags & ~(pdfium::annotation_flags::kInvisible | + pdfium::annotation_flags::kHidden | + pdfium::annotation_flags::kNoView)) | + pdfium::annotation_flags::kPrint; + case EPDF_FORM_DISPLAY_HIDDEN: + return (current_flags & ~(pdfium::annotation_flags::kInvisible | + pdfium::annotation_flags::kNoView)) | + pdfium::annotation_flags::kHidden | + pdfium::annotation_flags::kPrint; + case EPDF_FORM_DISPLAY_NO_PRINT: + return current_flags & ~(pdfium::annotation_flags::kInvisible | + pdfium::annotation_flags::kHidden | + pdfium::annotation_flags::kPrint | + pdfium::annotation_flags::kNoView); + case EPDF_FORM_DISPLAY_NO_VIEW: + return (current_flags & ~pdfium::annotation_flags::kHidden) | + pdfium::annotation_flags::kNoView | + pdfium::annotation_flags::kPrint; + default: + return current_flags; + } +} + +bool ApplyFieldDisplay(CPDF_Document* doc, + uint32_t field_objnum, + int display, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + if (display < EPDF_FORM_DISPLAY_VISIBLE || + display > EPDF_FORM_DISPLAY_NO_VIEW) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field) { + return false; + } + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, /*reconciled=*/nullptr, + &controls)) { + return false; + } + + struct Step { + size_t control_index; + uint32_t flags; + }; + std::vector steps; + for (size_t i = 0; i < controls.size(); ++i) { + const uint32_t current_flags = + static_cast(controls[i].dict->GetIntegerFor("F")); + const uint32_t new_flags = DisplayFlags(current_flags, display); + if (new_flags != current_flags) { + steps.push_back({i, new_flags}); + } + } + if (steps.empty()) { + ReportChangedWidgets({}, 0, changed_widget_objnums, buffer_size, + out_changed_count); + return true; + } + + const bool needs_promoted_field = + std::any_of(steps.begin(), steps.end(), [&](const Step& step) { + const TxnControl& control = controls[step.control_index]; + return control.merged || control.objnum == 0; + }); + RetainPtr promoted_field; + if (needs_promoted_field) { + promoted_field = ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + } + + std::vector changed; + unsigned long total_changed = 0; + for (const Step& step : steps) { + const TxnControl& control = controls[step.control_index]; + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (!widget) { + return false; + } + widget->SetNewFor("F", static_cast(step.flags)); + ++total_changed; + if (control.objnum != 0) { + changed.push_back(control.objnum); + } + } + ReportChangedWidgets(changed, total_changed, changed_widget_objnums, + buffer_size, out_changed_count); + return true; +} + +bool ApplyFieldAppearanceText(CPDF_Document* doc, + uint32_t field_objnum, + const WideString& appearance_text, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field) { + return false; + } + const ByteString field_type = InheritedFieldType(field.Get()); + CPDF_GenerateAP::FormType appearance_type; + if (field_type == pdfium::form_fields::kTx) { + appearance_type = CPDF_GenerateAP::kTextField; + } else if (field_type == pdfium::form_fields::kCh && + (InheritedFieldFlags(field.Get()) & + pdfium::form_flags::kChoiceCombo)) { + appearance_type = CPDF_GenerateAP::kComboBox; + } else { + return false; + } + + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, /*reconciled=*/nullptr, + &controls)) { + return false; + } + const bool needs_promoted_field = std::any_of( + controls.begin(), controls.end(), [](const TxnControl& control) { + return control.merged || control.objnum == 0; + }); + RetainPtr promoted_field; + if (needs_promoted_field) { + promoted_field = ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + } + + std::vector changed; + unsigned long total_changed = 0; + for (const TxnControl& control : controls) { + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (!widget || !CPDF_GenerateAP::GenerateFormAPWithValueOverride( + doc, widget.Get(), appearance_type, appearance_text)) { + return false; + } + ++total_changed; + if (control.objnum != 0) { + changed.push_back(control.objnum); + } + } + ReportChangedWidgets(changed, total_changed, changed_widget_objnums, + buffer_size, out_changed_count); + return true; +} + +// --------------------------------------------------------------------------- +// FDF / XFDF interchange helpers. +// --------------------------------------------------------------------------- + +unsigned long CopyPayloadToBuffer(const ByteString& payload, + void* buffer, + unsigned long buflen) { + const auto length = static_cast(payload.GetLength()); + if (buffer && length > 0 && buflen >= length) { + fxcrt::Copy(payload.unsigned_span(), + UNSAFE_BUFFERS(pdfium::span(static_cast(buffer), + static_cast(buflen)))); + } + return length; +} + +struct ImportStats { + uint32_t total = 0; + uint32_t applied = 0; + uint32_t skipped = 0; + uint32_t widgets_changed = 0; +}; + +void WriteImportResult(const ImportStats& stats, + EPDF_FORM_IMPORT_RESULT* out_result) { + if (!out_result) { + return; + } + out_result->fields_total = stats.total; + out_result->fields_applied = stats.applied; + out_result->fields_skipped = stats.skipped; + out_result->widgets_changed = stats.widgets_changed; +} + +// Route one imported (fqn, values) entry through the typed transactions. +void ApplyImportedValues(CPDF_Document* doc, + CPDF_InteractiveForm* form, + const WideString& fqn, + const std::vector& values, + ImportStats* stats) { + ++stats->total; + CPDF_FormField* field = + form->CountFields(fqn) > 0 ? form->GetField(0, fqn) : nullptr; + if (!field || values.empty()) { + ++stats->skipped; + return; + } + const uint32_t field_objnum = field->GetFieldDict()->GetObjNum(); + if (field_objnum == 0) { + ++stats->skipped; + return; + } + + unsigned long changed = 0; + bool applied = false; + switch (field->GetType()) { + case CPDF_FormField::kCheckBox: + case CPDF_FormField::kRadioButton: + applied = values.size() == 1 && + ApplyToggleByExport(doc, form, field_objnum, values[0], nullptr, + 0, &changed); + break; + case CPDF_FormField::kText: + case CPDF_FormField::kRichText: + case CPDF_FormField::kFile: + applied = values.size() == 1 && + ApplyTextValue(doc, form, field_objnum, values[0], nullptr, 0, + &changed); + break; + case CPDF_FormField::kComboBox: + case CPDF_FormField::kListBox: + applied = ApplyChoiceValues(doc, form, field_objnum, values, nullptr, 0, + &changed); + break; + default: + break; // Push buttons, signatures, unknown: never written. + } + if (applied) { + ++stats->applied; + stats->widgets_changed += static_cast(changed); + } else { + ++stats->skipped; + } +} + +// Walk an FDF /Fields array: flat entries with dotted /T names and +// hierarchical /Kids trees both resolve to fully qualified names. +void WalkFdfFields(CPDF_Document* doc, + CPDF_InteractiveForm* form, + const CPDF_Array* entries, + const WideString& prefix, + ImportStats* stats, + int depth) { + if (!entries || depth > 32) { + return; + } + for (size_t i = 0; i < entries->size(); ++i) { + RetainPtr entry = entries->GetDictAt(i); + if (!entry) { + continue; + } + const WideString name = entry->GetUnicodeTextFor("T"); + WideString fqn = prefix; + if (!name.IsEmpty()) { + fqn = prefix.IsEmpty() ? name : prefix + L"." + name; + } + RetainPtr value = entry->GetDirectObjectFor("V"); + if (value && !fqn.IsEmpty()) { + std::vector values; + if (const CPDF_Array* value_array = value->AsArray()) { + for (size_t j = 0; j < value_array->size(); ++j) { + values.push_back(value_array->GetUnicodeTextAt(j)); + } + } else { + values.push_back(value->GetUnicodeText()); + } + ApplyImportedValues(doc, form, fqn, values, stats); + } + RetainPtr kids = entry->GetArrayFor("Kids"); + if (kids) { + WalkFdfFields(doc, form, kids.Get(), fqn, stats, depth + 1); + } + } +} + +// XFDF field tree, keyed by fully-qualified-name component. +struct XfdfNode { + std::map children; + std::vector values; +}; + +bool IsFieldValueEmpty(const CPDF_Object* value) { + if (!value || value->IsNull()) { + return true; + } + const CPDF_Array* array = value->AsArray(); + return array ? array->IsEmpty() : value->GetString().IsEmpty(); +} + +// Assemble / elements into the document-owned DOM. +void EmitXfdfFieldNodes(const std::map& nodes, + CFX_XMLDocument* xml, + CFX_XMLElement* parent) { + for (const auto& it : nodes) { + CFX_XMLElement* field = xml->CreateNode(L"field"); + field->SetAttribute(L"name", it.first); + parent->AppendLastChild(field); + for (const WideString& value : it.second.values) { + CFX_XMLElement* value_element = xml->CreateNode(L"value"); + value_element->AppendLastChild(xml->CreateNode(value)); + field->AppendLastChild(value_element); + } + EmitXfdfFieldNodes(it.second.children, xml, field); + } +} + +ByteString BuildXfdf(CPDF_InteractiveForm* form, + const WideString& pdf_path, + bool skip_empty_required) { + std::map root; + const size_t field_count = form->CountFields(WideString()); + for (size_t i = 0; i < field_count; ++i) { + CPDF_FormField* field = form->GetField(i, WideString()); + if (!field) { + continue; + } + const CPDF_FormField::Type type = field->GetType(); + if (type == CPDF_FormField::kPushButton || type == CPDF_FormField::kSign) { + continue; + } + const uint32_t flags = field->GetFieldFlags(); + if (flags & pdfium::form_flags::kNoExport) { + continue; + } + RetainPtr value_object = + field->GetFieldAttr(pdfium::form_fields::kV); + if (skip_empty_required && (flags & pdfium::form_flags::kRequired) && + IsFieldValueEmpty(value_object.Get())) { + continue; + } + const WideString fqn = field->GetFullName(); + if (fqn.IsEmpty()) { + continue; + } + + // Nest by fully-qualified-name component. + std::map* level = &root; + XfdfNode* node = nullptr; + size_t start = 0; + while (true) { + std::optional dot = fqn.Find(L'.', start); + const size_t end = dot.value_or(fqn.GetLength()); + node = &(*level)[fqn.Substr(start, end - start)]; + level = &node->children; + if (!dot.has_value()) { + break; + } + start = dot.value() + 1; + } + + if (value_object) { + if (const CPDF_Array* value_array = value_object->AsArray()) { + for (size_t j = 0; j < value_array->size(); ++j) { + node->values.push_back(value_array->GetUnicodeTextAt(j)); + } + } else { + // Toggles surface the checked export value ("Off" when cleared), + // matching the FDF exporter. + node->values.push_back(field->GetValue()); + } + } + } + + // Serialize through the CFX_XML DOM: EncodeEntities() is the single + // escaping authority (the exact inverse of the parser used on import), + // and SaveCompact() keeps text content whitespace-exact as XFDF's + // xml:space="preserve" requires. + CFX_XMLDocument xml; + CFX_XMLElement* xfdf = xml.CreateNode(L"xfdf"); + xfdf->SetAttribute(L"xmlns", L"http://ns.adobe.com/xfdf/"); + xfdf->SetAttribute(L"xml:space", L"preserve"); + CFX_XMLElement* fields = xml.CreateNode(L"fields"); + xfdf->AppendLastChild(fields); + EmitXfdfFieldNodes(root, &xml, fields); + if (!pdf_path.IsEmpty()) { + CFX_XMLElement* filespec = xml.CreateNode(L"f"); + filespec->SetAttribute(L"href", pdf_path); + xfdf->AppendLastChild(filespec); + } + + auto stream = pdfium::MakeRetain(); + stream->WriteString("\n"); + xfdf->SaveCompact(stream); + return ByteString(ByteStringView(stream->GetSpan())); +} + +CFX_XMLElement* FindXmlChildByTag(CFX_XMLNode* parent, WideStringView tag) { + for (CFX_XMLNode* child = parent->GetFirstChild(); child; + child = child->GetNextSibling()) { + CFX_XMLElement* element = ToXMLElement(child); + if (element && element->GetLocalTagName() == tag) { + return element; + } + } + return nullptr; +} + +// Accepts nested elements and dotted name attributes; multiple +// children form a multi-select selection. +void WalkXfdfField(CPDF_Document* doc, + CPDF_InteractiveForm* form, + CFX_XMLElement* element, + const WideString& prefix, + ImportStats* stats, + int depth) { + if (depth > 32) { + return; + } + const WideString name = element->GetAttribute(L"name"); + WideString fqn = prefix; + if (!name.IsEmpty()) { + fqn = prefix.IsEmpty() ? name : prefix + L"." + name; + } + std::vector values; + for (CFX_XMLNode* child = element->GetFirstChild(); child; + child = child->GetNextSibling()) { + CFX_XMLElement* child_element = ToXMLElement(child); + if (!child_element) { + continue; + } + const WideString tag = child_element->GetLocalTagName(); + if (tag == L"value") { + values.push_back(child_element->GetTextData()); + } else if (tag == L"field") { + WalkXfdfField(doc, form, child_element, fqn, stats, depth + 1); + } + } + if (!values.empty() && !fqn.IsEmpty()) { + ApplyImportedValues(doc, form, fqn, values, stats); + } +} + +// --------------------------------------------------------------------------- +// Repair helpers. +// --------------------------------------------------------------------------- + +// Climb /Parent to the field root, re-resolving every hop by object number +// so layer promotions win. Cycle-guarded. +RetainPtr ClimbToFieldRoot( + CPDF_Document* doc, + RetainPtr dict) { + std::vector visited = {dict.Get()}; + for (int i = 0; i < 32; ++i) { + RetainPtr parent = + dict->GetDictFor(pdfium::form_fields::kParent); + if (parent && parent->GetObjNum() != 0) { + parent = ToDictionary(doc->GetOrParseIndirectObject(parent->GetObjNum())); + } + if (!parent || pdfium::Contains(visited, parent.Get())) { + break; + } + visited.push_back(parent.Get()); + dict = std::move(parent); + } + return dict; +} + +// Resolve /AcroForm for mutation, handling all three storage shapes: +// missing (optionally bootstrap one), an indirect reference (promote the +// target), or a direct dictionary inside the catalog (promote the root and +// mutate the embedded clone). Never mutates through a reference held by a +// frozen base object. +RetainPtr GetMutableAcroForm(CPDF_Document* doc, + bool create_if_missing, + bool* out_created) { + const CPDF_Dictionary* root = doc->GetRoot(); + if (!root) { + return nullptr; + } + RetainPtr entry = root->GetObjectFor("AcroForm"); + if (!entry) { + if (!create_if_missing) { + return nullptr; + } + RetainPtr acro_form = + CPDF_InteractiveForm::InitAcroFormDict(doc); + if (acro_form && out_created) { + *out_created = true; + } + return acro_form; + } + if (const CPDF_Reference* ref = entry->AsReference()) { + return ToDictionary(doc->GetMutableIndirectObject(ref->GetRefObjNum())); + } + RetainPtr mutable_root = doc->GetMutableRoot(); + return mutable_root ? mutable_root->GetMutableDictFor("AcroForm") : nullptr; +} + +// Resolve an array member of an already-mutable dictionary, following (and +// promoting) an indirect reference when present, creating the array when +// absent. +RetainPtr GetMutableArrayMember(CPDF_Document* doc, + CPDF_Dictionary* dict, + const ByteString& key) { + RetainPtr entry = dict->GetObjectFor(key.AsStringView()); + if (!entry) { + return dict->SetNewFor(key); + } + if (const CPDF_Reference* ref = entry->AsReference()) { + return ToArray(doc->GetMutableIndirectObject(ref->GetRefObjNum())); + } + return dict->GetMutableArrayFor(key.AsStringView()); +} + +// Membership of a raw array: indirect references by object number, direct +// dictionaries by pointer identity. +bool ArrayReferencesDict(const CPDF_Array* array, + uint32_t objnum, + const CPDF_Dictionary* dict) { + if (!array) { + return false; + } + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr element = array->GetObjectAt(i); + if (!element) { + continue; + } + if (const CPDF_Reference* ref = element->AsReference()) { + if (objnum != 0 && ref->GetRefObjNum() == objnum) { + return true; + } + } else if (element.Get() == static_cast(dict)) { + return true; + } + } + return false; +} + +} // namespace + +FPDF_EXPORT EPDF_FORM_MODEL FPDF_CALLCONV +EPDFForm_LoadModel(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return nullptr; + } + + auto model = std::make_unique(); + + const CPDF_Dictionary* root = doc->GetRoot(); + RetainPtr acro_form = + root ? root->GetDictFor("AcroForm") : nullptr; + if (acro_form) { + model->kind = + acro_form->KeyExist("XFA") ? EPDF_FORMKIND_XFA : EPDF_FORMKIND_ACROFORM; + model->need_appearances = + acro_form->GetBooleanFor("NeedAppearances", false); + } + + // Phase 1: the declared field tree. + auto form = std::make_unique(doc); + const std::set initial_fields = + CollectFieldDicts(*form); + + // Phase 2: reconcile widgets only reachable through page /Annots. + const std::map widget_pages = + SweepPageWidgets(doc, form.get()); + + // Phase 3: detach into a plain snapshot. + const size_t field_count = CountFormFields(*form); + model->fields.reserve(field_count); + std::map field_index_by_dict; + for (size_t i = 0; i < field_count; ++i) { + CPDF_FormField* field = form->GetField(i, WideString()); + if (!field) { + continue; + } + FieldRecord record = SnapshotField(field, initial_fields, widget_pages); + const int index = fxcrt::CollectionSize(model->fields); + field_index_by_dict.try_emplace(field->GetFieldDict().Get(), index); + if (record.objnum != 0) { + model->field_index_by_objnum.try_emplace(record.objnum, index); + } + for (const WidgetRecord& widget : record.widgets) { + if (widget.objnum != 0) { + model->field_index_by_widget_objnum.try_emplace(widget.objnum, index); + } + } + model->fields.push_back(std::move(record)); + } + + const int calculation_count = form->CountFieldsInCalculationOrder(); + model->calculation_order.reserve(calculation_count); + for (int i = 0; i < calculation_count; ++i) { + CPDF_FormField* field = form->GetFieldInCalculationOrder(i); + const auto it = field + ? field_index_by_dict.find(field->GetFieldDict().Get()) + : field_index_by_dict.end(); + model->calculation_order.push_back( + it != field_index_by_dict.end() ? it->second : -1); + } + + return HandleFromFormModel(model.release()); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDFForm_CloseModel(EPDF_FORM_MODEL model) { + delete FormModelFromHandle(model); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFormKind(EPDF_FORM_MODEL model) { + FormModel* form = FormModelFromHandle(model); + return form ? form->kind : EPDF_FORMKIND_NONE; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_GetNeedAppearances(EPDF_FORM_MODEL model) { + FormModel* form = FormModelFromHandle(model); + return form && form->need_appearances; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFields(EPDF_FORM_MODEL model) { + FormModel* form = FormModelFromHandle(model); + return form ? fxcrt::CollectionSize(form->fields) : 0; +} + +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFForm_GetFieldActionModel(EPDF_FORM_MODEL model, + int field_index, + int event) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field || event < EPDF_FORM_ACTION_KEYSTROKE || + event > EPDF_FORM_ACTION_CALCULATE) { + return nullptr; + } + return epdf::MakeActionModelHandle( + field->actions[static_cast(event)]); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_CountCalculationOrder(EPDF_FORM_MODEL model) { + FormModel* form = FormModelFromHandle(model); + return form ? fxcrt::CollectionSize(form->calculation_order) : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetCalculationOrderFieldIndex(EPDF_FORM_MODEL model, int order_index) { + FormModel* form = FormModelFromHandle(model); + if (!form || order_index < 0 || + order_index >= fxcrt::CollectionSize(form->calculation_order)) { + return -1; + } + return form->calculation_order[order_index]; +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldObjNum(EPDF_FORM_MODEL model, int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->objnum : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldFamily(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->family : EPDF_FORMFIELD_FAMILY_UNKNOWN; +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV EPDFForm_GetFieldFlags(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->flags : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldOrigin(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->origin : -1; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + field->fqn, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldAlternateName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + field->alternate_name, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldMappingName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + field->mapping_name, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldValueKind(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->value.kind : EPDF_FORM_VALUE_NONE; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldValues(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? fxcrt::CollectionSize(field->value.values) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldValueAt(EPDF_FORM_MODEL model, + int field_index, + int value_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field || value_index < 0 || + value_index >= fxcrt::CollectionSize(field->value.values)) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + field->value.values[value_index], + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldDefaultValueKind(EPDF_FORM_MODEL model, int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->default_value.kind : EPDF_FORM_VALUE_NONE; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_CountFieldDefaultValues(EPDF_FORM_MODEL model, int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? fxcrt::CollectionSize(field->default_value.values) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldDefaultValueAt(EPDF_FORM_MODEL model, + int field_index, + int value_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const FieldRecord* field = GetFieldRecord(model, field_index); + if (!field || value_index < 0 || + value_index >= fxcrt::CollectionSize(field->default_value.values)) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + field->default_value.values[value_index], + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldMaxLen(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? field->max_len : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldOptions(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? fxcrt::CollectionSize(field->options) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldOptionLabel(EPDF_FORM_MODEL model, + int field_index, + int option_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const OptionRecord* option = + GetOptionRecord(model, field_index, option_index); + if (!option) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + option->label, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldOptionValue(EPDF_FORM_MODEL model, + int field_index, + int option_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const OptionRecord* option = + GetOptionRecord(model, field_index, option_index); + if (!option) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + option->value, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_IsFieldOptionSelected(EPDF_FORM_MODEL model, + int field_index, + int option_index) { + const OptionRecord* option = + GetOptionRecord(model, field_index, option_index); + return option && option->selected; +} + +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldWidgets(EPDF_FORM_MODEL model, + int field_index) { + const FieldRecord* field = GetFieldRecord(model, field_index); + return field ? fxcrt::CollectionSize(field->widgets) : 0; +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldWidgetObjNum(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + const WidgetRecord* widget = + GetWidgetRecord(model, field_index, widget_index); + return widget ? widget->objnum : 0; +} + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldWidgetPageObjNum(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + const WidgetRecord* widget = + GetWidgetRecord(model, field_index, widget_index); + return widget ? widget->page_objnum : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldWidgetOnState(EPDF_FORM_MODEL model, + int field_index, + int widget_index, + void* buffer, + unsigned long buflen) { + const WidgetRecord* widget = + GetWidgetRecord(model, field_index, widget_index); + if (!widget) { + return 0; + } + return NulTerminateMaybeCopyAndReturnLength( + widget->on_state, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldWidgetExportValue(EPDF_FORM_MODEL model, + int field_index, + int widget_index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + const WidgetRecord* widget = + GetWidgetRecord(model, field_index, widget_index); + if (!widget) { + return 0; + } + return Utf16EncodeMaybeCopyAndReturnLength( + widget->export_value, + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_IsFieldWidgetChecked(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + const WidgetRecord* widget = + GetWidgetRecord(model, field_index, widget_index); + return widget && widget->checked; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldIndexByObjNum(EPDF_FORM_MODEL model, uint32_t field_objnum) { + FormModel* form = FormModelFromHandle(model); + if (!form || field_objnum == 0) { + return -1; + } + const auto it = form->field_index_by_objnum.find(field_objnum); + return it != form->field_index_by_objnum.end() ? it->second : -1; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldIndexForWidget(EPDF_FORM_MODEL model, uint32_t widget_objnum) { + FormModel* form = FormModelFromHandle(model); + if (!form || widget_objnum == 0) { + return -1; + } + const auto it = form->field_index_by_widget_objnum.find(widget_objnum); + return it != form->field_index_by_widget_objnum.end() ? it->second : -1; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetToggle(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_BYTESTRING on_state, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return false; + } + return ApplyToggle(doc, /*reconciled=*/nullptr, field_objnum, + ByteString(on_state ? on_state : ""), + /*lenient_unknown_state=*/false, changed_widget_objnums, + buffer_size, out_changed_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetTextValue(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return false; + } + return ApplyTextValue( + doc, /*reconciled=*/nullptr, field_objnum, + value ? WideStringFromFPDFWideString(value) : WideString(), + changed_widget_objnums, buffer_size, out_changed_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetChoiceValues(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* values, + unsigned long value_count, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || (value_count > 0 && !values)) { + return false; + } + std::vector new_values; + if (value_count > 0) { + pdfium::span values_span = + UNSAFE_BUFFERS(pdfium::span(values, static_cast(value_count))); + for (FPDF_WIDESTRING wide_value : values_span) { + new_values.push_back(wide_value ? WideStringFromFPDFWideString(wide_value) + : WideString()); + } + } + return ApplyChoiceValues(doc, /*reconciled=*/nullptr, field_objnum, + new_values, changed_widget_objnums, buffer_size, + out_changed_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ResetField(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field) { + return false; + } + const ByteString field_type = InheritedFieldType(field.Get()); + const uint32_t flags = InheritedFieldFlags(field.Get()); + RetainPtr default_value = + CPDF_FormField::GetFieldAttrForDict(field.Get(), + pdfium::form_fields::kDV); + + if (field_type == pdfium::form_fields::kBtn) { + if (flags & pdfium::form_flags::kButtonPushbutton) { + return false; + } + return ApplyToggleDefault(doc, field_objnum, default_value.Get(), + changed_widget_objnums, buffer_size, + out_changed_count); + } + + if (field_type != pdfium::form_fields::kTx && + field_type != pdfium::form_fields::kCh) { + return false; // Push buttons handled above; signatures are never reset. + } + + if (field_type == pdfium::form_fields::kCh) { + std::vector defaults; + if (default_value && !default_value->IsNull()) { + if (default_value->IsString()) { + WideString value = default_value->GetUnicodeText(); + // An empty choice default represents no selected option unless an + // option actually uses the empty export value (normalization below + // will retain it in that case). + defaults.push_back(std::move(value)); + } else if (const CPDF_Array* array = default_value->AsArray()) { + defaults.reserve(array->size()); + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr element = array->GetDirectObjectAt(i); + if (!element || !element->IsString()) { + return false; + } + defaults.push_back(element->GetUnicodeText()); + } + } else { + return false; + } + } + if (defaults.size() == 1 && defaults[0].IsEmpty()) { + std::optional normalized = + NormalizeChoiceValues(field.Get(), flags, defaults); + if (!normalized.has_value()) { + defaults.clear(); + } + } + return ApplyChoiceValues(doc, /*reconciled=*/nullptr, field_objnum, + defaults, changed_widget_objnums, buffer_size, + out_changed_count); + } + + if (default_value && !default_value->IsNull() && !default_value->IsString()) { + return false; + } + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, /*reconciled=*/nullptr, + &controls)) { + return false; + } + RetainPtr promoted_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return false; + } + if (default_value && !default_value->IsNull()) { + promoted_field->SetNewFor( + pdfium::form_fields::kV, + default_value->GetUnicodeText().AsStringView()); + } else { + if (HasInheritedFieldAttribute(doc, field.Get(), pdfium::form_fields::kV)) { + promoted_field->SetNewFor(pdfium::form_fields::kV, + WideStringView()); + } else { + promoted_field->RemoveFor(pdfium::form_fields::kV); + } + } + promoted_field->RemoveFor("RV"); + MirrorFieldValueToTwinControls(doc, promoted_field, controls); + return RegenerateControlAppearances( + doc, controls, promoted_field, CPDF_GenerateAP::kTextField, + changed_widget_objnums, buffer_size, out_changed_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDisplay(FPDF_DOCUMENT document, + uint32_t field_objnum, + int display, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc && + ApplyFieldDisplay(doc, field_objnum, display, changed_widget_objnums, + buffer_size, out_changed_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldAppearanceText(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING appearance_text, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !appearance_text) { + return false; + } + return ApplyFieldAppearanceText( + doc, field_objnum, WideStringFromFPDFWideString(appearance_text), + changed_widget_objnums, buffer_size, out_changed_count); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_ExportFDF(FPDF_DOCUMENT document, + FPDF_WIDESTRING pdf_path, + uint32_t export_flags, + void* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return 0; + } + std::unique_ptr form = BuildReconciledForm(doc); + const WideString path = + pdf_path ? WideStringFromFPDFWideString(pdf_path) : WideString(); + std::unique_ptr fdf = form->ExportToFDF( + path, !!(export_flags & EPDF_FORM_EXPORT_SKIP_EMPTY_REQUIRED)); + if (!fdf) { + return 0; + } + return CopyPayloadToBuffer(fdf->WriteToString(), buffer, buflen); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_ExportXFDF(FPDF_DOCUMENT document, + FPDF_WIDESTRING pdf_path, + uint32_t export_flags, + void* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return 0; + } + std::unique_ptr form = BuildReconciledForm(doc); + const WideString path = + pdf_path ? WideStringFromFPDFWideString(pdf_path) : WideString(); + const ByteString payload = + BuildXfdf(form.get(), path, + !!(export_flags & EPDF_FORM_EXPORT_SKIP_EMPTY_REQUIRED)); + return CopyPayloadToBuffer(payload, buffer, buflen); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ImportFDF(FPDF_DOCUMENT document, + const void* data, + unsigned long size, + EPDF_FORM_IMPORT_RESULT* out_result) { + if (out_result) { + *out_result = {}; + } + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !data || size == 0) { + return false; + } + pdfium::span payload = UNSAFE_BUFFERS(pdfium::span( + static_cast(data), static_cast(size))); + std::unique_ptr fdf = CFDF_Document::ParseMemory(payload); + if (!fdf || !fdf->GetRoot()) { + return false; + } + RetainPtr main_dict = + fdf->GetRoot()->GetDictFor("FDF"); + if (!main_dict) { + return false; + } + + std::unique_ptr form = BuildReconciledForm(doc); + ImportStats stats; + RetainPtr fields = main_dict->GetArrayFor("Fields"); + if (fields) { + WalkFdfFields(doc, form.get(), fields.Get(), WideString(), &stats, + /*depth=*/0); + } + WriteImportResult(stats, out_result); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ImportXFDF(FPDF_DOCUMENT document, + const void* data, + unsigned long size, + EPDF_FORM_IMPORT_RESULT* out_result) { + if (out_result) { + *out_result = {}; + } + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !data || size == 0) { + return false; + } + pdfium::span payload = UNSAFE_BUFFERS(pdfium::span( + static_cast(data), static_cast(size))); + auto stream = pdfium::MakeRetain(payload); + CFX_XMLParser parser(stream); + std::unique_ptr xml = parser.Parse(); + if (!xml || !xml->GetRoot()) { + return false; + } + CFX_XMLElement* xfdf = xml->GetRoot()->GetLocalTagName() == L"xfdf" + ? xml->GetRoot() + : FindXmlChildByTag(xml->GetRoot(), L"xfdf"); + if (!xfdf) { + return false; + } + + std::unique_ptr form = BuildReconciledForm(doc); + ImportStats stats; + CFX_XMLElement* fields = FindXmlChildByTag(xfdf, L"fields"); + if (fields) { + for (CFX_XMLNode* child = fields->GetFirstChild(); child; + child = child->GetNextSibling()) { + CFX_XMLElement* field_element = ToXMLElement(child); + if (field_element && field_element->GetLocalTagName() == L"field") { + WalkXfdfField(doc, form.get(), field_element, WideString(), &stats, + /*depth=*/0); + } + } + } + WriteImportResult(stats, out_result); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_Repair(FPDF_DOCUMENT document, + uint32_t repair_flags, + EPDF_FORM_REPAIR_REPORT* out_report) { + if (out_report) { + *out_report = {}; + } + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !doc->GetRoot()) { + return false; + } + EPDF_FORM_REPAIR_REPORT report = {}; + + std::unique_ptr form = BuildReconciledForm(doc); + const size_t field_count = form->CountFields(WideString()); + const bool bake = repair_flags & EPDF_FORM_REPAIR_BAKE_APPEARANCES; + const bool bake_all = bake && form->NeedConstructAP(); + + // ---- Plan (const reads only; a no-op repair must promote nothing). ---- + RetainPtr const_acro_form = + doc->GetRoot()->GetDictFor("AcroForm"); + RetainPtr const_fields = + const_acro_form ? const_acro_form->GetArrayFor("Fields") : nullptr; + + std::vector roots_to_link; + std::set seen_roots; + struct KidFix { + uint32_t field_objnum; + uint32_t widget_objnum; + }; + std::vector kid_fixes; + struct BakeStep { + uint32_t field_objnum; + int family; + }; + std::vector bake_fields; + + for (size_t i = 0; i < field_count; ++i) { + CPDF_FormField* field = form->GetField(i, WideString()); + if (!field) { + continue; + } + RetainPtr field_dict = field->GetFieldDict(); + + // Recovered roots -> /AcroForm /Fields. + RetainPtr root = ClimbToFieldRoot(doc, field_dict); + const uint32_t root_objnum = root->GetObjNum(); + if (!ArrayReferencesDict(const_fields.Get(), root_objnum, root.Get())) { + if (root_objnum == 0) { + ++report.fields_unrepairable; + } else if (seen_roots.insert(root_objnum).second) { + roots_to_link.push_back(root_objnum); + } + } + + // Stray widgets -> parent /Kids (only when /Parent already points at + // this field, so the fix is purely additive). + const uint32_t field_objnum = field_dict->GetObjNum(); + RetainPtr kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); + if (kids && field_objnum != 0) { + for (const auto& control : form->GetControlsForField(field)) { + RetainPtr widget = control->GetWidgetDict(); + if (widget.Get() == field_dict.Get() || widget->GetObjNum() == 0) { + continue; + } + if (ArrayReferencesDict(kids.Get(), widget->GetObjNum(), + widget.Get())) { + continue; + } + RetainPtr parent = + widget->GetDictFor(pdfium::form_fields::kParent); + if (parent && parent->GetObjNum() == field_objnum) { + kid_fixes.push_back({field_objnum, widget->GetObjNum()}); + } + } + } + + if (bake && field_objnum != 0) { + const int family = FamilyFromFieldType(field->GetType()); + if (family != EPDF_FORMFIELD_FAMILY_PUSHBUTTON && + family != EPDF_FORMFIELD_FAMILY_SIGNATURE && + family != EPDF_FORMFIELD_FAMILY_UNKNOWN) { + bake_fields.push_back({field_objnum, family}); + } + } + } + + // ---- Apply. ---- + if (!roots_to_link.empty()) { + bool created = false; + RetainPtr acro_form = + GetMutableAcroForm(doc, /*create_if_missing=*/true, &created); + if (!acro_form) { + return false; + } + report.acroform_created = created ? 1 : 0; + RetainPtr fields_array = + GetMutableArrayMember(doc, acro_form.Get(), "Fields"); + if (!fields_array) { + return false; + } + for (uint32_t objnum : roots_to_link) { + fields_array->AppendNew(doc, objnum); + ++report.fields_linked; + } + } + + for (const KidFix& fix : kid_fixes) { + RetainPtr field_dict = + ToDictionary(doc->GetMutableIndirectObject(fix.field_objnum)); + if (!field_dict) { + continue; + } + RetainPtr kids = GetMutableArrayMember( + doc, field_dict.Get(), pdfium::form_fields::kKids); + if (!kids) { + continue; + } + kids->AppendNew(doc, fix.widget_objnum); + ++report.widgets_linked; + } + + // The structural phase above may have linked fields and widgets; bake + // against a FRESH reconciled view so just-linked widgets participate. + std::unique_ptr bake_form; + if (!bake_fields.empty()) { + bake_form = BuildReconciledForm(doc); + } + for (const BakeStep& step : bake_fields) { + RetainPtr field_dict = + ResolveFieldDict(doc, step.field_objnum); + if (!field_dict) { + continue; + } + std::vector controls; + if (!CollectTxnControls(doc, field_dict.Get(), step.field_objnum, + /*want_toggle_info=*/false, bake_form.get(), + &controls)) { + continue; + } + RetainPtr promoted_field; + for (const TxnControl& control : controls) { + RetainPtr ap = control.dict->GetDictFor("AP"); + const bool has_normal_ap = ap && ap->GetObjectFor("N"); + if (has_normal_ap && !bake_all) { + continue; + } + if (!promoted_field && (control.merged || control.objnum == 0)) { + promoted_field = + ToDictionary(doc->GetMutableIndirectObject(step.field_objnum)); + if (!promoted_field) { + break; + } + } + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (!widget) { + continue; + } + switch (step.family) { + case EPDF_FORMFIELD_FAMILY_TEXT: + CPDF_GenerateAP::GenerateFormAP(doc, widget.Get(), + CPDF_GenerateAP::kTextField); + break; + case EPDF_FORMFIELD_FAMILY_COMBOBOX: + CPDF_GenerateAP::GenerateFormAP(doc, widget.Get(), + CPDF_GenerateAP::kComboBox); + break; + case EPDF_FORMFIELD_FAMILY_LISTBOX: + CPDF_GenerateAP::GenerateFormAP(doc, widget.Get(), + CPDF_GenerateAP::kListBox); + break; + case EPDF_FORMFIELD_FAMILY_CHECKBOX: + CPDF_GenerateAP::GenerateCheckboxFormAP(doc, widget.Get()); + break; + case EPDF_FORMFIELD_FAMILY_RADIO: + CPDF_GenerateAP::GenerateRadioButtonFormAP(doc, widget.Get()); + break; + default: + continue; + } + ++report.appearances_baked; + } + } + + if (bake_all) { + RetainPtr acro_form = + GetMutableAcroForm(doc, /*create_if_missing=*/false, nullptr); + if (acro_form) { + acro_form->RemoveFor("NeedAppearances"); + report.need_appearances_cleared = 1; + } + } + + if (out_report) { + *out_report = report; + } + return true; +} + +// --------------------------------------------------------------------------- +// Authoring: field lifecycle and adoption. +// --------------------------------------------------------------------------- + +namespace { + +// Family-defining /Ff bits are immutable through EPDFForm_SetFieldFlags. +constexpr uint32_t kFamilyDefiningFlags = + pdfium::form_flags::kButtonRadio | pdfium::form_flags::kButtonPushbutton | + pdfium::form_flags::kChoiceCombo; + +// Widget-plane keys that move to the new kid when a legacy merged field is +// split by EPDFForm_AttachWidget. Field-plane keys (/FT /T /Ff /V /DV /Opt +// /MaxLen /TU /TM /DA /Q /AA) stay on the field dictionary. +constexpr const char* kWidgetPlaneKeys[] = { + "Type", "Subtype", "Rect", "AP", "AS", "MK", "BS", "Border", + "F", "P", "H", "OC", "CA", "NM", "M", "StructParent", +}; + +struct AuthorFamily { + ByteString field_type; + uint32_t flags; + bool toggle; +}; + +bool AuthorFamilyFromCode(int family, AuthorFamily* out) { + switch (family) { + case 4 /* EPDF_FORMFIELD_FAMILY_TEXT */: + *out = {pdfium::form_fields::kTx, 0, false}; + return true; + case 2 /* CHECKBOX */: + *out = {pdfium::form_fields::kBtn, 0, true}; + return true; + case 3 /* RADIO */: + *out = {pdfium::form_fields::kBtn, pdfium::form_flags::kButtonRadio, + true}; + return true; + case 5 /* COMBOBOX */: + *out = {pdfium::form_fields::kCh, pdfium::form_flags::kChoiceCombo, + false}; + return true; + case 6 /* LISTBOX */: + *out = {pdfium::form_fields::kCh, 0, false}; + return true; + default: + return false; + } +} + +int FamilyOfFieldDict(const CPDF_Dictionary* field_dict) { + const ByteString field_type = InheritedFieldType(field_dict); + const uint32_t flags = InheritedFieldFlags(field_dict); + if (field_type == pdfium::form_fields::kBtn) { + if (flags & pdfium::form_flags::kButtonPushbutton) { + return 1; + } + if (flags & pdfium::form_flags::kButtonRadio) { + return 3; + } + return 2; + } + if (field_type == pdfium::form_fields::kTx) { + return 4; + } + if (field_type == pdfium::form_fields::kCh) { + return (flags & pdfium::form_flags::kChoiceCombo) ? 5 : 6; + } + if (field_type == pdfium::form_fields::kSig) { + return 7; + } + return 0; +} + +std::vector SplitFqnSegments(const WideString& full_name) { + std::vector segments; + size_t start = 0; + while (start <= full_name.GetLength()) { + std::optional dot = full_name.Find(L'.', start); + const size_t end = dot.value_or(full_name.GetLength()); + if (end == start) { + return {}; // empty segment -> invalid + } + segments.push_back(full_name.Substr(start, end - start)); + if (!dot.has_value()) { + break; + } + start = dot.value() + 1; + } + return segments; +} + +// Find a direct child (of /Fields or a /Kids array) whose own /T equals +// |segment|, resolving every entry through the document. +RetainPtr FindChildFieldByName( + CPDF_Document* doc, + const CPDF_Array* entries, + const WideString& segment) { + if (!entries) { + return nullptr; + } + for (size_t i = 0; i < entries->size(); ++i) { + RetainPtr element = entries->GetObjectAt(i); + if (!element) { + continue; + } + RetainPtr child; + if (const CPDF_Reference* ref = element->AsReference()) { + child = ToDictionary(doc->GetOrParseIndirectObject(ref->GetRefObjNum())); + } else { + child = ToDictionary(std::move(element)); + } + if (child && child->GetUnicodeTextFor(pdfium::form_fields::kT) == segment) { + return child; + } + } + return nullptr; +} + +bool RemoveObjNumFromMutableArray(CPDF_Array* array, uint32_t objnum) { + if (!array) { + return false; + } + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr element = array->GetObjectAt(i); + const CPDF_Reference* ref = element ? element->AsReference() : nullptr; + if (ref && ref->GetRefObjNum() == objnum) { + array->RemoveAt(i); + return true; + } + } + return false; +} + +// Locate the page whose /Annots references |annot_objnum|. Page-tree walk +// only; returns the page's object number or 0. +uint32_t FindPageContainingAnnot(CPDF_Document* doc, uint32_t annot_objnum) { + const int page_count = doc->GetPageCount(); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = doc->GetPageDictionary(i); + if (!page) { + continue; + } + RetainPtr annots = page->GetArrayFor("Annots"); + if (!annots) { + continue; + } + for (size_t j = 0; j < annots->size(); ++j) { + RetainPtr element = annots->GetObjectAt(j); + const CPDF_Reference* ref = element ? element->AsReference() : nullptr; + if (ref && ref->GetRefObjNum() == annot_objnum) { + return page->GetObjNum(); + } + } + } + return 0; +} + +// After toggle AP generation, make sure the /AP /N "on" state carries the +// requested name so EPDFForm_SetToggle can address it. +void NormalizeToggleOnState(CPDF_Dictionary* widget, + const ByteString& on_state) { + RetainPtr ap = widget->GetMutableDictFor("AP"); + if (!ap) { + return; + } + RetainPtr normal = ap->GetMutableDictFor("N"); + if (!normal) { + return; + } + ByteString current_on; + { + CPDF_DictionaryLocker locker(normal); + for (const auto& it : locker) { + if (it.first != kOffState) { + current_on = it.first; + break; + } + } + } + if (current_on.IsEmpty() || current_on == on_state) { + return; + } + RetainPtr stream = + normal->GetMutableObjectFor(current_on.AsStringView()); + if (!stream) { + return; + } + normal->SetFor(on_state, stream->Clone()); + normal->RemoveFor(current_on.AsStringView()); +} + +// Bake the family-correct appearance for an attached widget. +void BakeWidgetAppearance(CPDF_Document* doc, + CPDF_Dictionary* widget, + int family, + const ByteString& on_state) { + switch (family) { + case 2: // checkbox + CPDF_GenerateAP::GenerateCheckboxFormAP(doc, widget); + NormalizeToggleOnState(widget, on_state); + break; + case 3: // radio + CPDF_GenerateAP::GenerateRadioButtonFormAP(doc, widget); + NormalizeToggleOnState(widget, on_state); + break; + case 4: + CPDF_GenerateAP::GenerateFormAP(doc, widget, CPDF_GenerateAP::kTextField); + break; + case 5: + CPDF_GenerateAP::GenerateFormAP(doc, widget, CPDF_GenerateAP::kComboBox); + break; + case 6: + CPDF_GenerateAP::GenerateFormAP(doc, widget, CPDF_GenerateAP::kListBox); + break; + default: + break; + } +} + +} // namespace + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_CreateField(FPDF_DOCUMENT document, + int family, + FPDF_WIDESTRING full_name) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + AuthorFamily author; + if (!doc || !doc->GetRoot() || !AuthorFamilyFromCode(family, &author)) { + return 0; + } + const WideString name = + full_name ? WideStringFromFPDFWideString(full_name) : WideString(); + const std::vector segments = SplitFqnSegments(name); + if (segments.empty()) { + return 0; + } + + // ---- Plan (const reads only): walk existing nodes, find conflicts. ---- + // existing_path[i] holds the object number of the node matching + // segments[i], for the leading run of segments that already exist. + std::vector existing_path; + { + RetainPtr acro_form = + doc->GetRoot()->GetDictFor("AcroForm"); + RetainPtr entries = + acro_form ? acro_form->GetArrayFor("Fields") : nullptr; + const CPDF_Array* level = entries.Get(); + RetainPtr keep_alive = entries; + for (size_t i = 0; i < segments.size(); ++i) { + RetainPtr found = + FindChildFieldByName(doc, level, segments[i]); + if (!found) { + break; + } + if (i + 1 == segments.size()) { + return 0; // sibling name collision at the terminal level + } + if (found->KeyExist(pdfium::form_fields::kFT)) { + return 0; // cannot nest under a terminal field + } + if (found->GetObjNum() == 0) { + return 0; // direct-object intermediate: not authorable + } + existing_path.push_back(found->GetObjNum()); + keep_alive = found->GetArrayFor(pdfium::form_fields::kKids); + level = keep_alive.Get(); + } + } + + // ---- Apply. ---- + bool created = false; + RetainPtr acro_form = + GetMutableAcroForm(doc, /*create_if_missing=*/true, &created); + if (!acro_form) { + return 0; + } + + RetainPtr parent_array = + GetMutableArrayMember(doc, acro_form.Get(), "Fields"); + RetainPtr parent_field; // null at the root level + for (uint32_t objnum : existing_path) { + parent_field = ToDictionary(doc->GetMutableIndirectObject(objnum)); + if (!parent_field) { + return 0; + } + parent_array = GetMutableArrayMember(doc, parent_field.Get(), + pdfium::form_fields::kKids); + } + if (!parent_array) { + return 0; + } + + for (size_t i = existing_path.size(); i < segments.size(); ++i) { + auto node = doc->NewIndirect(); + node->SetNewFor(pdfium::form_fields::kT, + segments[i].AsStringView()); + if (parent_field) { + node->SetNewFor(pdfium::form_fields::kParent, doc, + parent_field->GetObjNum()); + } + const bool terminal = i + 1 == segments.size(); + if (terminal) { + node->SetNewFor(pdfium::form_fields::kFT, author.field_type); + if (author.flags != 0) { + node->SetNewFor(pdfium::form_fields::kFf, + static_cast(author.flags)); + } + } + parent_array->AppendNew(doc, node->GetObjNum()); + if (terminal) { + return node->GetObjNum(); + } + parent_field = node; + parent_array = GetMutableArrayMember(doc, parent_field.Get(), + pdfium::form_fields::kKids); + if (!parent_array) { + return 0; + } + } + return 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_AttachWidget(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t widget_objnum, + FPDF_BYTESTRING on_state) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || widget_objnum == 0 || + field_objnum == widget_objnum) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()).IsEmpty()) { + return false; // must address the terminal field dictionary itself + } + const int family = FamilyOfFieldDict(field.Get()); + if (family == 0 || family == 1 || family == 7) { + return false; // unknown / pushbutton / signature are not authorable + } + const bool toggle = family == 2 || family == 3; + const ByteString state(on_state ? on_state : ""); + if (toggle && (state.IsEmpty() || state == kOffState)) { + return false; // toggles need a real on-state name + } + + RetainPtr widget = + ToDictionary(doc->GetOrParseIndirectObject(widget_objnum)); + if (!widget || widget->GetNameFor("Subtype") != "Widget" || + widget->KeyExist(pdfium::form_fields::kParent) || + widget->KeyExist(pdfium::form_fields::kFT)) { + return false; // must be an unattached, non-merged widget annotation + } + + // ---- Apply. ---- + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + + // Legacy merged field: split it first. The field keeps its object number; + // the previously merged widget half moves into a new kid annotation. + if (mutable_field->GetNameFor("Subtype") == "Widget") { + const uint32_t page_objnum = FindPageContainingAnnot(doc, field_objnum); + auto split_widget = doc->NewIndirect(); + for (const char* key : kWidgetPlaneKeys) { + RetainPtr value = mutable_field->GetMutableObjectFor(key); + if (!value) { + continue; + } + split_widget->SetFor(key, value->Clone()); + mutable_field->RemoveFor(key); + } + split_widget->SetNewFor("Type", "Annot"); + split_widget->SetNewFor("Subtype", "Widget"); + split_widget->SetNewFor(pdfium::form_fields::kParent, doc, + field_objnum); + RetainPtr kids = GetMutableArrayMember( + doc, mutable_field.Get(), pdfium::form_fields::kKids); + if (!kids) { + return false; + } + kids->AppendNew(doc, split_widget->GetObjNum()); + if (page_objnum != 0) { + RetainPtr page = + ToDictionary(doc->GetMutableIndirectObject(page_objnum)); + RetainPtr annots = + page ? page->GetMutableArrayFor("Annots") : nullptr; + if (annots && RemoveObjNumFromMutableArray(annots.Get(), field_objnum)) { + annots->AppendNew(doc, split_widget->GetObjNum()); + } + } + } + + RetainPtr mutable_widget = + ToDictionary(doc->GetMutableIndirectObject(widget_objnum)); + if (!mutable_widget) { + return false; + } + mutable_widget->SetNewFor(pdfium::form_fields::kParent, doc, + field_objnum); + RetainPtr kids = GetMutableArrayMember( + doc, mutable_field.Get(), pdfium::form_fields::kKids); + if (!kids) { + return false; + } + kids->AppendNew(doc, widget_objnum); + + if (toggle) { + mutable_widget->SetNewFor("AS", kOffState); + BakeWidgetAppearance(doc, mutable_widget.Get(), family, state); + // The generator may key the "on" stream off a default; make sure the + // requested state name is addressable even when generation bailed. + NormalizeToggleOnState(mutable_widget.Get(), state); + } else { + BakeWidgetAppearance(doc, mutable_widget.Get(), family, ByteString()); + } + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_DetachWidget(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t widget_objnum) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || widget_objnum == 0) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + RetainPtr widget = + ToDictionary(doc->GetOrParseIndirectObject(widget_objnum)); + if (!field || !widget) { + return false; + } + RetainPtr parent = + widget->GetDictFor(pdfium::form_fields::kParent); + if (!parent || parent->GetObjNum() != field_objnum) { + return false; + } + RetainPtr kids = + field->GetArrayFor(pdfium::form_fields::kKids); + if (!ArrayReferencesDict(kids.Get(), widget_objnum, widget.Get())) { + return false; + } + + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + RetainPtr mutable_widget = + ToDictionary(doc->GetMutableIndirectObject(widget_objnum)); + if (!mutable_field || !mutable_widget) { + return false; + } + RetainPtr mutable_kids = GetMutableArrayMember( + doc, mutable_field.Get(), pdfium::form_fields::kKids); + if (!mutable_kids || + !RemoveObjNumFromMutableArray(mutable_kids.Get(), widget_objnum)) { + return false; + } + // An empty /Kids array would make CPDF_InteractiveForm skip the field + // entirely; drop the key so the field stays visible as "unplaced". + if (mutable_kids->IsEmpty()) { + mutable_field->RemoveFor(pdfium::form_fields::kKids); + } + mutable_widget->RemoveFor(pdfium::form_fields::kParent); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_DeleteField(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t* out_detached_widgets, + unsigned long buffer_size, + unsigned long* out_detached_count) { + if (out_detached_count) { + *out_detached_count = 0; + } + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()).IsEmpty()) { + return false; + } + + // Collect widget kids (a terminal field's kids are widgets; a kid with + // /T is a child FIELD, which makes this node non-terminal -> fail). + std::vector widget_objnums; + RetainPtr kids = + field->GetArrayFor(pdfium::form_fields::kKids); + if (kids) { + for (size_t i = 0; i < kids->size(); ++i) { + RetainPtr element = kids->GetObjectAt(i); + const CPDF_Reference* ref = element ? element->AsReference() : nullptr; + if (!ref) { + return false; // direct kid: not authorable + } + RetainPtr kid = + ToDictionary(doc->GetOrParseIndirectObject(ref->GetRefObjNum())); + if (!kid) { + continue; + } + if (kid->KeyExist(pdfium::form_fields::kT)) { + return false; // non-terminal field + } + widget_objnums.push_back(ref->GetRefObjNum()); + } + } + + // ---- Apply: detach widgets, unlink the field, prune empty ancestors. ---- + for (uint32_t objnum : widget_objnums) { + RetainPtr widget = + ToDictionary(doc->GetMutableIndirectObject(objnum)); + if (widget) { + widget->RemoveFor(pdfium::form_fields::kParent); + } + } + + // Walk up: remove |current| from its container; prune empty non-terminal + // ancestors (never the /AcroForm itself). + uint32_t current = field_objnum; + for (int depth = 0; depth < 32; ++depth) { + RetainPtr node = + ToDictionary(doc->GetOrParseIndirectObject(current)); + if (!node) { + break; + } + RetainPtr parent = + node->GetDictFor(pdfium::form_fields::kParent); + if (parent && parent->GetObjNum() != 0) { + RetainPtr mutable_parent = + ToDictionary(doc->GetMutableIndirectObject(parent->GetObjNum())); + RetainPtr parent_kids = GetMutableArrayMember( + doc, mutable_parent.Get(), pdfium::form_fields::kKids); + if (!parent_kids || + !RemoveObjNumFromMutableArray(parent_kids.Get(), current)) { + break; + } + if (!parent_kids->IsEmpty() || + mutable_parent->KeyExist(pdfium::form_fields::kFT)) { + break; // parent still has children, or is itself a real field + } + mutable_parent->RemoveFor(pdfium::form_fields::kKids); + current = parent->GetObjNum(); // parent is now empty: prune it too + continue; + } + // Root level: remove from /AcroForm /Fields. + RetainPtr acro_form = + GetMutableAcroForm(doc, /*create_if_missing=*/false, nullptr); + if (acro_form) { + RetainPtr fields = + GetMutableArrayMember(doc, acro_form.Get(), "Fields"); + if (fields) { + RemoveObjNumFromMutableArray(fields.Get(), current); + } + } + break; + } + + ReportChangedWidgets(widget_objnums, + static_cast(widget_objnums.size()), + out_detached_widgets, buffer_size, out_detached_count); + return true; +} + +// --------------------------------------------------------------------------- +// Authoring: field-plane property setters. +// --------------------------------------------------------------------------- + +namespace { + +// The array holding this field: the parent field's /Kids, or /AcroForm +// /Fields at the root. Const view for sibling checks. +RetainPtr SiblingArrayOf(CPDF_Document* doc, + const CPDF_Dictionary* field) { + RetainPtr parent = + field->GetDictFor(pdfium::form_fields::kParent); + if (parent) { + if (parent->GetObjNum() != 0) { + parent = ToDictionary(doc->GetOrParseIndirectObject(parent->GetObjNum())); + } + return parent ? parent->GetArrayFor(pdfium::form_fields::kKids) : nullptr; + } + const CPDF_Dictionary* root = doc->GetRoot(); + RetainPtr acro_form = + root ? root->GetDictFor("AcroForm") : nullptr; + return acro_form ? acro_form->GetArrayFor("Fields") : nullptr; +} + +// Regenerate appearances after a field-plane change that affects rendering +// (options, flags, MaxLen). No-op for families without generated text APs. +void RegenerateFieldAppearances(CPDF_Document* doc, uint32_t field_objnum) { + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field) { + return; + } + const int family = FamilyOfFieldDict(field.Get()); + if (family != 4 && family != 5 && family != 6) { + return; + } + std::vector controls; + if (!CollectTxnControls(doc, field.Get(), field_objnum, + /*want_toggle_info=*/false, /*reconciled=*/nullptr, + &controls)) { + return; + } + RetainPtr promoted_field; + for (const TxnControl& control : controls) { + if (!promoted_field && (control.merged || control.objnum == 0)) { + promoted_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!promoted_field) { + return; + } + } + RetainPtr widget = + MutableControlDict(doc, control, promoted_field); + if (widget) { + BakeWidgetAppearance(doc, widget.Get(), family, ByteString()); + } + } +} + +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING partial_name) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0) { + return false; + } + const WideString name = + partial_name ? WideStringFromFPDFWideString(partial_name) : WideString(); + if (name.IsEmpty() || name.Find(L'.', 0).has_value()) { + return false; + } + + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || !field->KeyExist(pdfium::form_fields::kT)) { + return false; + } + + RetainPtr siblings = SiblingArrayOf(doc, field.Get()); + if (siblings) { + for (size_t i = 0; i < siblings->size(); ++i) { + RetainPtr element = siblings->GetObjectAt(i); + if (!element) { + continue; + } + RetainPtr sibling; + if (const CPDF_Reference* ref = element->AsReference()) { + if (ref->GetRefObjNum() == field_objnum) { + continue; + } + sibling = + ToDictionary(doc->GetOrParseIndirectObject(ref->GetRefObjNum())); + } else { + sibling = ToDictionary(std::move(element)); + if (sibling.Get() == field.Get()) { + continue; + } + } + if (sibling && + sibling->GetUnicodeTextFor(pdfium::form_fields::kT) == name) { + return false; // sibling name collision + } + } + } + + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + mutable_field->SetNewFor(pdfium::form_fields::kT, + name.AsStringView()); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldFlags(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t set_bits, + uint32_t clear_bits) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0) { + return false; + } + if ((set_bits | clear_bits) & kFamilyDefiningFlags) { + return false; + } + + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()).IsEmpty()) { + return false; + } + + const uint32_t current = InheritedFieldFlags(field.Get()); + const uint32_t next = (current & ~clear_bits) | set_bits; + if (next == current) { + return true; + } + if (InheritedFieldType(field.Get()) == pdfium::form_fields::kCh && + ((current ^ next) & (pdfium::form_flags::kChoiceEdit | + pdfium::form_flags::kChoiceMultiSelect))) { + const bool is_combo = next & pdfium::form_flags::kChoiceCombo; + const bool is_edit = next & pdfium::form_flags::kChoiceEdit; + const bool is_multi = next & pdfium::form_flags::kChoiceMultiSelect; + if ((is_combo && is_multi) || (!is_combo && is_edit)) { + return false; + } + RetainPtr options = + ToArray(CPDF_FormField::GetFieldAttrForDict(field.Get(), "Opt")); + auto value_is_compatible = [&](ByteStringView key) { + RetainPtr value = + CPDF_FormField::GetFieldAttrForDict(field.Get(), key); + if (!value || value->IsNull()) { + return true; + } + if (value->IsArray()) { + return is_multi && !is_combo; + } + if (!value->IsString()) { + return false; + } + const WideString text = value->GetUnicodeText(); + if (text.IsEmpty() || !is_combo || is_edit) { + return true; + } + if (!options) { + return false; + } + for (size_t i = 0; i < options->size(); ++i) { + if (OptExportAt(options.Get(), i) == text) { + return true; + } + } + return false; + }; + if (!value_is_compatible(pdfium::form_fields::kV) || + !value_is_compatible(pdfium::form_fields::kDV)) { + return false; + } + } + + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + mutable_field->SetNewFor(pdfium::form_fields::kFf, + static_cast(next)); + // Rendering-relevant text/choice bits (multiline, comb, ...) changed. + RegenerateFieldAppearances(doc, field_objnum); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldMaxLen(FPDF_DOCUMENT document, + uint32_t field_objnum, + int max_len) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || max_len < 0) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()) != pdfium::form_fields::kTx) { + return false; + } + if (max_len > 0) { + RetainPtr value = CPDF_FormField::GetFieldAttrForDict( + field.Get(), pdfium::form_fields::kV); + if (value && + value->GetUnicodeText().GetLength() > static_cast(max_len)) { + return false; // never truncate an existing value implicitly + } + } + RetainPtr current_max_len = + CPDF_FormField::GetFieldAttrForDict(field.Get(), "MaxLen"); + if ((!current_max_len && max_len == 0) || + (current_max_len && current_max_len->IsNumber() && + current_max_len->GetInteger() == max_len)) { + return true; + } + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + // Keep a local zero so clearing an inherited limit has an effective result + // without mutating the ancestor (and therefore its sibling fields). + mutable_field->SetNewFor("MaxLen", max_len); + RegenerateFieldAppearances(doc, field_objnum); // comb cells follow MaxLen + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDefaultValues(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* values, + unsigned long value_count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || value_count == 0 || !values) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field) { + return false; + } + const ByteString field_type = InheritedFieldType(field.Get()); + if (field_type != pdfium::form_fields::kTx && + field_type != pdfium::form_fields::kCh) { + return false; + } + std::vector defaults; + pdfium::span values_span = + UNSAFE_BUFFERS(pdfium::span(values, static_cast(value_count))); + defaults.reserve(values_span.size()); + for (FPDF_WIDESTRING value : values_span) { + defaults.push_back(value ? WideStringFromFPDFWideString(value) + : WideString()); + } + + std::optional normalized; + if (field_type == pdfium::form_fields::kTx) { + if (defaults.size() != 1) { + return false; + } + RetainPtr current = CPDF_FormField::GetFieldAttrForDict( + field.Get(), pdfium::form_fields::kDV); + if (current && current->IsString() && + current->GetUnicodeText() == defaults[0]) { + return true; + } + } else { + normalized = NormalizeChoiceValues( + field.Get(), InheritedFieldFlags(field.Get()), defaults); + if (!normalized.has_value()) { + return false; + } + } + + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + if (field_type == pdfium::form_fields::kTx) { + mutable_field->SetNewFor(pdfium::form_fields::kDV, + defaults[0].AsStringView()); + } else { + WriteChoiceDefaultValues(mutable_field.Get(), defaults, normalized.value()); + } + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDefaultToggle(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_BYTESTRING on_state) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || !on_state || on_state[0] == '\0') { + return false; + } + ToggleContext ctx; + if (!PrepareToggle(doc, /*reconciled=*/nullptr, field_objnum, &ctx)) { + return false; + } + + const ByteString requested(on_state); + ByteString stored_default; + if (requested == kOffState) { + stored_default = kOffState; + } else { + RetainPtr opt_array = + ToArray(CPDF_FormField::GetFieldAttrForDict(ctx.field.Get(), "Opt")); + for (size_t i = 0; i < ctx.controls.size(); ++i) { + if (ctx.controls[i].on_state == requested) { + stored_default = + opt_array ? ByteString::FormatInteger(pdfium::checked_cast(i)) + : requested; + break; + } + } + if (stored_default.IsEmpty()) { + return false; + } + } + + RetainPtr current = CPDF_FormField::GetFieldAttrForDict( + ctx.field.Get(), pdfium::form_fields::kDV); + if (current && current->IsName() && current->GetString() == stored_default) { + return true; + } + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + mutable_field->SetNewFor(pdfium::form_fields::kDV, stored_default); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_RemoveFieldDefaultValue(FPDF_DOCUMENT document, + uint32_t field_objnum) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()).IsEmpty()) { + return false; + } + if (!field->KeyExist(pdfium::form_fields::kDV)) { + return true; + } + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + mutable_field->RemoveFor(pdfium::form_fields::kDV); + return true; +} + +namespace { + +FPDF_BOOL SetOptionalFieldText(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value, + const char* key) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()).IsEmpty()) { + return false; + } + const WideString text = + value ? WideStringFromFPDFWideString(value) : WideString(); + RetainPtr current = + CPDF_FormField::GetFieldAttrForDict(field.Get(), key); + if ((!current && text.IsEmpty()) || + (current && current->IsString() && current->GetUnicodeText() == text)) { + return true; + } + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + // An empty local string shadows an inherited value. Removing the key would + // make the ancestor's value effective again and would not actually clear + // what EPDFForm_LoadModel reports. + mutable_field->SetNewFor(key, text.AsStringView()); + return true; +} + +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldAlternateName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value) { + return SetOptionalFieldText(document, field_objnum, value, "TU"); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldMappingName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value) { + return SetOptionalFieldText(document, field_objnum, value, "TM"); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldOptions(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* labels, + const FPDF_WIDESTRING* exports, + unsigned long count) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || field_objnum == 0 || (count > 0 && (!labels || !exports))) { + return false; + } + RetainPtr field = ResolveFieldDict(doc, field_objnum); + if (!field || InheritedFieldType(field.Get()) != pdfium::form_fields::kCh) { + return false; + } + const uint32_t flags = InheritedFieldFlags(field.Get()); + const bool free_text_combo = (flags & pdfium::form_flags::kChoiceCombo) && + (flags & pdfium::form_flags::kChoiceEdit); + + std::vector new_labels; + std::vector new_exports; + if (count > 0) { + pdfium::span labels_span = + UNSAFE_BUFFERS(pdfium::span(labels, static_cast(count))); + pdfium::span exports_span = + UNSAFE_BUFFERS(pdfium::span(exports, static_cast(count))); + for (unsigned long i = 0; i < count; ++i) { + new_labels.push_back(labels_span[i] + ? WideStringFromFPDFWideString(labels_span[i]) + : WideString()); + new_exports.push_back(exports_span[i] + ? WideStringFromFPDFWideString(exports_span[i]) + : WideString()); + } + } + + RetainPtr current_value = + CPDF_FormField::GetFieldAttrForDict(field.Get(), pdfium::form_fields::kV); + std::optional> selected = + ReadChoiceValues(current_value.Get()); + RetainPtr current_default = + CPDF_FormField::GetFieldAttrForDict(field.Get(), + pdfium::form_fields::kDV); + std::optional> defaults = + ReadChoiceValues(current_default.Get()); + if (!selected.has_value() || !defaults.has_value()) { + return false; + } + std::vector kept = + FilterChoiceValues(selected.value(), new_exports, free_text_combo); + std::vector kept_defaults = + FilterChoiceValues(defaults.value(), new_exports, free_text_combo); + if (kept_defaults.size() > 1 && + ((flags & pdfium::form_flags::kChoiceCombo) || + !(flags & pdfium::form_flags::kChoiceMultiSelect))) { + return false; + } + + // ---- Apply: rewrite /Opt, then re-sync selection + appearances. ---- + RetainPtr mutable_field = + ToDictionary(doc->GetMutableIndirectObject(field_objnum)); + if (!mutable_field) { + return false; + } + if (count == 0) { + // An empty local array also shadows an inherited /Opt, so count=0 has the + // same effective meaning for hierarchical and non-hierarchical fields. + mutable_field->SetNewFor("Opt"); + } else { + auto opt = mutable_field->SetNewFor("Opt"); + for (unsigned long i = 0; i < count; ++i) { + if (new_labels[i] == new_exports[i]) { + opt->AppendNew(new_exports[i].AsStringView()); + } else { + auto pair = opt->AppendNew(); + pair->AppendNew(new_exports[i].AsStringView()); + pair->AppendNew(new_labels[i].AsStringView()); + } + } + } + + if (free_text_combo && !kept.empty() && + !pdfium::Contains(new_exports, kept.front())) { + // Free text survives; only the index hint is stale now. + if (HasInheritedFieldAttribute(doc, field.Get(), "I")) { + mutable_field->SetNewFor("I"); + } else { + mutable_field->RemoveFor("I"); + } + RegenerateFieldAppearances(doc, field_objnum); + } else if (!ApplyChoiceValues(doc, /*reconciled=*/nullptr, field_objnum, kept, + nullptr, 0, nullptr)) { + return false; + } + + if (current_default && !current_default->IsNull()) { + if (kept_defaults.empty()) { + if (!(flags & pdfium::form_flags::kChoiceCombo) && + (flags & pdfium::form_flags::kChoiceMultiSelect)) { + mutable_field->SetNewFor(pdfium::form_fields::kDV); + } else { + mutable_field->SetNewFor(pdfium::form_fields::kDV, + WideStringView()); + } + } else { + std::optional normalized_defaults = + NormalizeChoiceValues(mutable_field.Get(), flags, kept_defaults); + if (!normalized_defaults.has_value()) { + return false; + } + WriteChoiceDefaultValues(mutable_field.Get(), kept_defaults, + normalized_defaults.value()); + } + } + return true; +} diff --git a/fpdfsdk/epdf_form_embeddertest.cpp b/fpdfsdk/epdf_form_embeddertest.cpp new file mode 100644 index 0000000000..e3b6a7fe46 --- /dev/null +++ b/fpdfsdk/epdf_form_embeddertest.cpp @@ -0,0 +1,1851 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_form.h" + +#include +#include + +#include "constants/form_fields.h" +#include "constants/form_flags.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_boolean.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_save.h" +#include "public/fpdfview.h" +#include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/test_loader.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +namespace { + +using WideStringGetter = unsigned long (*)(EPDF_FORM_MODEL, + int, + FPDF_WCHAR*, + unsigned long); + +std::wstring GetWideString(WideStringGetter getter, + EPDF_FORM_MODEL model, + int field_index) { + unsigned long length_bytes = getter(model, field_index, nullptr, 0); + if (length_bytes == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + getter(model, field_index, buffer.data(), length_bytes)); + return GetPlatformWString(buffer.data()); +} + +using FieldValueGetter = + unsigned long (*)(EPDF_FORM_MODEL, int, int, FPDF_WCHAR*, unsigned long); + +std::wstring GetFieldValue(FieldValueGetter getter, + EPDF_FORM_MODEL model, + int field_index, + int value_index = 0) { + unsigned long length_bytes = + getter(model, field_index, value_index, nullptr, 0); + if (length_bytes == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, getter(model, field_index, value_index, buffer.data(), + length_bytes)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetCurrentFieldValue(EPDF_FORM_MODEL model, + int field_index, + int value_index = 0) { + return GetFieldValue(EPDFForm_GetFieldValueAt, model, field_index, + value_index); +} + +std::wstring GetDefaultFieldValue(EPDF_FORM_MODEL model, + int field_index, + int value_index = 0) { + return GetFieldValue(EPDFForm_GetFieldDefaultValueAt, model, field_index, + value_index); +} + +std::wstring GetWidgetExportValue(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + unsigned long length_bytes = EPDFForm_GetFieldWidgetExportValue( + model, field_index, widget_index, nullptr, 0); + if (length_bytes == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + EPDFForm_GetFieldWidgetExportValue(model, field_index, widget_index, + buffer.data(), length_bytes)); + return GetPlatformWString(buffer.data()); +} + +std::string GetWidgetOnState(EPDF_FORM_MODEL model, + int field_index, + int widget_index) { + unsigned long length_bytes = EPDFForm_GetFieldWidgetOnState( + model, field_index, widget_index, nullptr, 0); + if (length_bytes == 0) { + return std::string(); + } + std::vector buffer(length_bytes); + EXPECT_EQ(length_bytes, + EPDFForm_GetFieldWidgetOnState(model, field_index, widget_index, + buffer.data(), length_bytes)); + // |length_bytes| includes the trailing NUL. + return std::string(buffer.data()); +} + +RetainPtr GetEffectiveIndirectDictionary( + FPDF_DOCUMENT document, + uint32_t object_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? ToDictionary(doc->GetOrParseIndirectObject(object_number)) + : nullptr; +} + +RetainPtr GetMutableIndirectDictionary( + FPDF_DOCUMENT document, + uint32_t object_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? ToDictionary(doc->GetMutableIndirectObject(object_number)) + : nullptr; +} + +std::wstring GetEffectiveWidgetAppearance(FPDF_DOCUMENT document, + uint32_t widget_object_number) { + RetainPtr widget = + GetEffectiveIndirectDictionary(document, widget_object_number); + RetainPtr appearance = + widget ? widget->GetDictFor("AP") : nullptr; + RetainPtr normal = + appearance ? appearance->GetStreamFor("N") : nullptr; + if (!normal) { + return std::wstring(); + } + const WideString text = normal->GetUnicodeText(); + return std::wstring(text.c_str(), text.GetLength()); +} + +int FieldIndexByName(EPDF_FORM_MODEL model, const wchar_t* name) { + for (int i = 0; i < EPDFForm_CountFields(model); ++i) { + if (GetWideString(EPDFForm_GetFieldName, model, i) == name) { + return i; + } + } + return -1; +} + +class EPDFFormEmbedderTest : public EmbedderTest { + protected: + // A base document plus a fresh empty layer over it, for delta assertions. + struct LayerDoc { + std::vector bytes; + EPDF_BASE_DOCUMENT base = nullptr; + FPDF_DOCUMENT layer = nullptr; + + ~LayerDoc() { + if (layer) { + FPDF_CloseDocument(layer); + } + if (base) { + EPDF_ReleaseBaseDocument(base); + } + } + }; + + bool OpenLayer(const char* file_name, LayerDoc* out) { + std::string file_path = PathService::GetTestFilePath(file_name); + if (file_path.empty()) { + return false; + } + out->bytes = GetFileContents(file_path.c_str()); + if (out->bytes.empty()) { + return false; + } + out->base = EPDF_LoadMemBaseDocument( + out->bytes.data(), static_cast(out->bytes.size()), nullptr); + if (!out->base) { + return false; + } + EPDFLayerOpenStatus status; + out->layer = EPDFLayer_OpenLayer(out->base, nullptr, nullptr, &status); + return out->layer && status == EPDFLayerOpenStatus_kSuccess; + } +}; + +} // namespace + +TEST_F(EPDFFormEmbedderTest, NoFormYieldsEmptyModel) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_NONE, EPDFForm_GetFormKind(model)); + EXPECT_FALSE(EPDFForm_GetNeedAppearances(model)); + EXPECT_EQ(0, EPDFForm_CountFields(model)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, TextFormModel) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_ACROFORM, EPDFForm_GetFormKind(model)); + ASSERT_EQ(1, EPDFForm_CountFields(model)); + + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_TEXT, EPDFForm_GetFieldFamily(model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, EPDFForm_GetFieldOrigin(model, 0)); + EXPECT_EQ(L"Text Box", GetWideString(EPDFForm_GetFieldName, model, 0)); + EXPECT_EQ(4u, EPDFForm_GetFieldObjNum(model, 0)); + + // Merged field/widget dictionary: one widget sharing the field's object + // number, placed on the page (object 3). + ASSERT_EQ(1, EPDFForm_CountFieldWidgets(model, 0)); + EXPECT_EQ(4u, EPDFForm_GetFieldWidgetObjNum(model, 0, 0)); + EXPECT_EQ(3u, EPDFForm_GetFieldWidgetPageObjNum(model, 0, 0)); + EXPECT_EQ(0, EPDFForm_GetFieldIndexForWidget(model, 4u)); + EXPECT_EQ(0, EPDFForm_GetFieldIndexByObjNum(model, 4u)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, TypedValueSnapshotPreservesPdfShapes) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + + RetainPtr multi = + GetMutableIndirectDictionary(document(), 12u); + ASSERT_TRUE(multi); + RetainPtr defaults = + multi->SetNewFor(pdfium::form_fields::kDV); + defaults->AppendNew(L"Alpha"); + defaults->AppendNew(L"Gamma"); + + RetainPtr empty_array = + GetMutableIndirectDictionary(document(), 9u); + ASSERT_TRUE(empty_array); + empty_array->SetNewFor(pdfium::form_fields::kDV); + + RetainPtr malformed = + GetMutableIndirectDictionary(document(), 10u); + ASSERT_TRUE(malformed); + malformed->SetNewFor(pdfium::form_fields::kV, 7); + malformed->SetNewFor(pdfium::form_fields::kDV); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + + int field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, EPDFForm_GetFieldValueKind(model, field)); + ASSERT_EQ(2, EPDFForm_CountFieldValues(model, field)); + EXPECT_EQ(L"Epsilon", GetCurrentFieldValue(model, field, 0)); + EXPECT_EQ(L"Gamma", GetCurrentFieldValue(model, field, 1)); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, + EPDFForm_GetFieldDefaultValueKind(model, field)); + ASSERT_EQ(2, EPDFForm_CountFieldDefaultValues(model, field)); + EXPECT_EQ(L"Alpha", GetDefaultFieldValue(model, field, 0)); + EXPECT_EQ(L"Gamma", GetDefaultFieldValue(model, field, 1)); + EXPECT_EQ(0u, EPDFForm_GetFieldValueAt(model, field, 2, nullptr, 0)); + + field = EPDFForm_GetFieldIndexByObjNum(model, 9u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, EPDFForm_GetFieldValueKind(model, field)); + EXPECT_EQ(L"Banana", GetCurrentFieldValue(model, field)); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EXPECT_EQ(0, EPDFForm_CountFieldDefaultValues(model, field)); + + field = EPDFForm_GetFieldIndexByObjNum(model, 10u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_UNSUPPORTED, + EPDFForm_GetFieldValueKind(model, field)); + EXPECT_EQ(0, EPDFForm_CountFieldValues(model, field)); + EXPECT_EQ(EPDF_FORM_VALUE_UNSUPPORTED, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EXPECT_EQ(0, EPDFForm_CountFieldDefaultValues(model, field)); + + EXPECT_EQ(EPDF_FORM_VALUE_NONE, EPDFForm_GetFieldValueKind(nullptr, 0)); + EXPECT_EQ(0, EPDFForm_CountFieldValues(nullptr, 0)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ClickFormModel) { + ASSERT_TRUE(OpenDocument("click_form.pdf")); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_ACROFORM, EPDFForm_GetFormKind(model)); + ASSERT_EQ(4, EPDFForm_CountFields(model)); + + // Field 0: merged read-only checkbox, checked via /AS /Yes. + EXPECT_EQ(L"readOnlyCheckbox", + GetWideString(EPDFForm_GetFieldName, model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_CHECKBOX, EPDFForm_GetFieldFamily(model, 0)); + EXPECT_TRUE(EPDFForm_GetFieldFlags(model, 0) & 1); // ReadOnly. + EXPECT_EQ(L"Yes", GetCurrentFieldValue(model, 0)); + ASSERT_EQ(1, EPDFForm_CountFieldWidgets(model, 0)); + EXPECT_EQ("Yes", GetWidgetOnState(model, 0, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, 0, 0)); + + // Field 1: merged checkbox, unchecked. + EXPECT_EQ(L"checkbox", GetWideString(EPDFForm_GetFieldName, model, 1)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_CHECKBOX, EPDFForm_GetFieldFamily(model, 1)); + EXPECT_EQ(L"Off", GetCurrentFieldValue(model, 1)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, 1, 0)); + + // Field 2: read-only radio group with three separate widget kids. + EXPECT_EQ(L"readOnlyRadioButton", + GetWideString(EPDFForm_GetFieldName, model, 2)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_RADIO, EPDFForm_GetFieldFamily(model, 2)); + ASSERT_EQ(3, EPDFForm_CountFieldWidgets(model, 2)); + EXPECT_EQ("value1", GetWidgetOnState(model, 2, 0)); + EXPECT_EQ("value2", GetWidgetOnState(model, 2, 1)); + EXPECT_EQ("value3", GetWidgetOnState(model, 2, 2)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, 2, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, 2, 2)); + EXPECT_EQ(L"value3", GetCurrentFieldValue(model, 2)); + EXPECT_EQ(L"value3", GetWidgetExportValue(model, 2, 2)); + + // Field 3: radio group; widgets 13/14/15 all map back to it. + EXPECT_EQ(L"radioButton", GetWideString(EPDFForm_GetFieldName, model, 3)); + ASSERT_EQ(3, EPDFForm_CountFieldWidgets(model, 3)); + EXPECT_EQ(3, EPDFForm_GetFieldIndexForWidget(model, 13u)); + EXPECT_EQ(3, EPDFForm_GetFieldIndexForWidget(model, 14u)); + EXPECT_EQ(3, EPDFForm_GetFieldIndexForWidget(model, 15u)); + EXPECT_EQ(-1, EPDFForm_GetFieldIndexForWidget(model, 9999u)); + + // Everything in this document is properly linked into /AcroForm /Fields. + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, + EPDFForm_GetFieldOrigin(model, i)); + } + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, OrphanWidgetsRecovered) { + ASSERT_TRUE(OpenDocument("orphan_widgets.pdf")); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_ACROFORM, EPDFForm_GetFormKind(model)); + + // /AcroForm /Fields only lists the text field; the checkbox and the whole + // radio group are reachable through page /Annots alone. Without the sweep + // this model would contain one field instead of three. + ASSERT_EQ(3, EPDFForm_CountFields(model)); + + EXPECT_EQ(L"linked_text", GetWideString(EPDFForm_GetFieldName, model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_TEXT, EPDFForm_GetFieldFamily(model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, EPDFForm_GetFieldOrigin(model, 0)); + EXPECT_EQ(L"hello", GetCurrentFieldValue(model, 0)); + + EXPECT_EQ(L"orphan_check", GetWideString(EPDFForm_GetFieldName, model, 1)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_CHECKBOX, EPDFForm_GetFieldFamily(model, 1)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_RECOVERED, EPDFForm_GetFieldOrigin(model, 1)); + EXPECT_EQ(5u, EPDFForm_GetFieldObjNum(model, 1)); + ASSERT_EQ(1, EPDFForm_CountFieldWidgets(model, 1)); + EXPECT_EQ("Yes", GetWidgetOnState(model, 1, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, 1, 0)); + EXPECT_EQ(L"Yes", GetCurrentFieldValue(model, 1)); + + // The radio group's parent field dictionary is not referenced anywhere in + // /AcroForm /Fields. The sweep climbs /Parent from the first widget it + // sees, so BOTH widgets must land on ONE logical field. + EXPECT_EQ(L"orphan_radio", GetWideString(EPDFForm_GetFieldName, model, 2)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_RADIO, EPDFForm_GetFieldFamily(model, 2)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_RECOVERED, EPDFForm_GetFieldOrigin(model, 2)); + EXPECT_EQ(6u, EPDFForm_GetFieldObjNum(model, 2)); + EXPECT_TRUE(EPDFForm_GetFieldFlags(model, 2) & 0x8000); // Radio. + ASSERT_EQ(2, EPDFForm_CountFieldWidgets(model, 2)); + EXPECT_EQ(8u, EPDFForm_GetFieldWidgetObjNum(model, 2, 0)); + EXPECT_EQ(9u, EPDFForm_GetFieldWidgetObjNum(model, 2, 1)); + EXPECT_EQ(3u, EPDFForm_GetFieldWidgetPageObjNum(model, 2, 0)); + EXPECT_EQ("a", GetWidgetOnState(model, 2, 0)); + EXPECT_EQ("b", GetWidgetOnState(model, 2, 1)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, 2, 0)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, 2, 1)); + EXPECT_EQ(L"a", GetCurrentFieldValue(model, 2)); + + EXPECT_EQ(2, EPDFForm_GetFieldIndexForWidget(model, 8u)); + EXPECT_EQ(2, EPDFForm_GetFieldIndexForWidget(model, 9u)); + EXPECT_EQ(2, EPDFForm_GetFieldIndexByObjNum(model, 6u)); + EPDFForm_CloseModel(model); +} + +// A "two-plane" document (the IRS f1040 class): every field exists TWICE +// under one fully qualified name — an orphaned twin inside /AcroForm +// /Fields that no page references, and a standalone merged twin in page +// /Annots that /AcroForm cannot reach. Reads reconcile the planes into ONE +// field, so writes must cover BOTH twins; a write planned from the raw +// field dictionary alone would edit the invisible orphan while the +// on-screen widget never changes. +TEST_F(EPDFFormEmbedderTest, TwoPlaneTwinWidgetsFillTogether) { + ASSERT_TRUE(OpenDocument("two_plane_form.pdf")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + // Two logical fields, not four: the same-FQN twins merge, and each field + // carries both twin widgets (the orphan first — /Fields loads before the + // page sweep — then the page twin). + ASSERT_EQ(2, EPDFForm_CountFields(model)); + const int checkbox = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(checkbox, 0); + ASSERT_EQ(2, EPDFForm_CountFieldWidgets(model, checkbox)); + EXPECT_EQ(4u, EPDFForm_GetFieldWidgetObjNum(model, checkbox, 0)); + EXPECT_EQ(9u, EPDFForm_GetFieldWidgetObjNum(model, checkbox, 1)); + const int text = EPDFForm_GetFieldIndexByObjNum(model, 5u); + ASSERT_GE(text, 0); + ASSERT_EQ(2, EPDFForm_CountFieldWidgets(model, text)); + EXPECT_EQ(10u, EPDFForm_GetFieldWidgetObjNum(model, text, 1)); + EPDFForm_CloseModel(model); + + // Toggling flips /AS on BOTH twins — above all the page twin (obj 11), + // the only one the user can see. + uint32_t changed[4] = {}; + unsigned long changed_count = 0; + ASSERT_TRUE( + EPDFForm_SetToggle(document(), 4u, "1", changed, 4, &changed_count)); + ASSERT_EQ(2ul, changed_count); + EXPECT_EQ(4u, changed[0]); + EXPECT_EQ(9u, changed[1]); + for (const uint32_t objnum : {4u, 9u}) { + RetainPtr widget = + GetEffectiveIndirectDictionary(document(), objnum); + ASSERT_TRUE(widget); + EXPECT_EQ("1", widget->GetNameFor("AS")) << "widget " << objnum; + } + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int checked = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(checked, 0); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, checked, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, checked, 1)); + EPDFForm_CloseModel(model); + + // A text commit regenerates the page twin's appearance with the value — + // even though this document has no /AcroForm /DR: generation must seed a + // fallback font instead of vetoing the appearance. + ScopedFPDFWideString value = GetFPDFWideString(L"TWIN"); + changed_count = 0; + ASSERT_TRUE(EPDFForm_SetTextValue(document(), 5u, value.get(), changed, 4, + &changed_count)); + ASSERT_EQ(2ul, changed_count); + EXPECT_EQ(5u, changed[0]); + EXPECT_EQ(10u, changed[1]); + const std::wstring appearance = + GetEffectiveWidgetAppearance(document(), 10u); + EXPECT_NE(std::wstring::npos, appearance.find(L"TWIN")) << appearance; + + // The write seeded /DR/Font with the /DA-named font. + RetainPtr acroform = + GetEffectiveIndirectDictionary(document(), 2u); + ASSERT_TRUE(acroform); + RetainPtr dr_dict = acroform->GetDictFor("DR"); + ASSERT_TRUE(dr_dict); + RetainPtr dr_font_dict = dr_dict->GetDictFor("Font"); + ASSERT_TRUE(dr_font_dict); + EXPECT_TRUE(dr_font_dict->KeyExist("Helv")); +} + +// Building a form model must be a pure read: over a layer document it must +// not promote a single object into the layer, even while it reconciles +// orphan widgets in memory. +TEST_F(EPDFFormEmbedderTest, LayerModelLoadIsPure) { + std::string file_path = PathService::GetTestFilePath("orphan_widgets.pdf"); + ASSERT_FALSE(file_path.empty()); + std::vector contents = GetFileContents(file_path.c_str()); + ASSERT_FALSE(contents.empty()); + + EPDF_BASE_DOCUMENT base = EPDF_LoadMemBaseDocument( + contents.data(), static_cast(contents.size()), nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status; + FPDF_DOCUMENT layer = EPDFLayer_OpenLayer(base, nullptr, nullptr, &status); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(layer); + ASSERT_TRUE(model); + EXPECT_EQ(3, EPDFForm_CountFields(model)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_RECOVERED, EPDFForm_GetFieldOrigin(model, 2)); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(layer)); + EPDFForm_CloseModel(model); + + FPDF_CloseDocument(layer); + EPDF_ReleaseBaseDocument(base); +} + +// The radio walkthrough: flipping the group promotes exactly the field plus +// the two widgets whose /AS changed - the minimal FDF-shaped delta. +TEST_F(EPDFFormEmbedderTest, SetToggleRadioOnLayer) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("orphan_widgets.pdf", &doc)); + + uint32_t changed[4] = {}; + unsigned long changed_count = 0; + ASSERT_TRUE( + EPDFForm_SetToggle(doc.layer, 6u, "b", changed, 4, &changed_count)); + EXPECT_EQ(2ul, changed_count); + EXPECT_EQ(8u, changed[0]); // /AS a -> Off + EXPECT_EQ(9u, changed[1]); // /AS Off -> b + EXPECT_EQ(3ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 6u)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 8u)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 9u)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(doc.layer); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 6u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"b", GetCurrentFieldValue(model, field)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 1)); + EPDFForm_CloseModel(model); + + // Idempotence: re-setting the same state changes nothing and promotes + // nothing further. + ASSERT_TRUE( + EPDFForm_SetToggle(doc.layer, 6u, "b", nullptr, 0, &changed_count)); + EXPECT_EQ(0ul, changed_count); + EXPECT_EQ(3ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); +} + +// A failed transaction must be side-effect free: zero objects promoted. +TEST_F(EPDFFormEmbedderTest, SetToggleFailuresAreSideEffectFree) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("orphan_widgets.pdf", &doc)); + + // Unknown on-state. + EXPECT_FALSE(EPDFForm_SetToggle(doc.layer, 6u, "zz", nullptr, 0, nullptr)); + // Not a toggle field (the text field). + EXPECT_FALSE(EPDFForm_SetToggle(doc.layer, 4u, "Yes", nullptr, 0, nullptr)); + // Unknown field object number. + EXPECT_FALSE(EPDFForm_SetToggle(doc.layer, 9999u, "a", nullptr, 0, nullptr)); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); +} + +TEST_F(EPDFFormEmbedderTest, SetToggleClearRadioGroup) { + ASSERT_TRUE(OpenDocument("orphan_widgets.pdf")); + // orphan_radio has no NoToggleToOff flag, so clearing is legal. + unsigned long changed_count = 0; + ASSERT_TRUE( + EPDFForm_SetToggle(document(), 6u, nullptr, nullptr, 0, &changed_count)); + EXPECT_EQ(1ul, changed_count); // Only widget 8 was checked. + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 6u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"Off", GetCurrentFieldValue(model, field)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 1)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ToggleSemantics) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + + // NoToggleToOff: clearing the group is rejected; switching is fine. + EXPECT_FALSE( + EPDFForm_SetToggle(document(), 5u, nullptr, nullptr, 0, nullptr)); + ASSERT_TRUE(EPDFForm_SetToggle(document(), 5u, "y", nullptr, 0, nullptr)); + + // Radios in unison: both /u1 widgets check together. + unsigned long changed_count = 0; + ASSERT_TRUE( + EPDFForm_SetToggle(document(), 8u, "u1", nullptr, 0, &changed_count)); + EXPECT_EQ(2ul, changed_count); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 5u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"y", GetCurrentFieldValue(model, field)); + + field = EPDFForm_GetFieldIndexByObjNum(model, 8u); + ASSERT_GE(field, 0); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 1)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 2)); + EXPECT_EQ(L"u1", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + // Switching to /u2 unchecks both unison widgets: three /AS flips. + ASSERT_TRUE( + EPDFForm_SetToggle(document(), 8u, "u2", nullptr, 0, &changed_count)); + EXPECT_EQ(3ul, changed_count); + + // Checkbox with /Opt: raw /V is the control index name. The semantic + // export value remains available on the widget. + ASSERT_TRUE(EPDFForm_SetToggle(document(), 12u, "On", nullptr, 0, nullptr)); + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EXPECT_EQ(L"0", GetCurrentFieldValue(model, field)); + EXPECT_EQ(L"Alpha", GetWidgetExportValue(model, field, 0)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, SetTextValue) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + + ScopedFPDFWideString text = GetFPDFWideString(L"Hello EmbedPDF"); + uint32_t changed[2] = {}; + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_SetTextValue(document(), 4u, text.get(), changed, 2, + &changed_count)); + EXPECT_EQ(1ul, changed_count); + EXPECT_EQ(4u, changed[0]); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(L"Hello EmbedPDF", GetCurrentFieldValue(model, 0)); + EPDFForm_CloseModel(model); + + // The widget's normal appearance stream was regenerated. + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 0)); + ASSERT_TRUE(annot); + EXPECT_GT(FPDFAnnot_GetAP(annot.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL, + nullptr, 0), + 2u); + } + UnloadPage(page); + + // Idempotence: same value again reports zero changes. + ASSERT_TRUE(EPDFForm_SetTextValue(document(), 4u, text.get(), nullptr, 0, + &changed_count)); + EXPECT_EQ(0ul, changed_count); +} + +TEST_F(EPDFFormEmbedderTest, SetTextValueMaxLenAndLayerDelta) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("toggle_fields.pdf", &doc)); + + // Six characters against /MaxLen 5: Acrobat-compatible writes truncate. + ScopedFPDFWideString too_long = GetFPDFWideString(L"abcdef"); + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_SetTextValue(doc.layer, 4u, too_long.get(), nullptr, 0, + &changed_count)); + EXPECT_EQ(1ul, changed_count); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 4u)); + // Merged field/widget plus the regenerated appearance machinery; the + // delta must stay small. + EXPECT_LE(EPDFLayer_GetPromotedObjectCount(doc.layer), 4ul); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(doc.layer); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"abcde", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + // Reassigning a value that normalizes to the stored value is a no-op. + ScopedFPDFWideString fits = GetFPDFWideString(L"abcde"); + ASSERT_TRUE(EPDFForm_SetTextValue(doc.layer, 4u, fits.get(), nullptr, 0, + &changed_count)); + EXPECT_EQ(0ul, changed_count); +} + +TEST_F(EPDFFormEmbedderTest, SetFieldDisplayOnLayerIsDurable) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("text_form.pdf", &doc)); + + EXPECT_FALSE( + EPDFForm_SetFieldDisplay(doc.layer, 4u, 99, nullptr, 0, nullptr)); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + + uint32_t changed[1] = {}; + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_SetFieldDisplay(doc.layer, 4u, EPDF_FORM_DISPLAY_HIDDEN, + changed, 1, &changed_count)); + ASSERT_EQ(1ul, changed_count); + EXPECT_EQ(4u, changed[0]); + EXPECT_EQ(1ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 4u)); + + RetainPtr widget = + GetEffectiveIndirectDictionary(doc.layer, 4u); + ASSERT_TRUE(widget); + int flags = widget->GetIntegerFor("F"); + EXPECT_TRUE(flags & FPDF_ANNOT_FLAG_HIDDEN); + EXPECT_TRUE(flags & FPDF_ANNOT_FLAG_PRINT); + EXPECT_FALSE(flags & FPDF_ANNOT_FLAG_NOVIEW); + + ClearString(); + EPDFLayerSaveStatus save_status; + ASSERT_TRUE(EPDFLayer_SaveDelta(doc.layer, this, &save_status)); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS file_access = {}; + file_access.m_FileLen = static_cast(delta.size()); + file_access.m_GetBlock = TestLoader::GetBlock; + file_access.m_Param = &loader; + EPDFLayerOpenStatus status; + FPDF_DOCUMENT second = + EPDFLayer_OpenLayer(doc.base, &file_access, nullptr, &status); + ASSERT_TRUE(second); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + widget = GetEffectiveIndirectDictionary(second, 4u); + ASSERT_TRUE(widget); + flags = widget->GetIntegerFor("F"); + EXPECT_TRUE(flags & FPDF_ANNOT_FLAG_HIDDEN); + EXPECT_TRUE(flags & FPDF_ANNOT_FLAG_PRINT); + FPDF_CloseDocument(second); +} + +TEST_F(EPDFFormEmbedderTest, SetFieldAppearanceTextOnLayerIsDurable) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("text_form.pdf", &doc)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(doc.layer); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + ASSERT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + ScopedFPDFWideString formatted = GetFPDFWideString(L"FormattedValue"); + uint32_t changed[1] = {}; + unsigned long changed_count = 0; + ASSERT_TRUE(EPDFForm_SetFieldAppearanceText(doc.layer, 4u, formatted.get(), + changed, 1, &changed_count)); + ASSERT_EQ(1ul, changed_count); + EXPECT_EQ(4u, changed[0]); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 4u)); + + model = EPDFForm_LoadModel(doc.layer); + ASSERT_TRUE(model); + field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + const std::wstring first_appearance = + GetEffectiveWidgetAppearance(doc.layer, 4u); + EXPECT_NE(std::wstring::npos, first_appearance.find(L"FormattedValue")) + << first_appearance; + + ClearString(); + EPDFLayerSaveStatus save_status; + ASSERT_TRUE(EPDFLayer_SaveDelta(doc.layer, this, &save_status)); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS file_access = {}; + file_access.m_FileLen = static_cast(delta.size()); + file_access.m_GetBlock = TestLoader::GetBlock; + file_access.m_Param = &loader; + EPDFLayerOpenStatus status; + FPDF_DOCUMENT second = + EPDFLayer_OpenLayer(doc.base, &file_access, nullptr, &status); + ASSERT_TRUE(second); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + model = EPDFForm_LoadModel(second); + ASSERT_TRUE(model); + field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + const std::wstring second_appearance = + GetEffectiveWidgetAppearance(second, 4u); + EXPECT_NE(std::wstring::npos, second_appearance.find(L"FormattedValue")) + << second_appearance; + FPDF_CloseDocument(second); +} + +TEST_F(EPDFFormEmbedderTest, SetChoiceValuesCombo) { + ASSERT_TRUE(OpenDocument("combobox_form.pdf")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int combo1 = FieldIndexByName(model, L"Combo1"); + const int editable = FieldIndexByName(model, L"Combo_Editable"); + ASSERT_GE(combo1, 0); + ASSERT_GE(editable, 0); + const uint32_t combo1_objnum = EPDFForm_GetFieldObjNum(model, combo1); + const uint32_t editable_objnum = EPDFForm_GetFieldObjNum(model, editable); + EPDFForm_CloseModel(model); + + // Non-edit combo: option values only. + ScopedFPDFWideString cherry = GetFPDFWideString(L"Cherry"); + FPDF_WIDESTRING one_value[] = {cherry.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), combo1_objnum, one_value, 1, + nullptr, 0, nullptr)); + ScopedFPDFWideString bogus = GetFPDFWideString(L"NotAnOption"); + FPDF_WIDESTRING bogus_value[] = {bogus.get()}; + EXPECT_FALSE(EPDFForm_SetChoiceValues(document(), combo1_objnum, bogus_value, + 1, nullptr, 0, nullptr)); + + // Edit combo: free text is accepted and clears /I; an option export value + // selects that option. + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), editable_objnum, bogus_value, + 1, nullptr, 0, nullptr)); + ScopedFPDFWideString bar = GetFPDFWideString(L"bar"); + FPDF_WIDESTRING bar_value[] = {bar.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), editable_objnum, bar_value, + 1, nullptr, 0, nullptr)); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(L"Cherry", GetCurrentFieldValue(model, combo1)); + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, combo1, 2)); + EXPECT_EQ(L"bar", GetCurrentFieldValue(model, editable)); + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, editable, 1)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, SetChoiceValuesListbox) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int multi = FieldIndexByName(model, L"Listbox_MultiSelect"); + const int single = FieldIndexByName(model, L"Listbox_SingleSelect"); + ASSERT_GE(multi, 0); + ASSERT_GE(single, 0); + const uint32_t multi_objnum = EPDFForm_GetFieldObjNum(model, multi); + const uint32_t single_objnum = EPDFForm_GetFieldObjNum(model, single); + EPDFForm_CloseModel(model); + + // Multi-select accepts several values regardless of input order. + ScopedFPDFWideString cherry = GetFPDFWideString(L"Cherry"); + ScopedFPDFWideString apple = GetFPDFWideString(L"Apple"); + FPDF_WIDESTRING two_values[] = {cherry.get(), apple.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), multi_objnum, two_values, 2, + nullptr, 0, nullptr)); + // Single-select rejects multiple values. + EXPECT_FALSE(EPDFForm_SetChoiceValues(document(), single_objnum, two_values, + 2, nullptr, 0, nullptr)); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, multi, 0)); // Apple + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, multi, 1)); // Banana + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, multi, 2)); // Cherry + EPDFForm_CloseModel(model); + + // Clearing the selection. + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), multi_objnum, nullptr, 0, + nullptr, 0, nullptr)); + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, multi, 0)); + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, multi, 2)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ResetField) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + + // Toggle reset restores the /DV state. + ASSERT_TRUE(EPDFForm_SetToggle(document(), 5u, "y", nullptr, 0, nullptr)); + ASSERT_TRUE(EPDFForm_ResetField(document(), 5u, nullptr, 0, nullptr)); + + // Text reset with no /DV removes the value. + ScopedFPDFWideString text = GetFPDFWideString(L"xyz"); + ASSERT_TRUE( + EPDFForm_SetTextValue(document(), 4u, text.get(), nullptr, 0, nullptr)); + ASSERT_TRUE(EPDFForm_ResetField(document(), 4u, nullptr, 0, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 5u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"x", GetCurrentFieldValue(model, field)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 1)); + + field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, MultiSelectDefaultsResetValueAndIndices) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + + ScopedFPDFWideString epsilon = GetFPDFWideString(L"Epsilon"); + ScopedFPDFWideString gamma = GetFPDFWideString(L"Gamma"); + FPDF_WIDESTRING defaults[] = {epsilon.get(), gamma.get()}; + ASSERT_TRUE(EPDFForm_SetFieldDefaultValues(document(), 12u, defaults, 2)); + + // Defaults are stored in option order, matching current-value writes. + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, + EPDFForm_GetFieldDefaultValueKind(model, field)); + ASSERT_EQ(2, EPDFForm_CountFieldDefaultValues(model, field)); + EXPECT_EQ(L"Gamma", GetDefaultFieldValue(model, field, 0)); + EXPECT_EQ(L"Epsilon", GetDefaultFieldValue(model, field, 1)); + EPDFForm_CloseModel(model); + + ScopedFPDFWideString alpha = GetFPDFWideString(L"Alpha"); + FPDF_WIDESTRING current[] = {alpha.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), 12u, current, 1, nullptr, 0, + nullptr)); + ASSERT_TRUE(EPDFForm_ResetField(document(), 12u, nullptr, 0, nullptr)); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, EPDFForm_GetFieldValueKind(model, field)); + ASSERT_EQ(2, EPDFForm_CountFieldValues(model, field)); + EXPECT_EQ(L"Gamma", GetCurrentFieldValue(model, field, 0)); + EXPECT_EQ(L"Epsilon", GetCurrentFieldValue(model, field, 1)); + EPDFForm_CloseModel(model); + + RetainPtr dictionary = + GetEffectiveIndirectDictionary(document(), 12u); + ASSERT_TRUE(dictionary); + RetainPtr indices = dictionary->GetArrayFor("I"); + ASSERT_TRUE(indices); + ASSERT_EQ(2u, indices->size()); + EXPECT_EQ(2, indices->GetIntegerAt(0)); // Gamma. + EXPECT_EQ(4, indices->GetIntegerAt(1)); // Epsilon. +} + +TEST_F(EPDFFormEmbedderTest, MultiSelectDefaultsAreLayerDurable) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("listbox_form.pdf", &doc)); + + ScopedFPDFWideString gamma = GetFPDFWideString(L"Gamma"); + ScopedFPDFWideString epsilon = GetFPDFWideString(L"Epsilon"); + FPDF_WIDESTRING defaults[] = {gamma.get(), epsilon.get()}; + ASSERT_TRUE(EPDFForm_SetFieldDefaultValues(doc.layer, 12u, defaults, 2)); + EXPECT_EQ(1ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + ASSERT_TRUE(EPDFForm_ResetField(doc.layer, 12u, nullptr, 0, nullptr)); + // Reset also regenerates the appearance and therefore promotes its shared + // resource object in addition to the field/widget dictionary. + EXPECT_EQ(2ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + + ClearString(); + EPDFLayerSaveStatus save_status; + ASSERT_TRUE(EPDFLayer_SaveDelta(doc.layer, this, &save_status)); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS file_access = {}; + file_access.m_FileLen = static_cast(delta.size()); + file_access.m_GetBlock = TestLoader::GetBlock; + file_access.m_Param = &loader; + EPDFLayerOpenStatus status; + FPDF_DOCUMENT reopened = + EPDFLayer_OpenLayer(doc.base, &file_access, nullptr, &status); + ASSERT_TRUE(reopened); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(reopened); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, EPDFForm_GetFieldValueKind(model, field)); + ASSERT_EQ(2, EPDFForm_CountFieldDefaultValues(model, field)); + EXPECT_EQ(L"Gamma", GetDefaultFieldValue(model, field, 0)); + EXPECT_EQ(L"Epsilon", GetDefaultFieldValue(model, field, 1)); + EPDFForm_CloseModel(model); + FPDF_CloseDocument(reopened); +} + +TEST_F(EPDFFormEmbedderTest, EmptyTextDefaultIsScalarAndCanBeRemoved) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + + ScopedFPDFWideString empty = GetFPDFWideString(L""); + FPDF_WIDESTRING defaults[] = {empty.get()}; + ASSERT_TRUE(EPDFForm_SetFieldDefaultValues(document(), 4u, defaults, 1)); + EXPECT_FALSE(EPDFForm_SetFieldDefaultValues(document(), 4u, nullptr, 0)); + FPDF_WIDESTRING too_many[] = {empty.get(), empty.get()}; + EXPECT_FALSE(EPDFForm_SetFieldDefaultValues(document(), 4u, too_many, 2)); + + ScopedFPDFWideString current = GetFPDFWideString(L"not empty"); + ASSERT_TRUE(EPDFForm_SetTextValue(document(), 4u, current.get(), nullptr, 0, + nullptr)); + ASSERT_TRUE(EPDFForm_ResetField(document(), 4u, nullptr, 0, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EXPECT_EQ(1, EPDFForm_CountFieldDefaultValues(model, field)); + EXPECT_EQ(L"", GetDefaultFieldValue(model, field)); + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, EPDFForm_GetFieldValueKind(model, field)); + EXPECT_EQ(1, EPDFForm_CountFieldValues(model, field)); + EXPECT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + ASSERT_TRUE(EPDFForm_RemoveFieldDefaultValue(document(), 4u)); + model = EPDFForm_LoadModel(document()); + field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_EQ(EPDF_FORM_VALUE_NONE, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ToggleDefaultWithOptUsesControlIndex) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + + ASSERT_TRUE(EPDFForm_SetFieldDefaultToggle(document(), 12u, "On")); + EXPECT_FALSE(EPDFForm_SetFieldDefaultToggle(document(), 12u, "Missing")); + EXPECT_FALSE(EPDFForm_SetFieldDefaultToggle(document(), 12u, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, + EPDFForm_GetFieldDefaultValueKind(model, field)); + EXPECT_EQ(L"0", GetDefaultFieldValue(model, field)); + EPDFForm_CloseModel(model); + + ASSERT_TRUE(EPDFForm_SetToggle(document(), 12u, "On", nullptr, 0, nullptr)); + ASSERT_TRUE( + EPDFForm_SetToggle(document(), 12u, nullptr, nullptr, 0, nullptr)); + ASSERT_TRUE(EPDFForm_ResetField(document(), 12u, nullptr, 0, nullptr)); + + model = EPDFForm_LoadModel(document()); + field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + EXPECT_EQ(L"0", GetCurrentFieldValue(model, field)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EPDFForm_CloseModel(model); + + // /Off is distinct from a missing default and resets the widget off. + ASSERT_TRUE(EPDFForm_SetFieldDefaultToggle(document(), 12u, "Off")); + ASSERT_TRUE(EPDFForm_ResetField(document(), 12u, nullptr, 0, nullptr)); + model = EPDFForm_LoadModel(document()); + field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + EXPECT_EQ(L"Off", GetDefaultFieldValue(model, field)); + EXPECT_FALSE(EPDFForm_IsFieldWidgetChecked(model, field, 0)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ResetRejectsMalformedDefaultWithoutMutation) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + RetainPtr field = + GetMutableIndirectDictionary(document(), 4u); + ASSERT_TRUE(field); + field->SetNewFor(pdfium::form_fields::kDV, 42); + const WideString original = field->GetUnicodeTextFor(pdfium::form_fields::kV); + + EXPECT_FALSE(EPDFForm_ResetField(document(), 4u, nullptr, 0, nullptr)); + EXPECT_EQ(original, field->GetUnicodeTextFor(pdfium::form_fields::kV)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int index = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_EQ(EPDF_FORM_VALUE_UNSUPPORTED, + EPDFForm_GetFieldDefaultValueKind(model, index)); + EPDFForm_CloseModel(model); +} + +namespace { + +std::string ExportFdf(FPDF_DOCUMENT doc, uint32_t flags = 0) { + unsigned long length = EPDFForm_ExportFDF(doc, nullptr, flags, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, + EPDFForm_ExportFDF(doc, nullptr, flags, buffer.data(), length)); + return std::string(buffer.data(), length); +} + +std::string ExportXfdf(FPDF_DOCUMENT doc, uint32_t flags = 0) { + unsigned long length = EPDFForm_ExportXFDF(doc, nullptr, flags, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, + EPDFForm_ExportXFDF(doc, nullptr, flags, buffer.data(), length)); + return std::string(buffer.data(), length); +} + +} // namespace + +TEST_F(EPDFFormEmbedderTest, ExportFDF) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + const std::string fdf = ExportFdf(document()); + ASSERT_FALSE(fdf.empty()); + EXPECT_NE(std::string::npos, fdf.find("%FDF-1.2")); + EXPECT_NE(std::string::npos, fdf.find("(maxlen_text)")); + EXPECT_NE(std::string::npos, fdf.find("(abc)")); + EXPECT_NE(std::string::npos, fdf.find("(ntto_radio)")); + // Hierarchical fields export with their fully qualified name. + EXPECT_NE(std::string::npos, fdf.find("(billing.name)")); +} + +TEST_F(EPDFFormEmbedderTest, ExportFDFIncludesRecoveredFields) { + ASSERT_TRUE(OpenDocument("orphan_widgets.pdf")); + const std::string fdf = ExportFdf(document()); + ASSERT_FALSE(fdf.empty()); + // Only linked_text is reachable through /AcroForm /Fields; the exporter + // must see the reconciled view. + EXPECT_NE(std::string::npos, fdf.find("(linked_text)")); + EXPECT_NE(std::string::npos, fdf.find("(orphan_check)")); + EXPECT_NE(std::string::npos, fdf.find("(orphan_radio)")); +} + +TEST_F(EPDFFormEmbedderTest, RequiredMultiSelectArrayIsNotSkippedOnExport) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + ASSERT_TRUE(EPDFForm_SetFieldFlags(document(), 12u, + pdfium::form_flags::kRequired, 0)); + + const std::string fdf = + ExportFdf(document(), EPDF_FORM_EXPORT_SKIP_EMPTY_REQUIRED); + ASSERT_FALSE(fdf.empty()); + EXPECT_NE(std::string::npos, fdf.find("(Listbox_MultiSelectMultipleValues)")); + EXPECT_NE(std::string::npos, fdf.find("(Epsilon)")); + EXPECT_NE(std::string::npos, fdf.find("(Gamma)")); + + const std::string xfdf = + ExportXfdf(document(), EPDF_FORM_EXPORT_SKIP_EMPTY_REQUIRED); + ASSERT_FALSE(xfdf.empty()); + EXPECT_NE(std::string::npos, + xfdf.find("")); + EXPECT_NE(std::string::npos, xfdf.find("Epsilon")); + EXPECT_NE(std::string::npos, xfdf.find("Gamma")); +} + +TEST_F(EPDFFormEmbedderTest, ImportFDF) { + ASSERT_TRUE(OpenDocument("orphan_widgets.pdf")); + static const char kFdf[] = + "%FDF-1.2\r\n" + "1 0 obj\r\n" + "<< /FDF << /Fields [\r\n" + "<< /T (linked_text) /V (imported) >>\r\n" + "<< /T (orphan_radio) /V (b) >>\r\n" + "<< /T (no_such_field) /V (x) >>\r\n" + "] >> >>\r\n" + "endobj\r\n" + "trailer\r\n" + "<< /Root 1 0 R >>\r\n" + "%%EOF\r\n"; + + EPDF_FORM_IMPORT_RESULT result; + ASSERT_TRUE(EPDFForm_ImportFDF(document(), kFdf, sizeof(kFdf) - 1, &result)); + EXPECT_EQ(3u, result.fields_total); + EXPECT_EQ(2u, result.fields_applied); + EXPECT_EQ(1u, result.fields_skipped); + EXPECT_EQ(3u, result.widgets_changed); // text widget + both radio kids + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_EQ(L"imported", GetCurrentFieldValue(model, field)); + field = EPDFForm_GetFieldIndexByObjNum(model, 6u); + EXPECT_EQ(L"b", GetCurrentFieldValue(model, field)); + EXPECT_TRUE(EPDFForm_IsFieldWidgetChecked(model, field, 1)); + EPDFForm_CloseModel(model); + + // Garbage payloads are rejected. + EXPECT_FALSE(EPDFForm_ImportFDF(document(), "not fdf", 7, &result)); +} + +// Fill a layer, export its FDF, and replay it onto a second fresh layer of +// the same base: values must survive and only touched objects promote. +TEST_F(EPDFFormEmbedderTest, FdfRoundTripAcrossLayers) { + LayerDoc first; + ASSERT_TRUE(OpenLayer("orphan_widgets.pdf", &first)); + ScopedFPDFWideString bob = GetFPDFWideString(L"Bob"); + ASSERT_TRUE( + EPDFForm_SetTextValue(first.layer, 4u, bob.get(), nullptr, 0, nullptr)); + ASSERT_TRUE(EPDFForm_SetToggle(first.layer, 6u, "b", nullptr, 0, nullptr)); + + unsigned long length = + EPDFForm_ExportFDF(first.layer, nullptr, 0, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector fdf(length); + ASSERT_EQ(length, + EPDFForm_ExportFDF(first.layer, nullptr, 0, fdf.data(), length)); + + LayerDoc second; + ASSERT_TRUE(OpenLayer("orphan_widgets.pdf", &second)); + EPDF_FORM_IMPORT_RESULT result; + ASSERT_TRUE(EPDFForm_ImportFDF(second.layer, fdf.data(), length, &result)); + EXPECT_EQ(3u, + result.fields_total); // linked_text, orphan_check, orphan_radio + EXPECT_EQ(3u, result.fields_applied); + EXPECT_EQ(0u, result.fields_skipped); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(second.layer); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_EQ(L"Bob", GetCurrentFieldValue(model, field)); + field = EPDFForm_GetFieldIndexByObjNum(model, 6u); + EXPECT_EQ(L"b", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ExportXFDF) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + const std::string xfdf = ExportXfdf(document()); + ASSERT_FALSE(xfdf.empty()); + EXPECT_NE(std::string::npos, xfdf.find(". + EXPECT_NE(std::string::npos, xfdf.find("abc")); + EXPECT_NE(std::string::npos, xfdf.find("x")); + // Hierarchical names nest per component. + EXPECT_NE(std::string::npos, xfdf.find("")); + EXPECT_NE(std::string::npos, xfdf.find("\n" + "" + "" + "" + "Bob & Co" + "y" + ""; + + EPDF_FORM_IMPORT_RESULT result; + ASSERT_TRUE( + EPDFForm_ImportXFDF(document(), kXfdf, sizeof(kXfdf) - 1, &result)); + EXPECT_EQ(2u, result.fields_total); + EXPECT_EQ(2u, result.fields_applied); + EXPECT_EQ(0u, result.fields_skipped); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int billing_name = FieldIndexByName(model, L"billing.name"); + ASSERT_GE(billing_name, 0); + // Entity decoding round-trips. + EXPECT_EQ(L"Bob & Co", GetCurrentFieldValue(model, billing_name)); + const int radio = EPDFForm_GetFieldIndexByObjNum(model, 5u); + EXPECT_EQ(L"y", GetCurrentFieldValue(model, radio)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, ImportXFDFMultiSelect) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + static const char kXfdf[] = + "" + "" + "" + "CherryApple" + "" + ""; + + EPDF_FORM_IMPORT_RESULT result; + ASSERT_TRUE( + EPDFForm_ImportXFDF(document(), kXfdf, sizeof(kXfdf) - 1, &result)); + EXPECT_EQ(1u, result.fields_total); + EXPECT_EQ(1u, result.fields_applied); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int multi = FieldIndexByName(model, L"Listbox_MultiSelect"); + ASSERT_GE(multi, 0); + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, multi, 0)); // Apple + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, multi, 1)); // Banana + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, multi, 2)); // Cherry + EPDFForm_CloseModel(model); +} + +// The full circle: export XFDF from a filled document and re-import it into +// a pristine copy via a fresh layer - values must match exactly. +TEST_F(EPDFFormEmbedderTest, XfdfRoundTripPreservesValues) { + LayerDoc first; + ASSERT_TRUE(OpenLayer("toggle_fields.pdf", &first)); + ScopedFPDFWideString tricky = GetFPDFWideString(L"a&\"c\" 'd'"); + ASSERT_TRUE(EPDFForm_SetTextValue(first.layer, 17u, tricky.get(), nullptr, 0, + nullptr)); + + unsigned long length = + EPDFForm_ExportXFDF(first.layer, nullptr, 0, nullptr, 0); + ASSERT_GT(length, 0u); + std::vector xfdf(length); + ASSERT_EQ(length, + EPDFForm_ExportXFDF(first.layer, nullptr, 0, xfdf.data(), length)); + + LayerDoc second; + ASSERT_TRUE(OpenLayer("toggle_fields.pdf", &second)); + EPDF_FORM_IMPORT_RESULT result; + ASSERT_TRUE(EPDFForm_ImportXFDF(second.layer, xfdf.data(), length, &result)); + EXPECT_EQ(0u, result.fields_skipped); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(second.layer); + ASSERT_TRUE(model); + const int billing_name = FieldIndexByName(model, L"billing.name"); + ASSERT_GE(billing_name, 0); + EXPECT_EQ(L"a&\"c\" 'd'", GetCurrentFieldValue(model, billing_name)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, RepairLinksRecoveredFields) { + ASSERT_TRUE(OpenDocument("orphan_widgets.pdf")); + + EPDF_FORM_REPAIR_REPORT report; + ASSERT_TRUE(EPDFForm_Repair(document(), 0, &report)); + EXPECT_EQ(0u, report.acroform_created); + EXPECT_EQ(2u, report.fields_linked); // orphan_check + orphan_radio root + EXPECT_EQ(0u, report.widgets_linked); + EXPECT_EQ(0u, report.fields_unrepairable); + + // The reconciliation is now durable structure, not an in-memory patch. + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + ASSERT_EQ(3, EPDFForm_CountFields(model)); + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, + EPDFForm_GetFieldOrigin(model, i)); + } + EPDFForm_CloseModel(model); + + // Idempotent: a second pass fixes nothing. + ASSERT_TRUE(EPDFForm_Repair(document(), 0, &report)); + EXPECT_EQ(0u, report.fields_linked); + EXPECT_EQ(0u, report.widgets_linked); +} + +TEST_F(EPDFFormEmbedderTest, RepairCreatesAcroFormAndLinksKids) { + ASSERT_TRUE(OpenDocument("widgets_no_acroform.pdf")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_NONE, EPDFForm_GetFormKind(model)); + ASSERT_EQ(2, EPDFForm_CountFields(model)); + EPDFForm_CloseModel(model); + + EPDF_FORM_REPAIR_REPORT report; + ASSERT_TRUE(EPDFForm_Repair(document(), 0, &report)); + EXPECT_EQ(1u, report.acroform_created); + EXPECT_EQ(2u, report.fields_linked); // orphan_text + gap_radio + EXPECT_EQ(1u, report.widgets_linked); // widget 7 into gap_radio's /Kids + EXPECT_EQ(0u, report.fields_unrepairable); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_ACROFORM, EPDFForm_GetFormKind(model)); + ASSERT_EQ(2, EPDFForm_CountFields(model)); + const int radio = EPDFForm_GetFieldIndexByObjNum(model, 5u); + ASSERT_GE(radio, 0); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, + EPDFForm_GetFieldOrigin(model, radio)); + EXPECT_EQ(2, EPDFForm_CountFieldWidgets(model, radio)); + EPDFForm_CloseModel(model); +} + +// Repair on a layer is a tiny structural delta, and it survives a delta +// save/reload: the repaired document stays repaired. +TEST_F(EPDFFormEmbedderTest, RepairOnLayerIsDurable) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("orphan_widgets.pdf", &doc)); + + EPDF_FORM_REPAIR_REPORT report; + ASSERT_TRUE(EPDFForm_Repair(doc.layer, 0, &report)); + EXPECT_EQ(2u, report.fields_linked); + // /AcroForm lives inline in the catalog, so linking promotes exactly the + // root object and nothing else. + EXPECT_EQ(1ul, EPDFLayer_GetPromotedObjectCount(doc.layer)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 1u)); + + // Round-trip the delta into a second layer over the same base. + ClearString(); + EPDFLayerSaveStatus save_status; + ASSERT_TRUE(EPDFLayer_SaveDelta(doc.layer, this, &save_status)); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS file_access = {}; + file_access.m_FileLen = static_cast(delta.size()); + file_access.m_GetBlock = TestLoader::GetBlock; + file_access.m_Param = &loader; + + EPDFLayerOpenStatus status; + FPDF_DOCUMENT second = + EPDFLayer_OpenLayer(doc.base, &file_access, nullptr, &status); + ASSERT_TRUE(second); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(second); + ASSERT_TRUE(model); + ASSERT_EQ(3, EPDFForm_CountFields(model)); + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, + EPDFForm_GetFieldOrigin(model, i)); + } + EPDFForm_CloseModel(model); + FPDF_CloseDocument(second); +} + +TEST_F(EPDFFormEmbedderTest, RepairBakesMissingAppearances) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + + EPDF_FORM_REPAIR_REPORT report; + ASSERT_TRUE( + EPDFForm_Repair(document(), EPDF_FORM_REPAIR_BAKE_APPEARANCES, &report)); + // maxlen_text (4) and billing.name (17) ship without /AP. + EXPECT_GE(report.appearances_baked, 2u); + EXPECT_EQ(0u, report.need_appearances_cleared); // flag was never set + + // billing.name is /Annots index 7 on the page; it has an /AP now. + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page, 7)); + ASSERT_TRUE(annot); + EXPECT_GT(FPDFAnnot_GetAP(annot.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL, + nullptr, 0), + 2u); + } + UnloadPage(page); + + // Idempotent: everything has an appearance now. + ASSERT_TRUE( + EPDFForm_Repair(document(), EPDF_FORM_REPAIR_BAKE_APPEARANCES, &report)); + EXPECT_EQ(0u, report.appearances_baked); +} + +TEST_F(EPDFFormEmbedderTest, RepairBakeClearsNeedAppearances) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + RetainPtr acro_form = + doc->GetMutableRoot()->GetMutableDictFor("AcroForm"); + ASSERT_TRUE(acro_form); + acro_form->SetNewFor("NeedAppearances", true); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_TRUE(EPDFForm_GetNeedAppearances(model)); + EPDFForm_CloseModel(model); + + EPDF_FORM_REPAIR_REPORT report; + ASSERT_TRUE( + EPDFForm_Repair(document(), EPDF_FORM_REPAIR_BAKE_APPEARANCES, &report)); + EXPECT_GT(report.appearances_baked, 0u); + EXPECT_EQ(1u, report.need_appearances_cleared); + EXPECT_FALSE(acro_form->KeyExist("NeedAppearances")); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_FALSE(EPDFForm_GetNeedAppearances(model)); + EPDFForm_CloseModel(model); +} + +namespace { + +// Create an unattached widget annotation through the ANNOTATION API - the +// authoring model's first step (widgets are born as annotations). +uint32_t CreateWidgetAnnot(FPDF_PAGE page, + float left, + float bottom, + float right, + float top) { + // EPDFPage_CreateAnnot creates an INDIRECT annotation (durable object + // number), unlike upstream FPDFPage_CreateAnnot. + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page, FPDF_ANNOT_WIDGET)); + if (!annot) { + return 0; + } + FS_RECTF rect{left, top, right, bottom}; + if (!FPDFAnnot_SetRect(annot.get(), &rect)) { + return 0; + } + return EPDFAnnot_GetObjectNumber(annot.get()); +} + +} // namespace + +TEST_F(EPDFFormEmbedderTest, CreateUnplacedFieldBootstrapsAcroForm) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* text */, GetFPDFWideString(L"billing.name").get()); + ASSERT_GT(field, 0u); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(EPDF_FORMKIND_ACROFORM, EPDFForm_GetFormKind(model)); + ASSERT_EQ(1, EPDFForm_CountFields(model)); + EXPECT_EQ(L"billing.name", GetWideString(EPDFForm_GetFieldName, model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_FAMILY_TEXT, EPDFForm_GetFieldFamily(model, 0)); + EXPECT_EQ(EPDF_FORMFIELD_ORIGIN_ACROFORM, EPDFForm_GetFieldOrigin(model, 0)); + EXPECT_EQ(0, EPDFForm_CountFieldWidgets(model, 0)); // unplaced + EPDFForm_CloseModel(model); + + // Sibling collisions fail without touching the tree. + EXPECT_EQ(0u, EPDFForm_CreateField(document(), 4, + GetFPDFWideString(L"billing.name").get())); + EXPECT_EQ(0u, EPDFForm_CreateField(document(), 4, + GetFPDFWideString(L"billing").get())); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(1, EPDFForm_CountFields(model)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, AttachWidgetsFormsARadioGroup) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + + const uint32_t field = EPDFForm_CreateField( + document(), 3 /* radio */, GetFPDFWideString(L"gender").get()); + ASSERT_GT(field, 0u); + const uint32_t w1 = CreateWidgetAnnot(page, 20, 200, 40, 220); + const uint32_t w2 = CreateWidgetAnnot(page, 60, 200, 80, 220); + ASSERT_GT(w1, 0u); + ASSERT_GT(w2, 0u); + + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, w1, "male")); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, w2, "female")); + // Re-attaching an already attached widget fails. + EXPECT_FALSE(EPDFForm_AttachWidget(document(), field, w1, "male")); + // Toggles demand a usable on-state name. + EXPECT_FALSE(EPDFForm_AttachWidget(document(), field, w1, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int index = EPDFForm_GetFieldIndexByObjNum(model, field); + ASSERT_GE(index, 0); + ASSERT_EQ(2, EPDFForm_CountFieldWidgets(model, index)); + EXPECT_EQ("male", GetWidgetOnState(model, index, 0)); + EXPECT_EQ("female", GetWidgetOnState(model, index, 1)); + EPDFForm_CloseModel(model); + + // The newborn group is immediately fillable through the P1 transaction. + unsigned long changed = 0; + ASSERT_TRUE( + EPDFForm_SetToggle(document(), field, "male", nullptr, 0, &changed)); + EXPECT_EQ(1ul, changed); + model = EPDFForm_LoadModel(document()); + EXPECT_EQ(L"male", GetCurrentFieldValue( + model, EPDFForm_GetFieldIndexByObjNum(model, field))); + EPDFForm_CloseModel(model); + UnloadPage(page); +} + +TEST_F(EPDFFormEmbedderTest, AttachToLegacyMergedFieldKeepsFieldId) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + const int annots_before = FPDFPage_GetAnnotCount(page); + + // maxlen_text (object 4) is a merged field/widget. + const uint32_t widget = CreateWidgetAnnot(page, 20, 20, 280, 36); + ASSERT_GT(widget, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), 4u, widget, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int index = EPDFForm_GetFieldIndexByObjNum(model, 4u); + ASSERT_GE(index, 0); // the FIELD object number never changes + ASSERT_EQ(2, EPDFForm_CountFieldWidgets(model, index)); + // The split widget is a NEW object; neither widget is the field dict. + EXPECT_NE(4u, EPDFForm_GetFieldWidgetObjNum(model, index, 0)); + EXPECT_EQ(widget, EPDFForm_GetFieldWidgetObjNum(model, index, 1)); + EPDFForm_CloseModel(model); + + // /Annots: merged entry swapped for the split widget, new widget appended. + EXPECT_EQ(annots_before + 1, FPDFPage_GetAnnotCount(page)); + + // Both widgets still fill together. + ScopedFPDFWideString value = GetFPDFWideString(L"ab"); + ASSERT_TRUE( + EPDFForm_SetTextValue(document(), 4u, value.get(), nullptr, 0, nullptr)); + UnloadPage(page); +} + +TEST_F(EPDFFormEmbedderTest, DetachWidgetKeepsFieldVisible) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + + const uint32_t field = + EPDFForm_CreateField(document(), 4, GetFPDFWideString(L"note").get()); + const uint32_t widget = CreateWidgetAnnot(page, 20, 200, 200, 220); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, widget, nullptr)); + ASSERT_TRUE(EPDFForm_DetachWidget(document(), field, widget)); + // Detaching twice fails (no longer attached). + EXPECT_FALSE(EPDFForm_DetachWidget(document(), field, widget)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int index = EPDFForm_GetFieldIndexByObjNum(model, field); + ASSERT_GE(index, 0); // the field survives, unplaced + EXPECT_EQ(0, EPDFForm_CountFieldWidgets(model, index)); + // The widget is inert again: no field claims it. + EXPECT_EQ(-1, EPDFForm_GetFieldIndexForWidget(model, widget)); + EPDFForm_CloseModel(model); + UnloadPage(page); +} + +TEST_F(EPDFFormEmbedderTest, DeleteFieldDetachesAndPrunesAncestors) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + FPDF_PAGE page = LoadPage(0); + ASSERT_TRUE(page); + + const uint32_t field = EPDFForm_CreateField( + document(), 4, GetFPDFWideString(L"billing.name").get()); + const uint32_t widget = CreateWidgetAnnot(page, 20, 200, 200, 220); + ASSERT_TRUE(EPDFForm_AttachWidget(document(), field, widget, nullptr)); + + uint32_t detached[4] = {}; + unsigned long detached_count = 0; + ASSERT_TRUE( + EPDFForm_DeleteField(document(), field, detached, 4, &detached_count)); + EXPECT_EQ(1ul, detached_count); + EXPECT_EQ(widget, detached[0]); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + // The empty "billing" ancestor was pruned along with the field. + EXPECT_EQ(0, EPDFForm_CountFields(model)); + EPDFForm_CloseModel(model); + UnloadPage(page); +} + +TEST_F(EPDFFormEmbedderTest, FieldSettersValidateAndApply) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + + // Rename: the /T segment only; sibling collisions fail. + ASSERT_TRUE(EPDFForm_SetFieldName(document(), 17u, + GetFPDFWideString(L"fullName").get())); + EXPECT_FALSE(EPDFForm_SetFieldName(document(), 4u, + GetFPDFWideString(L"unison_radio").get())); + EXPECT_FALSE( + EPDFForm_SetFieldName(document(), 4u, GetFPDFWideString(L"a.b").get())); + + // Flags: masked update works; family-defining bits are immutable. + ASSERT_TRUE(EPDFForm_SetFieldFlags(document(), 4u, 1u << 1, 0)); // +Required + EXPECT_FALSE(EPDFForm_SetFieldFlags(document(), 4u, 1u << 15, 0)); + + // MaxLen: cannot cut below the current value ("abc"). + EXPECT_FALSE(EPDFForm_SetFieldMaxLen(document(), 4u, 2)); + ASSERT_TRUE(EPDFForm_SetFieldMaxLen(document(), 4u, 10)); + + ScopedFPDFWideString default_value = GetFPDFWideString(L"dflt"); + FPDF_WIDESTRING default_values[] = {default_value.get()}; + ASSERT_TRUE( + EPDFForm_SetFieldDefaultValues(document(), 4u, default_values, 1)); + ASSERT_TRUE(EPDFForm_SetFieldAlternateName( + document(), 4u, GetFPDFWideString(L"Your name").get())); + ASSERT_TRUE(EPDFForm_SetFieldMappingName(document(), 4u, + GetFPDFWideString(L"name_x").get())); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int index = EPDFForm_GetFieldIndexByObjNum(model, 17u); + EXPECT_EQ(L"billing.fullName", + GetWideString(EPDFForm_GetFieldName, model, index)); + index = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_TRUE(EPDFForm_GetFieldFlags(model, index) & (1u << 1)); + EXPECT_EQ(10, EPDFForm_GetFieldMaxLen(model, index)); + EXPECT_EQ(L"dflt", GetDefaultFieldValue(model, index)); + EXPECT_EQ(L"Your name", + GetWideString(EPDFForm_GetFieldAlternateName, model, index)); + EXPECT_EQ(L"name_x", + GetWideString(EPDFForm_GetFieldMappingName, model, index)); + EPDFForm_CloseModel(model); + + // Reset now restores the fresh /DV through the P1 transaction. + ASSERT_TRUE(EPDFForm_ResetField(document(), 4u, nullptr, 0, nullptr)); + model = EPDFForm_LoadModel(document()); + index = EPDFForm_GetFieldIndexByObjNum(model, 4u); + EXPECT_EQ(L"dflt", GetCurrentFieldValue(model, index)); + EPDFForm_CloseModel(model); +} + +TEST_F(EPDFFormEmbedderTest, EmptySettersShadowInheritedProperties) { + ASSERT_TRUE(OpenDocument("toggle_fields.pdf")); + RetainPtr parent = + GetMutableIndirectDictionary(document(), 16u); + ASSERT_TRUE(parent); + parent->SetNewFor("MaxLen", 8); + parent->SetNewFor(pdfium::form_fields::kTU, L"Parent tooltip"); + parent->SetNewFor(pdfium::form_fields::kTM, L"parent_mapping"); + parent->SetNewFor(pdfium::form_fields::kV, L"Parent value"); + + // Object 17 inherits /FT and these properties from object 16. Clearing the + // effective child properties must not mutate the shared parent. + ASSERT_TRUE(EPDFForm_SetFieldMaxLen(document(), 17u, 0)); + ASSERT_TRUE(EPDFForm_SetFieldAlternateName(document(), 17u, + GetFPDFWideString(L"").get())); + ASSERT_TRUE(EPDFForm_SetFieldMappingName(document(), 17u, + GetFPDFWideString(L"").get())); + ASSERT_TRUE(EPDFForm_ResetField(document(), 17u, nullptr, 0, nullptr)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 17u); + ASSERT_GE(field, 0); + EXPECT_EQ(0, EPDFForm_GetFieldMaxLen(model, field)); + EXPECT_EQ(L"", GetWideString(EPDFForm_GetFieldAlternateName, model, field)); + EXPECT_EQ(L"", GetWideString(EPDFForm_GetFieldMappingName, model, field)); + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, EPDFForm_GetFieldValueKind(model, field)); + EXPECT_EQ(L"", GetCurrentFieldValue(model, field)); + EPDFForm_CloseModel(model); + + EXPECT_EQ(8, parent->GetIntegerFor("MaxLen")); + EXPECT_EQ(L"Parent tooltip", + parent->GetUnicodeTextFor(pdfium::form_fields::kTU)); + EXPECT_EQ(L"parent_mapping", + parent->GetUnicodeTextFor(pdfium::form_fields::kTM)); + EXPECT_EQ(L"Parent value", + parent->GetUnicodeTextFor(pdfium::form_fields::kV)); + RetainPtr child = + GetEffectiveIndirectDictionary(document(), 17u); + ASSERT_TRUE(child); + EXPECT_EQ(0, child->GetIntegerFor("MaxLen")); + EXPECT_TRUE(child->KeyExist(pdfium::form_fields::kTU)); + EXPECT_TRUE(child->KeyExist(pdfium::form_fields::kTM)); + EXPECT_TRUE(child->KeyExist(pdfium::form_fields::kV)); +} + +TEST_F(EPDFFormEmbedderTest, SetFieldOptionsResyncsSelection) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int index = FieldIndexByName(model, L"Listbox_MultiSelectMultipleValues"); + ASSERT_GE(index, 0); + const uint32_t field = EPDFForm_GetFieldObjNum(model, index); + EPDFForm_CloseModel(model); + + // Current /V is [Epsilon, Gamma]; the new option list keeps only Gamma. + ScopedFPDFWideString alpha = GetFPDFWideString(L"Alpha"); + ScopedFPDFWideString gamma = GetFPDFWideString(L"Gamma"); + ScopedFPDFWideString zeta = GetFPDFWideString(L"Zeta"); + ScopedFPDFWideString epsilon = GetFPDFWideString(L"Epsilon"); + FPDF_WIDESTRING defaults[] = {epsilon.get(), gamma.get()}; + ASSERT_TRUE(EPDFForm_SetFieldDefaultValues(document(), field, defaults, 2)); + FPDF_WIDESTRING labels[] = {alpha.get(), gamma.get(), zeta.get()}; + ASSERT_TRUE(EPDFForm_SetFieldOptions(document(), field, labels, labels, 3)); + + model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + index = EPDFForm_GetFieldIndexByObjNum(model, field); + ASSERT_EQ(3, EPDFForm_CountFieldOptions(model, index)); + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, index, 0)); // Alpha + EXPECT_TRUE(EPDFForm_IsFieldOptionSelected(model, index, 1)); // Gamma kept + EXPECT_FALSE(EPDFForm_IsFieldOptionSelected(model, index, 2)); // Zeta + EXPECT_EQ(EPDF_FORM_VALUE_SCALAR, + EPDFForm_GetFieldDefaultValueKind(model, index)); + EXPECT_EQ(L"Gamma", GetDefaultFieldValue(model, index)); + EPDFForm_CloseModel(model); + + ASSERT_TRUE(EPDFForm_ResetField(document(), field, nullptr, 0, nullptr)); + model = EPDFForm_LoadModel(document()); + index = EPDFForm_GetFieldIndexByObjNum(model, field); + EXPECT_EQ(L"Gamma", GetCurrentFieldValue(model, index)); + EPDFForm_CloseModel(model); + + RetainPtr field_dictionary = + GetEffectiveIndirectDictionary(document(), field); + ASSERT_TRUE(field_dictionary); + RetainPtr indices = field_dictionary->GetArrayFor("I"); + ASSERT_TRUE(indices); + ASSERT_EQ(1u, indices->size()); + EXPECT_EQ(1, indices->GetIntegerAt(0)); +} + +TEST_F(EPDFFormEmbedderTest, FieldFlagsRejectInvalidChoiceShapeTransitions) { + ASSERT_TRUE(OpenDocument("listbox_form.pdf")); + + // Object 12 has array /V and MultiSelect. Clearing MultiSelect would make + // the existing /V invalid, so the transaction is rejected unchanged. + EXPECT_FALSE(EPDFForm_SetFieldFlags(document(), 12u, 0, + pdfium::form_flags::kChoiceMultiSelect)); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + int field = EPDFForm_GetFieldIndexByObjNum(model, 12u); + ASSERT_GE(field, 0); + EXPECT_TRUE(EPDFForm_GetFieldFlags(model, field) & + pdfium::form_flags::kChoiceMultiSelect); + EXPECT_EQ(EPDF_FORM_VALUE_ARRAY, EPDFForm_GetFieldValueKind(model, field)); + EPDFForm_CloseModel(model); + + // Edit is a combo-only flag; setting it on a list box is invalid. + EXPECT_FALSE(EPDFForm_SetFieldFlags(document(), 8u, + pdfium::form_flags::kChoiceEdit, 0)); +} + +// Authoring on a layer produces a minimal, durable delta. +TEST_F(EPDFFormEmbedderTest, AuthoringOnLayerIsDurable) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("hello_world.pdf", &doc)); + + const uint32_t field = EPDFForm_CreateField( + doc.layer, 4, GetFPDFWideString(L"layer_field").get()); + ASSERT_GT(field, 0u); + FPDF_PAGE page = FPDF_LoadPage(doc.layer, 0); + ASSERT_TRUE(page); + const uint32_t widget = CreateWidgetAnnot(page, 20, 200, 200, 220); + ASSERT_GT(widget, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget(doc.layer, field, widget, nullptr)); + FPDF_ClosePage(page); + + // Duplicate create fails without growing the delta. + const unsigned long promoted = EPDFLayer_GetPromotedObjectCount(doc.layer); + EXPECT_EQ(0u, EPDFForm_CreateField(doc.layer, 4, + GetFPDFWideString(L"layer_field").get())); + EXPECT_EQ(promoted, EPDFLayer_GetPromotedObjectCount(doc.layer)); + + ClearString(); + EPDFLayerSaveStatus save_status; + ASSERT_TRUE(EPDFLayer_SaveDelta(doc.layer, this, &save_status)); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS file_access = {}; + file_access.m_FileLen = static_cast(delta.size()); + file_access.m_GetBlock = TestLoader::GetBlock; + file_access.m_Param = &loader; + EPDFLayerOpenStatus status; + FPDF_DOCUMENT second = + EPDFLayer_OpenLayer(doc.base, &file_access, nullptr, &status); + ASSERT_TRUE(second); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(second); + ASSERT_TRUE(model); + const int index = EPDFForm_GetFieldIndexByObjNum(model, field); + ASSERT_GE(index, 0); + EXPECT_EQ(L"layer_field", GetWideString(EPDFForm_GetFieldName, model, index)); + EXPECT_EQ(1, EPDFForm_CountFieldWidgets(model, index)); + EPDFForm_CloseModel(model); + FPDF_CloseDocument(second); +} diff --git a/fpdfsdk/epdf_pieceinfo.cpp b/fpdfsdk/epdf_pieceinfo.cpp new file mode 100644 index 0000000000..ad44d32950 --- /dev/null +++ b/fpdfsdk/epdf_pieceinfo.cpp @@ -0,0 +1,1106 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_pieceinfo.h" + +#include +#include + +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_boolean.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.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_string.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/fx_string_wrappers.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "fpdfsdk/cpdfsdk_helpers.h" + +namespace { + +constexpr char kPieceInfoKey[] = "PieceInfo"; +constexpr char kPrivateKey[] = "Private"; +constexpr char kLastModifiedKey[] = "LastModified"; +constexpr char kModDateKey[] = "ModDate"; + +const CPDF_Dictionary* GetDocumentCatalog(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? doc->GetRoot() : nullptr; +} + +RetainPtr GetMutableDocumentCatalog(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? doc->GetMutableRoot() : nullptr; +} + +RetainPtr GetDocumentInfo(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + return doc ? doc->GetInfo() : nullptr; +} + +RetainPtr GetPageDictionaryByObjectNumber( + FPDF_DOCUMENT document, + unsigned int page_object_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || page_object_number == 0) { + return nullptr; + } + +#ifdef PDF_ENABLE_XFA + if (doc->GetExtension()) { + return nullptr; + } +#endif // PDF_ENABLE_XFA + + const int page_index = doc->GetPageIndex(page_object_number); + return page_index >= 0 ? doc->GetPageDictionary(page_index) : nullptr; +} + +RetainPtr GetMutablePageDictionaryByObjectNumber( + FPDF_DOCUMENT document, + unsigned int page_object_number) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || page_object_number == 0) { + return nullptr; + } + +#ifdef PDF_ENABLE_XFA + if (doc->GetExtension()) { + return nullptr; + } +#endif // PDF_ENABLE_XFA + + const int page_index = doc->GetPageIndex(page_object_number); + return page_index >= 0 ? doc->GetMutablePageDictionary(page_index) : nullptr; +} + +RetainPtr GetApplicationDictionary( + const CPDF_Dictionary* page, + FPDF_BYTESTRING application) { + if (!page || !application || !*application) { + return nullptr; + } + RetainPtr piece_info = page->GetDictFor(kPieceInfoKey); + return piece_info ? piece_info->GetDictFor(application) : nullptr; +} + +RetainPtr GetPrivateDictionary( + const CPDF_Dictionary* page, + FPDF_BYTESTRING application) { + RetainPtr app = + GetApplicationDictionary(page, application); + return app ? app->GetDictFor(kPrivateKey) : nullptr; +} + +RetainPtr GetPrivateObject(const CPDF_Dictionary* page, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key) { + if (!key || !*key) { + return nullptr; + } + RetainPtr private_dict = + GetPrivateDictionary(page, application); + return private_dict ? private_dict->GetDirectObjectFor(key) : nullptr; +} + +RetainPtr GetOrCreateDictionary(CPDF_Dictionary* parent, + ByteStringView key) { + if (!parent) { + return nullptr; + } + if (parent->KeyExist(key)) { + return parent->GetMutableDictFor(key); + } + return parent->SetNewFor(ByteString(key)); +} + +struct MutablePieceInfo { + RetainPtr holder; + RetainPtr application; + RetainPtr private_dict; +}; + +std::optional GetTimestamp(FPDF_WIDESTRING content_last_modified) { + if (!content_last_modified) { + return std::nullopt; + } + WideString timestamp = + UNSAFE_BUFFERS(WideStringFromFPDFWideString(content_last_modified)); + return timestamp.IsEmpty() ? std::nullopt + : std::optional(std::move(timestamp)); +} + +MutablePieceInfo GetOrCreateMutablePieceInfo( + FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_WIDESTRING content_last_modified) { + if (!application || !*application) { + return {}; + } + std::optional timestamp = GetTimestamp(content_last_modified); + if (!timestamp.has_value()) { + return {}; + } + + RetainPtr page = + GetMutablePageDictionaryByObjectNumber(document, page_object_number); + if (!page) { + return {}; + } + RetainPtr piece_info = + GetOrCreateDictionary(page.Get(), kPieceInfoKey); + if (!piece_info) { + return {}; + } + RetainPtr app = + GetOrCreateDictionary(piece_info.Get(), application); + if (!app) { + return {}; + } + RetainPtr private_dict = + GetOrCreateDictionary(app.Get(), kPrivateKey); + if (!private_dict) { + return {}; + } + + page->SetNewFor(kLastModifiedKey, timestamp->AsStringView()); + app->SetNewFor(kLastModifiedKey, timestamp->AsStringView()); + return {std::move(page), std::move(app), std::move(private_dict)}; +} + +MutablePieceInfo GetOrCreateMutableDocumentPieceInfo( + FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_WIDESTRING document_last_modified) { + if (!application || !*application) { + return {}; + } + std::optional timestamp = GetTimestamp(document_last_modified); + if (!timestamp.has_value()) { + return {}; + } + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return {}; + } + RetainPtr catalog = doc->GetMutableRoot(); + RetainPtr info = doc->GetOrCreateInfo(); + if (!catalog || !info) { + return {}; + } + RetainPtr piece_info = + GetOrCreateDictionary(catalog.Get(), kPieceInfoKey); + if (!piece_info) { + return {}; + } + RetainPtr app = + GetOrCreateDictionary(piece_info.Get(), application); + if (!app) { + return {}; + } + RetainPtr private_dict = + GetOrCreateDictionary(app.Get(), kPrivateKey); + if (!private_dict) { + return {}; + } + + info->SetNewFor(kModDateKey, timestamp->AsStringView()); + app->SetNewFor(kLastModifiedKey, timestamp->AsStringView()); + return {std::move(catalog), std::move(app), std::move(private_dict)}; +} + +unsigned long CopyWideString(const WideString& value, + FPDF_WCHAR* buffer, + unsigned long buflen) { + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + value, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +unsigned long CopyByteString(ByteStringView value, + char* buffer, + unsigned long buflen) { + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + ByteString(value), UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_HasPieceInfoEntry(FPDF_DOCUMENT document, FPDF_BYTESTRING application) { + return !!GetApplicationDictionary(GetDocumentCatalog(document), application); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoEntryCount(FPDF_DOCUMENT document) { + const CPDF_Dictionary* catalog = GetDocumentCatalog(document); + RetainPtr piece_info = + catalog ? catalog->GetDictFor(kPieceInfoKey) : nullptr; + return piece_info ? static_cast(piece_info->size()) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoEntryAt(FPDF_DOCUMENT document, + int index, + char* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + const CPDF_Dictionary* catalog = GetDocumentCatalog(document); + RetainPtr piece_info = + catalog ? catalog->GetDictFor(kPieceInfoKey) : nullptr; + if (!piece_info || index >= static_cast(piece_info->size())) { + return 0; + } + + int current = 0; + CPDF_DictionaryLocker locker(piece_info); + for (const auto& item : locker) { + if (current++ == index) { + return CopyByteString(item.first.AsStringView(), buffer, buflen); + } + } + return 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetLastModified(FPDF_DOCUMENT document, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr info = GetDocumentInfo(document); + if (!info) { + return 0; + } + RetainPtr value = info->GetDirectObjectFor(kModDateKey); + return value && value->GetType() == CPDF_Object::Type::kString + ? CopyWideString(value->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoLastModified(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr app = + GetApplicationDictionary(GetDocumentCatalog(document), application); + if (!app) { + return 0; + } + RetainPtr value = + app->GetDirectObjectFor(kLastModifiedKey); + return value && value->GetType() == CPDF_Object::Type::kString + ? CopyWideString(value->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoKeyCount(FPDF_DOCUMENT document, + FPDF_BYTESTRING application) { + RetainPtr private_dict = + GetPrivateDictionary(GetDocumentCatalog(document), application); + return private_dict ? static_cast(private_dict->size()) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoKeyAt(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + int index, + char* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + RetainPtr private_dict = + GetPrivateDictionary(GetDocumentCatalog(document), application); + if (!private_dict || index >= static_cast(private_dict->size())) { + return 0; + } + + int current = 0; + CPDF_DictionaryLocker locker(private_dict); + for (const auto& item : locker) { + if (current++ == index) { + return CopyByteString(item.first.AsStringView(), buffer, buflen); + } + } + return 0; +} + +FPDF_EXPORT FPDF_OBJECT_TYPE FPDF_CALLCONV +EPDFDoc_GetPieceInfoValueType(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key) { + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + return object ? static_cast(object->GetType()) + : FPDF_OBJECT_UNKNOWN; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoString(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key || !value) { + return false; + } + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor( + key, UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoString(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + return object && object->GetType() == CPDF_Object::Type::kString + ? CopyWideString(object->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoNumber(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float value, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key) { + return false; + } + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPieceInfoNumber(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float* value) { + if (!value) { + return false; + } + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + if (!object || object->GetType() != CPDF_Object::Type::kNumber) { + return false; + } + *value = object->GetNumber(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoBoolean(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL value, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key) { + return false; + } + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, !!value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPieceInfoBoolean(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL* value) { + if (!value) { + return false; + } + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + if (!object || object->GetType() != CPDF_Object::Type::kBoolean) { + return false; + } + *value = object->GetInteger() != 0; + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoName(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BYTESTRING value, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key || !value || !*value) { + return false; + } + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoName(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + char* buffer, + unsigned long buflen) { + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + return object && object->GetType() == CPDF_Object::Type::kName + ? CopyByteString(object->GetString().AsStringView(), buffer, + buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoStringArray(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + const FPDF_WIDESTRING* values, + unsigned long value_count, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key || (value_count > 0 && !values)) { + return false; + } + for (unsigned long i = 0; i < value_count; ++i) { + if (!UNSAFE_BUFFERS(values[i])) { + return false; + } + } + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + + auto array = pdfium::MakeRetain(); + for (unsigned long i = 0; i < value_count; ++i) { + FPDF_WIDESTRING value = UNSAFE_BUFFERS(values[i]); + array->AppendNew( + UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + } + target.private_dict->SetFor(key, std::move(array)); + return true; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoStringArrayCount(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key) { + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + RetainPtr array = ToArray(std::move(object)); + if (!array) { + return -1; + } + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr item = array->GetDirectObjectAt(i); + if (!item || item->GetType() != CPDF_Object::Type::kString) { + return -1; + } + } + return static_cast(array->size()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoStringArrayAt(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + RetainPtr object = + GetPrivateObject(GetDocumentCatalog(document), application, key); + RetainPtr array = ToArray(std::move(object)); + if (!array || index >= static_cast(array->size())) { + return 0; + } + RetainPtr item = array->GetDirectObjectAt(index); + return item && item->GetType() == CPDF_Object::Type::kString + ? CopyWideString(item->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPieceInfoKey(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING document_last_modified) { + if (!key || !*key || !application || !*application) { + return false; + } + + const CPDF_Dictionary* catalog = GetDocumentCatalog(document); + if (!catalog) { + return false; + } + RetainPtr piece_info_object = + catalog->GetDirectObjectFor(kPieceInfoKey); + if (!piece_info_object) { + return true; + } + const CPDF_Dictionary* piece_info = piece_info_object->AsDictionary(); + if (!piece_info) { + return false; + } + RetainPtr app_object = + piece_info->GetDirectObjectFor(application); + if (!app_object) { + return true; + } + const CPDF_Dictionary* app = app_object->AsDictionary(); + if (!app) { + return false; + } + RetainPtr private_object = + app->GetDirectObjectFor(kPrivateKey); + if (!private_object) { + return true; + } + const CPDF_Dictionary* private_dict = private_object->AsDictionary(); + if (!private_dict) { + return false; + } + if (!private_dict->KeyExist(key)) { + return true; + } + + MutablePieceInfo target = GetOrCreateMutableDocumentPieceInfo( + document, application, document_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->RemoveFor(key); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPieceInfoEntry(FPDF_DOCUMENT document, + FPDF_BYTESTRING application) { + if (!application || !*application) { + return false; + } + const CPDF_Dictionary* const_catalog = GetDocumentCatalog(document); + if (!const_catalog) { + return false; + } + RetainPtr piece_info_object = + const_catalog->GetDirectObjectFor(kPieceInfoKey); + if (!piece_info_object) { + return true; + } + const CPDF_Dictionary* const_piece_info = piece_info_object->AsDictionary(); + if (!const_piece_info) { + return false; + } + if (!const_piece_info->KeyExist(application)) { + return true; + } + + RetainPtr catalog = GetMutableDocumentCatalog(document); + if (!catalog) { + return false; + } + RetainPtr piece_info = + catalog->GetMutableDictFor(kPieceInfoKey); + if (!piece_info) { + return !catalog->KeyExist(kPieceInfoKey); + } + piece_info->RemoveFor(application); + if (piece_info->size() == 0) { + catalog->RemoveFor(kPieceInfoKey); + } + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_HasPagePieceInfoEntry(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + return !!GetApplicationDictionary(page.Get(), application); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoEntryCount(FPDF_DOCUMENT document, + unsigned int page_object_number) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr piece_info = + page ? page->GetDictFor(kPieceInfoKey) : nullptr; + return piece_info ? static_cast(piece_info->size()) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoEntryAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + int index, + char* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr piece_info = + page ? page->GetDictFor(kPieceInfoKey) : nullptr; + if (!piece_info || index >= static_cast(piece_info->size())) { + return 0; + } + + int current = 0; + CPDF_DictionaryLocker locker(piece_info); + for (const auto& item : locker) { + if (current++ == index) { + return CopyByteString(item.first.AsStringView(), buffer, buflen); + } + } + return 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPageLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + if (!page) { + return 0; + } + RetainPtr value = + page->GetDirectObjectFor(kLastModifiedKey); + return value && value->GetType() == CPDF_Object::Type::kString + ? CopyWideString(value->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr app = + GetApplicationDictionary(page.Get(), application); + if (!app) { + return 0; + } + RetainPtr value = + app->GetDirectObjectFor(kLastModifiedKey); + return value && value->GetType() == CPDF_Object::Type::kString + ? CopyWideString(value->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoKeyCount(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr private_dict = + GetPrivateDictionary(page.Get(), application); + return private_dict ? static_cast(private_dict->size()) : 0; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoKeyAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + int index, + char* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr private_dict = + GetPrivateDictionary(page.Get(), application); + if (!private_dict || index >= static_cast(private_dict->size())) { + return 0; + } + + int current = 0; + CPDF_DictionaryLocker locker(private_dict); + for (const auto& item : locker) { + if (current++ == index) { + return CopyByteString(item.first.AsStringView(), buffer, buflen); + } + } + return 0; +} + +FPDF_EXPORT FPDF_OBJECT_TYPE FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoValueType(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + return object ? static_cast(object->GetType()) + : FPDF_OBJECT_UNKNOWN; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoString(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key || !value) { + return false; + } + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor( + key, UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoString(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + return object && object->GetType() == CPDF_Object::Type::kString + ? CopyWideString(object->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoNumber(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float value, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key) { + return false; + } + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoNumber(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float* value) { + if (!value) { + return false; + } + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + if (!object || object->GetType() != CPDF_Object::Type::kNumber) { + return false; + } + *value = object->GetNumber(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoBoolean(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL value, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key) { + return false; + } + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, !!value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoBoolean(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL* value) { + if (!value) { + return false; + } + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + if (!object || object->GetType() != CPDF_Object::Type::kBoolean) { + return false; + } + *value = object->GetInteger() != 0; + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoName(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BYTESTRING value, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key || !value || !*value) { + return false; + } + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoName(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + char* buffer, + unsigned long buflen) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + return object && object->GetType() == CPDF_Object::Type::kName + ? CopyByteString(object->GetString().AsStringView(), buffer, + buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoStringArray(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + const FPDF_WIDESTRING* values, + unsigned long value_count, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key || (value_count > 0 && !values)) { + return false; + } + for (unsigned long i = 0; i < value_count; ++i) { + if (!UNSAFE_BUFFERS(values[i])) { + return false; + } + } + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + + auto array = pdfium::MakeRetain(); + for (unsigned long i = 0; i < value_count; ++i) { + FPDF_WIDESTRING value = UNSAFE_BUFFERS(values[i]); + array->AppendNew( + UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + } + target.private_dict->SetFor(key, std::move(array)); + return true; +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoStringArrayCount(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key) { + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + RetainPtr array = ToArray(std::move(object)); + if (!array) { + return -1; + } + for (size_t i = 0; i < array->size(); ++i) { + RetainPtr item = array->GetDirectObjectAt(i); + if (!item || item->GetType() != CPDF_Object::Type::kString) { + return -1; + } + } + return static_cast(array->size()); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoStringArrayAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (index < 0) { + return 0; + } + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + RetainPtr object = + GetPrivateObject(page.Get(), application, key); + RetainPtr array = ToArray(std::move(object)); + if (!array || index >= static_cast(array->size())) { + return 0; + } + RetainPtr item = array->GetDirectObjectAt(index); + return item && item->GetType() == CPDF_Object::Type::kString + ? CopyWideString(item->GetUnicodeText(), buffer, buflen) + : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPagePieceInfoKey(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING content_last_modified) { + if (!key || !*key) { + return false; + } + + RetainPtr page = + GetPageDictionaryByObjectNumber(document, page_object_number); + if (!page || !application || !*application) { + return false; + } + RetainPtr piece_info_object = + page->GetDirectObjectFor(kPieceInfoKey); + if (!piece_info_object) { + return true; + } + const CPDF_Dictionary* piece_info = piece_info_object->AsDictionary(); + if (!piece_info) { + return false; + } + RetainPtr app_object = + piece_info->GetDirectObjectFor(application); + if (!app_object) { + return true; + } + const CPDF_Dictionary* app = app_object->AsDictionary(); + if (!app) { + return false; + } + RetainPtr private_object = + app->GetDirectObjectFor(kPrivateKey); + if (!private_object) { + return true; + } + const CPDF_Dictionary* private_dict = private_object->AsDictionary(); + if (!private_dict) { + return false; + } + if (!private_dict->KeyExist(key)) { + return true; + } + + MutablePieceInfo target = GetOrCreateMutablePieceInfo( + document, page_object_number, application, content_last_modified); + if (!target.private_dict) { + return false; + } + target.private_dict->RemoveFor(key); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPagePieceInfoEntry(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application) { + if (!application || !*application) { + return false; + } + RetainPtr const_page = + GetPageDictionaryByObjectNumber(document, page_object_number); + if (!const_page) { + return false; + } + RetainPtr piece_info_object = + const_page->GetDirectObjectFor(kPieceInfoKey); + if (!piece_info_object) { + return true; + } + const CPDF_Dictionary* const_piece_info = piece_info_object->AsDictionary(); + if (!const_piece_info) { + return false; + } + if (!const_piece_info->KeyExist(application)) { + return true; + } + + RetainPtr page = + GetMutablePageDictionaryByObjectNumber(document, page_object_number); + if (!page) { + return false; + } + RetainPtr piece_info = + page->GetMutableDictFor(kPieceInfoKey); + if (!piece_info) { + return !page->KeyExist(kPieceInfoKey); + } + piece_info->RemoveFor(application); + if (piece_info->size() == 0) { + page->RemoveFor(kPieceInfoKey); + } + return true; +} diff --git a/fpdfsdk/epdf_pieceinfo_embeddertest.cpp b/fpdfsdk/epdf_pieceinfo_embeddertest.cpp new file mode 100644 index 0000000000..4c89387393 --- /dev/null +++ b/fpdfsdk/epdf_pieceinfo_embeddertest.cpp @@ -0,0 +1,555 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/epdf_pieceinfo.h" + +#include +#include +#include + +#include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_edit.h" +#include "public/fpdf_ppo.h" +#include "public/fpdf_save.h" +#include "public/fpdfview.h" +#include "testing/embedder_test.h" +#include "testing/fx_string_testhelpers.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +std::wstring GetDocumentLastModified(FPDF_DOCUMENT document) { + const unsigned long length = EPDFDoc_GetLastModified(document, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetLastModified(document, buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetDocumentPieceInfoLastModified(FPDF_DOCUMENT document, + const char* application) { + const unsigned long length = + EPDFDoc_GetPieceInfoLastModified(document, application, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPieceInfoLastModified(document, application, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetDocumentPieceInfoString(FPDF_DOCUMENT document, + const char* application, + const char* key) { + const unsigned long length = + EPDFDoc_GetPieceInfoString(document, application, key, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPieceInfoString(document, application, key, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetDocumentPieceInfoStringArrayAt(FPDF_DOCUMENT document, + const char* application, + const char* key, + int index) { + const unsigned long length = EPDFDoc_GetPieceInfoStringArrayAt( + document, application, key, index, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, + EPDFDoc_GetPieceInfoStringArrayAt(document, application, key, index, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::string GetDocumentPieceInfoName(FPDF_DOCUMENT document, + const char* application, + const char* key) { + const unsigned long length = + EPDFDoc_GetPieceInfoName(document, application, key, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, EPDFDoc_GetPieceInfoName(document, application, key, + buffer.data(), length)); + return std::string(buffer.data()); +} + +std::string GetDocumentPieceInfoEntryAt(FPDF_DOCUMENT document, int index) { + const unsigned long length = + EPDFDoc_GetPieceInfoEntryAt(document, index, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, EPDFDoc_GetPieceInfoEntryAt(document, index, buffer.data(), + length)); + return std::string(buffer.data()); +} + +std::wstring GetPageLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number) { + const unsigned long length = + EPDFDoc_GetPageLastModified(document, page_object_number, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPageLastModified(document, page_object_number, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetPieceInfoLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number, + const char* application) { + const unsigned long length = EPDFDoc_GetPagePieceInfoLastModified( + document, page_object_number, application, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPagePieceInfoLastModified( + document, page_object_number, application, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetPieceInfoString(FPDF_DOCUMENT document, + unsigned int page_object_number, + const char* application, + const char* key) { + const unsigned long length = EPDFDoc_GetPagePieceInfoString( + document, page_object_number, application, key, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPagePieceInfoString(document, page_object_number, + application, key, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::wstring GetPieceInfoStringArrayAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + const char* application, + const char* key, + int index) { + const unsigned long length = EPDFDoc_GetPagePieceInfoStringArrayAt( + document, page_object_number, application, key, index, nullptr, 0); + if (length == 0) { + return std::wstring(); + } + std::vector buffer = GetFPDFWideStringBuffer(length); + EXPECT_EQ(length, EPDFDoc_GetPagePieceInfoStringArrayAt( + document, page_object_number, application, key, index, + buffer.data(), length)); + return GetPlatformWString(buffer.data()); +} + +std::string GetPieceInfoName(FPDF_DOCUMENT document, + unsigned int page_object_number, + const char* application, + const char* key) { + const unsigned long length = EPDFDoc_GetPagePieceInfoName( + document, page_object_number, application, key, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, EPDFDoc_GetPagePieceInfoName(document, page_object_number, + application, key, + buffer.data(), length)); + return std::string(buffer.data()); +} + +std::string GetPieceInfoEntryAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + int index) { + const unsigned long length = EPDFDoc_GetPagePieceInfoEntryAt( + document, page_object_number, index, nullptr, 0); + if (length == 0) { + return std::string(); + } + std::vector buffer(length); + EXPECT_EQ(length, + EPDFDoc_GetPagePieceInfoEntryAt(document, page_object_number, index, + buffer.data(), length)); + return std::string(buffer.data()); +} + +} // namespace + +class EPDFPieceInfoEmbedderTest : public EmbedderTest {}; + +TEST_F(EPDFPieceInfoEmbedderTest, CatalogTypedValuesSaveReloadAndLayerRead) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 240, 100)); + ASSERT_TRUE(page); + + ScopedFPDFWideString timestamp = + GetFPDFWideString(L"D:20260713153000+03'00'"); + ScopedFPDFWideString name = GetFPDFWideString(L"Company Stamps \u2713"); + ScopedFPDFWideString review = GetFPDFWideString(L"Review"); + ScopedFPDFWideString internal = GetFPDFWideString(L"Internal"); + const FPDF_WIDESTRING categories[] = {review.get(), internal.get()}; + + EXPECT_TRUE(EPDFDoc_SetPieceInfoString(document(), "EMBD_StampLibrary", + "Name", name.get(), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoNumber(document(), "EMBD_StampLibrary", + "Version", 1.0f, timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoBoolean(document(), "EMBD_StampLibrary", + "Archived", true, timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoName(document(), "EMBD_StampLibrary", "Kind", + "StampLibrary", timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoStringArray( + document(), "EMBD_StampLibrary", "Categories", categories, + std::size(categories), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoBoolean(document(), "EMBD_Other", "Pinned", + true, timestamp.get())); + + EXPECT_TRUE(EPDFDoc_HasPieceInfoEntry(document(), "EMBD_StampLibrary")); + EXPECT_EQ(2, EPDFDoc_GetPieceInfoEntryCount(document())); + std::set applications; + applications.insert(GetDocumentPieceInfoEntryAt(document(), 0)); + applications.insert(GetDocumentPieceInfoEntryAt(document(), 1)); + EXPECT_EQ((std::set{"EMBD_Other", "EMBD_StampLibrary"}), + applications); + EXPECT_EQ(5, EPDFDoc_GetPieceInfoKeyCount(document(), "EMBD_StampLibrary")); + + EXPECT_EQ(FPDF_OBJECT_STRING, EPDFDoc_GetPieceInfoValueType( + document(), "EMBD_StampLibrary", "Name")); + EXPECT_EQ(FPDF_OBJECT_NUMBER, + EPDFDoc_GetPieceInfoValueType(document(), "EMBD_StampLibrary", + "Version")); + EXPECT_EQ(FPDF_OBJECT_BOOLEAN, + EPDFDoc_GetPieceInfoValueType(document(), "EMBD_StampLibrary", + "Archived")); + EXPECT_EQ(FPDF_OBJECT_NAME, EPDFDoc_GetPieceInfoValueType( + document(), "EMBD_StampLibrary", "Kind")); + EXPECT_EQ(FPDF_OBJECT_ARRAY, + EPDFDoc_GetPieceInfoValueType(document(), "EMBD_StampLibrary", + "Categories")); + EXPECT_EQ( + L"Company Stamps \u2713", + GetDocumentPieceInfoString(document(), "EMBD_StampLibrary", "Name")); + float number = 0.0f; + EXPECT_TRUE(EPDFDoc_GetPieceInfoNumber(document(), "EMBD_StampLibrary", + "Version", &number)); + EXPECT_FLOAT_EQ(1.0f, number); + FPDF_BOOL boolean = false; + EXPECT_TRUE(EPDFDoc_GetPieceInfoBoolean(document(), "EMBD_StampLibrary", + "Archived", &boolean)); + EXPECT_TRUE(boolean); + EXPECT_EQ("StampLibrary", + GetDocumentPieceInfoName(document(), "EMBD_StampLibrary", "Kind")); + EXPECT_EQ(2, EPDFDoc_GetPieceInfoStringArrayCount( + document(), "EMBD_StampLibrary", "Categories")); + EXPECT_EQ(L"Review", GetDocumentPieceInfoStringArrayAt( + document(), "EMBD_StampLibrary", "Categories", 0)); + EXPECT_EQ(L"Internal", GetDocumentPieceInfoStringArrayAt( + document(), "EMBD_StampLibrary", "Categories", 1)); + EXPECT_EQ(L"D:20260713153000+03'00'", GetDocumentLastModified(document())); + EXPECT_EQ(L"D:20260713153000+03'00'", + GetDocumentPieceInfoLastModified(document(), "EMBD_StampLibrary")); + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + const std::string saved_pdf = GetString(); + ASSERT_FALSE(saved_pdf.empty()); + + ScopedSavedDoc saved_document = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_document); + EXPECT_EQ(L"Company Stamps \u2713", + GetDocumentPieceInfoString(saved_document.get(), + "EMBD_StampLibrary", "Name")); + EXPECT_EQ(L"D:20260713153000+03'00'", + GetDocumentLastModified(saved_document.get())); + + EPDF_BASE_DOCUMENT base = + EPDF_LoadMemBaseDocument64(saved_pdf.data(), saved_pdf.size(), nullptr); + ASSERT_TRUE(base); + { + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + EXPECT_EQ(2, EPDFDoc_GetPieceInfoEntryCount(layer.get())); + EXPECT_EQ( + L"Company Stamps \u2713", + GetDocumentPieceInfoString(layer.get(), "EMBD_StampLibrary", "Name")); + EXPECT_EQ(L"D:20260713153000+03'00'", GetDocumentLastModified(layer.get())); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + } + + std::string delta; + { + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + ScopedFPDFWideString updated_timestamp = + GetFPDFWideString(L"D:20260713160000+03'00'"); + ScopedFPDFWideString updated_name = + GetFPDFWideString(L"Updated Company Stamps"); + ASSERT_TRUE(EPDFDoc_SetPieceInfoString(layer.get(), "EMBD_StampLibrary", + "Name", updated_name.get(), + updated_timestamp.get())); + EXPECT_GT(EPDFLayer_GetPromotedObjectCount(layer.get()), 0u); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + delta = GetString(); + ASSERT_FALSE(delta.empty()); + } + { + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + EXPECT_EQ(L"Updated Company Stamps", + GetDocumentPieceInfoString(replayed.get(), "EMBD_StampLibrary", + "Name")); + EXPECT_EQ(L"D:20260713160000+03'00'", + GetDocumentLastModified(replayed.get())); + } + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(EPDFPieceInfoEmbedderTest, CatalogGranularClearPreservesOtherData) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 240, 100)); + ASSERT_TRUE(page); + + ScopedFPDFWideString timestamp = + GetFPDFWideString(L"D:20260713154500+03'00'"); + ScopedFPDFWideString value = GetFPDFWideString(L"value"); + + EXPECT_TRUE(EPDFDoc_ClearPieceInfoKey(document(), "EMBD_Missing", "Missing", + nullptr)); + EXPECT_EQ(0, EPDFDoc_GetPieceInfoEntryCount(document())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoString(document(), "EMBD_First", "Remove", + value.get(), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoString(document(), "EMBD_First", "Keep", + value.get(), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPieceInfoString(document(), "EMBD_Second", "Keep", + value.get(), timestamp.get())); + + EXPECT_TRUE(EPDFDoc_ClearPieceInfoKey(document(), "EMBD_First", "Remove", + timestamp.get())); + EXPECT_EQ(FPDF_OBJECT_UNKNOWN, + EPDFDoc_GetPieceInfoValueType(document(), "EMBD_First", "Remove")); + EXPECT_EQ(L"value", + GetDocumentPieceInfoString(document(), "EMBD_First", "Keep")); + EXPECT_TRUE(EPDFDoc_ClearPieceInfoEntry(document(), "EMBD_First")); + EXPECT_FALSE(EPDFDoc_HasPieceInfoEntry(document(), "EMBD_First")); + EXPECT_TRUE(EPDFDoc_HasPieceInfoEntry(document(), "EMBD_Second")); + EXPECT_EQ(1, EPDFDoc_GetPieceInfoEntryCount(document())); + EXPECT_TRUE(EPDFDoc_ClearPieceInfoEntry(document(), "EMBD_Second")); + EXPECT_EQ(0, EPDFDoc_GetPieceInfoEntryCount(document())); + EXPECT_EQ(L"D:20260713154500+03'00'", GetDocumentLastModified(document())); +} + +TEST_F(EPDFPieceInfoEmbedderTest, TypedValuesSaveReloadAndImport) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 240, 100)); + ASSERT_TRUE(page); + const unsigned int page_object_number = EPDFPage_GetObjectNumber(page.get()); + ASSERT_NE(0u, page_object_number); + + ScopedFPDFWideString timestamp = + GetFPDFWideString(L"D:20260713093703+03'00'"); + ScopedFPDFWideString name = GetFPDFWideString(L"Approved \u2713"); + ScopedFPDFWideString review = GetFPDFWideString(L"Review"); + ScopedFPDFWideString internal = GetFPDFWideString(L"Internal"); + const FPDF_WIDESTRING categories[] = {review.get(), internal.get()}; + + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoString(document(), page_object_number, + "EMBD_Stamp", "Name", name.get(), + timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoNumber(document(), page_object_number, + "EMBD_Stamp", "Version", 1.0f, + timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoBoolean(document(), page_object_number, + "EMBD_Stamp", "Archived", true, + timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoName(document(), page_object_number, + "EMBD_Stamp", "Kind", "Stamp", + timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoStringArray( + document(), page_object_number, "EMBD_Stamp", "Categories", categories, + std::size(categories), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoBoolean(document(), page_object_number, + "EMBD_PageState", "Pinned", true, + timestamp.get())); + + EXPECT_TRUE(EPDFDoc_HasPagePieceInfoEntry(document(), page_object_number, + "EMBD_Stamp")); + EXPECT_EQ(2, + EPDFDoc_GetPagePieceInfoEntryCount(document(), page_object_number)); + std::set applications; + applications.insert(GetPieceInfoEntryAt(document(), page_object_number, 0)); + applications.insert(GetPieceInfoEntryAt(document(), page_object_number, 1)); + EXPECT_EQ((std::set{"EMBD_PageState", "EMBD_Stamp"}), + applications); + + EXPECT_EQ(FPDF_OBJECT_STRING, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_Stamp", "Name")); + EXPECT_EQ(FPDF_OBJECT_NUMBER, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_Stamp", "Version")); + EXPECT_EQ(FPDF_OBJECT_BOOLEAN, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_Stamp", "Archived")); + EXPECT_EQ(FPDF_OBJECT_NAME, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_Stamp", "Kind")); + EXPECT_EQ(FPDF_OBJECT_ARRAY, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_Stamp", "Categories")); + EXPECT_EQ( + L"Approved \u2713", + GetPieceInfoString(document(), page_object_number, "EMBD_Stamp", "Name")); + float number = 0.0f; + EXPECT_TRUE(EPDFDoc_GetPagePieceInfoNumber(document(), page_object_number, + "EMBD_Stamp", "Version", &number)); + EXPECT_FLOAT_EQ(1.0f, number); + FPDF_BOOL boolean = false; + EXPECT_TRUE(EPDFDoc_GetPagePieceInfoBoolean( + document(), page_object_number, "EMBD_Stamp", "Archived", &boolean)); + EXPECT_TRUE(boolean); + EXPECT_EQ("Stamp", GetPieceInfoName(document(), page_object_number, + "EMBD_Stamp", "Kind")); + EXPECT_EQ(2, EPDFDoc_GetPagePieceInfoStringArrayCount( + document(), page_object_number, "EMBD_Stamp", "Categories")); + EXPECT_EQ(L"Review", + GetPieceInfoStringArrayAt(document(), page_object_number, + "EMBD_Stamp", "Categories", 0)); + EXPECT_EQ(L"Internal", + GetPieceInfoStringArrayAt(document(), page_object_number, + "EMBD_Stamp", "Categories", 1)); + EXPECT_EQ(L"D:20260713093703+03'00'", + GetPageLastModified(document(), page_object_number)); + EXPECT_EQ( + L"D:20260713093703+03'00'", + GetPieceInfoLastModified(document(), page_object_number, "EMBD_Stamp")); + + ScopedFPDFDocument imported(FPDF_CreateNewDocument()); + ASSERT_TRUE(imported); + static constexpr int kPageIndices[] = {0}; + ASSERT_TRUE(FPDF_ImportPagesByIndex(imported.get(), document(), kPageIndices, + std::size(kPageIndices), 0)); + const unsigned int imported_page_object_number = + EPDFDoc_GetPageObjectNumberByIndex(imported.get(), 0); + ASSERT_NE(0u, imported_page_object_number); + EXPECT_EQ(L"Approved \u2713", + GetPieceInfoString(imported.get(), imported_page_object_number, + "EMBD_Stamp", "Name")); + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + const std::string saved_pdf = GetString(); + ASSERT_FALSE(saved_pdf.empty()); + + ScopedSavedDoc saved_document = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_document); + const unsigned int saved_page_object_number = + EPDFDoc_GetPageObjectNumberByIndex(saved_document.get(), 0); + ASSERT_NE(0u, saved_page_object_number); + EXPECT_EQ(L"Approved \u2713", + GetPieceInfoString(saved_document.get(), saved_page_object_number, + "EMBD_Stamp", "Name")); + EXPECT_EQ(L"D:20260713093703+03'00'", + GetPieceInfoLastModified(saved_document.get(), + saved_page_object_number, "EMBD_Stamp")); + + EPDF_BASE_DOCUMENT base = + EPDF_LoadMemBaseDocument64(saved_pdf.data(), saved_pdf.size(), nullptr); + ASSERT_TRUE(base); + { + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + const unsigned int layer_page_object_number = + EPDFDoc_GetPageObjectNumberByIndex(layer.get(), 0); + EXPECT_EQ(2, EPDFDoc_GetPagePieceInfoEntryCount(layer.get(), + layer_page_object_number)); + EXPECT_EQ(L"Approved \u2713", + GetPieceInfoString(layer.get(), layer_page_object_number, + "EMBD_Stamp", "Name")); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + } + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(EPDFPieceInfoEmbedderTest, GranularClearPreservesOtherData) { + CreateEmptyDocument(); + ScopedFPDFPage page(FPDFPage_New(document(), 0, 240, 100)); + ASSERT_TRUE(page); + const unsigned int page_object_number = EPDFPage_GetObjectNumber(page.get()); + ASSERT_NE(0u, page_object_number); + + ScopedFPDFWideString timestamp = + GetFPDFWideString(L"D:20260713103000+03'00'"); + ScopedFPDFWideString value = GetFPDFWideString(L"value"); + + EXPECT_TRUE(EPDFDoc_ClearPagePieceInfoKey( + document(), page_object_number, "EMBD_Missing", "Missing", nullptr)); + EXPECT_EQ(0, + EPDFDoc_GetPagePieceInfoEntryCount(document(), page_object_number)); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoString(document(), page_object_number, + "EMBD_First", "Remove", + value.get(), timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoString(document(), page_object_number, + "EMBD_First", "Keep", value.get(), + timestamp.get())); + EXPECT_TRUE(EPDFDoc_SetPagePieceInfoString(document(), page_object_number, + "EMBD_Second", "Keep", value.get(), + timestamp.get())); + + EXPECT_TRUE(EPDFDoc_ClearPagePieceInfoKey( + document(), page_object_number, "EMBD_First", "Remove", timestamp.get())); + EXPECT_EQ(FPDF_OBJECT_UNKNOWN, + EPDFDoc_GetPagePieceInfoValueType(document(), page_object_number, + "EMBD_First", "Remove")); + EXPECT_EQ(L"value", GetPieceInfoString(document(), page_object_number, + "EMBD_First", "Keep")); + EXPECT_TRUE(EPDFDoc_ClearPagePieceInfoEntry(document(), page_object_number, + "EMBD_First")); + EXPECT_FALSE(EPDFDoc_HasPagePieceInfoEntry(document(), page_object_number, + "EMBD_First")); + EXPECT_TRUE(EPDFDoc_HasPagePieceInfoEntry(document(), page_object_number, + "EMBD_Second")); + EXPECT_EQ(1, + EPDFDoc_GetPagePieceInfoEntryCount(document(), page_object_number)); + EXPECT_TRUE(EPDFDoc_ClearPagePieceInfoEntry(document(), page_object_number, + "EMBD_Second")); + EXPECT_EQ(0, + EPDFDoc_GetPagePieceInfoEntryCount(document(), page_object_number)); +} diff --git a/fpdfsdk/epdf_redact.cpp b/fpdfsdk/epdf_redact.cpp index d025043021..92b2fde9b4 100644 --- a/fpdfsdk/epdf_redact.cpp +++ b/fpdfsdk/epdf_redact.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -21,6 +20,7 @@ #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" #include "core/fpdfdoc/cpdf_annot.h" +#include "core/fpdfdoc/cpdf_generateap.h" #include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/containers/contains.h" @@ -71,20 +71,9 @@ std::vector GetRedactRectsFromAnnotDict( struct RemovedAnnotCandidate { size_t index = 0; uint32_t object_number = 0; - ByteString nm_utf8; RetainPtr dict; }; -struct RedactionReportBuffers { - EPDF_RemovedAnnotInfo* removed = nullptr; - uint32_t removed_capacity = 0; - char* nm_utf8_pool = nullptr; - uint32_t nm_utf8_pool_capacity = 0; - uint32_t* written_count = nullptr; - uint32_t* total_count = nullptr; - uint32_t* nm_utf8_bytes_used = nullptr; -}; - uint32_t GetAnnotObjectNumber(const CPDF_Object* entry, const CPDF_Dictionary* dict) { if (entry && entry->IsReference()) { @@ -93,13 +82,6 @@ uint32_t GetAnnotObjectNumber(const CPDF_Object* entry, return dict ? dict->GetObjNum() : 0; } -ByteString GetAnnotNMUtf8(const CPDF_Dictionary* dict) { - if (!dict || !dict->KeyExist("NM")) { - return ByteString(); - } - return dict->GetUnicodeTextFor("NM").ToUTF8(); -} - bool RectsIntersectWithPositiveArea(CFX_FloatRect a, CFX_FloatRect b) { a.Normalize(); b.Normalize(); @@ -152,7 +134,6 @@ bool AddRemovalCandidate(CPDF_Page* page, RemovedAnnotCandidate candidate; candidate.index = index; candidate.object_number = GetAnnotObjectNumber(entry.Get(), dict.Get()); - candidate.nm_utf8 = GetAnnotNMUtf8(dict.Get()); candidate.dict = std::move(dict); candidates->push_back(std::move(candidate)); return true; @@ -301,55 +282,20 @@ void DetachWidgetsFromAcroForm( } } -void WriteRemovalReport(const std::vector& candidates, - const RedactionReportBuffers* report) { - if (!report) { - return; - } - - const uint32_t total = - pdfium::checked_cast(candidates.size()); - uint32_t written = 0; - uint32_t nm_bytes_used = 0; - const uint32_t capacity = report->removed ? report->removed_capacity : 0; - const uint32_t limit = std::min(total, capacity); - - for (; written < limit; ++written) { - const RemovedAnnotCandidate& candidate = candidates[written]; - EPDF_RemovedAnnotInfo& out = report->removed[written]; - out.object_number = candidate.object_number; - out.index_at_removal = - pdfium::checked_cast(candidate.index); - out.nm_utf8_offset = 0; - out.nm_utf8_len = 0; - - const uint32_t nm_len = - pdfium::checked_cast(candidate.nm_utf8.GetLength()); - if (nm_len == 0) { - continue; - } - if (!report->nm_utf8_pool || - nm_len > report->nm_utf8_pool_capacity - nm_bytes_used) { - out.nm_utf8_len = EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW; - continue; +// The count deliberately excludes REDACT annotations: they are the removal +// instructions (the applied one, and every sibling consumed by a page-wide +// apply), not collateral. Callers use this to warn that a redaction also +// destroyed annotations the user never explicitly marked. +uint32_t CountRemovedNonRedactAnnots( + const std::vector& candidates) { + uint32_t count = 0; + for (const RemovedAnnotCandidate& candidate : candidates) { + if (candidate.dict && candidate.dict->GetNameFor( + pdfium::annotation::kSubtype) != "Redact") { + ++count; } - - out.nm_utf8_offset = nm_bytes_used; - out.nm_utf8_len = nm_len; - memcpy(report->nm_utf8_pool + nm_bytes_used, candidate.nm_utf8.c_str(), - nm_len); - nm_bytes_used += nm_len; - } - - if (report->written_count) { - *report->written_count = written; - } - if (report->total_count) { - *report->total_count = total; - } - if (report->nm_utf8_bytes_used) { - *report->nm_utf8_bytes_used = nm_bytes_used; } + return count; } void RemoveCandidatesFromPage( @@ -381,13 +327,33 @@ void SortCandidatesByOriginalIndex( const RemovedAnnotCandidate& b) { return a.index < b.index; }); } +// Resolve the overlay to flatten for one redact annotation: a pre-baked /RO +// always wins (ISO 32000-2); without one (e.g. the file was marked by another +// processor) the overlay is synthesized from the declarative entries. Returns +// null when there is nothing to paint. +RetainPtr ResolveRedactOverlay( + CPDF_Document* doc, + const CPDF_Dictionary* redact_dict) { + RetainPtr overlay = redact_dict->GetStreamFor("RO"); + if (overlay) { + return overlay; + } + return CPDF_GenerateAP::BuildRedactOverlayForm(doc, redact_dict); +} + bool ApplySingleRedactionCore(CPDF_Page* page, const CPDF_Dictionary* redact_dict, - const RedactionReportBuffers* report) { + uint32_t* out_removed_annot_count) { if (!page || !redact_dict) { return false; } + // The caller may hand us a never-rendered page. Redaction MUST see the full + // object model: an unparsed page would silently remove nothing (a security + // failure, not a cosmetic one) and would let content regeneration reason + // from an empty object list. + page->ParseContent(); + std::vector rects = GetRedactRectsFromAnnotDict(redact_dict); if (rects.empty()) { return false; @@ -426,26 +392,33 @@ bool ApplySingleRedactionCore(CPDF_Page* page, /*recurse_forms=*/true, /*draw_black_boxes=*/false); - RetainPtr ro_stream = redact_dict->GetStreamFor("RO"); - if (ro_stream) { + RetainPtr overlay = + ResolveRedactOverlay(page->GetDocument(), redact_dict); + if (overlay) { CFX_FloatRect annot_rect = redact_dict->GetRectFor(pdfium::annotation::kRect); annot_rect.Normalize(); - EpdfAppendFormXObjectToPage(page, ro_stream, annot_rect); + EpdfAppendFormXObjectToPage(page, overlay, annot_rect); } DetachWidgetsFromAcroForm(page, removals); - WriteRemovalReport(removals, report); + if (out_removed_annot_count) { + *out_removed_annot_count = CountRemovedNonRedactAnnots(removals); + } RemoveCandidatesFromPage(page, removals); return true; } bool ApplyAllRedactionsCore(CPDF_Page* page, - const RedactionReportBuffers* report) { + uint32_t* out_removed_annot_count) { if (!page) { return false; } + // See ApplySingleRedactionCore: redaction must never run on an unparsed + // object model. + page->ParseContent(); + RetainPtr annots = page->GetMutableAnnotsArray(); if (!annots || annots->IsEmpty()) { return false; @@ -453,7 +426,7 @@ bool ApplyAllRedactionsCore(CPDF_Page* page, std::vector all_rects; std::vector, CFX_FloatRect>> - ro_streams; + overlays; std::vector removals; for (size_t i = 0; i < annots->size(); ++i) { @@ -473,12 +446,13 @@ bool ApplyAllRedactionsCore(CPDF_Page* page, all_rects.push_back(rect); } - RetainPtr ro_stream = annot_dict->GetStreamFor("RO"); - if (ro_stream) { + RetainPtr overlay = + ResolveRedactOverlay(page->GetDocument(), annot_dict.Get()); + if (overlay) { CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); annot_rect.Normalize(); - ro_streams.push_back({ro_stream, annot_rect}); + overlays.push_back({std::move(overlay), annot_rect}); } } @@ -508,12 +482,14 @@ bool ApplyAllRedactionsCore(CPDF_Page* page, /*recurse_forms=*/true, /*draw_black_boxes=*/false); - for (const auto& [ro_stream, annot_rect] : ro_streams) { - EpdfAppendFormXObjectToPage(page, ro_stream, annot_rect); + for (const auto& [overlay, annot_rect] : overlays) { + EpdfAppendFormXObjectToPage(page, overlay, annot_rect); } DetachWidgetsFromAcroForm(page, removals); - WriteRemovalReport(removals, report); + if (out_removed_annot_count) { + *out_removed_annot_count = CountRemovedNonRedactAnnots(removals); + } RemoveCandidatesFromPage(page, removals); return true; } @@ -521,30 +497,11 @@ bool ApplyAllRedactionsCore(CPDF_Page* page, } // namespace FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { - return EPDFAnnot_ApplyRedactionWithReport( - page, annot, nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ApplyRedactionWithReport( - FPDF_PAGE page, - FPDF_ANNOTATION annot, - EPDF_RemovedAnnotInfo* out_removed, - uint32_t out_removed_capacity, - char* nm_utf8_pool, - uint32_t nm_utf8_pool_capacity, - uint32_t* out_written_count, - uint32_t* out_total_count, - uint32_t* out_nm_utf8_bytes_used) { - if (out_written_count) { - *out_written_count = 0; - } - if (out_total_count) { - *out_total_count = 0; - } - if (out_nm_utf8_bytes_used) { - *out_nm_utf8_bytes_used = 0; +EPDFAnnot_ApplyRedaction(FPDF_PAGE page, + FPDF_ANNOTATION annot, + uint32_t* out_removed_annot_count) { + if (out_removed_annot_count) { + *out_removed_annot_count = 0; } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); @@ -558,41 +515,13 @@ EPDFAnnot_ApplyRedactionWithReport( return false; } - RedactionReportBuffers report = { - out_removed, - out_removed_capacity, - nm_utf8_pool, - nm_utf8_pool_capacity, - out_written_count, - out_total_count, - out_nm_utf8_bytes_used, - }; - return ApplySingleRedactionCore(pPage, annot_dict, &report); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_ApplyRedactions(FPDF_PAGE page) { - return EPDFPage_ApplyRedactionsWithReport(page, nullptr, 0, nullptr, 0, - nullptr, nullptr, nullptr); + return ApplySingleRedactionCore(pPage, annot_dict, out_removed_annot_count); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactionsWithReport( - FPDF_PAGE page, - EPDF_RemovedAnnotInfo* out_removed, - uint32_t out_removed_capacity, - char* nm_utf8_pool, - uint32_t nm_utf8_pool_capacity, - uint32_t* out_written_count, - uint32_t* out_total_count, - uint32_t* out_nm_utf8_bytes_used) { - if (out_written_count) { - *out_written_count = 0; - } - if (out_total_count) { - *out_total_count = 0; - } - if (out_nm_utf8_bytes_used) { - *out_nm_utf8_bytes_used = 0; +EPDFPage_ApplyRedactions(FPDF_PAGE page, uint32_t* out_removed_annot_count) { + if (out_removed_annot_count) { + *out_removed_annot_count = 0; } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); @@ -600,14 +529,5 @@ EPDFPage_ApplyRedactionsWithReport( return false; } - RedactionReportBuffers report = { - out_removed, - out_removed_capacity, - nm_utf8_pool, - nm_utf8_pool_capacity, - out_written_count, - out_total_count, - out_nm_utf8_bytes_used, - }; - return ApplyAllRedactionsCore(pPage, &report); + return ApplyAllRedactionsCore(pPage, out_removed_annot_count); } diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 8659c21a9e..6976f78547 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -50,10 +50,10 @@ #include "core/fxcrt/ptr_util.h" #include "core/fxcrt/stl_util.h" #include "core/fxge/cfx_color.h" +#include "core/fxge/cfx_fontregistry.h" #include "fpdfsdk/cpdfsdk_formfillenvironment.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/cpdfsdk_interactiveform.h" -#include "fpdfsdk/epdf_page_content_helpers.h" namespace { @@ -802,42 +802,6 @@ CPDF_FormField* GetFormField(FPDF_FORMHANDLE hHandle, FPDF_ANNOTATION annot) { return pPDFForm->GetFieldByDict(pAnnotDict); } -RetainPtr GetMutableFieldDict(CPDF_FormField* pFormField) { - if (!pFormField) { - return nullptr; - } - - return pdfium::WrapRetain( - const_cast(pFormField->GetFieldDict().Get())); -} - -bool ArrayContainsDictWithObjNum(const CPDF_Array* pArray, uint32_t obj_num) { - if (!pArray || obj_num == 0) { - return false; - } - - for (size_t i = 0; i < pArray->size(); ++i) { - RetainPtr pDict = pArray->GetDictAt(i); - if (pDict && pDict->GetObjNum() == obj_num) { - return true; - } - } - return false; -} - -void RemoveDictWithObjNumFromArray(CPDF_Array* pArray, uint32_t obj_num) { - if (!pArray || obj_num == 0) { - return; - } - - for (size_t i = pArray->size(); i > 0; --i) { - RetainPtr pDict = pArray->GetDictAt(i - 1); - if (pDict && pDict->GetObjNum() == obj_num) { - pArray->RemoveAt(i - 1); - } - } -} - // If `allowed_types` is empty, then match all types. const CPDFSDK_Widget* GetWidgetOfTypes( FPDF_FORMHANDLE hHandle, @@ -1109,6 +1073,10 @@ FPDFAnnot_IsSupportedSubtype(FPDF_ANNOTATION_SUBTYPE subtype) { case FPDF_ANNOT_POLYLINE: case FPDF_ANNOT_LINE: case FPDF_ANNOT_CARET: + // EmbedPDF: widgets are born through the annotation API and adopted by a + // form field via EPDFForm_AttachWidget (public/epdf_form.h). An + // unattached widget is an ordinary, inert annotation. + case FPDF_ANNOT_WIDGET: return true; default: return false; @@ -2735,11 +2703,44 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAction(FPDF_ANNOTATION annot, CPDF_Document* pDoc = pAnnotContext->GetPage()->GetDocument(); - // Set /A as an indirect reference to the action. + // Set /A as an indirect reference to the action. A link dictionary must + // not carry both /A and /Dest (ISO 32000-1 Table 173), so drop any + // pre-existing direct destination while we are at it. annot_dict->SetNewFor("A", pDoc, act_dict->GetObjNum()); + annot_dict->RemoveFor("Dest"); + return true; +} + +namespace { + +// Shared body of the two link-entry removers: single-purpose, idempotent. +FPDF_BOOL RemoveLinkDictEntry(FPDF_ANNOTATION annot, const char* key) { + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINK) { + return false; + } + + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return false; + } + + annot_dict->RemoveFor(key); return true; } +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_RemoveAction(FPDF_ANNOTATION annot) { + return RemoveLinkDictEntry(annot, "A"); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_RemoveDest(FPDF_ANNOTATION annot) { + return RemoveLinkDictEntry(annot, "Dest"); +} + FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot) { if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FILEATTACHMENT) { @@ -3644,6 +3645,46 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, doc, annot_dict.Get(), internal_font, font_size, CFX_Color(R, G, B)); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B) { + // EmbedPDF: annotation-specific bridge from public API to the registered font + // AP pipeline. Generic EPDFFont_* registration lives in epdf_font.cpp because + // the same registry is also used for page-rendering fallback. + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return false; + } + + RetainPtr annot_dict = context->GetMutableAnnotDict(); + if (!annot_dict) { + return false; + } + + FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && + subtype != FPDF_ANNOT_REDACT) { + return false; + } + + CPDF_Document* doc = context->GetPage()->GetDocument(); + if (!doc) { + return false; + } + + if (!CFX_FontRegistry::IsValidFont(font_id) || font_size < 0 || R > 255 || + G > 255 || B > 255) { + return false; + } + + return CPDF_GenerateAP::UpdateDefaultAppearanceRegisteredFont( + doc, annot_dict.Get(), font_id, font_size, CFX_Color(R, G, B)); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, FPDF_STANDARD_FONT* font, @@ -4376,72 +4417,6 @@ EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot) { return dict->GetBooleanFor("Repeat", false); } -namespace { - -// Find the index of an annotation in the page's annotation array. -// Returns -1 if not found. -int GetAnnotIndexOnPage(const CPDF_Page* page, - const CPDF_Dictionary* annot_dict) { - if (!page || !annot_dict) { - return -1; - } - - RetainPtr annots = page->GetAnnotsArray(); - if (!annots) { - return -1; - } - - for (size_t i = 0; i < annots->size(); ++i) { - if (annots->GetDictAt(i) == annot_dict) { - return static_cast(i); - } - } - return -1; -} - -} // namespace - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, - FPDF_ANNOTATION annot) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { - return false; - } - - const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return false; - } - - // Get the annotation's Normal appearance stream (AP/N) - RetainPtr ap_dict = - annot_dict->GetDictFor(pdfium::annotation::kAP); - if (!ap_dict) { - return false; - } - - RetainPtr ap_stream = ap_dict->GetStreamFor("N"); - if (!ap_stream) { - return false; - } - - CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - - EpdfAppendFormXObjectToPage(pPage, ap_stream, annot_rect); - - // Remove the annotation from the page - int annot_index = GetAnnotIndexOnPage(pPage, annot_dict); - if (annot_index >= 0) { - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (annots) { - annots->RemoveAt(annot_index); - } - } - - return true; -} - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAppearanceFromPage(FPDF_ANNOTATION annot, FPDF_DOCUMENT src_doc_handle, @@ -5043,117 +5018,6 @@ EPDFAnnot_ClearMKColor(FPDF_ANNOTATION annot, EPDF_MK_COLORTYPE type) { return true; } -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV -EPDFPage_CreateFormField(FPDF_PAGE page, - FPDF_FORMHANDLE handle, - int field_type, - FPDF_WIDESTRING field_name) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { - return nullptr; - } - - CPDFSDK_InteractiveForm* pSDKForm = FormHandleToInteractiveForm(handle); - if (!pSDKForm) { - return nullptr; - } - - // Validate field_type - switch (field_type) { - case FPDF_FORMFIELD_TEXTFIELD: - case FPDF_FORMFIELD_CHECKBOX: - case FPDF_FORMFIELD_RADIOBUTTON: - case FPDF_FORMFIELD_COMBOBOX: - case FPDF_FORMFIELD_LISTBOX: - case FPDF_FORMFIELD_PUSHBUTTON: - break; - default: - return nullptr; - } - - CPDF_Document* pDoc = pPage->GetDocument(); - - // Determine /FT and base /Ff from the field_type - ByteString ft_value; - uint32_t base_flags = 0; - switch (field_type) { - case FPDF_FORMFIELD_TEXTFIELD: - ft_value = "Tx"; - break; - case FPDF_FORMFIELD_CHECKBOX: - ft_value = "Btn"; - break; - case FPDF_FORMFIELD_RADIOBUTTON: - ft_value = "Btn"; - base_flags = (1 << 15); // kRadio - break; - case FPDF_FORMFIELD_PUSHBUTTON: - ft_value = "Btn"; - base_flags = (1 << 16); // kPushbutton - break; - case FPDF_FORMFIELD_COMBOBOX: - ft_value = "Ch"; - base_flags = (1 << 17); // kCombo - break; - case FPDF_FORMFIELD_LISTBOX: - ft_value = "Ch"; - break; - } - - // Create the parent field dictionary (indirect) - RetainPtr pFieldDict = pDoc->NewIndirect(); - pFieldDict->SetNewFor("FT", ft_value); - if (base_flags != 0) { - pFieldDict->SetNewFor("Ff", static_cast(base_flags)); - } - - // Set field name /T - if (field_name) { - WideString ws_name = WideStringFromFPDFWideString(field_name); - if (!ws_name.IsEmpty()) { - pFieldDict->SetNewFor("T", ws_name.ToUTF8()); - } - } - - // Create the widget annotation dictionary (indirect) - RetainPtr pAnnotDict = pDoc->NewIndirect(); - pAnnotDict->SetNewFor(pdfium::annotation::kType, "Annot"); - pAnnotDict->SetNewFor(pdfium::annotation::kSubtype, "Widget"); - - // Link widget -> parent via /Parent - pAnnotDict->SetNewFor("Parent", pDoc, - pFieldDict->GetObjNum()); - - // Link parent -> widget via /Kids - RetainPtr pKids = pFieldDict->SetNewFor("Kids"); - pKids->AppendNew(pDoc, pAnnotDict->GetObjNum()); - - // Ensure /AcroForm exists on document root - RetainPtr pRoot = pDoc->GetMutableRoot(); - if (!pRoot) { - return nullptr; - } - - RetainPtr pAcroForm = pRoot->GetOrCreateDictFor("AcroForm"); - - // Append field to /AcroForm/Fields - RetainPtr pFields = pAcroForm->GetOrCreateArrayFor("Fields"); - pFields->AppendNew(pDoc, pFieldDict->GetObjNum()); - - // Append widget annotation to page /Annots - RetainPtr pAnnots = pPage->GetOrCreateAnnotsArray(); - pAnnots->AppendNew(pDoc, pAnnotDict->GetObjNum()); - - // Register the new field with the interactive form - CPDF_InteractiveForm* pPDFForm = pSDKForm->GetInteractiveForm(); - pPDFForm->FixPageFields(pPage); - - // Build and return the annotation handle - auto pContext = std::make_unique( - pAnnotDict, IPDFPageFromFPDFPage(page)); - return FPDFAnnotationFromCPDFAnnotContext(pContext.release()); -} - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GenerateFormFieldAP(FPDF_ANNOTATION annot) { CPDF_AnnotContext* pContext = CPDFAnnotContextFromFPDFAnnotation(annot); @@ -5227,204 +5091,6 @@ EPDFAnnot_GenerateFormFieldAP(FPDF_ANNOTATION annot) { return false; } -FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFAnnot_GetButtonExportValue(FPDF_ANNOTATION annot, - FPDF_WCHAR* buffer, - unsigned long buflen) { - const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) { - return 0; - } - - RetainPtr pAP = - pAnnotDict->GetDictFor(pdfium::annotation::kAP); - if (!pAP) { - return 0; - } - - RetainPtr pN = pAP->GetDictFor("N"); - if (!pN) { - return 0; - } - - ByteString on_state; - CPDF_DictionaryLocker locker(pN); - for (const auto& it : locker) { - if (it.first != "Off") { - on_state = it.first; - break; - } - } - - if (on_state.IsEmpty()) { - return 0; - } - - return Utf16EncodeMaybeCopyAndReturnLength( - WideString::FromUTF8(on_state.AsStringView()), - UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); -} - -FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFAnnot_GetFormFieldRawValue(FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - FPDF_WCHAR* buffer, - unsigned long buflen) { - const CPDF_FormField* pFormField = GetFormField(hHandle, annot); - if (!pFormField) { - return 0; - } - // SAFETY: required from caller. - return Utf16EncodeMaybeCopyAndReturnLength( - pFormField->GetRawValue(), - UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldValue(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - FPDF_WIDESTRING value) { - CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) { - return false; - } - - return pFormField->SetValue(WideStringFromFPDFWideString(value), - NotificationOption::kDoNotNotify); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldName(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - FPDF_WIDESTRING name) { - CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) { - return false; - } - - RetainPtr pFieldDict = pdfium::WrapRetain( - const_cast(pFormField->GetFieldDict().Get())); - if (!pFieldDict) { - return false; - } - - WideString ws_name = WideStringFromFPDFWideString(name); - pFieldDict->SetNewFor("T", ws_name.ToUTF8()); - return true; -} - -FPDF_EXPORT int FPDF_CALLCONV -EPDFAnnot_GetFormFieldObjectNumber(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot) { - RetainPtr pFieldDict = - GetMutableFieldDict(GetFormField(handle, annot)); - if (!pFieldDict) { - return 0; - } - - return static_cast(pFieldDict->GetObjNum()); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION source_annot, - FPDF_ANNOTATION target_annot) { - CPDFSDK_InteractiveForm* pSDKForm = FormHandleToInteractiveForm(handle); - if (!pSDKForm) { - return false; - } - - CPDF_FormField* pSourceField = GetFormField(handle, source_annot); - CPDF_FormField* pTargetField = GetFormField(handle, target_annot); - if (!pSourceField || !pTargetField) { - return false; - } - - RetainPtr pSourceFieldDict = - GetMutableFieldDict(pSourceField); - RetainPtr pTargetFieldDict = - GetMutableFieldDict(pTargetField); - if (!pSourceFieldDict || !pTargetFieldDict) { - return false; - } - - if (pSourceFieldDict->GetObjNum() == pTargetFieldDict->GetObjNum()) { - return true; - } - - if (pSourceField->GetType() != pTargetField->GetType()) { - return false; - } - - CPDF_AnnotContext* pSourceContext = - CPDFAnnotContextFromFPDFAnnotation(source_annot); - CPDF_AnnotContext* pTargetContext = - CPDFAnnotContextFromFPDFAnnotation(target_annot); - if (!pSourceContext || !pTargetContext) { - return false; - } - - IPDF_Page* pSourcePage = pSourceContext->GetPage(); - IPDF_Page* pTargetPage = pTargetContext->GetPage(); - if (!pSourcePage || !pTargetPage) { - return false; - } - - CPDF_Document* pDoc = pSourcePage->GetDocument(); - if (!pDoc || pDoc != pTargetPage->GetDocument()) { - return false; - } - - RetainPtr pSourceKids = - pSourceFieldDict->GetMutableArrayFor("Kids"); - RetainPtr pTargetKids = - pTargetFieldDict->GetOrCreateArrayFor("Kids"); - if (!pSourceKids || !pTargetKids) { - return false; - } - - for (size_t i = 0; i < pSourceKids->size(); ++i) { - RetainPtr pKidDict = pSourceKids->GetMutableDictAt(i); - if (!pKidDict) { - continue; - } - - pKidDict->SetNewFor("Parent", pDoc, - pTargetFieldDict->GetObjNum()); - - if (!ArrayContainsDictWithObjNum(pTargetKids.Get(), - pKidDict->GetObjNum())) { - pTargetKids->AppendNew(pDoc, pKidDict->GetObjNum()); - } - } - - pSourceFieldDict->RemoveFor("Kids"); - - RetainPtr pRoot = pDoc->GetMutableRoot(); - if (pRoot) { - RetainPtr pAcroForm = pRoot->GetMutableDictFor("AcroForm"); - if (pAcroForm) { - RetainPtr pFields = pAcroForm->GetMutableArrayFor("Fields"); - if (pFields) { - RemoveDictWithObjNumFromArray(pFields.Get(), - pSourceFieldDict->GetObjNum()); - } - } - } - - CPDF_InteractiveForm* pPDFForm = pSDKForm->GetInteractiveForm(); - if (CPDF_Page* pSourcePdfPage = ToPDFPage(pSourcePage)) { - pPDFForm->FixPageFields(pSourcePdfPage); - } - if (pTargetPage != pSourcePage) { - if (CPDF_Page* pTargetPdfPage = ToPDFPage(pTargetPage)) { - pPDFForm->FixPageFields(pTargetPdfPage); - } - } - - return true; -} - FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetCalloutLineCount(FPDF_ANNOTATION annot) { if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { @@ -5530,34 +5196,6 @@ EPDFAnnot_SetCalloutLine(FPDF_ANNOTATION annot, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldOptions(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - const FPDF_WIDESTRING* labels, - int count) { - if (count < 0 || (count > 0 && !labels)) { - return false; - } - - CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) { - return false; - } - - RetainPtr pFieldDict = pdfium::WrapRetain( - const_cast(pFormField->GetFieldDict().Get())); - if (!pFieldDict) { - return false; - } - - RetainPtr pOpt = pFieldDict->SetNewFor("Opt"); - for (int i = 0; i < count; i++) { - WideString ws_label = WideStringFromFPDFWideString(labels[i]); - pOpt->AppendNew(ws_label.AsStringView()); - } - return true; -} - FPDF_EXPORT unsigned int FPDF_CALLCONV EPDFAnnot_GetObjectNumber(FPDF_ANNOTATION annot) { CPDF_AnnotContext* pCtx = CPDFAnnotContextFromFPDFAnnotation(annot); diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 47449aed4e..d67f89a18d 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -3,26 +3,37 @@ // found in the LICENSE file. #include "public/fpdf_annot.h" +#include "public/epdf_form.h" #include #include #include #include +#include +#include #include #include #include #include "build/build_config.h" #include "constants/annotation_common.h" +#include "core/fpdfapi/font/cpdf_tounicodemap.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/page/cpdf_page.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_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" #include "core/fxcrt/fx_memcpy_wrappers.h" +#include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_system.h" #include "core/fxcrt/span.h" #include "core/fxge/cfx_defaultrenderdevice.h" @@ -31,6 +42,7 @@ #include "public/fpdf_attachment.h" #include "public/fpdf_edit.h" #include "public/fpdf_formfill.h" +#include "public/fpdf_save.h" #include "public/fpdf_text.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" @@ -38,9 +50,12 @@ #include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" #include "testing/utils/hash.h" +#include "testing/utils/path_service.h" using pdfium::kAnnotationStampWithApPng; +using testing::HasSubstr; namespace { @@ -64,44 +79,360 @@ std::wstring ExtractPageText(FPDF_PAGE page) { return GetPlatformWString(buffer.data()); } -struct RedactionReport { - std::vector object_numbers; - uint32_t written_count = 0; - uint32_t total_count = 0; - uint32_t nm_utf8_bytes_used = 0; +std::vector LoadNotoSansSCFontData() { + std::string font_path = PathService::GetThirdPartyFilePath( + "NotoSansCJK/NotoSansSC-Regular.subset.otf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find NotoSansSC subset font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadRobotoFontData() { + std::string font_path = PathService::GetThirdPartyFilePath( + "harfbuzz-ng/src/perf/fonts/Roboto-Regular.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find Roboto test font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +std::vector LoadDroidSansFallbackFullFontData() { + std::string font_path = + PathService::GetTestFilePath("fonts/DroidSansFallbackFull.ttf"); + if (font_path.empty()) { + ADD_FAILURE() << "Failed to find DroidSansFallbackFull test font"; + return {}; + } + return GetFileContents(font_path.c_str()); +} + +EPDF_FONT_ID RegisterDroidSansFallbackFullFont() { + std::vector font_data = LoadDroidSansFallbackFullFontData(); + if (font_data.empty()) { + return 0; + } + + EPDF_FONT_ID font_id = EPDFFont_RegisterMemFont64( + "DroidSansFallbackFull", /*weight=*/400, /*italic=*/0, font_data.data(), + font_data.size()); + if (font_id == 0 || !EPDFFont_AddFallbackFont(font_id)) { + ADD_FAILURE() << "Failed to register DroidSansFallbackFull as fallback"; + return 0; + } + return font_id; +} + +std::wstring GetNormalAppearance(FPDF_ANNOTATION annot) { + unsigned long length_bytes = + FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, nullptr, 0); + if (length_bytes == 0) { + ADD_FAILURE() << "Missing normal appearance stream"; + return L""; + } + + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + FPDFAnnot_GetAP(annot, FPDF_ANNOT_APPEARANCEMODE_NORMAL, + buffer.data(), length_bytes)); + return GetPlatformWString(buffer.data()); +} + +class ScopedRegisteredFonts { + public: + ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } + ~ScopedRegisteredFonts() { EPDFFont_ClearRegisteredFonts(); } }; -RedactionReport ApplyRedactionWithReport(FPDF_PAGE page, - FPDF_ANNOTATION annot) { - std::array removed = {}; - std::array nm_utf8_pool = {}; - RedactionReport report; +class MemoryFileAccess final : public FPDF_FILEACCESS { + public: + explicit MemoryFileAccess(std::vector data) : data_(std::move(data)) { + m_FileLen = static_cast(data_.size()); + m_GetBlock = &MemoryFileAccess::GetBlock; + m_Param = this; + } + + private: + static int GetBlock(void* param, + unsigned long pos, + unsigned char* buf, + unsigned long size) { + auto* file_access = static_cast(param); + if (!file_access || !buf || pos > file_access->data_.size() || + size > file_access->data_.size() - pos) { + return 0; + } - EXPECT_TRUE(EPDFAnnot_ApplyRedactionWithReport( - page, annot, removed.data(), removed.size(), nm_utf8_pool.data(), - nm_utf8_pool.size(), &report.written_count, &report.total_count, - &report.nm_utf8_bytes_used)); + std::copy_n(file_access->data_.data() + pos, size, buf); + return 1; + } - for (uint32_t i = 0; i < report.written_count; ++i) { - report.object_numbers.push_back(removed[i].object_number); + std::vector data_; +}; + +ByteString RegisteredFontAlias(EPDF_FONT_ID font_id) { + return ByteString::Format("ERegF%u", font_id); +} + +ByteString GetDefaultAppearanceFontAlias(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return ByteString(); + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return ByteString(); } - return report; + + ByteString da = annot_dict->GetByteStringFor("DA"); + std::optional slash_pos = da.Find('/'); + if (!slash_pos.has_value()) { + return ByteString(); + } + + ByteStringView remainder = da.AsStringView().Substr(slash_pos.value() + 1); + std::optional end_pos = remainder.Find(' '); + if (!end_pos.has_value()) { + return ByteString(remainder); + } + return ByteString(remainder.First(end_pos.value())); } -RedactionReport ApplyPageRedactionsWithReport(FPDF_PAGE page) { - std::array removed = {}; - std::array nm_utf8_pool = {}; - RedactionReport report; +ByteString GetNormalAppearanceStreamBytes(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return ByteString(); + } - EXPECT_TRUE(EPDFPage_ApplyRedactionsWithReport( - page, removed.data(), removed.size(), nm_utf8_pool.data(), - nm_utf8_pool.size(), &report.written_count, &report.total_count, - &report.nm_utf8_bytes_used)); + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return ByteString(); + } - for (uint32_t i = 0; i < report.written_count; ++i) { - report.object_numbers.push_back(removed[i].object_number); + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr normal_stream = + ap_dict ? ap_dict->GetStreamFor("N") : nullptr; + if (!normal_stream) { + return ByteString(); } - return report; + + RetainPtr stream_acc = + pdfium::MakeRetain(std::move(normal_stream)); + stream_acc->LoadAllDataFiltered(); + return ByteString(ByteStringView(stream_acc->GetSpan())); +} + +RetainPtr GetAppearanceFontDict( + FPDF_ANNOTATION annot, + const ByteString& font_alias) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return nullptr; + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return nullptr; + } + + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr stream_dict = + ap_dict ? ap_dict->GetDictFor("N") : nullptr; + RetainPtr resources_dict = + stream_dict ? stream_dict->GetDictFor("Resources") : nullptr; + RetainPtr font_dict = + resources_dict ? resources_dict->GetDictFor("Font") : nullptr; + return font_dict ? font_dict->GetDictFor(font_alias.AsStringView()) : nullptr; +} + +bool AppearanceFontHasEmbeddedSubset(const CPDF_Dictionary* font_dict) { + if (!font_dict || font_dict->GetNameFor("Subtype") != "Type0" || + !font_dict->GetStreamFor("ToUnicode")) { + return false; + } + + RetainPtr descendant_fonts = + font_dict->GetArrayFor("DescendantFonts"); + if (!descendant_fonts || descendant_fonts->size() == 0) { + return false; + } + + RetainPtr cid_font_dict = + descendant_fonts->GetDictAt(0); + RetainPtr font_descriptor = + cid_font_dict ? cid_font_dict->GetDictFor("FontDescriptor") : nullptr; + if (!font_descriptor || !font_descriptor->GetStreamFor("FontFile2")) { + return false; + } + + ByteString base_font = font_dict->GetNameFor("BaseFont"); + return base_font.GetLength() > 7 && base_font[6] == '+'; +} + +bool AppearanceFontMapsUnicode(const CPDF_Dictionary* font_dict, + wchar_t value) { + RetainPtr to_unicode = + font_dict ? font_dict->GetStreamFor("ToUnicode") : nullptr; + if (!to_unicode) { + return false; + } + + CPDF_ToUnicodeMap to_unicode_map(std::move(to_unicode)); + return to_unicode_map.ReverseLookup(value) != 0; +} + +void ExpectRegisteredAppearanceMapsUnicode( + FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + std::initializer_list unicodes) { + RetainPtr font_dict = + GetAppearanceFontDict(annot, RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + for (wchar_t unicode : unicodes) { + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), unicode)); + } +} + +std::string BitmapChecksum(FPDF_BITMAP bitmap) { + if (!bitmap) { + return std::string(); + } + + const int stride = FPDFBitmap_GetStride(bitmap); + const int height = FPDFBitmap_GetHeight(bitmap); + FX_SAFE_SIZE_T size = stride; + size *= height; + if (!size.IsValid()) { + return std::string(); + } + + return GenerateMD5Base16( + pdfium::span(static_cast(FPDFBitmap_GetBuffer(bitmap)), + size.ValueOrDie())); +} + +bool BitmapHasNonWhitePixels(FPDF_BITMAP bitmap) { + if (!bitmap) { + return false; + } + + const int stride = FPDFBitmap_GetStride(bitmap); + const int height = FPDFBitmap_GetHeight(bitmap); + FX_SAFE_SIZE_T size = stride; + size *= height; + if (!size.IsValid()) { + return false; + } + + pdfium::span bytes( + static_cast(FPDFBitmap_GetBuffer(bitmap)), + size.ValueOrDie()); + return std::ranges::any_of(bytes, + [](uint8_t value) { return value != 0xff; }); +} + +void AddBrokenTrueTypeCjkTextPageContent(FPDF_DOCUMENT doc, FPDF_PAGE page) { + CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(doc); + CPDF_Page* cpdf_page = CPDFPageFromFPDFPage(page); + ASSERT_TRUE(cpdf_doc); + ASSERT_TRUE(cpdf_page); + + auto font_dict = cpdf_doc->NewIndirect(); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "TrueType"); + font_dict->SetNewFor("BaseFont", "Helvetica"); + + auto encoding_dict = pdfium::MakeRetain(); + encoding_dict->SetNewFor("Type", "Encoding"); + auto differences = pdfium::MakeRetain(); + differences->AppendNew(65); + differences->AppendNew("uni8FD9"); + encoding_dict->SetFor("Differences", std::move(differences)); + font_dict->SetFor("Encoding", std::move(encoding_dict)); + + RetainPtr page_dict = cpdf_page->GetMutableDict(); + RetainPtr resources = + page_dict->GetOrCreateDictFor("Resources"); + RetainPtr font_resources = + resources->GetOrCreateDictFor("Font"); + font_resources->SetNewFor("F1", cpdf_doc, + font_dict->GetObjNum()); + + const ByteString kContent = + "BT\n" + "/F1 72 Tf\n" + "40 100 Td\n" + "<41> Tj\n" + "ET\n"; + RetainPtr contents = + cpdf_doc->NewIndirect(kContent.unsigned_span()); + page_dict->SetNewFor("Contents", cpdf_doc, + contents->GetObjNum()); +} + +// Applies one redaction and returns how many annotations other than REDACT +// ones were removed alongside it. +uint32_t ApplyRedactionCountingRemoved(FPDF_PAGE page, FPDF_ANNOTATION annot) { + uint32_t removed_count = 0; + EXPECT_TRUE(EPDFAnnot_ApplyRedaction(page, annot, &removed_count)); + return removed_count; +} + +uint32_t ApplyPageRedactionsCountingRemoved(FPDF_PAGE page) { + uint32_t removed_count = 0; + EXPECT_TRUE(EPDFPage_ApplyRedactions(page, &removed_count)); + return removed_count; +} + +ScopedFPDFAnnotation CreateRedactAnnot(FPDF_PAGE page, const FS_RECTF& rect) { + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page, FPDF_ANNOT_REDACT)); + EXPECT_TRUE(annot); + if (annot) { + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + } + return annot; +} + +ScopedFPDFBitmap RenderPageOnWhite(FPDF_PAGE page, int width, int height) { + ScopedFPDFBitmap bitmap(FPDFBitmap_Create(width, height, /*alpha=*/0)); + FPDFBitmap_FillRect(bitmap.get(), 0, 0, width, height, 0xFFFFFFFF); + FPDF_RenderPageBitmap(bitmap.get(), page, 0, 0, width, height, /*rotate=*/0, + /*flags=*/0); + return bitmap; +} + +uint32_t GetPixelColor(FPDF_BITMAP bitmap, int x, int y) { + const uint8_t* buffer = + static_cast(FPDFBitmap_GetBuffer(bitmap)); + const int stride = FPDFBitmap_GetStride(bitmap); + const uint8_t* pixel = buffer + y * stride + x * 4; + return (uint32_t{pixel[3]} << 24) | (uint32_t{pixel[2]} << 16) | + (uint32_t{pixel[1]} << 8) | uint32_t{pixel[0]}; +} + +// Number of pixels in [left, right) x [top, bottom), bitmap coordinates, that +// differ from `background`. +int CountInkPixels(FPDF_BITMAP bitmap, + int left, + int top, + int right, + int bottom, + uint32_t background) { + int count = 0; + for (int y = top; y < bottom; ++y) { + for (int x = left; x < right; ++x) { + if (GetPixelColor(bitmap, x, y) != background) { + ++count; + } + } + } + return count; } void VerifyFocusableAnnotSubtypes( @@ -403,6 +734,63 @@ TEST_F(FPDFAnnotEmbedderTest, RemoveInkList) { EXPECT_FALSE(annot_dict->KeyExist("InkList")); } +TEST_F(FPDFAnnotEmbedderTest, GenerateInkAppearanceIsIdempotentOnRect) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK)); + ASSERT_TRUE(annot); + + static constexpr FS_POINTF kStroke[] = { + {50.0f, 50.0f}, {80.0f, 90.0f}, {120.0f, 60.0f}}; + ASSERT_EQ(0, + FPDFAnnot_AddInkStroke(annot.get(), kStroke, std::size(kStroke))); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 255, + 0, 0, 255)); + ASSERT_TRUE(FPDFAnnot_SetBorder(annot.get(), /*horizontal_radius=*/0.0f, + /*vertical_radius=*/0.0f, + /*border_width=*/6.0f)); + + // A caller-authored /Rect that already encloses the STROKED ink: the point + // bounds (50..120, 50..90) inflated by border_width / 2 = 3 on every side — + // exactly the rect EmbedPDF's writers supply. + const FS_RECTF authored{/*left=*/47.0f, /*top=*/93.0f, /*right=*/123.0f, + /*bottom=*/47.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &authored)); + + // Generating the appearance must NOT disturb a rect the ink already fits in + // — no matter how many times it runs (the engine re-bakes after every + // edit). The old behavior inflated /Rect by border_width / 2 per call. + for (int i = 0; i < 3; ++i) { + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + FS_RECTF rect; + ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &rect)); + EXPECT_FLOAT_EQ(authored.left, rect.left) << "iteration " << i; + EXPECT_FLOAT_EQ(authored.top, rect.top) << "iteration " << i; + EXPECT_FLOAT_EQ(authored.right, rect.right) << "iteration " << i; + EXPECT_FLOAT_EQ(authored.bottom, rect.bottom) << "iteration " << i; + } + + // A TIGHT rect (bare point bounds, no stroke padding — common in foreign + // documents, the case the upstream inflate was hacked in for) is corrected + // ONCE to the minimal rect that contains the stroked ink, then stays + // stable on further regenerations. + const FS_RECTF tight{/*left=*/50.0f, /*top=*/90.0f, /*right=*/120.0f, + /*bottom=*/50.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &tight)); + for (int i = 0; i < 3; ++i) { + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + FS_RECTF rect; + ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &rect)); + EXPECT_FLOAT_EQ(47.0f, rect.left) << "iteration " << i; + EXPECT_FLOAT_EQ(93.0f, rect.top) << "iteration " << i; + EXPECT_FLOAT_EQ(123.0f, rect.right) << "iteration " << i; + EXPECT_FLOAT_EQ(47.0f, rect.bottom) << "iteration " << i; + } +} + TEST_F(FPDFAnnotEmbedderTest, BadParams) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); ScopedPage page = LoadScopedPage(0); @@ -506,6 +894,548 @@ TEST_F(FPDFAnnotEmbedderTest, ExplicitGenerateAppearanceAllowed) { EXPECT_GT(doc->GetLastObjNum(), before); } +TEST_F(FPDFAnnotEmbedderTest, TextFieldGenerateAppearanceStreamIsStable) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + static const FS_POINTF kTextFieldPoint = {120.0f, 120.0f}; + ScopedFPDFAnnotation annot(FPDFAnnot_GetFormFieldAtPoint( + form_handle(), page.get(), &kTextFieldPoint)); + ASSERT_TRUE(annot); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ByteString appearance = GetNormalAppearanceStreamBytes(annot.get()); + ASSERT_FALSE(appearance.IsEmpty()); + EXPECT_EQ("68a94799890022965d780f65db1e7430", + GenerateMD5Base16(appearance.unsigned_span())); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextAppearanceUsesRegisteredMemoryFont) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"这是第一句。"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + FPDF_STANDARD_FONT font = FPDF_FONT_COURIER; + float font_size = 0.0f; + unsigned int r = 0; + unsigned int g = 0; + unsigned int b = 0; + ASSERT_TRUE(EPDFAnnot_GetDefaultAppearance(annot.get(), &font, &font_size, &r, + &g, &b)); + EXPECT_EQ(FPDF_FONT_UNKNOWN, font); + EXPECT_FLOAT_EQ(18.0f, font_size); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_THAT(GetNormalAppearance(annot.get()), HasSubstr(L"/ERegF")); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextAppearanceFallsBackToRegisteredFont) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"Hello 这是"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + std::wstring appearance = GetNormalAppearance(annot.get()); + EXPECT_THAT(appearance, HasSubstr(L"/Helv")); + EXPECT_THAT(appearance, HasSubstr(L"/ERegF")); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"Hello \xD55C\xAE00"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontEmbedsSubsetInSavedPdf) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + std::string saved_pdf(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + + EXPECT_LT(saved_pdf.size(), font_data.size() / 2); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegistersFontFromFileAccess) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + const size_t original_font_size = font_data.size(); + MemoryFileAccess font_access(std::move(font_data)); + + EPDF_FONT_ID font_id = EPDFFont_RegisterFont("Roboto", /*weight=*/400, + /*italic=*/0, &font_access); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + ASSERT_TRUE(saved_buffer); + std::string saved_pdf(static_cast(saved_buffer), saved_size); + EPDF_FreeBuffer(saved_buffer); + + EXPECT_LT(saved_pdf.size(), original_font_size / 2); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontMarkerSurvivesAliasSuffix) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + + CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(doc.get()); + ASSERT_TRUE(cpdf_doc); + RetainPtr root_dict = cpdf_doc->GetMutableRoot(); + ASSERT_TRUE(root_dict); + RetainPtr font_resources = + root_dict->GetOrCreateDictFor("AcroForm") + ->GetOrCreateDictFor("DR") + ->GetOrCreateDictFor("Font"); + + auto colliding_font_dict = cpdf_doc->NewIndirect(); + colliding_font_dict->SetNewFor("Type", "Font"); + colliding_font_dict->SetNewFor("Subtype", "Type1"); + colliding_font_dict->SetNewFor("BaseFont", "Helvetica"); + const ByteString base_alias = RegisteredFontAlias(font_id); + font_resources->SetNewFor(base_alias, cpdf_doc, + colliding_font_dict->GetObjNum()); + + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(L"ABC"); + ASSERT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont(annot.get(), font_id, + 18.0f, 0, 0, 0)); + + ByteString actual_alias = GetDefaultAppearanceFontAlias(annot.get()); + ASSERT_FALSE(actual_alias.IsEmpty()); + EXPECT_NE(base_alias, actual_alias); + EXPECT_EQ(base_alias, actual_alias.First(base_alias.GetLength())); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), actual_alias); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'A')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'B')); + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), 'C')); +} + +TEST_F(FPDFAnnotEmbedderTest, TextFieldKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + // Widgets are born through the annotation API and adopted by a field + // (EPDFForm_AttachWidget); values flow through the typed EPDFForm_* + // transactions, which regenerate the appearance stream. + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_text"); + const uint32_t field = EPDFForm_CreateField( + document(), 4 /* EPDF_FORMFIELD_FAMILY_TEXT */, field_name.get()); + ASSERT_GT(field, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot.get()), nullptr)); + + ScopedFPDFWideString value = GetFPDFWideString(L"\xD55C\xAE00"); + ASSERT_TRUE(EPDFForm_SetTextValue(document(), field, value.get(), nullptr, + 0, nullptr)); + + // The annotation-plane companion regenerates the same appearance. + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, ComboBoxKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_combo"); + const uint32_t field = EPDFForm_CreateField( + document(), 5 /* EPDF_FORMFIELD_FAMILY_COMBOBOX */, field_name.get()); + ASSERT_GT(field, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot.get()), nullptr)); + + ScopedFPDFWideString latin_option = GetFPDFWideString(L"Latin"); + ScopedFPDFWideString korean_option = GetFPDFWideString(L"\xD55C\xAE00"); + FPDF_WIDESTRING labels[] = {latin_option.get(), korean_option.get()}; + ASSERT_TRUE(EPDFForm_SetFieldOptions(document(), field, labels, labels, 2)); + FPDF_WIDESTRING selection[] = {korean_option.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), field, selection, 1, + nullptr, 0, nullptr)); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, ListBoxKoreanUsesRegisteredDroidFallbackFont) { + ScopedRegisteredFonts scoped_fonts; + EPDF_FONT_ID font_id = RegisterDroidSansFallbackFullFont(); + ASSERT_NE(0u, font_id); + + CreateEmptyDocument(); + { + ScopedFPDFPage page(FPDFPage_New(document(), 0, 400, 400)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_WIDGET)); + ASSERT_TRUE(annot); + const FS_RECTF rect{50.0f, 220.0f, 350.0f, 330.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 18.0f, 0, 0, 0)); + + ScopedFPDFWideString field_name = GetFPDFWideString(L"korean_list"); + const uint32_t field = EPDFForm_CreateField( + document(), 6 /* EPDF_FORMFIELD_FAMILY_LISTBOX */, field_name.get()); + ASSERT_GT(field, 0u); + ASSERT_TRUE(EPDFForm_AttachWidget( + document(), field, EPDFAnnot_GetObjectNumber(annot.get()), nullptr)); + + ScopedFPDFWideString latin_option = GetFPDFWideString(L"Latin"); + ScopedFPDFWideString korean_option = GetFPDFWideString(L"\xD55C\xAE00"); + FPDF_WIDESTRING labels[] = {latin_option.get(), korean_option.get()}; + ASSERT_TRUE(EPDFForm_SetFieldOptions(document(), field, labels, labels, 2)); + FPDF_WIDESTRING selection[] = {korean_option.get()}; + ASSERT_TRUE(EPDFForm_SetChoiceValues(document(), field, selection, 1, + nullptr, 0, nullptr)); + + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + ExpectRegisteredAppearanceMapsUnicode(annot.get(), font_id, + {L'\xD55C', L'\xAE00'}); + } + CloseDocument(); +} + +TEST_F(FPDFAnnotEmbedderTest, FreeTextRegisteredFontSubsetsAreLayerLocal) { + ScopedRegisteredFonts scoped_fonts; + + std::vector font_data = LoadRobotoFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("Roboto", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + struct LayerSubsetResult { + std::string delta; + ByteString subset_base_font; + }; + + auto save_layer_with_text = [&](const wchar_t* text, + const std::vector& expected_chars, + const std::vector& unexpected_chars) { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + EXPECT_TRUE(layer); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + EXPECT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FREETEXT)); + EXPECT_TRUE(annot); + + const FS_RECTF rect{50.0f, 250.0f, 350.0f, 320.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ScopedFPDFWideString contents = GetFPDFWideString(text); + EXPECT_TRUE( + FPDFAnnot_SetStringValue(annot.get(), "Contents", contents.get())); + EXPECT_TRUE(EPDFAnnot_SetDefaultAppearanceRegisteredFont( + annot.get(), font_id, 18.0f, 0, 0, 0)); + EXPECT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + + RetainPtr font_dict = + GetAppearanceFontDict(annot.get(), RegisteredFontAlias(font_id)); + EXPECT_TRUE(font_dict); + EXPECT_TRUE(AppearanceFontHasEmbeddedSubset(font_dict.Get())); + for (char value : expected_chars) { + EXPECT_TRUE(AppearanceFontMapsUnicode(font_dict.Get(), value)); + } + for (char value : unexpected_chars) { + EXPECT_FALSE(AppearanceFontMapsUnicode(font_dict.Get(), value)); + } + + ByteString subset_base_font = + font_dict ? font_dict->GetNameFor("BaseFont") : ByteString(); + + unsigned long delta_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* delta_buffer = EPDFLayer_SaveDeltaToOwnedBuffer( + layer.get(), &delta_size, &save_status); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_TRUE(delta_buffer); + std::string delta(static_cast(delta_buffer), delta_size); + EPDF_FreeBuffer(delta_buffer); + return LayerSubsetResult{std::move(delta), std::move(subset_base_font)}; + }; + + LayerSubsetResult layer_a = + save_layer_with_text(L"ABC", {'A', 'B', 'C'}, {'D', 'E', 'F'}); + LayerSubsetResult layer_b = + save_layer_with_text(L"DEF", {'D', 'E', 'F'}, {'A', 'B', 'C'}); + EPDF_ReleaseBaseDocument(base); + + EXPECT_FALSE(layer_a.delta.empty()); + EXPECT_FALSE(layer_b.delta.empty()); + EXPECT_LT(layer_a.delta.size(), font_data.size() / 2); + EXPECT_LT(layer_b.delta.size(), font_data.size() / 2); + EXPECT_NE(layer_a.subset_base_font, layer_b.subset_base_font); + EXPECT_NE(std::string::npos, + layer_a.delta.find(layer_a.subset_base_font.c_str())); + EXPECT_NE(std::string::npos, + layer_b.delta.find(layer_b.subset_base_font.c_str())); + EXPECT_EQ(std::string::npos, + layer_a.delta.find(layer_b.subset_base_font.c_str())); + EXPECT_EQ(std::string::npos, + layer_b.delta.find(layer_a.subset_base_font.c_str())); +} + +TEST_F(FPDFAnnotEmbedderTest, RegisteredFallbackFontRendersPageMissingGlyph) { + auto create_broken_pdf = []() { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + EXPECT_TRUE(doc); + { + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + EXPECT_TRUE(page); + AddBrokenTrueTypeCjkTextPageContent(doc.get(), page.get()); + } + + unsigned long saved_size = 0; + void* saved_buffer = + EPDF_SaveDocumentToOwnedBuffer(doc.get(), /*flags=*/0, &saved_size); + EXPECT_TRUE(saved_buffer); + if (!saved_buffer) { + return std::vector(); + } + std::vector pdf_bytes( + static_cast(saved_buffer), + static_cast(saved_buffer) + saved_size); + EPDF_FreeBuffer(saved_buffer); + return pdf_bytes; + }; + + auto render_broken_pdf = [](const std::vector& pdf_bytes) { + MemoryFileAccess file_access(pdf_bytes); + ScopedFPDFDocument doc(FPDF_LoadCustomDocument(&file_access, nullptr)); + EXPECT_TRUE(doc); + ScopedFPDFPage page(FPDF_LoadPage(doc.get(), 0)); + EXPECT_TRUE(page); + ScopedFPDFBitmap bitmap = EmbedderTest::RenderPage(page.get()); + EXPECT_TRUE(bitmap); + return bitmap; + }; + + ScopedRegisteredFonts scoped_fonts; + std::vector broken_pdf = create_broken_pdf(); + ASSERT_FALSE(broken_pdf.empty()); + ScopedFPDFBitmap bitmap_without_fallback = render_broken_pdf(broken_pdf); + ASSERT_TRUE(bitmap_without_fallback); + std::string without_registered_fallback = + BitmapChecksum(bitmap_without_fallback.get()); + ASSERT_FALSE(without_registered_fallback.empty()); + + std::vector font_data = LoadNotoSansSCFontData(); + ASSERT_FALSE(font_data.empty()); + + EPDF_FONT_ID font_id = + EPDFFont_RegisterMemFont64("NotoSansSC", /*weight=*/400, /*italic=*/0, + font_data.data(), font_data.size()); + ASSERT_NE(0u, font_id); + ASSERT_TRUE(EPDFFont_AddFallbackFont(font_id)); + + ScopedFPDFBitmap bitmap = render_broken_pdf(broken_pdf); + ASSERT_TRUE(bitmap); + + EXPECT_TRUE(BitmapHasNonWhitePixels(bitmap.get())); + EXPECT_NE(without_registered_fallback, BitmapChecksum(bitmap.get())); +} + TEST_F(FPDFAnnotEmbedderTest, ExtractHighlightLongContent) { // Open a file with one annotation and load its first page. ASSERT_TRUE(OpenDocument("annotation_highlight_long_content.pdf")); @@ -3404,7 +4334,7 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionRemovesTextInMiddleOfSentence) { { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); ASSERT_TRUE(annot); - ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get())); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); } ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); @@ -3426,7 +4356,7 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionRemovesTextInMiddleOfSentence) { EXPECT_NE(before_hash, after_hash); } -TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionReportsIntersectingAnnotation) { +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionCountsIntersectingAnnotation) { ASSERT_TRUE(OpenDocument("redact_remove_annots.pdf")); ScopedPage page = LoadScopedPage(0); ASSERT_TRUE(page); @@ -3435,10 +4365,8 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionReportsIntersectingAnnotation) { { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); ASSERT_TRUE(annot); - RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); - EXPECT_EQ(2u, report.written_count); - EXPECT_EQ(2u, report.total_count); - EXPECT_THAT(report.object_numbers, testing::UnorderedElementsAre(5u, 6u)); + // The intersecting Square counts; the applied REDACT itself does not. + EXPECT_EQ(1u, ApplyRedactionCountingRemoved(page.get(), annot.get())); } EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); @@ -3453,10 +4381,9 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionPreservesSiblingRedactions) { { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); ASSERT_TRUE(annot); - RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); - EXPECT_EQ(2u, report.written_count); - EXPECT_EQ(2u, report.total_count); - EXPECT_THAT(report.object_numbers, testing::UnorderedElementsAre(5u, 7u)); + // Only the intersecting Square counts: the applied REDACT is the + // instruction and the sibling REDACT is preserved. + EXPECT_EQ(1u, ApplyRedactionCountingRemoved(page.get(), annot.get())); } ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get())); @@ -3474,10 +4401,9 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionDoesNotRemoveTouchOnlyAnnotation) { { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); ASSERT_TRUE(annot); - RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); - EXPECT_EQ(1u, report.written_count); - EXPECT_EQ(1u, report.total_count); - EXPECT_THAT(report.object_numbers, testing::ElementsAre(5u)); + // The edge-touching Square (no positive-area intersection) survives, so + // nothing but the REDACT itself was removed. + EXPECT_EQ(0u, ApplyRedactionCountingRemoved(page.get(), annot.get())); } ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get())); @@ -3495,30 +4421,321 @@ TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionCascadesPopupRemoval) { { ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); ASSERT_TRUE(annot); - RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); - EXPECT_EQ(3u, report.written_count); - EXPECT_EQ(3u, report.total_count); - EXPECT_THAT(report.object_numbers, - testing::UnorderedElementsAre(5u, 6u, 7u)); + // The Text annotation and its cascaded Popup both count. + EXPECT_EQ(2u, ApplyRedactionCountingRemoved(page.get(), annot.get())); } EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); } -TEST_F(FPDFAnnotEmbedderTest, ApplyPageRedactionsReportsAllRemovedAnnotations) { +TEST_F(FPDFAnnotEmbedderTest, ApplyPageRedactionsCountsRemovedAnnotations) { ASSERT_TRUE(OpenDocument("redact_apply_all_visible.pdf")); ScopedPage page = LoadScopedPage(0); ASSERT_TRUE(page); ASSERT_EQ(4, FPDFPage_GetAnnotCount(page.get())); - RedactionReport report = ApplyPageRedactionsWithReport(page.get()); - EXPECT_EQ(4u, report.written_count); - EXPECT_EQ(4u, report.total_count); - EXPECT_THAT(report.object_numbers, - testing::UnorderedElementsAre(5u, 6u, 7u, 8u)); + // Two Squares count; the two REDACT annotations consumed by the apply do + // not. + EXPECT_EQ(2u, ApplyPageRedactionsCountingRemoved(page.get())); EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); } +// The overlay tests below author a REDACT annotation via the public API on a +// blank page. Since FPDFPage_CreateAnnot() bakes no /RO, applying exercises +// the synthesis path from the declarative entries (/IC, /OverlayText, /DA, +// /Q, /Repeat) — the same situation as a file marked by another processor. +// Page: 200x200; region /Rect [20 50 180 150] => bitmap x [20,180), y +// [50,150) with the top half of the region at y [50,100). + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionSynthesizesInteriorColorFill) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + uint32_t removed_count = 7; // Sentinel: must be zeroed on entry. + ASSERT_TRUE( + EPDFAnnot_ApplyRedaction(page.get(), annot.get(), &removed_count)); + EXPECT_EQ(0u, removed_count); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + EXPECT_EQ(0xFF000000u, GetPixelColor(bitmap.get(), 100, 100)); // inside + EXPECT_EQ(0xFFFFFFFFu, GetPixelColor(bitmap.get(), 10, 100)); // outside +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionSynthesizesOverlayTextLabel) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + ScopedFPDFWideString text = GetFPDFWideString(L"SECRET"); + ASSERT_TRUE(EPDFAnnot_SetOverlayText(annot.get(), text.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), + FPDF_FONT_HELVETICA, 12.0f, + /*R=*/255, /*G=*/255, + /*B=*/255)); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + // The label is drawn top-aligned into the black fill, so the top half of + // the region has non-black ink and a single 12pt line never reaches the + // bottom half. + EXPECT_GT(CountInkPixels(bitmap.get(), 20, 50, 180, 100, 0xFF000000u), 0); + EXPECT_EQ(0, CountInkPixels(bitmap.get(), 20, 100, 180, 150, 0xFF000000u)); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionRepeatsOverlayTextToFillRegion) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + ScopedFPDFWideString text = GetFPDFWideString(L"SECRET"); + ASSERT_TRUE(EPDFAnnot_SetOverlayText(annot.get(), text.get())); + ASSERT_TRUE(EPDFAnnot_SetOverlayTextRepeat(annot.get(), true)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), + FPDF_FONT_HELVETICA, 12.0f, + /*R=*/255, /*G=*/255, + /*B=*/255)); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + // /Repeat tiles the label down the region, so unlike the single-label case + // the bottom half of the region carries ink too. + EXPECT_GT(CountInkPixels(bitmap.get(), 20, 50, 180, 100, 0xFF000000u), 0); + EXPECT_GT(CountInkPixels(bitmap.get(), 20, 100, 180, 150, 0xFF000000u), 0); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionHonorsOverlayTextAlignment) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + ScopedFPDFWideString text = GetFPDFWideString(L"X"); + ASSERT_TRUE(EPDFAnnot_SetOverlayText(annot.get(), text.get())); + ASSERT_TRUE(EPDFAnnot_SetTextAlignment(annot.get(), + FPDF_TEXT_ALIGNMENT_RIGHT)); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), + FPDF_FONT_HELVETICA, 12.0f, + /*R=*/255, /*G=*/255, + /*B=*/255)); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + // /Q 2 pushes the single short label into the right half of the region. + EXPECT_EQ(0, CountInkPixels(bitmap.get(), 20, 50, 100, 150, 0xFF000000u)); + EXPECT_GT(CountInkPixels(bitmap.get(), 100, 50, 180, 150, 0xFF000000u), 0); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionPrefersBakedOverlayStream) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + // Bake the appearance (and with it /RO, black fill), then flip /IC to + // white WITHOUT regenerating. ISO 32000-2: an existing /RO takes + // precedence over the declarative entries, so apply must paint black. + // FPDFAnnot_SetColor() refuses to touch an annotation that already has an + // /AP, which is precisely the stale-/RO situation this test needs — write + // the dict entry directly. + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + CPDF_AnnotContext* context = + CPDFAnnotContextFromFPDFAnnotation(annot.get()); + ASSERT_TRUE(context); + RetainPtr ic = + context->GetMutableAnnotDict()->SetNewFor("IC"); + ic->AppendNew(1.0f); + ic->AppendNew(1.0f); + ic->AppendNew(1.0f); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + EXPECT_EQ(0xFF000000u, GetPixelColor(bitmap.get(), 100, 100)); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionWithoutOverlayLeavesRegionClear) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + { + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + // No /RO, no /IC, no /OverlayText: ISO leaves the region transparent. + uint32_t removed_count = 7; + ASSERT_TRUE( + EPDFAnnot_ApplyRedaction(page.get(), annot.get(), &removed_count)); + EXPECT_EQ(0u, removed_count); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + ScopedFPDFBitmap bitmap = RenderPageOnWhite(page.get(), 200, 200); + EXPECT_EQ(0, CountInkPixels(bitmap.get(), 20, 50, 180, 150, 0xFFFFFFFFu)); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionPreservesInheritedColorspaceResources) { + // The fixture models a common exporter pattern: the page content stream is + // a bare prolog (`/C1 CS /C1 cs q /X1 Do Q`) and the artwork form paints + // with `scn` alone, INHERITING the page-level colorspace. No page object + // "owns" the /C1 reference, so an append-only regeneration (the redaction + // overlay) must NOT let resource pruning delete it — that turned whole + // pages grayscale. + ASSERT_TRUE(OpenDocument("redact_inherited_colorspace.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + auto is_reddish = [](uint32_t argb) { + const int r = (argb >> 16) & 0xff; + const int g = (argb >> 8) & 0xff; + const int b = argb & 0xff; + return r > 180 && g < 100 && b < 100; + }; + + { + ScopedFPDFBitmap bmp = RenderPageOnWhite(page.get(), 200, 200); + ASSERT_TRUE(is_reddish(GetPixelColor(bmp.get(), 100, 100))); + } + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + uint32_t removed_count = 7; + ASSERT_TRUE( + EPDFAnnot_ApplyRedaction(page.get(), annot.get(), &removed_count)); + EXPECT_EQ(0u, removed_count); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + + // Live: the artwork keeps its colour, the corner box is painted. + { + ScopedFPDFBitmap bmp = RenderPageOnWhite(page.get(), 200, 200); + EXPECT_TRUE(is_reddish(GetPixelColor(bmp.get(), 100, 100))); + EXPECT_EQ(0xFF000000u, GetPixelColor(bmp.get(), 15, 185)); + } + + // Round-trip: the saved file must still carry the /C1 resource. + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ASSERT_TRUE(OpenSavedDocument()); + FPDF_PAGE saved_page = LoadSavedPage(0); + ASSERT_TRUE(saved_page); + { + ScopedFPDFBitmap bmp = RenderPageOnWhite(saved_page, 200, 200); + EXPECT_TRUE(is_reddish(GetPixelColor(bmp.get(), 100, 100))); + EXPECT_EQ(0xFF000000u, GetPixelColor(bmp.get(), 15, 185)); + } + CloseSavedPage(saved_page); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionOnNeverParsedPageRemovesText) { + ASSERT_TRUE(OpenDocument("redact_text_middle.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + // Deliberately touch NOTHING that would parse the page first — a headless + // worker page looks exactly like this. The apply itself must parse; an + // unparsed apply would silently remove nothing. + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + + ASSERT_TRUE(OpenSavedDocument()); + FPDF_PAGE saved_page = LoadSavedPage(0); + ASSERT_TRUE(saved_page); + std::wstring after = ExtractPageText(saved_page); + EXPECT_EQ(std::wstring::npos, after.find(L"secret")); + EXPECT_NE(std::wstring::npos, after.find(L"hello")); + EXPECT_NE(std::wstring::npos, after.find(L"world")); + CloseSavedPage(saved_page); +} + +TEST_F(FPDFAnnotEmbedderTest, GenerateRedactAppearanceBakesLabelIntoOverlay) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot = + CreateRedactAnnot(page.get(), {20, 150, 180, 50}); + ASSERT_TRUE(annot); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), + FPDFANNOT_COLORTYPE_InteriorColor, + /*R=*/0, /*G=*/0, /*B=*/0, /*A=*/255)); + ScopedFPDFWideString text = GetFPDFWideString(L"SECRET"); + ASSERT_TRUE(EPDFAnnot_SetOverlayText(annot.get(), text.get())); + ASSERT_TRUE(EPDFAnnot_SetDefaultAppearance(annot.get(), FPDF_FONT_HELVETICA, + 12.0f, /*R=*/255, /*G=*/255, + /*B=*/255)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + + // The rollover appearance shares the final overlay stream with /RO, so the + // baked marking-stage hover preview must contain both the fill and the + // label text ops. + unsigned long length_bytes = FPDFAnnot_GetAP( + annot.get(), FPDF_ANNOT_APPEARANCEMODE_ROLLOVER, nullptr, 0); + ASSERT_GT(length_bytes, 0u); + std::vector buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + FPDFAnnot_GetAP(annot.get(), FPDF_ANNOT_APPEARANCEMODE_ROLLOVER, + buffer.data(), length_bytes)); + std::wstring rollover = GetPlatformWString(buffer.data()); + EXPECT_NE(std::wstring::npos, rollover.find(L" re f")); // /IC fill + EXPECT_NE(std::wstring::npos, rollover.find(L"Tj")); // label text +} + TEST_F(FPDFAnnotEmbedderTest, PolygonAnnotation) { ASSERT_TRUE(OpenDocument("polygon_annot.pdf")); ScopedPage page = LoadScopedPage(0); @@ -4087,3 +5304,79 @@ TEST_F(FPDFAnnotEmbedderTest, SharedFormXObjectMatrix) { EXPECT_FLOAT_EQ(-10.395f, matrix2.e); EXPECT_FLOAT_EQ(-5.42212f, matrix2.f); } + +TEST_F(FPDFAnnotEmbedderTest, GenerateFileAttachmentAppearance) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FILEATTACHMENT)); + ASSERT_TRUE(annot); + + const FS_RECTF rect{50.0f, 130.0f, 90.0f, 50.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 255, 0, + 0, 255)); + ASSERT_TRUE( + EPDFAnnot_SetName(annot.get(), FPDF_ANNOT_NAME_File_Paperclip)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + + // The paperclip is a stroked wire glyph. + std::wstring appearance = GetNormalAppearance(annot.get()); + EXPECT_THAT(appearance, HasSubstr(L"S\n")); + + // Like the note icon, the /Rect is forced to the fixed 20x20 icon box + // anchored at the original bottom-left corner. + FS_RECTF actual_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &actual_rect)); + EXPECT_FLOAT_EQ(50.0f, actual_rect.left); + EXPECT_FLOAT_EQ(50.0f, actual_rect.bottom); + EXPECT_FLOAT_EQ(70.0f, actual_rect.right); + EXPECT_FLOAT_EQ(70.0f, actual_rect.top); +} + +TEST_F(FPDFAnnotEmbedderTest, GenerateFileAttachmentAppearancePerIcon) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 400, 400)); + ASSERT_TRUE(page); + + auto make_appearance = [&](FPDF_ANNOT_NAME icon) { + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_FILEATTACHMENT)); + EXPECT_TRUE(annot); + const FS_RECTF rect{10.0f, 30.0f, 30.0f, 10.0f}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + if (icon != FPDF_ANNOT_NAME_UNKNOWN) { + EXPECT_TRUE(EPDFAnnot_SetName(annot.get(), icon)); + } + EXPECT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + return GetNormalAppearance(annot.get()); + }; + + const std::wstring pushpin = make_appearance(FPDF_ANNOT_NAME_File_PushPin); + const std::wstring paperclip = + make_appearance(FPDF_ANNOT_NAME_File_Paperclip); + const std::wstring graph = make_appearance(FPDF_ANNOT_NAME_File_Graph); + const std::wstring tag = make_appearance(FPDF_ANNOT_NAME_File_Tag); + + // Each icon draws a distinct glyph. + EXPECT_NE(pushpin, paperclip); + EXPECT_NE(pushpin, graph); + EXPECT_NE(pushpin, tag); + EXPECT_NE(paperclip, graph); + EXPECT_NE(paperclip, tag); + EXPECT_NE(graph, tag); + + // Filled glyphs paint fill+stroke; the paperclip wire only strokes. + EXPECT_THAT(pushpin, HasSubstr(L"B\n")); + EXPECT_THAT(graph, HasSubstr(L"B*\n")); + EXPECT_THAT(tag, HasSubstr(L"B*\n")); + EXPECT_THAT(paperclip, HasSubstr(L"S\n")); + + // An absent /Name renders the PushPin glyph (the ISO 32000 default). + const std::wstring default_icon = make_appearance(FPDF_ANNOT_NAME_UNKNOWN); + EXPECT_EQ(pushpin, default_icon); +} diff --git a/fpdfsdk/fpdf_attachment.cpp b/fpdfsdk/fpdf_attachment.cpp index 40ae66baef..66b293dfd1 100644 --- a/fpdfsdk/fpdf_attachment.cpp +++ b/fpdfsdk/fpdf_attachment.cpp @@ -5,9 +5,14 @@ #include "public/fpdf_attachment.h" #include +#include +#include #include +#include +#include #include +#include #include #include "constants/stream_dict_common.h" @@ -19,14 +24,17 @@ #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_stream_acc.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfdoc/cpdf_filespec.h" #include "core/fpdfdoc/cpdf_nametree.h" #include "core/fxcodec/data_and_bytes_consumed.h" +#include "core/fxcodec/flate/flatemodule.h" #include "core/fxcrt/cfx_datetime.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/fx_extension.h" +#include "core/fxcrt/notreached.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "fpdfsdk/cpdfsdk_helpers.h" @@ -34,6 +42,101 @@ namespace { constexpr char kChecksumKey[] = "CheckSum"; +// How EPDFAttachment_ExtractFile* decodes a given embedded file stream. +enum class ExtractFilterPath { + kUnfiltered, // No filters — the raw stream bytes ARE the file. + kSingleFlate, // Exactly one predictor-less FlateDecode — streamable. + kGeneric, // Anything else — decode fully in memory (stock behavior). +}; + +ExtractFilterPath ClassifyExtractFilters(const CPDF_Stream* stream) { + std::optional decoders = GetDecoderArray(stream->GetDict()); + if (!decoders.has_value()) { + return ExtractFilterPath::kGeneric; + } + if (decoders->empty()) { + return ExtractFilterPath::kUnfiltered; + } + if (decoders->size() != 1) { + return ExtractFilterPath::kGeneric; + } + const ByteString& name = (*decoders)[0].first; + if (name != "FlateDecode" && name != "Fl") { + return ExtractFilterPath::kGeneric; + } + RetainPtr param = ToDictionary((*decoders)[0].second); + if (param && param->GetIntegerFor("Predictor", 1) > 1) { + return ExtractFilterPath::kGeneric; + } + return ExtractFilterPath::kSingleFlate; +} + +struct ExtractOutcome { + EPDFAttachmentExtractStatus status; + uint64_t size; +}; + +using ExtractSink = std::function)>; + +// Shared core of the EPDFAttachment_ExtractFile* APIs: locates the embedded +// file stream and pushes its decoded bytes into |sink|. Termination and +// malformed-filter behavior deliberately match FPDFAttachment_GetFile(), +// which this replaces on the read path. +ExtractOutcome ExtractAttachmentFileToSink(FPDF_ATTACHMENT attachment, + uint64_t max_decoded_bytes, + const ExtractSink& sink) { + CPDF_Object* file = CPDFObjectFromFPDFAttachment(attachment); + if (!file) { + return {EPDFAttachmentExtractStatus_kNoFileStream, 0}; + } + + CPDF_FileSpec spec(pdfium::WrapRetain(file)); + RetainPtr file_stream = spec.GetFileStream(); + if (!file_stream) { + return {EPDFAttachmentExtractStatus_kNoFileStream, 0}; + } + + const ExtractFilterPath path = ClassifyExtractFilters(file_stream.Get()); + auto stream_acc = pdfium::MakeRetain(std::move(file_stream)); + if (path == ExtractFilterPath::kSingleFlate) { + stream_acc->LoadAllDataRaw(); + uint64_t total = 0; + switch (FlateModule::FlateDecodeToSink(stream_acc->GetSpan(), + max_decoded_bytes, sink, &total)) { + case FlateModule::SinkDecodeStatus::kSuccess: + return {EPDFAttachmentExtractStatus_kSuccess, total}; + case FlateModule::SinkDecodeStatus::kLimitExceeded: + return {EPDFAttachmentExtractStatus_kSizeLimitExceeded, total}; + case FlateModule::SinkDecodeStatus::kSinkError: + return {EPDFAttachmentExtractStatus_kWriteFailed, total}; + } + NOTREACHED(); + } + + if (path == ExtractFilterPath::kUnfiltered) { + stream_acc->LoadAllDataRaw(); + } else { + stream_acc->LoadAllDataFiltered(); + } + pdfium::span data = stream_acc->GetSpan(); + if (max_decoded_bytes && data.size() > max_decoded_bytes) { + return {EPDFAttachmentExtractStatus_kSizeLimitExceeded, 0}; + } + if (!data.empty() && !sink(data)) { + return {EPDFAttachmentExtractStatus_kWriteFailed, 0}; + } + return {EPDFAttachmentExtractStatus_kSuccess, data.size()}; +} + +// Sizes these APIs can report are capped by the uint32_t |out_size|. +ExtractOutcome CapOutcomeToUint32(ExtractOutcome outcome) { + if (outcome.status == EPDFAttachmentExtractStatus_kSuccess && + outcome.size > std::numeric_limits::max()) { + outcome.status = EPDFAttachmentExtractStatus_kSizeLimitExceeded; + } + return outcome; +} + } // namespace FPDF_EXPORT int FPDF_CALLCONV @@ -99,6 +202,55 @@ FPDFDoc_GetAttachment(FPDF_DOCUMENT document, int index) { name_tree->LookupValueAndName(index, &csName)); } +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetAttachmentKey(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || index < 0) { + return 0; + } + + auto name_tree = CPDF_NameTree::CreateForReading(doc, "EmbeddedFiles"); + if (!name_tree || static_cast(index) >= name_tree->GetCount()) { + return 0; + } + + WideString key; + if (!name_tree->LookupValueAndName(index, &key)) { + return 0; + } + + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + key, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetAttachmentIndexByKey(FPDF_DOCUMENT document, FPDF_WIDESTRING key) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !key) { + return -1; + } + + auto name_tree = CPDF_NameTree::CreateForReading(doc, "EmbeddedFiles"); + if (!name_tree) { + return -1; + } + + // SAFETY: required from caller. + WideString target = UNSAFE_BUFFERS(WideStringFromFPDFWideString(key)); + const size_t count = name_tree->GetCount(); + for (size_t i = 0; i < count; ++i) { + WideString candidate; + if (name_tree->LookupValueAndName(i, &candidate) && candidate == target) { + return pdfium::checked_cast(i); + } + } + return -1; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFDoc_DeleteAttachment(FPDF_DOCUMENT document, int index) { CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); @@ -435,3 +587,91 @@ EPDFAttachment_GetIntegerValue(FPDF_ATTACHMENT attachment, num->IsInteger() ? num->GetInteger() : static_cast(num->GetNumber()); return true; } + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ExtractFile(FPDF_ATTACHMENT attachment, + FPDF_FILEWRITE* file_write, + uint64_t max_decoded_bytes, + uint32_t* out_size, + EPDFAttachmentExtractStatus* out_status) { + if (out_size) { + *out_size = 0; + } + if (out_status) { + *out_status = EPDFAttachmentExtractStatus_kWriteFailed; + } + if (!file_write || file_write->version != 1 || !file_write->WriteBlock) { + return false; + } + + ExtractOutcome outcome = CapOutcomeToUint32(ExtractAttachmentFileToSink( + attachment, max_decoded_bytes, + [file_write](pdfium::span chunk) { + return file_write->WriteBlock( + file_write, chunk.data(), + pdfium::checked_cast(chunk.size())) != 0; + })); + if (out_status) { + *out_status = outcome.status; + } + if (outcome.status != EPDFAttachmentExtractStatus_kSuccess) { + return false; + } + if (out_size) { + *out_size = static_cast(outcome.size); + } + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ExtractFileToOwnedBuffer( + FPDF_ATTACHMENT attachment, + uint64_t max_decoded_bytes, + void** out_buffer, + uint32_t* out_size, + EPDFAttachmentExtractStatus* out_status) { + if (out_buffer) { + *out_buffer = nullptr; + } + if (out_size) { + *out_size = 0; + } + if (out_status) { + *out_status = EPDFAttachmentExtractStatus_kWriteFailed; + } + if (!out_buffer || !out_size) { + return false; + } + + DataVector data; + ExtractOutcome outcome = CapOutcomeToUint32(ExtractAttachmentFileToSink( + attachment, max_decoded_bytes, + [&data](pdfium::span chunk) { + data.insert(data.end(), chunk.begin(), chunk.end()); + return true; + })); + if (out_status) { + *out_status = outcome.status; + } + if (outcome.status != EPDFAttachmentExtractStatus_kSuccess) { + return false; + } + // A zero-byte embedded file is a valid success: NULL buffer, size 0. + if (data.empty()) { + return true; + } + + // Must be malloc() so EPDF_FreeBuffer() (which calls free()) can release + // it — same contract as the EPDF_*ToOwnedBuffer() save APIs. + void* buffer = malloc(data.size()); + if (!buffer) { + if (out_status) { + *out_status = EPDFAttachmentExtractStatus_kWriteFailed; + } + return false; + } + memcpy(buffer, data.data(), data.size()); + *out_buffer = buffer; + *out_size = static_cast(data.size()); + return true; +} diff --git a/fpdfsdk/fpdf_attachment_embeddertest.cpp b/fpdfsdk/fpdf_attachment_embeddertest.cpp index ca51f3f9a3..5f247a85f4 100644 --- a/fpdfsdk/fpdf_attachment_embeddertest.cpp +++ b/fpdfsdk/fpdf_attachment_embeddertest.cpp @@ -439,3 +439,325 @@ TEST_F(FPDFAttachmentEmbedderTest, GetSubtypeInvalid) { EXPECT_EQ(2u * (strlen(kExpectedSubtype) + 1), FPDFAttachment_GetSubtype(attachment, nullptr, 10)); } + +namespace { + +class CollectingFileWriter final : public FPDF_FILEWRITE { + public: + CollectingFileWriter() { + version = 1; + WriteBlock = WriteBlockImpl; + } + + const std::string& data() const { return data_; } + int write_calls() const { return write_calls_; } + + private: + static int WriteBlockImpl(FPDF_FILEWRITE* self, + const void* data, + unsigned long size) { + auto* writer = static_cast(self); + ++writer->write_calls_; + writer->data_.append(static_cast(data), size); + return 1; + } + + std::string data_; + int write_calls_ = 0; +}; + +class FailingFileWriter final : public FPDF_FILEWRITE { + public: + FailingFileWriter() { + version = 1; + WriteBlock = WriteBlockImpl; + } + + private: + static int WriteBlockImpl(FPDF_FILEWRITE*, const void*, unsigned long) { + return 0; + } +}; + +std::string GetFileViaStockApi(FPDF_ATTACHMENT attachment) { + unsigned long length = 0; + if (!FPDFAttachment_GetFile(attachment, nullptr, 0, &length)) { + ADD_FAILURE() << "stock FPDFAttachment_GetFile failed"; + return std::string(); + } + std::vector buf(length); + unsigned long actual = 0; + EXPECT_TRUE(FPDFAttachment_GetFile(attachment, buf.data(), length, &actual)); + return std::string(buf.data(), actual); +} + +} // namespace + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileMatchesGetFile) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + ASSERT_EQ(2, FPDFDoc_GetAttachmentCount(document())); + + for (int i = 0; i < 2; ++i) { + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), i); + ASSERT_TRUE(attachment); + const std::string expected = GetFileViaStockApi(attachment); + ASSERT_FALSE(expected.empty()); + + // FPDF_FILEWRITE variant produces byte-identical output. + CollectingFileWriter writer; + uint32_t size = 0; + EPDFAttachmentExtractStatus status = + EPDFAttachmentExtractStatus_kWriteFailed; + ASSERT_TRUE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/0, &size, + &status)) + << " for attachment " << i; + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + EXPECT_EQ(expected.size(), static_cast(size)); + EXPECT_EQ(expected, writer.data()); + + // Owned-buffer variant too. + void* buffer = nullptr; + uint32_t buffer_size = 0; + ASSERT_TRUE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/0, &buffer, &buffer_size, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + ASSERT_EQ(expected.size(), static_cast(buffer_size)); + ASSERT_TRUE(buffer); + EXPECT_EQ(expected, std::string(static_cast(buffer), + buffer_size)); + EPDF_FreeBuffer(buffer); + } +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileSizeLimit) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + + // The second attachment is 5869 bytes. + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), 1); + ASSERT_TRUE(attachment); + + CollectingFileWriter writer; + uint32_t size = 0; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kSuccess; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/100, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSizeLimitExceeded, status); + EXPECT_EQ(0u, size); + + void* buffer = nullptr; + uint32_t buffer_size = 0; + EXPECT_FALSE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/100, &buffer, &buffer_size, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSizeLimitExceeded, status); + EXPECT_FALSE(buffer); + EXPECT_EQ(0u, buffer_size); + + // A limit exactly equal to the file size succeeds. + CollectingFileWriter exact_writer; + EXPECT_TRUE(EPDFAttachment_ExtractFile(attachment, &exact_writer, + /*max_decoded_bytes=*/5869, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + EXPECT_EQ(5869u, size); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileNoFileStream) { + // This fixture's attachment is missing the embedded file (/EF). + ASSERT_TRUE(OpenDocument("embedded_attachments_invalid_data.pdf")); + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), 0); + ASSERT_TRUE(attachment); + + CollectingFileWriter writer; + uint32_t size = 0; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kSuccess; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kNoFileStream, status); + EXPECT_EQ(0, writer.write_calls()); + + void* buffer = nullptr; + uint32_t buffer_size = 0; + EXPECT_FALSE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/0, &buffer, &buffer_size, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kNoFileStream, status); + EXPECT_FALSE(buffer); + + // A null attachment behaves the same. + EXPECT_FALSE(EPDFAttachment_ExtractFile(nullptr, &writer, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kNoFileStream, status); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileInvalidWriter) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), 0); + ASSERT_TRUE(attachment); + + uint32_t size = 1; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kSuccess; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, nullptr, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); + EXPECT_EQ(0u, size); + + CollectingFileWriter bad_version; + bad_version.version = 2; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, &bad_version, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); + + CollectingFileWriter no_callback; + no_callback.WriteBlock = nullptr; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, &no_callback, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileWriterFailure) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), 0); + ASSERT_TRUE(attachment); + + FailingFileWriter writer; + uint32_t size = 1; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kSuccess; + EXPECT_FALSE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); + EXPECT_EQ(0u, size); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileEmptyAttachment) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + ScopedFPDFWideString file_name = GetFPDFWideString(L"empty.bin"); + FPDF_ATTACHMENT attachment = + FPDFDoc_AddAttachment(document(), file_name.get()); + ASSERT_TRUE(attachment); + ASSERT_TRUE(FPDFAttachment_SetFile(attachment, document(), nullptr, 0)); + + // A zero-byte embedded file extracts successfully without any writes. + CollectingFileWriter writer; + uint32_t size = 1; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kWriteFailed; + EXPECT_TRUE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + EXPECT_EQ(0u, size); + EXPECT_EQ(0, writer.write_calls()); + + // The owned-buffer variant reports success with a null buffer. + void* buffer = reinterpret_cast(1); + uint32_t buffer_size = 1; + EXPECT_TRUE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/0, &buffer, &buffer_size, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + EXPECT_FALSE(buffer); + EXPECT_EQ(0u, buffer_size); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileLargeAttachment) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + ScopedFPDFWideString file_name = GetFPDFWideString(L"big.bin"); + FPDF_ATTACHMENT attachment = + FPDFDoc_AddAttachment(document(), file_name.get()); + ASSERT_TRUE(attachment); + + std::string contents(2 * 1024 * 1024 + 17, '\0'); + for (size_t i = 0; i < contents.size(); ++i) { + contents[i] = static_cast((i * 31 + i / 997) & 0xff); + } + ASSERT_TRUE(FPDFAttachment_SetFile(attachment, document(), contents.data(), + contents.size())); + + CollectingFileWriter writer; + uint32_t size = 0; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kWriteFailed; + ASSERT_TRUE(EPDFAttachment_ExtractFile(attachment, &writer, + /*max_decoded_bytes=*/0, &size, + &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kSuccess, status); + ASSERT_EQ(contents.size(), static_cast(size)); + EXPECT_EQ(contents, writer.data()); +} + +TEST_F(FPDFAttachmentEmbedderTest, ExtractFileToOwnedBufferBadArgs) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + FPDF_ATTACHMENT attachment = FPDFDoc_GetAttachment(document(), 0); + ASSERT_TRUE(attachment); + + void* buffer = nullptr; + uint32_t size = 0; + EPDFAttachmentExtractStatus status = EPDFAttachmentExtractStatus_kSuccess; + EXPECT_FALSE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/0, nullptr, &size, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); + EXPECT_FALSE(EPDFAttachment_ExtractFileToOwnedBuffer( + attachment, /*max_decoded_bytes=*/0, &buffer, nullptr, &status)); + EXPECT_EQ(EPDFAttachmentExtractStatus_kWriteFailed, status); + EXPECT_FALSE(buffer); +} + +TEST_F(FPDFAttachmentEmbedderTest, GetAttachmentKey) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + ASSERT_EQ(2, FPDFDoc_GetAttachmentCount(document())); + + // This fixture's tree keys equal the /UF names (as do all + // FPDFDoc_AddAttachment-created entries). + unsigned long length_bytes = + EPDFDoc_GetAttachmentKey(document(), 0, nullptr, 0); + ASSERT_EQ(12u, length_bytes); + std::vector buf = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(12u, + EPDFDoc_GetAttachmentKey(document(), 0, buf.data(), length_bytes)); + EXPECT_EQ(L"1.txt", GetPlatformWString(buf.data())); + + length_bytes = EPDFDoc_GetAttachmentKey(document(), 1, nullptr, 0); + ASSERT_EQ(26u, length_bytes); + buf = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(26u, + EPDFDoc_GetAttachmentKey(document(), 1, buf.data(), length_bytes)); + EXPECT_EQ(L"attached.pdf", GetPlatformWString(buf.data())); + + // Bad indices / bad document. + EXPECT_EQ(0u, EPDFDoc_GetAttachmentKey(document(), -1, nullptr, 0)); + EXPECT_EQ(0u, EPDFDoc_GetAttachmentKey(document(), 2, nullptr, 0)); + EXPECT_EQ(0u, EPDFDoc_GetAttachmentKey(nullptr, 0, nullptr, 0)); +} + +TEST_F(FPDFAttachmentEmbedderTest, GetAttachmentIndexByKey) { + ASSERT_TRUE(OpenDocument("embedded_attachments.pdf")); + + ScopedFPDFWideString key1 = GetFPDFWideString(L"1.txt"); + ScopedFPDFWideString key2 = GetFPDFWideString(L"attached.pdf"); + ScopedFPDFWideString missing = GetFPDFWideString(L"nope.bin"); + EXPECT_EQ(0, EPDFDoc_GetAttachmentIndexByKey(document(), key1.get())); + EXPECT_EQ(1, EPDFDoc_GetAttachmentIndexByKey(document(), key2.get())); + EXPECT_EQ(-1, EPDFDoc_GetAttachmentIndexByKey(document(), missing.get())); + EXPECT_EQ(-1, EPDFDoc_GetAttachmentIndexByKey(nullptr, key1.get())); + EXPECT_EQ(-1, EPDFDoc_GetAttachmentIndexByKey(document(), nullptr)); + + // The name tree is sorted, so adding "0.txt" shifts every index. Keys + // keep resolving to the CURRENT position. + ScopedFPDFWideString key0 = GetFPDFWideString(L"0.txt"); + FPDF_ATTACHMENT attachment = + FPDFDoc_AddAttachment(document(), key0.get()); + ASSERT_TRUE(attachment); + EXPECT_EQ(0, EPDFDoc_GetAttachmentIndexByKey(document(), key0.get())); + EXPECT_EQ(1, EPDFDoc_GetAttachmentIndexByKey(document(), key1.get())); + EXPECT_EQ(2, EPDFDoc_GetAttachmentIndexByKey(document(), key2.get())); + + // Deleting shifts them back; the deleted key stops resolving. + EXPECT_TRUE(FPDFDoc_DeleteAttachment(document(), 0)); + EXPECT_EQ(-1, EPDFDoc_GetAttachmentIndexByKey(document(), key0.get())); + EXPECT_EQ(0, EPDFDoc_GetAttachmentIndexByKey(document(), key1.get())); + EXPECT_EQ(1, EPDFDoc_GetAttachmentIndexByKey(document(), key2.get())); +} diff --git a/fpdfsdk/fpdf_doc.cpp b/fpdfsdk/fpdf_doc.cpp index f0ba7f4c51..cec99ec448 100644 --- a/fpdfsdk/fpdf_doc.cpp +++ b/fpdfsdk/fpdf_doc.cpp @@ -336,6 +336,23 @@ FPDF_EXPORT int FPDF_CALLCONV FPDFDest_GetDestPageIndex(FPDF_DOCUMENT document, return destination.GetDestPageIndex(doc); } +FPDF_EXPORT unsigned int FPDF_CALLCONV +EPDFDest_GetPageObjectNumber(FPDF_DOCUMENT document, FPDF_DEST dest) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || !dest) { + return 0; + } + +#ifdef PDF_ENABLE_XFA + if (doc->GetExtension()) { + return 0; + } +#endif // PDF_ENABLE_XFA + + CPDF_Dest destination(pdfium::WrapRetain(CPDFArrayFromFPDFDest(dest))); + return destination.GetPageObjectNumber(doc); +} + FPDF_EXPORT unsigned long FPDF_CALLCONV FPDFDest_GetView(FPDF_DEST dest, unsigned long* pNumParams, FS_FLOAT* pParams) { if (!dest) { diff --git a/fpdfsdk/fpdf_doc_embeddertest.cpp b/fpdfsdk/fpdf_doc_embeddertest.cpp index c76a91986c..1ae6e07161 100644 --- a/fpdfsdk/fpdf_doc_embeddertest.cpp +++ b/fpdfsdk/fpdf_doc_embeddertest.cpp @@ -142,6 +142,42 @@ TEST_F(FPDFDocEmbedderTest, DestGetPageIndex) { EXPECT_EQ(-1, FPDFDest_GetDestPageIndex(document(), dest)); } +TEST_F(FPDFDocEmbedderTest, DestGetPageObjectNumber) { + ASSERT_TRUE(OpenDocument("named_dests.pdf")); + + EXPECT_EQ(0u, EPDFDest_GetPageObjectNumber(nullptr, nullptr)); + EXPECT_EQ(0u, EPDFDest_GetPageObjectNumber(document(), nullptr)); + + const unsigned int page_1_object_number = + EPDFDoc_GetPageObjectNumberByIndex(document(), 1); + ASSERT_GT(page_1_object_number, 0u); + + // Numeric page index in the Dests NameTree. + FPDF_DEST dest = FPDF_GetNamedDestByName(document(), "First"); + ASSERT_TRUE(dest); + EXPECT_EQ(0u, EPDFDest_GetPageObjectNumber(nullptr, dest)); + EXPECT_EQ(page_1_object_number, + EPDFDest_GetPageObjectNumber(document(), dest)); + + // Page dictionary reference in the Dests NameTree. + dest = FPDF_GetNamedDestByName(document(), "Next"); + ASSERT_TRUE(dest); + EXPECT_EQ(page_1_object_number, + EPDFDest_GetPageObjectNumber(document(), dest)); + + // Out-of-range numeric page index in the legacy Dests dictionary. The + // compatibility index API reports the stored 11, but there is no visible + // page object at that index in this two-page fixture. + dest = FPDF_GetNamedDestByName(document(), "FirstAlternate"); + ASSERT_TRUE(dest); + EXPECT_EQ(0u, EPDFDest_GetPageObjectNumber(document(), dest)); + + // Invalid object reference in the Dests NameTree. + dest = FPDF_GetNamedDestByName(document(), "LastAlternate"); + ASSERT_TRUE(dest); + EXPECT_EQ(0u, EPDFDest_GetPageObjectNumber(document(), dest)); +} + TEST_F(FPDFDocEmbedderTest, DestGetView) { ASSERT_TRUE(OpenDocument("named_dests.pdf")); diff --git a/fpdfsdk/fpdf_flatten_embeddertest.cpp b/fpdfsdk/fpdf_flatten_embeddertest.cpp index e8c69ccd03..c42b0b0a2f 100644 --- a/fpdfsdk/fpdf_flatten_embeddertest.cpp +++ b/fpdfsdk/fpdf_flatten_embeddertest.cpp @@ -3,20 +3,70 @@ // found in the LICENSE file. #include "build/build_config.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/fpdf_parser_utility.h" #include "core/fxge/cfx_defaultrenderdevice.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/epdf_form.h" +#include "public/fpdf_annot.h" #include "public/fpdf_flatten.h" +#include "public/fpdf_save.h" #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.h" #include "testing/gtest/include/gtest/gtest.h" +#include "testing/test_loader.h" +#include "testing/utils/file_util.h" +#include "testing/utils/path_service.h" + +#include +#include using testing::HasSubstr; using testing::Not; namespace { -class FPDFFlattenEmbedderTest : public EmbedderTest {}; +class FPDFFlattenEmbedderTest : public EmbedderTest { + protected: + struct LayerDocument { + std::vector bytes; + EPDF_BASE_DOCUMENT base = nullptr; + FPDF_DOCUMENT layer = nullptr; + + ~LayerDocument() { + if (layer) { + FPDF_CloseDocument(layer); + } + if (base) { + EPDF_ReleaseBaseDocument(base); + } + } + }; + + bool OpenLayer(const char* file_name, LayerDocument* out) { + const std::string path = PathService::GetTestFilePath(file_name); + if (path.empty()) { + return false; + } + out->bytes = GetFileContents(path.c_str()); + if (out->bytes.empty()) { + return false; + } + out->base = EPDF_LoadMemBaseDocument( + out->bytes.data(), static_cast(out->bytes.size()), nullptr); + if (!out->base) { + return false; + } + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + out->layer = EPDFLayer_OpenLayer(out->base, nullptr, nullptr, &status); + return out->layer && status == EPDFLayerOpenStatus_kSuccess; + } +}; } // namespace @@ -42,6 +92,211 @@ TEST_F(FPDFFlattenEmbedderTest, FlatPrint) { EXPECT_EQ(FLATTEN_SUCCESS, FPDFPage_Flatten(page.get(), FLAT_PRINT)); } +TEST_F(FPDFFlattenEmbedderTest, FlattenSpecificAnnotationByHandle) { + ASSERT_TRUE(OpenDocument("flatten_selective.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(6, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation target(EPDFPage_GetAnnotByObjectNumber(page.get(), 4u)); + ASSERT_TRUE(target); + EXPECT_EQ(FLATTEN_FAIL, EPDFAnnot_Flatten(page.get(), target.get(), 99)); + EXPECT_EQ(FLATTEN_FAIL, + EPDFAnnot_Flatten(page.get(), nullptr, FLAT_NORMALDISPLAY)); + + ScopedFPDFAnnotation hidden(EPDFPage_GetAnnotByObjectNumber(page.get(), 5u)); + ASSERT_TRUE(hidden); + EXPECT_EQ(FLATTEN_NOTHINGTODO, + EPDFAnnot_Flatten(page.get(), hidden.get(), FLAT_NORMALDISPLAY)); + EXPECT_EQ(6, FPDFPage_GetAnnotCount(page.get())); + + ASSERT_EQ(FLATTEN_SUCCESS, + EPDFAnnot_Flatten(page.get(), target.get(), FLAT_NORMALDISPLAY)); + EXPECT_EQ(5, FPDFPage_GetAnnotCount(page.get())); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 4u)); + ScopedFPDFAnnotation preserved( + EPDFPage_GetAnnotByObjectNumber(page.get(), 5u)); + EXPECT_TRUE(preserved); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ASSERT_TRUE(OpenSavedDocument()); + FPDF_PAGE saved_page = LoadSavedPage(0); + ASSERT_TRUE(saved_page); + EXPECT_EQ(5, FPDFPage_GetAnnotCount(saved_page)); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(saved_page, 4u)); + CloseSavedPage(saved_page); +} + +TEST_F(FPDFFlattenEmbedderTest, + FlattenPagePreservesUnpaintedAnnotationsAndDetachesWidget) { + ASSERT_TRUE(OpenDocument("flatten_selective.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + ASSERT_EQ(FLATTEN_SUCCESS, EPDFPage_Flatten(page.get(), FLAT_NORMALDISPLAY)); + // Loading a regular PDFium page synthesizes a default appearance for the + // Text annotation (object 6), so it is paintable here as well. + EXPECT_EQ(2, FPDFPage_GetAnnotCount(page.get())); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 4u)); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 13u)); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 16u)); + for (unsigned int object_number : {5u, 9u}) { + ScopedFPDFAnnotation annotation( + EPDFPage_GetAnnotByObjectNumber(page.get(), object_number)); + EXPECT_TRUE(annotation) << object_number; + } + + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(pdf); + RetainPtr page_dictionary = pdf->GetPageDictionary(0); + ASSERT_TRUE(page_dictionary); + EXPECT_TRUE(page_dictionary->KeyExist("MediaBox")); + EXPECT_TRUE(page_dictionary->KeyExist("CropBox")); + RetainPtr resources = + page_dictionary->GetDictFor("Resources"); + ASSERT_TRUE(resources); + EXPECT_TRUE(resources->KeyExist("ExtGState")); + EXPECT_TRUE(resources->KeyExist("XObject")); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + ASSERT_EQ(1, EPDFForm_CountFields(model)); + EXPECT_EQ(0, EPDFForm_CountFieldWidgets(model, 0)); + EPDFForm_CloseModel(model); +} + +TEST_F(FPDFFlattenEmbedderTest, FlattenPageHonorsPrintUsage) { + ASSERT_TRUE(OpenDocument("flatten_selective.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(FLATTEN_SUCCESS, EPDFPage_Flatten(page.get(), FLAT_PRINT)); + EXPECT_EQ(4, FPDFPage_GetAnnotCount(page.get())); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 4u)); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 13u)); + ScopedFPDFAnnotation normal_only( + EPDFPage_GetAnnotByObjectNumber(page.get(), 16u)); + EXPECT_TRUE(normal_only); +} + +TEST_F(FPDFFlattenEmbedderTest, FlattenMergedWidgetRemovesFieldTreeEntry) { + ASSERT_TRUE(OpenDocument("text_form.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ScopedFPDFAnnotation widget(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(widget); + ASSERT_EQ(4u, EPDFAnnot_GetObjectNumber(widget.get())); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(widget.get())); + + ASSERT_EQ(FLATTEN_SUCCESS, + EPDFAnnot_Flatten(page.get(), widget.get(), FLAT_NORMALDISPLAY)); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(document()); + ASSERT_TRUE(model); + EXPECT_EQ(0, EPDFForm_CountFields(model)); + EPDFForm_CloseModel(model); +} + +TEST_F(FPDFFlattenEmbedderTest, FlattenPageIsLayerSafeAndDeltaDurable) { + LayerDocument document; + ASSERT_TRUE(OpenLayer("flatten_selective.pdf", &document)); + ASSERT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(document.layer)); + + ScopedFPDFPage page(FPDF_LoadPage(document.layer, 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation hidden(EPDFPage_GetAnnotByObjectNumber(page.get(), 5u)); + ASSERT_TRUE(hidden); + EXPECT_EQ(FLATTEN_NOTHINGTODO, + EPDFAnnot_Flatten(page.get(), hidden.get(), FLAT_NORMALDISPLAY)); + EXPECT_EQ(0ul, EPDFLayer_GetPromotedObjectCount(document.layer)); + + ASSERT_EQ(FLATTEN_SUCCESS, EPDFPage_Flatten(page.get(), FLAT_NORMALDISPLAY)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(document.layer, 3u)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(document.layer, 12u)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(document.layer, 13u)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 4u)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 7u)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 14u)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 15u)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 17u)); + + EXPECT_EQ(3, FPDFPage_GetAnnotCount(page.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(document.layer, this, &save_status)); + ASSERT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + const std::string delta = GetString(); + ASSERT_FALSE(delta.empty()); + + TestLoader loader(pdfium::as_bytes(pdfium::span(delta.data(), delta.size()))); + FPDF_FILEACCESS access = {}; + access.m_FileLen = static_cast(delta.size()); + access.m_GetBlock = TestLoader::GetBlock; + access.m_Param = &loader; + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replay( + EPDFLayer_OpenLayer(document.base, &access, nullptr, &open_status)); + ASSERT_TRUE(replay); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage replay_page(FPDF_LoadPage(replay.get(), 0)); + ASSERT_TRUE(replay_page); + EXPECT_EQ(3, FPDFPage_GetAnnotCount(replay_page.get())); + EPDF_FORM_MODEL model = EPDFForm_LoadModel(replay.get()); + ASSERT_TRUE(model); + ASSERT_EQ(1, EPDFForm_CountFields(model)); + EXPECT_EQ(0, EPDFForm_CountFieldWidgets(model, 0)); + EPDFForm_CloseModel(model); +} + +TEST_F(FPDFFlattenEmbedderTest, FlattenReadsAlreadyPromotedAnnotationState) { + LayerDocument document; + ASSERT_TRUE(OpenLayer("flatten_selective.pdf", &document)); + ScopedFPDFPage page(FPDF_LoadPage(document.layer, 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation target(EPDFPage_GetAnnotByObjectNumber(page.get(), 4u)); + ASSERT_TRUE(target); + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document.layer); + ASSERT_TRUE(pdf); + RetainPtr annotation = + ToDictionary(pdf->GetMutableIndirectObject(4u)); + ASSERT_TRUE(annotation); + annotation->SetNewFor("F", FPDF_ANNOT_FLAG_HIDDEN); + ASSERT_TRUE(EPDFLayer_IsObjectPromoted(document.layer, 4u)); + ASSERT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 3u)); + + EXPECT_EQ(FLATTEN_NOTHINGTODO, + EPDFAnnot_Flatten(page.get(), target.get(), FLAT_NORMALDISPLAY)); + EXPECT_EQ(1ul, EPDFLayer_GetPromotedObjectCount(document.layer)); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document.layer, 3u)); +} + +TEST_F(FPDFFlattenEmbedderTest, FlattenDirectAnnotationByHandle) { + ScopedFPDFDocument document(FPDF_CreateNewDocument()); + ASSERT_TRUE(document); + ScopedFPDFPage page(FPDFPage_New(document.get(), 0, 100, 100)); + ASSERT_TRUE(page); + ScopedFPDFPage other_page(FPDFPage_New(document.get(), 1, 100, 100)); + ASSERT_TRUE(other_page); + + ScopedFPDFAnnotation annotation( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK)); + ASSERT_TRUE(annotation); + ASSERT_EQ(0u, EPDFAnnot_GetObjectNumber(annotation.get())); + const FS_RECTF rectangle = {10.0f, 40.0f, 40.0f, 10.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annotation.get(), &rectangle)); + ScopedFPDFWideString appearance = GetFPDFWideString(L"0 0 10 10 re f"); + ASSERT_TRUE(FPDFAnnot_SetAP( + annotation.get(), FPDF_ANNOT_APPEARANCEMODE_NORMAL, appearance.get())); + + EXPECT_EQ(FLATTEN_FAIL, EPDFAnnot_Flatten(other_page.get(), annotation.get(), + FLAT_NORMALDISPLAY)); + ASSERT_EQ(FLATTEN_SUCCESS, EPDFAnnot_Flatten(page.get(), annotation.get(), + FLAT_NORMALDISPLAY)); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); +} + TEST_F(FPDFFlattenEmbedderTest, FlatWithBadFont) { ASSERT_TRUE(OpenDocument("344775293.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/fpdfsdk/fpdf_javascript.cpp b/fpdfsdk/fpdf_javascript.cpp index 9ee17d61c5..d4e6266016 100644 --- a/fpdfsdk/fpdf_javascript.cpp +++ b/fpdfsdk/fpdf_javascript.cpp @@ -14,25 +14,21 @@ #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/numerics/safe_conversions.h" #include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_action_helpers.h" +#include "public/epdf_action.h" struct CPDF_JavaScript { WideString name; WideString script; }; -FPDF_EXPORT int FPDF_CALLCONV -FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); - if (!doc) { - return -1; - } - - auto name_tree = CPDF_NameTree::CreateForReading(doc, "JavaScript"); - return name_tree ? pdfium::checked_cast(name_tree->GetCount()) : 0; -} +namespace { -FPDF_EXPORT FPDF_JAVASCRIPT_ACTION FPDF_CALLCONV -FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) { +RetainPtr GetNamedJavaScriptActionDictionary( + FPDF_DOCUMENT document, + int index, + WideString* name, + std::optional* script) { CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); if (!doc || index < 0) { return nullptr; @@ -43,21 +39,38 @@ FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) { return nullptr; } - WideString name; - RetainPtr obj = - ToDictionary(name_tree->LookupValueAndName(index, &name)); - if (!obj) { + RetainPtr dictionary = + ToDictionary(name_tree->LookupValueAndName(index, name)); + if (!dictionary) { return nullptr; } - // Validate |obj|. Type is optional, but must be valid if present. - CPDF_Action action(std::move(obj)); + CPDF_Action action(pdfium::WrapRetain(dictionary.Get())); if (action.GetType() != CPDF_Action::Type::kJavaScript) { return nullptr; } + *script = action.MaybeGetJavaScript(); + return script->has_value() ? std::move(dictionary) : nullptr; +} + +} // namespace + +FPDF_EXPORT int FPDF_CALLCONV +FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document) { + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc) { + return -1; + } + + auto name_tree = CPDF_NameTree::CreateForReading(doc, "JavaScript"); + return name_tree ? pdfium::checked_cast(name_tree->GetCount()) : 0; +} - std::optional script = action.MaybeGetJavaScript(); - if (!script.has_value()) { +FPDF_EXPORT FPDF_JAVASCRIPT_ACTION FPDF_CALLCONV +FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) { + WideString name; + std::optional script; + if (!GetNamedJavaScriptActionDictionary(document, index, &name, &script)) { return nullptr; } @@ -67,6 +80,17 @@ FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) { return FPDFJavaScriptActionFromCPDFJavaScriptAction(js.release()); } +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetNamedJavaScriptActionModel(FPDF_DOCUMENT document, int index) { + WideString name; + std::optional script; + RetainPtr dictionary = + GetNamedJavaScriptActionDictionary(document, index, &name, &script); + return dictionary ? epdf::MakeActionModelHandle(epdf::BuildActionModel( + CPDF_Action(std::move(dictionary)))) + : nullptr; +} + FPDF_EXPORT void FPDF_CALLCONV FPDFDoc_CloseJavaScriptAction(FPDF_JAVASCRIPT_ACTION javascript) { // Take object back across API and destroy it. diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index cf7ade5998..8f60478de6 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -56,6 +56,7 @@ #include "core/fxcrt/stl_util.h" #include "core/fxcrt/unowned_ptr.h" #include "core/fxge/cfx_defaultrenderdevice.h" +#include "core/fxge/cfx_fontregistry.h" #include "core/fxge/cfx_gemodule.h" #include "core/fxge/cfx_glyphcache.h" #include "core/fxge/cfx_renderdevice.h" @@ -280,6 +281,9 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_DestroyLibrary() { CFX_GlyphCache::DestroyGlobals(); #endif + // EmbedPDF: registered runtime fonts are global/TLS-backed PDFium state, so + // tear them down with the rest of the library singletons. + CFX_FontRegistry::DestroyGlobals(); pdfium::DestroyPageModule(); CFX_GEModule::Destroy(); CFX_Timer::DestroyGlobals(); @@ -1687,7 +1691,10 @@ FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, } #endif // PDF_ENABLE_XFA - RetainPtr dict = doc->GetMutablePageDictionary(page_index); + RetainPtr const_dict = + doc->GetPageDictionary(page_index); + RetainPtr dict = + pdfium::WrapRetain(const_cast(const_dict.Get())); if (!dict) { return false; } diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index cc3ae907c3..8203adc8f2 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -9,6 +9,7 @@ #include "fpdfsdk/fpdf_view_c_api_test.h" +#include "public/epdf_font.h" #include "public/fpdf_annot.h" #include "public/fpdf_attachment.h" #include "public/fpdf_catalog.h" @@ -33,559 +34,581 @@ #include "public/fpdfview.h" // Scheme for avoiding LTO out of existence, warnings, etc. -typedef void (*fnptr)(void); // Legal generic function type for casts. +typedef void (*fnptr)(void); // Legal generic function type for casts. fnptr g_c_api_test_fnptr = NULL; // Extern, so can't know it doesn't change. -#define CHK(x) if ((fnptr)(x) == g_c_api_test_fnptr) return 0 +#define CHK(x) \ + if ((fnptr)(x) == g_c_api_test_fnptr) \ + return 0 // Function to call from gtest harness to ensure linker resolution. int CheckPDFiumCApi() { - // fpdf_annot.h - CHK(FPDFAnnot_AddFileAttachment); - CHK(FPDFAnnot_AddInkStroke); - CHK(FPDFAnnot_AppendAttachmentPoints); - CHK(FPDFAnnot_AppendObject); - CHK(FPDFAnnot_CountAttachmentPoints); - CHK(FPDFAnnot_GetAP); - CHK(FPDFAnnot_GetAttachmentPoints); - CHK(FPDFAnnot_GetBorder); - CHK(FPDFAnnot_GetColor); - CHK(FPDFAnnot_GetFileAttachment); - CHK(FPDFAnnot_GetFlags); - CHK(FPDFAnnot_GetFocusableSubtypes); - CHK(FPDFAnnot_GetFocusableSubtypesCount); - CHK(FPDFAnnot_GetFontColor); - CHK(FPDFAnnot_GetFontSize); - CHK(FPDFAnnot_GetFormAdditionalActionJavaScript); - CHK(FPDFAnnot_GetFormControlCount); - CHK(FPDFAnnot_GetFormControlIndex); - CHK(FPDFAnnot_GetFormFieldAlternateName); - CHK(FPDFAnnot_GetFormFieldAtPoint); - CHK(FPDFAnnot_GetFormFieldExportValue); - CHK(FPDFAnnot_GetFormFieldFlags); - CHK(FPDFAnnot_GetFormFieldName); - CHK(FPDFAnnot_GetFormFieldType); - CHK(FPDFAnnot_GetFormFieldValue); - CHK(FPDFAnnot_GetInkListCount); - CHK(FPDFAnnot_GetInkListPath); - CHK(FPDFAnnot_GetLine); - CHK(FPDFAnnot_GetLink); - CHK(FPDFAnnot_GetLinkedAnnot); - CHK(FPDFAnnot_GetNumberValue); - CHK(FPDFAnnot_GetObject); - CHK(FPDFAnnot_GetObjectCount); - CHK(FPDFAnnot_GetOptionCount); - CHK(FPDFAnnot_GetOptionLabel); - CHK(FPDFAnnot_GetRect); - CHK(FPDFAnnot_GetStringValue); - CHK(FPDFAnnot_GetSubtype); - CHK(FPDFAnnot_GetValueType); - CHK(FPDFAnnot_GetVertices); - CHK(FPDFAnnot_HasAttachmentPoints); - CHK(FPDFAnnot_HasKey); - CHK(FPDFAnnot_IsChecked); - CHK(FPDFAnnot_IsObjectSupportedSubtype); - CHK(FPDFAnnot_IsOptionSelected); - CHK(FPDFAnnot_IsSupportedSubtype); - CHK(FPDFAnnot_RemoveInkList); - CHK(FPDFAnnot_RemoveObject); - CHK(FPDFAnnot_SetAP); - CHK(FPDFAnnot_SetAttachmentPoints); - CHK(FPDFAnnot_SetBorder); - CHK(FPDFAnnot_SetColor); - CHK(FPDFAnnot_SetFlags); - CHK(FPDFAnnot_SetFocusableSubtypes); - CHK(FPDFAnnot_SetFontColor); - CHK(FPDFAnnot_SetFormFieldFlags); - CHK(FPDFAnnot_SetRect); - CHK(FPDFAnnot_SetStringValue); - CHK(FPDFAnnot_SetURI); - CHK(FPDFAnnot_UpdateObject); - CHK(FPDFPage_CloseAnnot); - CHK(FPDFPage_CreateAnnot); - CHK(FPDFPage_GetAnnot); - CHK(FPDFPage_GetAnnotCount); - CHK(FPDFPage_GetAnnotIndex); - CHK(FPDFPage_RemoveAnnot); + // fpdf_annot.h + CHK(FPDFAnnot_AddFileAttachment); + CHK(FPDFAnnot_AddInkStroke); + CHK(FPDFAnnot_AppendAttachmentPoints); + CHK(FPDFAnnot_AppendObject); + CHK(FPDFAnnot_CountAttachmentPoints); + CHK(FPDFAnnot_GetAP); + CHK(FPDFAnnot_GetAttachmentPoints); + CHK(FPDFAnnot_GetBorder); + CHK(FPDFAnnot_GetColor); + CHK(FPDFAnnot_GetFileAttachment); + CHK(FPDFAnnot_GetFlags); + CHK(FPDFAnnot_GetFocusableSubtypes); + CHK(FPDFAnnot_GetFocusableSubtypesCount); + CHK(FPDFAnnot_GetFontColor); + CHK(FPDFAnnot_GetFontSize); + CHK(FPDFAnnot_GetFormAdditionalActionJavaScript); + CHK(FPDFAnnot_GetFormControlCount); + CHK(FPDFAnnot_GetFormControlIndex); + CHK(FPDFAnnot_GetFormFieldAlternateName); + CHK(FPDFAnnot_GetFormFieldAtPoint); + CHK(FPDFAnnot_GetFormFieldExportValue); + CHK(FPDFAnnot_GetFormFieldFlags); + CHK(FPDFAnnot_GetFormFieldName); + CHK(FPDFAnnot_GetFormFieldType); + CHK(FPDFAnnot_GetFormFieldValue); + CHK(FPDFAnnot_GetInkListCount); + CHK(FPDFAnnot_GetInkListPath); + CHK(FPDFAnnot_GetLine); + CHK(FPDFAnnot_GetLink); + CHK(FPDFAnnot_GetLinkedAnnot); + CHK(FPDFAnnot_GetNumberValue); + CHK(FPDFAnnot_GetObject); + CHK(FPDFAnnot_GetObjectCount); + CHK(FPDFAnnot_GetOptionCount); + CHK(FPDFAnnot_GetOptionLabel); + CHK(FPDFAnnot_GetRect); + CHK(FPDFAnnot_GetStringValue); + CHK(FPDFAnnot_GetSubtype); + CHK(FPDFAnnot_GetValueType); + CHK(FPDFAnnot_GetVertices); + CHK(FPDFAnnot_HasAttachmentPoints); + CHK(FPDFAnnot_HasKey); + CHK(FPDFAnnot_IsChecked); + CHK(FPDFAnnot_IsObjectSupportedSubtype); + CHK(FPDFAnnot_IsOptionSelected); + CHK(FPDFAnnot_IsSupportedSubtype); + CHK(FPDFAnnot_RemoveInkList); + CHK(FPDFAnnot_RemoveObject); + CHK(FPDFAnnot_SetAP); + CHK(FPDFAnnot_SetAttachmentPoints); + CHK(FPDFAnnot_SetBorder); + CHK(FPDFAnnot_SetColor); + CHK(FPDFAnnot_SetFlags); + CHK(FPDFAnnot_SetFocusableSubtypes); + CHK(FPDFAnnot_SetFontColor); + CHK(FPDFAnnot_SetFormFieldFlags); + CHK(FPDFAnnot_SetRect); + CHK(FPDFAnnot_SetStringValue); + CHK(FPDFAnnot_SetURI); + CHK(FPDFAnnot_UpdateObject); + CHK(FPDFPage_CloseAnnot); + CHK(FPDFPage_CreateAnnot); + CHK(FPDFPage_GetAnnot); + CHK(FPDFPage_GetAnnotCount); + CHK(FPDFPage_GetAnnotIndex); + CHK(FPDFPage_RemoveAnnot); + CHK(EPDFAnnot_SetDefaultAppearanceRegisteredFont); - // fpdf_attachment.h - CHK(FPDFAttachment_GetFile); - CHK(FPDFAttachment_GetName); - CHK(FPDFAttachment_GetStringValue); - CHK(FPDFAttachment_GetSubtype); - CHK(FPDFAttachment_GetValueType); - CHK(FPDFAttachment_HasKey); - CHK(FPDFAttachment_SetFile); - CHK(FPDFAttachment_SetStringValue); - CHK(FPDFDoc_AddAttachment); - CHK(FPDFDoc_DeleteAttachment); - CHK(FPDFDoc_GetAttachment); - CHK(FPDFDoc_GetAttachmentCount); + // epdf_font.h + CHK(EPDFFont_AddFallbackFont); + CHK(EPDFFont_ClearFallbackFonts); + CHK(EPDFFont_ClearRegisteredFonts); + CHK(EPDFFont_RegisterFont); + CHK(EPDFFont_RegisterMemFont); + CHK(EPDFFont_RegisterMemFont64); - // fpdf_catalog.h - CHK(FPDFCatalog_GetLanguage); - CHK(FPDFCatalog_IsTagged); - CHK(FPDFCatalog_SetLanguage); + // fpdf_attachment.h + CHK(EPDFAttachment_ExtractFile); + CHK(EPDFAttachment_ExtractFileToOwnedBuffer); + CHK(EPDFAttachment_GetDescription); + CHK(EPDFAttachment_GetIntegerValue); + CHK(EPDFAttachment_SetDescription); + CHK(EPDFAttachment_SetSubtype); + CHK(EPDFDoc_GetAttachmentIndexByKey); + CHK(EPDFDoc_GetAttachmentKey); + CHK(FPDFAttachment_GetFile); + CHK(FPDFAttachment_GetName); + CHK(FPDFAttachment_GetStringValue); + CHK(FPDFAttachment_GetSubtype); + CHK(FPDFAttachment_GetValueType); + CHK(FPDFAttachment_HasKey); + CHK(FPDFAttachment_SetFile); + CHK(FPDFAttachment_SetStringValue); + CHK(FPDFDoc_AddAttachment); + CHK(FPDFDoc_DeleteAttachment); + CHK(FPDFDoc_GetAttachment); + CHK(FPDFDoc_GetAttachmentCount); - // fpdf_dataavail.h - CHK(FPDFAvail_Create); - CHK(FPDFAvail_Destroy); - CHK(FPDFAvail_GetDocument); - CHK(FPDFAvail_GetFirstPageNum); - CHK(FPDFAvail_IsDocAvail); - CHK(FPDFAvail_IsFormAvail); - CHK(FPDFAvail_IsLinearized); - CHK(FPDFAvail_IsPageAvail); + // fpdf_catalog.h + CHK(FPDFCatalog_GetLanguage); + CHK(FPDFCatalog_IsTagged); + CHK(FPDFCatalog_SetLanguage); - // fpdf_doc.h - CHK(FPDFAction_GetDest); - CHK(FPDFAction_GetFilePath); - CHK(FPDFAction_GetType); - CHK(FPDFAction_GetURIPath); - CHK(FPDFBookmark_Find); - CHK(FPDFBookmark_GetAction); - CHK(FPDFBookmark_GetCount); - CHK(FPDFBookmark_GetDest); - CHK(FPDFBookmark_GetFirstChild); - CHK(FPDFBookmark_GetNextSibling); - CHK(FPDFBookmark_GetTitle); - CHK(FPDFDest_GetDestPageIndex); - CHK(FPDFDest_GetLocationInPage); - CHK(FPDFDest_GetView); - CHK(FPDFLink_CountQuadPoints); - CHK(FPDFLink_Enumerate); - CHK(FPDFLink_GetAction); - CHK(FPDFLink_GetAnnot); - CHK(FPDFLink_GetAnnotRect); - CHK(FPDFLink_GetDest); - CHK(FPDFLink_GetLinkAtPoint); - CHK(FPDFLink_GetLinkZOrderAtPoint); - CHK(FPDFLink_GetQuadPoints); - CHK(FPDF_GetFileIdentifier); - CHK(FPDF_GetMetaText); - CHK(FPDF_GetPageAAction); - CHK(FPDF_GetPageLabel); + // fpdf_dataavail.h + CHK(FPDFAvail_Create); + CHK(FPDFAvail_Destroy); + CHK(FPDFAvail_GetDocument); + CHK(FPDFAvail_GetFirstPageNum); + CHK(FPDFAvail_IsDocAvail); + CHK(FPDFAvail_IsFormAvail); + CHK(FPDFAvail_IsLinearized); + CHK(FPDFAvail_IsPageAvail); - // fpdf_edit.h - CHK(FPDFFont_Close); - CHK(FPDFFont_GetAscent); - CHK(FPDFFont_GetBaseFontName); - CHK(FPDFFont_GetDescent); - CHK(FPDFFont_GetFamilyName); - CHK(FPDFFont_GetFlags); - CHK(FPDFFont_GetFontData); - CHK(FPDFFont_GetGlyphPath); - CHK(FPDFFont_GetGlyphWidth); - CHK(FPDFFont_GetIsEmbedded); - CHK(FPDFFont_GetItalicAngle); - CHK(FPDFFont_GetWeight); - CHK(FPDFFormObj_CountObjects); - CHK(FPDFFormObj_GetObject); - CHK(FPDFFormObj_RemoveObject); - CHK(FPDFGlyphPath_CountGlyphSegments); - CHK(FPDFGlyphPath_GetGlyphPathSegment); - CHK(FPDFImageObj_GetBitmap); - CHK(FPDFImageObj_GetIccProfileDataDecoded); - CHK(FPDFImageObj_GetImageDataDecoded); - CHK(FPDFImageObj_GetImageDataRaw); - CHK(FPDFImageObj_GetImageFilter); - CHK(FPDFImageObj_GetImageFilterCount); - CHK(FPDFImageObj_GetImageMetadata); - CHK(FPDFImageObj_GetImagePixelSize); - CHK(FPDFImageObj_GetRenderedBitmap); - CHK(FPDFImageObj_LoadJpegFile); - CHK(FPDFImageObj_LoadJpegFileInline); - CHK(FPDFImageObj_SetBitmap); - CHK(FPDFImageObj_SetMatrix); - CHK(FPDFPageObjMark_CountParams); - CHK(FPDFPageObjMark_GetName); - CHK(FPDFPageObjMark_GetParamBlobValue); - CHK(FPDFPageObjMark_GetParamFloatValue); - CHK(FPDFPageObjMark_GetParamIntValue); - CHK(FPDFPageObjMark_GetParamKey); - CHK(FPDFPageObjMark_GetParamStringValue); - CHK(FPDFPageObjMark_GetParamValueType); - CHK(FPDFPageObjMark_RemoveParam); - CHK(FPDFPageObjMark_SetBlobParam); - CHK(FPDFPageObjMark_SetFloatParam); - CHK(FPDFPageObjMark_SetIntParam); - CHK(FPDFPageObjMark_SetStringParam); - CHK(FPDFPageObj_AddMark); - CHK(FPDFPageObj_CountMarks); - CHK(FPDFPageObj_CreateNewPath); - CHK(FPDFPageObj_CreateNewRect); - CHK(FPDFPageObj_CreateTextObj); - CHK(FPDFPageObj_Destroy); - CHK(FPDFPageObj_GetBounds); - CHK(FPDFPageObj_GetDashArray); - CHK(FPDFPageObj_GetDashCount); - CHK(FPDFPageObj_GetDashPhase); - CHK(FPDFPageObj_GetFillColor); - CHK(FPDFPageObj_GetIsActive); - CHK(FPDFPageObj_GetLineCap); - CHK(FPDFPageObj_GetLineJoin); - CHK(FPDFPageObj_GetMark); - CHK(FPDFPageObj_GetMarkedContentID); - CHK(FPDFPageObj_GetMatrix); - CHK(FPDFPageObj_GetRotatedBounds); - CHK(FPDFPageObj_GetStrokeColor); - CHK(FPDFPageObj_GetStrokeWidth); - CHK(FPDFPageObj_GetType); - CHK(FPDFPageObj_HasTransparency); - CHK(FPDFPageObj_NewImageObj); - CHK(FPDFPageObj_NewTextObj); - CHK(FPDFPageObj_RemoveMark); - CHK(FPDFPageObj_SetBlendMode); - CHK(FPDFPageObj_SetDashArray); - CHK(FPDFPageObj_SetDashPhase); - CHK(FPDFPageObj_SetFillColor); - CHK(FPDFPageObj_SetIsActive); - CHK(FPDFPageObj_SetLineCap); - CHK(FPDFPageObj_SetLineJoin); - CHK(FPDFPageObj_SetMatrix); - CHK(FPDFPageObj_SetStrokeColor); - CHK(FPDFPageObj_SetStrokeWidth); - CHK(FPDFPageObj_Transform); - CHK(FPDFPageObj_TransformF); - CHK(FPDFPage_CountObjects); - CHK(FPDFPage_Delete); - CHK(FPDFPage_GenerateContent); - CHK(FPDFPage_GetObject); - CHK(FPDFPage_GetRotation); - CHK(FPDFPage_HasTransparency); - CHK(FPDFPage_InsertObject); - CHK(FPDFPage_InsertObjectAtIndex); - CHK(FPDFPage_New); - CHK(FPDFPage_RemoveObject); - CHK(FPDFPage_SetRotation); - CHK(FPDFPage_TransformAnnots); - CHK(FPDFPathSegment_GetClose); - CHK(FPDFPathSegment_GetPoint); - CHK(FPDFPathSegment_GetType); - CHK(FPDFPath_BezierTo); - CHK(FPDFPath_Close); - CHK(FPDFPath_CountSegments); - CHK(FPDFPath_GetDrawMode); - CHK(FPDFPath_GetPathSegment); - CHK(FPDFPath_LineTo); - CHK(FPDFPath_MoveTo); - CHK(FPDFPath_SetDrawMode); - CHK(FPDFTextObj_GetFont); - CHK(FPDFTextObj_GetFontSize); - CHK(FPDFTextObj_GetRenderedBitmap); - CHK(FPDFTextObj_GetText); - CHK(FPDFTextObj_GetTextRenderMode); - CHK(FPDFTextObj_SetTextRenderMode); - CHK(FPDFText_LoadCidType2Font); - CHK(FPDFText_LoadFont); - CHK(FPDFText_LoadStandardFont); - CHK(FPDFText_SetCharcodes); - CHK(FPDFText_SetText); - CHK(FPDF_CreateNewDocument); - CHK(FPDF_MovePages); + // fpdf_doc.h + CHK(FPDFAction_GetDest); + CHK(FPDFAction_GetFilePath); + CHK(FPDFAction_GetType); + CHK(FPDFAction_GetURIPath); + CHK(FPDFBookmark_Find); + CHK(FPDFBookmark_GetAction); + CHK(FPDFBookmark_GetCount); + CHK(FPDFBookmark_GetDest); + CHK(FPDFBookmark_GetFirstChild); + CHK(FPDFBookmark_GetNextSibling); + CHK(FPDFBookmark_GetTitle); + CHK(EPDFDest_GetPageObjectNumber); + CHK(FPDFDest_GetDestPageIndex); + CHK(FPDFDest_GetLocationInPage); + CHK(FPDFDest_GetView); + CHK(FPDFLink_CountQuadPoints); + CHK(FPDFLink_Enumerate); + CHK(FPDFLink_GetAction); + CHK(FPDFLink_GetAnnot); + CHK(FPDFLink_GetAnnotRect); + CHK(FPDFLink_GetDest); + CHK(FPDFLink_GetLinkAtPoint); + CHK(FPDFLink_GetLinkZOrderAtPoint); + CHK(FPDFLink_GetQuadPoints); + CHK(FPDF_GetFileIdentifier); + CHK(FPDF_GetMetaText); + CHK(FPDF_GetPageAAction); + CHK(FPDF_GetPageLabel); - // fpdf_ext.h - CHK(FPDFDoc_GetPageMode); - CHK(FSDK_SetLocaltimeFunction); - CHK(FSDK_SetTimeFunction); - CHK(FSDK_SetUnSpObjProcessHandler); + // fpdf_edit.h + CHK(FPDFFont_Close); + CHK(FPDFFont_GetAscent); + CHK(FPDFFont_GetBaseFontName); + CHK(FPDFFont_GetDescent); + CHK(FPDFFont_GetFamilyName); + CHK(FPDFFont_GetFlags); + CHK(FPDFFont_GetFontData); + CHK(FPDFFont_GetGlyphPath); + CHK(FPDFFont_GetGlyphWidth); + CHK(FPDFFont_GetIsEmbedded); + CHK(FPDFFont_GetItalicAngle); + CHK(FPDFFont_GetWeight); + CHK(FPDFFormObj_CountObjects); + CHK(FPDFFormObj_GetObject); + CHK(FPDFFormObj_RemoveObject); + CHK(FPDFGlyphPath_CountGlyphSegments); + CHK(FPDFGlyphPath_GetGlyphPathSegment); + CHK(FPDFImageObj_GetBitmap); + CHK(FPDFImageObj_GetIccProfileDataDecoded); + CHK(FPDFImageObj_GetImageDataDecoded); + CHK(FPDFImageObj_GetImageDataRaw); + CHK(FPDFImageObj_GetImageFilter); + CHK(FPDFImageObj_GetImageFilterCount); + CHK(FPDFImageObj_GetImageMetadata); + CHK(FPDFImageObj_GetImagePixelSize); + CHK(FPDFImageObj_GetRenderedBitmap); + CHK(FPDFImageObj_LoadJpegFile); + CHK(FPDFImageObj_LoadJpegFileInline); + CHK(FPDFImageObj_SetBitmap); + CHK(FPDFImageObj_SetMatrix); + CHK(FPDFPageObjMark_CountParams); + CHK(FPDFPageObjMark_GetName); + CHK(FPDFPageObjMark_GetParamBlobValue); + CHK(FPDFPageObjMark_GetParamFloatValue); + CHK(FPDFPageObjMark_GetParamIntValue); + CHK(FPDFPageObjMark_GetParamKey); + CHK(FPDFPageObjMark_GetParamStringValue); + CHK(FPDFPageObjMark_GetParamValueType); + CHK(FPDFPageObjMark_RemoveParam); + CHK(FPDFPageObjMark_SetBlobParam); + CHK(FPDFPageObjMark_SetFloatParam); + CHK(FPDFPageObjMark_SetIntParam); + CHK(FPDFPageObjMark_SetStringParam); + CHK(FPDFPageObj_AddMark); + CHK(FPDFPageObj_CountMarks); + CHK(FPDFPageObj_CreateNewPath); + CHK(FPDFPageObj_CreateNewRect); + CHK(FPDFPageObj_CreateTextObj); + CHK(FPDFPageObj_Destroy); + CHK(FPDFPageObj_GetBounds); + CHK(FPDFPageObj_GetDashArray); + CHK(FPDFPageObj_GetDashCount); + CHK(FPDFPageObj_GetDashPhase); + CHK(FPDFPageObj_GetFillColor); + CHK(FPDFPageObj_GetIsActive); + CHK(FPDFPageObj_GetLineCap); + CHK(FPDFPageObj_GetLineJoin); + CHK(FPDFPageObj_GetMark); + CHK(FPDFPageObj_GetMarkedContentID); + CHK(FPDFPageObj_GetMatrix); + CHK(FPDFPageObj_GetRotatedBounds); + CHK(FPDFPageObj_GetStrokeColor); + CHK(FPDFPageObj_GetStrokeWidth); + CHK(FPDFPageObj_GetType); + CHK(FPDFPageObj_HasTransparency); + CHK(FPDFPageObj_NewImageObj); + CHK(FPDFPageObj_NewTextObj); + CHK(FPDFPageObj_RemoveMark); + CHK(FPDFPageObj_SetBlendMode); + CHK(FPDFPageObj_SetDashArray); + CHK(FPDFPageObj_SetDashPhase); + CHK(FPDFPageObj_SetFillColor); + CHK(FPDFPageObj_SetIsActive); + CHK(FPDFPageObj_SetLineCap); + CHK(FPDFPageObj_SetLineJoin); + CHK(FPDFPageObj_SetMatrix); + CHK(FPDFPageObj_SetStrokeColor); + CHK(FPDFPageObj_SetStrokeWidth); + CHK(FPDFPageObj_Transform); + CHK(FPDFPageObj_TransformF); + CHK(FPDFPage_CountObjects); + CHK(FPDFPage_Delete); + CHK(FPDFPage_GenerateContent); + CHK(FPDFPage_GetObject); + CHK(FPDFPage_GetRotation); + CHK(FPDFPage_HasTransparency); + CHK(FPDFPage_InsertObject); + CHK(FPDFPage_InsertObjectAtIndex); + CHK(FPDFPage_New); + CHK(FPDFPage_RemoveObject); + CHK(FPDFPage_SetRotation); + CHK(FPDFPage_TransformAnnots); + CHK(FPDFPathSegment_GetClose); + CHK(FPDFPathSegment_GetPoint); + CHK(FPDFPathSegment_GetType); + CHK(FPDFPath_BezierTo); + CHK(FPDFPath_Close); + CHK(FPDFPath_CountSegments); + CHK(FPDFPath_GetDrawMode); + CHK(FPDFPath_GetPathSegment); + CHK(FPDFPath_LineTo); + CHK(FPDFPath_MoveTo); + CHK(FPDFPath_SetDrawMode); + CHK(FPDFTextObj_GetFont); + CHK(FPDFTextObj_GetFontSize); + CHK(FPDFTextObj_GetRenderedBitmap); + CHK(FPDFTextObj_GetText); + CHK(FPDFTextObj_GetTextRenderMode); + CHK(FPDFTextObj_SetTextRenderMode); + CHK(FPDFText_LoadCidType2Font); + CHK(FPDFText_LoadFont); + CHK(FPDFText_LoadStandardFont); + CHK(FPDFText_SetCharcodes); + CHK(FPDFText_SetText); + CHK(FPDF_CreateNewDocument); + CHK(FPDF_MovePages); - // fpdf_flatten.h - CHK(FPDFPage_Flatten); + // fpdf_ext.h + CHK(FPDFDoc_GetPageMode); + CHK(FSDK_SetLocaltimeFunction); + CHK(FSDK_SetTimeFunction); + CHK(FSDK_SetUnSpObjProcessHandler); - // fpdf_fwlevent.h - no exports. + // fpdf_flatten.h + CHK(EPDFAnnot_Flatten); + CHK(EPDFPage_Flatten); + CHK(FPDFPage_Flatten); - // fpdf_formfill.h - CHK(FORM_CanRedo); - CHK(FORM_CanUndo); - CHK(FORM_DoDocumentAAction); - CHK(FORM_DoDocumentJSAction); - CHK(FORM_DoDocumentOpenAction); - CHK(FORM_DoPageAAction); - CHK(FORM_ForceToKillFocus); - CHK(FORM_GetFocusedAnnot); - CHK(FORM_GetFocusedText); - CHK(FORM_GetSelectedText); - CHK(FORM_IsIndexSelected); - CHK(FORM_OnAfterLoadPage); - CHK(FORM_OnBeforeClosePage); - CHK(FORM_OnChar); - CHK(FORM_OnFocus); - CHK(FORM_OnKeyDown); - CHK(FORM_OnKeyUp); - CHK(FORM_OnLButtonDoubleClick); - CHK(FORM_OnLButtonDown); - CHK(FORM_OnLButtonUp); - CHK(FORM_OnMouseMove); - CHK(FORM_OnMouseWheel); - CHK(FORM_OnRButtonDown); - CHK(FORM_OnRButtonUp); - CHK(FORM_Redo); - CHK(FORM_ReplaceAndKeepSelection); - CHK(FORM_ReplaceSelection); - CHK(FORM_SelectAllText); - CHK(FORM_SetFocusedAnnot); - CHK(FORM_SetIndexSelected); - CHK(FORM_Undo); - CHK(FPDFDOC_ExitFormFillEnvironment); - CHK(FPDFDOC_InitFormFillEnvironment); - CHK(FPDFPage_FormFieldZOrderAtPoint); - CHK(FPDFPage_HasFormFieldAtPoint); - CHK(FPDF_FFLDraw); + // fpdf_fwlevent.h - no exports. + + // fpdf_formfill.h + CHK(FORM_CanRedo); + CHK(FORM_CanUndo); + CHK(FORM_DoDocumentAAction); + CHK(FORM_DoDocumentJSAction); + CHK(FORM_DoDocumentOpenAction); + CHK(FORM_DoPageAAction); + CHK(FORM_ForceToKillFocus); + CHK(FORM_GetFocusedAnnot); + CHK(FORM_GetFocusedText); + CHK(FORM_GetSelectedText); + CHK(FORM_IsIndexSelected); + CHK(FORM_OnAfterLoadPage); + CHK(FORM_OnBeforeClosePage); + CHK(FORM_OnChar); + CHK(FORM_OnFocus); + CHK(FORM_OnKeyDown); + CHK(FORM_OnKeyUp); + CHK(FORM_OnLButtonDoubleClick); + CHK(FORM_OnLButtonDown); + CHK(FORM_OnLButtonUp); + CHK(FORM_OnMouseMove); + CHK(FORM_OnMouseWheel); + CHK(FORM_OnRButtonDown); + CHK(FORM_OnRButtonUp); + CHK(FORM_Redo); + CHK(FORM_ReplaceAndKeepSelection); + CHK(FORM_ReplaceSelection); + CHK(FORM_SelectAllText); + CHK(FORM_SetFocusedAnnot); + CHK(FORM_SetIndexSelected); + CHK(FORM_Undo); + CHK(FPDFDOC_ExitFormFillEnvironment); + CHK(FPDFDOC_InitFormFillEnvironment); + CHK(FPDFPage_FormFieldZOrderAtPoint); + CHK(FPDFPage_HasFormFieldAtPoint); + CHK(FPDF_FFLDraw); #if defined(PDF_USE_SKIA) - CHK(FPDF_FFLDrawSkia); + CHK(FPDF_FFLDrawSkia); #endif - CHK(FPDF_GetFormType); - CHK(FPDF_LoadXFA); - CHK(FPDF_RemoveFormFieldHighlight); - CHK(FPDF_SetFormFieldHighlightAlpha); - CHK(FPDF_SetFormFieldHighlightColor); + CHK(FPDF_GetFormType); + CHK(FPDF_LoadXFA); + CHK(FPDF_RemoveFormFieldHighlight); + CHK(FPDF_SetFormFieldHighlightAlpha); + CHK(FPDF_SetFormFieldHighlightColor); - // fpdf_javascript.h - CHK(FPDFDoc_CloseJavaScriptAction); - CHK(FPDFDoc_GetJavaScriptAction); - CHK(FPDFDoc_GetJavaScriptActionCount); - CHK(FPDFJavaScriptAction_GetName); - CHK(FPDFJavaScriptAction_GetScript); + // fpdf_javascript.h + CHK(FPDFDoc_CloseJavaScriptAction); + CHK(FPDFDoc_GetJavaScriptAction); + CHK(FPDFDoc_GetJavaScriptActionCount); + CHK(FPDFJavaScriptAction_GetName); + CHK(FPDFJavaScriptAction_GetScript); - // fpdf_ppo.h - CHK(FPDF_CloseXObject); - CHK(FPDF_CopyViewerPreferences); - CHK(FPDF_ImportNPagesToOne); - CHK(FPDF_ImportPages); - CHK(FPDF_ImportPagesByIndex); - CHK(FPDF_NewFormObjectFromXObject); - CHK(FPDF_NewXObjectFromPage); + // fpdf_ppo.h + CHK(FPDF_CloseXObject); + CHK(FPDF_CopyViewerPreferences); + CHK(FPDF_ImportNPagesToOne); + CHK(FPDF_ImportPages); + CHK(FPDF_ImportPagesByIndex); + CHK(FPDF_NewFormObjectFromXObject); + CHK(FPDF_NewXObjectFromPage); - // fpdf_progressive.h - CHK(FPDF_RenderPageBitmapWithColorScheme_Start); - CHK(FPDF_RenderPageBitmap_Start); - CHK(FPDF_RenderPage_Close); - CHK(FPDF_RenderPage_Continue); + // fpdf_progressive.h + CHK(FPDF_RenderPageBitmapWithColorScheme_Start); + CHK(FPDF_RenderPageBitmap_Start); + CHK(FPDF_RenderPage_Close); + CHK(FPDF_RenderPage_Continue); - // fpdf_save.h - CHK(FPDF_SaveAsCopy); - CHK(FPDF_SaveWithVersion); + // fpdf_save.h + CHK(FPDF_SaveAsCopy); + CHK(FPDF_SaveWithVersion); - // fpdf_searchex.h - CHK(FPDFText_GetCharIndexFromTextIndex); - CHK(FPDFText_GetTextIndexFromCharIndex); + // fpdf_searchex.h + CHK(FPDFText_GetCharIndexFromTextIndex); + CHK(FPDFText_GetTextIndexFromCharIndex); - // fpdf_signature.h - CHK(FPDFSignatureObj_GetByteRange); - CHK(FPDFSignatureObj_GetContents); - CHK(FPDFSignatureObj_GetDocMDPPermission); - CHK(FPDFSignatureObj_GetReason); - CHK(FPDFSignatureObj_GetSubFilter); - CHK(FPDFSignatureObj_GetTime); - CHK(FPDF_GetSignatureCount); - CHK(FPDF_GetSignatureObject); + // fpdf_signature.h + CHK(FPDFSignatureObj_GetByteRange); + CHK(FPDFSignatureObj_GetContents); + CHK(FPDFSignatureObj_GetDocMDPPermission); + CHK(FPDFSignatureObj_GetReason); + CHK(FPDFSignatureObj_GetSubFilter); + CHK(FPDFSignatureObj_GetTime); + CHK(FPDF_GetSignatureCount); + CHK(FPDF_GetSignatureObject); - // fpdf_structtree.h - CHK(FPDF_StructElement_Attr_CountChildren); - CHK(FPDF_StructElement_Attr_GetBlobValue); - CHK(FPDF_StructElement_Attr_GetBooleanValue); - CHK(FPDF_StructElement_Attr_GetChildAtIndex); - CHK(FPDF_StructElement_Attr_GetCount); - CHK(FPDF_StructElement_Attr_GetName); - CHK(FPDF_StructElement_Attr_GetNumberValue); - CHK(FPDF_StructElement_Attr_GetStringValue); - CHK(FPDF_StructElement_Attr_GetType); - CHK(FPDF_StructElement_Attr_GetValue); - CHK(FPDF_StructElement_CountChildren); - CHK(FPDF_StructElement_GetActualText); - CHK(FPDF_StructElement_GetAltText); - CHK(FPDF_StructElement_GetAttributeAtIndex); - CHK(FPDF_StructElement_GetAttributeCount); - CHK(FPDF_StructElement_GetChildAtIndex); - CHK(FPDF_StructElement_GetChildMarkedContentID); - CHK(FPDF_StructElement_GetID); - CHK(FPDF_StructElement_GetLang); - CHK(FPDF_StructElement_GetMarkedContentID); - CHK(FPDF_StructElement_GetMarkedContentIdAtIndex); - CHK(FPDF_StructElement_GetMarkedContentIdCount); - CHK(FPDF_StructElement_GetObjType); - CHK(FPDF_StructElement_GetParent); - CHK(FPDF_StructElement_GetStringAttribute); - CHK(FPDF_StructElement_GetTitle); - CHK(FPDF_StructElement_GetType); - CHK(FPDF_StructTree_Close); - CHK(FPDF_StructTree_CountChildren); - CHK(FPDF_StructTree_GetChildAtIndex); - CHK(FPDF_StructTree_GetForPage); + // fpdf_structtree.h + CHK(FPDF_StructElement_Attr_CountChildren); + CHK(FPDF_StructElement_Attr_GetBlobValue); + CHK(FPDF_StructElement_Attr_GetBooleanValue); + CHK(FPDF_StructElement_Attr_GetChildAtIndex); + CHK(FPDF_StructElement_Attr_GetCount); + CHK(FPDF_StructElement_Attr_GetName); + CHK(FPDF_StructElement_Attr_GetNumberValue); + CHK(FPDF_StructElement_Attr_GetStringValue); + CHK(FPDF_StructElement_Attr_GetType); + CHK(FPDF_StructElement_Attr_GetValue); + CHK(FPDF_StructElement_CountChildren); + CHK(FPDF_StructElement_GetActualText); + CHK(FPDF_StructElement_GetAltText); + CHK(FPDF_StructElement_GetAttributeAtIndex); + CHK(FPDF_StructElement_GetAttributeCount); + CHK(FPDF_StructElement_GetChildAtIndex); + CHK(FPDF_StructElement_GetChildMarkedContentID); + CHK(FPDF_StructElement_GetID); + CHK(FPDF_StructElement_GetLang); + CHK(FPDF_StructElement_GetMarkedContentID); + CHK(FPDF_StructElement_GetMarkedContentIdAtIndex); + CHK(FPDF_StructElement_GetMarkedContentIdCount); + CHK(FPDF_StructElement_GetObjType); + CHK(FPDF_StructElement_GetParent); + CHK(FPDF_StructElement_GetStringAttribute); + CHK(FPDF_StructElement_GetTitle); + CHK(FPDF_StructElement_GetType); + CHK(FPDF_StructTree_Close); + CHK(FPDF_StructTree_CountChildren); + CHK(FPDF_StructTree_GetChildAtIndex); + CHK(FPDF_StructTree_GetForPage); - // fpdf_sysfontinfo.h - CHK(FPDF_AddInstalledFont); - CHK(FPDF_FreeDefaultSystemFontInfo); - CHK(FPDF_GetDefaultSystemFontInfo); - CHK(FPDF_GetDefaultTTFMap); - CHK(FPDF_GetDefaultTTFMapCount); - CHK(FPDF_GetDefaultTTFMapEntry); - CHK(FPDF_SetSystemFontInfo); + // fpdf_sysfontinfo.h + CHK(FPDF_AddInstalledFont); + CHK(FPDF_FreeDefaultSystemFontInfo); + CHK(FPDF_GetDefaultSystemFontInfo); + CHK(FPDF_GetDefaultTTFMap); + CHK(FPDF_GetDefaultTTFMapCount); + CHK(FPDF_GetDefaultTTFMapEntry); + CHK(FPDF_SetSystemFontInfo); - // fpdf_text.h - CHK(FPDFLink_CloseWebLinks); - CHK(FPDFLink_CountRects); - CHK(FPDFLink_CountWebLinks); - CHK(FPDFLink_GetRect); - CHK(FPDFLink_GetTextRange); - CHK(FPDFLink_GetURL); - CHK(FPDFLink_LoadWebLinks); - CHK(FPDFText_ClosePage); - CHK(FPDFText_CountChars); - CHK(FPDFText_CountRects); - CHK(FPDFText_FindClose); - CHK(FPDFText_FindNext); - CHK(FPDFText_FindPrev); - CHK(FPDFText_FindStart); - CHK(FPDFText_GetBoundedText); - CHK(FPDFText_GetCharAngle); - CHK(FPDFText_GetCharBox); - CHK(FPDFText_GetCharIndexAtPos); - CHK(FPDFText_GetCharOrigin); - CHK(FPDFText_GetFillColor); - CHK(FPDFText_GetFontInfo); - CHK(FPDFText_GetFontSize); - CHK(FPDFText_GetFontWeight); - CHK(FPDFText_GetLooseCharBox); - CHK(FPDFText_GetMatrix); - CHK(FPDFText_GetRect); - CHK(FPDFText_GetSchCount); - CHK(FPDFText_GetSchResultIndex); - CHK(FPDFText_GetStrokeColor); - CHK(FPDFText_GetText); - CHK(FPDFText_GetTextObject); - CHK(FPDFText_GetUnicode); - CHK(FPDFText_HasUnicodeMapError); - CHK(FPDFText_IsGenerated); - CHK(FPDFText_IsHyphen); - CHK(FPDFText_LoadPage); + // fpdf_text.h + CHK(FPDFLink_CloseWebLinks); + CHK(FPDFLink_CountRects); + CHK(FPDFLink_CountWebLinks); + CHK(FPDFLink_GetRect); + CHK(FPDFLink_GetTextRange); + CHK(FPDFLink_GetURL); + CHK(FPDFLink_LoadWebLinks); + CHK(FPDFText_ClosePage); + CHK(FPDFText_CountChars); + CHK(FPDFText_CountRects); + CHK(FPDFText_FindClose); + CHK(FPDFText_FindNext); + CHK(FPDFText_FindPrev); + CHK(FPDFText_FindStart); + CHK(FPDFText_GetBoundedText); + CHK(FPDFText_GetCharAngle); + CHK(FPDFText_GetCharBox); + CHK(FPDFText_GetCharIndexAtPos); + CHK(FPDFText_GetCharOrigin); + CHK(FPDFText_GetFillColor); + CHK(FPDFText_GetFontInfo); + CHK(FPDFText_GetFontSize); + CHK(FPDFText_GetFontWeight); + CHK(FPDFText_GetLooseCharBox); + CHK(FPDFText_GetMatrix); + CHK(FPDFText_GetRect); + CHK(FPDFText_GetSchCount); + CHK(FPDFText_GetSchResultIndex); + CHK(FPDFText_GetStrokeColor); + CHK(FPDFText_GetText); + CHK(FPDFText_GetTextObject); + CHK(FPDFText_GetUnicode); + CHK(FPDFText_HasUnicodeMapError); + CHK(FPDFText_IsGenerated); + CHK(FPDFText_IsHyphen); + CHK(FPDFText_LoadPage); - // fpdf_thumbnail.h - CHK(FPDFPage_GetDecodedThumbnailData); - CHK(FPDFPage_GetRawThumbnailData); - CHK(FPDFPage_GetThumbnailAsBitmap); + // fpdf_thumbnail.h + CHK(FPDFPage_GetDecodedThumbnailData); + CHK(FPDFPage_GetRawThumbnailData); + CHK(FPDFPage_GetThumbnailAsBitmap); - // fpdf_transformpage.h - CHK(FPDFClipPath_CountPathSegments); - CHK(FPDFClipPath_CountPaths); - CHK(FPDFClipPath_GetPathSegment); - CHK(FPDFPageObj_GetClipPath); - CHK(FPDFPageObj_TransformClipPath); - CHK(FPDFPage_GetArtBox); - CHK(FPDFPage_GetBleedBox); - CHK(FPDFPage_GetCropBox); - CHK(FPDFPage_GetMediaBox); - CHK(FPDFPage_GetTrimBox); - CHK(FPDFPage_InsertClipPath); - CHK(FPDFPage_SetArtBox); - CHK(FPDFPage_SetBleedBox); - CHK(FPDFPage_SetCropBox); - CHK(FPDFPage_SetMediaBox); - CHK(FPDFPage_SetTrimBox); - CHK(FPDFPage_TransFormWithClip); - CHK(FPDF_CreateClipPath); - CHK(FPDF_DestroyClipPath); + // fpdf_transformpage.h + CHK(FPDFClipPath_CountPathSegments); + CHK(FPDFClipPath_CountPaths); + CHK(FPDFClipPath_GetPathSegment); + CHK(FPDFPageObj_GetClipPath); + CHK(FPDFPageObj_TransformClipPath); + CHK(FPDFPage_GetArtBox); + CHK(FPDFPage_GetBleedBox); + CHK(FPDFPage_GetCropBox); + CHK(FPDFPage_GetMediaBox); + CHK(FPDFPage_GetTrimBox); + CHK(FPDFPage_InsertClipPath); + CHK(FPDFPage_SetArtBox); + CHK(FPDFPage_SetBleedBox); + CHK(FPDFPage_SetCropBox); + CHK(FPDFPage_SetMediaBox); + CHK(FPDFPage_SetTrimBox); + CHK(FPDFPage_TransFormWithClip); + CHK(FPDF_CreateClipPath); + CHK(FPDF_DestroyClipPath); - // fpdfview.h - CHK(FPDFBitmap_Create); - CHK(FPDFBitmap_CreateEx); - CHK(FPDFBitmap_Destroy); - CHK(FPDFBitmap_FillRect); - CHK(FPDFBitmap_GetBuffer); - CHK(FPDFBitmap_GetFormat); - CHK(FPDFBitmap_GetHeight); - CHK(FPDFBitmap_GetStride); - CHK(FPDFBitmap_GetWidth); + // fpdfview.h + CHK(FPDFBitmap_Create); + CHK(FPDFBitmap_CreateEx); + CHK(FPDFBitmap_Destroy); + CHK(FPDFBitmap_FillRect); + CHK(FPDFBitmap_GetBuffer); + CHK(FPDFBitmap_GetFormat); + CHK(FPDFBitmap_GetHeight); + CHK(FPDFBitmap_GetStride); + CHK(FPDFBitmap_GetWidth); #ifdef PDF_ENABLE_XFA - CHK(FPDF_BStr_Clear); - CHK(FPDF_BStr_Init); - CHK(FPDF_BStr_Set); + CHK(FPDF_BStr_Clear); + CHK(FPDF_BStr_Init); + CHK(FPDF_BStr_Set); #endif - CHK(FPDF_CloseDocument); - CHK(FPDF_ClosePage); - CHK(FPDF_CountNamedDests); - CHK(FPDF_DestroyLibrary); - CHK(FPDF_DeviceToPage); - CHK(FPDF_DocumentHasValidCrossReferenceTable); + CHK(FPDF_CloseDocument); + CHK(FPDF_ClosePage); + CHK(FPDF_CountNamedDests); + CHK(FPDF_DestroyLibrary); + CHK(FPDF_DeviceToPage); + CHK(FPDF_DocumentHasValidCrossReferenceTable); #ifdef PDF_ENABLE_V8 - CHK(FPDF_GetArrayBufferAllocatorSharedInstance); + CHK(FPDF_GetArrayBufferAllocatorSharedInstance); #endif - CHK(EPDF_CheckPasswordPermissions); - CHK(EPDF_SetRuntimeOwnerPermissions); - CHK(FPDF_GetDocPermissions); - CHK(FPDF_GetDocUserPermissions); - CHK(FPDF_GetFileVersion); - CHK(FPDF_GetLastError); - CHK(FPDF_GetNamedDest); - CHK(FPDF_GetNamedDestByName); - CHK(FPDF_GetPageBoundingBox); - CHK(FPDF_GetPageCount); - CHK(FPDF_GetPageHeight); - CHK(FPDF_GetPageHeightF); - CHK(FPDF_GetPageSizeByIndex); - CHK(FPDF_GetPageSizeByIndexF); - CHK(FPDF_GetPageWidth); - CHK(FPDF_GetPageWidthF); + CHK(EPDF_CheckPasswordPermissions); + CHK(EPDF_SetRuntimeOwnerPermissions); + CHK(FPDF_GetDocPermissions); + CHK(FPDF_GetDocUserPermissions); + CHK(FPDF_GetFileVersion); + CHK(FPDF_GetLastError); + CHK(FPDF_GetNamedDest); + CHK(FPDF_GetNamedDestByName); + CHK(FPDF_GetPageBoundingBox); + CHK(FPDF_GetPageCount); + CHK(FPDF_GetPageHeight); + CHK(FPDF_GetPageHeightF); + CHK(FPDF_GetPageSizeByIndex); + CHK(FPDF_GetPageSizeByIndexF); + CHK(FPDF_GetPageWidth); + CHK(FPDF_GetPageWidthF); #ifdef PDF_ENABLE_V8 - CHK(FPDF_GetRecommendedV8Flags); + CHK(FPDF_GetRecommendedV8Flags); #endif - CHK(FPDF_GetSecurityHandlerRevision); - CHK(FPDF_GetTrailerEnds); - CHK(FPDF_GetXFAPacketContent); - CHK(FPDF_GetXFAPacketCount); - CHK(FPDF_GetXFAPacketName); - CHK(FPDF_InitLibrary); - CHK(FPDF_InitLibraryWithConfig); - CHK(EPDF_InitThread); - CHK(EPDF_ShutdownThread); - CHK(EPDF_GetPageBoxByIndex); - CHK(EPDF_GetPageUserUnitByIndex); - CHK(EPDF_LoadBaseDocument); - CHK(EPDF_LoadMemBaseDocument); - CHK(EPDF_LoadMemBaseDocument64); - CHK(EPDFDoc_DeletePageByObjectNumber); - CHK(EPDFDoc_GetPageObjectNumberByIndex); - CHK(EPDFDoc_SetPageRotationByObjectNumber); - CHK(EPDF_FreeBuffer); - CHK(EPDF_SaveDocumentToOwnedBuffer); - CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); - CHK(EPDFLayer_GetBaseDocument); - CHK(EPDFLayer_GetPromotedObjectCount); - CHK(EPDFLayer_IsObjectPromoted); - CHK(EPDFLayer_OpenLayer); - CHK(EPDFLayer_OpenLayerArtifact); - CHK(EPDFLayer_SaveDelta); - CHK(EPDFLayer_SaveDeltaToOwnedBuffer); - CHK(EPDFLayer_SaveLayerArtifact); - CHK(EPDFLayer_SaveLayerArtifactToOwnedBuffer); - CHK(EPDF_ReleaseBaseDocument); - CHK(FPDF_LoadCustomDocument); - CHK(FPDF_LoadDocument); - CHK(FPDF_LoadMemDocument); - CHK(FPDF_LoadMemDocument64); - CHK(FPDF_LoadPage); - CHK(FPDF_PageToDevice); + CHK(FPDF_GetSecurityHandlerRevision); + CHK(FPDF_GetTrailerEnds); + CHK(FPDF_GetXFAPacketContent); + CHK(FPDF_GetXFAPacketCount); + CHK(FPDF_GetXFAPacketName); + CHK(FPDF_InitLibrary); + CHK(FPDF_InitLibraryWithConfig); + CHK(EPDF_InitThread); + CHK(EPDF_ShutdownThread); + CHK(EPDF_GetPageBoxByIndex); + CHK(EPDF_GetPageUserUnitByIndex); + CHK(EPDF_LoadBaseDocument); + CHK(EPDF_LoadMemBaseDocument); + CHK(EPDF_LoadMemBaseDocument64); + CHK(EPDFDoc_DeletePageByObjectNumber); + CHK(EPDFDoc_GetPageObjectNumberByIndex); + CHK(EPDFDoc_SetPageRotationByObjectNumber); + CHK(EPDF_FreeBuffer); + CHK(EPDF_SaveDocumentToOwnedBuffer); + CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); + CHK(EPDFLayer_GetBaseDocument); + CHK(EPDFLayer_GetPromotedObjectCount); + CHK(EPDFLayer_IsObjectPromoted); + CHK(EPDFLayer_OpenLayer); + CHK(EPDFLayer_OpenLayerArtifact); + CHK(EPDFLayer_SaveDelta); + CHK(EPDFLayer_SaveDeltaToOwnedBuffer); + CHK(EPDFLayer_SaveLayerArtifact); + CHK(EPDFLayer_SaveLayerArtifactToOwnedBuffer); + CHK(EPDF_ReleaseBaseDocument); + CHK(FPDF_LoadCustomDocument); + CHK(FPDF_LoadDocument); + CHK(FPDF_LoadMemDocument); + CHK(FPDF_LoadMemDocument64); + CHK(FPDF_LoadPage); + CHK(FPDF_PageToDevice); #ifdef _WIN32 - CHK(FPDF_RenderPage); + CHK(FPDF_RenderPage); #endif - CHK(FPDF_RenderPageBitmap); - CHK(FPDF_RenderPageBitmapWithMatrix); + CHK(FPDF_RenderPageBitmap); + CHK(FPDF_RenderPageBitmapWithMatrix); #if defined(PDF_USE_SKIA) - CHK(FPDF_RenderPageSkia); + CHK(FPDF_RenderPageSkia); #endif #if defined(_WIN32) - CHK(FPDF_SetPrintMode); + CHK(FPDF_SetPrintMode); #endif - CHK(FPDF_SetSandBoxPolicy); - CHK(FPDF_VIEWERREF_GetDuplex); - CHK(FPDF_VIEWERREF_GetName); - CHK(FPDF_VIEWERREF_GetNumCopies); - CHK(FPDF_VIEWERREF_GetPrintPageRange); - CHK(FPDF_VIEWERREF_GetPrintPageRangeCount); - CHK(FPDF_VIEWERREF_GetPrintPageRangeElement); - CHK(FPDF_VIEWERREF_GetPrintScaling); + CHK(FPDF_SetSandBoxPolicy); + CHK(FPDF_VIEWERREF_GetDuplex); + CHK(FPDF_VIEWERREF_GetName); + CHK(FPDF_VIEWERREF_GetNumCopies); + CHK(FPDF_VIEWERREF_GetPrintPageRange); + CHK(FPDF_VIEWERREF_GetPrintPageRangeCount); + CHK(FPDF_VIEWERREF_GetPrintPageRangeElement); + CHK(FPDF_VIEWERREF_GetPrintScaling); - return 1; + return 1; } #undef CHK diff --git a/public/epdf_action.h b/public/epdf_action.h new file mode 100644 index 0000000000..cc59ef6f51 --- /dev/null +++ b/public/epdf_action.h @@ -0,0 +1,218 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PUBLIC_EPDF_ACTION_H_ +#define PUBLIC_EPDF_ACTION_H_ + +#include + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +// +// Detached PDF action model. A model contains one root action and its +// normalized /Next descendants. The structural fields (type, subtype, +// script, chain) are copied at build time and stay valid after the +// document is closed or mutated. Each node additionally RETAINS its action +// dictionary so the EPDFAction_GetNode{Dest,URI,FilePath,Name} payload +// getters can read payloads on demand: those getters read the CURRENT +// dictionary state, and the ones taking a FPDF_DOCUMENT require the +// originating document to still be open (they resolve named destinations +// and URI normalization through it). +// +// The API extracts action data only. It never executes JavaScript. +typedef struct epdf_action_model_t__* EPDF_ACTION_MODEL; +typedef uint32_t EPDF_ACTION_NODE_ID; + +#define EPDF_ACTION_NODE_INVALID UINT32_MAX + +// Normalized values of an action dictionary's /S name. The raw /S name is +// also available so unknown future action types are preserved. +#define EPDF_ACTION_TYPE_UNKNOWN 0 +#define EPDF_ACTION_TYPE_GOTO 1 +#define EPDF_ACTION_TYPE_GOTO_REMOTE 2 +#define EPDF_ACTION_TYPE_GOTO_EMBEDDED 3 +#define EPDF_ACTION_TYPE_LAUNCH 4 +#define EPDF_ACTION_TYPE_THREAD 5 +#define EPDF_ACTION_TYPE_URI 6 +#define EPDF_ACTION_TYPE_SOUND 7 +#define EPDF_ACTION_TYPE_MOVIE 8 +#define EPDF_ACTION_TYPE_HIDE 9 +#define EPDF_ACTION_TYPE_NAMED 10 +#define EPDF_ACTION_TYPE_SUBMIT_FORM 11 +#define EPDF_ACTION_TYPE_RESET_FORM 12 +#define EPDF_ACTION_TYPE_IMPORT_DATA 13 +#define EPDF_ACTION_TYPE_JAVASCRIPT 14 +#define EPDF_ACTION_TYPE_SET_OCG_STATE 15 +#define EPDF_ACTION_TYPE_RENDITION 16 +#define EPDF_ACTION_TYPE_TRANSITION 17 +#define EPDF_ACTION_TYPE_GOTO_3D_VIEW 18 + +// Non-fatal normalization warnings. Models marked INCOMPLETE must not be +// executed: a safety bound prevented the complete action sequence from being +// represented. Cyclic back-edges and malformed /Next entries are dropped; +// their other well-formed siblings remain available. +#define EPDF_ACTION_WARNING_CYCLE_DROPPED 0x1 +#define EPDF_ACTION_WARNING_MALFORMED_NEXT 0x2 +#define EPDF_ACTION_WARNING_INCOMPLETE 0x4 + +// Release a model returned by any EPDF*ActionModel() function below. +FPDF_EXPORT void FPDF_CALLCONV EPDFAction_CloseModel(EPDF_ACTION_MODEL model); + +// Build a detached model from an existing borrowed FPDF_ACTION. This lets +// callers normalize actions returned by APIs such as FPDFBookmark_GetAction(). +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFAction_LoadModel(FPDF_ACTION action); + +// Return the root node id, or EPDF_ACTION_NODE_INVALID for an invalid model. +FPDF_EXPORT EPDF_ACTION_NODE_ID FPDF_CALLCONV +EPDFAction_GetRootNode(EPDF_ACTION_MODEL model); + +FPDF_EXPORT int FPDF_CALLCONV EPDFAction_GetNodeCount(EPDF_ACTION_MODEL model); + +// Return an EPDF_ACTION_TYPE_* value for |node|. +FPDF_EXPORT int FPDF_CALLCONV EPDFAction_GetNodeType(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node); + +// Copy the raw PDF /S name as UTF-8, including the trailing NUL. Returns the +// required byte length, or 0 on error. |buffer| may be NULL to query length. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeSubtype(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + char* buffer, + unsigned long buflen); + +// Return whether |node| contains a string or stream /JS entry that belongs to +// either a /JavaScript or /Rendition action. This distinguishes an empty +// script from a missing or malformed /JS entry. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAction_NodeHasJavaScript(EPDF_ACTION_MODEL model, EPDF_ACTION_NODE_ID node); + +// Copy decoded /JS source as UTF-16LE, including the trailing NUL. Returns the +// required byte length, or 0 when absent/malformed. Rendition /JS is exposed +// through this same getter. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeJavaScript(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Get the destination of a goto / goto-remote / goto-embedded |node| as an +// explicit FPDF_DEST. Named destinations resolve through |document|'s +// catalog — same normalization as FPDFLink_GetDest. Returns NULL when the +// node carries no destination, has a different type, or |document| is +// invalid. |document| must be the document the model was built from. +FPDF_EXPORT FPDF_DEST FPDF_CALLCONV +EPDFAction_GetNodeDest(FPDF_DOCUMENT document, + EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node); + +// Copy the /URI of a uri-type |node| as a NUL-terminated byte string. +// Returns the required byte length including the NUL, or 0 when the node +// is not a uri action. |buffer| may be NULL to query the length. +// |document| must be the document the model was built from. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeURI(FPDF_DOCUMENT document, + EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen); + +// Copy the file spec of a goto-remote / goto-embedded / launch |node| as +// UTF-8, including the trailing NUL. Returns the required byte length, or +// 0 for other node types. |buffer| may be NULL to query the length. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeFilePath(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen); + +// Copy the /N name of a named-type |node| (NextPage, PrevPage, ...) as +// UTF-8, including the trailing NUL. Returns the required byte length, or +// 0 for other node types. |buffer| may be NULL to query the length. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAction_GetNodeName(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + void* buffer, + unsigned long buflen); + +FPDF_EXPORT int FPDF_CALLCONV EPDFAction_GetNextCount(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node); + +// Return the normalized child node at |index| in PDF /Next order. +FPDF_EXPORT EPDF_ACTION_NODE_ID FPDF_CALLCONV +EPDFAction_GetNextAt(EPDF_ACTION_MODEL model, + EPDF_ACTION_NODE_ID node, + int index); + +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFAction_GetWarningFlags(EPDF_ACTION_MODEL model); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAction_IsComplete(EPDF_ACTION_MODEL model); + +// Document-owned actions ---------------------------------------------------- + +#define EPDF_DOCUMENT_ACTION_WILL_CLOSE 0 +#define EPDF_DOCUMENT_ACTION_WILL_SAVE 1 +#define EPDF_DOCUMENT_ACTION_DID_SAVE 2 +#define EPDF_DOCUMENT_ACTION_WILL_PRINT 3 +#define EPDF_DOCUMENT_ACTION_DID_PRINT 4 + +// Return the action model for /Names /JavaScript entry |index|. Index pairing +// is guaranteed with FPDFDoc_GetJavaScriptAction(document, index): both calls +// resolve the same name-tree entry and therefore preserve boot order. +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetNamedJavaScriptActionModel(FPDF_DOCUMENT document, int index); + +// Return the action form of catalog /OpenAction. Returns NULL when absent, +// malformed, or when /OpenAction is a destination rather than an action. +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetOpenActionModel(FPDF_DOCUMENT document); + +// Return one catalog /AA action selected by EPDF_DOCUMENT_ACTION_*. +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetAdditionalActionModel(FPDF_DOCUMENT document, int event); + +// Page-owned actions -------------------------------------------------------- + +#define EPDF_PAGE_ACTION_OPEN 0 +#define EPDF_PAGE_ACTION_CLOSE 1 + +// Read page /AA without loading or rendering the page. +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFDoc_GetPageActionModel(FPDF_DOCUMENT document, + uint32_t page_object_number, + int event); + +// Annotation-owned actions -------------------------------------------------- + +#define EPDF_ANNOT_ACTION_ACTIVATE 0 +#define EPDF_ANNOT_ACTION_CURSOR_ENTER 1 +#define EPDF_ANNOT_ACTION_CURSOR_EXIT 2 +#define EPDF_ANNOT_ACTION_MOUSE_DOWN 3 +#define EPDF_ANNOT_ACTION_MOUSE_UP 4 +#define EPDF_ANNOT_ACTION_FOCUS 5 +#define EPDF_ANNOT_ACTION_BLUR 6 +#define EPDF_ANNOT_ACTION_PAGE_OPEN 7 +#define EPDF_ANNOT_ACTION_PAGE_CLOSE 8 +#define EPDF_ANNOT_ACTION_PAGE_VISIBLE 9 +#define EPDF_ANNOT_ACTION_PAGE_INVISIBLE 10 + +// Return annotation /A (ACTIVATE) or an annotation /AA action. Field events +// K/F/V/C are deliberately not accepted here, including for merged +// field/widget dictionaries. +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFAnnot_GetActionModel(FPDF_ANNOTATION annotation, int event); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_ACTION_H_ diff --git a/public/epdf_font.h b/public/epdf_font.h new file mode 100644 index 0000000000..9f2a5f4e70 --- /dev/null +++ b/public/epdf_font.h @@ -0,0 +1,96 @@ +// Copyright 2026 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PUBLIC_EPDF_FONT_H_ +#define PUBLIC_EPDF_FONT_H_ + +#include +#include + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +typedef uint32_t EPDF_FONT_ID; + +// Experimental EmbedPDF Extension API. +// Register a font for runtime fallback use and PDF authoring from file access. +// Font registration follows PDFium's normal handle/thread ownership model. In +// TLS builds, register and use fonts on the initialized worker thread that owns +// the document/page handles, and call EPDF_ShutdownThread() on that worker to +// release registered font state. In non-TLS builds, do not mutate the registry +// concurrently with rendering, saving, or editing. +// +// family_name - optional family/resource base name. Pass NULL or "" to +// infer from the font. +// weight - style weight for matching. Pass 0 to infer from the font. +// italic - style italic flag for matching. Pass -1 to infer from the +// font, 0 for non-italic, or 1 for italic. +// file_access - font bytes as FPDF_FILEACCESS. The underlying file resources +// must remain valid until EPDFFont_ClearRegisteredFonts() or +// PDFium shutdown. The FPDF_FILEACCESS struct itself may be +// stack-owned. +// +// Returns a non-zero font id on success, or 0 on failure. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + FPDF_FILEACCESS* file_access); + +// Experimental EmbedPDF Extension API. +// Register an in-memory font for runtime fallback use and PDF authoring. +// +// family_name - optional family/resource base name. Pass NULL or "" to infer +// from the font. +// weight - style weight for matching. Pass 0 to infer from the font. +// italic - style italic flag for matching. Pass -1 to infer from the +// font, 0 for non-italic, or 1 for italic. +// data_buf - pointer to font bytes. +// size - size of |data_buf| in bytes. +// +// Returns a non-zero font id on success, or 0 on failure. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + int size); + +// Experimental EmbedPDF Extension API. +// Same as EPDFFont_RegisterMemFont(), but supports size_t byte counts. +FPDF_EXPORT EPDF_FONT_ID FPDF_CALLCONV +EPDFFont_RegisterMemFont64(FPDF_BYTESTRING family_name, + int weight, + int italic, + const void* data_buf, + size_t size); + +// Experimental EmbedPDF Extension API. +// Clear all registered fonts and the fallback font order. +// +// Existing documents may still contain registered-font DA marker resources +// after this call; those markers are invalid until their fonts are registered +// again. +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearRegisteredFonts(void); + +// Experimental EmbedPDF Extension API. +// Add a registered font to the ordered fallback list used when the selected +// font does not contain a glyph or a PDF page needs a substitute font. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFFont_AddFallbackFont(EPDF_FONT_ID font_id); + +// Experimental EmbedPDF Extension API. +// Clear the ordered fallback font list without unregistering fonts. +FPDF_EXPORT void FPDF_CALLCONV EPDFFont_ClearFallbackFonts(void); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_FONT_H_ diff --git a/public/epdf_form.h b/public/epdf_form.h new file mode 100644 index 0000000000..1d7aabd934 --- /dev/null +++ b/public/epdf_form.h @@ -0,0 +1,736 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PUBLIC_EPDF_FORM_H_ +#define PUBLIC_EPDF_FORM_H_ + +#include + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#include "epdf_action.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +// +// Session-free AcroForm model API. +// +// EPDFForm_LoadModel() builds an immutable, detached snapshot of the +// document's interactive form: the /AcroForm field tree, reconciled with a +// sweep over every page's /Annots array so that widget annotations that were +// never linked into /AcroForm /Fields (a common producer bug) still appear +// as fields. The sweep walks page-tree dictionaries only; it never loads +// pages and never parses content streams. +// +// The snapshot is a pure read: it NEVER mutates the document, so it is safe +// to build over a frozen shared base document or a layer document without +// promoting a single object. +// +// All strings are copied into the snapshot at build time. The model stays +// valid after the document is closed, and is invalidated (in the sense of +// becoming stale, not dangling) by any document mutation - callers should +// rebuild after a mutation. Free with EPDFForm_CloseModel(). +typedef struct epdf_form_model_t__* EPDF_FORM_MODEL; + +// Document form kind, as declared by the document catalog. +// Note that a document with no /AcroForm dictionary can still yield +// recovered fields from the page sweep; kind reports what the catalog +// declares, not whether fields exist. +#define EPDF_FORMKIND_NONE 0 +#define EPDF_FORMKIND_ACROFORM 1 +// /AcroForm has an /XFA entry. Fields describe the AcroForm shell only. +#define EPDF_FORMKIND_XFA 2 + +// Field families. Text-family subtleties (password, file-select, rich text, +// multiline, comb) are expressed through the /Ff flags, not extra families. +#define EPDF_FORMFIELD_FAMILY_UNKNOWN 0 +#define EPDF_FORMFIELD_FAMILY_PUSHBUTTON 1 +#define EPDF_FORMFIELD_FAMILY_CHECKBOX 2 +#define EPDF_FORMFIELD_FAMILY_RADIO 3 +#define EPDF_FORMFIELD_FAMILY_TEXT 4 +#define EPDF_FORMFIELD_FAMILY_COMBOBOX 5 +#define EPDF_FORMFIELD_FAMILY_LISTBOX 6 +#define EPDF_FORMFIELD_FAMILY_SIGNATURE 7 + +// Field provenance. +// kAcroForm: reachable from the /AcroForm /Fields tree. +// kRecovered: only reachable through a page's /Annots array; the document +// needs repair for other processors to see this field. +#define EPDF_FORMFIELD_ORIGIN_ACROFORM 0 +#define EPDF_FORMFIELD_ORIGIN_RECOVERED 1 + +// Experimental EmbedPDF Extension API. +// Build a form model snapshot for |document|. +// +// Returns a model handle, or NULL if |document| is NULL or the build failed. +// Documents without any form yield a valid empty model with kind +// EPDF_FORMKIND_NONE. +FPDF_EXPORT EPDF_FORM_MODEL FPDF_CALLCONV +EPDFForm_LoadModel(FPDF_DOCUMENT document); + +// Experimental EmbedPDF Extension API. +// Release a model returned by EPDFForm_LoadModel(). +FPDF_EXPORT void FPDF_CALLCONV EPDFForm_CloseModel(EPDF_FORM_MODEL model); + +// Experimental EmbedPDF Extension API. +// Return the document's declared form kind (EPDF_FORMKIND_*). +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFormKind(EPDF_FORM_MODEL model); + +// Experimental EmbedPDF Extension API. +// Return whether the /AcroForm dictionary sets /NeedAppearances. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_GetNeedAppearances(EPDF_FORM_MODEL model); + +// Experimental EmbedPDF Extension API. +// Return the number of terminal fields in the model. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFields(EPDF_FORM_MODEL model); + +// Field additional-action events. EPDFForm_GetFieldActionModel() reads the +// effective field /AA through the field hierarchy. These are distinct from +// annotation/widget /AA events even when a field and widget share one merged +// PDF dictionary. +#define EPDF_FORM_ACTION_KEYSTROKE 0 +#define EPDF_FORM_ACTION_FORMAT 1 +#define EPDF_FORM_ACTION_VALIDATE 2 +#define EPDF_FORM_ACTION_CALCULATE 3 + +// Return a caller-owned detached action model for one effective field action, +// or NULL when absent/malformed. Close with EPDFAction_CloseModel(). +FPDF_EXPORT EPDF_ACTION_MODEL FPDF_CALLCONV +EPDFForm_GetFieldActionModel(EPDF_FORM_MODEL model, int field_index, int event); + +// Return the raw number of entries in /AcroForm /CO. Each entry resolves to a +// field index in this same snapshot, or -1 when malformed/unresolved. Keeping +// malformed slots preserves the declared calculation order. +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_CountCalculationOrder(EPDF_FORM_MODEL model); +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetCalculationOrderFieldIndex(EPDF_FORM_MODEL model, int order_index); + +// Experimental EmbedPDF Extension API. +// Return the indirect object number of the field dictionary, or 0 when the +// field dictionary is a direct object (spec-violating; identity is weak). +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldObjNum(EPDF_FORM_MODEL model, int field_index); + +// Experimental EmbedPDF Extension API. +// Return the field family (EPDF_FORMFIELD_FAMILY_*), or UNKNOWN when +// |field_index| is out of range. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldFamily(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Return the effective /Ff flags (inheritance resolved), or 0 on error. +FPDF_EXPORT uint32_t FPDF_CALLCONV EPDFForm_GetFieldFlags(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Return the field provenance (EPDF_FORMFIELD_ORIGIN_*), or -1 on error. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldOrigin(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Copy the fully qualified field name ("parent.child") into |buffer| as +// UTF-16LE, including the trailing NUL. Returns the byte length of the +// string, or 0 on error. |buffer| may be NULL to query the length. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Copy the field's alternate name (/TU, the tooltip) into |buffer| as +// UTF-16LE. Same conventions as EPDFForm_GetFieldName(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldAlternateName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Copy the field's mapping name (/TM, the export name) into |buffer| as +// UTF-16LE. Same conventions as EPDFForm_GetFieldName(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldMappingName(EPDF_FORM_MODEL model, + int field_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// String-oriented PDF field value shapes. Text strings and button name +// objects are exposed as SCALAR. A multi-select choice array is ARRAY, +// including an empty array. NONE means the inherited entry is absent or +// explicitly null. UNSUPPORTED means an entry exists with another shape +// (for example a signature dictionary or a malformed choice array). +#define EPDF_FORM_VALUE_NONE 0 +#define EPDF_FORM_VALUE_SCALAR 1 +#define EPDF_FORM_VALUE_ARRAY 2 +#define EPDF_FORM_VALUE_UNSUPPORTED 3 + +// Experimental EmbedPDF Extension API. +// Return the shape of the field's raw, inheritance-resolved /V entry. +// Unlike the former scalar getter, this does not fall back to /DV and does +// not derive a button value from widget /AS. Widget state remains available +// through EPDFForm_IsFieldWidgetChecked() and the widget state/value getters. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldValueKind(EPDF_FORM_MODEL model, + int field_index); + +// Return the number of string/name values in /V. SCALAR has one value; +// ARRAY has its exact element count; NONE and UNSUPPORTED have zero. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldValues(EPDF_FORM_MODEL model, + int field_index); + +// Copy /V value |value_index| into |buffer| as UTF-16LE. Same buffer +// conventions as EPDFForm_GetFieldName(). Returns 0 when out of range or +// when the value shape is NONE/UNSUPPORTED. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldValueAt(EPDF_FORM_MODEL model, + int field_index, + int value_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Equivalent typed accessors for the raw, inheritance-resolved /DV entry. +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldDefaultValueKind(EPDF_FORM_MODEL model, int field_index); +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_CountFieldDefaultValues(EPDF_FORM_MODEL model, int field_index); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldDefaultValueAt(EPDF_FORM_MODEL model, + int field_index, + int value_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Return /MaxLen for text fields, or 0 when absent or not applicable. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_GetFieldMaxLen(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Return the number of /Opt options for choice fields, 0 otherwise. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldOptions(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Copy a choice option's display label into |buffer| as UTF-16LE. +// Same conventions as EPDFForm_GetFieldName(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldOptionLabel(EPDF_FORM_MODEL model, + int field_index, + int option_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Copy a choice option's export value into |buffer| as UTF-16LE. +// Same conventions as EPDFForm_GetFieldName(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldOptionValue(EPDF_FORM_MODEL model, + int field_index, + int option_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Return whether a choice option is currently selected. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_IsFieldOptionSelected(EPDF_FORM_MODEL model, + int field_index, + int option_index); + +// Experimental EmbedPDF Extension API. +// Return the number of widget annotations bound to the field. A merged +// field/widget dictionary counts as one widget whose object number equals +// the field's. Zero widgets means the field is unplaced. +FPDF_EXPORT int FPDF_CALLCONV EPDFForm_CountFieldWidgets(EPDF_FORM_MODEL model, + int field_index); + +// Experimental EmbedPDF Extension API. +// Return the widget annotation's indirect object number, or 0 for direct +// (spec-violating) widget dictionaries. +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldWidgetObjNum(EPDF_FORM_MODEL model, + int field_index, + int widget_index); + +// Experimental EmbedPDF Extension API. +// Return the object number of the page whose /Annots array references the +// widget (resolved during the sweep, falling back to the widget's /P +// entry), or 0 when the widget is not reachable from any page. +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_GetFieldWidgetPageObjNum(EPDF_FORM_MODEL model, + int field_index, + int widget_index); + +// Experimental EmbedPDF Extension API. +// Copy the widget's on-state name (the non-"Off" key of its /AP /N +// dictionary) into |buffer| as raw PDF name bytes, including the trailing +// NUL. Only meaningful for checkbox and radio widgets; empty otherwise. +// Returns the byte length of the string, or 0 on error. |buffer| may be +// NULL to query the length. The returned bytes are an opaque token for use +// with future toggle APIs. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldWidgetOnState(EPDF_FORM_MODEL model, + int field_index, + int widget_index, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Copy the widget's export value (/Opt entry for its control index when +// present, else the on-state name) into |buffer| as UTF-16LE. Only +// meaningful for checkbox and radio widgets. Same conventions as +// EPDFForm_GetFieldName(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_GetFieldWidgetExportValue(EPDF_FORM_MODEL model, + int field_index, + int widget_index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Return whether a checkbox/radio widget is currently checked +// (its /AS equals its on-state). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_IsFieldWidgetChecked(EPDF_FORM_MODEL model, + int field_index, + int widget_index); + +// Experimental EmbedPDF Extension API. +// Return the index of the field whose dictionary has the given indirect +// object number, or -1 when unknown. +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldIndexByObjNum(EPDF_FORM_MODEL model, uint32_t field_objnum); + +// --------------------------------------------------------------------------- +// Write transactions. +// +// All write APIs below are stateless, field-local transactions keyed by the +// field dictionary's indirect object number (from EPDFForm_GetFieldObjNum). +// They take the DOCUMENT, not a model: models are immutable snapshots and +// become stale after any successful write - rebuild with EPDFForm_LoadModel. +// +// Transactions validate first and mutate second: on FALSE the document is +// untouched (on a layer document: zero objects promoted). On success, only +// the objects that actually change are written (on a layer document: only +// those promote), which keeps layer deltas minimal. +// +// /Ff ReadOnly (bit 1) is deliberately NOT enforced here: the PDF spec +// forbids USER modification of read-only fields, not programmatic writes +// (calculated fields are read-only yet script-written). Enforcing fill +// policy is the caller's responsibility. +// +// Changed-widget reporting (uniform across all write APIs): +// changed_widget_objnums - optional caller buffer receiving the object +// numbers of widget annotations whose appearance +// changed (may span multiple pages). May be NULL. +// buffer_size - capacity of |changed_widget_objnums| in +// elements. +// out_changed_count - optional; receives the TOTAL number of changed +// widgets, which may exceed |buffer_size|. +// Widgets stored as direct objects (no object +// number) are counted but not reported. +// --------------------------------------------------------------------------- + +// Experimental EmbedPDF Extension API. +// Set the value of a checkbox or radio field. +// +// |on_state| is the target widget appearance state, exactly as returned by +// EPDFForm_GetFieldWidgetOnState(), and selects WHICH widget of the group +// is checked. NULL clears the group (rejected for radio fields with +// NoToggleToOff). Every sibling widget's /AS is updated (checkboxes and +// in-unison radios check all widgets sharing the target's export value and +// on-state) and the field's /V is set to the export value name, or to the +// control index for fields carrying /Opt, matching Acrobat conventions. +// +// No appearance streams are regenerated: toggle widgets carry one appearance +// per state, so flipping /AS IS the visual change. +// +// Fails when |field_objnum| is not a checkbox/radio terminal field or +// |on_state| matches no widget. Returns TRUE with zero changes when the +// field is already in the requested state. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetToggle(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_BYTESTRING on_state, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// Experimental EmbedPDF Extension API. +// Set the value of a text field. +// +// Writes /V, drops any stale rich-text /RV, and regenerates the /AP stream +// of every widget of the field. Fails when |field_objnum| is not a text +// terminal field. When the value exceeds the field's effective /MaxLen, only +// the first /MaxLen characters are written, matching Acrobat assignment +// semantics. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetTextValue(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// Experimental EmbedPDF Extension API. +// Set the selection of a combo box or list box field. +// +// |values| holds |value_count| option export values. Zero values clears the +// effective selection, using a local empty value when needed to shadow an +// inherited /V. Multiple values require a multi-select list box. For combo +// boxes with the Edit flag a single non-option value is accepted as free +// text; otherwise every value must match an option's export value. +// +// Writes /V (string, or array for multiple values ordered by option index), +// keeps /I in sync (sorted ascending; removed when free text is set), and +// regenerates every widget's /AP stream. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetChoiceValues(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* values, + unsigned long value_count, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// Experimental EmbedPDF Extension API. +// Reset a field to its default value. +// +// Restores /V from the effective /DV (clearing the effective value when no +// default exists), clears stale /RV and /I, updates toggle widget /AS states, +// and regenerates /AP streams for text and choice widgets. Fails for push +// buttons and signature fields. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ResetField(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// Acrobat-compatible Field.display values. Only the Invisible, Hidden, Print, +// and NoView annotation flag bits are changed; all unrelated flags survive. +#define EPDF_FORM_DISPLAY_VISIBLE 0 +#define EPDF_FORM_DISPLAY_HIDDEN 1 +#define EPDF_FORM_DISPLAY_NO_PRINT 2 +#define EPDF_FORM_DISPLAY_NO_VIEW 3 + +// Set Field.display for every widget of a terminal field. The write follows +// the same validate-then-promote transaction path as value writes. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDisplay(FPDF_DOCUMENT document, + uint32_t field_objnum, + int display, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// Regenerate every text/combo widget /AP using |appearance_text| without +// changing the field's semantic /V. This is the native sink for /AA /F +// formatting results. List boxes, buttons, and signatures are rejected. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldAppearanceText(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING appearance_text, + uint32_t* changed_widget_objnums, + unsigned long buffer_size, + unsigned long* out_changed_count); + +// --------------------------------------------------------------------------- +// Form data interchange (FDF / XFDF). +// +// Exports read through the same reconciled view as EPDFForm_LoadModel, so +// recovered fields (origin kRecovered) are included, and on layer documents +// promoted values win. Imports replay each entry through the typed write +// transactions above, so validation, appearance regeneration, and minimal +// layer promotion apply per field; one bad entry never poisons the rest. +// --------------------------------------------------------------------------- + +// Omit required fields whose value is empty (Acrobat's form-submission +// behavior). Off by default: interchange exports are faithful. +#define EPDF_FORM_EXPORT_SKIP_EMPTY_REQUIRED 0x1 + +// Experimental EmbedPDF Extension API. +// Serialize the document's form data as FDF. +// +// pdf_path - optional /F filespec recorded in the FDF; NULL to omit. +// export_flags - EPDF_FORM_EXPORT_* bits. +// +// Returns the byte length of the FDF payload, or 0 on error. When |buffer| +// is non-NULL and |buflen| is large enough, the payload is copied into it. +// Call with a NULL buffer first to query the size. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_ExportFDF(FPDF_DOCUMENT document, + FPDF_WIDESTRING pdf_path, + uint32_t export_flags, + void* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Serialize the document's form data as XFDF (UTF-8 XML, form data only - +// no annotations). Field names nest per fully-qualified-name component. +// Same conventions as EPDFForm_ExportFDF. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFForm_ExportXFDF(FPDF_DOCUMENT document, + FPDF_WIDESTRING pdf_path, + uint32_t export_flags, + void* buffer, + unsigned long buflen); + +// Per-import accounting. A field entry is "applied" when its value was +// written (including no-op writes of an unchanged value) and "skipped" when +// the name is unknown, the field family cannot take the value, or the value +// failed validation (unknown toggle state, MaxLen, non-option choice, ...). +typedef struct { + uint32_t fields_total; + uint32_t fields_applied; + uint32_t fields_skipped; + uint32_t widgets_changed; +} EPDF_FORM_IMPORT_RESULT; + +// Experimental EmbedPDF Extension API. +// Apply form data from an FDF payload to the document. +// +// Accepts both flat entries with dotted /T names and hierarchical /Kids +// trees. Returns TRUE when the FDF parsed, regardless of per-field skips +// (see |out_result|); FALSE when the payload is not FDF. On a layer +// document only the fields that actually change promote. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ImportFDF(FPDF_DOCUMENT document, + const void* data, + unsigned long size, + EPDF_FORM_IMPORT_RESULT* out_result); + +// Experimental EmbedPDF Extension API. +// Apply form data from an XFDF payload to the document. Accepts nested +// elements and dotted name attributes; multiple elements +// select multiple options of a multi-select list box. Same conventions as +// EPDFForm_ImportFDF. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_ImportXFDF(FPDF_DOCUMENT document, + const void* data, + unsigned long size, + EPDF_FORM_IMPORT_RESULT* out_result); + +// --------------------------------------------------------------------------- +// Repair ("form doctor"). +// +// EPDFForm_LoadModel() reconciles broken documents in memory on every load; +// EPDFForm_Repair() makes those fixes durable in the document so any other +// PDF processor sees the same form. Validate-then-apply: the document is +// only touched when there is something to fix, so on a layer document a +// no-op repair promotes nothing and a real repair promotes only the +// structural containers it edits. Idempotent: a second call reports zero +// fixes. +// --------------------------------------------------------------------------- + +// Also regenerate widget appearance streams: widgets with no /AP get one, +// and when the /AcroForm sets /NeedAppearances every widget is re-baked and +// the flag is cleared, making rendering deterministic across viewers. +#define EPDF_FORM_REPAIR_BAKE_APPEARANCES 0x1 + +typedef struct { + // 1 when a missing /AcroForm dictionary was created (with /DR + /DA). + uint32_t acroform_created; + // Recovered field roots appended to /AcroForm /Fields. + uint32_t fields_linked; + // Stray widgets appended to their parent field's /Kids (only when the + // widget's /Parent already references that field). + uint32_t widgets_linked; + // Recovered fields stored as direct objects: they cannot be referenced + // from /Fields and stay reconciled-in-memory only. + uint32_t fields_unrepairable; + // Widgets whose appearance stream was (re)generated. + uint32_t appearances_baked; + // 1 when /NeedAppearances was cleared after re-baking. + uint32_t need_appearances_cleared; +} EPDF_FORM_REPAIR_REPORT; + +// Experimental EmbedPDF Extension API. +// Repair the document's form structure. |repair_flags| is a bitset of +// EPDF_FORM_REPAIR_* values. Returns TRUE when the repair pass ran (even +// with zero fixes); FALSE on error. |out_report| may be NULL. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_Repair(FPDF_DOCUMENT document, + uint32_t repair_flags, + EPDF_FORM_REPAIR_REPORT* out_report); + +// --------------------------------------------------------------------------- +// Authoring: field lifecycle and adoption. +// +// Widgets are born, styled, moved, and deleted through the ANNOTATION APIs +// (FPDFPage_CreateAnnot with the Widget subtype, EPDFAnnot_SetMKColor, +// EPDFAnnot_SetBorderStyle, EPDFAnnot_SetDefaultAppearance, ...). The forms +// API below only does the field-tree side: create a logical (unplaced) +// field, ADOPT an existing widget annotation as one of its views, detach +// it again, and configure field-plane properties. An unattached widget is +// an ordinary, inert annotation - adoption is what turns it into a form +// control (and what bakes its family-correct appearance stream). +// +// All operations are validate-then-apply: a FALSE/0 return leaves the +// document untouched (on layer documents: zero objects promoted). +// --------------------------------------------------------------------------- + +// Experimental EmbedPDF Extension API. +// Create a logical form field with no widgets ("unplaced"). +// +// family - EPDF_FORMFIELD_FAMILY_TEXT / CHECKBOX / RADIO / COMBOBOX / +// LISTBOX. Push buttons, signatures, and unknown are not +// authorable. +// full_name - dotted fully qualified name ("billing.name"). Missing +// non-terminal ancestors are created; a sibling name +// collision at any level fails. +// +// Bootstraps /AcroForm (with /DR and /DA) when the document has none. +// Returns the new field dictionary's object number, or 0 on failure. +FPDF_EXPORT uint32_t FPDF_CALLCONV +EPDFForm_CreateField(FPDF_DOCUMENT document, + int family, + FPDF_WIDESTRING full_name); + +// Experimental EmbedPDF Extension API. +// Adopt an existing widget annotation as a view of |field_objnum|. +// +// The widget must be an unattached widget annotation (no /Parent, not a +// merged field). For checkbox/radio fields |on_state| names the widget's +// checked appearance state and must be non-empty; for other families pass +// NULL. Adoption wires /Parent + /Kids, seeds toggle /AP states and /AS, +// and bakes the family-correct appearance stream. Adopting into a legacy +// MERGED field first splits it (the field keeps its object number; the +// previously merged widget becomes a new kid annotation - widget identity +// changes, field identity never does). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_AttachWidget(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t widget_objnum, + FPDF_BYTESTRING on_state); + +// Experimental EmbedPDF Extension API. +// Detach a widget from its field. The widget keeps its page placement and +// last appearance but becomes an ordinary inert annotation (deletable via +// the annotation APIs). The field survives, "unplaced" when this was its +// last widget. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_DetachWidget(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t widget_objnum); + +// Experimental EmbedPDF Extension API. +// Delete a terminal field: detaches every widget (reported through the +// caller buffer so they can be deleted as annotations), removes the field +// from the tree, and prunes non-terminal ancestors left empty. Fails for +// non-terminal fields (nodes with child FIELDS). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_DeleteField(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t* out_detached_widgets, + unsigned long buffer_size, + unsigned long* out_detached_count); + +// --------------------------------------------------------------------------- +// Authoring: field-plane property setters. Each is an independent +// validate-then-apply transaction; the TypeScript layer composes them into +// one updateField() call. +// --------------------------------------------------------------------------- + +// Experimental EmbedPDF Extension API. +// Rename the field's own /T segment (NOT the dotted path - reparenting is +// not supported). Fails when a sibling under the same parent already +// carries that name, or when |partial_name| is empty or contains '.'. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING partial_name); + +// Experimental EmbedPDF Extension API. +// Masked /Ff update: bits in |set_bits| are set, bits in |clear_bits| are +// cleared. Family-DEFINING bits (Radio, Pushbutton, Combo) are immutable - +// touching them fails. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldFlags(FPDF_DOCUMENT document, + uint32_t field_objnum, + uint32_t set_bits, + uint32_t clear_bits); + +// Experimental EmbedPDF Extension API. +// Set /MaxLen on a text field. 0 clears the limit. Fails when the current +// value is already longer than the new limit. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldMaxLen(FPDF_DOCUMENT document, + uint32_t field_objnum, + int max_len); + +// Experimental EmbedPDF Extension API. +// Set /DV on a text or choice field. Text fields require exactly one value. +// Choice fields accept one value, or multiple values for a multi-select list +// box; option validation and ordering match EPDFForm_SetChoiceValues(). A +// single empty string is a real scalar default, not a request to remove /DV. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDefaultValues(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* values, + unsigned long value_count); + +// Set a checkbox/radio /DV from the opaque widget appearance-state token +// returned by EPDFForm_GetFieldWidgetOnState(). "Off" is an explicit default; +// NULL and the empty string are rejected. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldDefaultToggle(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_BYTESTRING on_state); + +// Remove /DV from the addressed field dictionary. If an ancestor provides an +// inherited /DV, that inherited default becomes effective; this API removes a +// local override rather than mutating a shared ancestor. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_RemoveFieldDefaultValue(FPDF_DOCUMENT document, uint32_t field_objnum); + +// Experimental EmbedPDF Extension API. +// Set /TU (the accessible tooltip). An empty string clears it. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldAlternateName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value); + +// Experimental EmbedPDF Extension API. +// Set /TM (the export mapping name). An empty string clears it. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldMappingName(FPDF_DOCUMENT document, + uint32_t field_objnum, + FPDF_WIDESTRING value); + +// Experimental EmbedPDF Extension API. +// Replace a choice field's effective /Opt with |count| options. Entries where +// label equals export are written as plain strings, otherwise as [export label] +// pairs. The current selection is re-synced: selected exports that vanish +// are dropped, /V and /I are rewritten consistently, and widget appearance +// streams are regenerated. /DV is filtered through the same new option set. +// |count| of 0 writes an empty local /Opt array (shadowing inherited options); +// an edit combo's current/default free text survives and other selections +// clear. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFForm_SetFieldOptions(FPDF_DOCUMENT document, + uint32_t field_objnum, + const FPDF_WIDESTRING* labels, + const FPDF_WIDESTRING* exports, + unsigned long count); + +// Experimental EmbedPDF Extension API. +// Return the index of the field owning the widget annotation with the given +// indirect object number, or -1 when the object is not a known widget. This +// is the join key for decorating widget annotations in page annotation +// listings with their logical field. +FPDF_EXPORT int FPDF_CALLCONV +EPDFForm_GetFieldIndexForWidget(EPDF_FORM_MODEL model, uint32_t widget_objnum); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_FORM_H_ diff --git a/public/epdf_pieceinfo.h b/public/epdf_pieceinfo.h new file mode 100644 index 0000000000..fbf18d9bc5 --- /dev/null +++ b/public/epdf_pieceinfo.h @@ -0,0 +1,352 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PUBLIC_EPDF_PIECEINFO_H_ +#define PUBLIC_EPDF_PIECEINFO_H_ + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +// +// Generic document- and page-piece metadata access (ISO 32000-2, 14.5). +// Every entry is addressed by an application name and maps to: +// +// /PieceInfo << +// / << +// /LastModified (D:...) +// /Private << / >> +// >> +// >> +// +// This API intentionally supports a constrained set of safe /Private value +// types. It is not a general-purpose PDF object editor. Unknown values are +// preserved by writes to other keys and reported through +// EPDFDoc_GetPieceInfoValueType() or +// EPDFDoc_GetPagePieceInfoValueType(). + +// Document-level /PieceInfo -------------------------------------------------- +// +// These functions operate on /PieceInfo in the document catalog. The +// document revision marker is /ModDate in the document information +// dictionary; setters store |document_last_modified| in both /Info /ModDate +// and the application data dictionary's /LastModified entry. + +// Return whether the catalog has a well-formed data dictionary for +// |application| under /PieceInfo. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_HasPieceInfoEntry(FPDF_DOCUMENT document, FPDF_BYTESTRING application); + +// Enumerate application names under the catalog's /PieceInfo dictionary. +// Returns 0 when /PieceInfo is absent or malformed. Entry ordering is +// unspecified. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoEntryCount(FPDF_DOCUMENT document); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoEntryAt(FPDF_DOCUMENT document, + int index, + char* buffer, + unsigned long buflen); + +// Copy /Info /ModDate into |buffer| as UTF-16LE. Returns the required byte +// length including the trailing NUL, or 0 if absent or malformed. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetLastModified(FPDF_DOCUMENT document, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Copy the application data dictionary's /LastModified date string using the +// same two-call convention as EPDFDoc_GetLastModified(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoLastModified(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_WCHAR* buffer, + unsigned long buflen); + +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoKeyCount(FPDF_DOCUMENT document, + FPDF_BYTESTRING application); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoKeyAt(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + int index, + char* buffer, + unsigned long buflen); +FPDF_EXPORT FPDF_OBJECT_TYPE FPDF_CALLCONV +EPDFDoc_GetPieceInfoValueType(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoString(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoString(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoNumber(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float value, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPieceInfoNumber(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float* value); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoBoolean(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL value, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPieceInfoBoolean(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL* value); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoName(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BYTESTRING value, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoName(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + char* buffer, + unsigned long buflen); + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPieceInfoStringArray(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + const FPDF_WIDESTRING* values, + unsigned long value_count, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPieceInfoStringArrayCount(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPieceInfoStringArrayAt(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Missing keys and entries are successful no-ops. Clearing the final entry +// removes /PieceInfo from the catalog but leaves /Info /ModDate intact. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPieceInfoKey(FPDF_DOCUMENT document, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING document_last_modified); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPieceInfoEntry(FPDF_DOCUMENT document, + FPDF_BYTESTRING application); + +// Page-level /PieceInfo ------------------------------------------------------ + +// Return whether the page has a well-formed data dictionary for +// |application| under /PieceInfo. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_HasPagePieceInfoEntry(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application); + +// Enumerate application names under the page's /PieceInfo dictionary. Returns +// 0 when /PieceInfo is absent or malformed. Entry ordering is unspecified. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoEntryCount(FPDF_DOCUMENT document, + unsigned int page_object_number); + +// Copy the UTF-8 PDF name of the /PieceInfo entry at |index| into |buffer|, +// including a trailing NUL. Returns the required byte length, or 0 on error. +// |buffer| may be NULL to query the length. Malformed entries are enumerated +// so callers can distinguish them with EPDFDoc_HasPagePieceInfoEntry(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoEntryAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + int index, + char* buffer, + unsigned long buflen); + +// Copy the page dictionary's /LastModified date string into |buffer| as +// UTF-16LE. Returns the required byte length including the trailing NUL, or 0 +// if the page/date is absent or malformed. |buffer| may be NULL to query the +// length. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPageLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Copy the application data dictionary's /LastModified date string into +// |buffer| as UTF-16LE. Uses the same two-call convention as +// EPDFDoc_GetPageLastModified(). +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoLastModified(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Return the number of keys in the application's /Private dictionary, or 0 +// when the entry/private dictionary is absent or malformed. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoKeyCount(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application); + +// Copy the UTF-8 PDF name of the /Private key at |index| into |buffer|, +// including a trailing NUL. Returns the required byte length, or 0 on error. +// |buffer| may be NULL to query the length. Key ordering is unspecified. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoKeyAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + int index, + char* buffer, + unsigned long buflen); + +// Return the resolved FPDF_OBJECT_* type of |key| in the application's +// /Private dictionary, or FPDF_OBJECT_UNKNOWN when absent/malformed. +FPDF_EXPORT FPDF_OBJECT_TYPE FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoValueType(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key); + +// Set/get a PDF text string. |content_last_modified| is a PDF date string +// identifying the page-content revision represented by this application data; +// the setter stores it in both the page and application data dictionaries. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoString(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value, + FPDF_WIDESTRING content_last_modified); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoString(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Set/get a PDF number. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoNumber(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float value, + FPDF_WIDESTRING content_last_modified); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoNumber(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + float* value); + +// Set/get a PDF boolean. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoBoolean(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL value, + FPDF_WIDESTRING content_last_modified); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoBoolean(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BOOL* value); + +// Set/get a PDF name. Names are passed and returned as UTF-8 byte strings. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoName(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_BYTESTRING value, + FPDF_WIDESTRING content_last_modified); +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoName(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + char* buffer, + unsigned long buflen); + +// Replace |key| with an array of PDF text strings. |values| may be NULL only +// when |value_count| is 0, which writes an empty array. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPagePieceInfoStringArray(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + const FPDF_WIDESTRING* values, + unsigned long value_count, + FPDF_WIDESTRING content_last_modified); + +// Return the number of text strings in the array at |key|, or -1 when the key +// is absent, is not an array, or contains a non-string value. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoStringArrayCount(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key); + +// Copy the text string at |index| using the standard UTF-16LE two-call +// convention. Returns 0 on error. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetPagePieceInfoStringArrayAt(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Remove |key| from the application's /Private dictionary. Missing keys are a +// successful no-op. When the entry exists, both /LastModified values are set +// to |content_last_modified|. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPagePieceInfoKey(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application, + FPDF_BYTESTRING key, + FPDF_WIDESTRING content_last_modified); + +// Remove the complete application entry. Other /PieceInfo applications are +// preserved; an empty /PieceInfo dictionary is removed from the page. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_ClearPagePieceInfoEntry(FPDF_DOCUMENT document, + unsigned int page_object_number, + FPDF_BYTESTRING application); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_PIECEINFO_H_ diff --git a/public/epdf_redact.h b/public/epdf_redact.h index e0f62b68a3..52b3ce97d6 100644 --- a/public/epdf_redact.h +++ b/public/epdf_redact.h @@ -14,91 +14,55 @@ extern "C" { #endif // __cplusplus -// Experimental EmbedPDF Extension API. -// Report entry produced by redaction APIs that delete annotations. -// -// `object_number` is 0 when the removed annotation was a direct object. -// `nm_utf8_len` is 0 when no /NM was present. It is -// EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW when /NM existed but the caller's -// UTF-8 byte pool had no room for it. -#define EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW 0xFFFFFFFFu - -typedef struct { - uint32_t object_number; - uint32_t index_at_removal; - uint32_t nm_utf8_offset; - uint32_t nm_utf8_len; -} EPDF_RemovedAnnotInfo; - // Experimental EmbedPDF Extension API. // Apply a redact annotation, permanently removing content underneath. -// If the annotation has an RO (Redact Overlay) stream, it will be flattened -// as page content (filled rectangles with overlay text). -// If no RO stream exists, content is simply removed with no overlay. -// The annotation is automatically removed from the page after applying. +// +// Overlay precedence follows ISO 32000-2: if the annotation has an /RO +// (Redact Overlay) stream it is flattened as page content; otherwise an +// overlay is synthesized from the declarative entries (/IC fill and +// /OverlayText per /DA, /Q and /Repeat) and flattened the same way. An +// annotation with neither /RO nor declarative entries leaves the region +// transparent. +// +// Annotations whose /Rect intersects the redacted region with positive area +// are removed as well, including their popup cascade; removed widgets are +// detached from the AcroForm tree. Sibling REDACT annotations are preserved. +// The applied annotation itself is removed from the page. // // The caller is responsible for: // 1. Closing the annotation handle with FPDFPage_CloseAnnot after this call // 2. Calling FPDFPage_GenerateContent to persist changes // -// page - handle to the page containing the annotation -// annot - handle to a REDACT annotation +// page - handle to the page containing the annotation +// annot - handle to a REDACT annotation +// out_removed_annot_count - optional, may be NULL. Receives the number of +// annotations removed as a side effect of this +// redaction, NOT counting REDACT annotations +// themselves. Zeroed on entry. // // Returns TRUE on success, FALSE if not a REDACT annotation or on error. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot); - -// Experimental EmbedPDF Extension API. -// Same as EPDFAnnot_ApplyRedaction(), but also reports every annotation that -// was removed. This includes annotations whose /Rect intersects the redaction -// area, popup annotations cascaded from removed parents, and the originating -// REDACT annotation itself. Sibling REDACT annotations are preserved. -// -// The caller owns both output buffers. `out_written_count` is the number of -// records safely written to `out_removed`; `out_total_count` is the total -// number of annotations removed. If total > written, the report was truncated. -// /NM values are normalized to UTF-8 and written into `nm_utf8_pool`. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ApplyRedactionWithReport( - FPDF_PAGE page, - FPDF_ANNOTATION annot, - EPDF_RemovedAnnotInfo* out_removed, - uint32_t out_removed_capacity, - char* nm_utf8_pool, - uint32_t nm_utf8_pool_capacity, - uint32_t* out_written_count, - uint32_t* out_total_count, - uint32_t* out_nm_utf8_bytes_used); +EPDFAnnot_ApplyRedaction(FPDF_PAGE page, + FPDF_ANNOTATION annot, + uint32_t* out_removed_annot_count); // Experimental EmbedPDF Extension API. // Apply all redact annotations on a page, permanently removing content -// underneath each one. For each annotation with an RO stream, the overlay -// is flattened as page content. Annotations without RO simply have content -// removed with no overlay. -// All REDACT annotations are automatically removed from the page after applying. +// underneath each one. Overlay and removal semantics match +// EPDFAnnot_ApplyRedaction, as does the count contract: REDACT annotations +// (all of which are consumed by the apply) are never counted. // // The caller is responsible for: // 1. Calling FPDFPage_GenerateContent to persist changes // -// page - handle to a page +// page - handle to a page +// out_removed_annot_count - optional, may be NULL. Receives the number of +// non-REDACT annotations removed. Zeroed on +// entry. // // Returns TRUE if any redactions were applied, FALSE otherwise. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactions(FPDF_PAGE page); - -// Experimental EmbedPDF Extension API. -// Same as EPDFPage_ApplyRedactions(), but reports removed annotations using -// the same buffer contract as EPDFAnnot_ApplyRedactionWithReport(). -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactionsWithReport( - FPDF_PAGE page, - EPDF_RemovedAnnotInfo* out_removed, - uint32_t out_removed_capacity, - char* nm_utf8_pool, - uint32_t nm_utf8_pool_capacity, - uint32_t* out_written_count, - uint32_t* out_total_count, - uint32_t* out_nm_utf8_bytes_used); +EPDFPage_ApplyRedactions(FPDF_PAGE page, uint32_t* out_removed_annot_count); #ifdef __cplusplus } // extern "C" diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h index a9d7c84db4..78117e4fec 100644 --- a/public/fpdf_annot.h +++ b/public/fpdf_annot.h @@ -6,13 +6,15 @@ #define PUBLIC_FPDF_ANNOT_H_ #include - // NOLINTNEXTLINE(build/include) #include "fpdfview.h" // NOLINTNEXTLINE(build/include) #include "fpdf_formfill.h" +// NOLINTNEXTLINE(build/include) +#include "epdf_font.h" + // NOLINTNEXTLINE(build/include) #include "epdf_redact.h" @@ -1611,6 +1613,24 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, unsigned int G, unsigned int B); +// Experimental EmbedPDF Extension API. +// Set the default appearance of a FreeText annotation using a registered font. +// +// annot - handle to an annotation. +// font_id - font id returned by EPDFFont_RegisterFont() or +// EPDFFont_RegisterMemFont64(). +// font_size - the font size to be set. +// R, G, B - the color to be set. +// +// Returns true on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetDefaultAppearanceRegisteredFont(FPDF_ANNOTATION annot, + EPDF_FONT_ID font_id, + float font_size, + unsigned int R, + unsigned int G, + unsigned int B); + // Experimental EmbedPDF Extension API. // Get the default appearance of a FreeText annotation. // @@ -1690,10 +1710,42 @@ EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, // Notes: // * Only valid for FPDF_ANNOT_LINK annotations. // * The action must be an indirect object. -// * Any existing /A entry will be replaced. +// * Any existing /A entry will be replaced, and any direct /Dest entry is +// removed — ISO 32000-1 Table 173 forbids a link dictionary carrying +// both, so setting an action leaves the dictionary spec-clean. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action); +// Experimental EmbedPDF Extension API. +// Remove the /A action entry of a Link annotation. +// +// annot - handle to a link annotation. +// +// Returns true when the entry is absent afterwards, including when there +// was none to begin with (idempotent). False for non-link annotations. +// +// Notes: +// * Only valid for FPDF_ANNOT_LINK annotations. +// * Removes ONLY /A. A link may also carry a direct /Dest — remove it +// with EPDFAnnot_RemoveDest() when the intent is a target-less link, +// otherwise the /Dest becomes the link's effective target again. +// * The removed action dictionary itself is not garbage-collected; like +// every unreferenced indirect object it is dropped by a full save. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_RemoveAction(FPDF_ANNOTATION annot); + +// Experimental EmbedPDF Extension API. +// Remove the direct /Dest destination entry of a Link annotation. +// +// annot - handle to a link annotation. +// +// Returns true when the entry is absent afterwards, including when there +// was none to begin with (idempotent). False for non-link annotations. +// +// Notes: +// * Only valid for FPDF_ANNOT_LINK annotations. +// * Removes ONLY /Dest; an /A action entry is left untouched. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_RemoveDest(FPDF_ANNOTATION annot); + // Experimental EmbedPDF Extension API. // Get the annotation count. // @@ -1860,22 +1912,6 @@ EPDFAnnot_SetOverlayTextRepeat(FPDF_ANNOTATION annot, FPDF_BOOL repeat); FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot); -// Experimental EmbedPDF Extension API. -// Flatten an annotation's normal appearance (AP/N) to page content. -// The annotation's appearance becomes part of the page itself. -// The annotation is automatically removed from the page after flattening. -// -// The caller is responsible for: -// 1. Closing the annotation handle with FPDFPage_CloseAnnot after this call -// 2. Calling FPDFPage_GenerateContent to persist changes -// -// page - handle to the page containing the annotation -// annot - handle to an annotation with an appearance stream -// -// Returns TRUE on success, FALSE if no appearance stream or error. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, - FPDF_ANNOTATION annot); - // Experimental EmbedPDF Extension API. // Set an annotation's normal appearance (AP/N) from a page of another document. // The page's content stream and resources are deep-cloned into the annotation's @@ -2043,103 +2079,6 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearMKColor(FPDF_ANNOTATION annot, EPDF_MK_COLORTYPE type); -// Experimental EmbedPDF Extension API. -// Create a form field widget annotation on a page. This is the form-field -// counterpart of FPDFPage_CreateAnnot -- it creates the /Widget annotation, -// a parent field dictionary with /FT and base /Ff, wires /Parent//Kids, -// registers the field in /AcroForm/Fields, and notifies the interactive form -// model so that subsequent FPDFAnnot_SetFormFieldFlags etc. calls work. -// -// page - handle to the page. -// handle - handle to the form fill module -// (FPDFDOC_InitFormFillEnvironment). field_type - one of -// FPDF_FORMFIELD_TEXTFIELD, FPDF_FORMFIELD_CHECKBOX, -// FPDF_FORMFIELD_RADIOBUTTON, FPDF_FORMFIELD_COMBOBOX, -// FPDF_FORMFIELD_LISTBOX, FPDF_FORMFIELD_PUSHBUTTON. -// field_name - the partial field name (/T). May be NULL for unnamed fields. -// -// Returns a handle to the new annotation, or NULL on failure. -// Caller must call FPDFPage_CloseAnnot() when done. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV -EPDFPage_CreateFormField(FPDF_PAGE page, - FPDF_FORMHANDLE handle, - int field_type, - FPDF_WIDESTRING field_name); - -// Experimental EmbedPDF Extension API. -// Set the value (/V) of a form field associated with a widget annotation. -// -// handle - handle to the form fill module (FPDFDOC_InitFormFillEnvironment). -// annot - handle to a widget annotation. -// value - the new value as a UTF-16LE string. -// -// Returns true on success, false if the annotation is not a form field -// or the value could not be set. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldValue(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - FPDF_WIDESTRING value); - -// Experimental EmbedPDF Extension API. -// Set the partial field name (/T) of a form field associated with a widget -// annotation. -// -// handle - handle to the form fill module (FPDFDOC_InitFormFillEnvironment). -// annot - handle to a widget annotation. -// name - the new partial field name as a UTF-16LE string. -// -// Returns true on success, false if the annotation is not a form field. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldName(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - FPDF_WIDESTRING name); - -// Experimental EmbedPDF Extension API. -// Returns the object number of the logical form field dictionary associated -// with a widget annotation. -// -// handle - handle to the form fill module (FPDFDOC_InitFormFillEnvironment). -// annot - handle to a widget annotation. -// -// Returns the field dictionary object number on success, or 0 if the -// annotation is not a form field or has no indirect field dictionary. -FPDF_EXPORT int FPDF_CALLCONV -EPDFAnnot_GetFormFieldObjectNumber(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot); - -// Experimental EmbedPDF Extension API. -// Re-parent the source widget field into the target widget field so both -// widgets share the same logical AcroForm field. -// -// handle - handle to the form fill module -// (FPDFDOC_InitFormFillEnvironment). source_annot - handle to the widget -// annotation whose field should be merged. target_annot - handle to the -// widget annotation whose field should be reused. -// -// Returns true on success, false if the annotations are not compatible form -// fields or the share operation could not be completed. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION source_annot, - FPDF_ANNOTATION target_annot); - -// Experimental EmbedPDF Extension API. -// Set the options (/Opt array) for a Choice form field (ComboBox or ListBox). -// Replaces any existing options with the provided labels. -// -// handle - handle to the form fill environment. -// annot - handle to a widget annotation backed by a Choice field. -// labels - array of |count| UTF-16LE option label strings. -// count - number of entries in |labels|. Pass 0 to clear all options. -// -// Returns true on success, false if the annotation is not a form field -// or if |labels| is NULL when |count| > 0. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetFormFieldOptions(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - const FPDF_WIDESTRING* labels, - int count); - // Experimental EmbedPDF Extension API. // Generate the appearance stream for a form field widget annotation. // The standard EPDFAnnot_GenerateAppearance does NOT handle Widget subtypes. @@ -2155,42 +2094,6 @@ EPDFAnnot_SetFormFieldOptions(FPDF_FORMHANDLE handle, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GenerateFormFieldAP(FPDF_ANNOTATION annot); -// Experimental EmbedPDF Extension API. -// Get the "export value" of a checkbox or radio button widget — the -// non-"Off" key in its /AP/N (Normal Appearance) dictionary. -// -// annot - handle to a widget annotation. -// buffer - buffer for holding the value string, encoded in UTF-16LE. -// buflen - length of the buffer in bytes. -// -// Returns the length of the string value in bytes (including the trailing -// NUL pair), or 0 when the annotation has no /AP/N dictionary or contains -// only an "Off" entry. -FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFAnnot_GetButtonExportValue(FPDF_ANNOTATION annot, - FPDF_WCHAR* buffer, - unsigned long buflen); - -// Experimental EmbedPDF Extension API. -// Get the raw /V value of a form field without Opt-array translation. -// For checkbox/radio fields, FPDFAnnot_GetFormFieldValue translates /V -// through the Opt array; this function returns the raw Name/String instead. -// For other field types, behaves identically to FPDFAnnot_GetFormFieldValue. -// -// hHandle - handle to the form fill module, returned by -// FPDFDOC_InitFormFillEnvironment(). -// annot - handle to a widget annotation. -// buffer - buffer for holding the value string, encoded in UTF-16LE. -// buflen - length of the buffer in bytes. -// -// Returns the length of the string value in bytes (including the trailing -// NUL pair), or 0 on error. -FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFAnnot_GetFormFieldRawValue(FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - FPDF_WCHAR* buffer, - unsigned long buflen); - // Experimental EmbedPDF Extension API. // Get the number of callout line points (/CL array) on a FreeText annotation. // diff --git a/public/fpdf_attachment.h b/public/fpdf_attachment.h index cc32447f0b..5aa9580972 100644 --- a/public/fpdf_attachment.h +++ b/public/fpdf_attachment.h @@ -5,9 +5,14 @@ #ifndef PUBLIC_FPDF_ATTACHMENT_H_ #define PUBLIC_FPDF_ATTACHMENT_H_ +#include + // NOLINTNEXTLINE(build/include) #include "fpdfview.h" +// For FPDF_FILEWRITE. NOLINTNEXTLINE(build/include) +#include "fpdf_save.h" + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -58,6 +63,41 @@ FPDFDoc_GetAttachment(FPDF_DOCUMENT document, int index); FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFDoc_DeleteAttachment(FPDF_DOCUMENT document, int index); +// Experimental EmbedPDF API. +// Get the |document|'s EmbeddedFiles name-tree KEY at |index|, encoded in +// UTF-16LE. The key is the tree's unique identifier for the entry; it is +// usually — but not necessarily — equal to the filespec's /UF file name +// returned by FPDFAttachment_GetName() (foreign PDFs may diverge, and /UF +// values may collide while keys cannot). |buffer| is only modified if +// |buflen| is longer than the length of the key. On errors, |buffer| is +// unmodified and the returned length is 0. +// +// document - handle to a document. +// index - the index of the embedded file. +// buffer - buffer for holding the key, encoded in UTF-16LE. +// buflen - length of the buffer in bytes. +// +// Returns the length of the key in bytes, or 0 on error. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFDoc_GetAttachmentKey(FPDF_DOCUMENT document, + int index, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF API. +// Find the current index of the embedded file whose EmbeddedFiles +// name-tree key equals |key|. Keys are unique within the tree, so at most +// one entry matches. Note that indices shift when attachments are added +// (the tree is name-sorted) or deleted — the returned index is only valid +// until the next mutation. +// +// document - handle to a document. +// key - the name-tree key to look for, encoded in UTF-16LE. +// +// Returns the index of the matching embedded file, or -1 if there is none. +FPDF_EXPORT int FPDF_CALLCONV +EPDFDoc_GetAttachmentIndexByKey(FPDF_DOCUMENT document, FPDF_WIDESTRING key); + // Experimental API. // Get the name of the |attachment| file. |buffer| is only modified if |buflen| // is longer than the length of the file name. On errors, |buffer| is unmodified @@ -172,6 +212,71 @@ FPDFAttachment_GetFile(FPDF_ATTACHMENT attachment, unsigned long buflen, unsigned long* out_buflen); +// Detailed outcome of the EPDFAttachment_ExtractFile* APIs. +typedef enum { + EPDFAttachmentExtractStatus_kSuccess = 0, + // The attachment has no embedded file stream (no /EF entry). + EPDFAttachmentExtractStatus_kNoFileStream = 1, + // The file stream could not be decoded. + EPDFAttachmentExtractStatus_kDecodeFailed = 2, + // The decoded file exceeds |max_decoded_bytes| (or 2^32 - 1 bytes, the + // largest size these APIs can report). + EPDFAttachmentExtractStatus_kSizeLimitExceeded = 3, + // Writing to the destination failed (invalid FPDF_FILEWRITE, a + // WriteBlock() failure, or an invalid output argument). The destination + // may contain a partial file. + EPDFAttachmentExtractStatus_kWriteFailed = 4, +} EPDFAttachmentExtractStatus; + +// Experimental EmbedPDF API. +// Decode the embedded file of |attachment| ONCE and write it to |file_write|. +// Unfiltered and Flate-compressed file streams (the overwhelmingly common +// cases) are written through |file_write| in bounded chunks without +// materializing the whole decoded file; other filter chains are decoded in +// memory first, then written out. Unlike the two-call +// FPDFAttachment_GetFile() pattern, the stream is never decoded twice. +// +// attachment - handle to an attachment. +// file_write - the destination; |version| must be 1 and +// |WriteBlock| non-null. +// max_decoded_bytes - fail with kSizeLimitExceeded once the decoded file +// would exceed this many bytes (decompression-bomb +// guard). 0 means unlimited. +// out_size - optional; receives the decoded file size in bytes. +// out_status - optional; receives the detailed status. +// +// Returns true on success. On failure the destination may contain a +// partial file. Output parameters are zeroed before any work is done. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ExtractFile(FPDF_ATTACHMENT attachment, + FPDF_FILEWRITE* file_write, + uint64_t max_decoded_bytes, + uint32_t* out_size, + EPDFAttachmentExtractStatus* out_status); + +// Experimental EmbedPDF API. +// Decode the embedded file of |attachment| ONCE into an owned memory buffer. +// The caller must release the returned buffer with EPDF_FreeBuffer(). A +// zero-byte embedded file is a valid success: true is returned with +// |*out_buffer| set to NULL and |*out_size| set to 0. +// +// attachment - handle to an attachment. +// max_decoded_bytes - fail with kSizeLimitExceeded once the decoded file +// would exceed this many bytes (decompression-bomb +// guard). 0 means unlimited. +// out_buffer - receives the owned buffer holding the decoded file. +// out_size - receives the decoded file size in bytes. +// out_status - optional; receives the detailed status. +// +// Returns true on success. Output parameters are zeroed before any work is +// done. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAttachment_ExtractFileToOwnedBuffer(FPDF_ATTACHMENT attachment, + uint64_t max_decoded_bytes, + void** out_buffer, + uint32_t* out_size, + EPDFAttachmentExtractStatus* out_status); + // Experimental API. // Get the MIME type (Subtype) of the embedded file |attachment|. |buffer| is // only modified if |buflen| is longer than the length of the MIME type string. diff --git a/public/fpdf_doc.h b/public/fpdf_doc.h index 2feee5303e..78632566a3 100644 --- a/public/fpdf_doc.h +++ b/public/fpdf_doc.h @@ -48,10 +48,10 @@ typedef enum { // The trapped status of the document. See section 14.10.2.4 "Trapped" of the // ISO 32000-1:2008 spec. typedef enum FPDF_TRAPPED_STATUS { - PDFTRAPPED_NOTSET = 0, // No /Trapped key - PDFTRAPPED_TRUE = 1, // Explicitly /Trapped /True - PDFTRAPPED_FALSE = 2, // Explicitly /Trapped /False - PDFTRAPPED_UNKNOWN = 3 // Explicitly /Trapped /Unknown or invalid + PDFTRAPPED_NOTSET = 0, // No /Trapped key + PDFTRAPPED_TRUE = 1, // Explicitly /Trapped /True + PDFTRAPPED_FALSE = 2, // Explicitly /Trapped /False + PDFTRAPPED_UNKNOWN = 3 // Explicitly /Trapped /Unknown or invalid } FPDF_TRAPPED_STATUS; // Get the first child of |bookmark|, or the first top-level bookmark item. @@ -227,6 +227,21 @@ FPDFAction_GetURIPath(FPDF_DOCUMENT document, FPDF_EXPORT int FPDF_CALLCONV FPDFDest_GetDestPageIndex(FPDF_DOCUMENT document, FPDF_DEST dest); +// Experimental EmbedPDF Extension API. +// Get the PDF indirect object number of the page targeted by |dest|. +// +// document - handle to the document containing the destination page. +// dest - handle to the destination. +// +// Returns the page dictionary object number (> 0) on success, or 0 if the +// document/destination is invalid, the target is not a visible page in the +// document, the page dictionary is a direct object, or the page is an XFA +// page. If the destination contains a numeric page index, it is resolved +// against |document|, matching FPDFDest_GetDestPageIndex() compatibility +// behavior. +FPDF_EXPORT unsigned int FPDF_CALLCONV +EPDFDest_GetPageObjectNumber(FPDF_DOCUMENT document, FPDF_DEST dest); + // Experimental API. // Get the view (fit type) specified by |dest|. // @@ -448,10 +463,9 @@ FPDF_GetPageLabel(FPDF_DOCUMENT document, // value - the value to set. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDF_SetMetaText(FPDF_DOCUMENT document, - FPDF_BYTESTRING tag, - FPDF_WIDESTRING value); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_SetMetaText(FPDF_DOCUMENT document, + FPDF_BYTESTRING tag, + FPDF_WIDESTRING value); // Experimental EmbedPDF Extension API. // Check if meta-data |tag| exists in |document|. @@ -460,8 +474,8 @@ EPDF_SetMetaText(FPDF_DOCUMENT document, // tag - the tag to check. // // Returns true if |tag| exists in |document|. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDF_HasMetaText(FPDF_DOCUMENT document, FPDF_BYTESTRING tag); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_HasMetaText(FPDF_DOCUMENT document, + FPDF_BYTESTRING tag); // Experimental EmbedPDF Extension API. // Get the trapped status of |document|. @@ -490,8 +504,8 @@ EPDF_SetMetaTrapped(FPDF_DOCUMENT document, FPDF_TRAPPED_STATUS status); // count all keys. // // Returns the number of keys (possibly 0). On error, returns 0. -FPDF_EXPORT int FPDF_CALLCONV -EPDF_GetMetaKeyCount(FPDF_DOCUMENT document, FPDF_BOOL custom_only); +FPDF_EXPORT int FPDF_CALLCONV EPDF_GetMetaKeyCount(FPDF_DOCUMENT document, + FPDF_BOOL custom_only); // Experimental EmbedPDF Extension API. // Get the name of the Info dictionary key at |index|. @@ -500,8 +514,8 @@ EPDF_GetMetaKeyCount(FPDF_DOCUMENT document, FPDF_BOOL custom_only); // index - 0-based key index in the order returned by PDFium. // custom_only - if true, indexes only over non-reserved (custom) keys; if // false, indexes over all keys. -// buffer - a buffer for the key name in UTF-8 with trailing NUL. May be NULL. -// buflen - the length of the buffer, in bytes. May be 0. +// buffer - a buffer for the key name in UTF-8 with trailing NUL. May be +// NULL. buflen - the length of the buffer, in bytes. May be 0. // // Returns the number of bytes in the key name including the trailing NUL, or 0 // on error (bad |document|, |index| out of range, etc.). If |buflen| is less @@ -521,38 +535,44 @@ EPDF_GetMetaKeyName(FPDF_DOCUMENT document, // Create a new destination array of the form [page /XYZ left top zoom]. // // page - handle to the destination page. -// has_left - whether |left| is specified; if false, |left| is encoded as null. -// left - the left coordinate, in page coordinates. -// has_top - whether |top| is specified; if false, |top| is encoded as null. -// top - the top coordinate, in page coordinates. -// has_zoom - whether |zoom| is specified; if false or |zoom|==0, encoded as null. -// zoom - the zoom factor (must be non-zero to be considered specified). +// has_left - whether |left| is specified; if false, |left| is encoded as +// null. left - the left coordinate, in page coordinates. has_top - +// whether |top| is specified; if false, |top| is encoded as null. top - +// the top coordinate, in page coordinates. has_zoom - whether |zoom| is +// specified; if false or |zoom|==0, encoded as null. zoom - the zoom +// factor (must be non-zero to be considered specified). // -// Returns a handle to the created (INDIRECT) destination array, or NULL on error. +// Returns a handle to the created (INDIRECT) destination array, or NULL on +// error. // // Notes: // * The returned object is an INDIRECT array suitable for use by // EPDFBookmark_SetDest() or EPDFAction_CreateGoTo(). // * Unspecified fields are encoded as PDF nulls. -FPDF_EXPORT FPDF_DEST FPDF_CALLCONV -EPDFDest_CreateXYZ(FPDF_PAGE page, - FPDF_BOOL has_left, FS_FLOAT left, - FPDF_BOOL has_top, FS_FLOAT top, - FPDF_BOOL has_zoom, FS_FLOAT zoom); +FPDF_EXPORT FPDF_DEST FPDF_CALLCONV EPDFDest_CreateXYZ(FPDF_PAGE page, + FPDF_BOOL has_left, + FS_FLOAT left, + FPDF_BOOL has_top, + FS_FLOAT top, + FPDF_BOOL has_zoom, + FS_FLOAT zoom); // Experimental EmbedPDF Extension API. // Create a new destination array of the form [page / params…]. // // page - handle to the destination page. // view - one of the PDFDEST_VIEW_* constants EXCEPT PDFDEST_VIEW_XYZ. -// Valid: PDFDEST_VIEW_FIT, FITH, FITV, FITR, FITB, FITBH, FITBV. +// Valid: PDFDEST_VIEW_FIT, FITH, FITV, FITR, FITB, FITBH, +// FITBV. // params - pointer to an array of float parameters (may be NULL). // num_params - number of entries in |params|. // -// Returns a handle to the created (INDIRECT) destination array, or NULL on error. +// Returns a handle to the created (INDIRECT) destination array, or NULL on +// error. // // Notes: -// * The required parameter count depends on |view| and matches FPDFDest_GetView(). +// * The required parameter count depends on |view| and matches +// FPDFDest_GetView(). // Excess parameters are ignored; missing parameters default to 0. // * Use EPDFDest_CreateXYZ() for /XYZ destinations. FPDF_EXPORT FPDF_DEST FPDF_CALLCONV @@ -562,7 +582,8 @@ EPDFDest_CreateView(FPDF_PAGE page, unsigned long num_params); // Experimental EmbedPDF Extension API. -// Create a new *remote* destination array of the form [pageIndex / params…]. +// Create a new *remote* destination array of the form [pageIndex / +// params…]. // // document - handle to the owning document. // page_index - 0-based page index in the *remote* file (must be >= 0). @@ -570,7 +591,8 @@ EPDFDest_CreateView(FPDF_PAGE page, // params - pointer to float parameters (may be NULL). // num_params - number of parameters. // -// Returns a handle to the created (INDIRECT) destination array, or NULL on error. +// Returns a handle to the created (INDIRECT) destination array, or NULL on +// error. // // Notes: // * This is an explicit *remote* dest (first element is a number). @@ -589,16 +611,20 @@ EPDFDest_CreateRemoteView(FPDF_DOCUMENT document, // page_index - 0-based page index in the *remote* file (must be >= 0). // has_left,left,has_top,top,has_zoom,zoom - as in EPDFDest_CreateXYZ(). // -// Returns a handle to the created (INDIRECT) destination array, or NULL on error. +// Returns a handle to the created (INDIRECT) destination array, or NULL on +// error. // // Notes: // * The left/top/zoom fields are encoded as number-or-null as in local /XYZ. FPDF_EXPORT FPDF_DEST FPDF_CALLCONV EPDFDest_CreateRemoteXYZ(FPDF_DOCUMENT document, int page_index, - FPDF_BOOL has_left, FS_FLOAT left, - FPDF_BOOL has_top, FS_FLOAT top, - FPDF_BOOL has_zoom, FS_FLOAT zoom); + FPDF_BOOL has_left, + FS_FLOAT left, + FPDF_BOOL has_top, + FS_FLOAT top, + FPDF_BOOL has_zoom, + FS_FLOAT zoom); // ----------------------------------------------------------------------------- // Named destinations @@ -609,7 +635,8 @@ EPDFDest_CreateRemoteXYZ(FPDF_DOCUMENT document, // // document - handle to the document owning both the name tree and |dest|. // name - UTF-8 zero-terminated name key. -// dest - handle to an INDIRECT destination array that belongs to |document|. +// dest - handle to an INDIRECT destination array that belongs to +// |document|. // // Returns true on success. // @@ -628,8 +655,8 @@ EPDFNamedDest_SetDest(FPDF_DOCUMENT document, // name - UTF-8 zero-terminated name key. // // Returns true on success (or if the name did not exist). -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFNamedDest_Remove(FPDF_DOCUMENT document, FPDF_BYTESTRING name); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFNamedDest_Remove(FPDF_DOCUMENT document, + FPDF_BYTESTRING name); // ----------------------------------------------------------------------------- // Actions @@ -759,8 +786,8 @@ EPDFBookmark_Create(FPDF_DOCUMENT document, FPDF_WIDESTRING title); // * Removes the node from its parent's list and deletes the entire subtree, // fixing /First, /Last, /Prev, /Next as needed. // * Fails if |bookmark| does not belong to |document|. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFBookmark_Delete(FPDF_DOCUMENT document, FPDF_BOOKMARK bookmark); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFBookmark_Delete(FPDF_DOCUMENT document, + FPDF_BOOKMARK bookmark); // Experimental EmbedPDF Outline API. // Create and append a new child bookmark under |parent|. @@ -823,11 +850,11 @@ EPDFBookmark_SetTitle(FPDF_BOOKMARK bookmark, FPDF_WIDESTRING title); // Notes: // * On success, /Dest is set to an indirect reference to |dest| and any /A is // removed. -// * |dest| must belong to |document| and be indirect; otherwise the call fails. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFBookmark_SetDest(FPDF_DOCUMENT document, - FPDF_BOOKMARK bookmark, - FPDF_DEST dest); +// * |dest| must belong to |document| and be indirect; otherwise the call +// fails. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFBookmark_SetDest(FPDF_DOCUMENT document, + FPDF_BOOKMARK bookmark, + FPDF_DEST dest); // Experimental EmbedPDF Extension API. // Set the target of |bookmark| to |action| (clears any existing destination). @@ -841,7 +868,8 @@ EPDFBookmark_SetDest(FPDF_DOCUMENT document, // Notes: // * On success, /A is set to an indirect reference to |action| and any /Dest // is removed. -// * |action| must belong to |document| and be indirect; otherwise the call fails. +// * |action| must belong to |document| and be indirect; otherwise the call +// fails. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFBookmark_SetAction(FPDF_DOCUMENT document, FPDF_BOOKMARK bookmark, @@ -862,8 +890,7 @@ EPDFBookmark_ClearTarget(FPDF_BOOKMARK bookmark); // document - handle to the document. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFBookmark_Clear(FPDF_DOCUMENT document); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFBookmark_Clear(FPDF_DOCUMENT document); #ifdef __cplusplus } // extern "C" diff --git a/public/fpdf_flatten.h b/public/fpdf_flatten.h index aba5186baf..ab8c3038f1 100644 --- a/public/fpdf_flatten.h +++ b/public/fpdf_flatten.h @@ -37,6 +37,47 @@ extern "C" { // cause. FPDF_EXPORT int FPDF_CALLCONV FPDFPage_Flatten(FPDF_PAGE page, int nFlag); +// Experimental EmbedPDF Extension API. +// Flatten every eligible annotation appearance on a page. This is the +// layer-safe counterpart to FPDFPage_Flatten(): the page dictionary is +// promoted before any mutation. +// +// Only annotations whose normal appearance was successfully added to page +// content are removed. Hidden, usage-ineligible, popup, malformed, and +// appearance-less annotations remain in /Annots. Flattened widgets are also +// detached from the AcroForm field tree; a separate logical field remains as +// an unplaced field. +// +// page - handle to the page. +// usage - exactly one of the |FLAT_*| values. +// +// Returns FLATTEN_SUCCESS if at least one annotation was flattened, +// FLATTEN_NOTHINGTODO if the page has no eligible usable appearances, or +// FLATTEN_FAIL for invalid arguments. +FPDF_EXPORT int FPDF_CALLCONV EPDFPage_Flatten(FPDF_PAGE page, int usage); + +// Experimental EmbedPDF Extension API. +// Flatten one annotation from a page. The annotation may have been loaded by +// index, /NM, object number, or any other API returning an FPDF_ANNOTATION +// handle. Eligibility, appearance resolution, widget cleanup, and layer +// promotion are identical to EPDFPage_Flatten(). +// +// The caller must close |annot| with FPDFPage_CloseAnnot() after this call. +// A successful call removes the annotation from the page, so the handle must +// not be used again before it is closed. +// +// page - handle to the page containing |annot|. +// annot - handle to the annotation to flatten. +// usage - exactly one of the |FLAT_*| values. +// +// Returns FLATTEN_SUCCESS if the annotation was flattened, +// FLATTEN_NOTHINGTODO if it belongs to the page but is ineligible or has no +// usable normal appearance, or FLATTEN_FAIL for invalid arguments or when +// |annot| does not belong to |page|. +FPDF_EXPORT int FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, + FPDF_ANNOTATION annot, + int usage); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/testing/resources/flatten_selective.in b/testing/resources/flatten_selective.in new file mode 100644 index 0000000000..7787372371 --- /dev/null +++ b/testing/resources/flatten_selective.in @@ -0,0 +1,120 @@ +{{header}} +{{object 1 0}} +<< /Type /Catalog /Pages 2 0 R /AcroForm 11 0 R >> +endobj +{{object 2 0}} +<< + /Type /Pages + /Count 1 + /Kids [3 0 R] + /MediaBox [0 0 200 200] + /Resources << /ExtGState << /GS0 << /Type /ExtGState /CA 1 >> >> >> +>> +endobj +{{object 3 0}} +<< + /Type /Page + /Parent 2 0 R + /Contents 15 0 R + /Annots [4 0 R 5 0 R 6 0 R 9 0 R 13 0 R 16 0 R] +>> +endobj +{{object 4 0}} +<< + /Type /Annot + /Subtype /Square + /Rect [10 10 40 40] + /F 4 + /AP << /N 7 0 R >> +>> +endobj +{{object 5 0}} +<< + /Type /Annot + /Subtype /Square + /Rect [50 10 80 40] + /F 2 + /AP << /N 8 0 R >> +>> +endobj +{{object 6 0}} +<< + /Type /Annot + /Subtype /Text + /Rect [90 10 120 40] +>> +endobj +{{object 7 0}} +<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources <<>> {{streamlen}} >> +stream +0 0 1 rg 0 0 10 10 re f +endstream +endobj +{{object 8 0}} +<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources <<>> {{streamlen}} >> +stream +1 0 0 rg 0 0 10 10 re f +endstream +endobj +{{object 9 0}} +<< + /Type /Annot + /Subtype /Square + /Rect [130 10 160 40] + /F 1 + /AP << /N 10 0 R >> +>> +endobj +{{object 10 0}} +<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources <<>> {{streamlen}} >> +stream +0 1 0 rg 0 0 10 10 re f +endstream +endobj +{{object 11 0}} +<< /Fields [12 0 R] >> +endobj +{{object 12 0}} +<< /FT /Tx /T (Separate field) /Kids [13 0 R] >> +endobj +{{object 13 0}} +<< + /Type /Annot + /Subtype /Widget + /Parent 12 0 R + /Rect [10 60 80 85] + /F 4 + /AP << /N 14 0 R >> +>> +endobj +{{object 14 0}} +<< /Type /XObject /Subtype /Form /BBox [0 0 70 25] /Resources <<>> {{streamlen}} >> +stream +0.8 0.8 0.8 rg 0 0 70 25 re f +endstream +endobj +{{object 15 0}} +<< {{streamlen}} >> +stream +0 0 m 1 1 l S +endstream +endobj +{{object 16 0}} +<< + /Type /Annot + /Subtype /Square + /Rect [90 60 120 90] + /F 0 + /AP << /N 17 0 R >> +>> +endobj +{{object 17 0}} +<< /Type /XObject /Subtype /Form /BBox [0 0 10 10] /Resources <<>> {{streamlen}} >> +stream +0 1 1 rg 0 0 10 10 re f +endstream +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/flatten_selective.pdf b/testing/resources/flatten_selective.pdf new file mode 100644 index 0000000000..f53d181b1b Binary files /dev/null and b/testing/resources/flatten_selective.pdf differ diff --git a/testing/resources/fonts/DroidSansFallbackFull.ttf b/testing/resources/fonts/DroidSansFallbackFull.ttf new file mode 100644 index 0000000000..c1f08d9dcd Binary files /dev/null and b/testing/resources/fonts/DroidSansFallbackFull.ttf differ diff --git a/testing/resources/orphan_widgets.in b/testing/resources/orphan_widgets.in new file mode 100644 index 0000000000..b256c9fcb2 --- /dev/null +++ b/testing/resources/orphan_widgets.in @@ -0,0 +1,107 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R + /AcroForm << + /Fields [4 0 R] + /DR << /Font << /F1 7 0 R >> >> + /DA (0 g /F1 12 Tf) + >> +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 300 300] + /Annots [4 0 R 5 0 R 8 0 R 9 0 R] +>> +endobj +% A merged text field that is properly linked into /AcroForm /Fields. +{{object 4 0}} << + /Type /Annot + /Subtype /Widget + /FT /Tx + /T (linked_text) + /V (hello) + /DA (0 g /F1 12 Tf) + /Rect [20 250 280 280] +>> +endobj +% A merged checkbox that only exists in the page's /Annots array. It is not +% referenced from /AcroForm /Fields: recovered by the reconciliation sweep. +{{object 5 0}} << + /Type /Annot + /Subtype /Widget + /FT /Btn + /T (orphan_check) + /V /Yes + /AS /Yes + /Rect [20 200 40 220] + /AP << /N << /Yes 10 0 R /Off 11 0 R >> >> +>> +endobj +% A radio group whose parent field dictionary is not in /AcroForm /Fields. +% Only its widget kids appear in /Annots; the sweep must climb /Parent and +% reconcile both widgets onto one logical field. +{{object 6 0}} << + /FT /Btn + /T (orphan_radio) + /Ff 32768 + /V /a + /Kids [8 0 R 9 0 R] +>> +endobj +{{object 7 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{object 8 0}} << + /Type /Annot + /Subtype /Widget + /Parent 6 0 R + /AS /a + /Rect [20 150 40 170] + /AP << /N << /a 10 0 R /Off 11 0 R >> >> +>> +endobj +{{object 9 0}} << + /Type /Annot + /Subtype /Widget + /Parent 6 0 R + /AS /Off + /Rect [60 150 80 170] + /AP << /N << /b 10 0 R /Off 11 0 R >> >> +>> +endobj +{{object 10 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{object 11 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/orphan_widgets.pdf b/testing/resources/orphan_widgets.pdf new file mode 100644 index 0000000000..21aa272e4d Binary files /dev/null and b/testing/resources/orphan_widgets.pdf differ diff --git a/testing/resources/redact_inherited_colorspace.pdf b/testing/resources/redact_inherited_colorspace.pdf new file mode 100644 index 0000000000..3e1f835045 Binary files /dev/null and b/testing/resources/redact_inherited_colorspace.pdf differ diff --git a/testing/resources/toggle_fields.in b/testing/resources/toggle_fields.in new file mode 100644 index 0000000000..85a2d84e36 --- /dev/null +++ b/testing/resources/toggle_fields.in @@ -0,0 +1,162 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R + /AcroForm << + /Fields [4 0 R 5 0 R 8 0 R 12 0 R 16 0 R] + /DR << /Font << /F1 15 0 R >> >> + /DA (0 g /F1 12 Tf) + >> +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 300 300] + /Annots [4 0 R 6 0 R 7 0 R 9 0 R 10 0 R 11 0 R 12 0 R 17 0 R] +>> +endobj +% Text field with a length limit. +{{object 4 0}} << + /Type /Annot + /Subtype /Widget + /FT /Tx + /T (maxlen_text) + /V (abc) + /MaxLen 5 + /DA (0 g /F1 12 Tf) + /Rect [20 250 280 280] +>> +endobj +% Radio group with NoToggleToOff (Ff = radio 32768 + no-toggle-to-off 16384). +{{object 5 0}} << + /FT /Btn + /T (ntto_radio) + /Ff 49152 + /V /x + /DV /x + /Kids [6 0 R 7 0 R] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Widget + /Parent 5 0 R + /AS /x + /Rect [20 200 40 220] + /AP << /N << /x 13 0 R /Off 14 0 R >> >> +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Widget + /Parent 5 0 R + /AS /Off + /Rect [60 200 80 220] + /AP << /N << /y 13 0 R /Off 14 0 R >> >> +>> +endobj +% Radio group in unison (Ff = radio 32768 + radios-in-unison 33554432). +% Two widgets share the on-state /u1 and must check together. +{{object 8 0}} << + /FT /Btn + /T (unison_radio) + /Ff 33587200 + /V /Off + /Kids [9 0 R 10 0 R 11 0 R] +>> +endobj +{{object 9 0}} << + /Type /Annot + /Subtype /Widget + /Parent 8 0 R + /AS /Off + /Rect [20 150 40 170] + /AP << /N << /u1 13 0 R /Off 14 0 R >> >> +>> +endobj +{{object 10 0}} << + /Type /Annot + /Subtype /Widget + /Parent 8 0 R + /AS /Off + /Rect [60 150 80 170] + /AP << /N << /u1 13 0 R /Off 14 0 R >> >> +>> +endobj +{{object 11 0}} << + /Type /Annot + /Subtype /Widget + /Parent 8 0 R + /AS /Off + /Rect [100 150 120 170] + /AP << /N << /u2 13 0 R /Off 14 0 R >> >> +>> +endobj +% Merged checkbox carrying /Opt: checking it must set /V to the control +% index name ("0"), while its export value reads as "Alpha". +{{object 12 0}} << + /Type /Annot + /Subtype /Widget + /FT /Btn + /T (opt_check) + /Opt [(Alpha)] + /V /Off + /AS /Off + /Rect [20 100 40 120] + /AP << /N << /On 13 0 R /Off 14 0 R >> >> +>> +endobj +{{object 13 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{object 14 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{object 15 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +% Hierarchical field: non-terminal parent "billing" with a merged +% field/widget kid "name" - fully qualified name "billing.name". +{{object 16 0}} << + /FT /Tx + /T (billing) + /Kids [17 0 R] +>> +endobj +{{object 17 0}} << + /Type /Annot + /Subtype /Widget + /T (name) + /Parent 16 0 R + /DA (0 g /F1 12 Tf) + /Rect [20 40 280 70] +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/toggle_fields.pdf b/testing/resources/toggle_fields.pdf new file mode 100644 index 0000000000..c7c103bd11 Binary files /dev/null and b/testing/resources/toggle_fields.pdf differ diff --git a/testing/resources/two_plane_form.pdf b/testing/resources/two_plane_form.pdf new file mode 100644 index 0000000000..210d6cc235 Binary files /dev/null and b/testing/resources/two_plane_form.pdf differ diff --git a/testing/resources/widgets_no_acroform.in b/testing/resources/widgets_no_acroform.in new file mode 100644 index 0000000000..69cbca4ea2 --- /dev/null +++ b/testing/resources/widgets_no_acroform.in @@ -0,0 +1,84 @@ +{{header}} +% A document with form widgets but NO /AcroForm dictionary at all, plus a +% radio group whose second widget is absent from its parent's /Kids. +% Exercises every EPDFForm_Repair fix: AcroForm bootstrap, /Fields linking, +% and /Kids linking. +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 300 300] + /Annots [4 0 R 6 0 R 7 0 R] +>> +endobj +{{object 4 0}} << + /Type /Annot + /Subtype /Widget + /FT /Tx + /T (orphan_text) + /V (hi) + /Rect [20 250 280 280] +>> +endobj +% Radio parent: /Kids lists only widget 6; widget 7 references it via +% /Parent but is only reachable through the page's /Annots. +{{object 5 0}} << + /FT /Btn + /T (gap_radio) + /Ff 32768 + /V /Off + /Kids [6 0 R] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Widget + /Parent 5 0 R + /AS /Off + /Rect [20 200 40 220] + /AP << /N << /a 8 0 R /Off 9 0 R >> >> +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Widget + /Parent 5 0 R + /AS /Off + /Rect [60 200 80 220] + /AP << /N << /b 8 0 R /Off 9 0 R >> >> +>> +endobj +{{object 8 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{object 9 0}} << + /Type /XObject + /Subtype /Form + /BBox [0 0 20 20] + {{streamlen}} +>> +stream +q Q +endstream +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/widgets_no_acroform.pdf b/testing/resources/widgets_no_acroform.pdf new file mode 100644 index 0000000000..6a120897c4 Binary files /dev/null and b/testing/resources/widgets_no_acroform.pdf differ