diff --git a/.github/workflows/pdfium-tsan-thread-soak.yml b/.github/workflows/pdfium-tsan-thread-soak.yml new file mode 100644 index 0000000000..e4347fc457 --- /dev/null +++ b/.github/workflows/pdfium-tsan-thread-soak.yml @@ -0,0 +1,79 @@ +name: PDFium TSAN Thread Soak + +on: + workflow_dispatch: + inputs: + ref: + description: Git ref to test + required: false + default: embedpdf/main + pdf_path: + description: PDF fixture to render during the soak + required: false + default: testing/resources/hello_world.pdf + threads: + description: Number of worker threads + required: false + default: '2' + iterations: + description: Iterations per worker thread + required: false + default: '2' + +permissions: + contents: read + +jobs: + tsan-thread-soak: + name: Linux x64 TSAN thread soak + runs-on: ubuntu-24.04 + timeout-minutes: 90 + env: + PDF_RUNTIME_SYNC: auto + PDF_RUNTIME_TARGET_OS_LIST: linux + EMBEDPDF_TLS_GLOBALS: true + EMBEDPDF_TSAN: 1 + PDF_PATH: ${{ inputs.pdf_path }} + SOAK_THREADS: ${{ inputs.threads }} + SOAK_ITERATIONS: ${{ inputs.iterations }} + SOAK_OUT: out/embedpdf-runtime-thread-soak-linux-x64 + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref_name }} + + - name: Install depot_tools + shell: bash + run: | + git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git "$RUNNER_TEMP/depot_tools" + echo "$RUNNER_TEMP/depot_tools" >> "$GITHUB_PATH" + + - name: Install Linux base deps + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake clang lld curl g++ ninja-build pkg-config tar + + - name: Run TSAN thread soak + shell: bash + run: | + set -euo pipefail + mkdir -p "$SOAK_OUT" + scripts/embedpdf-runtime/thread-soak-target.sh \ + linux-x64 \ + "$PDF_PATH" \ + -- \ + --threads="$SOAK_THREADS" \ + --iterations="$SOAK_ITERATIONS" \ + 2>&1 | tee "$SOAK_OUT/thread-soak.log" + + - name: Upload TSAN logs + if: always() + uses: actions/upload-artifact@v6 + with: + name: pdfium-tsan-thread-soak-logs + path: | + out/embedpdf-runtime-thread-soak-linux-x64/thread-soak.log + out/embedpdf-runtime-thread-soak-linux-x64/args.gn + if-no-files-found: ignore diff --git a/BUILD.gn b/BUILD.gn index fbdb13a4e3..3b58698a15 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -51,6 +51,13 @@ config("pdfium_common_config") { defines += [ "PDF_USE_PARTITION_ALLOC" ] } + # EmbedPDF: thread-confined runtime. Translates the embedpdf_thread_local_globals + # GN arg into the EPDF_THREAD_LOCAL_GLOBALS define consumed by EPDF_TLS + # (core/fxcrt/epdf_tls.h). Default off; see pdfium.gni. + if (embedpdf_thread_local_globals) { + defines += [ "EPDF_THREAD_LOCAL_GLOBALS" ] + } + if (is_win) { # Assume UTF-8 by default to avoid code page dependencies. cflags += [ "/utf-8" ] @@ -191,6 +198,16 @@ 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", "public/fpdf_catalog.h", @@ -431,6 +448,9 @@ group("pdfium_all") { ":pdfium_unittests", "testing:pdfium_test", "testing/fuzzers", + "testing/tools:epdf_layer_memory_benchmark", + "testing/tools:epdf_layer_replay_soak", + "testing/tools:epdf_thread_soak", ] if (pdf_is_standalone) { diff --git a/core/fpdfapi/edit/BUILD.gn b/core/fpdfapi/edit/BUILD.gn index fd0b1eac7f..b5a8ab45bd 100644 --- a/core/fpdfapi/edit/BUILD.gn +++ b/core/fpdfapi/edit/BUILD.gn @@ -53,6 +53,7 @@ source_set("contentstream_write_utils") { pdfium_unittest_source_set("unittests") { sources = [ + "cpdf_creator_unittest.cpp", "cpdf_npagetooneexporter_unittest.cpp", "cpdf_pagecontentgenerator_unittest.cpp", ] diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index 612502abab..b03d76115d 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp @@ -8,10 +8,12 @@ #include +#include #include #include #include #include +#include #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_crypto_handler.h" @@ -44,10 +46,12 @@ constexpr Mask kAllValidFlags{ CPDF_Creator::CreateFlags::kIncremental, CPDF_Creator::CreateFlags::kNoOriginal, CPDF_Creator::CreateFlags::kRemoveSecurity, - CPDF_Creator::CreateFlags::kSubsetNewFonts}; + CPDF_Creator::CreateFlags::kSubsetNewFonts, + CPDF_Creator::CreateFlags::kIncrementalAppendOnly}; constexpr Mask kConflictingFlags{ CPDF_Creator::CreateFlags::kIncremental, CPDF_Creator::CreateFlags::kNoOriginal}; +constexpr FX_FILESIZE kMaxFourByteXrefOffset = 0xffffffff; class CFX_FileBufferArchive final : public IFX_ArchiveStream { public: @@ -56,6 +60,7 @@ class CFX_FileBufferArchive final : public IFX_ArchiveStream { bool WriteBlock(pdfium::span buffer) override; FX_FILESIZE CurrentOffset() const override { return offset_; } + void SetNotionalStartOffset(FX_FILESIZE offset) { offset_ = offset; } private: bool Flush(); @@ -128,6 +133,40 @@ bool OutputIndex(IFX_ArchiveStream* archive, FX_FILESIZE offset) { archive->WriteByte(0); } +ByteString FormatXrefOffset10(FX_FILESIZE offset) { + return ByteString::Format("%010" PRId64, static_cast(offset)); +} + +std::set CollectSaveReachableObjects( + CPDF_Document* document, + const CPDF_Dictionary* encrypt_dict) { + // CPDF_LayerDocument overlays new/promoted objects on a frozen base. + // References inherited from the base graph can still point through base + // holders, so resolving through the holder would skip overlay replacements. + // Walk through the layer document instead so the effective graph is what gets + // saved. + std::set objects = GetObjectsWithReferences( + document, document->IsLayerDocument() + ? ObjectTreeReferenceResolveMode::kEffectiveDocument + : ObjectTreeReferenceResolveMode::kReferenceHolder); + + // `GetObjectsWithReferences()` covers the normal document graph rooted at + // /Root. The save trailer may also reference dictionaries outside that graph. + // Keep those roots in sync with the trailer entries emitted in + // WriteDoc_Stage4(). + RetainPtr info = document->GetInfo(); + if (info && info->GetObjNum() != 0) { + objects.insert(info->GetObjNum()); + } + + if (encrypt_dict && !encrypt_dict->IsInline() && + encrypt_dict->GetObjNum() != 0) { + objects.insert(encrypt_dict->GetObjNum()); + } + + return objects; +} + } // namespace CPDF_Creator::CPDF_Creator(CPDF_Document* doc, @@ -141,6 +180,11 @@ CPDF_Creator::CPDF_Creator(CPDF_Document* doc, CPDF_Creator::~CPDF_Creator() = default; +// static +ByteString CPDF_Creator::FormatXrefOffset10ForTesting(FX_FILESIZE offset) { + return FormatXrefOffset10(offset); +} + bool CPDF_Creator::WriteIndirectObj(uint32_t objnum, const CPDF_Object* pObj) { if (!archive_->WriteDWord(objnum) || !archive_->WriteString(" 0 obj\r\n")) { return false; @@ -189,11 +233,9 @@ bool CPDF_Creator::WriteOldObjs() { return true; } - const std::set objects_with_refs = - GetObjectsWithReferences(document_); uint32_t last_object_number_written = 0; for (uint32_t objnum = cur_obj_num_; objnum <= nLastObjNum; ++objnum) { - if (!pdfium::Contains(objects_with_refs, objnum)) { + if (!pdfium::Contains(objects_with_refs_, objnum)) { continue; } if (!WriteOldIndirectObject(objnum)) { @@ -210,8 +252,13 @@ bool CPDF_Creator::WriteOldObjs() { } bool CPDF_Creator::WriteNewObjs() { + std::vector written_new_obj_nums; for (size_t i = cur_obj_num_; i < new_obj_num_array_.size(); ++i) { uint32_t objnum = new_obj_num_array_[i]; + if (!pdfium::Contains(objects_with_refs_, objnum)) { + continue; + } + RetainPtr pObj = document_->GetIndirectObject(objnum); if (!pObj) { continue; @@ -221,10 +268,20 @@ bool CPDF_Creator::WriteNewObjs() { if (!WriteIndirectObj(pObj->GetObjNum(), pObj.Get())) { return false; } + written_new_obj_nums.push_back(objnum); } + new_obj_num_array_ = std::move(written_new_obj_nums); return true; } +bool CPDF_Creator::CheckEmittedOffset(FX_FILESIZE offset) { + if (offset <= kMaxFourByteXrefOffset) { + return true; + } + failure_reason_ = FailureReason::kAppendOnlyOffsetTooLarge; + return false; +} + void CPDF_Creator::InitNewObjNumOffsets() { for (const auto& pair : *document_) { const uint32_t objnum = pair.first; @@ -269,13 +326,16 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage1() { } stage_ = Stage::kInitWriteObjs20; } else { - saved_offset_ = parser_->GetDocumentSize(); + saved_offset_ = is_incremental_append_only_ + ? document_->GetLayerAppendBaseOffset() + : parser_->GetDocumentSize(); stage_ = Stage::kWriteIncremental15; } } if (stage_ == Stage::kWriteIncremental15) { - if (is_original_ && saved_offset_ > 0) { + if (is_original_ && !is_incremental_append_only_ && saved_offset_ > 0) { if (!parser_->WriteToArchive(archive_.get(), saved_offset_)) { + failure_reason_ = FailureReason::kArchiveError; return Stage::kInvalid; } } @@ -348,7 +408,8 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() { uint32_t dwLastObjNum = last_obj_num_; if (stage_ == Stage::kInitWriteXRefs80) { xref_start_ = archive_->CurrentOffset(); - if (!is_incremental_ || !parser_->IsXRefStream()) { + if (!is_incremental_ || is_incremental_append_only_ || + !parser_->IsXRefStream()) { if (!is_incremental_ || parser_->GetLastXRefOffset() == 0) { ByteString str; str = pdfium::Contains(object_offsets_, 1) @@ -401,7 +462,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() { } while (i < j) { - str = ByteString::Format("%010d 00000 n\r\n", object_offsets_[i++]); + const FX_FILESIZE offset = object_offsets_[i++]; + if (!CheckEmittedOffset(offset)) { + return Stage::kInvalid; + } + str = FormatXrefOffset10(offset) + " 00000 n\r\n"; if (!archive_->WriteString(str.AsStringView())) { return Stage::kInvalid; } @@ -442,7 +507,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() { while (i < j) { objnum = new_obj_num_array_[i++]; - str = ByteString::Format("%010d 00000 n\r\n", object_offsets_[objnum]); + const FX_FILESIZE offset = object_offsets_[objnum]; + if (!CheckEmittedOffset(offset)) { + return Stage::kInvalid; + } + str = FormatXrefOffset10(offset) + " 00000 n\r\n"; if (!archive_->WriteString(str.AsStringView())) { return Stage::kInvalid; } @@ -456,7 +525,8 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() { CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { DCHECK(stage_ >= Stage::kWriteTrailerAndFinish90); - bool bXRefStream = is_incremental_ && parser_->IsXRefStream(); + bool bXRefStream = is_incremental_ && !is_incremental_append_only_ && + parser_->IsXRefStream(); if (!bXRefStream) { if (!archive_->WriteString("trailer\r\n<<")) { return Stage::kInvalid; @@ -468,6 +538,12 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { } } + RetainPtr current_info = document_->GetInfo(); + const uint32_t current_info_objnum = + current_info ? current_info->GetObjNum() : 0; + const uint32_t parser_info_objnum = parser_ ? parser_->GetInfoObjNum() : 0; + const bool should_write_current_info = + current_info_objnum != 0 && current_info_objnum != parser_info_objnum; if (parser_) { CPDF_DictionaryLocker locker(parser_->GetCombinedTrailer()); for (const auto& it : locker) { @@ -476,7 +552,7 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { if (key == "Encrypt" || key == "Size" || key == "Filter" || key == "Index" || key == "Length" || key == "Prev" || key == "W" || key == "XRefStm" || key == "ID" || key == "DecodeParms" || - key == "Type") { + key == "Type" || (key == "Info" && should_write_current_info)) { continue; } if (!archive_->WriteString(("/")) || @@ -493,12 +569,12 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { !archive_->WriteString(" 0 R\r\n")) { return Stage::kInvalid; } - if (document_->GetInfo()) { - if (!archive_->WriteString("/Info ") || - !archive_->WriteDWord(document_->GetInfo()->GetObjNum()) || - !archive_->WriteString(" 0 R\r\n")) { - return Stage::kInvalid; - } + } + if (should_write_current_info) { + if (!archive_->WriteString("/Info ") || + !archive_->WriteDWord(current_info_objnum) || + !archive_->WriteString(" 0 R\r\n")) { + return Stage::kInvalid; } } if (encrypt_dict_) { @@ -562,6 +638,9 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { if (it == object_offsets_.end()) { continue; } + if (!CheckEmittedOffset(it->second)) { + return Stage::kInvalid; + } if (!OutputIndex(archive_.get(), it->second)) { return Stage::kInvalid; } @@ -581,8 +660,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { return Stage::kInvalid; } for (i = 0; i < count; ++i) { - if (!OutputIndex(archive_.get(), - object_offsets_[new_obj_num_array_[i]])) { + const FX_FILESIZE offset = object_offsets_[new_obj_num_array_[i]]; + if (!CheckEmittedOffset(offset)) { + return Stage::kInvalid; + } + if (!OutputIndex(archive_.get(), offset)) { return Stage::kInvalid; } } @@ -603,6 +685,7 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() { } bool CPDF_Creator::Create(Mask flags, int32_t file_version) { + failure_reason_ = FailureReason::kNone; if (flags & ~kAllValidFlags) { flags = CreateFlags::kNone; } @@ -617,7 +700,16 @@ bool CPDF_Creator::Create(Mask flags, int32_t file_version) { } is_incremental_ = !!(flags & CreateFlags::kIncremental); + is_incremental_append_only_ = !!(flags & CreateFlags::kIncrementalAppendOnly); + if (is_incremental_append_only_ && !is_incremental_) { + failure_reason_ = FailureReason::kOther; + return false; + } is_original_ = !(flags & CreateFlags::kNoOriginal); + if (is_incremental_append_only_ && parser_) { + static_cast(archive_.get()) + ->SetNotionalStartOffset(document_->GetLayerAppendBaseOffset()); + } if (file_version >= 10 && file_version <= 17) { file_version_ = file_version; @@ -627,9 +719,16 @@ bool CPDF_Creator::Create(Mask flags, int32_t file_version) { last_obj_num_ = document_->GetLastObjNum(); object_offsets_.clear(); new_obj_num_array_.clear(); + objects_with_refs_.clear(); InitID(); - return Continue(); + objects_with_refs_ = + CollectSaveReachableObjects(document_, encrypt_dict_.Get()); + const bool result = Continue(); + if (!result && failure_reason_ == FailureReason::kNone) { + failure_reason_ = FailureReason::kOther; + } + return result; } void CPDF_Creator::InitID() { diff --git a/core/fpdfapi/edit/cpdf_creator.h b/core/fpdfapi/edit/cpdf_creator.h index f96e708a7c..94ccd6343b 100644 --- a/core/fpdfapi/edit/cpdf_creator.h +++ b/core/fpdfapi/edit/cpdf_creator.h @@ -11,9 +11,11 @@ #include #include +#include #include #include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/fx_string.h" #include "core/fxcrt/mask.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/unowned_ptr.h" @@ -36,6 +38,14 @@ class CPDF_Creator { kRemoveSecurity = (1 << 2), // TODO(crbug.com/42270430): Implement font subsetting. kSubsetNewFonts = (1 << 3), + kIncrementalAppendOnly = (1 << 4), + }; + + enum class FailureReason { + kNone, + kAppendOnlyOffsetTooLarge, + kArchiveError, + kOther, }; CPDF_Creator(CPDF_Document* doc, @@ -43,10 +53,14 @@ class CPDF_Creator { ~CPDF_Creator(); bool Create(Mask flags, int32_t file_version); + FailureReason GetFailureReason() const { return failure_reason_; } + + static ByteString FormatXrefOffset10ForTesting(FX_FILESIZE offset); // Experimental EmbedPDF Extension: Set encryption for documents that weren't // originally encrypted. This sets both encrypt_dict_ (for trailer writing) - // and security_handler_ (for GetCryptoHandler() used in stream/string encryption). + // and security_handler_ (for GetCryptoHandler() used in stream/string + // encryption). void SetEncryption(RetainPtr encrypt_dict, RetainPtr security_handler); @@ -83,6 +97,7 @@ class CPDF_Creator { bool WriteOldObjs(); bool WriteNewObjs(); bool WriteIndirectObj(uint32_t objnum, const CPDF_Object* pObj); + bool CheckEmittedOffset(FX_FILESIZE offset); void RemoveSecurity(); @@ -101,11 +116,14 @@ class CPDF_Creator { FX_FILESIZE xref_start_ = 0; std::map object_offsets_; std::vector new_obj_num_array_; // Sorted, ascending. + std::set objects_with_refs_; RetainPtr id_array_; int32_t file_version_ = 0; bool security_changed_ = false; bool is_incremental_ = false; bool is_original_ = false; + bool is_incremental_append_only_ = false; + FailureReason failure_reason_ = FailureReason::kNone; }; #endif // CORE_FPDFAPI_EDIT_CPDF_CREATOR_H_ diff --git a/core/fpdfapi/edit/cpdf_creator_unittest.cpp b/core/fpdfapi/edit/cpdf_creator_unittest.cpp new file mode 100644 index 0000000000..4ea0c8ba10 --- /dev/null +++ b/core/fpdfapi/edit/cpdf_creator_unittest.cpp @@ -0,0 +1,23 @@ +// 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 "core/fpdfapi/edit/cpdf_creator.h" + +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CPDFCreatorTest, FormatXrefOffset10Is64BitClean) { + ByteString small = CPDF_Creator::FormatXrefOffset10ForTesting(42); + EXPECT_EQ("0000000042", small); + EXPECT_EQ(10u, small.GetLength()); + + ByteString mid = + CPDF_Creator::FormatXrefOffset10ForTesting(2500000000LL); + EXPECT_EQ("2500000000", mid); + EXPECT_EQ(10u, mid.GetLength()); + + ByteString max = + CPDF_Creator::FormatXrefOffset10ForTesting(0xffffffff); + EXPECT_EQ("4294967295", max); + EXPECT_EQ(10u, max.GetLength()); +} 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/font/cpdf_fontglobals.cpp b/core/fpdfapi/font/cpdf_fontglobals.cpp index bd57de4cfd..17f3b48c02 100644 --- a/core/fpdfapi/font/cpdf_fontglobals.cpp +++ b/core/fpdfapi/font/cpdf_fontglobals.cpp @@ -18,10 +18,13 @@ #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fxcrt/check.h" #include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/epdf_tls.h" namespace { -CPDF_FontGlobals* g_FontGlobals = nullptr; +// EmbedPDF: thread-confined runtime - each worker thread owns its own font +// globals (stock fonts + predefined CMaps), created/destroyed on that thread. +EPDF_TLS CPDF_FontGlobals* g_FontGlobals = nullptr; RetainPtr LoadPredefinedCMap(ByteStringView name) { if (!name.IsEmpty() && name[0] == '/') { diff --git a/core/fpdfapi/font/cpdf_type3font.cpp b/core/fpdfapi/font/cpdf_type3font.cpp index 00d95298c8..44b8ca7d03 100644 --- a/core/fpdfapi/font/cpdf_type3font.cpp +++ b/core/fpdfapi/font/cpdf_type3font.cpp @@ -60,7 +60,10 @@ void CPDF_Type3Font::WillBeDestroyed() { } bool CPDF_Type3Font::Load() { - font_resources_ = font_dict_->GetMutableDictFor("Resources"); + RetainPtr font_resources = + font_dict_->GetDictFor("Resources"); + font_resources_ = + pdfium::WrapRetain(const_cast(font_resources.Get())); RetainPtr pMatrix = font_dict_->GetArrayFor("FontMatrix"); float xscale = 1.0f; float yscale = 1.0f; @@ -93,7 +96,10 @@ bool CPDF_Type3Font::Load() { } } } - char_procs_ = font_dict_->GetMutableDictFor("CharProcs"); + RetainPtr char_procs = + font_dict_->GetDictFor("CharProcs"); + char_procs_ = + pdfium::WrapRetain(const_cast(char_procs.Get())); if (font_dict_->GetDirectObjectFor("Encoding")) { LoadPDFEncoding(false, false); } @@ -125,8 +131,10 @@ CPDF_Type3Char* CPDF_Type3Font::LoadChar(uint32_t charcode) { return nullptr; } + RetainPtr const_stream = + ToStream(char_procs_->GetDirectObjectFor(name)); RetainPtr pStream = - ToStream(char_procs_->GetMutableDirectObjectFor(name)); + pdfium::WrapRetain(const_cast(const_stream.Get())); if (!pStream) { return nullptr; } diff --git a/core/fpdfapi/page/cpdf_annotcontext.cpp b/core/fpdfapi/page/cpdf_annotcontext.cpp index dcaf973548..259e60d52d 100644 --- a/core/fpdfapi/page/cpdf_annotcontext.cpp +++ b/core/fpdfapi/page/cpdf_annotcontext.cpp @@ -10,24 +10,37 @@ #include "core/fpdfapi/page/cpdf_form.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_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/check.h" +#include "core/fxcrt/check_op.h" CPDF_AnnotContext::CPDF_AnnotContext(RetainPtr pAnnotDict, - IPDF_Page* pPage) - : annot_dict_(std::move(pAnnotDict)), page_(pPage) { + IPDF_Page* pPage, + int annot_index) + : annot_dict_(std::move(pAnnotDict)), + page_(pPage), + annot_index_(annot_index) { DCHECK(annot_dict_); DCHECK(page_); DCHECK(page_->AsPDFPage()); + annot_dict_epoch_ = page_->GetDocument()->GetOverlayEpoch(); } CPDF_AnnotContext::~CPDF_AnnotContext() = default; void CPDF_AnnotContext::SetForm(RetainPtr pStream) { CHECK(pStream); + CPDF_DocumentViewScope document_view(page_->GetDocument()); annot_form_ = std::make_unique( - page_->GetDocument(), page_->AsPDFPage()->GetMutableResources(), pStream); + page_->GetDocument(), + pdfium::WrapRetain(const_cast( + page_->AsPDFPage()->GetResources().Get())), + pStream); // The annotation expects the form content to be parsed with the identity // matrix (ignoring the matrix defined in the stream). To achieve this without @@ -38,3 +51,82 @@ void CPDF_AnnotContext::SetForm(RetainPtr pStream) { pStream->GetDict()->GetMatrixFor("Matrix").GetInverse(); annot_form_->ParseContent(nullptr, &inverse_stream_matrix, nullptr); } + +RetainPtr CPDF_AnnotContext::GetMutableAnnotDict() { + RefreshAnnotDictIfNeeded(); + + CPDF_Page* page = page_ ? page_->AsPDFPage() : nullptr; + CPDF_Document* doc = page ? page->GetDocument() : nullptr; + if (!doc) { + return annot_dict_; + } + + const uint32_t objnum = annot_dict_->GetObjNum(); + if (objnum != 0) { + RetainPtr live = doc->GetMutableIndirectObject(objnum); + if (live && live.Get() != annot_dict_.Get()) { + annot_dict_ = pdfium::WrapRetain(live->AsMutableDictionary()); + } + annot_dict_epoch_ = doc->GetOverlayEpoch(); + return annot_dict_; + } + + if (annot_dict_->IsFrozen()) { + EnsureMutableBackingForAnnotDict(); + } + annot_dict_epoch_ = doc->GetOverlayEpoch(); + return annot_dict_; +} + +const CPDF_Dictionary* CPDF_AnnotContext::GetAnnotDict() const { + RefreshAnnotDictIfNeeded(); + return annot_dict_.Get(); +} + +void CPDF_AnnotContext::RefreshAnnotDictIfNeeded() const { + CPDF_Page* page = page_ ? page_->AsPDFPage() : nullptr; + CPDF_Document* doc = page ? page->GetDocument() : nullptr; + if (!doc) { + return; + } + + const uint64_t current_epoch = doc->GetOverlayEpoch(); + if (annot_dict_epoch_ == current_epoch) { + return; + } + + CPDF_DocumentViewScope document_view(doc); + const uint32_t objnum = annot_dict_->GetObjNum(); + if (objnum != 0) { + RetainPtr effective = doc->GetIndirectObject(objnum); + const CPDF_Dictionary* effective_dict = + effective ? effective->AsDictionary() : nullptr; + if (effective_dict && effective_dict != annot_dict_.Get()) { + annot_dict_ = pdfium::WrapRetain( + const_cast(effective_dict)); + annot_form_.reset(); + } + } else if (annot_index_ >= 0) { + RetainPtr annots = page->GetAnnotsArray(); + RetainPtr effective = + annots && static_cast(annot_index_) < annots->size() + ? annots->GetDictAt(static_cast(annot_index_)) + : nullptr; + if (effective && effective.Get() != annot_dict_.Get()) { + annot_dict_ = + pdfium::WrapRetain(const_cast(effective.Get())); + annot_form_.reset(); + } + } + annot_dict_epoch_ = current_epoch; +} + +void CPDF_AnnotContext::EnsureMutableBackingForAnnotDict() { + CHECK_GE(annot_index_, 0); + CPDF_Page* page = page_->AsPDFPage(); + RetainPtr page_dict = page->GetMutableDict(); + RetainPtr annots = page_dict->GetMutableArrayFor("Annots"); + CHECK(annots); + annot_dict_ = annots->GetMutableDictAt(annot_index_); + CHECK(annot_dict_); +} diff --git a/core/fpdfapi/page/cpdf_annotcontext.h b/core/fpdfapi/page/cpdf_annotcontext.h index b1d008bf26..d156f293df 100644 --- a/core/fpdfapi/page/cpdf_annotcontext.h +++ b/core/fpdfapi/page/cpdf_annotcontext.h @@ -7,6 +7,8 @@ #ifndef CORE_FPDFAPI_PAGE_CPDF_ANNOTCONTEXT_H_ #define CORE_FPDFAPI_PAGE_CPDF_ANNOTCONTEXT_H_ +#include + #include #include "core/fxcrt/retain_ptr.h" @@ -19,7 +21,9 @@ class IPDF_Page; class CPDF_AnnotContext { public: - CPDF_AnnotContext(RetainPtr pAnnotDict, IPDF_Page* pPage); + CPDF_AnnotContext(RetainPtr pAnnotDict, + IPDF_Page* pPage, + int annot_index = -1); ~CPDF_AnnotContext(); void SetForm(RetainPtr pStream); @@ -27,16 +31,25 @@ class CPDF_AnnotContext { CPDF_Form* GetForm() const { return annot_form_.get(); } // Never nullptr. - RetainPtr GetMutableAnnotDict() { return annot_dict_; } - const CPDF_Dictionary* GetAnnotDict() const { return annot_dict_.Get(); } + RetainPtr GetMutableAnnotDict(); + const CPDF_Dictionary* GetAnnotDict() const; // 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: - std::unique_ptr annot_form_; - RetainPtr const annot_dict_; + void RefreshAnnotDictIfNeeded() const; + void EnsureMutableBackingForAnnotDict(); + + mutable std::unique_ptr annot_form_; + mutable RetainPtr annot_dict_; UnownedPtr const page_; + const int annot_index_ = -1; + mutable uint64_t annot_dict_epoch_ = 0; }; #endif // CORE_FPDFAPI_PAGE_CPDF_ANNOTCONTEXT_H_ diff --git a/core/fpdfapi/page/cpdf_colorspace.cpp b/core/fpdfapi/page/cpdf_colorspace.cpp index e49d7af436..186fe40b55 100644 --- a/core/fpdfapi/page/cpdf_colorspace.cpp +++ b/core/fpdfapi/page/cpdf_colorspace.cpp @@ -38,6 +38,7 @@ #include "core/fxcrt/check_op.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/fx_2d_size.h" #include "core/fxcrt/fx_safe_types.h" @@ -450,7 +451,9 @@ class StockColorSpaces { RetainPtr pattern_; }; -StockColorSpaces* g_stock_colorspaces = nullptr; +// EmbedPDF: thread-confined runtime - per-thread stock device colorspaces +// (gray/rgb/cmyk/pattern), created/destroyed on the owning thread. +EPDF_TLS StockColorSpaces* g_stock_colorspaces = nullptr; } // namespace diff --git a/core/fpdfapi/page/cpdf_contentparser.cpp b/core/fpdfapi/page/cpdf_contentparser.cpp index 1d6bd7b678..80e7c9a620 100644 --- a/core/fpdfapi/page/cpdf_contentparser.cpp +++ b/core/fpdfapi/page/cpdf_contentparser.cpp @@ -36,9 +36,8 @@ CPDF_ContentParser::CPDF_ContentParser(CPDF_Page* pPage) return; } - RetainPtr pContent = - pPage->GetMutableDict()->GetMutableDirectObjectFor( - pdfium::page_object::kContents); + RetainPtr pContent = + pPage->GetDict()->GetDirectObjectFor(pdfium::page_object::kContents); if (!pContent) { HandlePageContentFailure(); return; @@ -94,14 +93,17 @@ CPDF_ContentParser::CPDF_ContentParser( } } - RetainPtr pResources = - page_object_holder_->GetMutableDict()->GetMutableDictFor("Resources"); + RetainPtr pResources = + page_object_holder_->GetDict()->GetDictFor("Resources"); parser_ = std::make_unique( page_object_holder_->GetDocument(), - page_object_holder_->GetMutablePageResources(), - page_object_holder_->GetMutableResources(), pParentMatrix, - page_object_holder_, std::move(pResources), form_bbox, pGraphicStates, - recursion_state); + pdfium::WrapRetain(const_cast( + page_object_holder_->GetPageResources().Get())), + pdfium::WrapRetain(const_cast( + page_object_holder_->GetResources().Get())), + pParentMatrix, page_object_holder_, + pdfium::WrapRetain(const_cast(pResources.Get())), + form_bbox, pGraphicStates, recursion_state); parser_->GetCurStates()->set_current_transformation_matrix(form_matrix); parser_->GetCurStates()->set_parent_matrix(form_matrix); if (ClipPath.HasRef()) { @@ -214,8 +216,11 @@ CPDF_ContentParser::Stage CPDF_ContentParser::Parse() { recursion_state_.parsed_set.clear(); parser_ = std::make_unique( page_object_holder_->GetDocument(), - page_object_holder_->GetMutablePageResources(), nullptr, nullptr, - page_object_holder_, page_object_holder_->GetMutableResources(), + pdfium::WrapRetain(const_cast( + page_object_holder_->GetPageResources().Get())), + nullptr, nullptr, page_object_holder_, + pdfium::WrapRetain(const_cast( + page_object_holder_->GetResources().Get())), page_object_holder_->GetBBox(), nullptr, &recursion_state_); parser_->GetCurStates()->mutable_color_state().SetDefault(); } diff --git a/core/fpdfapi/page/cpdf_docpagedata.cpp b/core/fpdfapi/page/cpdf_docpagedata.cpp index 2c3f841df9..a40e1e9369 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.cpp +++ b/core/fpdfapi/page/cpdf_docpagedata.cpp @@ -178,6 +178,9 @@ CPDF_DocPageData* CPDF_DocPageData::FromDocument(const CPDF_Document* doc) { CPDF_DocPageData::CPDF_DocPageData() = default; +CPDF_DocPageData::CPDF_DocPageData(CPDF_DocPageData* fallback) + : fallback_(fallback) {} + CPDF_DocPageData::~CPDF_DocPageData() { for (auto& it : image_map_) { it.second->WillBeDestroyed(); @@ -220,6 +223,10 @@ RetainPtr CPDF_DocPageData::GetFont( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && CanUseFallbackForObject(font_dict.Get())) { + return fallback_->GetFont(font_dict); + } + RetainPtr font = CPDF_Font::Create(GetDocument(), font_dict, this); if (!font) { return nullptr; @@ -370,6 +377,10 @@ RetainPtr CPDF_DocPageData::GetColorSpaceInternal( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && CanUseFallbackForObject(pArray.Get())) { + return fallback_->GetColorSpaceGuarded(pCSObj, pResources, pVisited); + } + RetainPtr pCS = CPDF_ColorSpace::Load(GetDocument(), pArray.Get(), pVisited); if (!pCS) { @@ -390,6 +401,10 @@ RetainPtr CPDF_DocPageData::GetPattern( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && CanUseFallbackForObject(pPatternObj.Get())) { + return fallback_->GetPattern(pPatternObj, matrix); + } + RetainPtr pattern; switch (pPatternObj->GetDict()->GetIntegerFor("PatternType")) { case CPDF_Pattern::kTiling: @@ -417,6 +432,10 @@ RetainPtr CPDF_DocPageData::GetShading( return pdfium::WrapRetain(it->second->AsShadingPattern()); } + if (fallback_ && CanUseFallbackForObject(pPatternObj.Get())) { + return fallback_->GetShading(pPatternObj, matrix); + } + auto pPattern = pdfium::MakeRetain( GetDocument(), pPatternObj, true, matrix); pattern_map_[pPatternObj].Reset(pPattern.Get()); @@ -427,9 +446,15 @@ RetainPtr CPDF_DocPageData::GetImage(uint32_t dwStreamObjNum) { DCHECK(dwStreamObjNum); auto it = image_map_.find(dwStreamObjNum); if (it != image_map_.end()) { + if (GetDocument()->IsObjectPromoted(dwStreamObjNum)) { + it->second->RebindStreamIfPromoted(); + } return it->second; } + // CPDF_Image carries a document back-pointer and CPDF_PageImageCache rejects + // cross-document images. Build a document-local CPDF_Image even when the + // underlying stream falls through to a shared base document. auto pImage = pdfium::MakeRetain(GetDocument(), dwStreamObjNum); image_map_[dwStreamObjNum] = pImage; return pImage; @@ -452,6 +477,10 @@ RetainPtr CPDF_DocPageData::GetIccProfile( return it->second; } + if (fallback_ && CanUseFallbackForObject(pProfileStream.Get())) { + return fallback_->GetIccProfile(pProfileStream); + } + auto pAccessor = pdfium::MakeRetain(pProfileStream); pAccessor->LoadAllDataFiltered(); @@ -486,6 +515,10 @@ RetainPtr CPDF_DocPageData::GetFontFileStreamAcc( return it->second; } + if (fallback_ && CanUseFallbackForObject(font_stream.Get())) { + return fallback_->GetFontFileStreamAcc(font_stream); + } + RetainPtr font_dict = font_stream->GetDict(); int32_t len1 = font_dict->GetIntegerFor("Length1"); int32_t len2 = font_dict->GetIntegerFor("Length2"); @@ -522,6 +555,16 @@ void CPDF_DocPageData::MaybePurgeFontFileStreamAcc( } } +bool CPDF_DocPageData::CanUseFallbackForObject( + const CPDF_Object* object) const { + if (!object) { + return false; + } + + const uint32_t objnum = object->GetObjNum(); + return objnum != 0 && !GetDocument()->IsObjectPromoted(objnum); +} + std::unique_ptr CPDF_DocPageData::CreateForm( CPDF_Document* document, RetainPtr pPageResources, diff --git a/core/fpdfapi/page/cpdf_docpagedata.h b/core/fpdfapi/page/cpdf_docpagedata.h index a46ff6ecc0..ebaeb47210 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.h +++ b/core/fpdfapi/page/cpdf_docpagedata.h @@ -19,6 +19,7 @@ #include "core/fxcrt/fx_codepage_forward.h" #include "core/fxcrt/fx_coordinates.h" #include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" class CFX_Font; class CPDF_Dictionary; @@ -36,8 +37,11 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, static CPDF_DocPageData* FromDocument(const CPDF_Document* doc); CPDF_DocPageData(); + explicit CPDF_DocPageData(CPDF_DocPageData* fallback); ~CPDF_DocPageData() override; + void SetFallback(CPDF_DocPageData* fallback) { fallback_ = fallback; } + // CPDF_Document::PageDataIface: void ClearStockFont() override; RetainPtr GetFontFileStreamAcc( @@ -109,6 +113,7 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, const CPDF_Dictionary* pResources, std::set* pVisited, std::set* pVisitedInternal); + bool CanUseFallbackForObject(const CPDF_Object* object) const; size_t CalculateEncodingDict(FX_Charset charset, CPDF_Dictionary* pBaseDict); RetainPtr ProcessbCJK( @@ -118,6 +123,7 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, std::function Insert); bool force_clear_ = false; + UnownedPtr fallback_; // Specific destruction order may be required between maps. std::map> diff --git a/core/fpdfapi/page/cpdf_form.cpp b/core/fpdfapi/page/cpdf_form.cpp index 2f55ac308c..e64b586d02 100644 --- a/core/fpdfapi/page/cpdf_form.cpp +++ b/core/fpdfapi/page/cpdf_form.cpp @@ -14,6 +14,9 @@ #include "core/fpdfapi/page/cpdf_pageobject.h" #include "core/fpdfapi/page/cpdf_pageobjectholder.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/check_op.h" #include "core/fxge/dib/cfx_dibitmap.h" @@ -23,10 +26,10 @@ CPDF_Form::RecursionState::RecursionState() = default; CPDF_Form::RecursionState::~RecursionState() = default; // static -CPDF_Dictionary* CPDF_Form::ChooseResourcesDict( - CPDF_Dictionary* pResources, - CPDF_Dictionary* pParentResources, - CPDF_Dictionary* pPageResources) { +const CPDF_Dictionary* CPDF_Form::ChooseResourcesDict( + const CPDF_Dictionary* pResources, + const CPDF_Dictionary* pParentResources, + const CPDF_Dictionary* pPageResources) { if (pResources) { return pResources; } @@ -45,17 +48,19 @@ CPDF_Form::CPDF_Form(CPDF_Document* doc, RetainPtr pPageResources, RetainPtr pFormStream, CPDF_Dictionary* pParentResources) - : CPDF_PageObjectHolder(doc, - pFormStream->GetMutableDict(), - pPageResources, - pdfium::WrapRetain(ChooseResourcesDict( - pFormStream->GetMutableDict() - ->GetMutableDictFor("Resources") - .Get(), - pParentResources, - pPageResources.Get()))), + : CPDF_PageObjectHolder( + doc, + pdfium::WrapRetain( + const_cast(pFormStream->GetDict().Get())), + pPageResources, + nullptr), + fallback_resources_( + pParentResources ? pdfium::WrapRetain(pParentResources) + : std::move(pPageResources)), form_stream_(std::move(pFormStream)) { - LoadTransparencyInfo(); + CPDF_DocumentViewScope document_view(doc); + RebindFormStream(form_stream_, /*reset_parsed_content=*/false); + form_stream_epoch_ = doc ? doc->GetOverlayEpoch() : 0; } CPDF_Form::~CPDF_Form() = default; @@ -78,13 +83,16 @@ void CPDF_Form::ParseContentInternal(const CPDF_AllStates* pGraphicStates, const CFX_Matrix* pParentMatrix, CPDF_Type3Char* pType3Char, RecursionState* recursion_state) { + CPDF_DocumentViewScope document_view(GetDocument()); + RetainPtr stream = GetStream(); + if (GetParseState() == ParseState::kParsed) { return; } if (GetParseState() == ParseState::kNotParsed) { StartParse(std::make_unique( - GetStream(), this, pGraphicStates, pParentMatrix, pType3Char, + std::move(stream), this, pGraphicStates, pParentMatrix, pType3Char, recursion_state ? recursion_state : &recursion_state_)); } DCHECK_EQ(GetParseState(), ParseState::kParsing); @@ -118,13 +126,85 @@ CFX_FloatRect CPDF_Form::CalcBoundingBox() const { } RetainPtr CPDF_Form::GetMutableFormStream() { + CPDF_Document* doc = GetDocument(); + if (!doc || !form_stream_) { + return form_stream_; + } + + // Rebind a cached base stream to the effective layer object before asking + // the document for a mutable version. + (void)GetStream(); + const uint32_t objnum = form_stream_->GetObjNum(); + DCHECK(objnum); + RetainPtr live = doc->GetMutableIndirectObject(objnum); + if (live && live.Get() != form_stream_.Get()) { + RebindFormStream(pdfium::WrapRetain(live->AsMutableStream()), + /*reset_parsed_content=*/false); + } + form_stream_epoch_ = doc->GetOverlayEpoch(); return form_stream_; } +void CPDF_Form::EnsureMutableBackingObjectForDict() { + RetainPtr live_stream = GetMutableFormStream(); + if (live_stream) { + dict_ = live_stream->GetMutableDict(); + } +} + RetainPtr CPDF_Form::GetStream() const { + CPDF_Document* doc = GetDocument(); + if (!doc || !form_stream_) { + return form_stream_; + } + + const uint64_t current_epoch = doc->GetOverlayEpoch(); + if (form_stream_epoch_ == current_epoch) { + return form_stream_; + } + form_stream_epoch_ = current_epoch; + + const uint32_t objnum = form_stream_->GetObjNum(); + if (objnum != 0) { + RetainPtr effective = doc->GetIndirectObject(objnum); + const CPDF_Stream* effective_stream = + effective ? effective->AsStream() : nullptr; + if (effective_stream && effective_stream != form_stream_.Get()) { + const_cast(this)->RebindFormStream( + pdfium::WrapRetain(const_cast(effective_stream)), + /*reset_parsed_content=*/true); + } + } return form_stream_; } +void CPDF_Form::RebindFormStream(RetainPtr stream, + bool reset_parsed_content) { + CHECK(stream); + form_stream_ = std::move(stream); + dict_ = form_stream_->GetMutableDict(); + + RetainPtr stream_resources = + dict_->GetDictFor("Resources"); + resources_ = stream_resources + ? pdfium::WrapRetain( + const_cast(stream_resources.Get())) + : fallback_resources_; + + const uint64_t current_epoch = + GetDocument() ? GetDocument()->GetOverlayEpoch() : 0; + dict_epoch_ = current_epoch; + resources_epoch_ = current_epoch; + + if (reset_parsed_content) { + ResetParsedContent(); + recursion_state_.parsed_set.clear(); + } + + transparency_ = CPDF_Transparency(); + LoadTransparencyInfo(); +} + std::optional, CFX_Matrix>> CPDF_Form::GetBitmapAndMatrixFromSoleImageOfForm() const { // TODO(crbug.com/377660088): Determine if there is a case where only a single diff --git a/core/fpdfapi/page/cpdf_form.h b/core/fpdfapi/page/cpdf_form.h index 354c50d051..a95a0f6405 100644 --- a/core/fpdfapi/page/cpdf_form.h +++ b/core/fpdfapi/page/cpdf_form.h @@ -7,6 +7,8 @@ #ifndef CORE_FPDFAPI_PAGE_CPDF_FORM_H_ #define CORE_FPDFAPI_PAGE_CPDF_FORM_H_ +#include + #include #include @@ -32,9 +34,10 @@ class CPDF_Form final : public CPDF_PageObjectHolder, }; // Helper method to choose the first non-null resources dictionary. - static CPDF_Dictionary* ChooseResourcesDict(CPDF_Dictionary* pResources, - CPDF_Dictionary* pParentResources, - CPDF_Dictionary* pPageResources); + static const CPDF_Dictionary* ChooseResourcesDict( + const CPDF_Dictionary* pResources, + const CPDF_Dictionary* pParentResources, + const CPDF_Dictionary* pPageResources); CPDF_Form(CPDF_Document* document, RetainPtr pPageResources, @@ -64,13 +67,20 @@ class CPDF_Form final : public CPDF_PageObjectHolder, RetainPtr GetStream() const; private: + // CPDF_PageObjectHolder: + void EnsureMutableBackingObjectForDict() override; + + void RebindFormStream(RetainPtr stream, + bool reset_parsed_content); void ParseContentInternal(const CPDF_AllStates* pGraphicStates, const CFX_Matrix* pParentMatrix, CPDF_Type3Char* pType3Char, RecursionState* recursion_state); RecursionState recursion_state_; - RetainPtr const form_stream_; + RetainPtr const fallback_resources_; + mutable RetainPtr form_stream_; + mutable uint64_t form_stream_epoch_ = 0; }; #endif // CORE_FPDFAPI_PAGE_CPDF_FORM_H_ diff --git a/core/fpdfapi/page/cpdf_image.cpp b/core/fpdfapi/page/cpdf_image.cpp index 6cb86bbe65..384cb91b31 100644 --- a/core/fpdfapi/page/cpdf_image.cpp +++ b/core/fpdfapi/page/cpdf_image.cpp @@ -42,67 +42,74 @@ namespace { - // Internal helper that overwrites an existing stream's dict + bytes - // and purges any cached image. - bool OverwriteStreamData(CPDF_Stream* s, - CPDF_Document* doc, - DataVector new_data, - RetainPtr new_dict, - bool data_is_decoded) { - if (!s || !new_dict) - return false; - - // Replace dictionary entries (no streams allowed as values). - RetainPtr old = s->GetMutableDict(); - if (!old) - return false; - - // Clear existing keys. - for (const ByteString& k : old->GetKeys()) - old->RemoveFor(k.AsStringView()); - - // Deep-copy all entries from new_dict into old. - CPDF_DictionaryLocker lock(new_dict); - for (auto it = lock.begin(); it != lock.end(); ++it) - old->SetFor(it->first, it->second->Clone()); - - // Swap in the bytes. - if (data_is_decoded) { - // Decoded pixels: also removes Filter/DecodeParms from the stream dict. - s->SetDataAndRemoveFilter(pdfium::span(new_data)); - } else { - // Already filtered (e.g., JPEG with /Filter /DCTDecode). - s->TakeData(std::move(new_data)); - } - - if (doc) - doc->MaybePurgeImage(s->GetObjNum()); - - return true; +// Internal helper that overwrites an existing stream's dict + bytes +// and purges any cached image. +bool OverwriteStreamData(CPDF_Stream* s, + CPDF_Document* doc, + DataVector new_data, + RetainPtr new_dict, + bool data_is_decoded) { + if (!s || !new_dict) { + return false; + } + + // Replace dictionary entries (no streams allowed as values). + RetainPtr old = s->GetMutableDict(); + if (!old) { + return false; + } + + // Clear existing keys. + for (const ByteString& k : old->GetKeys()) { + old->RemoveFor(k.AsStringView()); } - + + // Deep-copy all entries from new_dict into old. + CPDF_DictionaryLocker lock(new_dict); + for (auto it = lock.begin(); it != lock.end(); ++it) { + old->SetFor(it->first, it->second->Clone()); + } + + // Swap in the bytes. + if (data_is_decoded) { + // Decoded pixels: also removes Filter/DecodeParms from the stream dict. + s->SetDataAndRemoveFilter(pdfium::span(new_data)); + } else { + // Already filtered (e.g., JPEG with /Filter /DCTDecode). + s->TakeData(std::move(new_data)); + } + + if (doc) { + doc->MaybePurgeImage(s->GetObjNum()); + } + + return true; +} + } // namespace bool CPDF_Image::OverwriteStreamInPlace(DataVector new_data, RetainPtr new_dict, bool data_is_decoded) { // Ensure we can mutate the underlying stream. - if (stream_->IsInline()) + if (stream_->IsInline()) { ConvertStreamToIndirectObject(); + } RetainPtr s_const = GetStream(); - if (!s_const) + if (!s_const) { return false; + } // Get a mutable stream by objnum. RetainPtr s = ToStream(document_->GetMutableIndirectObject(s_const->GetObjNum())); - if (!s) + if (!s) { return false; + } - const bool ok = - OverwriteStreamData(s.Get(), document_, std::move(new_data), - std::move(new_dict), data_is_decoded); + const bool ok = OverwriteStreamData(s.Get(), document_, std::move(new_data), + std::move(new_dict), data_is_decoded); if (ok) { // Refresh cached flags/size from the new dictionary. FinishInitialization(); @@ -132,7 +139,7 @@ CPDF_Image::CPDF_Image(CPDF_Document* doc, RetainPtr pStream) CPDF_Image::CPDF_Image(CPDF_Document* doc, uint32_t dwStreamObjNum) : document_(doc), - stream_(ToStream(doc->GetMutableIndirectObject(dwStreamObjNum))) { + stream_(ToStream(doc->GetIndirectObject(dwStreamObjNum))) { DCHECK(document_); FinishInitialization(); } @@ -151,7 +158,11 @@ void CPDF_Image::FinishInitialization() { void CPDF_Image::ConvertStreamToIndirectObject() { CHECK(stream_->IsInline()); - document_->AddIndirectObject(stream_); + document_->AddIndirectObject(AcquireMutableStreamForEdit()); +} + +RetainPtr CPDF_Image::AcquireMutableStreamForEdit() { + return pdfium::WrapRetain(const_cast(stream_.Get())); } RetainPtr CPDF_Image::GetDict() const { @@ -166,6 +177,22 @@ RetainPtr CPDF_Image::GetOC() const { return oc_; } +bool CPDF_Image::RebindStreamIfPromoted() { + if (!stream_ || stream_->GetObjNum() == 0) { + return false; + } + + RetainPtr current_stream = + ToStream(document_->GetIndirectObject(stream_->GetObjNum())); + if (!current_stream || current_stream.Get() == stream_.Get()) { + return false; + } + + stream_ = std::move(current_stream); + FinishInitialization(); + return true; +} + RetainPtr CPDF_Image::InitJPEG( pdfium::span src_span) { std::optional info_opt = @@ -254,7 +281,6 @@ void CPDF_Image::SetJpegImageInline(RetainPtr pFile) { stream_ = pdfium::MakeRetain(std::move(data), std::move(dict)); } - void CPDF_Image::SetImage(const RetainPtr& pBitmap) { int32_t BitmapWidth = pBitmap->GetWidth(); int32_t BitmapHeight = pBitmap->GetHeight(); @@ -393,10 +419,11 @@ void CPDF_Image::SetImage(const RetainPtr& pBitmap) { } void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { - auto decoded = PngModule::Decode( - pdfium::span(png_data, png_size)); - if (!decoded.has_value()) + auto decoded = + PngModule::Decode(pdfium::span(png_data, png_size)); + if (!decoded.has_value()) { return; + } const uint32_t w = decoded->width; const uint32_t h = decoded->height; @@ -431,13 +458,12 @@ void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { parms->SetNewFor("BitsPerComponent", 8); parms->SetNewFor("Columns", static_cast(w)); - dict->SetNewFor( - "Length", pdfium::checked_cast(compressed.size())); + dict->SetNewFor("Length", + pdfium::checked_cast(compressed.size())); // Build alpha (SMask) stream if the PNG had transparency. if (!decoded->alpha.empty()) { - DataVector alpha_compressed = - FlateModule::Encode(decoded->alpha); + DataVector alpha_compressed = FlateModule::Encode(decoded->alpha); RetainPtr mask_dict = CreateXObjectImageDict(w, h); mask_dict->SetNewFor("ColorSpace", "DeviceGray"); @@ -452,8 +478,8 @@ void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { mask_stream->GetObjNum()); } - stream_ = pdfium::MakeRetain( - std::move(compressed), std::move(dict)); + stream_ = + pdfium::MakeRetain(std::move(compressed), std::move(dict)); is_mask_ = false; width_ = w; height_ = h; diff --git a/core/fpdfapi/page/cpdf_image.h b/core/fpdfapi/page/cpdf_image.h index b547578f2d..f9137ff1e9 100644 --- a/core/fpdfapi/page/cpdf_image.h +++ b/core/fpdfapi/page/cpdf_image.h @@ -10,10 +10,10 @@ #include #include "core/fpdfapi/page/cpdf_colorspace.h" +#include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" -#include "core/fxcrt/data_vector.h" class CFX_DIBBase; class CFX_DIBitmap; @@ -53,6 +53,9 @@ class CPDF_Image final : public Retainable { RetainPtr CreateNewDIB() const; RetainPtr LoadDIBBase() const; + // Rebinds the image to the current stream object for its object number. + bool RebindStreamIfPromoted(); + void SetImage(const RetainPtr& pBitmap); void SetJpegImage(RetainPtr pFile); void SetJpegImageInline(RetainPtr pFile); @@ -88,6 +91,8 @@ class CPDF_Image final : public Retainable { ~CPDF_Image() override; void FinishInitialization(); + // Used only by explicit edit paths that need to promote this stream. + RetainPtr AcquireMutableStreamForEdit(); RetainPtr InitJPEG(pdfium::span src_span); RetainPtr CreateXObjectImageDict(int width, int height); @@ -101,7 +106,7 @@ class CPDF_Image final : public Retainable { UnownedPtr const document_; RetainPtr dibbase_; RetainPtr mask_; - RetainPtr stream_; + RetainPtr stream_; RetainPtr oc_; }; diff --git a/core/fpdfapi/page/cpdf_page.cpp b/core/fpdfapi/page/cpdf_page.cpp index f0f7c25993..dbe40e3afd 100644 --- a/core/fpdfapi/page/cpdf_page.cpp +++ b/core/fpdfapi/page/cpdf_page.cpp @@ -15,6 +15,8 @@ #include "core/fpdfapi/page/cpdf_pageobject.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_object.h" #include "core/fxcrt/check.h" #include "core/fxcrt/check_op.h" @@ -25,12 +27,15 @@ CPDF_Page::CPDF_Page(CPDF_Document* document, : CPDF_PageObjectHolder(document, std::move(pPageDict), nullptr, nullptr), page_size_(100, 100), pdf_document_(document) { + CPDF_DocumentViewScope document_view(document); + // Cannot initialize |resources_| and |page_resources_| via the // CPDF_PageObjectHolder ctor because GetPageAttr() requires // CPDF_PageObjectHolder to finish initializing first. - RetainPtr pPageAttr = - GetMutablePageAttr(pdfium::page_object::kResources); - resources_ = pPageAttr ? pPageAttr->GetMutableDict() : nullptr; + RetainPtr pPageAttr = + GetPageAttr(pdfium::page_object::kResources); + resources_ = pdfium::WrapRetain(const_cast( + pPageAttr ? pPageAttr->GetDict().Get() : nullptr)); page_resources_ = resources_; UpdateDimensions(); @@ -64,7 +69,19 @@ bool CPDF_Page::IsPage() const { return true; } +RetainPtr CPDF_Page::GetResources() const { + RefreshResourcesIfNeeded(); + return CPDF_PageObjectHolder::GetResources(); +} + +RetainPtr CPDF_Page::GetPageResources() const { + RefreshResourcesIfNeeded(); + return CPDF_PageObjectHolder::GetPageResources(); +} + void CPDF_Page::ParseContent() { + CPDF_DocumentViewScope document_view(GetDocument()); + if (GetParseState() == ParseState::kParsed) { return; } @@ -81,7 +98,48 @@ RetainPtr CPDF_Page::GetMutablePageAttr(ByteStringView name) { return pdfium::WrapRetain(const_cast(GetPageAttr(name).Get())); } +void CPDF_Page::EnsureMutableBackingObjectForResources() { + RetainPtr page_dict = GetMutableDict(); + if (GetDocument() && GetDocument()->IsLayerDocument() && + !page_dict->KeyExist(pdfium::page_object::kResources) && resources_) { + page_dict->SetFor(pdfium::page_object::kResources, + resources_->CloneDirectObject()); + } + resources_ = page_dict->GetMutableDictFor(pdfium::page_object::kResources); +} + +void CPDF_Page::EnsureMutableBackingObjectForPageResources() { + EnsureMutableBackingObjectForResources(); + page_resources_ = resources_; +} + +void CPDF_Page::RefreshResourcesIfNeeded() const { + CPDF_Document* document = GetDocument(); + if (!document) { + return; + } + + const uint64_t current_epoch = document->GetOverlayEpoch(); + if (resources_epoch_ == current_epoch && + page_resources_epoch_ == current_epoch) { + return; + } + + RetainPtr page_attr = + GetPageAttr(pdfium::page_object::kResources); + RetainPtr effective_resources = + page_attr ? page_attr->GetDict() : nullptr; + auto* mutable_this = const_cast(this); + mutable_this->resources_ = pdfium::WrapRetain( + const_cast(effective_resources.Get())); + mutable_this->page_resources_ = mutable_this->resources_; + resources_epoch_ = current_epoch; + page_resources_epoch_ = current_epoch; +} + RetainPtr CPDF_Page::GetPageAttr(ByteStringView name) const { + CPDF_DocumentViewScope document_view(GetDocument()); + std::set> visited; RetainPtr pPageDict = GetDict(); while (pPageDict && !pdfium::Contains(visited, pPageDict)) { diff --git a/core/fpdfapi/page/cpdf_page.h b/core/fpdfapi/page/cpdf_page.h index ba1da8dac4..798e2d3123 100644 --- a/core/fpdfapi/page/cpdf_page.h +++ b/core/fpdfapi/page/cpdf_page.h @@ -70,6 +70,8 @@ class CPDF_Page final : public IPDF_Page, public CPDF_PageObjectHolder { // CPDF_PageObjectHolder: bool IsPage() const override; + RetainPtr GetResources() const override; + RetainPtr GetPageResources() const override; void ParseContent(); const CFX_SizeF& GetPageSize() const { return page_size_; } @@ -106,6 +108,11 @@ class CPDF_Page final : public IPDF_Page, public CPDF_PageObjectHolder { CPDF_Page(CPDF_Document* document, RetainPtr pPageDict); ~CPDF_Page() override; + // CPDF_PageObjectHolder: + void EnsureMutableBackingObjectForResources() override; + void EnsureMutableBackingObjectForPageResources() override; + void RefreshResourcesIfNeeded() const; + RetainPtr GetMutablePageAttr(ByteStringView name); RetainPtr GetPageAttr(ByteStringView name) const; CFX_FloatRect GetBox(ByteStringView name) const; diff --git a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp index cc7ba1e177..9f05687b32 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp @@ -13,13 +13,62 @@ #include "core/fpdfapi/page/cpdf_imageobject.h" #include "core/fpdfapi/page/cpdf_page.h" #include "core/fpdfapi/page/cpdf_pagemodule.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" +#include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/render/cpdf_docrenderdata.h" #include "core/fxcrt/cfx_fileaccess_stream.h" +#include "core/fxcrt/data_vector.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/utils/path_service.h" namespace pdfium { +namespace { + +class ScopedPageModule { + public: + ScopedPageModule() { InitializePageModule(); } + ~ScopedPageModule() { DestroyPageModule(); } +}; + +RetainPtr CreateImageDict(int width, int height) { + auto dict = pdfium::MakeRetain(); + dict->SetNewFor("Type", "XObject"); + dict->SetNewFor("Subtype", "Image"); + dict->SetNewFor("Width", width); + dict->SetNewFor("Height", height); + dict->SetNewFor("ColorSpace", "DeviceRGB"); + dict->SetNewFor("BitsPerComponent", 8); + return dict; +} + +DataVector MakeRgbPixel(uint8_t r, uint8_t g, uint8_t b) { + return {r, g, b}; +} + +class PromotedImageDocument final : public CPDF_Document { + public: + PromotedImageDocument() + : CPDF_Document(std::make_unique(), + std::make_unique()) {} + + void SetPromotedObject(uint32_t objnum) { promoted_objnum_ = objnum; } + + RetainPtr FindPromotedObject(uint32_t objnum) const override { + return objnum == promoted_objnum_ + ? const_cast(this) + ->GetMutableIndirectObject(objnum) + : nullptr; + } + + private: + uint32_t promoted_objnum_ = 0; +}; + +} // namespace TEST(CPDFPageImageCache, RenderBug1924) { // If you render a page with a JPEG2000 image as a thumbnail (small picture) @@ -84,4 +133,89 @@ TEST(CPDFPageImageCache, RenderBug1924) { DestroyPageModule(); } +TEST(CPDFDocPageDataTest, GetImageDoesNotMutateDocument) { + ScopedPageModule page_module; + CPDF_Document document(std::make_unique(), + std::make_unique()); + RetainPtr stream = document.NewIndirect( + MakeRgbPixel(1, 2, 3), CreateImageDict(1, 1)); + const uint32_t stream_objnum = stream->GetObjNum(); + const uint32_t last_objnum = document.GetLastObjNum(); + + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(&document); + RetainPtr image; + { + CPDF_ReadOnlyGraphGuard guard; + image = page_data->GetImage(stream_objnum); + } + + EXPECT_EQ(last_objnum, document.GetLastObjNum()); + EXPECT_EQ(stream.Get(), image->GetStream().Get()); + + RetainPtr cached_image; + { + CPDF_ReadOnlyGraphGuard guard; + cached_image = page_data->GetImage(stream_objnum); + } + EXPECT_EQ(last_objnum, document.GetLastObjNum()); + EXPECT_EQ(image.Get(), cached_image.Get()); +} + +TEST(CPDFDocPageDataTest, GetImageRebindsPromotedStream) { + ScopedPageModule page_module; + PromotedImageDocument document; + RetainPtr original_stream = document.NewIndirect( + MakeRgbPixel(1, 2, 3), CreateImageDict(1, 1)); + const uint32_t stream_objnum = original_stream->GetObjNum(); + + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(&document); + RetainPtr image = page_data->GetImage(stream_objnum); + ASSERT_EQ(original_stream.Get(), image->GetStream().Get()); + + auto promoted_stream = pdfium::MakeRetain(MakeRgbPixel(4, 5, 6), + CreateImageDict(1, 1)); + promoted_stream->SetGenNum(1); + ASSERT_TRUE(document.ReplaceIndirectObjectIfHigherGeneration( + stream_objnum, promoted_stream)); + document.SetPromotedObject(stream_objnum); + + RetainPtr cached_image = page_data->GetImage(stream_objnum); + + EXPECT_EQ(image.Get(), cached_image.Get()); + EXPECT_EQ(promoted_stream.Get(), cached_image->GetStream().Get()); + pdfium::span raw_data = + cached_image->GetStream()->GetInMemoryRawData(); + ASSERT_EQ(3u, raw_data.size()); + EXPECT_EQ(4u, raw_data[0]); + EXPECT_EQ(5u, raw_data[1]); + EXPECT_EQ(6u, raw_data[2]); +} + +TEST(CPDFDocPageDataTest, OverwriteStreamInPlaceUpdatesCachedImage) { + ScopedPageModule page_module; + CPDF_Document document(std::make_unique(), + std::make_unique()); + RetainPtr stream = document.NewIndirect( + MakeRgbPixel(1, 2, 3), CreateImageDict(1, 1)); + const uint32_t stream_objnum = stream->GetObjNum(); + const uint32_t last_objnum = document.GetLastObjNum(); + + CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(&document); + RetainPtr image = page_data->GetImage(stream_objnum); + + ASSERT_TRUE(image->OverwriteStreamInPlace(MakeRgbPixel(7, 8, 9), + CreateImageDict(1, 1), + /*data_is_decoded=*/false)); + RetainPtr cached_image = page_data->GetImage(stream_objnum); + + EXPECT_EQ(last_objnum, document.GetLastObjNum()); + EXPECT_EQ(image.Get(), cached_image.Get()); + pdfium::span raw_data = + cached_image->GetStream()->GetInMemoryRawData(); + ASSERT_EQ(3u, raw_data.size()); + EXPECT_EQ(7u, raw_data[0]); + EXPECT_EQ(8u, raw_data[1]); + EXPECT_EQ(9u, raw_data[2]); +} + } // namespace pdfium diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.cpp b/core/fpdfapi/page/cpdf_pageobjectholder.cpp index f05f01a61a..609431c3a3 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.cpp +++ b/core/fpdfapi/page/cpdf_pageobjectholder.cpp @@ -15,11 +15,13 @@ #include "core/fpdfapi/page/cpdf_pageobject.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/check.h" #include "core/fxcrt/check_op.h" #include "core/fxcrt/containers/unique_ptr_adapters.h" #include "core/fxcrt/fx_extension.h" +#include "core/fxcrt/notreached.h" #include "core/fxcrt/stl_util.h" bool GraphicsData::operator<(const GraphicsData& other) const { @@ -49,10 +51,19 @@ CPDF_PageObjectHolder::CPDF_PageObjectHolder( dict_(std::move(dict)), document_(doc) { DCHECK(dict_); + const uint64_t overlay_epoch = document_ ? document_->GetOverlayEpoch() : 0; + page_resources_epoch_ = overlay_epoch; + resources_epoch_ = overlay_epoch; + dict_epoch_ = overlay_epoch; } CPDF_PageObjectHolder::~CPDF_PageObjectHolder() = default; +void CPDF_PageObjectHolder::SetResources(RetainPtr dict) { + resources_ = std::move(dict); + resources_epoch_ = document_ ? document_->GetOverlayEpoch() : 0; +} + bool CPDF_PageObjectHolder::IsPage() const { return false; } @@ -61,6 +72,160 @@ RetainPtr CPDF_PageObjectHolder::GetMutableFormStream() { return nullptr; } +RetainPtr CPDF_PageObjectHolder::GetDict() const { + if (!document_) { + return dict_; + } + + const uint64_t current_epoch = document_->GetOverlayEpoch(); + if (dict_epoch_ == current_epoch) { + return dict_; + } + dict_epoch_ = current_epoch; + + const uint32_t objnum = dict_->GetObjNum(); + if (objnum == 0) { + return dict_; + } + + RetainPtr live = document_->GetIndirectObject(objnum); + if (live && live.Get() != dict_.Get()) { + const_cast(this)->dict_ = + pdfium::WrapRetain(const_cast(live->AsDictionary())); + } + return dict_; +} + +RetainPtr CPDF_PageObjectHolder::GetMutableDict() { + if (!document_) { + return dict_; + } + + const uint32_t objnum = dict_->GetObjNum(); + if (objnum != 0) { + RetainPtr live = document_->GetMutableIndirectObject(objnum); + if (live && live.Get() != dict_.Get()) { + dict_ = pdfium::WrapRetain(live->AsMutableDictionary()); + } + dict_epoch_ = document_->GetOverlayEpoch(); + return dict_; + } + + if (dict_->IsFrozen()) { + EnsureMutableBackingObjectForDict(); + } + dict_epoch_ = document_->GetOverlayEpoch(); + return dict_; +} + +RetainPtr CPDF_PageObjectHolder::GetResources() const { + if (!document_ || !resources_) { + return resources_; + } + + const uint64_t current_epoch = document_->GetOverlayEpoch(); + if (resources_epoch_ == current_epoch) { + return resources_; + } + resources_epoch_ = current_epoch; + + const uint32_t objnum = resources_->GetObjNum(); + if (objnum == 0) { + return resources_; + } + + RetainPtr live = document_->GetIndirectObject(objnum); + if (live && live.Get() != resources_.Get()) { + const_cast(this)->resources_ = + pdfium::WrapRetain(const_cast(live->AsDictionary())); + } + return resources_; +} + +RetainPtr CPDF_PageObjectHolder::GetMutableResources() { + if (!document_ || !resources_) { + return resources_; + } + + const uint32_t objnum = resources_->GetObjNum(); + if (objnum != 0) { + RetainPtr live = document_->GetMutableIndirectObject(objnum); + if (live && live.Get() != resources_.Get()) { + resources_ = pdfium::WrapRetain(live->AsMutableDictionary()); + } + resources_epoch_ = document_->GetOverlayEpoch(); + return resources_; + } + + if (resources_->IsFrozen()) { + EnsureMutableBackingObjectForResources(); + } + resources_epoch_ = document_->GetOverlayEpoch(); + return resources_; +} + +RetainPtr CPDF_PageObjectHolder::GetPageResources() + const { + if (!document_ || !page_resources_) { + return page_resources_; + } + + const uint64_t current_epoch = document_->GetOverlayEpoch(); + if (page_resources_epoch_ == current_epoch) { + return page_resources_; + } + page_resources_epoch_ = current_epoch; + + const uint32_t objnum = page_resources_->GetObjNum(); + if (objnum == 0) { + return page_resources_; + } + + RetainPtr live = document_->GetIndirectObject(objnum); + if (live && live.Get() != page_resources_.Get()) { + const_cast(this)->page_resources_ = + pdfium::WrapRetain(const_cast(live->AsDictionary())); + } + return page_resources_; +} + +RetainPtr CPDF_PageObjectHolder::GetMutablePageResources() { + if (!document_ || !page_resources_) { + return page_resources_; + } + + const uint32_t objnum = page_resources_->GetObjNum(); + if (objnum != 0) { + RetainPtr live = document_->GetMutableIndirectObject(objnum); + if (live && live.Get() != page_resources_.Get()) { + page_resources_ = pdfium::WrapRetain(live->AsMutableDictionary()); + } + page_resources_epoch_ = document_->GetOverlayEpoch(); + return page_resources_; + } + + if (page_resources_->IsFrozen()) { + EnsureMutableBackingObjectForPageResources(); + } + page_resources_epoch_ = document_->GetOverlayEpoch(); + return page_resources_; +} + +void CPDF_PageObjectHolder::EnsureMutableBackingObjectForDict() { + NOTREACHED(); + CHECK(false); +} + +void CPDF_PageObjectHolder::EnsureMutableBackingObjectForResources() { + RetainPtr dict = GetMutableDict(); + resources_ = dict ? dict->GetMutableDictFor("Resources") : nullptr; +} + +void CPDF_PageObjectHolder::EnsureMutableBackingObjectForPageResources() { + RetainPtr dict = GetMutableDict(); + page_resources_ = dict ? dict->GetMutableDictFor("Resources") : nullptr; +} + void CPDF_PageObjectHolder::StartParse( std::unique_ptr pParser) { DCHECK_EQ(parse_state_, ParseState::kNotParsed); @@ -85,6 +250,21 @@ void CPDF_PageObjectHolder::ContinueParse(PauseIndicatorIface* pPause) { parser_.reset(); } +void CPDF_PageObjectHolder::ResetParsedContent() { + parser_.reset(); + parse_state_ = ParseState::kNotParsed; + page_object_list_.clear(); + all_ctms_.clear(); + mask_bounding_boxes_.clear(); + dirty_streams_.clear(); + graphics_map_.clear(); + fonts_map_.clear(); + fonts_by_objnum_.clear(); + colorspace_map_.clear(); + all_removed_resources_map_.clear(); + background_alpha_needed_ = false; +} + void CPDF_PageObjectHolder::AddImageMaskBoundingBox(const CFX_FloatRect& box) { mask_bounding_boxes_.push_back(box); } diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.h b/core/fpdfapi/page/cpdf_pageobjectholder.h index 898a3d1c00..af4cd30732 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.h +++ b/core/fpdfapi/page/cpdf_pageobjectholder.h @@ -79,8 +79,9 @@ class CPDF_PageObjectHolder { virtual bool IsPage() const; - // Returns the mutable Form XObject stream for CPDF_Form, or nullptr for Pages. - // Used by CPDF_PageContentManager to update Form XObject content directly. + // Returns the mutable Form XObject stream for CPDF_Form, or nullptr for + // Pages. Used by CPDF_PageContentManager to update Form XObject content + // directly. virtual RetainPtr GetMutableFormStream(); void StartParse(std::unique_ptr pParser); @@ -88,19 +89,13 @@ class CPDF_PageObjectHolder { ParseState GetParseState() const { return parse_state_; } CPDF_Document* GetDocument() const { return document_; } - RetainPtr GetDict() const { return dict_; } - RetainPtr GetMutableDict() { return dict_; } - RetainPtr GetResources() const { return resources_; } - RetainPtr GetMutableResources() { return resources_; } - void SetResources(RetainPtr dict) { - resources_ = std::move(dict); - } - RetainPtr GetPageResources() const { - return page_resources_; - } - RetainPtr GetMutablePageResources() { - return page_resources_; - } + virtual RetainPtr GetDict() const; + RetainPtr GetMutableDict(); + virtual RetainPtr GetResources() const; + RetainPtr GetMutableResources(); + void SetResources(RetainPtr dict); + virtual RetainPtr GetPageResources() const; + RetainPtr GetMutablePageResources(); size_t GetPageObjectCount() const { return page_object_list_.size(); } size_t GetActivePageObjectCount() const; CPDF_PageObject* GetPageObjectByIndex(size_t index) const; @@ -159,9 +154,17 @@ class CPDF_PageObjectHolder { protected: void LoadTransparencyInfo(); + void ResetParsedContent(); + virtual void EnsureMutableBackingObjectForDict(); + virtual void EnsureMutableBackingObjectForResources(); + virtual void EnsureMutableBackingObjectForPageResources(); RetainPtr page_resources_; RetainPtr resources_; + RetainPtr dict_; + mutable uint64_t page_resources_epoch_ = 0; + mutable uint64_t resources_epoch_ = 0; + mutable uint64_t dict_epoch_ = 0; std::map graphics_map_; std::map fonts_map_; std::map fonts_by_objnum_; @@ -172,7 +175,6 @@ class CPDF_PageObjectHolder { private: bool background_alpha_needed_ = false; ParseState parse_state_ = ParseState::kNotParsed; - RetainPtr const dict_; UnownedPtr document_; std::vector mask_bounding_boxes_; std::unique_ptr parser_; diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.cpp b/core/fpdfapi/page/cpdf_streamcontentparser.cpp index e503fbf164..b22e4c8d7c 100644 --- a/core/fpdfapi/page/cpdf_streamcontentparser.cpp +++ b/core/fpdfapi/page/cpdf_streamcontentparser.cpp @@ -33,6 +33,7 @@ #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/fpdf_parser_utility.h" @@ -41,6 +42,7 @@ #include "core/fxcrt/check.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/scoped_set_insertion.h" #include "core/fxcrt/span.h" @@ -67,7 +69,9 @@ const char kPathOperatorClosePath = 'h'; const char kPathOperatorRectangle[] = "re"; using OpCodes = std::map; -OpCodes* g_opcodes = nullptr; +// EmbedPDF: thread-confined runtime - per-thread content-operator dispatch +// table, lazily built and torn down on the owning thread. +EPDF_TLS OpCodes* g_opcodes = nullptr; CFX_FloatRect GetShadingBBox(CPDF_ShadingPattern* pShading, const CFX_Matrix& matrix) { @@ -187,6 +191,7 @@ ByteStringView FindFullName(pdfium::span table, void ReplaceAbbr(RetainPtr pObj); void ReplaceAbbrInDictionary(CPDF_Dictionary* dict) { + CPDF_ScopedInlineRewrite inline_rewrite; std::vector replacements; { CPDF_DictionaryLocker locker(dict); @@ -228,6 +233,7 @@ void ReplaceAbbrInDictionary(CPDF_Dictionary* dict) { } void ReplaceAbbrInArray(CPDF_Array* pArray) { + CPDF_ScopedInlineRewrite inline_rewrite; for (size_t i = 0; i < pArray->size(); ++i) { RetainPtr pElement = pArray->GetMutableObjectAt(i); if (pElement->IsName()) { @@ -398,9 +404,10 @@ CPDF_StreamContentParser::CPDF_StreamContentParser( : document_(document), page_resources_(pPageResources), parent_resources_(pParentResources), - resources_(CPDF_Form::ChooseResourcesDict(pResources.Get(), - pParentResources.Get(), - pPageResources.Get())), + resources_(pdfium::WrapRetain(const_cast( + CPDF_Form::ChooseResourcesDict(pResources.Get(), + pParentResources.Get(), + pPageResources.Get())))), object_holder_(pObjHolder), recursion_state_(recursion_state), bbox_(rcBBox), @@ -624,7 +631,10 @@ void CPDF_StreamContentParser::Handle_BeginMarkedContent_Dictionary() { if (pProperty->IsName()) { ByteString property_name = pProperty->GetString(); - RetainPtr pHolder = FindResourceHolder("Properties"); + RetainPtr const_holder = + FindResourceHolder("Properties"); + RetainPtr pHolder = + pdfium::WrapRetain(const_cast(const_holder.Get())); if (!pHolder || !pHolder->GetDictFor(property_name.AsStringView())) { return; } @@ -779,7 +789,10 @@ void CPDF_StreamContentParser::Handle_ExecuteXObject() { return; } - RetainPtr pXObject(ToStream(FindResourceObj("XObject", name))); + RetainPtr const_xobject = + ToStream(FindResourceObj("XObject", name)); + RetainPtr pXObject = + pdfium::WrapRetain(const_cast(const_xobject.Get())); if (!pXObject) { return; } @@ -946,8 +959,10 @@ void CPDF_StreamContentParser::Handle_SetGray_Stroke() { void CPDF_StreamContentParser::Handle_SetExtendGraphState() { ByteString name = GetString(0); - RetainPtr pGS = + RetainPtr const_gs = ToDictionary(FindResourceObj("ExtGState", name)); + RetainPtr pGS = + pdfium::WrapRetain(const_cast(const_gs.Get())); if (!pGS) { return; } @@ -1196,13 +1211,13 @@ void CPDF_StreamContentParser::Handle_SetFont() { } } -RetainPtr CPDF_StreamContentParser::FindResourceHolder( +RetainPtr CPDF_StreamContentParser::FindResourceHolder( ByteStringView type) { if (!resources_) { return nullptr; } - RetainPtr dict = resources_->GetMutableDictFor(type); + RetainPtr dict = resources_->GetDictFor(type); if (dict) { return dict; } @@ -1211,21 +1226,22 @@ RetainPtr CPDF_StreamContentParser::FindResourceHolder( return nullptr; } - return page_resources_->GetMutableDictFor(type); + return page_resources_->GetDictFor(type); } -RetainPtr CPDF_StreamContentParser::FindResourceObj( +RetainPtr CPDF_StreamContentParser::FindResourceObj( ByteStringView type, const ByteString& name) { - RetainPtr pHolder = FindResourceHolder(type); - return pHolder ? pHolder->GetMutableDirectObjectFor(name.AsStringView()) - : nullptr; + RetainPtr pHolder = FindResourceHolder(type); + return pHolder ? pHolder->GetDirectObjectFor(name.AsStringView()) : nullptr; } RetainPtr CPDF_StreamContentParser::FindFont( const ByteString& name) { - RetainPtr font_dict( + RetainPtr const_font_dict( ToDictionary(FindResourceObj("Font", name))); + RetainPtr font_dict = + pdfium::WrapRetain(const_cast(const_font_dict.Get())); if (!font_dict) { return CPDF_Font::GetStockFont(document_, CFX_Font::kDefaultAnsiFontName); } @@ -1284,7 +1300,9 @@ RetainPtr CPDF_StreamContentParser::FindColorSpace( RetainPtr CPDF_StreamContentParser::FindPattern( const ByteString& name) { - RetainPtr pPattern = FindResourceObj("Pattern", name); + RetainPtr const_pattern = FindResourceObj("Pattern", name); + RetainPtr pPattern = + pdfium::WrapRetain(const_cast(const_pattern.Get())); if (!pPattern || (!pPattern->IsDictionary() && !pPattern->IsStream())) { return nullptr; } @@ -1294,7 +1312,9 @@ RetainPtr CPDF_StreamContentParser::FindPattern( RetainPtr CPDF_StreamContentParser::FindShading( const ByteString& name) { - RetainPtr pPattern = FindResourceObj("Shading", name); + RetainPtr const_pattern = FindResourceObj("Shading", name); + RetainPtr pPattern = + pdfium::WrapRetain(const_cast(const_pattern.Get())); if (!pPattern || (!pPattern->IsDictionary() && !pPattern->IsStream())) { return nullptr; } diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.h b/core/fpdfapi/page/cpdf_streamcontentparser.h index 38e9977c20..91f4575364 100644 --- a/core/fpdfapi/page/cpdf_streamcontentparser.h +++ b/core/fpdfapi/page/cpdf_streamcontentparser.h @@ -125,9 +125,9 @@ class CPDF_StreamContentParser { RetainPtr FindColorSpace(const ByteString& name); RetainPtr FindPattern(const ByteString& name); RetainPtr FindShading(const ByteString& name); - RetainPtr FindResourceHolder(ByteStringView type); - RetainPtr FindResourceObj(ByteStringView type, - const ByteString& name); + RetainPtr FindResourceHolder(ByteStringView type); + RetainPtr FindResourceObj(ByteStringView type, + const ByteString& name); // Takes ownership of |pImageObj|, returns unowned pointer to it. CPDF_ImageObject* AddImageObject(std::unique_ptr pImageObj); diff --git a/core/fpdfapi/parser/BUILD.gn b/core/fpdfapi/parser/BUILD.gn index 94ab93ee7b..55f180fed5 100644 --- a/core/fpdfapi/parser/BUILD.gn +++ b/core/fpdfapi/parser/BUILD.gn @@ -11,8 +11,12 @@ source_set("parser") { "cfdf_document.h", "cpdf_array.cpp", "cpdf_array.h", + "cpdf_base_document.cpp", + "cpdf_base_document.h", "cpdf_boolean.cpp", "cpdf_boolean.h", + "cpdf_concat_read_stream.cpp", + "cpdf_concat_read_stream.h", "cpdf_cross_ref_avail.cpp", "cpdf_cross_ref_avail.h", "cpdf_cross_ref_table.cpp", @@ -25,6 +29,8 @@ source_set("parser") { "cpdf_dictionary.h", "cpdf_document.cpp", "cpdf_document.h", + "cpdf_document_view_scope.cpp", + "cpdf_document_view_scope.h", "cpdf_encryptor.cpp", "cpdf_encryptor.h", "cpdf_flateencoder.cpp", @@ -33,6 +39,8 @@ source_set("parser") { "cpdf_hint_tables.h", "cpdf_indirect_object_holder.cpp", "cpdf_indirect_object_holder.h", + "cpdf_layer_document.cpp", + "cpdf_layer_document.h", "cpdf_linearized_header.cpp", "cpdf_linearized_header.h", "cpdf_name.cpp", @@ -53,6 +61,8 @@ source_set("parser") { "cpdf_page_object_avail.h", "cpdf_parser.cpp", "cpdf_parser.h", + "cpdf_read_only_graph_guard.cpp", + "cpdf_read_only_graph_guard.h", "cpdf_read_validator.cpp", "cpdf_read_validator.h", "cpdf_reference.cpp", @@ -116,11 +126,13 @@ source_set("unit_test_support") { pdfium_unittest_source_set("unittests") { sources = [ "cpdf_array_unittest.cpp", + "cpdf_base_document_unittest.cpp", "cpdf_cross_ref_avail_unittest.cpp", "cpdf_dictionary_unittest.cpp", "cpdf_document_unittest.cpp", "cpdf_hint_tables_unittest.cpp", "cpdf_indirect_object_holder_unittest.cpp", + "cpdf_layer_document_unittest.cpp", "cpdf_number_unittest.cpp", "cpdf_object_avail_unittest.cpp", "cpdf_object_stream_unittest.cpp", @@ -128,6 +140,7 @@ pdfium_unittest_source_set("unittests") { "cpdf_object_walker_unittest.cpp", "cpdf_page_object_avail_unittest.cpp", "cpdf_parser_unittest.cpp", + "cpdf_read_only_graph_guard_unittest.cpp", "cpdf_read_validator_unittest.cpp", "cpdf_simple_parser_unittest.cpp", "cpdf_stream_acc_unittest.cpp", diff --git a/core/fpdfapi/parser/cpdf_array.cpp b/core/fpdfapi/parser/cpdf_array.cpp index 4939e24be0..5665b4ebca 100644 --- a/core/fpdfapi/parser/cpdf_array.cpp +++ b/core/fpdfapi/parser/cpdf_array.cpp @@ -13,6 +13,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.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_string.h" @@ -46,6 +47,12 @@ RetainPtr CPDF_Array::Clone() const { return CloneObjectNonCyclic(false); } +RetainPtr CPDF_Array::CloneForHolder( + CPDF_IndirectObjectHolder* holder) const { + std::set visited; + return CloneForHolderNonCyclic(holder, &visited); +} + RetainPtr CPDF_Array::CloneNonCyclic( bool bDirect, std::set* pVisited) const { @@ -62,6 +69,28 @@ RetainPtr CPDF_Array::CloneNonCyclic( return pCopy; } +RetainPtr CPDF_Array::CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const { + pVisited->insert(this); + auto pCopy = pdfium::MakeRetain(); + for (const auto& pValue : objects_) { + if (!pdfium::Contains(*pVisited, pValue.Get())) { + std::set visited(*pVisited); + if (auto obj = pValue->CloneForHolderNonCyclic(holder, &visited)) { + pCopy->objects_.push_back(std::move(obj)); + } + } + } + return pCopy; +} + +void CPDF_Array::FreezeChildren(std::set* visited) { + for (const auto& object : objects_) { + object->FreezeForHolder(visited); + } +} + CFX_FloatRect CPDF_Array::GetRect() const { CFX_FloatRect rect; if (objects_.size() != 4) { @@ -102,7 +131,7 @@ CPDF_Object* CPDF_Array::GetMutableObjectAtInternal(size_t index) { } const CPDF_Object* CPDF_Array::GetObjectAtInternal(size_t index) const { - return const_cast(this)->GetMutableObjectAtInternal(index); + return index < objects_.size() ? objects_[index].Get() : nullptr; } RetainPtr CPDF_Array::GetMutableObjectAt(size_t index) { @@ -114,7 +143,8 @@ RetainPtr CPDF_Array::GetObjectAt(size_t index) const { } RetainPtr CPDF_Array::GetDirectObjectAt(size_t index) const { - return const_cast(this)->GetMutableDirectObjectAt(index); + RetainPtr pObj = GetObjectAt(index); + return pObj ? pObj->GetDirect() : nullptr; } RetainPtr CPDF_Array::GetMutableDirectObjectAt(size_t index) { @@ -175,7 +205,15 @@ RetainPtr CPDF_Array::GetMutableDictAt(size_t index) { } RetainPtr CPDF_Array::GetDictAt(size_t index) const { - return const_cast(this)->GetMutableDictAt(index); + RetainPtr p = GetDirectObjectAt(index); + if (!p) { + return nullptr; + } + if (const CPDF_Dictionary* dict = p->AsDictionary()) { + return pdfium::WrapRetain(dict); + } + const CPDF_Stream* pStream = p->AsStream(); + return pStream ? pStream->GetDict() : nullptr; } RetainPtr CPDF_Array::GetMutableStreamAt(size_t index) { @@ -183,7 +221,7 @@ RetainPtr CPDF_Array::GetMutableStreamAt(size_t index) { } RetainPtr CPDF_Array::GetStreamAt(size_t index) const { - return const_cast(this)->GetMutableStreamAt(index); + return ToStream(GetDirectObjectAt(index)); } RetainPtr CPDF_Array::GetMutableArrayAt(size_t index) { @@ -191,7 +229,7 @@ RetainPtr CPDF_Array::GetMutableArrayAt(size_t index) { } RetainPtr CPDF_Array::GetArrayAt(size_t index) const { - return const_cast(this)->GetMutableArrayAt(index); + return ToArray(GetDirectObjectAt(index)); } RetainPtr CPDF_Array::GetNumberAt(size_t index) const { @@ -204,12 +242,16 @@ RetainPtr CPDF_Array::GetStringAt(size_t index) const { void CPDF_Array::Clear() { CHECK(!IsLocked()); + DCHECK(!IsFrozen()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); objects_.clear(); } void CPDF_Array::RemoveAt(size_t index) { CHECK(!IsLocked()); if (index < objects_.size()) { + DCHECK(!IsFrozen()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); objects_.erase(objects_.begin() + index); } } @@ -225,6 +267,8 @@ void CPDF_Array::ConvertToIndirectObjectAt(size_t index, return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); pHolder->AddIndirectObject(objects_[index]); objects_[index] = objects_[index]->MakeReference(pHolder); } @@ -251,6 +295,8 @@ CPDF_Object* CPDF_Array::SetAtInternal(size_t index, return nullptr; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_[index] = std::move(pObj); return pRet; @@ -266,6 +312,8 @@ CPDF_Object* CPDF_Array::InsertAtInternal(size_t index, return nullptr; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_.insert(objects_.begin() + index, std::move(pObj)); return pRet; @@ -276,6 +324,8 @@ CPDF_Object* CPDF_Array::AppendInternal(RetainPtr pObj) { CHECK(pObj); CHECK(pObj->IsInline()); CHECK(!pObj->IsStream()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_.push_back(std::move(pObj)); return pRet; diff --git a/core/fpdfapi/parser/cpdf_array.h b/core/fpdfapi/parser/cpdf_array.h index 473588dfc8..053d6cb9ad 100644 --- a/core/fpdfapi/parser/cpdf_array.h +++ b/core/fpdfapi/parser/cpdf_array.h @@ -33,6 +33,8 @@ class CPDF_Array final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; CPDF_Array* AsMutableArray() override; bool WriteTo(IFX_ArchiveStream* archive, const CPDF_Encryptor* encryptor) const override; @@ -166,6 +168,10 @@ class CPDF_Array final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, std::set* pVisited) const override; + RetainPtr CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const override; + void FreezeChildren(std::set* visited) override; std::vector> objects_; WeakPtr pool_; diff --git a/core/fpdfapi/parser/cpdf_base_document.cpp b/core/fpdfapi/parser/cpdf_base_document.cpp new file mode 100644 index 0000000000..aa06a1aaaf --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.cpp @@ -0,0 +1,203 @@ +// 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 "core/fpdfapi/parser/cpdf_base_document.h" + +#include +#include +#include +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_layer_document.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/render/cpdf_docrenderdata.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/span.h" + +namespace { + +constexpr size_t kSha256DigestSize = 32; + +void PushIfNew(RetainPtr object, + std::set* visited, + std::queue>* worklist) { + if (!object || !visited->insert(object.Get()).second) { + return; + } + worklist->push(std::move(object)); +} + +bool ComputeStreamSha256(IFX_SeekableReadStream* stream, + FX_FILESIZE size, + std::array* digest) { + if (!stream || size < 0 || !digest) { + return false; + } + + CRYPT_sha2_context context; + CRYPT_SHA256Start(&context); + std::array buffer = {}; + FX_FILESIZE offset = 0; + while (offset < size) { + const size_t read_size = static_cast( + std::min(buffer.size(), size - offset)); + if (!stream->ReadBlockAtOffset(pdfium::span(buffer).first(read_size), + offset)) { + return false; + } + CRYPT_SHA256Update(&context, pdfium::span(buffer).first(read_size)); + offset += read_size; + } + + CRYPT_SHA256Finish(&context, *digest); + return true; +} + +} // namespace + +CPDF_BaseDocument::CPDF_BaseDocument() + : CPDF_Document(std::make_unique(), + std::make_unique()) {} + +CPDF_BaseDocument::~CPDF_BaseDocument() = default; + +CPDF_Parser::Error CPDF_BaseDocument::LoadBaseDoc( + RetainPtr file_access, + const ByteString& password) { + CPDF_Parser::Error error = LoadDoc(std::move(file_access), password); + if (error != CPDF_Parser::SUCCESS) { + return error; + } + if (!CacheBaseIdentity()) { + return CPDF_Parser::FORMAT_ERROR; + } + return EagerlyParseAllReachable() ? CPDF_Parser::SUCCESS + : CPDF_Parser::FORMAT_ERROR; +} + +bool CPDF_BaseDocument::CacheBaseIdentity() { + CPDF_Parser* parser = GetParser(); + RetainPtr stream = + parser ? parser->GetFileAccess() : nullptr; + if (!parser || !stream) { + return false; + } + + raw_base_size_ = stream->GetSize(); + if (raw_base_size_ < 0) { + return false; + } + layer_append_base_offset_ = parser->GetDocumentSize(); + return ComputeStreamSha256(stream.Get(), raw_base_size_, &raw_base_sha256_); +} + +bool CPDF_BaseDocument::EagerlyParseAllReachable() { + if (!GetParser() || !GetRoot()) { + return false; + } + + std::set visited; + std::queue> worklist; + PushIfNew(pdfium::WrapRetain(GetParser()->GetTrailer()), &visited, &worklist); + PushIfNew(pdfium::WrapRetain(GetRoot()), &visited, &worklist); + PushIfNew(GetInfo(), &visited, &worklist); + PushIfNew(GetParser()->GetEncryptDict(), &visited, &worklist); + + while (!worklist.empty()) { + RetainPtr object = worklist.front(); + worklist.pop(); + + switch (object->GetType()) { + case CPDF_Object::kReference: { + const uint32_t ref_objnum = object->AsReference()->GetRefObjNum(); + PushIfNew(GetOrParseIndirectObject(ref_objnum), &visited, &worklist); + break; + } + case CPDF_Object::kArray: { + CPDF_ArrayLocker locker(object->AsArray()); + for (const auto& child : locker) { + PushIfNew(child, &visited, &worklist); + } + break; + } + case CPDF_Object::kDictionary: { + CPDF_DictionaryLocker locker(object->AsDictionary()); + for (const auto& child : locker) { + PushIfNew(child.second, &visited, &worklist); + } + break; + } + case CPDF_Object::kStream: { + PushIfNew(object->AsStream()->GetDict(), &visited, &worklist); + break; + } + default: + break; + } + } + + Freeze(); + return true; +} + +RetainPtr CPDF_BaseDocument::GetFrozenObjectForLayer( + uint32_t objnum) const { + return GetIndirectObject(objnum); +} + +#if DCHECK_IS_ON() +void CPDF_BaseDocument::RegisterLiveLayer( + const CPDF_LayerDocument* layer) { + DCHECK(layer); + DCHECK_EQ(layer->GetBaseDocument(), this); + DCHECK_EQ(std::find(live_layers_.begin(), live_layers_.end(), layer), + live_layers_.end()); + live_layers_.push_back(layer); +} + +void CPDF_BaseDocument::UnregisterLiveLayer( + const CPDF_LayerDocument* layer) { + auto it = std::find(live_layers_.begin(), live_layers_.end(), layer); + DCHECK(it != live_layers_.end()); + live_layers_.erase(it); +} + +bool CPDF_BaseDocument::IsObjectPromotedInAnyLiveLayer( + uint32_t objnum) const { + return std::any_of( + live_layers_.begin(), live_layers_.end(), + [objnum](const CPDF_LayerDocument* layer) { + return layer->FindPromotedObject(objnum) != nullptr; + }); +} +#endif + +CPDF_Object* CPDF_BaseDocument::GetOrParseIndirectObjectInternal( + uint32_t objnum) { + const CPDF_LayerDocument* layer = + CPDF_DocumentViewScope::GetCurrentLayerForBase(this); + if (layer && layer->HasPromotedObjects()) { + RetainPtr promoted = layer->FindPromotedObject(objnum); + if (promoted) { + return promoted.Get(); + } + } +#if DCHECK_IS_ON() + // A promoted object resolved without an effective or explicitly frozen view + // is path-dependent: the answer silently depends on which holder owns the + // reference that happened to lead here. + DCHECK(layer || !IsObjectPromotedInAnyLiveLayer(objnum) || + CPDF_DocumentViewScope::IsFrozenForBase(this)); +#endif + return CPDF_Document::GetOrParseIndirectObjectInternal(objnum); +} diff --git a/core/fpdfapi/parser/cpdf_base_document.h b/core/fpdfapi/parser/cpdf_base_document.h new file mode 100644 index 0000000000..ce3ac9fa66 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.h @@ -0,0 +1,72 @@ +// 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 CORE_FPDFAPI_PARSER_CPDF_BASE_DOCUMENT_H_ +#define CORE_FPDFAPI_PARSER_CPDF_BASE_DOCUMENT_H_ + +#include + +#include +#include + +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/fx_types.h" +#include "core/fxcrt/retain_ptr.h" + +class IFX_SeekableReadStream; +class CPDF_LayerDocument; + +class CPDF_BaseDocument final : public CPDF_Document, public Retainable { + public: + CONSTRUCT_VIA_MAKE_RETAIN; + + CPDF_Parser::Error LoadBaseDoc(RetainPtr file_access, + const ByteString& password); + bool EagerlyParseAllReachable(); + + RetainPtr GetFrozenObjectForLayer(uint32_t objnum) const; + FX_FILESIZE GetRawBaseSize() const { return raw_base_size_; } + FX_FILESIZE GetLayerAppendBaseOffset() const override { + return layer_append_base_offset_; + } + const CPDF_BaseDocument* GetBaseDocumentForViewScope() const override { + return this; + } + const std::array& GetRawBaseSha256() const { + return raw_base_sha256_; + } + +#if DCHECK_IS_ON() + void RegisterLiveLayer(const CPDF_LayerDocument* layer); + void UnregisterLiveLayer(const CPDF_LayerDocument* layer); +#endif + + private: + CPDF_BaseDocument(); + ~CPDF_BaseDocument() override; + + bool CacheBaseIdentity(); + + // CPDF_IndirectObjectHolder: + // This override is intentionally limited to the non-const reference + // resolution path. Const base lookups must remain frozen so layer promotion + // always clones from the shared base rather than resolving back into the + // layer overlay. + CPDF_Object* GetOrParseIndirectObjectInternal(uint32_t objnum) override; + +#if DCHECK_IS_ON() + bool IsObjectPromotedInAnyLiveLayer(uint32_t objnum) const; + std::vector live_layers_; +#endif + + FX_FILESIZE raw_base_size_ = 0; + // PDFium parser offsets are logical PDF offsets after the syntax parser's + // header offset has been subtracted. Layer append-only xref offsets must use + // the same coordinate system, not the raw stream byte size. + FX_FILESIZE layer_append_base_offset_ = 0; + std::array raw_base_sha256_ = {}; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_BASE_DOCUMENT_H_ diff --git a/core/fpdfapi/parser/cpdf_base_document_unittest.cpp b/core/fpdfapi/parser/cpdf_base_document_unittest.cpp new file mode 100644 index 0000000000..c1a637df16 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document_unittest.cpp @@ -0,0 +1,186 @@ +// 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 "core/fpdfapi/parser/cpdf_base_document.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/page/cpdf_pagemodule.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "core/fxcrt/cfx_read_only_span_stream.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class CPDFBaseDocumentTest : public testing::Test { + protected: + static void SetUpTestSuite() { pdfium::InitializePageModule(); } + static void TearDownTestSuite() { pdfium::DestroyPageModule(); } +}; + +RetainPtr LoadBaseDocumentFromString( + const std::string& data) { + RetainPtr document = + pdfium::MakeRetain(); + auto stream = pdfium::MakeRetain( + pdfium::span(reinterpret_cast(data.data()), + data.size())); + if (document->LoadBaseDoc(std::move(stream), "") != CPDF_Parser::SUCCESS) { + return nullptr; + } + return document; +} + +size_t CountIndirectObjects(const CPDF_IndirectObjectHolder& holder) { + return static_cast(std::distance(holder.begin(), holder.end())); +} + +std::string BuildPdfWithOrphanObject() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\n" + "endobj\n", + "4 0 obj\n<< /Orphan true >>\nendobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) + << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +} // namespace + +TEST_F(CPDFBaseDocumentTest, LoadFreezesReachableGraph) { + const std::string pdf = BuildPdfWithOrphanObject(); + RetainPtr document = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(document); + + const size_t initial_object_count = CountIndirectObjects(*document); + EXPECT_TRUE(document->IsHolderFrozen()); + EXPECT_GT(initial_object_count, 0u); + + ASSERT_TRUE(document->GetPageDictionary(0)); + EXPECT_EQ(initial_object_count, CountIndirectObjects(*document)); + ASSERT_TRUE(document->GetPageDictionary(0)); + EXPECT_EQ(initial_object_count, CountIndirectObjects(*document)); +} + +TEST_F(CPDFBaseDocumentTest, IsFrozenVisibleThroughConstObject) { + auto object = pdfium::MakeRetain(); + object->Freeze(); + const CPDF_Object* const_object = object.Get(); + EXPECT_TRUE(const_object->IsFrozen()); +} + +TEST_F(CPDFBaseDocumentTest, CloneOfFrozenObjectIsMutable) { + auto dict = pdfium::MakeRetain(); + RetainPtr nested = dict->SetNewFor("Kids"); + nested->AppendNew("child"); + dict->Freeze(); + + RetainPtr clone = ToDictionary(dict->Clone()); + ASSERT_TRUE(clone); + EXPECT_FALSE(clone->IsFrozen()); + + RetainPtr cloned_kids = clone->GetMutableArrayFor("Kids"); + ASSERT_TRUE(cloned_kids); + EXPECT_FALSE(cloned_kids->IsFrozen()); + EXPECT_FALSE(cloned_kids->GetMutableObjectAt(0)->IsFrozen()); + + clone->SetNewFor("Mutable", 1); + cloned_kids->AppendNew(2); +} + +TEST_F(CPDFBaseDocumentTest, RetainableRefCountSanity) { + RetainPtr document = + pdfium::MakeRetain(); + EXPECT_TRUE(document->HasOneRef()); + RetainPtr second_reference = document; + EXPECT_FALSE(document->HasOneRef()); + second_reference.Reset(); + EXPECT_TRUE(document->HasOneRef()); +} + +#if DCHECK_IS_ON() +TEST_F(CPDFBaseDocumentTest, HolderMutatorsDcheckAfterFreeze) { + CPDF_IndirectObjectHolder holder; + holder.NewIndirect(); + holder.Freeze(); + + EXPECT_DEATH_IF_SUPPORTED(holder.NewIndirect(), ""); + auto replacement = pdfium::MakeRetain(); + replacement->SetGenNum(1); + EXPECT_DEATH_IF_SUPPORTED(holder.ReplaceIndirectObjectIfHigherGeneration( + 1, std::move(replacement)), + ""); + EXPECT_DEATH_IF_SUPPORTED(holder.DeleteIndirectObject(1), ""); +} + +TEST_F(CPDFBaseDocumentTest, ObjectMutatorsDcheckAfterFreeze) { + auto dict = pdfium::MakeRetain(); + RetainPtr array = dict->SetNewFor("Array"); + array->AppendNew(0); + RetainPtr string = + dict->SetNewFor("String", "value"); + RetainPtr stream = + pdfium::MakeRetain(pdfium::span()); + dict->Freeze(); + stream->Freeze(); + + EXPECT_DEATH_IF_SUPPORTED(dict->SetNewFor("New", 1), ""); + EXPECT_DEATH_IF_SUPPORTED(dict->RemoveFor("String"), ""); + EXPECT_DEATH_IF_SUPPORTED(array->AppendNew(1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->InsertNewAt(0, 1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->SetNewAt(0, 1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->RemoveAt(0), ""); + EXPECT_DEATH_IF_SUPPORTED(array->Clear(), ""); + EXPECT_DEATH_IF_SUPPORTED(stream->SetData(pdfium::span()), + ""); + EXPECT_DEATH_IF_SUPPORTED( + stream->SetDataAndRemoveFilter(pdfium::span()), ""); + EXPECT_DEATH_IF_SUPPORTED(stream->TakeData(DataVector()), ""); + EXPECT_DEATH_IF_SUPPORTED(string->SetString("changed"), ""); +} + +TEST_F(CPDFBaseDocumentTest, ReadMissAfterFreezeDchecks) { + const std::string pdf = BuildPdfWithOrphanObject(); + RetainPtr document = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(document); + EXPECT_TRUE(document->IsHolderFrozen()); + EXPECT_FALSE(document->GetFrozenObjectForLayer(4)); + + EXPECT_DEATH_IF_SUPPORTED(document->GetOrParseIndirectObject(4), ""); +} +#endif // DCHECK_IS_ON() diff --git a/core/fpdfapi/parser/cpdf_boolean.cpp b/core/fpdfapi/parser/cpdf_boolean.cpp index b3ebb95530..4674bc763c 100644 --- a/core/fpdfapi/parser/cpdf_boolean.cpp +++ b/core/fpdfapi/parser/cpdf_boolean.cpp @@ -6,6 +6,7 @@ #include "core/fpdfapi/parser/cpdf_boolean.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" CPDF_Boolean::CPDF_Boolean() = default; @@ -31,6 +32,7 @@ int CPDF_Boolean::GetInteger() const { } void CPDF_Boolean::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); value_ = (str == "true"); } diff --git a/core/fpdfapi/parser/cpdf_concat_read_stream.cpp b/core/fpdfapi/parser/cpdf_concat_read_stream.cpp new file mode 100644 index 0000000000..fe0cac734d --- /dev/null +++ b/core/fpdfapi/parser/cpdf_concat_read_stream.cpp @@ -0,0 +1,63 @@ +// 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 "core/fpdfapi/parser/cpdf_concat_read_stream.h" + +#include +#include + +#include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/numerics/safe_conversions.h" + +CPDF_ConcatReadStream::CPDF_ConcatReadStream( + RetainPtr first, + RetainPtr second) + : first_(std::move(first)), + second_(std::move(second)), + first_size_(first_ ? first_->GetSize() : 0), + second_size_(second_ ? second_->GetSize() : 0) {} + +CPDF_ConcatReadStream::~CPDF_ConcatReadStream() = default; + +FX_FILESIZE CPDF_ConcatReadStream::GetSize() { + FX_SAFE_FILESIZE size = first_size_; + size += second_size_; + return size.ValueOrDefault(0); +} + +bool CPDF_ConcatReadStream::ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) { + if (offset < 0) { + return false; + } + if (buffer.empty()) { + return offset <= GetSize(); + } + + FX_SAFE_FILESIZE safe_end = offset; + safe_end += buffer.size(); + if (!safe_end.IsValid() || safe_end.ValueOrDie() > GetSize()) { + return false; + } + + if (offset < first_size_) { + const FX_FILESIZE remaining_first_size = first_size_ - offset; + const size_t first_read_size = + pdfium::IsValueInRangeForNumericType(remaining_first_size) + ? std::min(buffer.size(), + pdfium::checked_cast(remaining_first_size)) + : buffer.size(); + if (!first_ || + !first_->ReadBlockAtOffset(buffer.first(first_read_size), offset)) { + return false; + } + buffer = buffer.subspan(first_read_size); + offset = 0; + } else { + offset -= first_size_; + } + + return buffer.empty() || + (second_ && second_->ReadBlockAtOffset(buffer, offset)); +} diff --git a/core/fpdfapi/parser/cpdf_concat_read_stream.h b/core/fpdfapi/parser/cpdf_concat_read_stream.h new file mode 100644 index 0000000000..537040e8e9 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_concat_read_stream.h @@ -0,0 +1,31 @@ +// 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 CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ +#define CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ + +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/retain_ptr.h" + +class CPDF_ConcatReadStream final : public IFX_SeekableReadStream { + public: + CONSTRUCT_VIA_MAKE_RETAIN; + + // IFX_SeekableReadStream: + FX_FILESIZE GetSize() override; + bool ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) override; + + private: + CPDF_ConcatReadStream(RetainPtr first, + RetainPtr second); + ~CPDF_ConcatReadStream() override; + + RetainPtr const first_; + RetainPtr const second_; + FX_FILESIZE const first_size_; + FX_FILESIZE const second_size_; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ diff --git a/core/fpdfapi/parser/cpdf_dictionary.cpp b/core/fpdfapi/parser/cpdf_dictionary.cpp index de35427745..fa9e447458 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.cpp +++ b/core/fpdfapi/parser/cpdf_dictionary.cpp @@ -14,6 +14,7 @@ #include "core/fpdfapi/parser/cpdf_crypto_handler.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_string.h" @@ -51,6 +52,12 @@ RetainPtr CPDF_Dictionary::Clone() const { return CloneObjectNonCyclic(false); } +RetainPtr CPDF_Dictionary::CloneForHolder( + CPDF_IndirectObjectHolder* holder) const { + std::set visited; + return CloneForHolderNonCyclic(holder, &visited); +} + RetainPtr CPDF_Dictionary::CloneNonCyclic( bool bDirect, std::set* pVisited) const { @@ -69,6 +76,31 @@ RetainPtr CPDF_Dictionary::CloneNonCyclic( return pCopy; } +RetainPtr CPDF_Dictionary::CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const { + pVisited->insert(this); + auto pCopy = pdfium::MakeRetain(pool_); + CPDF_DictionaryLocker locker(this); + for (const auto& it : locker) { + if (!pdfium::Contains(*pVisited, it.second.Get())) { + std::set visited(*pVisited); + auto obj = it.second->CloneForHolderNonCyclic(holder, &visited); + if (obj) { + pCopy->map_.insert(std::make_pair(it.first, std::move(obj))); + } + } + } + return pCopy; +} + +void CPDF_Dictionary::FreezeChildren(std::set* visited) { + CPDF_DictionaryLocker locker(this); + for (const auto& item : locker) { + item.second->FreezeForHolder(visited); + } +} + const CPDF_Object* CPDF_Dictionary::GetObjectForInternal( ByteStringView key) const { auto it = map_.find(key); @@ -99,8 +131,8 @@ RetainPtr CPDF_Dictionary::GetDirectObjectFor( RetainPtr CPDF_Dictionary::GetMutableDirectObjectFor( ByteStringView key) { - return pdfium::WrapRetain( - const_cast(GetDirectObjectForInternal(key))); + RetainPtr p = GetMutableObjectFor(key); + return p ? p->GetMutableDirect() : nullptr; } ByteString CPDF_Dictionary::GetByteStringFor(ByteStringView key) const { @@ -169,8 +201,16 @@ RetainPtr CPDF_Dictionary::GetDictFor( RetainPtr CPDF_Dictionary::GetMutableDictFor( ByteStringView key) { - return pdfium::WrapRetain( - const_cast(GetDictForInternal(key))); + RetainPtr p = GetMutableDirectObjectFor(key); + if (!p) { + return nullptr; + } + CPDF_Dictionary* dict = p->AsMutableDictionary(); + if (dict) { + return pdfium::WrapRetain(dict); + } + CPDF_Stream* stream = p->AsMutableStream(); + return stream ? stream->GetMutableDict() : nullptr; } RetainPtr CPDF_Dictionary::GetOrCreateDictFor( @@ -193,7 +233,7 @@ RetainPtr CPDF_Dictionary::GetArrayFor( } RetainPtr CPDF_Dictionary::GetMutableArrayFor(ByteStringView key) { - return pdfium::WrapRetain(const_cast(GetArrayForInternal(key))); + return ToArray(GetMutableDirectObjectFor(key)); } RetainPtr CPDF_Dictionary::GetOrCreateArrayFor(ByteStringView key) { @@ -216,8 +256,7 @@ RetainPtr CPDF_Dictionary::GetStreamFor( RetainPtr CPDF_Dictionary::GetMutableStreamFor( ByteStringView key) { - return pdfium::WrapRetain( - const_cast(GetStreamForInternal(key))); + return ToStream(GetMutableDirectObjectFor(key)); } const CPDF_Number* CPDF_Dictionary::GetNumberForInternal( @@ -277,6 +316,8 @@ void CPDF_Dictionary::SetFor(const ByteString& key, CPDF_Object* CPDF_Dictionary::SetForInternal(const ByteString& key, RetainPtr pObj) { CHECK(!IsLocked()); + DCHECK(!IsFrozen()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); if (!pObj) { map_.erase(key); return nullptr; @@ -297,6 +338,8 @@ void CPDF_Dictionary::ConvertToIndirectObjectFor( return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); pHolder->AddIndirectObject(it->second); it->second = it->second->MakeReference(pHolder); } @@ -307,6 +350,8 @@ RetainPtr CPDF_Dictionary::RemoveFor(ByteStringView key) { if (it == map_.end()) { return RetainPtr(); } + DCHECK(!IsFrozen()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); auto node = map_.extract(it); return std::move(node.mapped()); } @@ -324,6 +369,8 @@ void CPDF_Dictionary::ReplaceKey(const ByteString& oldkey, return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); map_[MaybeIntern(newkey)] = std::move(old_it->second); map_.erase(old_it); } diff --git a/core/fpdfapi/parser/cpdf_dictionary.h b/core/fpdfapi/parser/cpdf_dictionary.h index a6c8653bfa..0f8b33a52a 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.h +++ b/core/fpdfapi/parser/cpdf_dictionary.h @@ -36,6 +36,8 @@ class CPDF_Dictionary final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; CPDF_Dictionary* AsMutableDictionary() override; bool WriteTo(IFX_ArchiveStream* archive, const CPDF_Encryptor* encryptor) const override; @@ -148,6 +150,10 @@ class CPDF_Dictionary final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, std::set* visited) const override; + RetainPtr CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* visited) const override; + void FreezeChildren(std::set* visited) override; mutable uint32_t lock_count_ = 0; WeakPtr pool_; diff --git a/core/fpdfapi/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp index 166ef94f83..75d69f3800 100644 --- a/core/fpdfapi/parser/cpdf_document.cpp +++ b/core/fpdfapi/parser/cpdf_document.cpp @@ -28,6 +28,7 @@ #include "core/fxcrt/check_op.h" #include "core/fxcrt/containers/contains.h" #include "core/fxcrt/fx_codepage.h" +#include "core/fxcrt/numerics/safe_conversions.h" #include "core/fxcrt/scoped_set_insertion.h" #include "core/fxcrt/span.h" #include "core/fxcrt/stl_util.h" @@ -60,6 +61,18 @@ NodeType GetNodeType(RetainPtr kid_dict) { return has_kids ? NodeType::kBranch : NodeType::kLeaf; } +NodeType GetNodeTypeForTraversal(const CPDF_Dictionary* kid_dict) { + const ByteString kid_type_value = kid_dict->GetNameFor("Type"); + if (kid_type_value == "Pages") { + return NodeType::kBranch; + } + if (kid_type_value == "Page") { + return NodeType::kLeaf; + } + + return kid_dict->KeyExist("Kids") ? NodeType::kBranch : NodeType::kLeaf; +} + // Returns a value in the range [0, `CPDF_Document::kPageMaxNum`), or nullopt on // error. Note that this function may modify `pages_dict` to correct PDF spec // violations. By normalizing the in-memory representation, other code that @@ -170,6 +183,42 @@ int FindPageIndex(const CPDF_Dictionary* pNode, return -1; } +bool AppendPageObjNumsConst(const CPDF_Dictionary* node, + const CPDF_Document* document, + size_t level, + std::set* visited, + std::vector* page_objnums) { + if (!node || !visited->insert(node).second || level >= kMaxPageLevel) { + return false; + } + + RetainPtr kids = node->GetArrayFor("Kids"); + if (!kids) { + if (!ValidateDictType(node, "Page") || !node->GetObjNum()) { + return false; + } + page_objnums->push_back(node->GetObjNum()); + return true; + } + + CPDF_ArrayLocker locker(kids.Get()); + for (const auto& kid : locker) { + RetainPtr direct = kid; + if (RetainPtr ref = ToReference(direct)) { + direct = document->GetIndirectObject(ref->GetRefObjNum()); + } else if (kid) { + direct = kid->GetDirect(); + } + RetainPtr kid_dict = ToDictionary(direct); + if (!kid_dict || + !AppendPageObjNumsConst(kid_dict.Get(), document, level + 1, visited, + page_objnums)) { + return false; + } + } + return true; +} + } // namespace CPDF_Document::CPDF_Document(std::unique_ptr pRenderData, @@ -195,17 +244,31 @@ bool CPDF_Document::IsValidPageObject(const CPDF_Object* obj) { return ValidateDictType(ToDictionary(obj), "Page"); } +CPDF_Parser* CPDF_Document::GetParser() const { + return parser_.get(); +} + +const CPDF_Dictionary* CPDF_Document::GetRoot() const { + return root_dict_.Get(); +} + +RetainPtr CPDF_Document::GetMutableRoot() { + return root_dict_; +} + RetainPtr CPDF_Document::ParseIndirectObject(uint32_t objnum) { - return parser_ ? parser_->ParseIndirectObject(objnum) : nullptr; + CPDF_Parser* parser = GetParser(); + return parser ? parser->ParseIndirectObject(objnum) : nullptr; } bool CPDF_Document::TryInit() { - SetLastObjNum(parser_->GetLastObjNum()); + CPDF_Parser* parser = GetParser(); + SetLastObjNum(parser->GetLastObjNum()); RetainPtr pRootObj = - GetOrParseIndirectObject(parser_->GetRootObjNum()); + GetOrParseIndirectObject(parser->GetRootObjNum()); if (pRootObj) { - root_dict_ = pRootObj->GetMutableDict(); + SetCachedRootDict(pRootObj->GetMutableDict()); } LoadPages(); @@ -215,44 +278,44 @@ bool CPDF_Document::TryInit() { CPDF_Parser::Error CPDF_Document::LoadDoc( RetainPtr pFileAccess, const ByteString& password) { - if (!parser_) { + if (!GetParser()) { SetParser(std::make_unique(this)); } return HandleLoadResult( - parser_->StartParse(std::move(pFileAccess), password)); + GetParser()->StartParse(std::move(pFileAccess), password)); } CPDF_Parser::Error CPDF_Document::LoadLinearizedDoc( RetainPtr validator, const ByteString& password) { - if (!parser_) { + if (!GetParser()) { SetParser(std::make_unique(this)); } return HandleLoadResult( - parser_->StartLinearizedParse(std::move(validator), password)); + GetParser()->StartLinearizedParse(std::move(validator), password)); } void CPDF_Document::LoadPages() { const CPDF_LinearizedHeader* linearized_header = - parser_->GetLinearizedHeader(); + GetParser()->GetLinearizedHeader(); if (!linearized_header) { - page_list_.resize(RetrievePageCount()); + ResizePageList(RetrievePageCount()); return; } uint32_t objnum = linearized_header->GetFirstPageObjNum(); if (!IsValidPageObject(GetOrParseIndirectObject(objnum).Get())) { - page_list_.resize(RetrievePageCount()); + ResizePageList(RetrievePageCount()); return; } uint32_t first_page_num = linearized_header->GetFirstPageNo(); uint32_t page_count = linearized_header->GetPageCount(); DCHECK(first_page_num < page_count); - page_list_.resize(page_count); - page_list_[first_page_num] = objnum; + ResizePageList(page_count); + SetPageObjNumAt(first_page_num, objnum); } RetainPtr CPDF_Document::TraversePDFPages(int iPage, @@ -269,7 +332,7 @@ RetainPtr CPDF_Document::TraversePDFPages(int iPage, if (*nPagesToGo != 1) { return nullptr; } - page_list_[iPage] = pPages->GetObjNum(); + SetPageObjNumAt(iPage, pPages->GetObjNum()); return pPages; } if (level >= kMaxPageLevel) { @@ -294,7 +357,7 @@ RetainPtr CPDF_Document::TraversePDFPages(int iPage, continue; } if (!pKid->KeyExist("Kids")) { - page_list_[iPage - (*nPagesToGo) + 1] = pKid->GetObjNum(); + SetPageObjNumAt(iPage - (*nPagesToGo) + 1, pKid->GetObjNum()); (*nPagesToGo)--; tree_traversal_[level].second++; if (*nPagesToGo == 0) { @@ -333,6 +396,35 @@ void CPDF_Document::ResetTraversal() { tree_traversal_.clear(); } +bool CPDF_Document::RebuildPageListFromCurrentPageTree() { + RetainPtr root = pdfium::WrapRetain(GetRoot()); + if (!root && GetParser()) { + root = ToDictionary(GetIndirectObject(GetParser()->GetRootObjNum())); + } + RetainPtr pages_object = + root ? root->GetObjectFor("Pages") : nullptr; + if (RetainPtr ref = ToReference(pages_object)) { + pages_object = GetIndirectObject(ref->GetRefObjNum()); + } else if (pages_object) { + pages_object = pages_object->GetDirect(); + } + RetainPtr pages = ToDictionary(pages_object); + std::set visited; + std::vector page_objnums; + if (!AppendPageObjNumsConst(pages.Get(), this, /*level=*/0, &visited, + &page_objnums) || + page_objnums.empty() || page_objnums.size() >= kPageMaxNum) { + return false; + } + + ResizePageList(page_objnums.size()); + for (size_t i = 0; i < page_objnums.size(); ++i) { + SetPageObjNumAt(i, page_objnums[i]); + } + ResetTraversal(); + return true; +} + void CPDF_Document::SetParser(std::unique_ptr pParser) { DCHECK(!parser_); parser_ = std::move(pParser); @@ -340,7 +432,7 @@ void CPDF_Document::SetParser(std::unique_ptr pParser) { CPDF_Parser::Error CPDF_Document::HandleLoadResult(CPDF_Parser::Error error) { if (error == CPDF_Parser::SUCCESS) { - has_valid_cross_reference_table_ = !parser_->xref_table_rebuilt(); + has_valid_cross_reference_table_ = !GetParser()->xref_table_rebuilt(); } return error; } @@ -356,15 +448,15 @@ RetainPtr CPDF_Document::GetMutablePagesDict() { } bool CPDF_Document::IsPageLoaded(int iPage) const { - return !!page_list_[iPage]; + return !!GetPageObjNumAt(iPage); } RetainPtr CPDF_Document::GetPageDictionary(int iPage) { - if (!fxcrt::IndexInBounds(page_list_, iPage)) { + if (iPage < 0 || static_cast(iPage) >= GetPageListSize()) { return nullptr; } - const uint32_t objnum = page_list_[iPage]; + const uint32_t objnum = GetPageObjNumAt(iPage); if (objnum) { RetainPtr result = ToDictionary(GetOrParseIndirectObject(objnum)); @@ -394,7 +486,7 @@ RetainPtr CPDF_Document::GetMutablePageDictionary(int iPage) { } void CPDF_Document::SetPageObjNum(int iPage, uint32_t objNum) { - page_list_[iPage] = objNum; + SetPageObjNumAt(iPage, objNum); } JBig2_DocumentContext* CPDF_Document::GetOrCreateCodecContext() { @@ -416,16 +508,55 @@ bool CPDF_Document::IsModifiedAPStream(const CPDF_Stream* stream) const { pdfium::Contains(modified_apstream_ids_, stream->GetObjNum()); } +void CPDF_Document::SetPendingSecurity(PendingSecurity pending) { + pending_security_ = std::move(pending); +} + +const CPDF_Document::PendingSecurity* CPDF_Document::GetPendingSecurity() + const { + return pending_security_ ? &pending_security_.value() : nullptr; +} + +void CPDF_Document::ClearPendingSecurity() { + pending_security_.reset(); +} + +RetainPtr CPDF_Document::FindPromotedObject( + uint32_t objnum) const { + return nullptr; +} + +bool CPDF_Document::IsObjectPromoted(uint32_t objnum) const { + return !!FindPromotedObject(objnum); +} + +uint64_t CPDF_Document::GetOverlayEpoch() const { + return 0; +} + +bool CPDF_Document::IsLayerDocument() const { + return false; +} + +const CPDF_BaseDocument* CPDF_Document::GetBaseDocumentForViewScope() const { + return nullptr; +} + +FX_FILESIZE CPDF_Document::GetLayerAppendBaseOffset() const { + CPDF_Parser* parser = GetParser(); + return parser ? parser->GetDocumentSize() : 0; +} + int CPDF_Document::GetPageIndex(uint32_t objnum) { uint32_t skip_count = 0; bool bSkipped = false; - for (uint32_t i = 0; i < page_list_.size(); ++i) { - if (page_list_[i] == objnum) { - return i; + for (size_t i = 0; i < GetPageListSize(); ++i) { + if (GetPageObjNumAt(i) == objnum) { + return pdfium::checked_cast(i); } - if (!bSkipped && page_list_[i] == 0) { - skip_count = i; + if (!bSkipped && GetPageObjNumAt(i) == 0) { + skip_count = pdfium::checked_cast(i); bSkipped = true; } } @@ -438,19 +569,20 @@ int CPDF_Document::GetPageIndex(uint32_t objnum) { int found_index = FindPageIndex(pPages, &skip_count, objnum, &start_index, 0); // Corrupt page tree may yield out-of-range results. - if (!fxcrt::IndexInBounds(page_list_, found_index)) { + if (found_index < 0 || + static_cast(found_index) >= GetPageListSize()) { return -1; } // Only update |page_list_| when |objnum| points to a /Page object. if (IsValidPageObject(GetOrParseIndirectObject(objnum).Get())) { - page_list_[found_index] = objnum; + SetPageObjNumAt(found_index, objnum); } return found_index; } int CPDF_Document::GetPageCount() const { - return fxcrt::CollectionSize(page_list_); + return pdfium::checked_cast(GetPageListSize()); } int CPDF_Document::RetrievePageCount() { @@ -468,7 +600,8 @@ int CPDF_Document::RetrievePageCount() { } uint32_t CPDF_Document::GetUserPermissions(bool get_owner_perms) const { - return parser_ ? parser_->GetPermissions(get_owner_perms) : 0; + CPDF_Parser* parser = GetParser(); + return parser ? parser->GetPermissions(get_owner_perms) : 0; } RetainPtr CPDF_Document::GetFontFileStreamAcc( @@ -486,17 +619,18 @@ void CPDF_Document::MaybePurgeImage(uint32_t objnum) { } void CPDF_Document::CreateNewDoc() { - DCHECK(!root_dict_); - DCHECK(!info_dict_); - root_dict_ = NewIndirect(); - root_dict_->SetNewFor("Type", "Catalog"); + DCHECK(!GetRoot()); + DCHECK(!GetInfo()); + RetainPtr root = NewIndirect(); + SetCachedRootDict(root); + root->SetNewFor("Type", "Catalog"); auto pPages = NewIndirect(); pPages->SetNewFor("Type", "Pages"); pPages->SetNewFor("Count", 0); pPages->SetNewFor("Kids"); - root_dict_->SetNewFor("Pages", this, pPages->GetObjNum()); - info_dict_ = NewIndirect(); + root->SetNewFor("Pages", this, pPages->GetObjNum()); + SetCachedInfoDict(NewIndirect()); } RetainPtr CPDF_Document::CreateNewPage(int iPage) { @@ -522,8 +656,16 @@ bool CPDF_Document::InsertDeletePDFPage( } for (size_t i = 0; i < kids_list->size(); i++) { - RetainPtr kid_dict = kids_list->GetMutableDictAt(i); - NodeType kid_type = GetNodeType(kid_dict); + // EmbedPDF layer documents must not promote a base /Page object merely to + // decide whether traversal should skip it or remove its reference from + // /Kids. Use const access first, and only request a mutable object for the + // branch /Pages node that will be structurally edited. + RetainPtr const_kid_dict = kids_list->GetDictAt(i); + if (!const_kid_dict) { + return false; + } + + NodeType kid_type = GetNodeTypeForTraversal(const_kid_dict.Get()); if (kid_type == NodeType::kLeaf) { if (pages_to_go != 0) { pages_to_go--; @@ -543,11 +685,16 @@ bool CPDF_Document::InsertDeletePDFPage( } CHECK_EQ(kid_type, NodeType::kBranch); - int page_count = kid_dict->GetIntegerFor("Count"); + int page_count = const_kid_dict->GetIntegerFor("Count"); if (pages_to_go >= page_count) { pages_to_go -= page_count; continue; } + + RetainPtr kid_dict = kids_list->GetMutableDictAt(i); + if (!kid_dict) { + return false; + } if (pdfium::Contains(*visited, kid_dict)) { return false; } @@ -594,7 +741,7 @@ bool CPDF_Document::InsertNewPage(int iPage, return false; } } - page_list_.insert(page_list_.begin() + iPage, pPageDict->GetObjNum()); + InsertPageObjNum(iPage, pPageDict->GetObjNum()); return true; } @@ -603,39 +750,53 @@ RetainPtr CPDF_Document::GetInfo() { return info_dict_; } - if (!parser_) { + CPDF_Parser* parser = GetParser(); + if (!parser) { return nullptr; } - uint32_t info_obj_num = parser_->GetInfoObjNum(); + uint32_t info_obj_num = parser->GetInfoObjNum(); if (info_obj_num == 0) { return nullptr; } auto ref = pdfium::MakeRetain(this, info_obj_num); - info_dict_ = ToDictionary(ref->GetMutableDirect()); + SetCachedInfoDict(ToDictionary(ref->GetMutableDirect())); return info_dict_; } -RetainPtr CPDF_Document::GetOrCreateInfo() { - if (info_dict_) - return info_dict_; +RetainPtr CPDF_Document::GetMutableInfo() { + return GetInfo(); +} - // If parser already has an Info object, reuse it (this populates info_dict_). - if (RetainPtr existing = GetInfo()) +RetainPtr CPDF_Document::GetOrCreateInfo() { + // If the document already has an Info object, return the mutable view of it. + // Layer documents override GetMutableInfo() to promote the base /Info into + // the layer overlay before returning it. + if (RetainPtr existing = GetMutableInfo()) { return existing; + } // No Info present: create a new indirect dictionary and cache it. - info_dict_ = NewIndirect(); + SetCachedInfoDict(NewIndirect()); return info_dict_; } RetainPtr CPDF_Document::GetFileIdentifier() const { - return parser_ ? parser_->GetIDArray() : nullptr; + CPDF_Parser* parser = GetParser(); + return parser ? parser->GetIDArray() : nullptr; } uint32_t CPDF_Document::DeletePage(int iPage) { - RetainPtr pPages = GetMutablePagesDict(); + RetainPtr pRoot = GetMutableRoot(); + if (!pRoot) { + return 0; + } + + // Use the mutable catalog path so layer documents promote the page tree + // before editing /Kids and /Count. A const-cast mutable page tree would leave + // no layer overlay object to save. + RetainPtr pPages = pRoot->GetMutableDictFor("Pages"); if (!pPages) { return 0; } @@ -655,21 +816,27 @@ uint32_t CPDF_Document::DeletePage(int iPage) { return 0; } - page_list_.erase(page_list_.begin() + iPage); + ErasePageObjNum(iPage); return page_dict->GetObjNum(); } void CPDF_Document::SetPageToNullObject(uint32_t page_obj_num) { - if (!page_obj_num || page_list_.empty()) { + if (!page_obj_num || GetPageListSize() == 0) { return; } // Load all pages so `page_list_` has all the object numbers. - for (size_t i = 0; i < page_list_.size(); ++i) { - GetPageDictionary(i); + for (size_t i = 0; i < GetPageListSize(); ++i) { + GetPageDictionary(pdfium::checked_cast(i)); + } + + for (size_t i = 0; i < GetPageListSize(); ++i) { + if (GetPageObjNumAt(i) == page_obj_num) { + return; + } } - if (pdfium::Contains(page_list_, page_obj_num)) { + if (!ShouldReplaceDeletedPageWithNull(page_obj_num)) { return; } @@ -684,8 +851,13 @@ void CPDF_Document::SetPageToNullObject(uint32_t page_obj_num) { CHECK(replaced); } +bool CPDF_Document::ShouldReplaceDeletedPageWithNull( + uint32_t page_obj_num) const { + return true; +} + void CPDF_Document::SetRootForTesting(RetainPtr root) { - root_dict_ = std::move(root); + SetCachedRootDict(std::move(root)); } bool CPDF_Document::MovePages(pdfium::span page_indices, @@ -769,9 +941,49 @@ bool CPDF_Document::MovePages(pdfium::span page_indices, } void CPDF_Document::ResizePageListForTesting(size_t size) { + ResizePageList(size); +} + +uint32_t CPDF_Document::GetPageObjNumAt(size_t index) const { + return page_list_[index]; +} + +void CPDF_Document::SetPageObjNumAt(size_t index, uint32_t objnum) { + page_list_[index] = objnum; +} + +void CPDF_Document::InsertPageObjNum(size_t index, uint32_t objnum) { + page_list_.insert(page_list_.begin() + index, objnum); +} + +void CPDF_Document::ErasePageObjNum(size_t index) { + page_list_.erase(page_list_.begin() + index); +} + +void CPDF_Document::ResizePageList(size_t size) { page_list_.resize(size); } +size_t CPDF_Document::GetPageListSize() const { + return page_list_.size(); +} + +void CPDF_Document::SetCachedRootDict(RetainPtr root) { + root_dict_ = std::move(root); +} + +void CPDF_Document::InvalidateCachedRootDict() { + root_dict_.Reset(); +} + +void CPDF_Document::SetCachedInfoDict(RetainPtr info) { + info_dict_ = std::move(info); +} + +void CPDF_Document::InvalidateCachedInfoDict() { + info_dict_.Reset(); +} + CPDF_Document::StockFontClearer::StockFontClearer( CPDF_Document::PageDataIface* pPageData) : page_data_(pPageData) {} diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 48b01270c9..a675b95f96 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -7,13 +7,17 @@ #ifndef CORE_FPDFAPI_PARSER_CPDF_DOCUMENT_H_ #define CORE_FPDFAPI_PARSER_CPDF_DOCUMENT_H_ +#include + #include +#include #include #include #include #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fpdfapi/parser/cpdf_security_handler.h" #include "core/fxcrt/fx_memory.h" #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" @@ -21,6 +25,7 @@ #include "core/fxcrt/unowned_ptr.h" class CPDF_ReadValidator; +class CPDF_BaseDocument; class CPDF_StreamAcc; class IFX_SeekableReadStream; class JBig2_DocumentContext; @@ -81,6 +86,14 @@ class CPDF_Document : public Observable, UnownedPtr doc_; }; + enum class PendingSecurityMode { kNone, kEncrypt, kRemove }; + + struct PendingSecurity { + PendingSecurityMode mode = PendingSecurityMode::kNone; + RetainPtr encrypt_dict; + RetainPtr security_handler; + }; + static constexpr int kPageMaxNum = 0xFFFFF; static bool IsValidPageObject(const CPDF_Object* obj); @@ -94,10 +107,11 @@ class CPDF_Document : public Observable, extension_ = std::move(pExt); } - CPDF_Parser* GetParser() const { return parser_.get(); } - const CPDF_Dictionary* GetRoot() const { return root_dict_.Get(); } - RetainPtr GetMutableRoot() { return root_dict_; } - RetainPtr GetInfo(); + virtual CPDF_Parser* GetParser() const; + virtual const CPDF_Dictionary* GetRoot() const; + virtual RetainPtr GetMutableRoot(); + virtual RetainPtr GetInfo(); + virtual RetainPtr GetMutableInfo(); RetainPtr GetOrCreateInfo(); RetainPtr GetFileIdentifier() const; @@ -111,12 +125,12 @@ class CPDF_Document : public Observable, int GetPageCount() const; bool IsPageLoaded(int iPage) const; - RetainPtr GetPageDictionary(int iPage); - RetainPtr GetMutablePageDictionary(int iPage); + virtual RetainPtr GetPageDictionary(int iPage); + virtual RetainPtr GetMutablePageDictionary(int iPage); int GetPageIndex(uint32_t objnum); // When `get_owner_perms` is true, returns full permissions if unlocked by // owner. - uint32_t GetUserPermissions(bool get_owner_perms) const; + virtual uint32_t GetUserPermissions(bool get_owner_perms) const; // PageDataIface wrappers, try to avoid explicit getter calls. RetainPtr GetFontFileStreamAcc( @@ -144,6 +158,24 @@ class CPDF_Document : public Observable, // Returns whether CreateModifiedAPStream() created `stream`. bool IsModifiedAPStream(const CPDF_Stream* stream) const; + void SetPendingSecurity(PendingSecurity pending); + const PendingSecurity* GetPendingSecurity() const; + void ClearPendingSecurity(); + + // Returns whether `objnum` has been promoted from its base storage into a + // document overlay. Always false for ordinary documents. + virtual RetainPtr FindPromotedObject(uint32_t objnum) const; + bool IsObjectPromoted(uint32_t objnum) const; + // Changes whenever the effective identity of an indirect object can change. + // Ordinary documents have no overlay and always return 0. + virtual uint64_t GetOverlayEpoch() const; + virtual bool IsLayerDocument() const; + // Returns non-null only for the immutable base document itself. This avoids + // RTTI in the ambient-view boundary while preserving exact frozen-view + // identity for the debug detector. + virtual const CPDF_BaseDocument* GetBaseDocumentForViewScope() const; + virtual FX_FILESIZE GetLayerAppendBaseOffset() const; + // CPDF_Parser::ParsedObjectsHolder: bool TryInit() override; RetainPtr ParseIndirectObject(uint32_t objnum) override; @@ -170,6 +202,25 @@ class CPDF_Document : public Observable, void ResizePageListForTesting(size_t size); + virtual uint32_t GetPageObjNumAt(size_t index) const; + virtual void SetPageObjNumAt(size_t index, uint32_t objnum); + virtual void InsertPageObjNum(size_t index, uint32_t objnum); + virtual void ErasePageObjNum(size_t index); + virtual void ResizePageList(size_t size); + virtual size_t GetPageListSize() const; + bool RebuildPageListFromCurrentPageTree(); + + void SetCachedRootDict(RetainPtr root); + void InvalidateCachedRootDict(); + void SetCachedInfoDict(RetainPtr info); + void InvalidateCachedInfoDict(); + + // EmbedPDF layer documents represent deletion by removing references from the + // promoted owning container, e.g. /Pages /Kids or /Annots. Base objects + // remain resolvable through the base xref and are not null-replaced in + // append-only layer deltas. + virtual bool ShouldReplaceDeletedPageWithNull(uint32_t page_obj_num) const; + private: class StockFontClearer { public: @@ -228,6 +279,7 @@ class CPDF_Document : public Observable, std::unique_ptr codec_context_; std::unique_ptr links_context_; std::set modified_apstream_ids_; + std::optional pending_security_; std::vector page_list_; // Page number to page's dict objnum. // Must be second to last. diff --git a/core/fpdfapi/parser/cpdf_document_view_scope.cpp b/core/fpdfapi/parser/cpdf_document_view_scope.cpp new file mode 100644 index 0000000000..6c13fdaa79 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_document_view_scope.cpp @@ -0,0 +1,83 @@ +// 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 "core/fpdfapi/parser/cpdf_document_view_scope.h" + +#include "core/fpdfapi/parser/cpdf_layer_document.h" +#include "core/fxcrt/check_op.h" +#include "core/fxcrt/epdf_tls.h" + +namespace { + +EPDF_TLS CPDF_DocumentViewScope::Mode g_current_view_mode = + CPDF_DocumentViewScope::Mode::kNone; +EPDF_TLS const CPDF_LayerDocument* g_current_layer_view = nullptr; +EPDF_TLS const CPDF_BaseDocument* g_current_frozen_base = nullptr; + +CPDF_DocumentViewScope::Mode GetModeForDocument(CPDF_Document* document) { + if (!document) { + return g_current_view_mode; + } + return CPDF_LayerDocument::FromDocument(document) + ? CPDF_DocumentViewScope::Mode::kEffective + : CPDF_DocumentViewScope::Mode::kFrozen; +} + +const CPDF_LayerDocument* GetLayerForDocument(CPDF_Document* document) { + return document ? CPDF_LayerDocument::FromDocument(document) + : g_current_layer_view; +} + +const CPDF_BaseDocument* GetFrozenBaseForDocument(CPDF_Document* document) { + if (!document) { + return g_current_frozen_base; + } + return !CPDF_LayerDocument::FromDocument(document) + ? document->GetBaseDocumentForViewScope() + : nullptr; +} + +} // namespace + +CPDF_DocumentViewScope::CPDF_DocumentViewScope(CPDF_Document* document) + : previous_mode_(g_current_view_mode), + previous_layer_(g_current_layer_view), + previous_frozen_base_(g_current_frozen_base), + mode_(GetModeForDocument(document)), + layer_(GetLayerForDocument(document)), + frozen_base_(GetFrozenBaseForDocument(document)) { + g_current_view_mode = mode_; + g_current_layer_view = layer_; + g_current_frozen_base = frozen_base_; +} + +CPDF_DocumentViewScope::~CPDF_DocumentViewScope() { + DCHECK_EQ(g_current_view_mode, mode_); + DCHECK_EQ(g_current_layer_view, layer_); + DCHECK_EQ(g_current_frozen_base, frozen_base_); + g_current_view_mode = previous_mode_; + g_current_layer_view = previous_layer_; + g_current_frozen_base = previous_frozen_base_; +} + +// static +const CPDF_LayerDocument* CPDF_DocumentViewScope::GetCurrentLayerForBase( + const CPDF_BaseDocument* base) { + return g_current_view_mode == Mode::kEffective && g_current_layer_view && + g_current_layer_view->GetBaseDocument() == base + ? g_current_layer_view + : nullptr; +} + +// static +CPDF_DocumentViewScope::Mode CPDF_DocumentViewScope::GetCurrentMode() { + return g_current_view_mode; +} + +// static +bool CPDF_DocumentViewScope::IsFrozenForBase( + const CPDF_BaseDocument* base) { + return g_current_view_mode == Mode::kFrozen && + g_current_frozen_base == base; +} diff --git a/core/fpdfapi/parser/cpdf_document_view_scope.h b/core/fpdfapi/parser/cpdf_document_view_scope.h new file mode 100644 index 0000000000..2bac24470f --- /dev/null +++ b/core/fpdfapi/parser/cpdf_document_view_scope.h @@ -0,0 +1,44 @@ +// 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 CORE_FPDFAPI_PARSER_CPDF_DOCUMENT_VIEW_SCOPE_H_ +#define CORE_FPDFAPI_PARSER_CPDF_DOCUMENT_VIEW_SCOPE_H_ + +class CPDF_BaseDocument; +class CPDF_Document; +class CPDF_LayerDocument; + +// Establishes the effective document view used while resolving references +// inherited from a frozen base document. Scopes are nestable so operations +// involving more than one document can switch views explicitly and restore the +// caller's view on return. +class CPDF_DocumentViewScope final { + public: + enum class Mode { + kNone, + kEffective, + kFrozen, + }; + + explicit CPDF_DocumentViewScope(CPDF_Document* document); + ~CPDF_DocumentViewScope(); + + CPDF_DocumentViewScope(const CPDF_DocumentViewScope&) = delete; + CPDF_DocumentViewScope& operator=(const CPDF_DocumentViewScope&) = delete; + + static const CPDF_LayerDocument* GetCurrentLayerForBase( + const CPDF_BaseDocument* base); + static Mode GetCurrentMode(); + static bool IsFrozenForBase(const CPDF_BaseDocument* base); + + private: + const Mode previous_mode_; + const CPDF_LayerDocument* const previous_layer_; + const CPDF_BaseDocument* const previous_frozen_base_; + const Mode mode_; + const CPDF_LayerDocument* const layer_; + const CPDF_BaseDocument* const frozen_base_; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_DOCUMENT_VIEW_SCOPE_H_ diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 6e02e258af..b9de488ec3 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -12,6 +12,7 @@ #include "core/fpdfapi/parser/cpdf_object.h" #include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fxcrt/check.h" namespace { @@ -37,7 +38,7 @@ RetainPtr CPDF_IndirectObjectHolder::GetIndirectObject( RetainPtr CPDF_IndirectObjectHolder::GetMutableIndirectObject( uint32_t objnum) { return pdfium::WrapRetain( - const_cast(GetIndirectObjectInternal(objnum))); + const_cast(GetOrParseIndirectObjectInternal(objnum))); } const CPDF_Object* CPDF_IndirectObjectHolder::GetIndirectObjectInternal( @@ -50,11 +51,49 @@ const CPDF_Object* CPDF_IndirectObjectHolder::GetIndirectObjectInternal( return FilterInvalidObjNum(it->second.Get()); } +RetainPtr CPDF_IndirectObjectHolder::FindLocalIndirectObject( + uint32_t objnum) const { + auto it = indirect_objs_.find(objnum); + if (it == indirect_objs_.end()) { + return nullptr; + } + + return pdfium::WrapRetain( + const_cast(FilterInvalidObjNum(it->second.Get()))); +} + +void CPDF_IndirectObjectHolder::AddPromotedObject( + uint32_t objnum, + RetainPtr object) { + DCHECK(objnum); + DCHECK(objnum != CPDF_Object::kInvalidObjNum); + CHECK(object); + + DCHECK_PDF_HOLDER_MUTABLE(); + DCHECK(!frozen_); + object->SetObjNum(objnum); + indirect_objs_[objnum] = std::move(object); + last_obj_num_ = std::max(last_obj_num_, objnum); +} + RetainPtr CPDF_IndirectObjectHolder::GetOrParseIndirectObject( uint32_t objnum) { return pdfium::WrapRetain(GetOrParseIndirectObjectInternal(objnum)); } +void CPDF_IndirectObjectHolder::Freeze() { + if (frozen_) { + return; + } + + frozen_ = true; + for (const auto& item : indirect_objs_) { + if (item.second) { + item.second->Freeze(); + } + } +} + CPDF_Object* CPDF_IndirectObjectHolder::GetOrParseIndirectObjectInternal( uint32_t objnum) { if (objnum == 0 || objnum == CPDF_Object::kInvalidObjNum) { @@ -67,6 +106,7 @@ CPDF_Object* CPDF_IndirectObjectHolder::GetOrParseIndirectObjectInternal( return const_cast( FilterInvalidObjNum(insert_result.first->second.Get())); } + DCHECK(!frozen_); RetainPtr pNewObj = ParseIndirectObject(objnum); if (!pNewObj) { indirect_objs_.erase(insert_result.first); @@ -88,6 +128,8 @@ RetainPtr CPDF_IndirectObjectHolder::ParseIndirectObject( uint32_t CPDF_IndirectObjectHolder::AddIndirectObject( RetainPtr pObj) { + DCHECK_PDF_HOLDER_MUTABLE(); + DCHECK(!frozen_); CHECK(!pObj->GetObjNum()); pObj->SetObjNum(++last_obj_num_); indirect_objs_[last_obj_num_] = std::move(pObj); @@ -108,6 +150,8 @@ bool CPDF_IndirectObjectHolder::ReplaceIndirectObjectIfHigherGeneration( return false; } + DCHECK_PDF_HOLDER_MUTABLE(); + DCHECK(!frozen_); pObj->SetObjNum(objnum); obj_holder = std::move(pObj); last_obj_num_ = std::max(last_obj_num_, objnum); @@ -120,5 +164,7 @@ void CPDF_IndirectObjectHolder::DeleteIndirectObject(uint32_t objnum) { return; } + DCHECK_PDF_HOLDER_MUTABLE(); + DCHECK(!frozen_); indirect_objs_.erase(it); } diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.h b/core/fpdfapi/parser/cpdf_indirect_object_holder.h index 46b9a7b405..51529e016c 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.h +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.h @@ -26,10 +26,10 @@ class CPDF_IndirectObjectHolder { CPDF_IndirectObjectHolder(); virtual ~CPDF_IndirectObjectHolder(); - RetainPtr GetOrParseIndirectObject(uint32_t objnum); - RetainPtr GetIndirectObject(uint32_t objnum) const; - RetainPtr GetMutableIndirectObject(uint32_t objnum); - void DeleteIndirectObject(uint32_t objnum); + virtual RetainPtr GetOrParseIndirectObject(uint32_t objnum); + virtual RetainPtr GetIndirectObject(uint32_t objnum) const; + virtual RetainPtr GetMutableIndirectObject(uint32_t objnum); + virtual void DeleteIndirectObject(uint32_t objnum); // Creates and adds a new object retained by the indirect object holder, // and returns a retained pointer to it. @@ -64,6 +64,8 @@ class CPDF_IndirectObjectHolder { uint32_t GetLastObjNum() const { return last_obj_num_; } void SetLastObjNum(uint32_t objnum) { last_obj_num_ = objnum; } + void Freeze(); + bool IsHolderFrozen() const { return frozen_; } WeakPtr GetByteStringPool() const { return byte_string_pool_; @@ -74,14 +76,17 @@ class CPDF_IndirectObjectHolder { protected: virtual RetainPtr ParseIndirectObject(uint32_t objnum); + virtual const CPDF_Object* GetIndirectObjectInternal(uint32_t objnum) const; + virtual CPDF_Object* GetOrParseIndirectObjectInternal(uint32_t objnum); + + RetainPtr FindLocalIndirectObject(uint32_t objnum) const; + void AddPromotedObject(uint32_t objnum, RetainPtr object); private: friend class CPDF_Reference; - const CPDF_Object* GetIndirectObjectInternal(uint32_t objnum) const; - CPDF_Object* GetOrParseIndirectObjectInternal(uint32_t objnum); - uint32_t last_obj_num_ = 0; + bool frozen_ = false; std::map> indirect_objs_; WeakPtr byte_string_pool_; }; diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp new file mode 100644 index 0000000000..ea3c6f6653 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -0,0 +1,457 @@ +// 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 "core/fpdfapi/parser/cpdf_layer_document.h" + +#include +#include + +#include "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_concat_read_stream.h" +#include "core/fpdfapi/parser/cpdf_cross_ref_table.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fpdfapi/render/cpdf_docrenderdata.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/notreached.h" +#include "core/fxcrt/unowned_ptr.h" + +namespace { + +class DeltaParseObjectHolder final : public CPDF_Parser::ParsedObjectsHolder { + public: + DeltaParseObjectHolder() = default; + ~DeltaParseObjectHolder() override = default; + + void SetParser(CPDF_Parser* parser) { parser_ = parser; } + bool TryInit() override { return true; } + + protected: + RetainPtr ParseIndirectObject(uint32_t objnum) override { + return parser_ ? parser_->ParseIndirectObject(objnum) : nullptr; + } + + private: + UnownedPtr parser_; +}; + +bool IsBaseObjectLive(const CPDF_Parser* base_parser, uint32_t objnum) { + return objnum != 0 && base_parser->IsValidObjectNumber(objnum) && + !base_parser->IsObjectFree(objnum); +} + +bool IsObjectOwnedByAppendedDelta(const CPDF_CrossRefTable* table, + uint32_t objnum, + const CPDF_CrossRefTable::ObjectInfo& info, + FX_FILESIZE layer_append_base_offset) { + if (objnum == table->trailer_object_number()) { + return false; + } + + switch (info.type) { + case CPDF_CrossRefTable::ObjectType::kFree: + return false; + case CPDF_CrossRefTable::ObjectType::kNormal: + return info.pos >= layer_append_base_offset; + case CPDF_CrossRefTable::ObjectType::kCompressed: { + const CPDF_CrossRefTable::ObjectInfo* archive_info = + table->GetObjectInfo(info.archive.obj_num); + return archive_info && + archive_info->type == CPDF_CrossRefTable::ObjectType::kNormal && + archive_info->pos >= layer_append_base_offset; + } + } +} + +} // namespace + +CPDF_LayerDocument::CPDF_LayerDocument( + RetainPtr base, + RetainPtr file_access) + : CPDF_Document(std::make_unique( + CPDF_DocRenderData::FromDocument(base.Get())), + std::make_unique( + CPDF_DocPageData::FromDocument(base.Get()))), + base_(std::move(base)), + file_access_(std::move(file_access)) { + CHECK(base_); + SetLastObjNum(base_->GetLastObjNum()); + { + // Initialization deliberately copies the immutable base view. + CPDF_DocumentViewScope frozen_view(base_.Get()); + InitializeFromBase(); + } +#if DCHECK_IS_ON() + base_->RegisterLiveLayer(this); +#endif + { + CPDF_DocumentViewScope effective_view(this); + IngestCurrentDelta(); + } +} + +CPDF_LayerDocument::~CPDF_LayerDocument() { +#if DCHECK_IS_ON() + base_->UnregisterLiveLayer(this); +#endif +} + +// static +CPDF_LayerDocument* CPDF_LayerDocument::FromDocument(CPDF_Document* document) { + return document && document->IsLayerDocument() + ? static_cast(document) + : nullptr; +} + +// static +const CPDF_LayerDocument* CPDF_LayerDocument::FromDocument( + const CPDF_Document* document) { + return document && document->IsLayerDocument() + ? static_cast(document) + : nullptr; +} + +size_t CPDF_LayerDocument::GetPromotedObjectCount() const { + return static_cast(std::distance(begin(), end())); +} + +CPDF_Parser* CPDF_LayerDocument::GetParser() const { + return base_->GetParser(); +} + +const CPDF_Dictionary* CPDF_LayerDocument::GetRoot() const { + const uint32_t root_objnum = base_->GetParser()->GetRootObjNum(); + if (RetainPtr local = FindLocalIndirectObject(root_objnum)) { + // The returned pointer is owned by this layer's indirect object holder. + // Const root reads must see the effective overlay after delta ingest even + // when the mutable root cache has been invalidated. + return local->AsDictionary(); + } + return base_->GetRoot(); +} + +RetainPtr CPDF_LayerDocument::GetMutableRoot() { + const uint32_t root_objnum = base_->GetParser()->GetRootObjNum(); + RetainPtr live = GetMutableIndirectObject(root_objnum); + RetainPtr root = + live ? pdfium::WrapRetain(live->AsMutableDictionary()) : nullptr; + SetCachedRootDict(root); + return root; +} + +RetainPtr CPDF_LayerDocument::GetMutableInfo() { + RetainPtr current_info = GetInfo(); + if (!current_info) { + return nullptr; + } + const uint32_t info_objnum = current_info->GetObjNum(); + DCHECK_NE(CPDF_Object::kInvalidObjNum, info_objnum); + DCHECK_NE(0u, info_objnum); + RetainPtr live = GetMutableIndirectObject(info_objnum); + RetainPtr info = + live ? pdfium::WrapRetain(live->AsMutableDictionary()) : nullptr; + SetCachedInfoDict(info); + return info; +} + +RetainPtr CPDF_LayerDocument::GetPageDictionary( + int iPage) { + if (iPage < 0 || static_cast(iPage) >= GetPageListSize()) { + return nullptr; + } + + const uint32_t objnum = GetPageObjNumAt(iPage); + if (!objnum) { + return nullptr; + } + + return ToDictionary(GetOrParseIndirectObject(objnum)); +} + +RetainPtr CPDF_LayerDocument::GetMutablePageDictionary( + int iPage) { + if (iPage < 0 || static_cast(iPage) >= GetPageListSize()) { + return nullptr; + } + + const uint32_t objnum = GetPageObjNumAt(iPage); + if (!objnum) { + return nullptr; + } + + // EmbedPDF layer documents must never return a const-cast frozen base page + // for mutation. Page moves reparent the moved page dictionary, so promote the + // page object before returning a mutable handle. + RetainPtr live = GetMutableIndirectObject(objnum); + return live ? pdfium::WrapRetain(live->AsMutableDictionary()) : nullptr; +} + +uint32_t CPDF_LayerDocument::GetUserPermissions(bool get_owner_perms) const { + return base_->GetUserPermissions(get_owner_perms); +} + +RetainPtr CPDF_LayerDocument::FindPromotedObject( + uint32_t objnum) const { + return FindLocalIndirectObject(objnum); +} + +uint64_t CPDF_LayerDocument::GetOverlayEpoch() const { + return overlay_epoch_; +} + +bool CPDF_LayerDocument::IsLayerDocument() const { + return true; +} + +FX_FILESIZE CPDF_LayerDocument::GetLayerAppendBaseOffset() const { + return base_->GetLayerAppendBaseOffset(); +} + +bool CPDF_LayerDocument::ShouldReplaceDeletedPageWithNull( + uint32_t page_obj_num) const { + CPDF_Parser* parser = base_->GetParser(); + if (parser && parser->IsValidObjectNumber(page_obj_num) && + !parser->IsObjectFree(page_obj_num)) { + return false; + } + + // If the page object was created in this layer, nulling it is a local overlay + // change. If it exists in the base document, deletion is represented solely + // by the promoted page tree no longer referencing it. + return FindPromotedObject(page_obj_num) != nullptr; +} + +RetainPtr CPDF_LayerDocument::ParseIndirectObject( + uint32_t objnum) { + NOTREACHED(); + return nullptr; +} + +RetainPtr CPDF_LayerDocument::GetMutableIndirectObject( + uint32_t objnum) { + if (RetainPtr local = FindLocalIndirectObject(objnum)) { + return local; + } + return PromoteFromBase(objnum); +} + +void CPDF_LayerDocument::DeleteIndirectObject(uint32_t objnum) { + if (FindLocalIndirectObject(objnum)) { + CPDF_Document::DeleteIndirectObject(objnum); + ++overlay_epoch_; + } +} + +const CPDF_Object* CPDF_LayerDocument::GetIndirectObjectInternal( + uint32_t objnum) const { + if (RetainPtr local = FindLocalIndirectObject(objnum)) { + return local.Get(); + } + return base_->GetFrozenObjectForLayer(objnum).Get(); +} + +CPDF_Object* CPDF_LayerDocument::GetOrParseIndirectObjectInternal( + uint32_t objnum) { + return const_cast(GetIndirectObjectInternal(objnum)); +} + +uint32_t CPDF_LayerDocument::GetPageObjNumAt(size_t index) const { + CHECK_LT(index, layer_page_list_.size()); + return layer_page_list_[index]; +} + +void CPDF_LayerDocument::SetPageObjNumAt(size_t index, uint32_t objnum) { + CHECK_LT(index, layer_page_list_.size()); + layer_page_list_[index] = objnum; +} + +void CPDF_LayerDocument::InsertPageObjNum(size_t index, uint32_t objnum) { + CHECK_LE(index, layer_page_list_.size()); + layer_page_list_.insert(layer_page_list_.begin() + index, objnum); +} + +void CPDF_LayerDocument::ErasePageObjNum(size_t index) { + CHECK_LT(index, layer_page_list_.size()); + layer_page_list_.erase(layer_page_list_.begin() + index); +} + +void CPDF_LayerDocument::ResizePageList(size_t size) { + layer_page_list_.resize(size); +} + +size_t CPDF_LayerDocument::GetPageListSize() const { + return layer_page_list_.size(); +} + +void CPDF_LayerDocument::InitializeFromBase() { + SetCachedRootDict( + pdfium::WrapRetain(const_cast(base_->GetRoot()))); + SetCachedInfoDict(base_->GetInfo()); + + const int page_count = base_->GetPageCount(); + if (page_count < 0) { + ingest_status_ = OpenStatus::kOpenFailed; + return; + } + + layer_page_list_.reserve(static_cast(page_count)); + for (int i = 0; i < page_count; ++i) { + RetainPtr page = base_->GetPageDictionary(i); + layer_page_list_.push_back(page ? page->GetObjNum() : 0); + } +} + +void CPDF_LayerDocument::IngestCurrentDelta() { + if (ingest_status_ != OpenStatus::kSuccess) { + return; + } + + CPDF_Parser* base_parser = base_->GetParser(); + if (!base_parser || !file_access_) { + if (!file_access_) { + return; + } + FailDeltaIngest(OpenStatus::kOpenFailed); + return; + } + + const FX_FILESIZE delta_size = file_access_->GetSize(); + if (delta_size == 0) { + file_access_.Reset(); + return; + } + + RetainPtr base_file = base_parser->GetFileAccess(); + if (!base_file) { + FailDeltaIngest(OpenStatus::kOpenFailed); + return; + } + + const FX_FILESIZE layer_append_base_offset = + base_->GetLayerAppendBaseOffset(); + DeltaParseObjectHolder temp_holder; + CPDF_Parser parser(&temp_holder); + temp_holder.SetParser(&parser); + CPDF_Parser::Error parse_error = + parser.StartParse(pdfium::MakeRetain( + std::move(base_file), file_access_), + base_parser->GetPassword()); + if (parse_error != CPDF_Parser::SUCCESS) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + if (parser.GetLastXRefOffset() < layer_append_base_offset) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + + const CPDF_CrossRefTable* table = parser.GetCrossRefTable(); + if (!table) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + + for (const auto& [objnum, info] : table->objects_info()) { + if (info.type == CPDF_CrossRefTable::ObjectType::kFree && + IsBaseObjectLive(base_parser, objnum)) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + } + + size_t selected_delta_object_count = 0; + for (const auto& [objnum, info] : table->objects_info()) { + if (!IsObjectOwnedByAppendedDelta(table, objnum, info, + layer_append_base_offset)) { + continue; + } + + RetainPtr parsed = parser.ParseIndirectObject(objnum); + if (!parsed) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + + RetainPtr clone = parsed->CloneForHolder(this); + if (!clone) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + clone->SetGenNum(info.gennum); + AddPromotedObject(objnum, std::move(clone)); + ++overlay_epoch_; + ++selected_delta_object_count; + } + + if (FindLocalIndirectObject(base_parser->GetRootObjNum())) { + InvalidateCachedRootDict(); + } + if (FindLocalIndirectObject(base_parser->GetInfoObjNum())) { + InvalidateCachedInfoDict(); + } + const uint32_t delta_info_objnum = parser.GetInfoObjNum(); + if (delta_info_objnum && delta_info_objnum != CPDF_Object::kInvalidObjNum && + delta_info_objnum != base_parser->GetInfoObjNum()) { + RetainPtr local_info = + FindLocalIndirectObject(delta_info_objnum); + RetainPtr info = + local_info ? pdfium::WrapRetain(local_info->AsMutableDictionary()) + : nullptr; + if (!info) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + SetCachedInfoDict(info); + } + if (selected_delta_object_count > 0 && + !RebuildPageListFromCurrentPageTree()) { + FailDeltaIngest(OpenStatus::kMalformedDelta); + return; + } + file_access_.Reset(); +} + +void CPDF_LayerDocument::FailDeltaIngest(OpenStatus status) { + ingest_status_ = status; + file_access_.Reset(); +} + +RetainPtr CPDF_LayerDocument::PromoteFromBase(uint32_t objnum) { + if (!objnum || objnum == CPDF_Object::kInvalidObjNum) { + return nullptr; + } + if (RetainPtr local = FindLocalIndirectObject(objnum)) { + return local; + } + + RetainPtr base_object = + base_->GetFrozenObjectForLayer(objnum); + if (!base_object) { + return nullptr; + } + + RetainPtr clone = base_object->CloneForHolder(this); + if (!clone) { + return nullptr; + } + clone->SetGenNum(base_object->GetGenNum()); + AddPromotedObject(objnum, clone); + ++overlay_epoch_; + + CPDF_Parser* parser = base_->GetParser(); + if (parser) { + if (parser->GetRootObjNum() == objnum) { + InvalidateCachedRootDict(); + } + if (parser->GetInfoObjNum() == objnum) { + InvalidateCachedInfoDict(); + } + } + + return clone; +} diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h new file mode 100644 index 0000000000..9532a5bca5 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -0,0 +1,86 @@ +// 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 CORE_FPDFAPI_PARSER_CPDF_LAYER_DOCUMENT_H_ +#define CORE_FPDFAPI_PARSER_CPDF_LAYER_DOCUMENT_H_ + +#include +#include + +#include + +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fxcrt/retain_ptr.h" + +class CPDF_BaseDocument; +class IFX_SeekableReadStream; + +class CPDF_LayerDocument final : public CPDF_Document { + public: + enum class OpenStatus { + kSuccess, + kMalformedDelta, + kBaseLayerMismatch, + kOpenFailed, + }; + + CPDF_LayerDocument(RetainPtr base, + RetainPtr file_access); + ~CPDF_LayerDocument() override; + + static CPDF_LayerDocument* FromDocument(CPDF_Document* document); + static const CPDF_LayerDocument* FromDocument(const CPDF_Document* document); + + OpenStatus ingest_status() const { return ingest_status_; } + size_t GetPromotedObjectCount() const; + bool HasPromotedObjects() const { return begin() != end(); } + CPDF_BaseDocument* GetBaseDocument() const { return base_.Get(); } + + // CPDF_Document: + CPDF_Parser* GetParser() const override; + const CPDF_Dictionary* GetRoot() const override; + RetainPtr GetMutableRoot() override; + RetainPtr GetMutableInfo() override; + RetainPtr GetPageDictionary(int iPage) override; + RetainPtr GetMutablePageDictionary(int iPage) override; + uint32_t GetUserPermissions(bool get_owner_perms) const override; + RetainPtr FindPromotedObject(uint32_t objnum) const override; + uint64_t GetOverlayEpoch() const override; + bool IsLayerDocument() const override; + FX_FILESIZE GetLayerAppendBaseOffset() const override; + bool ShouldReplaceDeletedPageWithNull(uint32_t page_obj_num) const override; + + // CPDF_Parser::ParsedObjectsHolder: + RetainPtr ParseIndirectObject(uint32_t objnum) override; + RetainPtr GetMutableIndirectObject(uint32_t objnum) override; + void DeleteIndirectObject(uint32_t objnum) override; + + protected: + // CPDF_IndirectObjectHolder: + const CPDF_Object* GetIndirectObjectInternal(uint32_t objnum) const override; + CPDF_Object* GetOrParseIndirectObjectInternal(uint32_t objnum) override; + + // CPDF_Document page-list storage: + uint32_t GetPageObjNumAt(size_t index) const override; + void SetPageObjNumAt(size_t index, uint32_t objnum) override; + void InsertPageObjNum(size_t index, uint32_t objnum) override; + void ErasePageObjNum(size_t index) override; + void ResizePageList(size_t size) override; + size_t GetPageListSize() const override; + + private: + void InitializeFromBase(); + void IngestCurrentDelta(); + void FailDeltaIngest(OpenStatus status); + RetainPtr PromoteFromBase(uint32_t objnum); + + RetainPtr const base_; + RetainPtr file_access_; + std::vector layer_page_list_; + // Generation for caches that retain effective-object pointers. + uint64_t overlay_epoch_ = 0; + OpenStatus ingest_status_ = OpenStatus::kSuccess; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_LAYER_DOCUMENT_H_ diff --git a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp new file mode 100644 index 0000000000..e6316f25b9 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -0,0 +1,729 @@ +// 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 "core/fpdfapi/parser/cpdf_layer_document.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_page.h" +#include "core/fpdfapi/page/cpdf_pagemodule.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_concat_read_stream.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_parser.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/cfx_read_only_span_stream.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class CPDFLayerDocumentTest : public testing::Test { + protected: + static void SetUpTestSuite() { pdfium::InitializePageModule(); } + static void TearDownTestSuite() { pdfium::DestroyPageModule(); } +}; + +class CountingReadStream final : public IFX_SeekableReadStream { + public: + CONSTRUCT_VIA_MAKE_RETAIN; + + FX_FILESIZE GetSize() override { + return static_cast(data_.size()); + } + + bool ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) override { + if (offset < 0 || static_cast(offset) > data_.size() || + buffer.size() > data_.size() - static_cast(offset)) { + return false; + } + ++read_count_; + read_bytes_ += buffer.size(); + if (!buffer.empty()) { + memcpy(buffer.data(), data_.data() + offset, buffer.size()); + } + return true; + } + + size_t read_count() const { return read_count_; } + size_t read_bytes() const { return read_bytes_; } + + private: + explicit CountingReadStream(std::string data) : data_(std::move(data)) {} + ~CountingReadStream() override = default; + + std::string data_; + size_t read_count_ = 0; + size_t read_bytes_ = 0; +}; + +std::string BuildSimplePdf() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\n" + "endobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +std::string BuildSimplePdfWithInfo() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\n" + "endobj\n", + "4 0 obj\n<< /Title (Base Title) >>\nendobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R /Info 4 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +std::string BuildPdfWithIndirectAnnotation() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100]\n" + " /Annots [4 0 R] >>\n" + "endobj\n", + "4 0 obj\n" + "<< /Type /Annot /Subtype /Polygon /Rect [10 10 30 30]\n" + " /Vertices [10 10 30 10 20 30] >>\n" + "endobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +std::string BuildPdfWithFormXObject() { + const std::string form_content = "0 0 10 10 re f\n"; + std::ostringstream form_object; + form_object << "4 0 obj\n" + << "<< /Type /XObject /Subtype /Form /BBox [0 0 100 100]\n" + << " /Resources << /BaseMarker 1 >> /Length " + << form_content.size() << " >>\n" + << "stream\n" + << form_content + << "endstream\nendobj\n"; + + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100]\n" + " /Resources << /XObject << /Fm0 4 0 R >> >> >>\n" + "endobj\n", + form_object.str(), + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +std::string BuildPdfWithDirectResources() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100]\n" + " /Resources << /ProcSet [/PDF] >> >>\n" + "endobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +size_t GetStartXrefOffsetFromPdf(const std::string& pdf) { + constexpr char kStartXref[] = "startxref\n"; + const size_t start = pdf.rfind(kStartXref); + CHECK_NE(std::string::npos, start); + return static_cast( + std::stoull(pdf.substr(start + sizeof(kStartXref) - 1))); +} + +std::string BuildFreeEntryDeltaForObject(const std::string& base_pdf, + uint32_t objnum) { + std::ostringstream delta; + const size_t xref_offset = base_pdf.size(); + delta << "xref\n" + << objnum << " 1\n0000000000 00001 f \n" + << "trailer\n<< /Size 5 /Root 1 0 R /Prev " + << GetStartXrefOffsetFromPdf(base_pdf) << " >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return delta.str(); +} + +std::string BuildCorruptPagesDelta(const std::string& base_pdf) { + std::ostringstream delta; + delta << "2 0 obj\n" + << "<< /Type /Pages /Count 1 /Kids [4 0 R] >>\n" + << "endobj\n"; + const size_t xref_offset = base_pdf.size() + delta.tellp(); + delta << "xref\n2 1\n" + << std::setw(10) << std::setfill('0') << base_pdf.size() + << " 00000 n \n" + << "trailer\n<< /Size 5 /Root 1 0 R /Prev " + << GetStartXrefOffsetFromPdf(base_pdf) << " >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return delta.str(); +} + +RetainPtr MakeStreamForString(const std::string& data) { + return pdfium::MakeRetain( + pdfium::span(reinterpret_cast(data.data()), data.size())); +} + +RetainPtr LoadBaseDocumentFromString( + const std::string& data) { + RetainPtr document = + pdfium::MakeRetain(); + if (document->LoadBaseDoc(MakeStreamForString(data), "") != + CPDF_Parser::SUCCESS) { + return nullptr; + } + return document; +} + +RetainPtr MakeLayerPage(CPDF_LayerDocument* layer, int page_index) { + RetainPtr page_dict = + layer->GetPageDictionary(page_index); + if (!page_dict) { + return nullptr; + } + return pdfium::MakeRetain( + layer, pdfium::WrapRetain(const_cast(page_dict.Get()))); +} + +} // namespace + +TEST(CPDFConcatReadStreamTest, DelegatesReadsAcrossStreamBoundary) { + RetainPtr first = + pdfium::MakeRetain("abc"); + RetainPtr second = + pdfium::MakeRetain("DEF"); + RetainPtr concat = + pdfium::MakeRetain(first, second); + + ASSERT_EQ(6, concat->GetSize()); + std::array buffer = {}; + ASSERT_TRUE(concat->ReadBlockAtOffset(pdfium::span(buffer), 2)); + EXPECT_EQ("cDEF", std::string(reinterpret_cast(buffer.data()), + buffer.size())); + EXPECT_EQ(1u, first->read_count()); + EXPECT_EQ(1u, first->read_bytes()); + EXPECT_EQ(1u, second->read_count()); + EXPECT_EQ(3u, second->read_bytes()); +} + +TEST(CPDFConcatReadStreamTest, AllowsZeroLengthReadAtEnd) { + RetainPtr concat = + pdfium::MakeRetain( + pdfium::MakeRetain("abc"), + pdfium::MakeRetain("")); + std::array buffer = {}; + EXPECT_TRUE(concat->ReadBlockAtOffset( + pdfium::span(buffer).first(static_cast(0)), 3)); + EXPECT_FALSE(concat->ReadBlockAtOffset( + pdfium::span(buffer).first(static_cast(0)), 4)); +} + +TEST_F(CPDFLayerDocumentTest, FreshLayerFallsThroughToFrozenBase) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + + auto layer = std::make_unique(base, nullptr); + + EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kSuccess, layer->ingest_status()); + EXPECT_TRUE(layer->IsLayerDocument()); + EXPECT_EQ(base->GetParser(), layer->GetParser()); + EXPECT_EQ(base->GetLastObjNum(), layer->GetLastObjNum()); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); + EXPECT_EQ(1, layer->GetPageCount()); + + RetainPtr page = layer->GetPageDictionary(0); + ASSERT_TRUE(page); + EXPECT_EQ(3u, page->GetObjNum()); + EXPECT_EQ(base->GetFrozenObjectForLayer(3).Get(), page.Get()); + EXPECT_EQ(base->GetUserPermissions(false), layer->GetUserPermissions(false)); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, DeleteBaseObjectIsNoOp) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + ASSERT_TRUE(layer->GetIndirectObject(1)); + layer->DeleteIndirectObject(1); + EXPECT_TRUE(layer->GetIndirectObject(1)); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, MalformedRawDeltaFailsClosed) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + const std::string delta = "\n% malformed delta placeholder\n"; + + auto layer = + std::make_unique(base, MakeStreamForString(delta)); + + EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kMalformedDelta, + layer->ingest_status()); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, DeltaFreeEntryOverBaseObjectFailsClosed) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + + auto layer = std::make_unique( + base, MakeStreamForString(BuildFreeEntryDeltaForObject(pdf, 3))); + + EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kMalformedDelta, + layer->ingest_status()); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, DeltaWithCorruptPageTreeFailsClosed) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + + auto layer = std::make_unique( + base, MakeStreamForString(BuildCorruptPagesDelta(pdf))); + + EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kMalformedDelta, + layer->ingest_status()); + EXPECT_TRUE(layer->FindPromotedObject(2)); +} + +TEST_F(CPDFLayerDocumentTest, GetMutableIndirectObjectPromotesFromBase) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + RetainPtr promoted = layer->GetMutableIndirectObject(1); + ASSERT_TRUE(promoted); + EXPECT_EQ(1u, promoted->GetObjNum()); + EXPECT_NE(base->GetFrozenObjectForLayer(1).Get(), promoted.Get()); + EXPECT_EQ(promoted.Get(), layer->FindPromotedObject(1).Get()); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + EXPECT_FALSE(promoted->IsFrozen()); + EXPECT_TRUE(base->GetFrozenObjectForLayer(1)->IsFrozen()); +} + +TEST_F(CPDFLayerDocumentTest, PageMutableDictPromotesAndLeavesBaseFrozen) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + auto page = MakeLayerPage(layer.get(), 0); + ASSERT_TRUE(page); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); + + RetainPtr page_dict = page->GetMutableDict(); + ASSERT_TRUE(page_dict); + EXPECT_EQ(3u, page_dict->GetObjNum()); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + EXPECT_TRUE(layer->FindPromotedObject(3)); + EXPECT_NE(base->GetFrozenObjectForLayer(3).Get(), page_dict.Get()); + + page_dict->SetNewFor("Tier3Marker", 73); + EXPECT_EQ(73, page->GetDict()->GetIntegerFor("Tier3Marker")); + ASSERT_TRUE(base->GetFrozenObjectForLayer(3)->AsDictionary()); + EXPECT_FALSE(base->GetFrozenObjectForLayer(3)->AsDictionary()->KeyExist( + "Tier3Marker")); +} + +TEST_F(CPDFLayerDocumentTest, PromotedReferencesResolveThroughLayerHolder) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + auto page = MakeLayerPage(layer.get(), 0); + ASSERT_TRUE(page); + RetainPtr page_dict = page->GetMutableDict(); + ASSERT_TRUE(page_dict); + + RetainPtr parent_ref = + ToReference(page_dict->GetObjectFor("Parent")); + ASSERT_TRUE(parent_ref); + EXPECT_TRUE(parent_ref->HasIndirectObjectHolder()); + + RetainPtr parent = page_dict->GetMutableDictFor("Parent"); + ASSERT_TRUE(parent); + EXPECT_EQ("Pages", parent->GetNameFor("Type")); + EXPECT_TRUE(layer->FindPromotedObject(2)); + EXPECT_EQ(2u, layer->GetPromotedObjectCount()); + EXPECT_FALSE(base->GetFrozenObjectForLayer(2)->AsDictionary()->KeyExist( + "Tier3ParentMarker")); + + parent->SetNewFor("Tier3ParentMarker", 91); + EXPECT_EQ( + 91, + layer->GetMutableIndirectObject(2)->AsMutableDictionary()->GetIntegerFor( + "Tier3ParentMarker")); + EXPECT_FALSE(base->GetFrozenObjectForLayer(2)->AsDictionary()->KeyExist( + "Tier3ParentMarker")); +} + +TEST_F(CPDFLayerDocumentTest, CrossHandleReadRefreshesAfterPagePromotion) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + auto page_a = MakeLayerPage(layer.get(), 0); + auto page_b = MakeLayerPage(layer.get(), 0); + ASSERT_TRUE(page_a); + ASSERT_TRUE(page_b); + + page_a->GetMutableDict()->SetNewFor("Foo", 1); + EXPECT_EQ(1, page_b->GetDict()->GetIntegerFor("Foo")); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + + page_b->GetMutableDict()->SetNewFor("Bar", 2); + EXPECT_EQ(2, page_a->GetDict()->GetIntegerFor("Bar")); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + EXPECT_FALSE( + base->GetFrozenObjectForLayer(3)->AsDictionary()->KeyExist("Foo")); + EXPECT_FALSE( + base->GetFrozenObjectForLayer(3)->AsDictionary()->KeyExist("Bar")); +} + +TEST_F(CPDFLayerDocumentTest, + FrozenReferencesResolveThroughOwningLayerWithoutCrossLayerLeakage) { + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kNone, + CPDF_DocumentViewScope::GetCurrentMode()); + const std::string pdf = BuildPdfWithIndirectAnnotation(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer_a = std::make_unique(base, nullptr); + auto layer_b = std::make_unique(base, nullptr); + + auto page_a = MakeLayerPage(layer_a.get(), 0); + auto page_b = MakeLayerPage(layer_b.get(), 0); + ASSERT_TRUE(page_a); + ASSERT_TRUE(page_b); + + RetainPtr annots_a = page_a->GetAnnotsArray(); + RetainPtr annots_b = page_b->GetAnnotsArray(); + ASSERT_TRUE(annots_a); + ASSERT_TRUE(annots_b); + ASSERT_TRUE(annots_a->GetDictAt(0)); + ASSERT_TRUE(annots_b->GetDictAt(0)); + EXPECT_FALSE(annots_a->GetDictAt(0)->KeyExist("LayerMarker")); + EXPECT_FALSE(annots_b->GetDictAt(0)->KeyExist("LayerMarker")); + + RetainPtr promoted_a = layer_a->GetMutableIndirectObject(4); + ASSERT_TRUE(promoted_a); + promoted_a->AsMutableDictionary()->SetNewFor("LayerMarker", 41); + + { + CPDF_DocumentViewScope scope(layer_a.get()); + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kEffective, + CPDF_DocumentViewScope::GetCurrentMode()); + EXPECT_EQ(41, page_a->GetAnnotsArray()->GetDictAt(0)->GetIntegerFor( + "LayerMarker")); + } + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kNone, + CPDF_DocumentViewScope::GetCurrentMode()); + { + CPDF_DocumentViewScope scope(layer_b.get()); + EXPECT_FALSE( + page_b->GetAnnotsArray()->GetDictAt(0)->KeyExist("LayerMarker")); + } + + RetainPtr promoted_b = layer_b->GetMutableIndirectObject(4); + ASSERT_TRUE(promoted_b); + promoted_b->AsMutableDictionary()->SetNewFor("LayerMarker", 82); + + { + CPDF_DocumentViewScope scope_a(layer_a.get()); + EXPECT_EQ(41, page_a->GetAnnotsArray()->GetDictAt(0)->GetIntegerFor( + "LayerMarker")); + { + CPDF_DocumentViewScope scope_b(layer_b.get()); + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kEffective, + CPDF_DocumentViewScope::GetCurrentMode()); + EXPECT_EQ(82, page_b->GetAnnotsArray()->GetDictAt(0)->GetIntegerFor( + "LayerMarker")); + } + EXPECT_EQ(41, page_a->GetAnnotsArray()->GetDictAt(0)->GetIntegerFor( + "LayerMarker")); + } + EXPECT_FALSE(base->GetFrozenObjectForLayer(4)->AsDictionary()->KeyExist( + "LayerMarker")); + + { + CPDF_DocumentViewScope frozen_scope(base.Get()); + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kFrozen, + CPDF_DocumentViewScope::GetCurrentMode()); + EXPECT_FALSE(base->GetOrParseIndirectObject(4) + ->AsDictionary() + ->KeyExist("LayerMarker")); + } + EXPECT_EQ(CPDF_DocumentViewScope::Mode::kNone, + CPDF_DocumentViewScope::GetCurrentMode()); +} + +#if DCHECK_IS_ON() +TEST_F(CPDFLayerDocumentTest, + DebugDetectorRejectsUnscopedPromotedBaseResolution) { + RetainPtr base = + LoadBaseDocumentFromString(BuildPdfWithIndirectAnnotation()); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + ASSERT_TRUE(layer->GetMutableIndirectObject(4)); + + EXPECT_DEATH_IF_SUPPORTED(base->GetOrParseIndirectObject(4), ""); + { + CPDF_DocumentViewScope frozen_scope(base.Get()); + EXPECT_TRUE(base->GetOrParseIndirectObject(4)); + } + { + CPDF_DocumentViewScope effective_scope(layer.get()); + EXPECT_EQ(layer->FindPromotedObject(4).Get(), + base->GetOrParseIndirectObject(4).Get()); + } +} +#endif // DCHECK_IS_ON() + +TEST_F(CPDFLayerDocumentTest, + ParsedFormRefreshesAfterSiblingPromotesStream) { + RetainPtr base = + LoadBaseDocumentFromString(BuildPdfWithFormXObject()); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + RetainPtr form_object = layer->GetIndirectObject(4); + ASSERT_TRUE(form_object); + const CPDF_Stream* form_stream = form_object->AsStream(); + ASSERT_TRUE(form_stream); + + CPDF_Form writer( + layer.get(), nullptr, + pdfium::WrapRetain(const_cast(form_stream))); + CPDF_Form reader( + layer.get(), nullptr, + pdfium::WrapRetain(const_cast(form_stream))); + writer.ParseContent(); + reader.ParseContent(); + ASSERT_EQ(1u, reader.GetPageObjectCount()); + EXPECT_FLOAT_EQ(10.0f, reader.CalcBoundingBox().Width()); + EXPECT_EQ(1, reader.GetResources()->GetIntegerFor("BaseMarker")); + + RetainPtr mutable_stream = writer.GetMutableFormStream(); + ASSERT_TRUE(mutable_stream); + fxcrt::ostringstream updated_content; + updated_content << "0 0 20 20 re f\n"; + mutable_stream->SetDataFromStringstreamAndRemoveFilter(&updated_content); + mutable_stream->GetMutableDict() + ->GetMutableDictFor("Resources") + ->SetNewFor("LayerMarker", 9); + + reader.ParseContent(); + ASSERT_EQ(1u, reader.GetPageObjectCount()); + EXPECT_FLOAT_EQ(20.0f, reader.CalcBoundingBox().Width()); + EXPECT_EQ(9, reader.GetResources()->GetIntegerFor("LayerMarker")); +} + +TEST_F(CPDFLayerDocumentTest, GetOrCreateInfoPromotesBaseInfo) { + const std::string pdf = BuildSimplePdfWithInfo(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + ASSERT_TRUE(base->GetInfo()); + EXPECT_EQ("Base Title", base->GetInfo()->GetUnicodeTextFor("Title").ToUTF8()); + + auto layer_a = std::make_unique(base, nullptr); + auto layer_b = std::make_unique(base, nullptr); + EXPECT_EQ(0u, layer_a->GetPromotedObjectCount()); + EXPECT_EQ(0u, layer_b->GetPromotedObjectCount()); + + RetainPtr info = layer_a->GetOrCreateInfo(); + ASSERT_TRUE(info); + EXPECT_EQ(4u, info->GetObjNum()); + EXPECT_EQ(1u, layer_a->GetPromotedObjectCount()); + EXPECT_TRUE(layer_a->FindPromotedObject(4)); + EXPECT_NE(base->GetFrozenObjectForLayer(4).Get(), info.Get()); + + info->SetNewFor("Title", "Layer A Title"); + + EXPECT_EQ("Layer A Title", + layer_a->GetInfo()->GetUnicodeTextFor("Title").ToUTF8()); + EXPECT_EQ("Base Title", + layer_b->GetInfo()->GetUnicodeTextFor("Title").ToUTF8()); + EXPECT_EQ("Base Title", base->GetInfo()->GetUnicodeTextFor("Title").ToUTF8()); + EXPECT_EQ(0u, layer_b->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, + GetOrCreateInfoCreatesLayerLocalInfoWhenBaseHasNone) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + EXPECT_FALSE(base->GetInfo()); + + auto layer = std::make_unique(base, nullptr); + EXPECT_FALSE(layer->GetInfo()); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); + + RetainPtr info = layer->GetOrCreateInfo(); + ASSERT_TRUE(info); + EXPECT_NE(0u, info->GetObjNum()); + EXPECT_EQ(info.Get(), layer->FindPromotedObject(info->GetObjNum()).Get()); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + + info->SetNewFor("Title", "Layer Title"); + + EXPECT_EQ("Layer Title", + layer->GetInfo()->GetUnicodeTextFor("Title").ToUTF8()); + EXPECT_FALSE(base->GetInfo()); +} + +TEST_F(CPDFLayerDocumentTest, DirectResourcesPromoteOwningPage) { + const std::string pdf = BuildPdfWithDirectResources(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + auto page = MakeLayerPage(layer.get(), 0); + auto second_page_handle = MakeLayerPage(layer.get(), 0); + ASSERT_TRUE(page); + ASSERT_TRUE(second_page_handle); + RetainPtr resources = page->GetMutableResources(); + ASSERT_TRUE(resources); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + + resources->SetNewFor("Tier3ResourceMarker", 5); + EXPECT_EQ(5, page->GetResources()->GetIntegerFor("Tier3ResourceMarker")); + EXPECT_EQ(5, second_page_handle->GetResources()->GetIntegerFor( + "Tier3ResourceMarker")); + + RetainPtr base_page = + base->GetFrozenObjectForLayer(3)->GetDict(); + ASSERT_TRUE(base_page); + RetainPtr base_resources = + base_page->GetDictFor("Resources"); + ASSERT_TRUE(base_resources); + EXPECT_FALSE(base_resources->KeyExist("Tier3ResourceMarker")); +} + +#if DCHECK_IS_ON() +TEST_F(CPDFLayerDocumentTest, ParseIndirectObjectStillUnsupportedOnLayer) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = std::make_unique(base, nullptr); + + EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); +} +#endif // DCHECK_IS_ON() diff --git a/core/fpdfapi/parser/cpdf_name.cpp b/core/fpdfapi/parser/cpdf_name.cpp index 8fd027aa4b..95204b943f 100644 --- a/core/fpdfapi/parser/cpdf_name.cpp +++ b/core/fpdfapi/parser/cpdf_name.cpp @@ -8,6 +8,7 @@ #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" CPDF_Name::CPDF_Name(WeakPtr pPool, const ByteString& str) @@ -32,6 +33,7 @@ ByteString CPDF_Name::GetString() const { } void CPDF_Name::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); name_ = str; } diff --git a/core/fpdfapi/parser/cpdf_number.cpp b/core/fpdfapi/parser/cpdf_number.cpp index cd5a9ed53a..7179d0dd03 100644 --- a/core/fpdfapi/parser/cpdf_number.cpp +++ b/core/fpdfapi/parser/cpdf_number.cpp @@ -9,6 +9,7 @@ #include #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" #include "core/fxcrt/fx_string_wrappers.h" @@ -55,6 +56,7 @@ CPDF_Number* CPDF_Number::AsMutableNumber() { } void CPDF_Number::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); number_ = FX_Number(str.AsStringView()); } diff --git a/core/fpdfapi/parser/cpdf_object.cpp b/core/fpdfapi/parser/cpdf_object.cpp index 1f901acd09..81c02a882b 100644 --- a/core/fpdfapi/parser/cpdf_object.cpp +++ b/core/fpdfapi/parser/cpdf_object.cpp @@ -7,6 +7,7 @@ #include "core/fpdfapi/parser/cpdf_object.h" #include +#include #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" @@ -34,6 +35,22 @@ uint64_t CPDF_Object::KeyForCache() const { static_cast(gen_num_); } +void CPDF_Object::Freeze() { + std::set visited; + FreezeForHolder(&visited); +} + +void CPDF_Object::FreezeForHolder(std::set* visited) { + if (!visited->insert(this).second) { + return; + } + + frozen_ = true; + FreezeChildren(visited); +} + +void CPDF_Object::FreezeChildren(std::set*) {} + RetainPtr CPDF_Object::GetMutableDirect() { return pdfium::WrapRetain(const_cast(GetDirectInternal())); } @@ -55,12 +72,24 @@ RetainPtr CPDF_Object::CloneDirectObject() const { return CloneObjectNonCyclic(true); } +RetainPtr CPDF_Object::CloneForHolder( + CPDF_IndirectObjectHolder* holder) const { + std::set visited_objs; + return CloneForHolderNonCyclic(holder, &visited_objs); +} + RetainPtr CPDF_Object::CloneNonCyclic( bool bDirect, std::set* pVisited) const { return Clone(); } +RetainPtr CPDF_Object::CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const { + return CloneNonCyclic(/*bDirect=*/false, pVisited); +} + ByteString CPDF_Object::GetString() const { return ByteString(); } diff --git a/core/fpdfapi/parser/cpdf_object.h b/core/fpdfapi/parser/cpdf_object.h index 8de91f6441..a954b6473e 100644 --- a/core/fpdfapi/parser/cpdf_object.h +++ b/core/fpdfapi/parser/cpdf_object.h @@ -74,6 +74,16 @@ class CPDF_Object : public Retainable { // Create a deep copy of the object. virtual RetainPtr Clone() const = 0; + // Create a deep copy of the object for `holder`. References in the clone are + // retargeted to `holder`, so promoted layer objects resolve through the + // layer overlay rather than the frozen base. + virtual RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const; + + void Freeze(); + void FreezeForHolder(std::set* visited); + bool IsFrozen() const { return frozen_; } + // Create a deep copy of the object except any reference object be // copied to the object it points to directly. RetainPtr CloneDirectObject() const; @@ -110,13 +120,20 @@ class CPDF_Object : public Retainable { bool bDirect, std::set* pVisited) const; + virtual RetainPtr CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const; + // Return a reference to itself. // The object must be direct (!IsInlined). virtual RetainPtr MakeReference( CPDF_IndirectObjectHolder* holder) const; - RetainPtr GetDirect() const; // Wraps virtual method. - RetainPtr GetMutableDirect(); // Wraps virtual method. + virtual void FreezeChildren(std::set* visited); + + RetainPtr GetDirect() const; // Wraps virtual method. + virtual RetainPtr GetMutableDirect(); + // Wraps virtual method. RetainPtr GetDict() const; // Wraps virtual method. RetainPtr GetMutableDict(); // Wraps virtual method. @@ -156,6 +173,7 @@ class CPDF_Object : public Retainable { uint32_t obj_num_ = 0; uint32_t gen_num_ = 0; + bool frozen_ = false; }; template diff --git a/core/fpdfapi/parser/cpdf_parser.cpp b/core/fpdfapi/parser/cpdf_parser.cpp index 30dcb11085..be440b584f 100644 --- a/core/fpdfapi/parser/cpdf_parser.cpp +++ b/core/fpdfapi/parser/cpdf_parser.cpp @@ -591,6 +591,11 @@ bool CPDF_Parser::ParseAndAppendCrossRefSubsectionData( pdfium::span pEntry = pdfium::span(buf).subspan(i * kEntrySize); + // TODO(art-snake): The info.gennum is uint16_t, but version may be + // greater than max. Need to solve this issue. + const int32_t version = + StringToInt(ByteStringView(pEntry.subspan<11u>())); + info.gennum = version; if (pEntry[17] == 'f') { info.pos = 0; info.type = ObjectType::kFree; @@ -610,11 +615,6 @@ bool CPDF_Parser::ParseAndAppendCrossRefSubsectionData( info.pos = offset.ValueOrDie(); - // TODO(art-snake): The info.gennum is uint16_t, but version may be - // greater than max. Need to solve this issue. - const int32_t version = - StringToInt(ByteStringView(pEntry.subspan<11u>())); - info.gennum = version; info.type = ObjectType::kNormal; } } @@ -1166,6 +1166,10 @@ FX_FILESIZE CPDF_Parser::GetDocumentSize() const { return syntax_->GetDocumentSize(); } +RetainPtr CPDF_Parser::GetFileAccess() const { + return syntax_ ? syntax_->GetFileAccess() : nullptr; +} + uint32_t CPDF_Parser::GetFirstPageNo() const { return linearized_ ? linearized_->GetFirstPageNo() : 0; } diff --git a/core/fpdfapi/parser/cpdf_parser.h b/core/fpdfapi/parser/cpdf_parser.h index 613a1d750d..1a3168d6c8 100644 --- a/core/fpdfapi/parser/cpdf_parser.h +++ b/core/fpdfapi/parser/cpdf_parser.h @@ -109,6 +109,7 @@ class CPDF_Parser { bool IsXRefStream() const { return xref_stream_; } FX_FILESIZE GetDocumentSize() const; + RetainPtr GetFileAccess() const; uint32_t GetFirstPageNo() const; const CPDF_LinearizedHeader* GetLinearizedHeader() const { return linearized_.get(); @@ -119,9 +120,12 @@ class CPDF_Parser { std::vector GetTrailerEnds(); bool WriteToArchive(IFX_ArchiveStream* archive, FX_FILESIZE src_size); - const CPDF_CrossRefTable* GetCrossRefTableForTesting() const { + const CPDF_CrossRefTable* GetCrossRefTable() const { return cross_ref_table_.get(); } + const CPDF_CrossRefTable* GetCrossRefTableForTesting() const { + return GetCrossRefTable(); + } CPDF_Dictionary* GetMutableTrailerForTesting(); diff --git a/core/fpdfapi/parser/cpdf_parser_unittest.cpp b/core/fpdfapi/parser/cpdf_parser_unittest.cpp index ecfd50f236..0c4301b3fd 100644 --- a/core/fpdfapi/parser/cpdf_parser_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_parser_unittest.cpp @@ -204,6 +204,8 @@ TEST(ParserTest, LoadCrossRefTable) { EXPECT_EQ(kExpected[i].offset, GetObjInfo(parser, i).pos); EXPECT_EQ(kExpected[i].type, GetObjInfo(parser, i).type); } + EXPECT_EQ(65535u, GetObjInfo(parser, 0).gennum); + EXPECT_EQ(7u, GetObjInfo(parser, 3).gennum); } { static const unsigned char kXrefTable[] = diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp new file mode 100644 index 0000000000..61e3c99bdd --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp @@ -0,0 +1,39 @@ +// 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 "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" + +namespace { + +thread_local bool g_read_only_graph_guard_active = false; +thread_local int g_inline_rewrite_depth = 0; + +} // namespace + +CPDF_ReadOnlyGraphGuard::CPDF_ReadOnlyGraphGuard() + : previous_(g_read_only_graph_guard_active) { + g_read_only_graph_guard_active = true; +} + +CPDF_ReadOnlyGraphGuard::~CPDF_ReadOnlyGraphGuard() { + g_read_only_graph_guard_active = previous_; +} + +// static +bool CPDF_ReadOnlyGraphGuard::IsActive() { + return g_read_only_graph_guard_active; +} + +// static +bool CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive() { + return g_inline_rewrite_depth > 0; +} + +CPDF_ScopedInlineRewrite::CPDF_ScopedInlineRewrite() { + ++g_inline_rewrite_depth; +} + +CPDF_ScopedInlineRewrite::~CPDF_ScopedInlineRewrite() { + --g_inline_rewrite_depth; +} diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard.h b/core/fpdfapi/parser/cpdf_read_only_graph_guard.h new file mode 100644 index 0000000000..ee3cae76ea --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.h @@ -0,0 +1,39 @@ +// 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 CORE_FPDFAPI_PARSER_CPDF_READ_ONLY_GRAPH_GUARD_H_ +#define CORE_FPDFAPI_PARSER_CPDF_READ_ONLY_GRAPH_GUARD_H_ + +#include "core/fxcrt/check.h" + +class CPDF_ReadOnlyGraphGuard { + public: + CPDF_ReadOnlyGraphGuard(); + ~CPDF_ReadOnlyGraphGuard(); + + static bool IsActive(); + static bool IsInlineRewriteActive(); + + private: + const bool previous_; +}; + +class CPDF_ScopedInlineRewrite { + public: + CPDF_ScopedInlineRewrite(); + ~CPDF_ScopedInlineRewrite(); +}; + +#if DCHECK_IS_ON() +#define DCHECK_PDF_GRAPH_MUTABLE_FOR(obj) \ + DCHECK(!CPDF_ReadOnlyGraphGuard::IsActive() || \ + CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive() || \ + (obj)->GetObjNum() == 0) +#define DCHECK_PDF_HOLDER_MUTABLE() DCHECK(!CPDF_ReadOnlyGraphGuard::IsActive()) +#else +#define DCHECK_PDF_GRAPH_MUTABLE_FOR(obj) ((void)0) +#define DCHECK_PDF_HOLDER_MUTABLE() ((void)0) +#endif + +#endif // CORE_FPDFAPI_PARSER_CPDF_READ_ONLY_GRAPH_GUARD_H_ diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp new file mode 100644 index 0000000000..7e761faf91 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp @@ -0,0 +1,44 @@ +// 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 "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" + +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CPDFReadOnlyGraphGuardTest, ActiveStateStacks) { + EXPECT_FALSE(CPDF_ReadOnlyGraphGuard::IsActive()); + { + CPDF_ReadOnlyGraphGuard guard; + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsActive()); + { + CPDF_ReadOnlyGraphGuard nested_guard; + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsActive()); + } + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsActive()); + } + EXPECT_FALSE(CPDF_ReadOnlyGraphGuard::IsActive()); +} + +TEST(CPDFReadOnlyGraphGuardTest, AllowsInlineObjects) { + auto dict = pdfium::MakeRetain(); + ASSERT_EQ(0u, dict->GetObjNum()); + + CPDF_ReadOnlyGraphGuard guard; + DCHECK_PDF_GRAPH_MUTABLE_FOR(dict.Get()); +} + +TEST(CPDFReadOnlyGraphGuardTest, InlineRewriteStateStacks) { + EXPECT_FALSE(CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive()); + { + CPDF_ScopedInlineRewrite rewrite; + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive()); + { + CPDF_ScopedInlineRewrite nested_rewrite; + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive()); + } + EXPECT_TRUE(CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive()); + } + EXPECT_FALSE(CPDF_ReadOnlyGraphGuard::IsInlineRewriteActive()); +} diff --git a/core/fpdfapi/parser/cpdf_read_validator.h b/core/fpdfapi/parser/cpdf_read_validator.h index c8665416e3..8056b508bb 100644 --- a/core/fpdfapi/parser/cpdf_read_validator.h +++ b/core/fpdfapi/parser/cpdf_read_validator.h @@ -43,6 +43,7 @@ class CPDF_ReadValidator : public IFX_SeekableReadStream { bool IsWholeFileAvailable(); bool CheckDataRangeAndRequestIfUnavailable(FX_FILESIZE offset, size_t size); bool CheckWholeFileAndRequestIfUnavailable(); + RetainPtr GetFileAccess() const { return file_read_; } // IFX_SeekableReadStream overrides: bool ReadBlockAtOffset(pdfium::span buffer, diff --git a/core/fpdfapi/parser/cpdf_reference.cpp b/core/fpdfapi/parser/cpdf_reference.cpp index 8dc56d5281..06e4d35cf8 100644 --- a/core/fpdfapi/parser/cpdf_reference.cpp +++ b/core/fpdfapi/parser/cpdf_reference.cpp @@ -49,6 +49,16 @@ RetainPtr CPDF_Reference::Clone() const { return CloneObjectNonCyclic(false); } +RetainPtr CPDF_Reference::CloneForHolder( + CPDF_IndirectObjectHolder* holder) const { + return pdfium::MakeRetain(holder, ref_obj_num_); +} + +RetainPtr CPDF_Reference::GetMutableDirect() { + return obj_list_ ? obj_list_->GetMutableIndirectObject(ref_obj_num_) + : nullptr; +} + RetainPtr CPDF_Reference::CloneNonCyclic( bool bDirect, std::set* pVisited) const { @@ -62,6 +72,13 @@ RetainPtr CPDF_Reference::CloneNonCyclic( : nullptr; } +RetainPtr CPDF_Reference::CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const { + pVisited->insert(this); + return pdfium::MakeRetain(holder, ref_obj_num_); +} + const CPDF_Object* CPDF_Reference::FastGetDirect() const { if (!obj_list_) { return nullptr; diff --git a/core/fpdfapi/parser/cpdf_reference.h b/core/fpdfapi/parser/cpdf_reference.h index 63baff1b0a..b0828a0f97 100644 --- a/core/fpdfapi/parser/cpdf_reference.h +++ b/core/fpdfapi/parser/cpdf_reference.h @@ -22,6 +22,9 @@ class CPDF_Reference final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; + RetainPtr GetMutableDirect() override; ByteString GetString() const override; float GetNumber() const override; int GetInteger() const override; @@ -46,6 +49,9 @@ class CPDF_Reference final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, std::set* pVisited) const override; + RetainPtr CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const override; const CPDF_Object* FastGetDirect() const; diff --git a/core/fpdfapi/parser/cpdf_security_handler.cpp b/core/fpdfapi/parser/cpdf_security_handler.cpp index debcaddfca..be1a71e11c 100644 --- a/core/fpdfapi/parser/cpdf_security_handler.cpp +++ b/core/fpdfapi/parser/cpdf_security_handler.cpp @@ -230,6 +230,17 @@ uint32_t CPDF_SecurityHandler::GetPermissions(bool get_owner_perms) const { return dwPermission; } +uint32_t CPDF_SecurityHandler::GetPermissionsForPasswordProbe( + bool owner) const { + uint32_t dwPermission = owner ? 0xFFFFFFFF : permissions_; + if (encrypt_dict_ && + encrypt_dict_->GetByteStringFor("Filter") == "Standard") { + dwPermission &= 0xFFFFFFFC; + dwPermission |= 0xFFFFF0C0; + } + return dwPermission; +} + bool CPDF_SecurityHandler::UnlockOwner(const ByteString& password) { if (owner_unlocked_) { return true; // Already unlocked @@ -246,6 +257,20 @@ bool CPDF_SecurityHandler::UnlockOwner(const ByteString& password) { return false; } +bool CPDF_SecurityHandler::CheckPasswordNoMutate(const ByteString& password, + bool bOwner) { + const PasswordEncodingConversion saved_conversion = + password_encoding_conversion_; + const std::array saved_key = encrypt_key_; + + password_encoding_conversion_ = kUnknown; + const bool valid = CheckPassword(password, bOwner); + + password_encoding_conversion_ = saved_conversion; + encrypt_key_ = saved_key; + return valid; +} + static bool LoadCryptInfo(const CPDF_Dictionary* pEncryptDict, const ByteString& name, CPDF_CryptoHandler::Cipher* cipher, diff --git a/core/fpdfapi/parser/cpdf_security_handler.h b/core/fpdfapi/parser/cpdf_security_handler.h index 702dcb447e..e15985a266 100644 --- a/core/fpdfapi/parser/cpdf_security_handler.h +++ b/core/fpdfapi/parser/cpdf_security_handler.h @@ -40,6 +40,7 @@ class CPDF_SecurityHandler final : public Retainable { // When `get_owner_perms` is true, returns full permissions if unlocked by // owner. uint32_t GetPermissions(bool get_owner_perms) const; + uint32_t GetPermissionsForPasswordProbe(bool owner) const; bool IsMetadataEncrypted() const; CPDF_CryptoHandler* GetCryptoHandler() const { return crypto_handler_.get(); } @@ -54,6 +55,15 @@ class CPDF_SecurityHandler final : public Retainable { // Returns false if password is invalid, empty, or document isn't encrypted. bool UnlockOwner(const ByteString& password); + // Checks a password without changing the document's current unlock state. + // This is for app-level authorization probes; it must not be used to decrypt + // content for normal document operation. + bool CheckPasswordNoMutate(const ByteString& password, bool bOwner); + + // Runtime policy override used by EmbedPDF after app-level authorization has + // been handled outside PDFium. This changes only this opened document handle. + void SetRuntimeOwnerUnlocked(bool enabled) { owner_unlocked_ = enabled; } + // Returns true if owner permissions are currently unlocked. bool IsOwnerUnlocked() const { return owner_unlocked_; } diff --git a/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp b/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp index 5cbf93f2b2..4c8789a8e7 100644 --- a/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp +++ b/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp @@ -138,6 +138,77 @@ TEST_F(CPDFSecurityHandlerEmbedderTest, OwnerPassword) { EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocUserPermissions(document())); } +TEST_F(CPDFSecurityHandlerEmbedderTest, + CheckPasswordPermissionsIgnoresCurrentOwnerUnlockState) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "5678")); + ASSERT_TRUE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFFFFC, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "1234", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_USER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFF2C0u, effective_permissions); + EXPECT_GE(revision, 0); + + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "5678", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_OWNER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFFFFCu, effective_permissions); +} + +TEST_F(CPDFSecurityHandlerEmbedderTest, + CheckPasswordPermissionsDoesNotUnlockOwner) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "1234")); + ASSERT_FALSE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "5678", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_OWNER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFFFFCu, effective_permissions); + EXPECT_FALSE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); +} + +TEST_F(CPDFSecurityHandlerEmbedderTest, SetRuntimeOwnerPermissions) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "1234")); + ASSERT_FALSE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); + + EXPECT_TRUE(EPDF_SetRuntimeOwnerPermissions(document(), true)); + EXPECT_TRUE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFFFFC, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "1234", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_USER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFF2C0u, effective_permissions); + + EXPECT_TRUE(EPDF_SetRuntimeOwnerPermissions(document(), false)); + EXPECT_FALSE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); +} + TEST_F(CPDFSecurityHandlerEmbedderTest, PasswordAfterGenerateSave) { constexpr char kBasename[] = "encrypted"; { diff --git a/core/fpdfapi/parser/cpdf_stream.cpp b/core/fpdfapi/parser/cpdf_stream.cpp index d6700e8b3f..057404aff2 100644 --- a/core/fpdfapi/parser/cpdf_stream.cpp +++ b/core/fpdfapi/parser/cpdf_stream.cpp @@ -17,6 +17,7 @@ #include "core/fpdfapi/parser/cpdf_encryptor.h" #include "core/fpdfapi/parser/cpdf_flateencoder.h" #include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fpdfapi/parser/cpdf_stream_acc.h" #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" @@ -87,6 +88,8 @@ CPDF_Stream* CPDF_Stream::AsMutableStream() { } void CPDF_Stream::InitStreamFromFile(RetainPtr file) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); const int size = pdfium::checked_cast(file->GetSize()); data_ = std::move(file); dict_ = pdfium::MakeRetain(); @@ -97,6 +100,12 @@ RetainPtr CPDF_Stream::Clone() const { return CloneObjectNonCyclic(false); } +RetainPtr CPDF_Stream::CloneForHolder( + CPDF_IndirectObjectHolder* holder) const { + std::set visited; + return CloneForHolderNonCyclic(holder, &visited); +} + RetainPtr CPDF_Stream::CloneNonCyclic( bool bDirect, std::set* pVisited) const { @@ -114,7 +123,30 @@ RetainPtr CPDF_Stream::CloneNonCyclic( std::move(pNewDict)); } +RetainPtr CPDF_Stream::CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const { + pVisited->insert(this); + auto pAcc = pdfium::MakeRetain(pdfium::WrapRetain(this)); + pAcc->LoadAllDataRaw(); + + RetainPtr dict = GetDict(); + RetainPtr pNewDict; + if (!pdfium::Contains(*pVisited, dict.Get())) { + pNewDict = ToDictionary(static_cast(dict.Get()) + ->CloneForHolderNonCyclic(holder, pVisited)); + } + return pdfium::MakeRetain(pAcc->DetachData(), + std::move(pNewDict)); +} + +void CPDF_Stream::FreezeChildren(std::set* visited) { + dict_->FreezeForHolder(visited); +} + void CPDF_Stream::SetDataAndRemoveFilter(pdfium::span pData) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); SetData(pData); dict_->RemoveFor("Filter"); dict_->RemoveFor(pdfium::stream::kDecodeParms); @@ -131,17 +163,23 @@ void CPDF_Stream::SetDataFromStringstreamAndRemoveFilter( } void CPDF_Stream::SetData(pdfium::span pData) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); DataVector data_copy(pData.begin(), pData.end()); TakeData(std::move(data_copy)); } void CPDF_Stream::TakeData(DataVector data) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); const int size = pdfium::checked_cast(data.size()); data_ = std::move(data); SetLengthInDict(size); } void CPDF_Stream::SetDataFromStringstream(fxcrt::ostringstream* stream) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); if (stream->tellp() <= 0) { SetData({}); return; @@ -216,5 +254,7 @@ pdfium::span CPDF_Stream::GetInMemoryRawData() const { } void CPDF_Stream::SetLengthInDict(int length) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); dict_->SetNewFor("Length", length); } diff --git a/core/fpdfapi/parser/cpdf_stream.h b/core/fpdfapi/parser/cpdf_stream.h index ffeea79cac..b291a724d1 100644 --- a/core/fpdfapi/parser/cpdf_stream.h +++ b/core/fpdfapi/parser/cpdf_stream.h @@ -29,6 +29,8 @@ class CPDF_Stream final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; WideString GetUnicodeText() const override; CPDF_Stream* AsMutableStream() override; bool WriteTo(IFX_ArchiveStream* archive, @@ -90,6 +92,10 @@ class CPDF_Stream final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, std::set* pVisited) const override; + RetainPtr CloneForHolderNonCyclic( + CPDF_IndirectObjectHolder* holder, + std::set* pVisited) const override; + void FreezeChildren(std::set* visited) override; void SetLengthInDict(int length); diff --git a/core/fpdfapi/parser/cpdf_string.cpp b/core/fpdfapi/parser/cpdf_string.cpp index 8bdba046ce..2b4553d792 100644 --- a/core/fpdfapi/parser/cpdf_string.cpp +++ b/core/fpdfapi/parser/cpdf_string.cpp @@ -11,7 +11,9 @@ #include #include "core/fpdfapi/parser/cpdf_encryptor.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fpdfapi/parser/fpdf_parser_decode.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/data_vector.h" #include "core/fxcrt/fx_stream.h" @@ -56,6 +58,8 @@ ByteString CPDF_String::GetString() const { } void CPDF_String::SetString(const ByteString& str) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); data_ = str; } diff --git a/core/fpdfapi/parser/cpdf_syntax_parser.cpp b/core/fpdfapi/parser/cpdf_syntax_parser.cpp index 749e59c07b..3753019c64 100644 --- a/core/fpdfapi/parser/cpdf_syntax_parser.cpp +++ b/core/fpdfapi/parser/cpdf_syntax_parser.cpp @@ -76,7 +76,7 @@ class ReadableSubStream final : public IFX_SeekableReadStream { } // namespace // static -int CPDF_SyntaxParser::s_CurrentRecursionDepth = 0; +EPDF_TLS int CPDF_SyntaxParser::s_CurrentRecursionDepth = 0; // static std::unique_ptr CPDF_SyntaxParser::CreateForTesting( @@ -908,6 +908,10 @@ RetainPtr CPDF_SyntaxParser::GetValidator() const { return file_access_; } +RetainPtr CPDF_SyntaxParser::GetFileAccess() const { + return file_access_ ? file_access_->GetFileAccess() : nullptr; +} + bool CPDF_SyntaxParser::IsWholeWord(FX_FILESIZE startpos, FX_FILESIZE limit, ByteStringView tag, diff --git a/core/fpdfapi/parser/cpdf_syntax_parser.h b/core/fpdfapi/parser/cpdf_syntax_parser.h index 79fc68d635..283dcbb20c 100644 --- a/core/fpdfapi/parser/cpdf_syntax_parser.h +++ b/core/fpdfapi/parser/cpdf_syntax_parser.h @@ -15,6 +15,7 @@ #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/data_vector.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_types.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" @@ -70,6 +71,7 @@ class CPDF_SyntaxParser { ByteString PeekNextWord(); RetainPtr GetValidator() const; + RetainPtr GetFileAccess() const; uint32_t GetDirectNum(); bool GetNextChar(uint8_t& ch); @@ -95,7 +97,7 @@ class CPDF_SyntaxParser { friend class cpdf_syntax_parser_ReadHexString_Test; static constexpr int kParserMaxRecursionDepth = 64; - static int s_CurrentRecursionDepth; + static EPDF_TLS int s_CurrentRecursionDepth; bool ReadBlockAt(FX_FILESIZE read_pos); bool GetCharAtBackward(FX_FILESIZE pos, uint8_t* ch); diff --git a/core/fpdfapi/parser/object_tree_traversal_util.cpp b/core/fpdfapi/parser/object_tree_traversal_util.cpp index eebcff1e4e..331437d001 100644 --- a/core/fpdfapi/parser/object_tree_traversal_util.cpp +++ b/core/fpdfapi/parser/object_tree_traversal_util.cpp @@ -25,8 +25,9 @@ namespace { class ObjectTreeTraverser { public: - explicit ObjectTreeTraverser(const CPDF_Document* document) - : document_(document) { + ObjectTreeTraverser(const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) + : document_(document), resolve_mode_(resolve_mode) { const CPDF_Parser* parser = document_->GetParser(); const CPDF_Dictionary* trailer = parser ? parser->GetTrailer() : nullptr; const CPDF_Dictionary* root = trailer ? trailer : document_->GetRoot(); @@ -83,11 +84,17 @@ class ObjectTreeTraverser { const uint32_t referenced_object_number = ref_object->GetRefObjNum(); RetainPtr referenced_object; - if (ref_object->HasIndirectObjectHolder()) { - // Calling GetIndirectObject() does not work for normal references. + if (resolve_mode_ == + ObjectTreeReferenceResolveMode::kEffectiveDocument) { + referenced_object = + document_->GetIndirectObject(referenced_object_number); + } else if (ref_object->HasIndirectObjectHolder()) { + // In kReferenceHolder mode, go through the reference's holder so + // lazy parsing can pull the referenced object off disk on demand. referenced_object = ref_object->GetDirect(); } else { - // Calling GetDirect() does not work for references from trailers. + // Inlined trailer references have no holder, so GetDirect() cannot + // resolve them. referenced_object = document_->GetIndirectObject(referenced_object_number); } @@ -171,6 +178,7 @@ class ObjectTreeTraverser { } UnownedPtr const document_; + const ObjectTreeReferenceResolveMode resolve_mode_; // Queue of objects to traverse. // - Pointers in the queue are non-null. @@ -196,8 +204,10 @@ class ObjectTreeTraverser { } // namespace -std::set GetObjectsWithReferences(const CPDF_Document* document) { - ObjectTreeTraverser traverser(document); +std::set GetObjectsWithReferences( + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) { + ObjectTreeTraverser traverser(document, resolve_mode); traverser.Traverse(); std::set results; @@ -208,8 +218,9 @@ std::set GetObjectsWithReferences(const CPDF_Document* document) { } std::set GetObjectsWithMultipleReferences( - const CPDF_Document* document) { - ObjectTreeTraverser traverser(document); + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) { + ObjectTreeTraverser traverser(document, resolve_mode); traverser.Traverse(); std::set results; diff --git a/core/fpdfapi/parser/object_tree_traversal_util.h b/core/fpdfapi/parser/object_tree_traversal_util.h index e9db96dce9..1ef6f0624d 100644 --- a/core/fpdfapi/parser/object_tree_traversal_util.h +++ b/core/fpdfapi/parser/object_tree_traversal_util.h @@ -11,12 +11,31 @@ class CPDF_Document; +enum class ObjectTreeReferenceResolveMode { + // Resolve references through the holder stored on each CPDF_Reference. This + // preserves the historical traversal behavior for ordinary documents. + kReferenceHolder, + + // Resolve all references through the document being traversed. This is needed + // when the document can override referenced objects, such as a layer document + // whose overlay should take precedence over the frozen base graph. + kEffectiveDocument, +}; + // Traverses `document` starting with its trailer, if it has one, or starting at // the catalog, which always exists. The trailer should have a reference to the // catalog. The traversal avoids cycles. +// +// In `kReferenceHolder` mode, references are followed through their +// CPDF_IndirectObjectHolder. In `kEffectiveDocument` mode, references are +// resolved through `document` so any overlay it provides is honored. +// // Returns all the PDF objects (not CPDF_Objects) the traversal reached as a set // of object numbers. -std::set GetObjectsWithReferences(const CPDF_Document* document); +std::set GetObjectsWithReferences( + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode = + ObjectTreeReferenceResolveMode::kReferenceHolder); // Same as GetObjectsWithReferences(), but only returns the objects with // multiple references. References that would create a cycle are ignored. @@ -41,6 +60,8 @@ std::set GetObjectsWithReferences(const CPDF_Document* document); // references (B). Since (B) -> (C) -> (B) creates a cycle, the (C) -> (B) // reference does not count. std::set GetObjectsWithMultipleReferences( - const CPDF_Document* document); + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode = + ObjectTreeReferenceResolveMode::kReferenceHolder); #endif // CORE_FPDFAPI_PARSER_OBJECT_TREE_TRAVERSAL_UTIL_H_ diff --git a/core/fpdfapi/render/cpdf_docrenderdata.cpp b/core/fpdfapi/render/cpdf_docrenderdata.cpp index c35e952d74..100d9199a5 100644 --- a/core/fpdfapi/render/cpdf_docrenderdata.cpp +++ b/core/fpdfapi/render/cpdf_docrenderdata.cpp @@ -40,6 +40,9 @@ CPDF_DocRenderData* CPDF_DocRenderData::FromDocument(const CPDF_Document* doc) { CPDF_DocRenderData::CPDF_DocRenderData() = default; +CPDF_DocRenderData::CPDF_DocRenderData(CPDF_DocRenderData* fallback) + : fallback_(fallback) {} + CPDF_DocRenderData::~CPDF_DocRenderData() = default; RetainPtr CPDF_DocRenderData::GetCachedType3( @@ -50,6 +53,11 @@ RetainPtr CPDF_DocRenderData::GetCachedType3( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && font->GetFontDictObjNum() != 0 && + !GetDocument()->IsObjectPromoted(font->GetFontDictObjNum())) { + return fallback_->GetCachedType3(font); + } + auto cache = pdfium::MakeRetain(font); type3_face_map_[font].Reset(cache.Get()); return cache; @@ -63,11 +71,25 @@ RetainPtr CPDF_DocRenderData::GetTransferFunc( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && CanUseFallbackForObject(obj.Get())) { + return fallback_->GetTransferFunc(obj); + } + auto func = CreateTransferFunc(obj); transfer_func_map_[obj].Reset(func.Get()); return func; } +bool CPDF_DocRenderData::CanUseFallbackForObject( + const CPDF_Object* object) const { + if (!object) { + return false; + } + + const uint32_t objnum = object->GetObjNum(); + return objnum != 0 && !GetDocument()->IsObjectPromoted(objnum); +} + #if BUILDFLAG(IS_WIN) CFX_PSFontTracker* CPDF_DocRenderData::GetPSFontTracker() { if (!psfont_tracker_) { diff --git a/core/fpdfapi/render/cpdf_docrenderdata.h b/core/fpdfapi/render/cpdf_docrenderdata.h index f11be0caec..691a97c751 100644 --- a/core/fpdfapi/render/cpdf_docrenderdata.h +++ b/core/fpdfapi/render/cpdf_docrenderdata.h @@ -14,6 +14,7 @@ #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" #if BUILDFLAG(IS_WIN) #include @@ -34,6 +35,7 @@ class CPDF_DocRenderData : public CPDF_Document::RenderDataIface { static CPDF_DocRenderData* FromDocument(const CPDF_Document* doc); CPDF_DocRenderData(); + explicit CPDF_DocRenderData(CPDF_DocRenderData* fallback); ~CPDF_DocRenderData() override; CPDF_DocRenderData(const CPDF_DocRenderData&) = delete; @@ -53,7 +55,11 @@ class CPDF_DocRenderData : public CPDF_Document::RenderDataIface { RetainPtr CreateTransferFunc( RetainPtr pObj) const; + bool CanUseFallbackForObject(const CPDF_Object* object) const; + private: + UnownedPtr fallback_; + // TODO(tsepez): investigate this map outliving its font keys. std::map> type3_face_map_; std::map, diff --git a/core/fpdfapi/render/cpdf_renderstatus.cpp b/core/fpdfapi/render/cpdf_renderstatus.cpp index 6f8ec8e54b..8485bc5db4 100644 --- a/core/fpdfapi/render/cpdf_renderstatus.cpp +++ b/core/fpdfapi/render/cpdf_renderstatus.cpp @@ -55,6 +55,7 @@ #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" #include "core/fxcrt/data_vector.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_2d_size.h" #include "core/fxcrt/fx_safe_types.h" #include "core/fxcrt/fx_system.h" @@ -80,7 +81,9 @@ namespace { constexpr int kRenderMaxRecursionDepth = 64; -int g_CurrentRecursionDepth = 0; +// EmbedPDF: thread-confined runtime - per-thread render recursion counter so +// concurrent renders on different workers don't corrupt each other's depth. +EPDF_TLS int g_CurrentRecursionDepth = 0; CFX_FillRenderOptions GetFillOptionsForDrawPathWithBlend( const CPDF_RenderOptions::Options& options, @@ -1438,7 +1441,9 @@ RetainPtr CPDF_RenderStatus::LoadSMask( CFX_Matrix matrix = smask_matrix; matrix.Translate(-clip_rect.left, -clip_rect.top); - CPDF_Form form(context_->GetDocument(), context_->GetMutablePageResources(), + CPDF_Form form(context_->GetDocument(), + pdfium::WrapRetain(const_cast( + context_->GetPageResources())), pGroup); form.ParseContent(); diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index 94ad43f0c2..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 = [ "../../*" ] } @@ -106,6 +115,7 @@ pdfium_unittest_source_set("unittests") { "cpdf_dest_unittest.cpp", "cpdf_filespec_unittest.cpp", "cpdf_formfield_unittest.cpp", + "cpdf_generateap_unittest.cpp", "cpdf_interactiveform_unittest.cpp", "cpdf_metadata_unittest.cpp", "cpdf_nametree_unittest.cpp", diff --git a/core/fpdfdoc/cpdf_annot.cpp b/core/fpdfdoc/cpdf_annot.cpp index 1d40c9e63d..3d8926c83c 100644 --- a/core/fpdfdoc/cpdf_annot.cpp +++ b/core/fpdfdoc/cpdf_annot.cpp @@ -75,11 +75,11 @@ CPDF_Form* AnnotGetMatrix(CPDF_Page* pPage, return pForm; } -RetainPtr GetAnnotAPInternal(CPDF_Dictionary* pAnnotDict, +RetainPtr GetAnnotAPInternal(const CPDF_Dictionary* pAnnotDict, CPDF_Annot::AppearanceMode eMode, bool bFallbackToNormal) { - RetainPtr pAP = - pAnnotDict->GetMutableDictFor(pdfium::annotation::kAP); + RetainPtr pAP = + pAnnotDict->GetDictFor(pdfium::annotation::kAP); if (!pAP) { return nullptr; } @@ -94,17 +94,17 @@ RetainPtr GetAnnotAPInternal(CPDF_Dictionary* pAnnotDict, ap_entry = "N"; } - RetainPtr psub = pAP->GetMutableDirectObjectFor(ap_entry); + RetainPtr psub = pAP->GetDirectObjectFor(ap_entry); if (!psub) { return nullptr; } - RetainPtr pStream(psub->AsMutableStream()); + RetainPtr pStream(psub->AsStream()); if (pStream) { - return pStream; + return pdfium::WrapRetain(const_cast(pStream.Get())); } - CPDF_Dictionary* dict = psub->AsMutableDictionary(); + const CPDF_Dictionary* dict = psub->AsDictionary(); if (!dict) { return nullptr; } @@ -120,7 +120,8 @@ RetainPtr GetAnnotAPInternal(CPDF_Dictionary* pAnnotDict, as = (!value.IsEmpty() && dict->KeyExist(value.AsStringView())) ? value : "Off"; } - return dict->GetMutableStreamFor(as.AsStringView()); + RetainPtr stream = dict->GetStreamFor(as.AsStringView()); + return pdfium::WrapRetain(const_cast(stream.Get())); } } // namespace @@ -132,8 +133,11 @@ CPDF_Annot::CPDF_Annot(RetainPtr dict, CPDF_Document* document) annot_dict_->GetByteStringFor(pdfium::annotation::kSubtype))), is_text_markup_annotation_(IsTextMarkupAnnotation(subtype_)), has_generated_ap_( - annot_dict_->GetBooleanFor(kPDFiumKey_HasGeneratedAP, false)) { - GenerateAPIfNeeded(); + annot_dict_->GetBooleanFor(kPDFiumKey_HasGeneratedAP, false) || + (CanGenerateEphemeralAP() && ShouldGenerateAP())) { + if (!CanGenerateEphemeralAP()) { + GenerateAPIfNeeded(); + } } CPDF_Annot::~CPDF_Annot() { @@ -165,6 +169,39 @@ bool CPDF_Annot::ShouldGenerateAP() const { return !IsHidden(); } +bool CPDF_Annot::CanGenerateEphemeralAP() const { + return CPDF_GenerateAP::CanGenerateEphemeralAnnotAP(subtype_); +} + +RetainPtr CPDF_Annot::GetOrBuildEphemeralAP(AppearanceMode mode) { + if (mode != AppearanceMode::kNormal || !CanGenerateEphemeralAP()) { + return nullptr; + } + + if (ephemeral_built_) { + return ephemeral_normal_ap_; + } + + ephemeral_built_ = true; + if (!ShouldGenerateAP()) { + return nullptr; + } + + std::optional generated = + CPDF_GenerateAP::GenerateEphemeralAnnotAP(document_, annot_dict_.Get(), + subtype_); + if (!generated.has_value()) { + return nullptr; + } + + ephemeral_normal_ap_ = std::move(generated->normal_stream); + if (subtype_ == CPDF_Annot::Subtype::INK) { + ephemeral_rect_ = ephemeral_normal_ap_->GetDict()->GetRectFor("BBox"); + } + has_generated_ap_ = true; + return ephemeral_normal_ap_; +} + bool CPDF_Annot::ShouldDrawAnnotation() const { if (IsHidden()) { return false; @@ -174,6 +211,12 @@ bool CPDF_Annot::ShouldDrawAnnotation() const { void CPDF_Annot::ClearCachedAP() { ap_map_.clear(); + ephemeral_normal_ap_.Reset(); + ephemeral_rect_.reset(); + ephemeral_built_ = false; + has_generated_ap_ = + annot_dict_->GetBooleanFor(kPDFiumKey_HasGeneratedAP, false) || + (CanGenerateEphemeralAP() && ShouldGenerateAP()); } CPDF_Annot::Subtype CPDF_Annot::GetSubtype() const { @@ -181,6 +224,10 @@ CPDF_Annot::Subtype CPDF_Annot::GetSubtype() const { } CFX_FloatRect CPDF_Annot::RectForDrawing() const { + if (ephemeral_rect_.has_value()) { + return ephemeral_rect_.value(); + } + bool bShouldUseQuadPointsCoords = is_text_markup_annotation_ && has_generated_ap_; if (bShouldUseQuadPointsCoords) { @@ -203,13 +250,13 @@ bool CPDF_Annot::IsHidden() const { return !!(GetFlags() & pdfium::annotation_flags::kHidden); } -RetainPtr GetAnnotAP(CPDF_Dictionary* pAnnotDict, +RetainPtr GetAnnotAP(const CPDF_Dictionary* pAnnotDict, CPDF_Annot::AppearanceMode eMode) { DCHECK(pAnnotDict); return GetAnnotAPInternal(pAnnotDict, eMode, true); } -RetainPtr GetAnnotAPNoFallback(CPDF_Dictionary* pAnnotDict, +RetainPtr GetAnnotAPNoFallback(const CPDF_Dictionary* pAnnotDict, CPDF_Annot::AppearanceMode eMode) { DCHECK(pAnnotDict); return GetAnnotAPInternal(pAnnotDict, eMode, false); @@ -217,6 +264,9 @@ RetainPtr GetAnnotAPNoFallback(CPDF_Dictionary* pAnnotDict, CPDF_Form* CPDF_Annot::GetAPForm(CPDF_Page* pPage, AppearanceMode mode) { RetainPtr pStream = GetAnnotAP(annot_dict_.Get(), mode); + if (!pStream) { + pStream = GetOrBuildEphemeralAP(mode); + } if (!pStream) { return nullptr; } @@ -227,7 +277,10 @@ CPDF_Form* CPDF_Annot::GetAPForm(CPDF_Page* pPage, AppearanceMode mode) { } auto pNewForm = std::make_unique( - document_, pPage->GetMutableResources(), pStream); + document_, + pdfium::WrapRetain( + const_cast(pPage->GetResources().Get())), + pStream); pNewForm->ParseContent(); CPDF_Form* pResult = pNewForm.get(); @@ -574,8 +627,9 @@ CPDF_Annot::Icon CPDF_Annot::StringToIcon(const ByteString& name) { ByteString prefix = name.First(2); if (prefix == "SB" || prefix == "SH") { Icon result = StringToIcon(name.Substr(2)); - if (result != Icon::kUnknown && result != Icon::kStamp_Custom) + if (result != Icon::kUnknown && result != Icon::kStamp_Custom) { return result; + } } } return Icon::kStamp_Custom; @@ -700,17 +754,17 @@ ByteString CPDF_Annot::LineEndingToString(CPDF_Annot::LineEnding le) { return "Circle"; case LineEnding::kDiamond: return "Diamond"; - case LineEnding::kOpenArrow: + case LineEnding::kOpenArrow: return "OpenArrow"; - case LineEnding::kClosedArrow: + case LineEnding::kClosedArrow: return "ClosedArrow"; - case LineEnding::kButt: + case LineEnding::kButt: return "Butt"; - case LineEnding::kROpenArrow: + case LineEnding::kROpenArrow: return "ROpenArrow"; - case LineEnding::kRClosedArrow: + case LineEnding::kRClosedArrow: return "RClosedArrow"; - case LineEnding::kSlash: + case LineEnding::kSlash: return "Slash"; case LineEnding::kUnknown: break; @@ -752,7 +806,8 @@ CPDF_Annot::LineEnding CPDF_Annot::StringToLineEnding(const ByteString& n) { return LineEnding::kUnknown; } -CPDF_Annot::StandardFont CPDF_Annot::StringToStandardFont(const ByteString& name) { +CPDF_Annot::StandardFont CPDF_Annot::StringToStandardFont( + const ByteString& name) { // Full canonical names (PDF Reference, Table 5.17) if (name == "Courier" || name == "Cour") { return StandardFont::kCourier; @@ -805,7 +860,7 @@ ByteString CPDF_Annot::StandardFontToString(CPDF_Annot::StandardFont font) { return "Courier"; case StandardFont::kCourier_Bold: return "Courier-Bold"; - case StandardFont::kCourier_BoldOblique: + case StandardFont::kCourier_BoldOblique: return "Courier-BoldOblique"; case StandardFont::kCourier_Oblique: return "Courier-Oblique"; @@ -838,11 +893,21 @@ ByteString CPDF_Annot::StandardFontToString(CPDF_Annot::StandardFont font) { // static CPDF_Annot::BorderStyle CPDF_Annot::StringToBorderStyle( const ByteString& sStyle) { - if (sStyle == "S") return CPDF_Annot::BorderStyle::kSolid; - if (sStyle == "D") return CPDF_Annot::BorderStyle::kDashed; - if (sStyle == "B") return CPDF_Annot::BorderStyle::kBeveled; - if (sStyle == "I") return CPDF_Annot::BorderStyle::kInset; - if (sStyle == "U") return CPDF_Annot::BorderStyle::kUnderline; + if (sStyle == "S") { + return CPDF_Annot::BorderStyle::kSolid; + } + if (sStyle == "D") { + return CPDF_Annot::BorderStyle::kDashed; + } + if (sStyle == "B") { + return CPDF_Annot::BorderStyle::kBeveled; + } + if (sStyle == "I") { + return CPDF_Annot::BorderStyle::kInset; + } + if (sStyle == "U") { + return CPDF_Annot::BorderStyle::kUnderline; + } return CPDF_Annot::BorderStyle::kUnknown; } @@ -877,12 +942,14 @@ bool CPDF_Annot::DrawAppearance(CPDF_Page* pPage, return false; } - // It might happen that by the time this annotation instance was created, - // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided - // to not "generate" its AP. - // If for a reason the object is no longer hidden, but still does not have - // its "AP" generated, generate it now. - GenerateAPIfNeeded(); + if (!CanGenerateEphemeralAP()) { + // It might happen that by the time this annotation instance was created, + // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided + // to not "generate" its AP. + // If for a reason the object is no longer hidden, but still does not have + // its "AP" generated, generate it now. + GenerateAPIfNeeded(); + } CFX_Matrix matrix; CPDF_Form* pForm = AnnotGetMatrix(pPage, this, mode, mtUser2Device, &matrix); @@ -891,7 +958,8 @@ bool CPDF_Annot::DrawAppearance(CPDF_Page* pPage, } CPDF_RenderContext context(pPage->GetDocument(), - pPage->GetMutablePageResources(), + pdfium::WrapRetain(const_cast( + pPage->GetPageResources().Get())), pPage->GetPageImageCache()); context.AppendLayer(pForm, matrix); context.Render(pDevice, nullptr, nullptr, nullptr); @@ -906,12 +974,14 @@ bool CPDF_Annot::DrawInContext(CPDF_Page* pPage, return false; } - // It might happen that by the time this annotation instance was created, - // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided - // to not "generate" its AP. - // If for a reason the object is no longer hidden, but still does not have - // its "AP" generated, generate it now. - GenerateAPIfNeeded(); + if (!CanGenerateEphemeralAP()) { + // It might happen that by the time this annotation instance was created, + // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided + // to not "generate" its AP. + // If for a reason the object is no longer hidden, but still does not have + // its "AP" generated, generate it now. + GenerateAPIfNeeded(); + } CFX_Matrix matrix; CPDF_Form* pForm = AnnotGetMatrix(pPage, this, mode, mtUser2Device, &matrix); diff --git a/core/fpdfdoc/cpdf_annot.h b/core/fpdfdoc/cpdf_annot.h index 1071f28f9b..9285f1b6a2 100644 --- a/core/fpdfdoc/cpdf_annot.h +++ b/core/fpdfdoc/cpdf_annot.h @@ -105,17 +105,9 @@ class CPDF_Annot { kZapfDingbats }; - enum class TextAlignment { - kLeft = 0, - kCenter = 1, - kRight = 2 - }; + enum class TextAlignment { kLeft = 0, kCenter = 1, kRight = 2 }; - enum class VerticalAlignment { - kTop = 0, - kMiddle = 1, - kBottom = 2 - }; + enum class VerticalAlignment { kTop = 0, kMiddle = 1, kBottom = 2 }; // -------------------------------------------------------------------- // Built‑in icon (/Name) enumeration – must stay in sync with the public @@ -230,6 +222,8 @@ class CPDF_Annot { private: void GenerateAPIfNeeded(); + RetainPtr GetOrBuildEphemeralAP(AppearanceMode mode); + bool CanGenerateEphemeralAP() const; bool ShouldGenerateAP() const; bool ShouldDrawAnnotation() const; @@ -238,24 +232,27 @@ class CPDF_Annot { RetainPtr const annot_dict_; UnownedPtr const document_; std::map, std::unique_ptr> ap_map_; + RetainPtr ephemeral_normal_ap_; + std::optional ephemeral_rect_; // If non-null, then this is not a popup annotation. UnownedPtr popup_annot_; const Subtype subtype_; const bool is_text_markup_annotation_; // |open_state_| is only set for popup annotations. bool open_state_ = false; + bool ephemeral_built_ = false; bool has_generated_ap_; }; // Get the AP in an annotation dict for a given appearance mode. // If |eMode| is not Normal and there is not AP for that mode, falls back to // the Normal AP. -RetainPtr GetAnnotAP(CPDF_Dictionary* pAnnotDict, +RetainPtr GetAnnotAP(const CPDF_Dictionary* pAnnotDict, CPDF_Annot::AppearanceMode eMode); // Get the AP in an annotation dict for a given appearance mode. // No fallbacks to Normal like in GetAnnotAP. -RetainPtr GetAnnotAPNoFallback(CPDF_Dictionary* pAnnotDict, +RetainPtr GetAnnotAPNoFallback(const CPDF_Dictionary* pAnnotDict, CPDF_Annot::AppearanceMode eMode); #endif // CORE_FPDFDOC_CPDF_ANNOT_H_ diff --git a/core/fpdfdoc/cpdf_annot_unittest.cpp b/core/fpdfdoc/cpdf_annot_unittest.cpp index 5e2f582b35..9c554cad64 100644 --- a/core/fpdfdoc/cpdf_annot_unittest.cpp +++ b/core/fpdfdoc/cpdf_annot_unittest.cpp @@ -6,9 +6,14 @@ #include +#include "constants/annotation_common.h" +#include "core/fpdfapi/page/cpdf_page.h" +#include "core/fpdfapi/page/test_with_page_module.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_test_document.h" #include "testing/gtest/include/gtest/gtest.h" namespace { @@ -24,6 +29,8 @@ RetainPtr CreateQuadPointArrayFromVector( } // namespace +class CPDFAnnotWithPageModuleTest : public TestWithPageModule {}; + TEST(CPDFAnnotTest, RectFromQuadPointsArray) { RetainPtr array = CreateQuadPointArrayFromVector( {0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}); @@ -136,3 +143,53 @@ TEST(CPDFAnnotTest, QuadPointCount) { } EXPECT_EQ(8u, CPDF_Annot::QuadPointCount(array.Get())); } + +TEST_F(CPDFAnnotWithPageModuleTest, + ConstructorDoesNotPersistEphemeralHighlightAP) { + CPDF_TestDocument doc; + auto annot_dict = pdfium::MakeRetain(); + annot_dict->SetNewFor(pdfium::annotation::kSubtype, "Highlight"); + annot_dict->SetRectFor(pdfium::annotation::kRect, + CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", CreateQuadPointArrayFromVector( + {10, 20, 50, 20, 10, 10, 50, 10})); + + const uint32_t last_obj_num = doc.GetLastObjNum(); + CPDF_Annot annot(annot_dict, &doc); + + EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); + EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); + EXPECT_FALSE(annot_dict->KeyExist("PDFIUM_HasGeneratedAP")); + EXPECT_EQ(CFX_FloatRect(10, 10, 50, 20), annot.GetRect()); +} + +TEST_F(CPDFAnnotWithPageModuleTest, + EphemeralInkAPUsesInflatedDrawingRectWithoutPersistingRect) { + CPDF_TestDocument doc; + doc.SetRoot(pdfium::MakeRetain()); + auto page = pdfium::MakeRetain( + &doc, pdfium::MakeRetain()); + + auto annot_dict = pdfium::MakeRetain(); + annot_dict->SetNewFor(pdfium::annotation::kSubtype, "Ink"); + annot_dict->SetRectFor(pdfium::annotation::kRect, + CFX_FloatRect(0, 0, 10, 10)); + auto border_style = annot_dict->SetNewFor("BS"); + border_style->SetNewFor("W", 4); + + auto ink_list = pdfium::MakeRetain(); + ink_list->Append(CreateQuadPointArrayFromVector({1, 1, 9, 9})); + annot_dict->SetFor("InkList", std::move(ink_list)); + + CPDF_Annot annot(annot_dict, &doc); + EXPECT_EQ(CFX_FloatRect(0, 0, 10, 10), + annot_dict->GetRectFor(pdfium::annotation::kRect)); + + ASSERT_TRUE(annot.GetAPForm(page.Get(), CPDF_Annot::AppearanceMode::kNormal)); + EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); + EXPECT_EQ(CFX_FloatRect(0, 0, 10, 10), + annot_dict->GetRectFor(pdfium::annotation::kRect)); + // 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_annotlist.cpp b/core/fpdfdoc/cpdf_annotlist.cpp index db8be16033..103fd0ed02 100644 --- a/core/fpdfdoc/cpdf_annotlist.cpp +++ b/core/fpdfdoc/cpdf_annotlist.cpp @@ -13,12 +13,12 @@ #include "constants/annotation_common.h" #include "constants/annotation_flags.h" #include "constants/form_fields.h" -#include "constants/form_flags.h" #include "core/fpdfapi/page/cpdf_occontext.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_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" @@ -26,9 +26,6 @@ #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/render/cpdf_renderoptions.h" #include "core/fpdfdoc/cpdf_annot.h" -#include "core/fpdfdoc/cpdf_formfield.h" -#include "core/fpdfdoc/cpdf_generateap.h" -#include "core/fpdfdoc/cpdf_interactiveform.h" #include "core/fxcrt/check.h" #include "core/fxcrt/containers/unique_ptr_adapters.h" @@ -125,90 +122,35 @@ std::unique_ptr CreatePopupAnnot(CPDF_Document* document, return pPopupAnnot; } -void GenerateAP(CPDF_Document* doc, CPDF_Dictionary* pAnnotDict) { - if (!pAnnotDict || - pAnnotDict->GetByteStringFor(pdfium::annotation::kSubtype) != "Widget") { - return; - } - - RetainPtr pFieldTypeObj = - CPDF_FormField::GetFieldAttrForDict(pAnnotDict, pdfium::form_fields::kFT); - if (!pFieldTypeObj) { - return; - } - - ByteString field_type = pFieldTypeObj->GetString(); - if (field_type == pdfium::form_fields::kTx) { - CPDF_GenerateAP::GenerateFormAP(doc, pAnnotDict, - CPDF_GenerateAP::kTextField); - return; - } - - RetainPtr pFieldFlagsObj = - CPDF_FormField::GetFieldAttrForDict(pAnnotDict, pdfium::form_fields::kFf); - uint32_t flags = pFieldFlagsObj ? pFieldFlagsObj->GetInteger() : 0; - if (field_type == pdfium::form_fields::kCh) { - auto type = (flags & pdfium::form_flags::kChoiceCombo) - ? CPDF_GenerateAP::kComboBox - : CPDF_GenerateAP::kListBox; - CPDF_GenerateAP::GenerateFormAP(doc, pAnnotDict, type); - return; - } - - if (field_type != pdfium::form_fields::kBtn) { - return; - } - if (flags & pdfium::form_flags::kButtonPushbutton) { - return; - } - if (pAnnotDict->KeyExist(pdfium::annotation::kAS)) { - return; - } - - RetainPtr pParentDict = - pAnnotDict->GetDictFor(pdfium::form_fields::kParent); - if (!pParentDict || !pParentDict->KeyExist(pdfium::annotation::kAS)) { - return; - } - - pAnnotDict->SetNewFor( - pdfium::annotation::kAS, - pParentDict->GetByteStringFor(pdfium::annotation::kAS)); -} - } // namespace CPDF_AnnotList::CPDF_AnnotList(CPDF_Page* pPage) : page_(pPage), document_(page_->GetDocument()) { - RetainPtr pAnnots = page_->GetMutableAnnotsArray(); + CPDF_DocumentViewScope document_view(document_); + + RetainPtr pAnnots = page_->GetAnnotsArray(); if (!pAnnots) { return; } - const CPDF_Dictionary* pRoot = document_->GetRoot(); - RetainPtr pAcroForm = pRoot->GetDictFor("AcroForm"); - bool bRegenerateAP = - pAcroForm && pAcroForm->GetBooleanFor("NeedAppearances", false); for (size_t i = 0; i < pAnnots->size(); ++i) { - RetainPtr dict = - ToDictionary(pAnnots->GetMutableDirectObjectAt(i)); - if (!dict) { + RetainPtr const_dict = pAnnots->GetDictAt(i); + if (!const_dict) { continue; } const ByteString subtype = - dict->GetByteStringFor(pdfium::annotation::kSubtype); + const_dict->GetByteStringFor(pdfium::annotation::kSubtype); if (subtype == "Popup") { // Skip creating Popup annotations in the PDF document since PDFium // provides its own Popup annotations. continue; } - pAnnots->ConvertToIndirectObjectAt(i, document_); + // CPDF_Annot still owns a mutable dictionary handle because explicit edit + // APIs mutate annotation dictionaries. Listing/rendering must not promote + // direct annotations to indirect objects. + RetainPtr dict = + pdfium::WrapRetain(const_cast(const_dict.Get())); annot_list_.push_back(std::make_unique(dict, document_)); - if (bRegenerateAP && subtype == "Widget" && - CPDF_InteractiveForm::IsUpdateAPEnabled() && - !dict->GetDictFor(pdfium::annotation::kAP)) { - GenerateAP(document_, dict.Get()); - } } annot_count_ = annot_list_.size(); @@ -243,6 +185,8 @@ void CPDF_AnnotList::DisplayPass(CPDF_RenderContext* context, bool bPrinting, const CFX_Matrix& mtMatrix, bool bWidgetPass) { + CPDF_DocumentViewScope document_view(document_); + CHECK(context); for (const auto& pAnnot : annot_list_) { bool bWidget = pAnnot->GetSubtype() == CPDF_Annot::Subtype::WIDGET; diff --git a/core/fpdfdoc/cpdf_annotlist_unittest.cpp b/core/fpdfdoc/cpdf_annotlist_unittest.cpp index a59f9595d1..2d7260265c 100644 --- a/core/fpdfdoc/cpdf_annotlist_unittest.cpp +++ b/core/fpdfdoc/cpdf_annotlist_unittest.cpp @@ -15,6 +15,7 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/cpdf_test_document.h" #include "core/fpdfdoc/cpdf_annot.h" @@ -52,6 +53,13 @@ class CPDFAnnotListTest : public TestWithPageModule { annotation->SetNewFor(pdfium::annotation::kContents, contents); } + RetainPtr AddDirectAnnotation(const ByteString& subtype) { + RetainPtr annotation = + page_->GetOrCreateAnnotsArray()->AppendNew(); + annotation->SetNewFor(pdfium::annotation::kSubtype, subtype); + return annotation; + } + std::unique_ptr document_; RetainPtr page_; }; @@ -124,3 +132,25 @@ TEST_F(CPDFAnnotListTest, CreatePopupAnnotFromEmptyUnicodedWithEscape) { EXPECT_EQ(1u, list.Count()); } + +TEST_F(CPDFAnnotListTest, ConstructionPreservesDirectAnnotations) { + RetainPtr annotation = AddDirectAnnotation("FreeText"); + RetainPtr annots = page_->GetAnnotsArray(); + ASSERT_TRUE(annots); + ASSERT_EQ(1u, annots->size()); + ASSERT_EQ(annotation.Get(), annots->GetObjectAt(0).Get()); + ASSERT_EQ(0u, annotation->GetObjNum()); + const uint32_t last_obj_num = document_->GetLastObjNum(); + + std::unique_ptr list; + { + CPDF_ReadOnlyGraphGuard guard; + list = std::make_unique(page_); + } + + ASSERT_EQ(1u, list->Count()); + EXPECT_EQ(annotation.Get(), list->GetAt(0)->GetAnnotDict()); + EXPECT_EQ(0u, annotation->GetObjNum()); + EXPECT_EQ(annotation.Get(), annots->GetObjectAt(0).Get()); + EXPECT_EQ(last_obj_num, document_->GetLastObjNum()); +} 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_formcontrol.cpp b/core/fpdfdoc/cpdf_formcontrol.cpp index 814a1fa97f..2a5187a527 100644 --- a/core/fpdfdoc/cpdf_formcontrol.cpp +++ b/core/fpdfdoc/cpdf_formcontrol.cpp @@ -222,16 +222,16 @@ RetainPtr CPDF_FormControl::GetDefaultControlFont() const { } const ByteString& font_name = maybe_font_name_and_size.value().name; - RetainPtr pDRDict = ToDictionary( - CPDF_FormField::GetMutableFieldAttrForDict(widget_dict_.Get(), "DR")); + RetainPtr pDRDict = ToDictionary( + CPDF_FormField::GetFieldAttrForDict(widget_dict_.Get(), "DR")); if (pDRDict) { - RetainPtr fonts = pDRDict->GetMutableDictFor("Font"); + RetainPtr fonts = pDRDict->GetDictFor("Font"); if (ValidateFontResourceDict(fonts.Get())) { - RetainPtr pElement = - fonts->GetMutableDictFor(font_name.AsStringView()); + RetainPtr pElement = + fonts->GetDictFor(font_name.AsStringView()); if (pElement) { - RetainPtr font = - form_->GetFontForElement(std::move(pElement)); + RetainPtr font = form_->GetFontForElement( + pdfium::WrapRetain(const_cast(pElement.Get()))); if (font) { return font; } @@ -243,25 +243,26 @@ RetainPtr CPDF_FormControl::GetDefaultControlFont() const { return pFormFont; } - RetainPtr pPageDict = widget_dict_->GetMutableDictFor("P"); - RetainPtr dict = ToDictionary( - CPDF_FormField::GetMutableFieldAttrForDict(pPageDict.Get(), "Resources")); + RetainPtr pPageDict = widget_dict_->GetDictFor("P"); + RetainPtr dict = ToDictionary( + CPDF_FormField::GetFieldAttrForDict(pPageDict.Get(), "Resources")); if (!dict) { return nullptr; } - RetainPtr fonts = dict->GetMutableDictFor("Font"); + RetainPtr fonts = dict->GetDictFor("Font"); if (!ValidateFontResourceDict(fonts.Get())) { return nullptr; } - RetainPtr pElement = - fonts->GetMutableDictFor(font_name.AsStringView()); + RetainPtr pElement = + fonts->GetDictFor(font_name.AsStringView()); if (!pElement) { return nullptr; } - return form_->GetFontForElement(std::move(pElement)); + return form_->GetFontForElement( + pdfium::WrapRetain(const_cast(pElement.Get()))); } int CPDF_FormControl::GetControlAlignment() const { diff --git a/core/fpdfdoc/cpdf_formfield.cpp b/core/fpdfdoc/cpdf_formfield.cpp index 00a54f39c9..220941d37b 100644 --- a/core/fpdfdoc/cpdf_formfield.cpp +++ b/core/fpdfdoc/cpdf_formfield.cpp @@ -19,6 +19,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.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/fpdfapi/parser/fpdf_parser_decode.h" @@ -62,6 +63,17 @@ bool IsComboOrListField(CPDF_FormField::Type type) { } } +WideString GetFieldNameComponent(const CPDF_Dictionary* dict) { + RetainPtr t_obj = + dict->GetObjectFor(pdfium::form_fields::kT); + if (ToReference(t_obj)) { + RetainPtr direct_obj = t_obj->GetDirect(); + return direct_obj && direct_obj->IsString() ? direct_obj->GetUnicodeText() + : WideString(); + } + return dict->GetUnicodeTextFor(pdfium::form_fields::kT); +} + } // namespace // static @@ -96,7 +108,7 @@ WideString CPDF_FormField::GetFullNameForDict( const CPDF_Dictionary* pLevel = pFieldDict; while (pLevel) { visited.insert(pLevel); - WideString short_name = pLevel->GetUnicodeTextFor(pdfium::form_fields::kT); + WideString short_name = GetFieldNameComponent(pLevel); if (!short_name.IsEmpty()) { if (full_name.IsEmpty()) { full_name = std::move(short_name); diff --git a/core/fpdfdoc/cpdf_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp index 76233297ae..a6002989d4 100644 --- a/core/fpdfdoc/cpdf_generateap.cpp +++ b/core/fpdfdoc/cpdf_generateap.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,7 @@ #include "constants/appearance.h" #include "constants/font_encodings.h" #include "constants/form_fields.h" +#include "constants/form_flags.h" #include "constants/transparency.h" #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" #include "core/fpdfapi/font/cpdf_font.h" @@ -32,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" @@ -41,9 +45,10 @@ #include "core/fpdfdoc/cpvt_variabletext.h" #include "core/fpdfdoc/cpvt_word.h" #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" -#include "core/fxcrt/fx_system.h" namespace { @@ -67,46 +72,54 @@ constexpr float kSlashLenFactor = 18.0f; // Return a unit‑length copy of `v`. If the vector has zero length, fall back // to the X axis so that later maths cannot explode. - static CFX_PointF UnitVector(const CFX_PointF& v) { +static CFX_PointF UnitVector(const CFX_PointF& v) { float len = std::hypot(v.x, v.y); - if (len <= 0.0f) + if (len <= 0.0f) { return CFX_PointF(1.0f, 0.0f); + } return CFX_PointF(v.x / len, v.y / len); } // Read one token from a /LE array and return it as a ByteString, accepting // both Name and String objects (some generators are sloppy). - static ByteString ReadLineEndingToken(const CPDF_Array* le, size_t idx) { - if (!le || idx >= le->size()) +static ByteString ReadLineEndingToken(const CPDF_Array* le, size_t idx) { + if (!le || idx >= le->size()) { return ByteString(); + } RetainPtr obj = le->GetDirectObjectAt(idx); - if (!obj) + if (!obj) { return ByteString(); + } - if (const CPDF_Name* n = obj->AsName()) + if (const CPDF_Name* n = obj->AsName()) { return n->GetString(); + } - if (const CPDF_String* s = obj->AsString()) + if (const CPDF_String* s = obj->AsString()) { return s->GetString(); + } - return ByteString(); // unsupported type + return ByteString(); // unsupported type } // Produce “q … Q” wrapper that translates + rotates local path so that: /// • the **tip** of the ending sits at `pos` /// • the **x‑axis** of the local coord points along the segment direction -template void EmitEndingWithAngle(fxcrt::ostringstream& out, - const CFX_PointF& pos, - float final_angle_rad, - const F& emitter) { +template +void EmitEndingWithAngle(fxcrt::ostringstream& out, + const CFX_PointF& pos, + float final_angle_rad, + const F& emitter) { 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"; } @@ -118,22 +131,20 @@ static void EmitArrowPath(fxcrt::ostringstream& out, ArrowStyle style, bool do_fill) { const float len = kArrowLenFactor * stroke_w; - const float a = kArrowAngle; // 30° - const float x = -len * std::cos(a); - const float y = len * std::sin(a); + const float a = kArrowAngle; // 30° + const float x = -len * std::cos(a); + const float y = len * std::sin(a); if (style == ArrowStyle::kOpen) { // OpenArrow / ROpenArrow - out << x << " " << y << " m 0 0 l " - << x << " " << -y << " l S\n"; + out << x << " " << y << " m 0 0 l " << x << " " << -y << " l S\n"; return; } // ClosedArrow / RClosedArrow - out << "0 0 m " << x << " " << y << " l " - << x << " " << -y << " l " - << (do_fill ? "b\n" // fill + stroke - : "h S\n"); // just close and stroke (no fill) + out << "0 0 m " << x << " " << y << " l " << x << " " << -y << " l " + << (do_fill ? "b\n" // fill + stroke + : "h S\n"); // just close and stroke (no fill) } void EmitCirclePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { @@ -142,30 +153,23 @@ void EmitCirclePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { constexpr float kL = 0.5523f; const float d = kL * r; - out << r << " 0 m " - << r << " " << d << " " << d << " " << r << " 0 " << r << " c " - << -d << " " << r << " " << -r << " " << d << " " << -r << " 0 c " - << -r << " " << -d << " " << -d << " " << -r << " 0 " << -r << " c " - << d << " " << -r << " " << r << " " << -d << " " << r << " 0 c " + out << r << " 0 m " << r << " " << d << " " << d << " " << r << " 0 " << r + << " c " << -d << " " << r << " " << -r << " " << d << " " << -r + << " 0 c " << -r << " " << -d << " " << -d << " " << -r << " 0 " << -r + << " c " << d << " " << -r << " " << r << " " << -d << " " << r << " 0 c " << (filled ? "B\n" : "S\n"); } void EmitSquarePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { const float h = (stroke_w * 6.0f) / 2.0f; - out << -h << " " << -h << " m " - << h << " " << -h << " l " - << h << " " << h << " l " - << -h << " " << h << " l h " - << (filled ? "B\n" : "S\n"); + out << -h << " " << -h << " m " << h << " " << -h << " l " << h << " " << h + << " l " << -h << " " << h << " l h " << (filled ? "B\n" : "S\n"); } void EmitDiamondPath(fxcrt::ostringstream& out, float stroke_w, bool filled) { const float h = (stroke_w * 6.0f) / 2.0f; - out << "0 " << -h << " m " - << h << " 0 l " - << "0 " << h << " l " - << -h << " 0 l h " - << (filled ? "B\n" : "S\n"); + out << "0 " << -h << " m " << h << " 0 l " + << "0 " << h << " l " << -h << " 0 l h " << (filled ? "B\n" : "S\n"); } void EmitButtOrSlashPath(fxcrt::ostringstream& out, @@ -177,22 +181,38 @@ void EmitButtOrSlashPath(fxcrt::ostringstream& out, ByteString BlendModeToPDFName(BlendMode bm) { switch (bm) { - case BlendMode::kNormal: return ByteString(pdfium::transparency::kNormal); - case BlendMode::kMultiply: return ByteString(pdfium::transparency::kMultiply); - case BlendMode::kScreen: return ByteString(pdfium::transparency::kScreen); - case BlendMode::kOverlay: return ByteString(pdfium::transparency::kOverlay); - case BlendMode::kDarken: return ByteString(pdfium::transparency::kDarken); - case BlendMode::kLighten: return ByteString(pdfium::transparency::kLighten); - case BlendMode::kColorDodge: return ByteString(pdfium::transparency::kColorDodge); - case BlendMode::kColorBurn: return ByteString(pdfium::transparency::kColorBurn); - case BlendMode::kHardLight: return ByteString(pdfium::transparency::kHardLight); - case BlendMode::kSoftLight: return ByteString(pdfium::transparency::kSoftLight); - case BlendMode::kDifference: return ByteString(pdfium::transparency::kDifference); - case BlendMode::kExclusion: return ByteString(pdfium::transparency::kExclusion); - case BlendMode::kHue: return ByteString(pdfium::transparency::kHue); - case BlendMode::kSaturation: return ByteString(pdfium::transparency::kSaturation); - case BlendMode::kColor: return ByteString(pdfium::transparency::kColor); - case BlendMode::kLuminosity: return ByteString(pdfium::transparency::kLuminosity); + case BlendMode::kNormal: + return ByteString(pdfium::transparency::kNormal); + case BlendMode::kMultiply: + return ByteString(pdfium::transparency::kMultiply); + case BlendMode::kScreen: + return ByteString(pdfium::transparency::kScreen); + case BlendMode::kOverlay: + return ByteString(pdfium::transparency::kOverlay); + case BlendMode::kDarken: + return ByteString(pdfium::transparency::kDarken); + case BlendMode::kLighten: + return ByteString(pdfium::transparency::kLighten); + case BlendMode::kColorDodge: + return ByteString(pdfium::transparency::kColorDodge); + case BlendMode::kColorBurn: + return ByteString(pdfium::transparency::kColorBurn); + case BlendMode::kHardLight: + return ByteString(pdfium::transparency::kHardLight); + case BlendMode::kSoftLight: + return ByteString(pdfium::transparency::kSoftLight); + case BlendMode::kDifference: + return ByteString(pdfium::transparency::kDifference); + case BlendMode::kExclusion: + return ByteString(pdfium::transparency::kExclusion); + case BlendMode::kHue: + return ByteString(pdfium::transparency::kHue); + case BlendMode::kSaturation: + return ByteString(pdfium::transparency::kSaturation); + case BlendMode::kColor: + return ByteString(pdfium::transparency::kColor); + case BlendMode::kLuminosity: + return ByteString(pdfium::transparency::kLuminosity); } return ByteString(pdfium::transparency::kNormal); } @@ -206,6 +226,26 @@ static BlendMode DefaultBlendModeFor(CPDF_Annot::Subtype subtype) { } } +bool SupportsEphemeralAnnotAP(CPDF_Annot::Subtype subtype) { + switch (subtype) { + case CPDF_Annot::Subtype::CIRCLE: + case CPDF_Annot::Subtype::FREETEXT: + case CPDF_Annot::Subtype::HIGHLIGHT: + case CPDF_Annot::Subtype::INK: + case CPDF_Annot::Subtype::LINE: + case CPDF_Annot::Subtype::POLYGON: + case CPDF_Annot::Subtype::POLYLINE: + case CPDF_Annot::Subtype::SQUARE: + case CPDF_Annot::Subtype::SQUIGGLY: + case CPDF_Annot::Subtype::STRIKEOUT: + case CPDF_Annot::Subtype::UNDERLINE: + case CPDF_Annot::Subtype::WIDGET: + return true; + default: + return false; + } +} + ByteString GetPDFWordString(IPVT_FontMap* font_map, int32_t font_index, uint16_t word, @@ -229,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; } @@ -403,12 +446,40 @@ AnnotationDimensionsAndColor GetAnnotationDimensionsAndColor( }; } +constexpr char kEmbedMetadataKey[] = "EMBD_Metadata"; +constexpr char kEmbedMetadataRotationKey[] = "Rotation"; +constexpr char kEmbedMetadataUnrotatedRectKey[] = "UnrotatedRect"; +constexpr char kEmbedMetadataVerticalAlignmentKey[] = "VerticalAlignment"; + +RetainPtr GetEmbedMetadataDict( + const CPDF_Dictionary* annot_dict) { + return annot_dict ? annot_dict->GetDictFor(kEmbedMetadataKey) : nullptr; +} + +float GetEmbedMetadataFloatFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetFloatFor(key) : 0.0f; +} + +CFX_FloatRect GetEmbedMetadataRectFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetRectFor(key) : CFX_FloatRect(); +} + +int GetEmbedMetadataIntegerFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetIntegerFor(key) : 0; +} + // Rotation info for shape annotations (Square, Circle) using EmbedPDF's -// custom /EPDFRotate and /EPDFUnrotatedRect entries. +// /EMBD_Metadata rotation fields. struct ShapeRotationInfo { - CFX_FloatRect bbox; // BBox for the AP stream (unrotated rect in page coords) - CFX_Matrix matrix; // Transforms from local BBox space to page/AABB space - bool is_rotated; // Whether rotation was applied + CFX_FloatRect bbox; // BBox for the AP stream (unrotated rect in page coords) + CFX_Matrix matrix; // Transforms from local BBox space to page/AABB space + bool is_rotated; // Whether rotation was applied }; ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict) { @@ -417,14 +488,16 @@ ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict) { info.matrix = CFX_Matrix(); info.bbox = annot_dict->GetRectFor(pdfium::annotation::kRect); - float rotate_deg = annot_dict->GetFloatFor("EPDFRotate"); + float rotate_deg = + GetEmbedMetadataFloatFor(annot_dict, kEmbedMetadataRotationKey); // Normalize to [0, 360) rotate_deg = fmod(fmod(rotate_deg, 360.0f) + 360.0f, 360.0f); if (rotate_deg < 0.01f || rotate_deg > 359.99f) { return info; // No rotation } - CFX_FloatRect unrotated = annot_dict->GetRectFor("EPDFUnrotatedRect"); + CFX_FloatRect unrotated = + GetEmbedMetadataRectFor(annot_dict, kEmbedMetadataUnrotatedRectKey); if (unrotated.IsEmpty()) { return info; // No unrotated rect stored -> no rotation in AP } @@ -433,17 +506,31 @@ 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; // Matrix: rotate around center of unrotated rect // M = T(cx, cy) * R(theta) * T(-cx, -cy) - info.matrix = CFX_Matrix( - cos_t, sin_t, -sin_t, cos_t, - cx * (1.0f - cos_t) + cy * sin_t, - cy * (1.0f - cos_t) - cx * sin_t); + info.matrix = + CFX_Matrix(cos_t, sin_t, -sin_t, cos_t, cx * (1.0f - cos_t) + cy * sin_t, + cy * (1.0f - cos_t) - cx * sin_t); return info; } @@ -728,8 +815,10 @@ ByteString GetColorStringWithDefault(const CPDF_Array* color_array, float GetBorderWidth(const CPDF_Dictionary* dict) { RetainPtr border_style_dict = dict->GetDictFor("BS"); - if (border_style_dict && border_style_dict->KeyExist("W")) { - return border_style_dict->GetFloatFor("W"); + if (border_style_dict) { + return border_style_dict->KeyExist("W") + ? border_style_dict->GetFloatFor("W") + : 1.0f; } auto border_array = dict->GetArrayFor(pdfium::annotation::kBorder); @@ -737,13 +826,15 @@ float GetBorderWidth(const CPDF_Dictionary* dict) { return border_array->GetFloatAt(2); } - return 1; + return 1.0f; } RetainPtr GetDashArray(const CPDF_Dictionary* dict) { RetainPtr border_style_dict = dict->GetDictFor("BS"); - if (border_style_dict && border_style_dict->GetByteStringFor("S") == "D") { - return border_style_dict->GetArrayFor("D"); + if (border_style_dict) { + return border_style_dict->GetByteStringFor("S") == "D" + ? border_style_dict->GetArrayFor("D") + : nullptr; } RetainPtr border_array = @@ -757,11 +848,11 @@ RetainPtr GetDashArray(const CPDF_Dictionary* dict) { inline CPDF_Annot::VerticalAlignment GetVerticalAlign( const CPDF_Dictionary* annot_dict) { - const int v = - annot_dict ? annot_dict->GetIntegerFor("EPDF:VerticalAlignment") : 0; + const int v = GetEmbedMetadataIntegerFor(annot_dict, + kEmbedMetadataVerticalAlignmentKey); if (v < static_cast(CPDF_Annot::VerticalAlignment::kTop) || v > static_cast(CPDF_Annot::VerticalAlignment::kBottom)) { - return CPDF_Annot::VerticalAlignment::kTop; // fallback + return CPDF_Annot::VerticalAlignment::kTop; // fallback } return static_cast(v); } @@ -828,6 +919,26 @@ RetainPtr GenerateFallbackFontDict(CPDF_Document* doc) { return font_dict; } +RetainPtr GenerateDirectFallbackFontDict() { + auto font_dict = pdfium::MakeRetain(); + font_dict->SetNewFor("Type", "Font"); + font_dict->SetNewFor("Subtype", "Type1"); + font_dict->SetNewFor("BaseFont", CFX_Font::kDefaultAnsiFontName); + font_dict->SetNewFor("Encoding", + pdfium::font_encodings::kWinAnsiEncoding); + return font_dict; +} + +RetainPtr GenerateEphemeralDefaultAcroFormDict() { + auto acroform_dict = pdfium::MakeRetain(); + acroform_dict->SetNewFor("DA", "/Helv 12 Tf 0 g"); + + auto dr_dict = acroform_dict->SetNewFor("DR"); + auto font_dict = dr_dict->SetNewFor("Font"); + font_dict->SetFor("Helv", GenerateDirectFallbackFontDict()); + return acroform_dict; +} + RetainPtr GetFontFromDrFontDictOrGenerateFallback( CPDF_Document* doc, CPDF_Dictionary* dr_font_dict, @@ -844,6 +955,21 @@ RetainPtr GetFontFromDrFontDictOrGenerateFallback( return new_font_dict; } +RetainPtr GetFontFromDrFontDictOrDirectFallback( + const CPDF_Dictionary* dr_font_dict, + const ByteString& font_name) { + RetainPtr font_dict = + dr_font_dict->GetDictFor(font_name.AsStringView()); + if (font_dict) { + // The font loader still takes a mutable dictionary handle. Ephemeral AP + // generation treats this as a read-only boundary and never writes through + // it. + return pdfium::WrapRetain(const_cast(font_dict.Get())); + } + + return GenerateDirectFallbackFontDict(); +} + RetainPtr GenerateResourceFontDict( CPDF_Document* doc, const ByteString& font_name, @@ -854,6 +980,20 @@ RetainPtr GenerateResourceFontDict( return resource_font_dict; } +RetainPtr GenerateResourceFontDict( + CPDF_Document* doc, + const ByteString& font_name, + const CPDF_Dictionary* font_dict) { + auto resource_font_dict = doc->New(); + const uint32_t font_obj_num = font_dict->GetObjNum(); + if (font_obj_num != 0) { + resource_font_dict->SetNewFor(font_name, doc, font_obj_num); + } else { + resource_font_dict->SetFor(font_name, font_dict->Clone()); + } + return resource_font_dict; +} + // Returns a PDF-name-safe alias for |base_font_name|, guaranteed unique inside // /AcroForm/DR/Font. Re-uses any existing alias that already maps to the same // BaseFont, otherwise creates a lean “standard-14” stub (or a fallback font @@ -870,8 +1010,9 @@ RetainPtr GenerateResourceFontDict( ByteString EnsureFontInAcroFormDR(CPDF_Document* doc, CPDF_Dictionary* acroform_dict, const ByteString& base_font_name) { - if (!doc || !acroform_dict || base_font_name.IsEmpty()) + if (!doc || !acroform_dict || base_font_name.IsEmpty()) { return ByteString(); + } // /DR /Font RetainPtr dr_dict = acroform_dict->GetOrCreateDictFor("DR"); @@ -883,14 +1024,16 @@ ByteString EnsureFontInAcroFormDR(CPDF_Document* doc, for (const auto& kv : locker) { const CPDF_Reference* ref = kv.second ? kv.second->AsReference() : nullptr; - if (!ref) + if (!ref) { continue; + } RetainPtr obj = doc->GetOrParseIndirectObject(ref->GetRefObjNum()); const CPDF_Dictionary* dict = obj ? obj->AsDictionary() : nullptr; - if (dict && dict->GetNameFor("BaseFont") == base_font_name) + if (dict && dict->GetNameFor("BaseFont") == base_font_name) { return kv.first; + } } } @@ -927,8 +1070,9 @@ struct CloudyBorderInfo { CloudyBorderInfo GetCloudyBorderInfo(const CPDF_Dictionary* annot_dict) { CloudyBorderInfo info; RetainPtr be = annot_dict->GetDictFor("BE"); - if (!be || be->GetNameFor("S") != "C") + if (!be || be->GetNameFor("S") != "C") { return info; + } info.is_cloudy = true; info.intensity = be->KeyExist("I") ? be->GetFloatFor("I") : 1.0f; return info; @@ -936,23 +1080,25 @@ CloudyBorderInfo GetCloudyBorderInfo(const CPDF_Dictionary* annot_dict) { CFX_FloatRect GetRectDifferences(const CPDF_Dictionary* annot_dict) { RetainPtr rd = annot_dict->GetArrayFor("RD"); - if (!rd || rd->size() < 4) + if (!rd || rd->size() < 4) { return CFX_FloatRect(); - return CFX_FloatRect(rd->GetFloatAt(0), rd->GetFloatAt(1), - rd->GetFloatAt(2), rd->GetFloatAt(3)); + } + return CFX_FloatRect(rd->GetFloatAt(0), rd->GetFloatAt(1), rd->GetFloatAt(2), + rd->GetFloatAt(3)); } CPDF_Annot::LineEnding ReadCalloutLineEnding( const CPDF_Dictionary* annot_dict) { // Per spec (Table 174), FreeText /LE is a single Name. ByteString name = annot_dict->GetNameFor("LE"); - if (!name.IsEmpty()) + if (!name.IsEmpty()) { return CPDF_Annot::StringToLineEnding(name); + } // Tolerance fallback: some writers store LE as an array. if (RetainPtr le = annot_dict->GetArrayFor("LE"); le) { - if (le->size() >= 1) - return CPDF_Annot::StringToLineEnding( - ReadLineEndingToken(le.Get(), 0)); + if (le->size() >= 1) { + return CPDF_Annot::StringToLineEnding(ReadLineEndingToken(le.Get(), 0)); + } } return CPDF_Annot::LineEnding::kNone; } @@ -969,7 +1115,8 @@ ByteString GenerateTextSymbolAP(const CFX_FloatRect& rect, fill_color = fpdfdoc::CFXColorFromArray(*color_array); } - // Compute luminance-based contrast stroke (matches JS getContrastStrokeColor). + // Compute luminance-based contrast stroke (matches JS + // getContrastStrokeColor). float luminance = 0.299f * fill_color.fColor1 + 0.587f * fill_color.fColor2 + 0.114f * fill_color.fColor3; CFX_Color stroke_color = luminance < 0.45f @@ -1022,6 +1169,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) { @@ -1055,72 +1349,129 @@ RetainPtr GenerateResourcesDict( return resources_dict; } -void GenerateAndSetAPDict(CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - fxcrt::ostringstream* app_stream, - RetainPtr resource_dict, - bool is_text_markup_annotation) { +struct APGenerationTarget { + CPDF_Document* const doc; + CPDF_Dictionary* const persistent_annot_dict; + RetainPtr normal_stream; + + bool IsPersistent() const { return !!persistent_annot_dict; } +}; + +RetainPtr BuildAPStreamDict( + const CPDF_Dictionary* annot_dict, + RetainPtr resource_dict, + bool is_text_markup_annotation, + const CFX_Matrix& matrix, + const CFX_FloatRect& bbox_override) { 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->SetMatrixFor("Matrix", matrix); - CFX_FloatRect rect = is_text_markup_annotation + CFX_FloatRect rect = !bbox_override.IsEmpty() ? bbox_override + : is_text_markup_annotation ? CPDF_Annot::BoundingRectFromQuadPoints(annot_dict) : annot_dict->GetRectFor(pdfium::annotation::kRect); stream_dict->SetRectFor("BBox", rect); stream_dict->SetFor("Resources", std::move(resource_dict)); + return stream_dict; +} + +bool GenerateAPDict(APGenerationTarget* target, + const CPDF_Dictionary* annot_dict, + fxcrt::ostringstream* app_stream, + RetainPtr resource_dict, + bool is_text_markup_annotation, + const CFX_Matrix& matrix, + const CFX_FloatRect& bbox_override) { + RetainPtr stream_dict = + BuildAPStreamDict(annot_dict, std::move(resource_dict), + is_text_markup_annotation, matrix, bbox_override); + + target->normal_stream = + target->IsPersistent() + ? target->doc->NewIndirect(std::move(stream_dict)) + : pdfium::MakeRetain(std::move(stream_dict)); + target->normal_stream->SetDataFromStringstream(app_stream); - auto normal_stream = doc->NewIndirect(std::move(stream_dict)); - normal_stream->SetDataFromStringstream(app_stream); + if (!target->IsPersistent()) { + return true; + } RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); + target->persistent_annot_dict->GetOrCreateDictFor( + pdfium::annotation::kAP); + ap_dict->SetNewFor("N", target->doc, + target->normal_stream->GetObjNum()); + return true; +} + +bool GenerateAndSetAPDict(APGenerationTarget* target, + const CPDF_Dictionary* annot_dict, + fxcrt::ostringstream* app_stream, + RetainPtr resource_dict, + bool is_text_markup_annotation) { + return GenerateAPDict(target, annot_dict, app_stream, + std::move(resource_dict), is_text_markup_annotation, + CFX_Matrix(), CFX_FloatRect()); } // Overload that accepts explicit Matrix and BBox, used by rotation-aware // shape annotation generators (Square, Circle). -void GenerateAndSetAPDictWithTransform( - CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - fxcrt::ostringstream* app_stream, - RetainPtr resource_dict, - const CFX_Matrix& matrix, - const CFX_FloatRect& bbox) { - auto stream_dict = pdfium::MakeRetain(); - stream_dict->SetNewFor("FormType", 1); - stream_dict->SetNewFor("Type", "XObject"); - stream_dict->SetNewFor("Subtype", "Form"); - stream_dict->SetMatrixFor("Matrix", matrix); - stream_dict->SetRectFor("BBox", bbox); - stream_dict->SetFor("Resources", std::move(resource_dict)); +bool GenerateAndSetAPDictWithTransform(APGenerationTarget* target, + const CPDF_Dictionary* annot_dict, + fxcrt::ostringstream* app_stream, + RetainPtr resource_dict, + const CFX_Matrix& matrix, + const CFX_FloatRect& bbox) { + return GenerateAPDict(target, annot_dict, app_stream, + std::move(resource_dict), + /*is_text_markup_annotation=*/false, matrix, bbox); +} - auto normal_stream = doc->NewIndirect(std::move(stream_dict)); - normal_stream->SetDataFromStringstream(app_stream); +bool GenerateAndSetAPDictWithBBox(APGenerationTarget* target, + const CPDF_Dictionary* annot_dict, + fxcrt::ostringstream* app_stream, + RetainPtr resource_dict, + const CFX_FloatRect& bbox) { + return GenerateAPDict( + target, annot_dict, app_stream, std::move(resource_dict), + /*is_text_markup_annotation=*/false, CFX_Matrix(), bbox); +} - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); +bool GenerateAndSetAPDict(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + fxcrt::ostringstream* app_stream, + RetainPtr resource_dict, + bool is_text_markup_annotation) { + APGenerationTarget target{doc, annot_dict}; + return GenerateAndSetAPDict(&target, annot_dict, app_stream, + std::move(resource_dict), + is_text_markup_annotation); } // This helper encapsulates all logic for drawing the start and end caps. void GenerateLineEndings(fxcrt::ostringstream& ap, const std::vector& points, const CPDF_Dictionary* annot_dict) { - if (points.size() < 2) + if (points.size() < 2) { return; + } // Get ending styles from the /LE array CPDF_Annot::LineEnding start_ending = CPDF_Annot::LineEnding::kNone; CPDF_Annot::LineEnding end_ending = CPDF_Annot::LineEnding::kNone; if (RetainPtr le = annot_dict->GetArrayFor("LE"); le) { - if (le->size() >= 1) - start_ending = CPDF_Annot::StringToLineEnding(ReadLineEndingToken(le.Get(), 0)); - if (le->size() >= 2) - end_ending = CPDF_Annot::StringToLineEnding(ReadLineEndingToken(le.Get(), 1)); + if (le->size() >= 1) { + start_ending = + CPDF_Annot::StringToLineEnding(ReadLineEndingToken(le.Get(), 0)); + } + if (le->size() >= 2) { + end_ending = + CPDF_Annot::StringToLineEnding(ReadLineEndingToken(le.Get(), 1)); + } } if (start_ending == CPDF_Annot::LineEnding::kNone && @@ -1135,11 +1486,11 @@ void GenerateLineEndings(fxcrt::ostringstream& ap, // Lambda to emit a single ending with the correct transformation auto emit_one = [&](const CPDF_Annot::LineEnding ending, - const CFX_PointF& tip, - const CFX_PointF& unit_dir) { + const CFX_PointF& tip, const CFX_PointF& unit_dir) { if (ending == CPDF_Annot::LineEnding::kNone || - ending == CPDF_Annot::LineEnding::kUnknown) + ending == CPDF_Annot::LineEnding::kUnknown) { return; + } const float line_angle = atan2(unit_dir.y, unit_dir.x); float final_angle = line_angle; @@ -1208,10 +1559,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; @@ -1258,12 +1612,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; @@ -1388,7 +1745,9 @@ ByteString GenerateListBoxAP(const CPDF_Dictionary* annot_dict, return ByteString(body_stream); } -bool GenerateCircleAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateCircleAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -1420,11 +1779,12 @@ bool GenerateCircleAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byt if (cloudy_info.is_cloudy) { CFX_FloatRect rd = GetRectDifferences(annot_dict); - GenerateCloudyEllipsePath(app_stream, draw_rect, rd, - cloudy_info.intensity, border_width); + GenerateCloudyEllipsePath(app_stream, draw_rect, rd, cloudy_info.intensity, + border_width); } else { - if (is_stroke_rect) + if (is_stroke_rect) { draw_rect.Deflate(border_width / 2, border_width / 2); + } const float middle_x = (draw_rect.left + draw_rect.right) / 2; const float middle_y = (draw_rect.top + draw_rect.bottom) / 2; @@ -1444,60 +1804,78 @@ bool GenerateCircleAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byt << draw_rect.left << " " << middle_y - delta_y << " " << draw_rect.left << " " << middle_y << " c\n"; app_stream << draw_rect.left << " " << middle_y + delta_y << " " - << middle_x - delta_x << " " << draw_rect.top << " " - << middle_x << " " << draw_rect.top << " c\n"; + << middle_x - delta_x << " " << draw_rect.top << " " << middle_x + << " " << draw_rect.top << " c\n"; } bool is_fill_rect = interior_color && !interior_color->IsEmpty(); app_stream << GetPaintOperatorString(is_stroke_rect, is_fill_rect) << "\n"; auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); if (rot_info.is_rotated) { - GenerateAndSetAPDictWithTransform(doc, annot_dict, &app_stream, - std::move(resources_dict), - rot_info.matrix, rot_info.bbox); + GenerateAndSetAPDictWithTransform(target, annot_dict, &app_stream, + std::move(resources_dict), + rot_info.matrix, rot_info.bbox); } else { - GenerateAndSetAPDict(doc, annot_dict, &app_stream, + GenerateAndSetAPDict(target, annot_dict, &app_stream, std::move(resources_dict), false /*IsTextMarkupAnnotation*/); } return true; } -bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { - RetainPtr root_dict = doc->GetMutableRoot(); +bool GenerateFreeTextAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { + CPDF_Document* const doc = target->doc; + const CPDF_Dictionary* root_dict = doc->GetRoot(); if (!root_dict) { return false; } - RetainPtr form_dict = - root_dict->GetMutableDictFor("AcroForm"); + RetainPtr form_dict = + root_dict->GetDictFor("AcroForm"); + RetainPtr ephemeral_form_dict; if (!form_dict) { - form_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); - CHECK(form_dict); + if (!target->IsPersistent()) { + ephemeral_form_dict = GenerateEphemeralDefaultAcroFormDict(); + form_dict = ephemeral_form_dict; + } else { + form_dict = CPDF_InteractiveForm::InitAcroFormDict(doc); + CHECK(form_dict); + } } std::optional default_appearance_info = - GetDefaultAppearanceInfo(annot_dict, form_dict); + GetDefaultAppearanceInfo(annot_dict, form_dict.Get()); if (!default_appearance_info.has_value()) { return false; } - RetainPtr dr_dict = form_dict->GetMutableDictFor("DR"); + RetainPtr dr_dict = form_dict->GetDictFor("DR"); if (!dr_dict) { return false; } - RetainPtr dr_font_dict = dr_dict->GetMutableDictFor("Font"); + RetainPtr dr_font_dict = dr_dict->GetDictFor("Font"); if (!ValidateFontResourceDict(dr_font_dict.Get())) { return false; } const ByteString& font_name = default_appearance_info.value().font_name; - RetainPtr font_dict = - GetFontFromDrFontDictOrGenerateFallback(doc, dr_font_dict, font_name); + RetainPtr font_dict; + if (target->IsPersistent()) { + font_dict = GetFontFromDrFontDictOrGenerateFallback( + doc, + pdfium::WrapRetain(const_cast(dr_font_dict.Get())), + font_name); + } else { + font_dict = + GetFontFromDrFontDictOrDirectFallback(dr_font_dict.Get(), font_name); + } auto* doc_page_data = CPDF_DocPageData::FromDocument(doc); RetainPtr default_font = doc_page_data->GetFont(font_dict); if (!default_font) { @@ -1522,19 +1900,27 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B CFX_PointF tip(cl->GetFloatAt(0), cl->GetFloatAt(1)); const bool has_knee = (cl->size() == 6); CFX_PointF knee(cl->GetFloatAt(2), cl->GetFloatAt(3)); - CFX_PointF conn = has_knee - ? CFX_PointF(cl->GetFloatAt(4), cl->GetFloatAt(5)) - : knee; + CFX_PointF conn = + has_knee ? CFX_PointF(cl->GetFloatAt(4), cl->GetFloatAt(5)) : knee; // (b) Compute text box from Rect + RD. CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); rect.Normalize(); CFX_FloatRect rd = GetRectDifferences(annot_dict); - CFX_FloatRect text_box( - rect.left + rd.left, - rect.bottom + rd.bottom, - rect.right - rd.right, - rect.top - rd.top); + 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); @@ -1548,8 +1934,9 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B color_array.Get(), CFX_Color(CFX_Color::Type::kTransparent), PaintOperation::kFill); appearance_stream << GenerateColorAP(da_color, PaintOperation::kStroke); - if (border_w > 0) + if (border_w > 0) { appearance_stream << border_w << " w\n"; + } // (e) Draw callout polyline. // Extend conn along the incoming segment direction by half the border @@ -1562,10 +1949,10 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B conn.y + line_dir.y * half_bw); appearance_stream << tip.x << " " << tip.y << " m\n"; - if (has_knee) + if (has_knee) { appearance_stream << knee.x << " " << knee.y << " l\n"; - appearance_stream << adjusted_conn.x << " " << adjusted_conn.y - << " l S\n"; + } + appearance_stream << adjusted_conn.x << " " << adjusted_conn.y << " l S\n"; // (f) Draw line ending at tip. CPDF_Annot::LineEnding le = ReadCalloutLineEnding(annot_dict); @@ -1626,6 +2013,16 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B // (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; @@ -1646,7 +2043,11 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B 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); @@ -1686,14 +2087,18 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B 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->GetObjNum()); + // 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(doc, annot_dict, &appearance_stream, + GenerateAndSetAPDict(target, annot_dict, &appearance_stream, std::move(resource_dict), /*is_text_markup_annotation=*/false); } else { @@ -1723,7 +2128,9 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B 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); @@ -1768,16 +2175,17 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B } auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict->GetObjNum()); + // 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) { - GenerateAndSetAPDictWithTransform(doc, annot_dict, &appearance_stream, - std::move(resource_dict), - rot_info.matrix, rot_info.bbox); + GenerateAndSetAPDictWithTransform(target, annot_dict, &appearance_stream, + std::move(resource_dict), + rot_info.matrix, rot_info.bbox); } else { - GenerateAndSetAPDict(doc, annot_dict, &appearance_stream, + GenerateAndSetAPDict(target, annot_dict, &appearance_stream, std::move(resource_dict), /*is_text_markup_annotation=*/false); } @@ -1785,7 +2193,9 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B return true; } -bool GenerateHighlightAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateHighlightAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -1809,35 +2219,36 @@ bool GenerateHighlightAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const } 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), + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &app_stream, + std::move(resources_dict), true /*IsTextMarkupAnnotation*/); return true; } -bool GeneratePolygonAP(CPDF_Document* doc, +bool GeneratePolygonAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { RetainPtr verts = annot_dict->GetArrayFor(pdfium::annotation::kVertices); // A polygon needs ≥ 3 points (= 6 floats). - if (!verts || verts->size() < 6) + if (!verts || verts->size() < 6) { return false; + } fxcrt::ostringstream app; app << "/" << kGSDictName << " gs "; RetainPtr interior_color = annot_dict->GetArrayFor("IC"); - app << GetColorStringWithDefault( - interior_color.Get(), - CFX_Color(CFX_Color::Type::kTransparent), - PaintOperation::kFill); + app << GetColorStringWithDefault(interior_color.Get(), + CFX_Color(CFX_Color::Type::kTransparent), + PaintOperation::kFill); app << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), - PaintOperation::kStroke); + annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), + CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), PaintOperation::kStroke); const float border_w = GetBorderWidth(annot_dict); const bool do_stroke = border_w > 0; @@ -1854,32 +2265,37 @@ bool GeneratePolygonAP(CPDF_Document* doc, if (cloudy_info.is_cloudy) { std::vector points; - for (size_t i = 0; i + 1 < verts->size(); i += 2) + for (size_t i = 0; i + 1 < verts->size(); i += 2) { points.push_back({verts->GetFloatAt(i), verts->GetFloatAt(i + 1)}); + } GenerateCloudyPolygonPath(app, points, cloudy_info.intensity, border_w); } else { app << verts->GetFloatAt(0) << " " << verts->GetFloatAt(1) << " m "; - for (size_t i = 2; i + 1 < verts->size(); i += 2) + for (size_t i = 2; i + 1 < verts->size(); i += 2) { app << verts->GetFloatAt(i) << " " << verts->GetFloatAt(i + 1) << " l "; + } app << "h "; } const bool do_fill = interior_color && !interior_color->IsEmpty(); app << GetPaintOperatorString(do_stroke, do_fill) << "\n"; - auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto res_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); - GenerateAndSetAPDict(doc, annot_dict, &app, std::move(res_dict), + auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); + auto res_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &app, std::move(res_dict), /*is_text_markup=*/false); return true; } -bool GenerateLineAP(CPDF_Document* doc, +bool GenerateLineAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { - RetainPtr L = annot_dict->GetArrayFor(pdfium::annotation::kL); - if (!L || L->size() < 4) + RetainPtr L = + annot_dict->GetArrayFor(pdfium::annotation::kL); + if (!L || L->size() < 4) { return false; + } std::vector points; points.push_back({L->GetFloatAt(0), L->GetFloatAt(1)}); @@ -1891,12 +2307,12 @@ bool GenerateLineAP(CPDF_Document* doc, // Set colors and border styles. RetainPtr interior_color = annot_dict->GetArrayFor("IC"); if (interior_color && !interior_color->IsEmpty()) { - ap << GetColorStringWithDefault(interior_color.Get(), {}, PaintOperation::kFill); + ap << GetColorStringWithDefault(interior_color.Get(), {}, + PaintOperation::kFill); } ap << GetColorStringWithDefault( annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), - PaintOperation::kStroke); + CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), PaintOperation::kStroke); const float border_w = GetBorderWidth(annot_dict); if (border_w > 0) { @@ -1904,27 +2320,29 @@ bool GenerateLineAP(CPDF_Document* doc, } // Draw the main line segment. - ap << points[0].x << " " << points[0].y << " m " - << points[1].x << " " << points[1].y << " l S\n"; + ap << points[0].x << " " << points[0].y << " m " << points[1].x << " " + << points[1].y << " l S\n"; // Draw the endings. GenerateLineEndings(ap, points, annot_dict); // Finalize and set the Appearance Stream. auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto res_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); - GenerateAndSetAPDict(doc, annot_dict, &ap, std::move(res_dict), + auto res_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &ap, std::move(res_dict), /*is_text_markup=*/false); return true; } -bool GeneratePolyLineAP(CPDF_Document* doc, +bool GeneratePolyLineAP(APGenerationTarget* target, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { RetainPtr verts = annot_dict->GetArrayFor(pdfium::annotation::kVertices); - if (!verts || verts->size() < 4) + if (!verts || verts->size() < 4) { return false; + } std::vector points; for (size_t i = 0; i + 1 < verts->size(); i += 2) { @@ -1937,12 +2355,12 @@ bool GeneratePolyLineAP(CPDF_Document* doc, // Set colors and border styles. RetainPtr interior_color = annot_dict->GetArrayFor("IC"); if (interior_color && !interior_color->IsEmpty()) { - ap << GetColorStringWithDefault(interior_color.Get(), {}, PaintOperation::kFill); + ap << GetColorStringWithDefault(interior_color.Get(), {}, + PaintOperation::kFill); } ap << GetColorStringWithDefault( annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), - PaintOperation::kStroke); + CFX_Color(CFX_Color::Type::kRGB, 0, 0, 0), PaintOperation::kStroke); const float border_w = GetBorderWidth(annot_dict); if (border_w > 0) { @@ -1961,13 +2379,16 @@ bool GeneratePolyLineAP(CPDF_Document* doc, // Finalize and set the Appearance Stream. auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto res_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); - GenerateAndSetAPDict(doc, annot_dict, &ap, std::move(res_dict), + auto res_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &ap, std::move(res_dict), /*is_text_markup=*/false); return true; } -bool GenerateInkAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateInkAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { RetainPtr ink_list = annot_dict->GetArrayFor("InkList"); if (!ink_list || ink_list->IsEmpty()) { return false; @@ -1992,11 +2413,13 @@ bool GenerateInkAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteSt 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); - 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); @@ -2005,27 +2428,64 @@ bool GenerateInkAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteSt 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(doc, std::move(gs_dict), nullptr); - GenerateAndSetAPDict(doc, annot_dict, &app_stream, std::move(resources_dict), - false /*IsTextMarkupAnnotation*/); + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + if (target->IsPersistent()) { + GenerateAndSetAPDict(target, annot_dict, &app_stream, + std::move(resources_dict), + false /*IsTextMarkupAnnotation*/); + } else { + GenerateAndSetAPDictWithBBox(target, annot_dict, &app_stream, + std::move(resources_dict), rect); + } return true; } -bool GenerateTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateTextAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2044,7 +2504,32 @@ bool GenerateTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteS return true; } -bool GenerateUnderlineAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +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) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2068,13 +2553,17 @@ bool GenerateUnderlineAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const } 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), + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &app_stream, + std::move(resources_dict), true /*IsTextMarkupAnnotation*/); return true; } -bool GeneratePopupAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GeneratePopupAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs\n"; @@ -2115,7 +2604,9 @@ bool GeneratePopupAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byte return true; } -bool GenerateSquareAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateSquareAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2150,8 +2641,9 @@ bool GenerateSquareAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byt GenerateCloudyRectanglePath(app_stream, draw_rect, rd, cloudy_info.intensity, border_width); } else { - if (is_stroke_rect) + if (is_stroke_rect) { draw_rect.Deflate(border_width / 2, border_width / 2); + } app_stream << draw_rect.left << " " << draw_rect.bottom << " " << draw_rect.Width() << " " << draw_rect.Height() << " re "; } @@ -2160,21 +2652,24 @@ bool GenerateSquareAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byt app_stream << GetPaintOperatorString(is_stroke_rect, is_fill_rect) << "\n"; auto gs_dict = GenerateExtGStateDict(*annot_dict, blend_name); - auto resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), nullptr); + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); if (rot_info.is_rotated) { - GenerateAndSetAPDictWithTransform(doc, annot_dict, &app_stream, - std::move(resources_dict), - rot_info.matrix, rot_info.bbox); + GenerateAndSetAPDictWithTransform(target, annot_dict, &app_stream, + std::move(resources_dict), + rot_info.matrix, rot_info.bbox); } else { - GenerateAndSetAPDict(doc, annot_dict, &app_stream, + GenerateAndSetAPDict(target, annot_dict, &app_stream, std::move(resources_dict), false /*IsTextMarkupAnnotation*/); } return true; } -bool GenerateSquigglyAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateSquigglyAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2218,13 +2713,17 @@ bool GenerateSquigglyAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B } 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), + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &app_stream, + std::move(resources_dict), true /*IsTextMarkupAnnotation*/); return true; } -bool GenerateStrikeOutAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateStrikeOutAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2249,8 +2748,10 @@ bool GenerateStrikeOutAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const } 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), + auto resources_dict = + GenerateResourcesDict(target->doc, std::move(gs_dict), nullptr); + GenerateAndSetAPDict(target, annot_dict, &app_stream, + std::move(resources_dict), true /*IsTextMarkupAnnotation*/); return true; } @@ -2260,8 +2761,9 @@ bool GenerateLinkAP(CPDF_Document* doc, const ByteString& blend_name) { // Get border width - default to 1 if not specified float border_width = GetBorderWidth(annot_dict); - if (border_width <= 0) + if (border_width <= 0) { return true; // No visible border, no AP needed + } CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); rect.Normalize(); @@ -2279,7 +2781,8 @@ bool GenerateLinkAP(CPDF_Document* doc, app_stream << GetDashPatternString(annot_dict); // Determine border style - BorderStyleInfo border_info = GetBorderStyleInfo(annot_dict->GetDictFor("BS")); + BorderStyleInfo border_info = + GetBorderStyleInfo(annot_dict->GetDictFor("BS")); switch (border_info.style) { case BorderStyle::kUnderline: { @@ -2309,160 +2812,287 @@ 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); - - // 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 - - // 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); -} - -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 "; +// 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; +} - // 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); - RetainPtr interior_color = annot_dict->GetArrayFor("IC"); - const bool has_fill = interior_color && !interior_color->IsEmpty(); +// 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; +} - // 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); +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; +} - // Rollover appearance: interior color for fill - rollover_stream << GetColorStringWithDefault( - interior_color.Get(), - CFX_Color(CFX_Color::Type::kTransparent), // default: no fill - PaintOperation::kFill); +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); - float border_width = GetBorderWidth(annot_dict); - if (border_width > 0) { - normal_stream << border_width << " w "; - normal_stream << GetDashPatternString(annot_dict); + 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(); - // Check for QuadPoints (text-based redaction) - RetainPtr quad_points_array = - annot_dict->GetArrayFor("QuadPoints"); + 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(); + } + } - 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(); + 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"; +} - // 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"; - } +// 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; + } - // 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"; - } + const std::vector regions = + GetRedactOverlayRegions(annot_dict); + if (regions.empty()) { + return false; + } + + 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"; } + } + + if (overlay_text.IsEmpty()) { + return true; + } + + // /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); + } + } + + 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; + + 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); + } + + 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); + + // 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); + } - // 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); + 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; -} +} -void GenerateTextFieldFormAP( - fxcrt::ostringstream& app_stream, - const CPDF_Dictionary* annot_dict, - const CFX_FloatRect& bbox, - const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { +void GenerateTextFieldFormAP(fxcrt::ostringstream& app_stream, + const CPDF_Dictionary* annot_dict, + const CFX_FloatRect& bbox, + const DefaultAppearanceInfo& da_info, + CPVT_VariableText::Provider& provider, + const WideString* value_override) { const AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); const bool has_bg = @@ -2503,8 +3133,8 @@ void GenerateTextFieldFormAP( 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); @@ -2519,19 +3149,19 @@ void GenerateTextFieldFormAP( app_stream << "Q\nEMC\n"; } -void GenerateComboBoxFormAP( - fxcrt::ostringstream& app_stream, - const CPDF_Dictionary* annot_dict, - const CFX_FloatRect& bbox, - const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { +void GenerateComboBoxFormAP(fxcrt::ostringstream& app_stream, + const CPDF_Dictionary* annot_dict, + const CFX_FloatRect& bbox, + const DefaultAppearanceInfo& da_info, + CPVT_VariableText::Provider& provider, + const WideString* value_override) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); const BorderStyleInfo border_info = GetBorderStyleInfo(annot_dict->GetDictFor("BS")); - const ByteString background = GenerateColorAP( - dims.background_color, PaintOperation::kFill); + const ByteString background = + GenerateColorAP(dims.background_color, PaintOperation::kFill); if (background.GetLength() > 0) { app_stream << "q\n" << background; WriteRect(app_stream, bbox) << " re f\nQ\n"; @@ -2547,22 +3177,21 @@ void GenerateComboBoxFormAP( 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, - const CPDF_Dictionary* annot_dict, - const CFX_FloatRect& bbox, - const DefaultAppearanceInfo& da_info, - CPVT_VariableText::Provider& provider) { +void GenerateListBoxFormAP(fxcrt::ostringstream& app_stream, + const CPDF_Dictionary* annot_dict, + const CFX_FloatRect& bbox, + const DefaultAppearanceInfo& da_info, + CPVT_VariableText::Provider& provider) { const AnnotationDimensionsAndColor dims = GetAnnotationDimensionsAndColor(annot_dict); const BorderStyleInfo border_info = GetBorderStyleInfo(annot_dict->GetDictFor("BS")); - const ByteString background = GenerateColorAP( - dims.background_color, PaintOperation::kFill); + const ByteString background = + GenerateColorAP(dims.background_color, PaintOperation::kFill); if (background.GetLength() > 0) { app_stream << "q\n" << background; WriteRect(app_stream, bbox) << " re f\nQ\n"; @@ -2628,8 +3257,8 @@ void GenerateCheckmarkPath(fxcrt::ostringstream& stream, WritePoint(stream, {pts[i][0].x + px1 * FXSYS_BEZIER, pts[i][0].y + py1 * FXSYS_BEZIER}) << " "; - WritePoint(stream, {pt_next.x + px2 * FXSYS_BEZIER, - pt_next.y + py2 * FXSYS_BEZIER}) + WritePoint(stream, + {pt_next.x + px2 * FXSYS_BEZIER, pt_next.y + py2 * FXSYS_BEZIER}) << " "; WritePoint(stream, pt_next) << " c\n"; } @@ -2678,105 +3307,245 @@ uint32_t CreateFormXObjectStream(CPDF_Document* doc, stream->SetDataFromStringstreamAndRemoveFilter(&content); return stream->GetObjNum(); } - -} // namespace -// static -void CPDF_GenerateAP::GenerateFormAP(CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - FormType type) { - RetainPtr root_dict = doc->GetMutableRoot(); +std::optional GetWidgetFormType( + const CPDF_Dictionary* annot_dict) { + RetainPtr field_type_obj = + CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kFT); + if (!field_type_obj) { + return std::nullopt; + } + + const ByteString field_type = field_type_obj->GetString(); + if (field_type == pdfium::form_fields::kTx) { + return CPDF_GenerateAP::kTextField; + } + + if (field_type != pdfium::form_fields::kCh) { + return std::nullopt; + } + + RetainPtr field_flags_obj = + CPDF_FormField::GetFieldAttrForDict(annot_dict, pdfium::form_fields::kFf); + const uint32_t flags = field_flags_obj ? field_flags_obj->GetInteger() : 0; + return (flags & pdfium::form_flags::kChoiceCombo) ? CPDF_GenerateAP::kComboBox + : CPDF_GenerateAP::kListBox; +} + +bool GenerateFormAPToTarget(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + CPDF_GenerateAP::FormType type, + const WideString* value_override) { + CPDF_Document* const doc = target->doc; + const CPDF_Dictionary* root_dict = doc->GetRoot(); if (!root_dict) { - return; + return false; } - RetainPtr form_dict = - root_dict->GetMutableDictFor("AcroForm"); + RetainPtr form_dict = + root_dict->GetDictFor("AcroForm"); + RetainPtr ephemeral_form_dict; if (!form_dict) { - return; + 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 = - GetDefaultAppearanceInfo(annot_dict, form_dict); + GetDefaultAppearanceInfo(annot_dict, form_dict.Get()); if (!default_appearance_info.has_value()) { - return; - } - - RetainPtr dr_dict = form_dict->GetMutableDictFor("DR"); - if (!dr_dict) { - return; + return false; } - RetainPtr dr_font_dict = dr_dict->GetMutableDictFor("Font"); + // 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"); + RetainPtr dr_font_dict = + dr_dict ? dr_dict->GetDictFor("Font") : nullptr; if (!ValidateFontResourceDict(dr_font_dict.Get())) { - return; + dr_font_dict.Reset(); } const ByteString& font_name = default_appearance_info.value().font_name; - RetainPtr font_dict = - GetFontFromDrFontDictOrGenerateFallback(doc, dr_font_dict, 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, mutable_dr_font_dict.Get(), font_name); + } else { + 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; + 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); - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - RetainPtr normal_stream = ap_dict->GetMutableStreamFor("N"); RetainPtr resources_dict; - if (normal_stream) { - RetainPtr stream_dict = normal_stream->GetMutableDict(); - const bool cloned = - CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict); - if (!cloned) { - if (!ValidateOrCreateFontResources(doc, stream_dict, font_dict, - font_name)) { - return; + RetainPtr normal_stream; + if (target->IsPersistent()) { + RetainPtr ap_dict = + annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); + normal_stream = ap_dict->GetMutableStreamFor("N"); + if (normal_stream) { + RetainPtr stream_dict = normal_stream->GetMutableDict(); + const bool cloned = + CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict.Get()); + if (!cloned) { + if (!ValidateOrCreateFontResources(doc, stream_dict, font_dict, + font_name)) { + return false; + } } + resources_dict = stream_dict->GetMutableDictFor("Resources"); + } else { + normal_stream = + doc->NewIndirect(pdfium::MakeRetain()); + ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); } - resources_dict = stream_dict->GetMutableDictFor("Resources"); } else { - normal_stream = - doc->NewIndirect(pdfium::MakeRetain()); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); + auto gs_dict = GenerateExtGStateDict(*annot_dict, "Normal"); + auto resource_font_dict = + GenerateResourceFontDict(doc, font_name, font_dict.Get()); + resources_dict = GenerateResourcesDict(doc, std::move(gs_dict), + 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( + target, annot_dict, &app_stream, std::move(ephemeral_resources_dict), + /*is_text_markup_annotation=*/false, dims.matrix, dims.bbox); } normal_stream->SetDataFromStringstreamAndRemoveFilter(&app_stream); RetainPtr stream_dict = normal_stream->GetMutableDict(); stream_dict->SetMatrixFor("Matrix", dims.matrix); stream_dict->SetRectFor("BBox", dims.bbox); - + const bool cloned = CloneResourcesDictIfMissingFromStream(stream_dict, dr_dict); if (cloned) { - return; + return true; } ValidateOrCreateFontResources(doc, stream_dict, font_dict, font_name); + return true; +} + +} // namespace + +// static +void CPDF_GenerateAP::GenerateFormAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + FormType type) { + APGenerationTarget target{doc, annot_dict}; + 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 +std::optional +CPDF_GenerateAP::GenerateEphemeralFormAP(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + FormType type) { + APGenerationTarget target{doc, nullptr}; + if (!GenerateFormAPToTarget(&target, const_cast(annot_dict), + type, nullptr)) { + return std::nullopt; + } + return GeneratedAP{std::move(target.normal_stream)}; } // static @@ -2823,11 +3592,11 @@ void CPDF_GenerateAP::GenerateCheckboxFormAP(CPDF_Document* doc, } } } - if (on_state.IsEmpty()) + if (on_state.IsEmpty()) { on_state = "Yes"; + } - RetainPtr n_dict = - ap_dict->SetNewFor("N"); + RetainPtr n_dict = ap_dict->SetNewFor("N"); n_dict->SetNewFor("Off", doc, off_obj_num); n_dict->SetNewFor(on_state, doc, yes_obj_num); @@ -2983,14 +3752,14 @@ void CPDF_GenerateAP::GenerateRadioButtonFormAP(CPDF_Document* doc, } if (on_state.IsEmpty()) { WideString nm = annot_dict->GetUnicodeTextFor("NM"); - if (!nm.IsEmpty()) + if (!nm.IsEmpty()) { on_state = nm.ToUTF8(); - else + } else { on_state = "Yes"; + } } - RetainPtr n_dict = - ap_dict->SetNewFor("N"); + RetainPtr n_dict = ap_dict->SetNewFor("N"); n_dict->SetNewFor("Off", doc, off_obj_num); n_dict->SetNewFor(on_state, doc, yes_obj_num); @@ -3010,7 +3779,9 @@ void CPDF_GenerateAP::GenerateEmptyAP(CPDF_Document* doc, false); } -bool GenerateCaretAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteString& blend_name) { +bool GenerateCaretAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -3046,8 +3817,8 @@ bool GenerateCaretAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byte // Left bezier: from (draw_left, draw_bottom) to (mid_x, draw_top) app_stream << draw_left << " " << draw_bottom << " m\n"; app_stream << (draw_left + width * 0.27f) << " " << draw_bottom << " " - << mid_x << " " << (draw_bottom + height * 0.44f) << " " - << mid_x << " " << draw_top << " c\n"; + << mid_x << " " << (draw_bottom + height * 0.44f) << " " << mid_x + << " " << draw_top << " c\n"; // Right bezier: from (mid_x, draw_top) to (draw_right, draw_bottom) app_stream << mid_x << " " << (draw_bottom + height * 0.44f) << " " @@ -3063,6 +3834,37 @@ bool GenerateCaretAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const Byte return true; } +bool GenerateAnnotAPToTarget(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + CPDF_Annot::Subtype subtype, + BlendMode blend_mode) { + ByteString blend_name = BlendModeToPDFName(blend_mode); + switch (subtype) { + case CPDF_Annot::Subtype::CIRCLE: + return GenerateCircleAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::HIGHLIGHT: + return GenerateHighlightAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::INK: + return GenerateInkAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::LINE: + return GenerateLineAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::POLYGON: + return GeneratePolygonAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::POLYLINE: + return GeneratePolyLineAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::SQUARE: + return GenerateSquareAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::SQUIGGLY: + return GenerateSquigglyAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::STRIKEOUT: + return GenerateStrikeOutAP(target, annot_dict, blend_name); + case CPDF_Annot::Subtype::UNDERLINE: + return GenerateUnderlineAP(target, annot_dict, blend_name); + default: + return false; + } +} + // static bool CPDF_GenerateAP::GenerateAnnotAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, @@ -3076,34 +3878,21 @@ bool CPDF_GenerateAP::GenerateAnnotAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, CPDF_Annot::Subtype subtype, BlendMode blend_mode) { + APGenerationTarget target{doc, annot_dict}; + if (GenerateAnnotAPToTarget(&target, annot_dict, subtype, blend_mode)) { + return true; + } + ByteString blend_name = BlendModeToPDFName(blend_mode); switch (subtype) { - case CPDF_Annot::Subtype::CIRCLE: - return GenerateCircleAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::FREETEXT: - return GenerateFreeTextAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::HIGHLIGHT: - return GenerateHighlightAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::INK: - return GenerateInkAP(doc, annot_dict, blend_name); + return GenerateFreeTextAP(&target, annot_dict, blend_name); case CPDF_Annot::Subtype::POPUP: return GeneratePopupAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::SQUARE: - return GenerateSquareAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::SQUIGGLY: - return GenerateSquigglyAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::STRIKEOUT: - return GenerateStrikeOutAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::TEXT: return GenerateTextAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::UNDERLINE: - return GenerateUnderlineAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::POLYGON: - return GeneratePolygonAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::POLYLINE: - return GeneratePolyLineAP(doc, annot_dict, blend_name); - case CPDF_Annot::Subtype::LINE: - return GenerateLineAP(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: @@ -3115,6 +3904,76 @@ bool CPDF_GenerateAP::GenerateAnnotAP(CPDF_Document* doc, } } +// static +std::optional +CPDF_GenerateAP::GenerateEphemeralAnnotAP(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + CPDF_Annot::Subtype subtype) { + return GenerateEphemeralAnnotAP(doc, annot_dict, subtype, + DefaultBlendModeFor(subtype)); +} + +// static +std::optional +CPDF_GenerateAP::GenerateEphemeralAnnotAP(CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + CPDF_Annot::Subtype subtype, + BlendMode blend_mode) { + if (!SupportsEphemeralAnnotAP(subtype)) { + return std::nullopt; + } + + if (subtype == CPDF_Annot::Subtype::WIDGET) { + std::optional type = GetWidgetFormType(annot_dict); + return type.has_value() + ? GenerateEphemeralFormAP(doc, annot_dict, type.value()) + : std::nullopt; + } + + APGenerationTarget target{doc, nullptr}; + CPDF_Dictionary* mutable_annot_dict = + const_cast(annot_dict); + if (subtype == CPDF_Annot::Subtype::FREETEXT) { + if (!GenerateFreeTextAP(&target, mutable_annot_dict, + BlendModeToPDFName(blend_mode))) { + return std::nullopt; + } + return GeneratedAP{std::move(target.normal_stream)}; + } + + if (!GenerateAnnotAPToTarget(&target, mutable_annot_dict, subtype, + blend_mode)) { + return std::nullopt; + } + + return GeneratedAP{std::move(target.normal_stream)}; +} + +// static +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, @@ -3138,6 +3997,13 @@ bool CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( return false; } + RetainPtr dr_font_dict = + acroform_dict->GetOrCreateDictFor("DR")->GetOrCreateDictFor("Font"); + if (!GetFontFromDrFontDictOrGenerateFallback( + doc, dr_font_dict.Get(), maybe_font_name_and_size.value().name)) { + return false; + } + ByteString new_default_appearance_font_name_and_size = StringFromFontNameAndSize(maybe_font_name_and_size.value().name, maybe_font_name_and_size.value().size); @@ -3158,20 +4024,18 @@ bool CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( new_default_appearance_color.TrimBack('\n'); new_default_appearance_font_name_and_size.TrimBack('\n'); annot_dict->SetNewFor( - "DA", - new_default_appearance_color + " " + - new_default_appearance_font_name_and_size); + "DA", new_default_appearance_color + " " + + new_default_appearance_font_name_and_size); // TODO(thestig): Call GenerateAnnotAP(); return true; } -bool CPDF_GenerateAP::UpdateDefaultAppearance( - CPDF_Document* doc, - CPDF_Dictionary* annot_dict, - CPDF_Annot::StandardFont font, - float font_size, - const CFX_Color& color) { +bool CPDF_GenerateAP::UpdateDefaultAppearance(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, + CPDF_Annot::StandardFont font, + float font_size, + const CFX_Color& color) { ByteString resource_key; // When font is kUnknown, preserve the existing non-standard font resource @@ -3181,8 +4045,9 @@ bool CPDF_GenerateAP::UpdateDefaultAppearance( ByteString existing_da = annot_dict->GetByteStringFor("DA"); CPDF_DefaultAppearance current_da(existing_da); auto font_info = current_da.GetFont(); - if (!font_info.has_value() || font_info->name.IsEmpty()) + if (!font_info.has_value() || font_info->name.IsEmpty()) { return false; + } resource_key = font_info->name; } else { RetainPtr root_dict = doc->GetMutableRoot(); @@ -3219,3 +4084,32 @@ bool CPDF_GenerateAP::UpdateDefaultAppearance( 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 865315e04c..250fc6b372 100644 --- a/core/fpdfdoc/cpdf_generateap.h +++ b/core/fpdfdoc/cpdf_generateap.h @@ -7,10 +7,15 @@ #ifndef CORE_FPDFDOC_CPDF_GENERATEAP_H_ #define CORE_FPDFDOC_CPDF_GENERATEAP_H_ +#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; @@ -22,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); @@ -39,6 +51,39 @@ class CPDF_GenerateAP { CPDF_Annot::Subtype subtype, BlendMode blend_mode); + struct GeneratedAP { + RetainPtr normal_stream; + }; + + static std::optional GenerateEphemeralAnnotAP( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + CPDF_Annot::Subtype subtype); + + static std::optional GenerateEphemeralAnnotAP( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + CPDF_Annot::Subtype subtype, + BlendMode blend_mode); + + static std::optional GenerateEphemeralFormAP( + CPDF_Document* doc, + const CPDF_Dictionary* annot_dict, + FormType type); + + 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); @@ -49,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 new file mode 100644 index 0000000000..10028c5336 --- /dev/null +++ b/core/fpdfdoc/cpdf_generateap_unittest.cpp @@ -0,0 +1,278 @@ +// 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 "core/fpdfdoc/cpdf_generateap.h" + +#include + +#include "constants/annotation_common.h" +#include "constants/font_encodings.h" +#include "constants/form_fields.h" +#include "core/fpdfapi/page/test_with_page_module.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.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/fpdfapi/parser/cpdf_test_document.h" +#include "core/fxge/cfx_color.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class CPDFGenerateAPTest : public TestWithPageModule {}; + +RetainPtr MakeNumberArray(const std::vector& values) { + auto array = pdfium::MakeRetain(); + for (float value : values) { + array->AppendNew(value); + } + return array; +} + +RetainPtr MakeAnnotDict(const ByteString& subtype, + const CFX_FloatRect& rect) { + auto annot_dict = pdfium::MakeRetain(); + annot_dict->SetNewFor(pdfium::annotation::kSubtype, subtype); + annot_dict->SetRectFor(pdfium::annotation::kRect, rect); + return annot_dict; +} + +RetainPtr AddAcroFormWithHelvetica(CPDF_TestDocument* doc) { + doc->CreateNewDoc(); + RetainPtr root = doc->GetMutableRoot(); + auto acroform = root->SetNewFor("AcroForm"); + acroform->SetNewFor("DA", "/Helv 12 Tf 0 g"); + + auto dr = acroform->SetNewFor("DR"); + auto fonts = dr->SetNewFor("Font"); + auto helv = doc->NewIndirect(); + helv->SetNewFor("Type", "Font"); + helv->SetNewFor("Subtype", "Type1"); + helv->SetNewFor("BaseFont", "Helvetica"); + helv->SetNewFor("Encoding", + pdfium::font_encodings::kWinAnsiEncoding); + fonts->SetNewFor("Helv", doc, helv->GetObjNum()); + return acroform; +} + +RetainPtr MakeSupportedAnnotDict(CPDF_Annot::Subtype subtype) { + switch (subtype) { + case CPDF_Annot::Subtype::CIRCLE: + return MakeAnnotDict("Circle", CFX_FloatRect(0, 0, 100, 100)); + case CPDF_Annot::Subtype::HIGHLIGHT: { + auto annot_dict = + MakeAnnotDict("Highlight", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", + MakeNumberArray({10, 20, 50, 20, 10, 10, 50, 10})); + return annot_dict; + } + case CPDF_Annot::Subtype::FREETEXT: { + auto annot_dict = MakeAnnotDict("FreeText", CFX_FloatRect(0, 0, 100, 40)); + annot_dict->SetNewFor("DA", "/Helv 12 Tf 0 g"); + annot_dict->SetNewFor(pdfium::annotation::kContents, + "hello"); + return annot_dict; + } + case CPDF_Annot::Subtype::INK: { + auto annot_dict = MakeAnnotDict("Ink", CFX_FloatRect(0, 0, 10, 10)); + auto ink_list = pdfium::MakeRetain(); + ink_list->Append(MakeNumberArray({1, 1, 9, 9})); + annot_dict->SetFor("InkList", std::move(ink_list)); + return annot_dict; + } + case CPDF_Annot::Subtype::LINE: { + auto annot_dict = MakeAnnotDict("Line", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("L", MakeNumberArray({10, 10, 90, 90})); + return annot_dict; + } + case CPDF_Annot::Subtype::POLYGON: { + auto annot_dict = MakeAnnotDict("Polygon", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor(pdfium::annotation::kVertices, + MakeNumberArray({10, 10, 90, 10, 90, 90})); + return annot_dict; + } + case CPDF_Annot::Subtype::POLYLINE: { + auto annot_dict = + MakeAnnotDict("PolyLine", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor(pdfium::annotation::kVertices, + MakeNumberArray({10, 10, 50, 90, 90, 10})); + return annot_dict; + } + case CPDF_Annot::Subtype::SQUARE: + return MakeAnnotDict("Square", CFX_FloatRect(0, 0, 100, 100)); + case CPDF_Annot::Subtype::SQUIGGLY: { + auto annot_dict = + MakeAnnotDict("Squiggly", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", + MakeNumberArray({10, 20, 50, 20, 10, 10, 50, 10})); + return annot_dict; + } + case CPDF_Annot::Subtype::STRIKEOUT: { + auto annot_dict = + MakeAnnotDict("StrikeOut", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", + MakeNumberArray({10, 20, 50, 20, 10, 10, 50, 10})); + return annot_dict; + } + case CPDF_Annot::Subtype::UNDERLINE: { + auto annot_dict = + MakeAnnotDict("Underline", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", + MakeNumberArray({10, 20, 50, 20, 10, 10, 50, 10})); + return annot_dict; + } + case CPDF_Annot::Subtype::WIDGET: { + auto annot_dict = MakeAnnotDict("Widget", CFX_FloatRect(0, 0, 100, 40)); + annot_dict->SetNewFor(pdfium::form_fields::kFT, + pdfium::form_fields::kTx); + annot_dict->SetNewFor("DA", "/Helv 12 Tf 0 g"); + annot_dict->SetNewFor("V", "hello"); + return annot_dict; + } + default: + return nullptr; + } +} + +} // namespace + +TEST_F(CPDFGenerateAPTest, + GenerateEphemeralSupportedAnnotAPDoesNotPersistGraphState) { + static constexpr CPDF_Annot::Subtype kSupportedSubtypes[] = { + CPDF_Annot::Subtype::CIRCLE, CPDF_Annot::Subtype::FREETEXT, + CPDF_Annot::Subtype::HIGHLIGHT, CPDF_Annot::Subtype::INK, + CPDF_Annot::Subtype::LINE, CPDF_Annot::Subtype::POLYGON, + CPDF_Annot::Subtype::POLYLINE, CPDF_Annot::Subtype::SQUARE, + CPDF_Annot::Subtype::SQUIGGLY, CPDF_Annot::Subtype::STRIKEOUT, + CPDF_Annot::Subtype::UNDERLINE, CPDF_Annot::Subtype::WIDGET}; + + for (CPDF_Annot::Subtype subtype : kSupportedSubtypes) { + CPDF_TestDocument doc; + if (subtype == CPDF_Annot::Subtype::FREETEXT || + subtype == CPDF_Annot::Subtype::WIDGET) { + AddAcroFormWithHelvetica(&doc); + } + RetainPtr annot_dict = MakeSupportedAnnotDict(subtype); + ASSERT_TRUE(annot_dict); + + const uint32_t last_obj_num = doc.GetLastObjNum(); + std::optional generated = + CPDF_GenerateAP::GenerateEphemeralAnnotAP(&doc, annot_dict.Get(), + subtype); + + ASSERT_TRUE(generated.has_value()); + ASSERT_TRUE(generated->normal_stream); + EXPECT_EQ(0u, generated->normal_stream->GetObjNum()); + EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); + EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); + } +} + +TEST_F(CPDFGenerateAPTest, GenerateEphemeralFreeTextAPDoesNotCreateAcroForm) { + CPDF_TestDocument doc; + doc.CreateNewDoc(); + auto annot_dict = MakeSupportedAnnotDict(CPDF_Annot::Subtype::FREETEXT); + + const uint32_t last_obj_num = doc.GetLastObjNum(); + std::optional generated = + CPDF_GenerateAP::GenerateEphemeralAnnotAP(&doc, annot_dict.Get(), + CPDF_Annot::Subtype::FREETEXT); + + ASSERT_TRUE(generated.has_value()); + ASSERT_TRUE(generated->normal_stream); + EXPECT_EQ(0u, generated->normal_stream->GetObjNum()); + EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); + EXPECT_FALSE(doc.GetRoot()->KeyExist("AcroForm")); + EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); +} + +TEST_F(CPDFGenerateAPTest, + GeneratePersistentFreeTextAPAfterDefaultAppearanceColorUpdate) { + CPDF_TestDocument doc; + doc.CreateNewDoc(); + auto annot_dict = MakeAnnotDict("FreeText", CFX_FloatRect(100, 50, 150, 75)); + annot_dict->SetNewFor(pdfium::annotation::kContents, "Hello!"); + + ASSERT_TRUE(CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( + &doc, annot_dict.Get(), CFX_Color(60, 120, 180))); + ASSERT_TRUE(CPDF_GenerateAP::GenerateAnnotAP( + &doc, annot_dict.Get(), CPDF_Annot::Subtype::FREETEXT)); + + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + ASSERT_TRUE(ap_dict); + RetainPtr normal_stream = ap_dict->GetStreamFor("N"); + ASSERT_TRUE(normal_stream); + EXPECT_NE(0u, normal_stream->GetObjNum()); +} + +TEST_F(CPDFGenerateAPTest, + DefaultAppearanceColorUpdateEnsuresPersistentFontResource) { + CPDF_TestDocument doc; + doc.CreateNewDoc(); + RetainPtr acroform = + doc.GetMutableRoot()->SetNewFor("AcroForm"); + acroform->SetNewFor("DA", "/Helv 12 Tf 0 g"); + + auto annot_dict = MakeAnnotDict("FreeText", CFX_FloatRect(100, 50, 150, 75)); + + ASSERT_TRUE(CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( + &doc, annot_dict.Get(), CFX_Color(60, 120, 180))); + + RetainPtr font_dict = + acroform->GetDictFor("DR")->GetDictFor("Font"); + ASSERT_TRUE(font_dict); + EXPECT_TRUE(font_dict->KeyExist("Helv")); +} + +TEST_F(CPDFGenerateAPTest, GenerateEphemeralAnnotAPDoesNotPersistHighlightAP) { + CPDF_TestDocument doc; + auto annot_dict = MakeAnnotDict("Highlight", CFX_FloatRect(0, 0, 100, 100)); + annot_dict->SetFor("QuadPoints", + MakeNumberArray({10, 20, 50, 20, 10, 10, 50, 10})); + + const uint32_t last_obj_num = doc.GetLastObjNum(); + std::optional generated = + CPDF_GenerateAP::GenerateEphemeralAnnotAP(&doc, annot_dict.Get(), + CPDF_Annot::Subtype::HIGHLIGHT); + + ASSERT_TRUE(generated.has_value()); + ASSERT_TRUE(generated->normal_stream); + EXPECT_EQ(0u, generated->normal_stream->GetObjNum()); + EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); + EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); +} + +TEST_F(CPDFGenerateAPTest, GenerateEphemeralInkAPDoesNotInflateAnnotRect) { + CPDF_TestDocument doc; + auto annot_dict = MakeAnnotDict("Ink", CFX_FloatRect(0, 0, 10, 10)); + + auto border_style = annot_dict->SetNewFor("BS"); + border_style->SetNewFor("W", 4); + + auto ink_list = pdfium::MakeRetain(); + ink_list->Append(MakeNumberArray({1, 1, 9, 9})); + annot_dict->SetFor("InkList", std::move(ink_list)); + + const CFX_FloatRect original_rect = + annot_dict->GetRectFor(pdfium::annotation::kRect); + const uint32_t last_obj_num = doc.GetLastObjNum(); + std::optional generated = + CPDF_GenerateAP::GenerateEphemeralAnnotAP(&doc, annot_dict.Get(), + CPDF_Annot::Subtype::INK); + + ASSERT_TRUE(generated.has_value()); + ASSERT_TRUE(generated->normal_stream); + EXPECT_EQ(0u, generated->normal_stream->GetObjNum()); + EXPECT_EQ(last_obj_num, doc.GetLastObjNum()); + EXPECT_EQ(original_rect, annot_dict->GetRectFor(pdfium::annotation::kRect)); + // 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 24bf93ad41..22078d81d3 100644 --- a/core/fpdfdoc/cpdf_interactiveform.cpp +++ b/core/fpdfdoc/cpdf_interactiveform.cpp @@ -249,14 +249,15 @@ bool FindFontFromDoc(const CPDF_Dictionary* form_dict, CPDF_DictionaryLocker locker(font_dict); for (const auto& it : locker) { const ByteString& key = it.first; - RetainPtr element = - ToDictionary(it.second->GetMutableDirect()); + RetainPtr element = + ToDictionary(it.second->GetDirect()); if (!ValidateDictType(element.Get(), "Font")) { continue; } auto* pData = CPDF_DocPageData::FromDocument(document); - font = pData->GetFont(std::move(element)); + font = pData->GetFont( + pdfium::WrapRetain(const_cast(element.Get()))); if (!font) { continue; } @@ -349,14 +350,15 @@ RetainPtr GetNativeFont(const CPDF_Dictionary* form_dict, CPDF_DictionaryLocker locker(font_dict); for (const auto& it : locker) { const ByteString& key = it.first; - RetainPtr element = - ToDictionary(it.second->GetMutableDirect()); + RetainPtr element = + ToDictionary(it.second->GetDirect()); if (!ValidateDictType(element.Get(), "Font")) { continue; } auto* pData = CPDF_DocPageData::FromDocument(document); - RetainPtr pFind = pData->GetFont(std::move(element)); + RetainPtr pFind = pData->GetFont( + pdfium::WrapRetain(const_cast(element.Get()))); if (!pFind) { continue; } @@ -588,23 +590,25 @@ CFieldTree::Node* CFieldTree::FindNode(const WideString& full_name) { CPDF_InteractiveForm::CPDF_InteractiveForm(CPDF_Document* document) : document_(document), field_tree_(std::make_unique()) { - RetainPtr pRoot = document_->GetMutableRoot(); + const CPDF_Dictionary* pRoot = document_->GetRoot(); if (!pRoot) { return; } - form_dict_ = pRoot->GetMutableDictFor("AcroForm"); - if (!form_dict_) { + RetainPtr form_dict = pRoot->GetDictFor("AcroForm"); + if (!form_dict) { return; } + form_dict_ = + pdfium::WrapRetain(const_cast(form_dict.Get())); - RetainPtr fields = form_dict_->GetMutableArrayFor("Fields"); + RetainPtr fields = form_dict->GetArrayFor("Fields"); if (!fields) { return; } for (size_t i = 0; i < fields->size(); ++i) { - LoadField(fields->GetMutableDictAt(i), 0); + LoadField(fields->GetDictAt(i), 0); } } @@ -789,23 +793,24 @@ RetainPtr CPDF_InteractiveForm::GetFormFont( return nullptr; } - RetainPtr pDR = form_dict_->GetMutableDictFor("DR"); + RetainPtr pDR = form_dict_->GetDictFor("DR"); if (!pDR) { return nullptr; } - RetainPtr font_dict = pDR->GetMutableDictFor("Font"); + RetainPtr font_dict = pDR->GetDictFor("Font"); if (!ValidateFontResourceDict(font_dict.Get())) { return nullptr; } - RetainPtr element = - font_dict->GetMutableDictFor(alias.AsStringView()); + RetainPtr element = + font_dict->GetDictFor(alias.AsStringView()); if (!ValidateDictType(element.Get(), "Font")) { return nullptr; } - return GetFontForElement(std::move(element)); + return GetFontForElement( + pdfium::WrapRetain(const_cast(element.Get()))); } RetainPtr CPDF_InteractiveForm::GetFontForElement( @@ -851,18 +856,36 @@ CPDF_InteractiveForm::GetControlsForField(const CPDF_FormField* field) { return control_lists_[pdfium::WrapUnowned(field)]; } -void CPDF_InteractiveForm::LoadField(RetainPtr field_dict, - int nLevel) { +// 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) { if (nLevel > kMaxRecursion) { return; } 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 = - field_dict->GetMutableArrayFor(pdfium::form_fields::kKids); + RetainPtr kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); if (!kids) { AddTerminalField(std::move(field_dict)); return; @@ -879,103 +902,132 @@ void CPDF_InteractiveForm::LoadField(RetainPtr field_dict, return; } for (size_t i = 0; i < kids->size(); i++) { - RetainPtr pChildDict = kids->GetMutableDictAt(i); - if (pChildDict && pChildDict->GetObjNum() != dwParentObjNum) { + RetainPtr pChildDict = kids->GetDictAt(i); + if (pChildDict && pChildDict.Get() != field_dict.Get() && + (dwParentObjNum == 0 || pChildDict->GetObjNum() != dwParentObjNum)) { LoadField(std::move(pChildDict), nLevel + 1); } } } void CPDF_InteractiveForm::FixPageFields(CPDF_Page* page) { - RetainPtr annots = page->GetMutableAnnotsArray(); + RetainPtr annots = page->GetAnnotsArray(); if (!annots) { return; } for (size_t i = 0; i < annots->size(); i++) { - RetainPtr annot = annots->GetMutableDictAt(i); + RetainPtr annot = annots->GetDictAt(i); if (annot && annot->GetNameFor("Subtype") == "Widget") { LoadField(std::move(annot), 0); } } } -void CPDF_InteractiveForm::AddTerminalField( - RetainPtr field_dict) { - if (!field_dict->KeyExist(pdfium::form_fields::kFT)) { - // Key "FT" is required for terminal fields, it is also inheritable. - RetainPtr pParentDict = - field_dict->GetDictFor(pdfium::form_fields::kParent); - if (!pParentDict || !pParentDict->KeyExist(pdfium::form_fields::kFT)) { - return; - } +// 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; } - WideString field_name = CPDF_FormField::GetFullNameForDict(field_dict.Get()); - if (field_name.IsEmpty()) { + // 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; } - CPDF_FormField* field = field_tree_->GetField(field_name); - if (!field) { - RetainPtr pParent(field_dict); - if (!field_dict->KeyExist(pdfium::form_fields::kT) && - field_dict->GetNameFor("Subtype") == "Widget") { - pParent = field_dict->GetMutableDictFor(pdfium::form_fields::kParent); - if (!pParent) { - pParent = field_dict; - } + // 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 (pParent && pParent != field_dict && - !pParent->KeyExist(pdfium::form_fields::kFT)) { - if (field_dict->KeyExist(pdfium::form_fields::kFT)) { - RetainPtr pFTValue = - field_dict->GetDirectObjectFor(pdfium::form_fields::kFT); - if (pFTValue) { - pParent->SetFor(pdfium::form_fields::kFT, pFTValue->Clone()); - } - } + // 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); + } +} - if (field_dict->KeyExist(pdfium::form_fields::kFf)) { - RetainPtr pFfValue = - field_dict->GetDirectObjectFor(pdfium::form_fields::kFf); - if (pFfValue) { - pParent->SetFor(pdfium::form_fields::kFf, pFfValue->Clone()); +void CPDF_InteractiveForm::AddTerminalField( + RetainPtr field_dict) { + RetainPtr field_storage_dict = field_dict; + if (!CPDF_FormField::GetFieldAttrForDict(field_storage_dict.Get(), + pdfium::form_fields::kFT)) { + RetainPtr kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); + field_storage_dict.Reset(); + if (kids) { + for (size_t i = 0; i < kids->size(); ++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); + break; } } } + if (!field_storage_dict) { + return; + } + } + + WideString field_name = + CPDF_FormField::GetFullNameForDict(field_storage_dict.Get()); + if (field_name.IsEmpty()) { + return; + } - auto new_field = std::make_unique(this, std::move(pParent)); + CPDF_FormField* field = field_tree_->GetField(field_name); + if (!field) { + auto new_field = std::make_unique( + this, pdfium::WrapRetain( + const_cast(field_storage_dict.Get()))); field = new_field.get(); - RetainPtr t_obj = - field_dict->GetObjectFor(pdfium::form_fields::kT); - if (ToReference(t_obj)) { - RetainPtr t_obj_clone = t_obj->CloneDirectObject(); - if (t_obj_clone && t_obj_clone->IsString()) { - field_dict->SetFor(pdfium::form_fields::kT, std::move(t_obj_clone)); - } else { - field_dict->SetNewFor(pdfium::form_fields::kT, - ByteString()); - } - } if (!field_tree_->SetField(field_name, std::move(new_field))) { return; } } - RetainPtr kids = - field_dict->GetMutableArrayFor(pdfium::form_fields::kKids); + RetainPtr kids = + field_dict->GetArrayFor(pdfium::form_fields::kKids); if (!kids) { if (field_dict->GetNameFor("Subtype") == "Widget") { - AddControl(field, std::move(field_dict)); + AddControl(field, pdfium::WrapRetain( + const_cast(field_dict.Get()))); } return; } for (size_t i = 0; i < kids->size(); i++) { - RetainPtr kid = kids->GetMutableDictAt(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, std::move(kid)); + AddControl(field, + pdfium::WrapRetain(const_cast(kid.Get()))); } } } @@ -1034,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; @@ -1082,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 33c09170e7..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,8 +121,15 @@ class CPDF_InteractiveForm { CPDF_Document* document() { return document_; } private: - void LoadField(RetainPtr field_dict, int nLevel); - void AddTerminalField(RetainPtr field_dict); + // 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, RetainPtr widget_dict); diff --git a/core/fpdfdoc/cpdf_interactiveform_unittest.cpp b/core/fpdfdoc/cpdf_interactiveform_unittest.cpp index 9866687384..405497b070 100644 --- a/core/fpdfdoc/cpdf_interactiveform_unittest.cpp +++ b/core/fpdfdoc/cpdf_interactiveform_unittest.cpp @@ -10,6 +10,8 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.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_string.h" @@ -57,18 +59,67 @@ TEST_F(CPDFInteractiveFormTest, LoadFieldsWithReferencedNames) { bad_stream_field_dict->SetNewFor("T", doc.get(), bad_stream->GetObjNum()); - // Let `interactive_form` parse the dictionaries above and fix them up. - CPDF_InteractiveForm interactive_form(doc.get()); + const uint32_t last_obj_num = doc->GetLastObjNum(); - auto good_string_field_t = good_string_field_dict->GetStringFor("T"); - ASSERT_TRUE(good_string_field_t); - EXPECT_EQ("good_string", good_string_field_t->GetString()); + // Let `interactive_form` parse the dictionaries above. + std::unique_ptr interactive_form; + { + CPDF_ReadOnlyGraphGuard guard; + interactive_form = std::make_unique(doc.get()); + } - auto bad_name_field_t = bad_name_field_dict->GetStringFor("T"); - ASSERT_TRUE(bad_name_field_t); - EXPECT_TRUE(bad_name_field_t->GetString().IsEmpty()); + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); + EXPECT_TRUE(ToReference(good_string_field_dict->GetObjectFor("T"))); + EXPECT_TRUE(ToReference(bad_name_field_dict->GetObjectFor("T"))); + EXPECT_TRUE(ToReference(bad_stream_field_dict->GetObjectFor("T"))); - auto bad_stream_field_t = bad_stream_field_dict->GetStringFor("T"); - ASSERT_TRUE(bad_stream_field_t); - EXPECT_TRUE(bad_stream_field_t->GetString().IsEmpty()); + EXPECT_EQ( + 1u, interactive_form->CountFields(WideString::FromASCII("good_string"))); + EXPECT_EQ(0u, + interactive_form->CountFields(WideString::FromASCII("bad_name"))); + EXPECT_EQ(1u, interactive_form->CountFields(WideString())); +} + +TEST_F(CPDFInteractiveFormTest, LoadFieldDoesNotCopyInheritedTypeToParent) { + auto doc = std::make_unique(); + doc->CreateNewDoc(); + RetainPtr root = doc->GetMutableRoot(); + ASSERT_TRUE(root); + + auto acroform_dict = root->SetNewFor("AcroForm"); + auto fields_array = acroform_dict->SetNewFor("Fields"); + + auto parent_dict = doc->NewIndirect(); + parent_dict->SetNewFor("T", "Parent"); + fields_array->AppendNew(doc.get(), parent_dict->GetObjNum()); + + auto kids = parent_dict->SetNewFor("Kids"); + auto widget_dict = kids->AppendNew(); + widget_dict->SetNewFor("Type", "Annot"); + widget_dict->SetNewFor("Subtype", "Widget"); + widget_dict->SetNewFor("FT", "Tx"); + widget_dict->SetNewFor("Ff", 123); + widget_dict->SetNewFor("Parent", doc.get(), + parent_dict->GetObjNum()); + + ASSERT_FALSE(parent_dict->KeyExist("FT")); + ASSERT_FALSE(parent_dict->KeyExist("Ff")); + const uint32_t last_obj_num = doc->GetLastObjNum(); + + std::unique_ptr interactive_form; + { + CPDF_ReadOnlyGraphGuard guard; + interactive_form = std::make_unique(doc.get()); + } + + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); + EXPECT_FALSE(parent_dict->KeyExist("FT")); + EXPECT_FALSE(parent_dict->KeyExist("Ff")); + EXPECT_EQ(1u, + interactive_form->CountFields(WideString::FromASCII("Parent"))); + CPDF_FormField* field = + interactive_form->GetField(0, WideString::FromASCII("Parent")); + ASSERT_TRUE(field); + EXPECT_EQ(FormFieldType::kTextField, field->GetFieldType()); + EXPECT_EQ(1, field->CountControls()); } diff --git a/core/fpdfdoc/cpdf_linklist.cpp b/core/fpdfdoc/cpdf_linklist.cpp index c80270e7fb..08b64f3bbc 100644 --- a/core/fpdfdoc/cpdf_linklist.cpp +++ b/core/fpdfdoc/cpdf_linklist.cpp @@ -20,7 +20,7 @@ CPDF_LinkList::~CPDF_LinkList() = default; CPDF_Link CPDF_LinkList::GetLinkAtPoint(CPDF_Page* pPage, const CFX_PointF& point, int* z_order) { - const std::vector>* pPageLinkList = + const std::vector>* pPageLinkList = GetPageLinks(pPage); if (!pPageLinkList) { return CPDF_Link(); @@ -28,12 +28,13 @@ CPDF_Link CPDF_LinkList::GetLinkAtPoint(CPDF_Page* pPage, for (size_t i = pPageLinkList->size(); i > 0; --i) { size_t annot_index = i - 1; - RetainPtr pAnnot = (*pPageLinkList)[annot_index]; + RetainPtr pAnnot = (*pPageLinkList)[annot_index]; if (!pAnnot) { continue; } - CPDF_Link link(std::move(pAnnot)); + CPDF_Link link( + pdfium::WrapRetain(const_cast(pAnnot.Get()))); if (!link.GetRect().Contains(point)) { continue; } @@ -46,8 +47,14 @@ CPDF_Link CPDF_LinkList::GetLinkAtPoint(CPDF_Page* pPage, return CPDF_Link(); } -const std::vector>* CPDF_LinkList::GetPageLinks( - CPDF_Page* pPage) { +const std::vector>* +CPDF_LinkList::GetPageLinks(CPDF_Page* pPage) { + const uint64_t current_epoch = pPage->GetDocument()->GetOverlayEpoch(); + if (overlay_epoch_ != current_epoch) { + page_map_.clear(); + overlay_epoch_ = current_epoch; + } + uint32_t objnum = pPage->GetDict()->GetObjNum(); if (objnum == 0) { return nullptr; @@ -60,13 +67,13 @@ const std::vector>* CPDF_LinkList::GetPageLinks( // std::map::operator[] forces the creation of a map entry. auto* page_link_list = &page_map_[objnum]; - RetainPtr pAnnotList = pPage->GetMutableAnnotsArray(); + RetainPtr pAnnotList = pPage->GetAnnotsArray(); if (!pAnnotList) { return page_link_list; } for (size_t i = 0; i < pAnnotList->size(); ++i) { - RetainPtr pAnnot = pAnnotList->GetMutableDictAt(i); + RetainPtr pAnnot = pAnnotList->GetDictAt(i); bool add_link = (pAnnot && pAnnot->GetByteStringFor("Subtype") == "Link"); // Add non-links as nullptrs to preserve z-order. page_link_list->emplace_back(add_link ? pAnnot : nullptr); diff --git a/core/fpdfdoc/cpdf_linklist.h b/core/fpdfdoc/cpdf_linklist.h index b259aaf88a..a47b968ff5 100644 --- a/core/fpdfdoc/cpdf_linklist.h +++ b/core/fpdfdoc/cpdf_linklist.h @@ -29,9 +29,11 @@ class CPDF_LinkList final : public CPDF_Document::LinkListIface { int* z_order); private: - const std::vector>* GetPageLinks(CPDF_Page* pPage); + const std::vector>* GetPageLinks( + CPDF_Page* pPage); - std::map>> page_map_; + uint64_t overlay_epoch_ = 0; + std::map>> page_map_; }; #endif // CORE_FPDFDOC_CPDF_LINKLIST_H_ diff --git a/core/fpdfdoc/cpdf_nametree.cpp b/core/fpdfdoc/cpdf_nametree.cpp index b4f65c7a28..967a300e4a 100644 --- a/core/fpdfdoc/cpdf_nametree.cpp +++ b/core/fpdfdoc/cpdf_nametree.cpp @@ -455,8 +455,8 @@ RetainPtr LookupOldStyleNamedDest(CPDF_Document* doc, } // namespace -CPDF_NameTree::CPDF_NameTree(RetainPtr pRoot) - : root_(std::move(pRoot)) { +CPDF_NameTree::CPDF_NameTree(RetainPtr pRoot, bool read_only) + : root_(std::move(pRoot)), read_only_(read_only) { DCHECK(root_); } @@ -481,7 +481,34 @@ std::unique_ptr CPDF_NameTree::Create(CPDF_Document* doc, } return pdfium::WrapUnique( - new CPDF_NameTree(std::move(pCategory))); // Private ctor. + new CPDF_NameTree(std::move(pCategory), /*read_only=*/false)); +} + +// static +std::unique_ptr CPDF_NameTree::CreateForReading( + CPDF_Document* doc, + ByteStringView category) { + const CPDF_Dictionary* root = doc->GetRoot(); + if (!root) { + return nullptr; + } + + RetainPtr names = root->GetDictFor("Names"); + if (!names) { + return nullptr; + } + + RetainPtr category_dict = names->GetDictFor(category); + if (!category_dict) { + return nullptr; + } + + // CPDF_NameTree stores a mutable pointer because the same class also backs + // AddValueAndName() / DeleteValueAndName(). `read_only_` prevents mutation + // through trees constructed by this const traversal path. + return pdfium::WrapUnique(new CPDF_NameTree( + pdfium::WrapRetain(const_cast(category_dict.Get())), + /*read_only=*/true)); } // static @@ -509,14 +536,14 @@ std::unique_ptr CPDF_NameTree::CreateWithRootNameArray( pCategory->GetObjNum()); } - return pdfium::WrapUnique(new CPDF_NameTree(pCategory)); // Private ctor. + return pdfium::WrapUnique(new CPDF_NameTree(pCategory, /*read_only=*/false)); } // static std::unique_ptr CPDF_NameTree::CreateForTesting( CPDF_Dictionary* pRoot) { return pdfium::WrapUnique( - new CPDF_NameTree(pdfium::WrapRetain(pRoot))); // Private ctor. + new CPDF_NameTree(pdfium::WrapRetain(pRoot), /*read_only=*/false)); } // static @@ -524,7 +551,7 @@ RetainPtr CPDF_NameTree::LookupNamedDest( CPDF_Document* doc, const ByteString& name) { RetainPtr dest_array; - std::unique_ptr name_tree = Create(doc, "Dests"); + std::unique_ptr name_tree = CreateForReading(doc, "Dests"); if (name_tree) { dest_array = name_tree->LookupNewStyleNamedDest(name); } @@ -541,6 +568,7 @@ size_t CPDF_NameTree::GetCount() const { bool CPDF_NameTree::AddValueAndName(RetainPtr pObj, const WideString& name) { + CHECK(!read_only_); NodeToInsert node_to_insert; // Handle the corner case where the root node is empty. i.e. No kids and no // names. In which case, just insert into it and skip all the searches. @@ -601,6 +629,7 @@ bool CPDF_NameTree::AddValueAndName(RetainPtr pObj, } bool CPDF_NameTree::DeleteValueAndName(size_t nIndex) { + CHECK(!read_only_); std::optional result = SearchNameNodeByIndex(root_.Get(), nIndex); if (!result) { diff --git a/core/fpdfdoc/cpdf_nametree.h b/core/fpdfdoc/cpdf_nametree.h index a6505a65cd..de6b4043f8 100644 --- a/core/fpdfdoc/cpdf_nametree.h +++ b/core/fpdfdoc/cpdf_nametree.h @@ -27,6 +27,9 @@ class CPDF_NameTree { static std::unique_ptr Create(CPDF_Document* doc, ByteStringView category); + static std::unique_ptr CreateForReading( + CPDF_Document* doc, + ByteStringView category); // If necessary, create missing Names dictionary in |doc|, and/or missing // Names array in the dictionary that corresponds to |category|, if necessary. @@ -52,11 +55,12 @@ class CPDF_NameTree { CPDF_Dictionary* GetRootForTesting() const { return root_.Get(); } private: - explicit CPDF_NameTree(RetainPtr pRoot); + CPDF_NameTree(RetainPtr pRoot, bool read_only); RetainPtr LookupNewStyleNamedDest(const ByteString& name); RetainPtr const root_; + const bool read_only_; }; #endif // CORE_FPDFDOC_CPDF_NAMETREE_H_ 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/fxcodec/icc/icc_transform.cpp b/core/fxcodec/icc/icc_transform.cpp index 0cd02e7b2f..fad316a265 100644 --- a/core/fxcodec/icc/icc_transform.cpp +++ b/core/fxcodec/icc/icc_transform.cpp @@ -20,6 +20,22 @@ namespace fxcodec { namespace { +// EmbedPDF: thread-confined runtime - LCMS verification gate. +// +// The cms* calls below (cmsOpenProfileFromMem, cmsCreate_sRGBProfile, +// cmsCreateTransform, cmsDoTransform, ...) use the default/null cmsContext, +// which is a process-global in lcms2. We intentionally do NOT rewrite this for +// the first thread_local slice: per-profile/per-transform work on the default +// context is independent across threads in practice, and our workers create +// and use their own profiles/transforms. +// +// Gate (not a code change): run the threaded soak (testing/tools:epdf_thread_ +// soak) under ThreadSanitizer with ICC-heavy PDFs before lifting the server +// pool cap. If TSAN flags contention/races in the default context, move to a +// per-thread cmsContext via cmsCreateContext + the cms*THR APIs (or guard +// transform creation with a mutex). Do not enable ICC-heavy concurrency in +// production until this gate is green. + // For use with std::unique_ptr. struct CmsProfileDeleter { inline void operator()(cmsHPROFILE p) { cmsCloseProfile(p); } 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/cfx_timer.cpp b/core/fxcrt/cfx_timer.cpp index 658f39a373..48fa5a07da 100644 --- a/core/fxcrt/cfx_timer.cpp +++ b/core/fxcrt/cfx_timer.cpp @@ -9,11 +9,16 @@ #include #include "core/fxcrt/check.h" +#include "core/fxcrt/epdf_tls.h" namespace { using TimerMap = std::map; -TimerMap* g_pwl_timer_map = nullptr; +// EmbedPDF: thread-confined runtime - per-thread timer map. The PWL timer +// subsystem is inactive in headless server rendering, but the global is still +// made per-thread so per-thread InitializeGlobals()/DestroyGlobals() (and the +// CHECK(!g_pwl_timer_map) inside Init) hold independently on each worker. +EPDF_TLS TimerMap* g_pwl_timer_map = nullptr; } // namespace diff --git a/core/fxcrt/epdf_tls.h b/core/fxcrt/epdf_tls.h new file mode 100644 index 0000000000..bca3c6eba3 --- /dev/null +++ b/core/fxcrt/epdf_tls.h @@ -0,0 +1,30 @@ +// Copyright 2025 The EmbedPDF Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// EmbedPDF: thread-confined runtime support. +// +// EPDF_TLS expands to `thread_local` when the build is configured with the +// `embedpdf_thread_local_globals` GN arg (which defines +// EPDF_THREAD_LOCAL_GLOBALS), and to nothing otherwise. It is used to give +// each worker thread its own copy of PDFium's process-global singletons so +// that N threads can run shared-nothing in a single process. +// +// Contract: a thread that touches any EPDF_TLS-backed global must initialize +// PDFium on that thread (EPDF_InitThread), use only handles created on that +// thread, and tear down on the same thread (EPDF_ShutdownThread). PDFium +// handles must never cross threads. +// +// Default (flag off) keeps every global as an ordinary process-global, so +// targets that do not opt in and upstream rebases are byte-for-byte unchanged. + +#ifndef CORE_FXCRT_EPDF_TLS_H_ +#define CORE_FXCRT_EPDF_TLS_H_ + +#if defined(EPDF_THREAD_LOCAL_GLOBALS) +#define EPDF_TLS thread_local +#else +#define EPDF_TLS +#endif + +#endif // CORE_FXCRT_EPDF_TLS_H_ diff --git a/core/fxcrt/fx_extension.cpp b/core/fxcrt/fx_extension.cpp index 28b4825b40..91c2151a94 100644 --- a/core/fxcrt/fx_extension.cpp +++ b/core/fxcrt/fx_extension.cpp @@ -6,6 +6,7 @@ #include "core/fxcrt/fx_extension.h" +#include #include #include @@ -24,7 +25,20 @@ time_t DefaultTimeFunction() { } struct tm* DefaultLocaltimeFunction(const time_t* tp) { - return localtime(tp); + // EmbedPDF: thread-confined runtime. Plain localtime() returns a pointer to + // shared static storage, which is a data race across worker threads. Fill a + // thread_local tm via the reentrant localtime_r/localtime_s instead so each + // thread gets its own result. struct tm is trivially destructible, so the + // thread_local adds no per-thread teardown cost. + thread_local struct tm result; +#if defined(_WIN32) + if (localtime_s(&result, tp) != 0) { + return nullptr; + } + return &result; +#else + return localtime_r(tp, &result); +#endif } time_t (*g_time_func)() = DefaultTimeFunction; diff --git a/core/fxcrt/fx_random.cpp b/core/fxcrt/fx_random.cpp index c0733698c9..ea0b63e447 100644 --- a/core/fxcrt/fx_random.cpp +++ b/core/fxcrt/fx_random.cpp @@ -9,6 +9,7 @@ #include #include "build/build_config.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_memory.h" #include "core/fxcrt/fx_string.h" #include "core/fxcrt/fx_system.h" @@ -33,8 +34,10 @@ struct MTContext { std::array mt; }; -bool g_bHaveGlobalSeed = false; -uint32_t g_nGlobalSeed = 0; +// EmbedPDF: thread-confined runtime - per-thread RNG seed state so concurrent +// workers don't race on the lazy global-seed initialization. +EPDF_TLS bool g_bHaveGlobalSeed = false; +EPDF_TLS uint32_t g_nGlobalSeed = 0; #if BUILDFLAG(IS_WIN) bool GenerateSeedFromCryptoRandom(uint32_t* pSeed) { diff --git a/core/fxcrt/fx_system.cpp b/core/fxcrt/fx_system.cpp index d5c00af4df..ff3a72d38f 100644 --- a/core/fxcrt/fx_system.cpp +++ b/core/fxcrt/fx_system.cpp @@ -12,12 +12,15 @@ #include "build/build_config.h" #include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_extension.h" namespace { #if !BUILDFLAG(IS_WIN) -uint32_t g_last_error = 0; +// EmbedPDF: thread-confined runtime - per-thread last-error, written during +// parse/render and read back by FPDF_GetLastError on the same thread. +EPDF_TLS uint32_t g_last_error = 0; #endif template 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/core/fxge/cfx_gemodule.cpp b/core/fxge/cfx_gemodule.cpp index e67b86ceca..b4294e8085 100644 --- a/core/fxge/cfx_gemodule.cpp +++ b/core/fxge/cfx_gemodule.cpp @@ -7,12 +7,16 @@ #include "core/fxge/cfx_gemodule.h" #include "core/fxcrt/check.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxge/cfx_folderfontinfo.h" #include "core/fxge/cfx_fontmgr.h" namespace { -CFX_GEModule* g_pGEModule = nullptr; +// EmbedPDF: thread-confined runtime - each worker thread owns its own +// CFX_GEModule (FreeType FT_Library + font/glyph caches), created and destroyed +// on that thread. +EPDF_TLS CFX_GEModule* g_pGEModule = nullptr; } // namespace diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index b903c2b31f..d387a009ef 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -7,9 +7,26 @@ 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", "epdf_jpeg_shim.cpp", + "epdf_redact.cpp", + "epdf_threading.cpp", "cpdfsdk_annot.cpp", "cpdfsdk_annot.h", "cpdfsdk_annotiteration.cpp", @@ -131,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/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp index bf887c5797..5c91858b62 100644 --- a/fpdfsdk/cpdfsdk_formfillenvironment.cpp +++ b/fpdfsdk/cpdfsdk_formfillenvironment.cpp @@ -685,7 +685,7 @@ CPDFSDK_PageView* CPDFSDK_FormFillEnvironment::GetPageViewAtIndex(int nIndex) { } void CPDFSDK_FormFillEnvironment::ProcJavascriptAction() { - auto name_tree = CPDF_NameTree::Create(cpdfdoc_, "JavaScript"); + auto name_tree = CPDF_NameTree::CreateForReading(cpdfdoc_, "JavaScript"); if (!name_tree) { return; } diff --git a/fpdfsdk/cpdfsdk_helpers.cpp b/fpdfsdk/cpdfsdk_helpers.cpp index 766f6033a5..6f294389ae 100644 --- a/fpdfsdk/cpdfsdk_helpers.cpp +++ b/fpdfsdk/cpdfsdk_helpers.cpp @@ -11,6 +11,7 @@ #include "build/build_config.h" #include "constants/form_fields.h" #include "constants/stream_dict_common.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" @@ -38,6 +39,18 @@ namespace { constexpr char kQuadPoints[] = "QuadPoints"; +// EmbedPDF: thread-confined runtime - config callbacks, NOT runtime knobs. +// These two process-globals are intentionally left process-wide (not EPDF_TLS): +// they are immutable configuration that MUST be set once during process +// bootstrap, before any worker thread starts, and MUST NOT be mutated while +// workers are live. Setting them per-request/per-thread is unsupported. The +// server sets neither after startup, so there is no cross-thread write race. +// +// (Other inactive process-globals in our build config - the Skia font manager +// and the Windows print-mode globals - are compiled out of the headless server +// runtime. They would be unsafe under multi-threading if those subsystems were +// ever re-enabled, and would need the same treatment then.) + // 0 bit: FPDF_POLICY_MACHINETIME_ACCESS uint32_t g_sandbox_policy = 0xFFFFFFFF; @@ -222,6 +235,26 @@ CPDF_Page* CPDFPageFromFPDFPage(FPDF_PAGE page) { return page ? IPDFPageFromFPDFPage(page)->AsPDFPage() : nullptr; } +ScopedFPDFDocumentView::ScopedFPDFDocumentView(FPDF_DOCUMENT document) + : document_(CPDFDocumentFromFPDFDocument(document)), + view_scope_(document_) {} + +ScopedFPDFDocumentView::~ScopedFPDFDocumentView() = default; + +ScopedFPDFPageView::ScopedFPDFPageView(FPDF_PAGE page) + : page_(CPDFPageFromFPDFPage(page)), + view_scope_(page_ ? page_->GetDocument() : nullptr) {} + +ScopedFPDFPageView::~ScopedFPDFPageView() = default; + +ScopedFPDFAnnotationView::ScopedFPDFAnnotationView( + FPDF_ANNOTATION annotation) + : annotation_(CPDFAnnotContextFromFPDFAnnotation(annotation)), + view_scope_(annotation_ ? annotation_->GetPage()->GetDocument() + : nullptr) {} + +ScopedFPDFAnnotationView::~ScopedFPDFAnnotationView() = default; + FXDIB_Format FXDIBFormatFromFPDFFormat(int format) { switch (format) { case FPDFBitmap_Gray: diff --git a/fpdfsdk/cpdfsdk_helpers.h b/fpdfsdk/cpdfsdk_helpers.h index 4e1dc08741..6d214ba9ef 100644 --- a/fpdfsdk/cpdfsdk_helpers.h +++ b/fpdfsdk/cpdfsdk_helpers.h @@ -11,6 +11,7 @@ #include "build/build_config.h" #include "core/fpdfapi/page/cpdf_page.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_parser.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/numerics/safe_conversions.h" @@ -57,6 +58,57 @@ CPDF_Page* CPDFPageFromFPDFPage(FPDF_PAGE page); FPDF_DOCUMENT FPDFDocumentFromCPDFDocument(CPDF_Document* doc); CPDF_Document* CPDFDocumentFromFPDFDocument(FPDF_DOCUMENT doc); +// Typed public-handle doors for APIs that traverse the live PDF graph. Keeping +// the view scope beside the unwrap makes effective-layer resolution structural +// instead of relying on every call site to remember a separate scope. +class ScopedFPDFDocumentView final { + public: + explicit ScopedFPDFDocumentView(FPDF_DOCUMENT document); + ~ScopedFPDFDocumentView(); + + ScopedFPDFDocumentView(const ScopedFPDFDocumentView&) = delete; + ScopedFPDFDocumentView& operator=(const ScopedFPDFDocumentView&) = delete; + + CPDF_Document* Get() const { return document_; } + explicit operator bool() const { return document_ != nullptr; } + + private: + CPDF_Document* const document_; + CPDF_DocumentViewScope view_scope_; +}; + +class ScopedFPDFPageView final { + public: + explicit ScopedFPDFPageView(FPDF_PAGE page); + ~ScopedFPDFPageView(); + + ScopedFPDFPageView(const ScopedFPDFPageView&) = delete; + ScopedFPDFPageView& operator=(const ScopedFPDFPageView&) = delete; + + CPDF_Page* Get() const { return page_; } + explicit operator bool() const { return page_ != nullptr; } + + private: + CPDF_Page* const page_; + CPDF_DocumentViewScope view_scope_; +}; + +class ScopedFPDFAnnotationView final { + public: + explicit ScopedFPDFAnnotationView(FPDF_ANNOTATION annotation); + ~ScopedFPDFAnnotationView(); + + ScopedFPDFAnnotationView(const ScopedFPDFAnnotationView&) = delete; + ScopedFPDFAnnotationView& operator=(const ScopedFPDFAnnotationView&) = delete; + + CPDF_AnnotContext* Get() const { return annotation_; } + explicit operator bool() const { return annotation_ != nullptr; } + + private: + CPDF_AnnotContext* const annotation_; + CPDF_DocumentViewScope view_scope_; +}; + // Conversions to/from incomplete FPDF_ API types. inline FPDF_ACTION FPDFActionFromCPDFDictionary(const CPDF_Dictionary* action) { return reinterpret_cast(const_cast(action)); diff --git a/fpdfsdk/cpdfsdk_pageview.cpp b/fpdfsdk/cpdfsdk_pageview.cpp index 339201caec..c4e8bc395e 100644 --- a/fpdfsdk/cpdfsdk_pageview.cpp +++ b/fpdfsdk/cpdfsdk_pageview.cpp @@ -107,9 +107,6 @@ std::unique_ptr CPDFSDK_PageView::NewAnnot(CPDF_Annot* annot) { auto ret = std::make_unique(annot, this, form); form->AddMap(form_control, ret.get()); - if (pdf_form->NeedConstructAP()) { - ret->ResetAppearance(std::nullopt, CPDFSDK_Widget::kValueUnchanged); - } return ret; } diff --git a/fpdfsdk/cpdfsdk_renderpage.cpp b/fpdfsdk/cpdfsdk_renderpage.cpp index 4def06a07c..39ef4d1cbb 100644 --- a/fpdfsdk/cpdfsdk_renderpage.cpp +++ b/fpdfsdk/cpdfsdk_renderpage.cpp @@ -11,6 +11,7 @@ #include "build/build_config.h" #include "core/fpdfapi/page/cpdf_pageimagecache.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/render/cpdf_pagerendercontext.h" #include "core/fpdfapi/render/cpdf_progressiverenderer.h" #include "core/fpdfapi/render/cpdf_renderoptions.h" @@ -62,7 +63,9 @@ void RenderPageImpl(CPDF_PageRenderContext* context, context->device_->SetBaseClip(clipping_rect); context->device_->SetClip_Rect(clipping_rect); context->context_ = std::make_unique( - pPage->GetDocument(), pPage->GetMutablePageResources(), + pPage->GetDocument(), + pdfium::WrapRetain( + const_cast(pPage->GetPageResources().Get())), pPage->GetPageImageCache()); context->context_->AppendLayer(pPage, matrix); diff --git a/fpdfsdk/epdf_action.cpp b/fpdfsdk/epdf_action.cpp new file mode 100644 index 0000000000..5e6f1b88e0 --- /dev/null +++ b/fpdfsdk/epdf_action.cpp @@ -0,0 +1,554 @@ +// 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/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfdoc/cpdf_action.h" +#include "core/fpdfdoc/cpdf_dest.h" +#include "core/fpdfdoc/cpdf_nametree.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; + RetainPtr destination; + ByteString named_destination; + ByteString uri; + ByteString file_path; + ByteString name; + std::vector next; +}; + +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; +} + +RetainPtr SnapshotDestination(const CPDF_Action& action, + CPDF_Document* document) { + const CPDF_Dictionary* dict = action.GetDict(); + if (!dict) { + return nullptr; + } + + RetainPtr source; + if (document) { + CPDF_Dest destination = action.GetDest(document); + source = pdfium::WrapRetain(destination.GetArray()); + } else { + source = ToArray(dict->GetDirectObjectFor("D")); + } + if (!source) { + return nullptr; + } + + auto snapshot = pdfium::MakeRetain(); + for (size_t i = 0; i < source->size(); ++i) { + RetainPtr value = source->GetDirectObjectAt(i); + if (!value) { + return nullptr; + } + if (i == 0 && value->IsDictionary()) { + // FPDFDest only needs the page dictionary's object number. A minimal + // detached stand-in preserves that identity without retaining the live + // page graph. + RetainPtr page = + snapshot->AppendNew(); + page->SetObjNum(value->GetObjNum()); + continue; + } + RetainPtr clone = value->CloneDirectObject(); + if (!clone) { + return nullptr; + } + snapshot->Append(std::move(clone)); + } + snapshot->Freeze(); + return snapshot; +} + +} // namespace + +struct ActionModelData { + std::vector nodes; + uint32_t warning_flags = 0; +}; + +namespace { + +std::optional AppendAction( + const CPDF_Action& action, + CPDF_Document* document, + 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()); + 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; + } + } + if (node.type == EPDF_ACTION_TYPE_GOTO || + node.type == EPDF_ACTION_TYPE_GOTO_REMOTE || + node.type == EPDF_ACTION_TYPE_GOTO_EMBEDDED) { + node.destination = SnapshotDestination(action, document); + RetainPtr raw_destination = + dict->GetDirectObjectFor("D"); + if (!node.destination && raw_destination && + (raw_destination->IsName() || raw_destination->IsString())) { + node.named_destination = raw_destination->GetString(); + } + } + if (node.type == EPDF_ACTION_TYPE_URI) { + node.uri = + document ? action.GetURI(document) : dict->GetByteStringFor("URI"); + } + if (node.type == EPDF_ACTION_TYPE_GOTO_REMOTE || + node.type == EPDF_ACTION_TYPE_GOTO_EMBEDDED || + node.type == EPDF_ACTION_TYPE_LAUNCH) { + node.file_path = action.GetFilePath().ToUTF8(); + } + if (node.type == EPDF_ACTION_TYPE_NAMED) { + node.name = dict->GetNameFor("N"); + } + + 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, document, 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, + CPDF_Document* document) { + if (!action.HasDict()) { + return nullptr; + } + auto model = std::make_shared(); + size_t javascript_code_units = 0; + std::set active_path; + AppendAction(action, document, 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, + CPDF_Document* document) { + return dictionary ? MakeActionModelHandle( + BuildActionModel(CPDF_Action(std::move(dictionary)), + document)) + : nullptr; +} + +RetainPtr GetPageDictionaryByObjectNumber( + CPDF_Document* doc, + uint32_t page_object_number) { + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!doc || !record) { + 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; + } + if (record->destination) { + return FPDFDestFromCPDFArray(record->destination.Get()); + } + RetainPtr named = + record->named_destination.IsEmpty() + ? nullptr + : CPDF_NameTree::LookupNamedDest(doc, record->named_destination); + return FPDFDestFromCPDFArray(named.Get()); +} + +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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + const epdf::ActionNodeRecord* record = epdf::GetNode(model, node); + if (!doc || !record || record->type != EPDF_ACTION_TYPE_URI) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + record->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) { + 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; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + record->file_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->type != EPDF_ACTION_TYPE_NAMED) { + return 0; + } + // SAFETY: required from caller. + return NulTerminateMaybeCopyAndReturnLength( + record->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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + const CPDF_Dictionary* root = doc ? doc->GetRoot() : nullptr; + return root ? epdf::MakeModelFromDictionary(root->GetDictFor("OpenAction"), + doc) + : 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; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + const CPDF_Dictionary* root = doc ? doc->GetRoot() : nullptr; + RetainPtr additional = + root ? root->GetDictFor("AA") : nullptr; + return additional ? epdf::MakeModelFromDictionary( + additional->GetDictFor(kKeys[event]), doc) + : 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; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + RetainPtr page = + epdf::GetPageDictionaryByObjectNumber(doc, 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), doc) + : 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; + } + ScopedFPDFAnnotationView annotation_view(annotation); + CPDF_AnnotContext* context = annotation_view.Get(); + 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"), context->GetPage()->GetDocument()); + } + 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]), + context->GetPage()->GetDocument()) + : nullptr; +} diff --git a/fpdfsdk/epdf_action_embeddertest.cpp b/fpdfsdk/epdf_action_embeddertest.cpp new file mode 100644 index 0000000000..46e71be8dc --- /dev/null +++ b/fpdfsdk/epdf_action_embeddertest.cpp @@ -0,0 +1,559 @@ +// 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, + NamedJavaScriptModelReadsPromotedActionFromLayer) { + const std::string path = PathService::GetTestFilePath("js.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); + + CPDF_Document* layer_doc = + CPDFDocumentFromFPDFDocument(layer.get()); + ASSERT_TRUE(layer_doc); + RetainPtr action = + ToDictionary(layer_doc->GetMutableIndirectObject(5u)); + ASSERT_TRUE(action); + action->SetNewFor("JS", L"layer();"); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(layer.get(), 5u)); + + ScopedEPDFActionModel model( + EPDFDoc_GetNamedJavaScriptActionModel(layer.get(), 0)); + ASSERT_TRUE(model); + EXPECT_EQ(L"layer();", GetActionJavaScript(model.get(), 0)); + + 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..eea7ef1a24 --- /dev/null +++ b/fpdfsdk/epdf_action_helpers.h @@ -0,0 +1,26 @@ +// 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; +class CPDF_Document; + +namespace epdf { + +struct ActionModelData; +using ActionModelDataPtr = std::shared_ptr; + +ActionModelDataPtr BuildActionModel(const CPDF_Action& action, + CPDF_Document* document = nullptr); +EPDF_ACTION_MODEL MakeActionModelHandle(ActionModelDataPtr data); + +} // namespace epdf + +#endif // FPDFSDK_EPDF_ACTION_HELPERS_H_ diff --git a/fpdfsdk/epdf_base_document.cpp b/fpdfsdk/epdf_base_document.cpp new file mode 100644 index 0000000000..2f62be0c2f --- /dev/null +++ b/fpdfsdk/epdf_base_document.cpp @@ -0,0 +1,94 @@ +// 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/fpdfview.h" + +#include + +#include + +#include "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fxcrt/cfx_read_only_span_stream.h" +#include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "fpdfsdk/cpdfsdk_customaccess.h" +#include "fpdfsdk/cpdfsdk_helpers.h" + +namespace { + +CPDF_BaseDocument* CPDFBaseDocumentFromEPDFBaseDocument( + EPDF_BASE_DOCUMENT base) { + return reinterpret_cast(base); +} + +EPDF_BASE_DOCUMENT EPDFBaseDocumentFromCPDFBaseDocument( + CPDF_BaseDocument* base) { + return reinterpret_cast(base); +} + +EPDF_BASE_DOCUMENT LoadBaseDocumentImpl( + RetainPtr file_access, + FPDF_BYTESTRING password) { + if (!file_access) { + return nullptr; + } + + RetainPtr base = pdfium::MakeRetain(); + CPDF_Parser::Error error = + base->LoadBaseDoc(std::move(file_access), password); + if (error != CPDF_Parser::SUCCESS) { + ProcessParseError(error); + return nullptr; + } + + return EPDFBaseDocumentFromCPDFBaseDocument(base.Leak()); +} + +EPDF_BASE_DOCUMENT LoadMemBaseDocumentImpl(const void* data_buf, + size_t size, + FPDF_BYTESTRING password) { + // SAFETY: required from caller. + auto data_span = + UNSAFE_BUFFERS(pdfium::span(static_cast(data_buf), size)); + return LoadBaseDocumentImpl( + pdfium::MakeRetain(data_span), password); +} + +} // namespace + +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password) { + if (!pFileAccess) { + return nullptr; + } + + return LoadBaseDocumentImpl( + pdfium::MakeRetain(pFileAccess), password); +} + +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadMemBaseDocument(const void* data_buf, + int size, + FPDF_BYTESTRING password) { + if (size < 0) { + return nullptr; + } + return LoadMemBaseDocumentImpl(data_buf, static_cast(size), password); +} + +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadMemBaseDocument64(const void* data_buf, + size_t size, + FPDF_BYTESTRING password) { + return LoadMemBaseDocumentImpl(data_buf, size, password); +} + +FPDF_EXPORT void FPDF_CALLCONV +EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base) { + RetainPtr retained; + retained.Unleak(CPDFBaseDocumentFromEPDFBaseDocument(base)); +} 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..c5d952b81c --- /dev/null +++ b/fpdfsdk/epdf_form.cpp @@ -0,0 +1,3785 @@ +// 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_Document* document, + 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]), + document); + } + } + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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(doc, 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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 = {}; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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 = {}; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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 = {}; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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; + } + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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..06f279db03 --- /dev/null +++ b/fpdfsdk/epdf_form_embeddertest.cpp @@ -0,0 +1,1872 @@ +// 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); +} + +// A promoted non-terminal field is only reachable through frozen /Fields and +// /Kids references. Rebuilding the model must resolve those references through +// the effective layer view so the child's fully qualified name observes the +// promoted ancestor. +TEST_F(EPDFFormEmbedderTest, LayerModelReadsPromotedFieldAncestor) { + LayerDoc doc; + ASSERT_TRUE(OpenLayer("toggle_fields.pdf", &doc)); + + ASSERT_TRUE(EPDFForm_SetFieldName( + doc.layer, 16u, GetFPDFWideString(L"account").get())); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(doc.layer, 16u)); + + EPDF_FORM_MODEL model = EPDFForm_LoadModel(doc.layer); + ASSERT_TRUE(model); + const int field = EPDFForm_GetFieldIndexByObjNum(model, 17u); + ASSERT_GE(field, 0); + EXPECT_EQ(L"account.name", + GetWideString(EPDFForm_GetFieldName, model, field)); + EPDFForm_CloseModel(model); +} + +// 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_layer.cpp b/fpdfsdk/epdf_layer.cpp new file mode 100644 index 0000000000..ed3d881ce1 --- /dev/null +++ b/fpdfsdk/epdf_layer.cpp @@ -0,0 +1,629 @@ +// 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/fpdfview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/fdrm/fx_crypt_sha.h" +#include "core/fpdfapi/edit/cpdf_creator.h" +#include "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_layer_document.h" +#include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span_util.h" +#include "fpdfsdk/cpdfsdk_customaccess.h" +#include "fpdfsdk/cpdfsdk_filewriteadapter.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "public/fpdf_save.h" + +namespace { + +constexpr FX_FILESIZE kReservedDeltaHeadroom = 16 * 1024 * 1024; +constexpr FX_FILESIZE kSafeNotionalStartOffsetMax = + 0xffffffff - kReservedDeltaHeadroom; +constexpr char kLayerArtifactMagic[] = "EPDFLYR1"; +constexpr uint32_t kLayerArtifactVersion = 1; +constexpr size_t kSha256DigestSize = 32; +constexpr size_t kLayerArtifactHeaderSize = + 8 + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t) * 3 + + kSha256DigestSize * 2; + +class OwnedReadOnlyMemoryStream final : public IFX_SeekableReadStream { + public: + CONSTRUCT_VIA_MAKE_RETAIN; + + FX_FILESIZE GetSize() override { + return static_cast(data_.size()); + } + + bool ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) override { + if (offset < 0 || static_cast(offset) > data_.size() || + buffer.size() > data_.size() - static_cast(offset)) { + return false; + } + if (buffer.empty()) { + return true; + } + memcpy(buffer.data(), data_.data() + offset, buffer.size()); + return true; + } + + private: + explicit OwnedReadOnlyMemoryStream(DataVector data) + : data_(std::move(data)) {} + ~OwnedReadOnlyMemoryStream() override = default; + + DataVector data_; +}; + +struct MemoryFileWriter : public FPDF_FILEWRITE { + std::string data; + + MemoryFileWriter() { + version = 1; + WriteBlock = [](FPDF_FILEWRITE* self, const void* buf, + unsigned long size) -> int { + static_cast(self)->data.append( + static_cast(buf), size); + return 1; + }; + } +}; + +struct HashingTempFileWriter : public FPDF_FILEWRITE { + FILE* file = nullptr; + CRYPT_sha2_context sha_context = {}; + uint64_t size = 0; + bool failed = false; + bool finalized = false; + + HashingTempFileWriter() { + version = 1; + file = std::tmpfile(); + CRYPT_SHA256Start(&sha_context); + WriteBlock = [](FPDF_FILEWRITE* self, const void* buf, + unsigned long block_size) -> int { + auto* writer = static_cast(self); + if (!writer || !writer->file || writer->failed || writer->finalized) { + return 0; + } + if (writer->size + block_size < writer->size) { + writer->failed = true; + return 0; + } + if (block_size == 0) { + return 1; + } + const size_t written = fwrite(buf, 1, block_size, writer->file); + if (written != block_size) { + writer->failed = true; + return 0; + } + CRYPT_SHA256Update(&writer->sha_context, + UNSAFE_BUFFERS(pdfium::span( + static_cast(buf), block_size))); + writer->size += block_size; + return 1; + }; + } + + ~HashingTempFileWriter() { + if (file) { + fclose(file); + } + } + + bool IsValid() const { return file && !failed; } + + std::optional> FinishSha256() { + if (!IsValid() || finalized) { + return std::nullopt; + } + if (fflush(file) != 0) { + failed = true; + return std::nullopt; + } + finalized = true; + std::array digest = {}; + CRYPT_SHA256Finish(&sha_context, digest); + return digest; + } + + bool ReplayTo(FPDF_FILEWRITE* out) { + if (!out || !IsValid() || !finalized) { + return false; + } + if (fseek(file, 0, SEEK_SET) != 0) { + return false; + } + + std::array buffer = {}; + uint64_t remaining = size; + while (remaining > 0) { + const size_t chunk_size = + static_cast(std::min(buffer.size(), remaining)); + const size_t read = fread(buffer.data(), 1, chunk_size, file); + if (read != chunk_size) { + return false; + } + if (!out->WriteBlock(out, buffer.data(), + static_cast(chunk_size))) { + return false; + } + remaining -= chunk_size; + } + return true; + } +}; + +CPDF_BaseDocument* CPDFBaseDocumentFromEPDFBaseDocument( + EPDF_BASE_DOCUMENT base) { + return reinterpret_cast(base); +} + +EPDF_BASE_DOCUMENT EPDFBaseDocumentFromCPDFBaseDocument( + CPDF_BaseDocument* base) { + return reinterpret_cast(base); +} + +EPDFLayerOpenStatus ToPublicStatus(CPDF_LayerDocument::OpenStatus status) { + switch (status) { + case CPDF_LayerDocument::OpenStatus::kSuccess: + return EPDFLayerOpenStatus_kSuccess; + case CPDF_LayerDocument::OpenStatus::kMalformedDelta: + return EPDFLayerOpenStatus_kMalformedDelta; + case CPDF_LayerDocument::OpenStatus::kBaseLayerMismatch: + return EPDFLayerOpenStatus_kBaseLayerMismatch; + case CPDF_LayerDocument::OpenStatus::kOpenFailed: + return EPDFLayerOpenStatus_kOpenFailed; + } +} + +void SetOpenStatus(EPDFLayerOpenStatus* out_status, + EPDFLayerOpenStatus status) { + if (out_status) { + *out_status = status; + } +} + +void SetSaveStatus(EPDFLayerSaveStatus* out_status, + EPDFLayerSaveStatus status) { + if (out_status) { + *out_status = status; + } +} + +FPDF_DOCUMENT OpenLayerWithDeltaStream( + EPDF_BASE_DOCUMENT base, + RetainPtr delta_stream, + EPDFLayerOpenStatus* out_status) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kOpenFailed); + if (!base) { + return nullptr; + } + + CPDF_BaseDocument* base_doc = CPDFBaseDocumentFromEPDFBaseDocument(base); + RetainPtr retained_base = pdfium::WrapRetain(base_doc); + auto layer = std::make_unique(std::move(retained_base), + std::move(delta_stream)); + + const EPDFLayerOpenStatus status = ToPublicStatus(layer->ingest_status()); + SetOpenStatus(out_status, status); + if (status != EPDFLayerOpenStatus_kSuccess) { + return nullptr; + } + + return FPDFDocumentFromCPDFDocument(layer.release()); +} + +std::optional> ComputeDeltaSha256( + IFX_SeekableReadStream* stream, + FX_FILESIZE size) { + if (!stream || size < 0) { + return std::nullopt; + } + + CRYPT_sha2_context context; + CRYPT_SHA256Start(&context); + std::array buffer = {}; + FX_FILESIZE offset = 0; + while (offset < size) { + const size_t read_size = static_cast( + std::min(buffer.size(), size - offset)); + if (!stream->ReadBlockAtOffset(pdfium::span(buffer).first(read_size), + offset)) { + return std::nullopt; + } + CRYPT_SHA256Update(&context, pdfium::span(buffer).first(read_size)); + offset += read_size; + } + + std::array digest = {}; + CRYPT_SHA256Finish(&context, digest); + return digest; +} + +void AppendUint32LE(std::vector* buffer, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + buffer->push_back(static_cast(value >> (i * 8))); + } +} + +void AppendUint64LE(std::vector* buffer, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + buffer->push_back(static_cast(value >> (i * 8))); + } +} + +std::vector BuildLayerArtifactHeader( + CPDF_BaseDocument* base_doc, + uint64_t delta_size, + const std::array& delta_sha) { + std::vector artifact; + artifact.reserve(kLayerArtifactHeaderSize); + artifact.insert(artifact.end(), kLayerArtifactMagic, kLayerArtifactMagic + 8); + AppendUint32LE(&artifact, kLayerArtifactVersion); + AppendUint32LE(&artifact, kLayerArtifactHeaderSize); + AppendUint64LE(&artifact, static_cast(base_doc->GetRawBaseSize())); + AppendUint64LE(&artifact, + static_cast(base_doc->GetLayerAppendBaseOffset())); + AppendUint64LE(&artifact, delta_size); + const std::array& base_sha = + base_doc->GetRawBaseSha256(); + artifact.insert(artifact.end(), base_sha.begin(), base_sha.end()); + artifact.insert(artifact.end(), delta_sha.begin(), delta_sha.end()); + return artifact; +} + +bool WriteBytes(FPDF_FILEWRITE* file_write, pdfium::span bytes) { + if (!file_write) { + return false; + } + if (bytes.empty()) { + return true; + } + if (bytes.size() > std::numeric_limits::max()) { + return false; + } + return file_write->WriteBlock(file_write, bytes.data(), + static_cast(bytes.size())); +} + +uint32_t ReadUint32LE(const uint8_t* data) { + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(data[3]) << 24); +} + +uint64_t ReadUint64LE(const uint8_t* data) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(data[i]) << (i * 8); + } + return value; +} + +void* CopyToOwnedBuffer(pdfium::span data, + unsigned long* out_size) { + if (!out_size || data.empty() || + data.size() > std::numeric_limits::max()) { + if (out_size) { + *out_size = 0; + } + return nullptr; + } + + void* buffer = malloc(data.size()); + if (!buffer) { + *out_size = 0; + return nullptr; + } + memcpy(buffer, data.data(), data.size()); + *out_size = static_cast(data.size()); + return buffer; +} + +DataVector ReadStreamToVector(IFX_SeekableReadStream* stream) { + if (!stream || stream->GetSize() < 0 || + !pdfium::IsValueInRangeForNumericType(stream->GetSize())) { + return {}; + } + + const FX_FILESIZE size = stream->GetSize(); + DataVector data(pdfium::checked_cast(size)); + if (!data.empty() && + !stream->ReadBlockAtOffset(pdfium::span(data), /*offset=*/0)) { + return {}; + } + return data; +} + +} // namespace + +FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV +EPDFLayer_OpenLayer(EPDF_BASE_DOCUMENT base, + FPDF_FILEACCESS* pFileAccess, + FPDF_BYTESTRING password, + EPDFLayerOpenStatus* out_status) { + // Slice 7.2 layers share the base parser/security state; password handling is + // already complete when the base is loaded. + (void)password; + + RetainPtr delta_stream = + pFileAccess ? pdfium::MakeRetain(pFileAccess) + : nullptr; + return OpenLayerWithDeltaStream(base, std::move(delta_stream), out_status); +} + +FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV +EPDFLayer_OpenLayerArtifact(EPDF_BASE_DOCUMENT base, + FPDF_FILEACCESS* pFileAccess, + FPDF_BYTESTRING password, + EPDFLayerOpenStatus* out_status) { + (void)password; + SetOpenStatus(out_status, EPDFLayerOpenStatus_kOpenFailed); + if (!base || !pFileAccess) { + return nullptr; + } + + CPDF_BaseDocument* base_doc = CPDFBaseDocumentFromEPDFBaseDocument(base); + if (!base_doc) { + return nullptr; + } + + RetainPtr artifact_stream = + pdfium::MakeRetain(pFileAccess); + DataVector artifact = ReadStreamToVector(artifact_stream.Get()); + if (artifact.size() < kLayerArtifactHeaderSize) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kMalformedDelta); + return nullptr; + } + + const uint8_t* data = artifact.data(); + if (memcmp(data, kLayerArtifactMagic, 8) != 0) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kMalformedDelta); + return nullptr; + } + size_t cursor = 8; + const uint32_t version = ReadUint32LE(data + cursor); + cursor += sizeof(uint32_t); + const uint32_t header_size = ReadUint32LE(data + cursor); + cursor += sizeof(uint32_t); + const uint64_t raw_base_size = ReadUint64LE(data + cursor); + cursor += sizeof(uint64_t); + const uint64_t layer_append_base_offset = ReadUint64LE(data + cursor); + cursor += sizeof(uint64_t); + const uint64_t delta_size = ReadUint64LE(data + cursor); + cursor += sizeof(uint64_t); + const uint8_t* base_sha = data + cursor; + cursor += kSha256DigestSize; + const uint8_t* delta_sha = data + cursor; + cursor += kSha256DigestSize; + + if (version != kLayerArtifactVersion || + header_size != kLayerArtifactHeaderSize || + raw_base_size != static_cast(base_doc->GetRawBaseSize()) || + layer_append_base_offset != + static_cast(base_doc->GetLayerAppendBaseOffset()) || + delta_size > artifact.size() - header_size) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kBaseLayerMismatch); + return nullptr; + } + if (header_size + delta_size != artifact.size()) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kMalformedDelta); + return nullptr; + } + + if (memcmp(base_doc->GetRawBaseSha256().data(), base_sha, + kSha256DigestSize) != 0) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kBaseLayerMismatch); + return nullptr; + } + + DataVector delta; + delta.resize(static_cast(delta_size)); + if (!delta.empty()) { + memcpy(delta.data(), artifact.data() + header_size, delta.size()); + } + std::optional> actual_delta_sha = + ComputeDeltaSha256( + pdfium::MakeRetain(delta).Get(), + delta.size()); + if (!actual_delta_sha || + memcmp(actual_delta_sha->data(), delta_sha, kSha256DigestSize) != 0) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kMalformedDelta); + return nullptr; + } + + return OpenLayerWithDeltaStream( + base, pdfium::MakeRetain(std::move(delta)), + out_status); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_IsObjectPromoted(FPDF_DOCUMENT layer, unsigned long obj_num) { + if (obj_num > std::numeric_limits::max()) { + return false; + } + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + return layer_doc && + layer_doc->IsObjectPromoted(static_cast(obj_num)); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFLayer_GetPromotedObjectCount(FPDF_DOCUMENT layer) { + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + return layer_doc ? layer_doc->GetPromotedObjectCount() : 0; +} + +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDFLayer_GetBaseDocument(FPDF_DOCUMENT layer) { + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + return layer_doc ? EPDFBaseDocumentFromCPDFBaseDocument( + layer_doc->GetBaseDocument()) + : nullptr; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveDelta(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSaveFailed); + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + if (!layer_doc || !file_write) { + return false; + } + + if (layer_doc->GetPromotedObjectCount() == 0) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return true; + } + + if (!layer_doc->GetParser()) { + return false; + } + if (layer_doc->GetLayerAppendBaseOffset() > kSafeNotionalStartOffsetMax) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge); + return false; + } + + CPDF_Creator creator( + layer_doc, pdfium::MakeRetain(file_write)); + const bool ok = + creator.Create(Mask( + CPDF_Creator::CreateFlags::kIncremental, + CPDF_Creator::CreateFlags::kIncrementalAppendOnly), + /*file_version=*/0); + if (ok) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return true; + } + + if (creator.GetFailureReason() == + CPDF_Creator::FailureReason::kAppendOnlyOffsetTooLarge) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge); + } + return false; +} + +FPDF_EXPORT void* FPDF_CALLCONV +EPDFLayer_SaveDeltaToOwnedBuffer(FPDF_DOCUMENT layer, + unsigned long* out_size, + EPDFLayerSaveStatus* out_status) { + if (out_size) { + *out_size = 0; + } + MemoryFileWriter writer; + if (!EPDFLayer_SaveDelta(layer, &writer, out_status) || writer.data.empty()) { + return nullptr; + } + return CopyToOwnedBuffer(pdfium::as_byte_span(writer.data), out_size); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveLayerArtifact(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSaveFailed); + if (!file_write) { + return false; + } + + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + CPDF_BaseDocument* base_doc = + layer_doc ? layer_doc->GetBaseDocument() : nullptr; + if (!layer_doc || !base_doc) { + return false; + } + + HashingTempFileWriter delta_writer; + if (!delta_writer.IsValid()) { + return false; + } + + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + if (!EPDFLayer_SaveDelta(layer, &delta_writer, &save_status)) { + SetSaveStatus(out_status, save_status); + return false; + } + + std::optional> delta_sha = + delta_writer.FinishSha256(); + if (!delta_sha) { + return false; + } + + const std::vector header = + BuildLayerArtifactHeader(base_doc, delta_writer.size, *delta_sha); + if (!WriteBytes(file_write, pdfium::span(header)) || + !delta_writer.ReplayTo(file_write)) { + return false; + } + + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return true; +} + +FPDF_EXPORT void* FPDF_CALLCONV +EPDFLayer_SaveLayerArtifactToOwnedBuffer(FPDF_DOCUMENT layer, + unsigned long* out_size, + EPDFLayerSaveStatus* out_status) { + if (out_size) { + *out_size = 0; + } + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSaveFailed); + + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + CPDF_BaseDocument* base_doc = + layer_doc ? layer_doc->GetBaseDocument() : nullptr; + if (!layer_doc || !base_doc) { + return nullptr; + } + + MemoryFileWriter delta_writer; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + if (!EPDFLayer_SaveDelta(layer, &delta_writer, &save_status)) { + SetSaveStatus(out_status, save_status); + return nullptr; + } + + const DataVector delta_bytes(delta_writer.data.begin(), + delta_writer.data.end()); + std::optional> delta_sha = + ComputeDeltaSha256( + pdfium::MakeRetain(delta_bytes).Get(), + delta_bytes.size()); + if (!delta_sha) { + return nullptr; + } + + std::vector artifact = BuildLayerArtifactHeader( + base_doc, static_cast(delta_writer.data.size()), *delta_sha); + artifact.reserve(kLayerArtifactHeaderSize + delta_writer.data.size()); + artifact.insert(artifact.end(), delta_writer.data.begin(), + delta_writer.data.end()); + + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return CopyToOwnedBuffer(pdfium::span(artifact), out_size); +} diff --git a/fpdfsdk/epdf_page_content_helpers.cpp b/fpdfsdk/epdf_page_content_helpers.cpp new file mode 100644 index 0000000000..857a65f544 --- /dev/null +++ b/fpdfsdk/epdf_page_content_helpers.cpp @@ -0,0 +1,66 @@ +// 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 "fpdfsdk/epdf_page_content_helpers.h" + +#include + +#include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_formobject.h" +#include "core/fpdfapi/page/cpdf_page.h" +#include "core/fpdfapi/page/cpdf_pageobject.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_stream.h" + +void EpdfAppendFormXObjectToPage(CPDF_Page* page, + RetainPtr form_stream, + const CFX_FloatRect& target_rect) { + if (!page || !form_stream) { + return; + } + + CPDF_Document* doc = page->GetDocument(); + if (!doc) { + return; + } + + RetainPtr form_dict = form_stream->GetDict(); + if (!form_dict) { + return; + } + + CFX_FloatRect form_bbox = form_dict->GetRectFor("BBox"); + form_bbox.Normalize(); + if (form_bbox.IsEmpty()) { + form_bbox = target_rect; + } + + float scale_x = 1.0f; + float scale_y = 1.0f; + if (form_bbox.Width() > 0) { + scale_x = target_rect.Width() / form_bbox.Width(); + } + if (form_bbox.Height() > 0) { + scale_y = target_rect.Height() / form_bbox.Height(); + } + + CFX_Matrix form_matrix; + form_matrix.a = scale_x; + form_matrix.d = scale_y; + form_matrix.e = target_rect.left - form_bbox.left * scale_x; + form_matrix.f = target_rect.bottom - form_bbox.bottom * scale_y; + + auto form = std::make_unique( + doc, page->GetMutableResources(), + pdfium::WrapRetain(const_cast(form_stream.Get()))); + form->ParseContent(); + + auto form_obj = std::make_unique( + CPDF_PageObject::kNoContentStream, std::move(form), form_matrix); + + form_obj->CalcBoundingBox(); + form_obj->SetDirty(true); + page->AppendPageObject(std::move(form_obj)); +} diff --git a/fpdfsdk/epdf_page_content_helpers.h b/fpdfsdk/epdf_page_content_helpers.h new file mode 100644 index 0000000000..bf49df5101 --- /dev/null +++ b/fpdfsdk/epdf_page_content_helpers.h @@ -0,0 +1,18 @@ +// 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_PAGE_CONTENT_HELPERS_H_ +#define FPDFSDK_EPDF_PAGE_CONTENT_HELPERS_H_ + +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/retain_ptr.h" + +class CPDF_Page; +class CPDF_Stream; + +void EpdfAppendFormXObjectToPage(CPDF_Page* page, + RetainPtr form_stream, + const CFX_FloatRect& target_rect); + +#endif // FPDFSDK_EPDF_PAGE_CONTENT_HELPERS_H_ 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 new file mode 100644 index 0000000000..92b2fde9b4 --- /dev/null +++ b/fpdfsdk/epdf_redact.cpp @@ -0,0 +1,533 @@ +// 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_redact.h" + +#include +#include +#include +#include + +#include "constants/annotation_common.h" +#include "core/fpdfapi/edit/cpdf_text_redactor.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_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/fpdfdoc/cpdf_generateap.h" +#include "core/fpdfdoc/cpdf_interactiveform.h" +#include "core/fxcrt/bytestring.h" +#include "core/fxcrt/containers/contains.h" +#include "core/fxcrt/numerics/safe_conversions.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_page_content_helpers.h" + +namespace { + +const CPDF_Dictionary* GetAnnotDictFromFPDFAnnotation( + const FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + return context ? context->GetAnnotDict() : nullptr; +} + +std::vector GetRedactRectsFromAnnotDict( + const CPDF_Dictionary* annot_dict) { + std::vector rects; + if (!annot_dict) { + return rects; + } + + RetainPtr quad_points_array = + annot_dict->GetArrayFor("QuadPoints"); + if (quad_points_array && quad_points_array->size() >= 8) { + 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()) { + rects.push_back(rect); + } + } + if (!rects.empty()) { + return rects; + } + } + + CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); + rect.Normalize(); + if (!rect.IsEmpty()) { + rects.push_back(rect); + } + + return rects; +} + +struct RemovedAnnotCandidate { + size_t index = 0; + uint32_t object_number = 0; + RetainPtr dict; +}; + +uint32_t GetAnnotObjectNumber(const CPDF_Object* entry, + const CPDF_Dictionary* dict) { + if (entry && entry->IsReference()) { + return entry->AsReference()->GetRefObjNum(); + } + return dict ? dict->GetObjNum() : 0; +} + +bool RectsIntersectWithPositiveArea(CFX_FloatRect a, CFX_FloatRect b) { + a.Normalize(); + b.Normalize(); + a.Intersect(b); + return !a.IsEmpty(); +} + +bool AnnotIntersectsAny(const CPDF_Dictionary* annot_dict, + pdfium::span redact_rects) { + if (!annot_dict) { + return false; + } + CFX_FloatRect annot_rect = + annot_dict->GetRectFor(pdfium::annotation::kRect); + annot_rect.Normalize(); + if (annot_rect.IsEmpty()) { + return false; + } + for (const CFX_FloatRect& redact_rect : redact_rects) { + if (RectsIntersectWithPositiveArea(annot_rect, redact_rect)) { + return true; + } + } + return false; +} + +bool CandidateExistsAtIndex( + const std::vector& candidates, + size_t index) { + return pdfium::Contains(candidates, index, &RemovedAnnotCandidate::index); +} + +bool AddRemovalCandidate(CPDF_Page* page, + size_t index, + std::vector* candidates) { + if (!page || !candidates || CandidateExistsAtIndex(*candidates, index)) { + return false; + } + RetainPtr annots = page->GetMutableAnnotsArray(); + if (!annots || index >= annots->size()) { + return false; + } + RetainPtr entry = annots->GetMutableObjectAt(index); + RetainPtr dict = + ToDictionary(entry ? entry->GetMutableDirect() : nullptr); + if (!dict) { + return false; + } + + RemovedAnnotCandidate candidate; + candidate.index = index; + candidate.object_number = GetAnnotObjectNumber(entry.Get(), dict.Get()); + candidate.dict = std::move(dict); + candidates->push_back(std::move(candidate)); + return true; +} + +int FindAnnotIndexOnPageByObjNumOrDict(const CPDF_Page* page, + const CPDF_Dictionary* annot_dict) { + if (!page || !annot_dict) { + return -1; + } + RetainPtr annots = page->GetAnnotsArray(); + if (!annots) { + return -1; + } + const uint32_t target_objnum = annot_dict->GetObjNum(); + for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr current = annots->GetDictAt(i); + if (!current) { + continue; + } + if (current.Get() == annot_dict || + (target_objnum != 0 && current->GetObjNum() == target_objnum)) { + return static_cast(i); + } + } + return -1; +} + +void AddPopupCascade(CPDF_Page* page, + std::vector* candidates) { + if (!page || !candidates) { + return; + } + for (size_t i = 0; i < candidates->size(); ++i) { + RetainPtr popup = + candidates->at(i).dict ? candidates->at(i).dict->GetDictFor("Popup") + : nullptr; + if (!popup) { + continue; + } + const int popup_index = + FindAnnotIndexOnPageByObjNumOrDict(page, popup.Get()); + if (popup_index >= 0) { + AddRemovalCandidate(page, static_cast(popup_index), candidates); + } + } +} + +bool RemoveDictFromArray(CPDF_Array* array, const CPDF_Dictionary* dict) { + if (!array || !dict) { + return false; + } + bool removed = false; + const uint32_t objnum = dict->GetObjNum(); + for (size_t i = array->size(); i > 0; --i) { + RetainPtr item = array->GetDictAt(i - 1); + if (item && + (item.Get() == dict || (objnum != 0 && item->GetObjNum() == objnum))) { + array->RemoveAt(i - 1); + removed = true; + } + } + return removed; +} + +RetainPtr GetAcroFormFields(CPDF_Document* doc) { + if (!doc) { + return nullptr; + } + RetainPtr root = doc->GetMutableRoot(); + if (!root) { + return nullptr; + } + RetainPtr acro_form = root->GetMutableDictFor("AcroForm"); + return acro_form ? acro_form->GetMutableArrayFor("Fields") : nullptr; +} + +bool DetachWidgetFromAcroForm(CPDF_Document* doc, + CPDF_Dictionary* widget_dict) { + if (!doc || !widget_dict || + widget_dict->GetNameFor(pdfium::annotation::kSubtype) != "Widget") { + return false; + } + + bool changed = false; + RetainPtr parent = widget_dict->GetMutableDictFor("Parent"); + if (!parent) { + RetainPtr fields = GetAcroFormFields(doc); + return fields ? RemoveDictFromArray(fields.Get(), widget_dict) : false; + } + + RetainPtr kids = parent->GetMutableArrayFor("Kids"); + if (kids) { + changed |= RemoveDictFromArray(kids.Get(), widget_dict); + } + + RetainPtr current = parent; + while (current) { + RetainPtr current_kids = current->GetMutableArrayFor("Kids"); + if (current_kids && !current_kids->IsEmpty()) { + break; + } + + RetainPtr next_parent = + current->GetMutableDictFor("Parent"); + if (next_parent) { + RetainPtr parent_kids = + next_parent->GetMutableArrayFor("Kids"); + if (parent_kids) { + changed |= RemoveDictFromArray(parent_kids.Get(), current.Get()); + } + current = std::move(next_parent); + continue; + } + + RetainPtr fields = GetAcroFormFields(doc); + if (fields) { + changed |= RemoveDictFromArray(fields.Get(), current.Get()); + } + break; + } + + return changed; +} + +void DetachWidgetsFromAcroForm( + CPDF_Page* page, + const std::vector& candidates) { + if (!page) { + return; + } + CPDF_Document* doc = page->GetDocument(); + if (!doc) { + return; + } + bool changed = false; + for (const RemovedAnnotCandidate& candidate : candidates) { + if (candidate.dict && + candidate.dict->GetNameFor(pdfium::annotation::kSubtype) == "Widget") { + changed |= DetachWidgetFromAcroForm(doc, candidate.dict.Get()); + } + } + if (changed) { + CPDF_InteractiveForm form(doc); + form.FixPageFields(page); + } +} + +// 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; + } + } + return count; +} + +void RemoveCandidatesFromPage( + CPDF_Page* page, + const std::vector& candidates) { + if (!page) { + return; + } + RetainPtr annots = page->GetMutableAnnotsArray(); + if (!annots) { + return; + } + CPDF_Document* doc = page->GetDocument(); + for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) { + if (it->index >= annots->size()) { + continue; + } + annots->RemoveAt(it->index); + if (doc && it->object_number) { + doc->DeleteIndirectObject(it->object_number); + } + } +} + +void SortCandidatesByOriginalIndex( + std::vector* candidates) { + std::sort(candidates->begin(), candidates->end(), + [](const RemovedAnnotCandidate& a, + 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, + 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; + } + + std::vector removals; + RetainPtr annots = page->GetMutableAnnotsArray(); + if (annots) { + for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr entry = annots->GetMutableObjectAt(i); + RetainPtr annot_dict = + ToDictionary(entry ? entry->GetMutableDirect() : nullptr); + if (!annot_dict) { + continue; + } + if (annot_dict->GetNameFor(pdfium::annotation::kSubtype) == "Redact") { + continue; + } + if (AnnotIntersectsAny(annot_dict.Get(), pdfium::span(rects))) { + AddRemovalCandidate(page, i, &removals); + } + } + + AddPopupCascade(page, &removals); + + const int redact_index = + FindAnnotIndexOnPageByObjNumOrDict(page, redact_dict); + if (redact_index >= 0) { + AddRemovalCandidate(page, static_cast(redact_index), &removals); + } + } + + SortCandidatesByOriginalIndex(&removals); + + RedactTextInRects(page, pdfium::span(rects), + /*recurse_forms=*/true, + /*draw_black_boxes=*/false); + + RetainPtr overlay = + ResolveRedactOverlay(page->GetDocument(), redact_dict); + if (overlay) { + CFX_FloatRect annot_rect = + redact_dict->GetRectFor(pdfium::annotation::kRect); + annot_rect.Normalize(); + EpdfAppendFormXObjectToPage(page, overlay, annot_rect); + } + + DetachWidgetsFromAcroForm(page, removals); + if (out_removed_annot_count) { + *out_removed_annot_count = CountRemovedNonRedactAnnots(removals); + } + RemoveCandidatesFromPage(page, removals); + return true; +} + +bool ApplyAllRedactionsCore(CPDF_Page* page, + 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; + } + + std::vector all_rects; + std::vector, CFX_FloatRect>> + overlays; + std::vector removals; + + for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr entry = annots->GetMutableObjectAt(i); + RetainPtr annot_dict = + ToDictionary(entry ? entry->GetMutableDirect() : nullptr); + if (!annot_dict || + annot_dict->GetNameFor(pdfium::annotation::kSubtype) != "Redact") { + continue; + } + + AddRemovalCandidate(page, i, &removals); + + std::vector rects = + GetRedactRectsFromAnnotDict(annot_dict.Get()); + for (const CFX_FloatRect& rect : rects) { + all_rects.push_back(rect); + } + + RetainPtr overlay = + ResolveRedactOverlay(page->GetDocument(), annot_dict.Get()); + if (overlay) { + CFX_FloatRect annot_rect = + annot_dict->GetRectFor(pdfium::annotation::kRect); + annot_rect.Normalize(); + overlays.push_back({std::move(overlay), annot_rect}); + } + } + + if (all_rects.empty()) { + return false; + } + + for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr entry = annots->GetMutableObjectAt(i); + RetainPtr annot_dict = + ToDictionary(entry ? entry->GetMutableDirect() : nullptr); + if (!annot_dict) { + continue; + } + if (annot_dict->GetNameFor(pdfium::annotation::kSubtype) == "Redact") { + continue; + } + if (AnnotIntersectsAny(annot_dict.Get(), pdfium::span(all_rects))) { + AddRemovalCandidate(page, i, &removals); + } + } + + AddPopupCascade(page, &removals); + SortCandidatesByOriginalIndex(&removals); + + RedactTextInRects(page, pdfium::span(all_rects), + /*recurse_forms=*/true, + /*draw_black_boxes=*/false); + + for (const auto& [overlay, annot_rect] : overlays) { + EpdfAppendFormXObjectToPage(page, overlay, annot_rect); + } + + DetachWidgetsFromAcroForm(page, removals); + if (out_removed_annot_count) { + *out_removed_annot_count = CountRemovedNonRedactAnnots(removals); + } + RemoveCandidatesFromPage(page, removals); + return true; +} + +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +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); + if (!pPage) { + return false; + } + + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict || + annot_dict->GetNameFor(pdfium::annotation::kSubtype) != "Redact") { + return false; + } + + return ApplySingleRedactionCore(pPage, annot_dict, out_removed_annot_count); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +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); + if (!pPage) { + return false; + } + + return ApplyAllRedactionsCore(pPage, out_removed_annot_count); +} diff --git a/fpdfsdk/epdf_threading.cpp b/fpdfsdk/epdf_threading.cpp new file mode 100644 index 0000000000..91fff70f02 --- /dev/null +++ b/fpdfsdk/epdf_threading.cpp @@ -0,0 +1,30 @@ +// 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: thread-confined runtime lifecycle. +// +// EPDF_InitThread / EPDF_ShutdownThread make the per-thread PDFium lifecycle +// explicit for callers that run PDFium on worker threads. With +// embedpdf_thread_local_globals enabled (see core/fxcrt/epdf_tls.h) each worker +// thread owns its own PDFium globals, so every such thread must initialize and +// tear down PDFium itself. With the flag disabled these are exact aliases of +// FPDF_InitLibrary / FPDF_DestroyLibrary. +// +// These are intentionally exported for ALL targets (including wasm, where they +// simply wrap the normal init/destroy) so shared runtime code can call a single +// lifecycle entry point regardless of platform. + +#include "public/fpdfview.h" + +FPDF_EXPORT void FPDF_CALLCONV EPDF_InitThread() { + // Routes through FPDF_InitLibrary so the standard config/init path runs for + // the calling thread. + FPDF_InitLibrary(); +} + +FPDF_EXPORT void FPDF_CALLCONV EPDF_ShutdownThread() { + // Lifecycle-strict: callers must have already closed every PDFium handle + // created on this thread before invoking this. + FPDF_DestroyLibrary(); +} diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 5603622e10..a474427cd8 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,20 +16,20 @@ #include "constants/annotation_common.h" #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" -#include "core/fpdfapi/edit/cpdf_pageorganizer.h" #include "core/fpdfapi/edit/cpdf_pagecontentgenerator.h" -#include "core/fpdfapi/edit/cpdf_text_redactor.h" +#include "core/fpdfapi/edit/cpdf_pageorganizer.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" -#include "core/fpdfapi/page/cpdf_formobject.h" #include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_formobject.h" +#include "core/fpdfapi/page/cpdf_image.h" #include "core/fpdfapi/page/cpdf_imageobject.h" -#include "core/fpdfapi/page/cpdf_image.h" #include "core/fpdfapi/page/cpdf_page.h" #include "core/fpdfapi/page/cpdf_pageobject.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_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" @@ -50,6 +51,7 @@ #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" @@ -213,57 +215,51 @@ static_assert(static_cast(CPDF_Annot::BorderStyle::kUnknown) == FPDF_ANNOT_BS_UNKNOWN, "CPDF_Annot::BorderStyle::kUnknown value mismatch"); -// These checks ensure the consistency of blend mode values across core/ and public. -static_assert(static_cast(BlendMode::kNormal) == - FPDF_BLENDMODE_Normal, +// These checks ensure the consistency of blend mode values across core/ and +// public. +static_assert(static_cast(BlendMode::kNormal) == FPDF_BLENDMODE_Normal, "BlendMode::kNormal value mismatch"); -static_assert(static_cast(BlendMode::kMultiply) == - FPDF_BLENDMODE_Multiply, +static_assert(static_cast(BlendMode::kMultiply) == FPDF_BLENDMODE_Multiply, "BlendMode::kMultiply value mismatch"); -static_assert(static_cast(BlendMode::kScreen) == - FPDF_BLENDMODE_Screen, +static_assert(static_cast(BlendMode::kScreen) == FPDF_BLENDMODE_Screen, "BlendMode::kScreen value mismatch"); -static_assert(static_cast(BlendMode::kOverlay) == - FPDF_BLENDMODE_Overlay, +static_assert(static_cast(BlendMode::kOverlay) == FPDF_BLENDMODE_Overlay, "BlendMode::kOverlay value mismatch"); -static_assert(static_cast(BlendMode::kDarken) == - FPDF_BLENDMODE_Darken, +static_assert(static_cast(BlendMode::kDarken) == FPDF_BLENDMODE_Darken, "BlendMode::kDarken value mismatch"); -static_assert(static_cast(BlendMode::kLighten) == - FPDF_BLENDMODE_Lighten, +static_assert(static_cast(BlendMode::kLighten) == FPDF_BLENDMODE_Lighten, "BlendMode::kLighten value mismatch"); -static_assert(static_cast(BlendMode::kColorDodge) == - FPDF_BLENDMODE_ColorDodge, +static_assert(static_cast(BlendMode::kColorDodge) == + FPDF_BLENDMODE_ColorDodge, "BlendMode::kColorDodge value mismatch"); -static_assert(static_cast(BlendMode::kColorBurn) == - FPDF_BLENDMODE_ColorBurn, +static_assert(static_cast(BlendMode::kColorBurn) == + FPDF_BLENDMODE_ColorBurn, "BlendMode::kColorBurn value mismatch"); -static_assert(static_cast(BlendMode::kHardLight) == - FPDF_BLENDMODE_HardLight, - "BlendMode::kHardLight value mismatch"); -static_assert(static_cast(BlendMode::kSoftLight) == - FPDF_BLENDMODE_SoftLight, +static_assert(static_cast(BlendMode::kHardLight) == + FPDF_BLENDMODE_HardLight, + "BlendMode::kHardLight value mismatch"); +static_assert(static_cast(BlendMode::kSoftLight) == + FPDF_BLENDMODE_SoftLight, "BlendMode::kSoftLight value mismatch"); -static_assert(static_cast(BlendMode::kDifference) == - FPDF_BLENDMODE_Difference, +static_assert(static_cast(BlendMode::kDifference) == + FPDF_BLENDMODE_Difference, "BlendMode::kDifference value mismatch"); -static_assert(static_cast(BlendMode::kExclusion) == - FPDF_BLENDMODE_Exclusion, +static_assert(static_cast(BlendMode::kExclusion) == + FPDF_BLENDMODE_Exclusion, "BlendMode::kExclusion value mismatch"); -static_assert(static_cast(BlendMode::kHue) == - FPDF_BLENDMODE_Hue, - "BlendMode::kHue value mismatch"); -static_assert(static_cast(BlendMode::kSaturation) == - FPDF_BLENDMODE_Saturation, +static_assert(static_cast(BlendMode::kHue) == FPDF_BLENDMODE_Hue, + "BlendMode::kHue value mismatch"); +static_assert(static_cast(BlendMode::kSaturation) == + FPDF_BLENDMODE_Saturation, "BlendMode::kSaturation value mismatch"); -static_assert(static_cast(BlendMode::kColor) == - FPDF_BLENDMODE_Color, +static_assert(static_cast(BlendMode::kColor) == FPDF_BLENDMODE_Color, "BlendMode::kColor value mismatch"); -static_assert(static_cast(BlendMode::kLuminosity) == - FPDF_BLENDMODE_Luminosity, +static_assert(static_cast(BlendMode::kLuminosity) == + FPDF_BLENDMODE_Luminosity, "BlendMode::kLuminosity value mismatch"); -// These checks ensure the consistency of line ending values across core/ and public. +// These checks ensure the consistency of line ending values across core/ and +// public. static_assert(static_cast(CPDF_Annot::LineEnding::kNone) == FPDF_ANNOT_LE_None, "LineEnding::kNone mismatch"); @@ -298,159 +294,163 @@ static_assert(static_cast(CPDF_Annot::LineEnding::kUnknown) == FPDF_ANNOT_LE_Unknown, "LineEnding::kUnknown mismatch"); -// These checks ensure the consistency of standard font values across core/ and public. -static_assert(static_cast(CPDF_Annot::StandardFont::kCourier) == - FPDF_FONT_COURIER, +// These checks ensure the consistency of standard font values across core/ and +// public. +static_assert(static_cast(CPDF_Annot::StandardFont::kCourier) == + FPDF_FONT_COURIER, "CPDF_Annot::StandardFont::kCourier mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kCourier_Bold) == - FPDF_FONT_COURIER_BOLD, +static_assert(static_cast(CPDF_Annot::StandardFont::kCourier_Bold) == + FPDF_FONT_COURIER_BOLD, "CPDF_Annot::StandardFont::kCourier_Bold mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kCourier_BoldOblique) == - FPDF_FONT_COURIER_BOLDITALIC, - "CPDF_Annot::StandardFont::kCourier_BoldOblique mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kCourier_Oblique) == - FPDF_FONT_COURIER_ITALIC, +static_assert( + static_cast(CPDF_Annot::StandardFont::kCourier_BoldOblique) == + FPDF_FONT_COURIER_BOLDITALIC, + "CPDF_Annot::StandardFont::kCourier_BoldOblique mismatch"); +static_assert(static_cast(CPDF_Annot::StandardFont::kCourier_Oblique) == + FPDF_FONT_COURIER_ITALIC, "CPDF_Annot::StandardFont::kCourier_Oblique mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica) == - FPDF_FONT_HELVETICA, +static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica) == + FPDF_FONT_HELVETICA, "CPDF_Annot::StandardFont::kHelvetica mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica_Bold) == - FPDF_FONT_HELVETICA_BOLD, +static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica_Bold) == + FPDF_FONT_HELVETICA_BOLD, "CPDF_Annot::StandardFont::kHelvetica_Bold mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica_BoldOblique) == - FPDF_FONT_HELVETICA_BOLDITALIC, - "CPDF_Annot::StandardFont::kHelvetica_BoldOblique mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica_Oblique) == - FPDF_FONT_HELVETICA_ITALIC, +static_assert( + static_cast(CPDF_Annot::StandardFont::kHelvetica_BoldOblique) == + FPDF_FONT_HELVETICA_BOLDITALIC, + "CPDF_Annot::StandardFont::kHelvetica_BoldOblique mismatch"); +static_assert(static_cast(CPDF_Annot::StandardFont::kHelvetica_Oblique) == + FPDF_FONT_HELVETICA_ITALIC, "CPDF_Annot::StandardFont::kHelvetica_Oblique mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Roman) == - FPDF_FONT_TIMES_ROMAN, +static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Roman) == + FPDF_FONT_TIMES_ROMAN, "CPDF_Annot::StandardFont::kTimes_Roman mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Bold) == - FPDF_FONT_TIMES_BOLD, +static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Bold) == + FPDF_FONT_TIMES_BOLD, "CPDF_Annot::StandardFont::kTimes_Bold mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_BoldItalic) == - FPDF_FONT_TIMES_BOLDITALIC, +static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_BoldItalic) == + FPDF_FONT_TIMES_BOLDITALIC, "CPDF_Annot::StandardFont::kTimes_BoldItalic mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Italic) == - FPDF_FONT_TIMES_ITALIC, +static_assert(static_cast(CPDF_Annot::StandardFont::kTimes_Italic) == + FPDF_FONT_TIMES_ITALIC, "CPDF_Annot::StandardFont::kTimes_Italic mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kSymbol) == - FPDF_FONT_SYMBOL, +static_assert(static_cast(CPDF_Annot::StandardFont::kSymbol) == + FPDF_FONT_SYMBOL, "CPDF_Annot::StandardFont::kSymbol mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kZapfDingbats) == - FPDF_FONT_ZAPFDINGBATS, +static_assert(static_cast(CPDF_Annot::StandardFont::kZapfDingbats) == + FPDF_FONT_ZAPFDINGBATS, "CPDF_Annot::StandardFont::kZapfDingbats mismatch"); -static_assert(static_cast(CPDF_Annot::StandardFont::kUnknown) == - FPDF_FONT_UNKNOWN, +static_assert(static_cast(CPDF_Annot::StandardFont::kUnknown) == + FPDF_FONT_UNKNOWN, "CPDF_Annot::StandardFont::kUnknown mismatch"); // These checks ensure consistency between the public API and internal enums. -static_assert(static_cast(CPDF_Annot::TextAlignment::kLeft) == - FPDF_TEXT_ALIGNMENT_LEFT, +static_assert(static_cast(CPDF_Annot::TextAlignment::kLeft) == + FPDF_TEXT_ALIGNMENT_LEFT, "CPDF_Annot::TextAlignment::kLeft mismatch"); -static_assert(static_cast(CPDF_Annot::TextAlignment::kCenter) == - FPDF_TEXT_ALIGNMENT_CENTER, +static_assert(static_cast(CPDF_Annot::TextAlignment::kCenter) == + FPDF_TEXT_ALIGNMENT_CENTER, "CPDF_Annot::TextAlignment::kCenter mismatch"); -static_assert(static_cast(CPDF_Annot::TextAlignment::kRight) == - FPDF_TEXT_ALIGNMENT_RIGHT, +static_assert(static_cast(CPDF_Annot::TextAlignment::kRight) == + FPDF_TEXT_ALIGNMENT_RIGHT, "CPDF_Annot::TextAlignment::kRight mismatch"); -// These checks ensure the consistency of vertical alignment values across core/ and public. -static_assert(static_cast(CPDF_Annot::VerticalAlignment::kTop) == - FPDF_VERTICAL_ALIGNMENT_TOP, +// These checks ensure the consistency of vertical alignment values across core/ +// and public. +static_assert(static_cast(CPDF_Annot::VerticalAlignment::kTop) == + FPDF_VERTICAL_ALIGNMENT_TOP, "CPDF_Annot::VerticalAlignment::kTop mismatch"); -static_assert(static_cast(CPDF_Annot::VerticalAlignment::kMiddle) == - FPDF_VERTICAL_ALIGNMENT_MIDDLE, +static_assert(static_cast(CPDF_Annot::VerticalAlignment::kMiddle) == + FPDF_VERTICAL_ALIGNMENT_MIDDLE, "CPDF_Annot::VerticalAlignment::kMiddle mismatch"); -static_assert(static_cast(CPDF_Annot::VerticalAlignment::kBottom) == - FPDF_VERTICAL_ALIGNMENT_BOTTOM, +static_assert(static_cast(CPDF_Annot::VerticalAlignment::kBottom) == + FPDF_VERTICAL_ALIGNMENT_BOTTOM, "CPDF_Annot::VerticalAlignment::kBottom mismatch"); // These checks ensure the consistency of icon values across core/ and public. static_assert(static_cast(CPDF_Annot::Icon::kUnknown) == - FPDF_ANNOT_NAME_UNKNOWN, + FPDF_ANNOT_NAME_UNKNOWN, "Icon::kUnknown mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Comment) == - FPDF_ANNOT_NAME_Text_Comment, +static_assert(static_cast(CPDF_Annot::Icon::kText_Comment) == + FPDF_ANNOT_NAME_Text_Comment, "Icon::kText_Comment mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Key) == - FPDF_ANNOT_NAME_Text_Key, +static_assert(static_cast(CPDF_Annot::Icon::kText_Key) == + FPDF_ANNOT_NAME_Text_Key, "Icon::kText_Key mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Note) == - FPDF_ANNOT_NAME_Text_Note, +static_assert(static_cast(CPDF_Annot::Icon::kText_Note) == + FPDF_ANNOT_NAME_Text_Note, "Icon::kText_Note mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Help) == - FPDF_ANNOT_NAME_Text_Help, +static_assert(static_cast(CPDF_Annot::Icon::kText_Help) == + FPDF_ANNOT_NAME_Text_Help, "Icon::kText_Help mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_NewParagraph) == - FPDF_ANNOT_NAME_Text_NewParagraph, +static_assert(static_cast(CPDF_Annot::Icon::kText_NewParagraph) == + FPDF_ANNOT_NAME_Text_NewParagraph, "Icon::kText_NewParagraph mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Paragraph) == - FPDF_ANNOT_NAME_Text_Paragraph, +static_assert(static_cast(CPDF_Annot::Icon::kText_Paragraph) == + FPDF_ANNOT_NAME_Text_Paragraph, "Icon::kText_Paragraph mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kText_Insert) == - FPDF_ANNOT_NAME_Text_Insert, +static_assert(static_cast(CPDF_Annot::Icon::kText_Insert) == + FPDF_ANNOT_NAME_Text_Insert, "Icon::kText_Insert mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kFile_Graph) == - FPDF_ANNOT_NAME_File_Graph, +static_assert(static_cast(CPDF_Annot::Icon::kFile_Graph) == + FPDF_ANNOT_NAME_File_Graph, "Icon::kFile_Graph mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kFile_PushPin) == - FPDF_ANNOT_NAME_File_PushPin, +static_assert(static_cast(CPDF_Annot::Icon::kFile_PushPin) == + FPDF_ANNOT_NAME_File_PushPin, "Icon::kFile_PushPin mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kFile_Paperclip) == - FPDF_ANNOT_NAME_File_Paperclip, +static_assert(static_cast(CPDF_Annot::Icon::kFile_Paperclip) == + FPDF_ANNOT_NAME_File_Paperclip, "Icon::kFile_Paperclip mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kFile_Tag) == - FPDF_ANNOT_NAME_File_Tag, +static_assert(static_cast(CPDF_Annot::Icon::kFile_Tag) == + FPDF_ANNOT_NAME_File_Tag, "Icon::kFile_Tag mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kSound_Speaker) == - FPDF_ANNOT_NAME_Sound_Speaker, +static_assert(static_cast(CPDF_Annot::Icon::kSound_Speaker) == + FPDF_ANNOT_NAME_Sound_Speaker, "Icon::kSound_Speaker mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kSound_Mic) == - FPDF_ANNOT_NAME_Sound_Mic, +static_assert(static_cast(CPDF_Annot::Icon::kSound_Mic) == + FPDF_ANNOT_NAME_Sound_Mic, "Icon::kSound_Mic mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Approved) == - FPDF_ANNOT_NAME_Stamp_Approved, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Approved) == + FPDF_ANNOT_NAME_Stamp_Approved, "Icon::kStamp_Approved mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Experimental) == - FPDF_ANNOT_NAME_Stamp_Experimental, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Experimental) == + FPDF_ANNOT_NAME_Stamp_Experimental, "Icon::kStamp_Experimental mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_NotApproved) == - FPDF_ANNOT_NAME_Stamp_NotApproved, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_NotApproved) == + FPDF_ANNOT_NAME_Stamp_NotApproved, "Icon::kStamp_NotApproved mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_AsIs) == - FPDF_ANNOT_NAME_Stamp_AsIs, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_AsIs) == + FPDF_ANNOT_NAME_Stamp_AsIs, "Icon::kStamp_AsIs mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Expired) == - FPDF_ANNOT_NAME_Stamp_Expired, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Expired) == + FPDF_ANNOT_NAME_Stamp_Expired, "Icon::kStamp_Expired mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_NotForPublicRelease) == - FPDF_ANNOT_NAME_Stamp_NotForPublicRelease, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_NotForPublicRelease) == + FPDF_ANNOT_NAME_Stamp_NotForPublicRelease, "Icon::kStamp_NotForPublicRelease mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Confidential) == - FPDF_ANNOT_NAME_Stamp_Confidential, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Confidential) == + FPDF_ANNOT_NAME_Stamp_Confidential, "Icon::kStamp_Confidential mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Final) == - FPDF_ANNOT_NAME_Stamp_Final, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Final) == + FPDF_ANNOT_NAME_Stamp_Final, "Icon::kStamp_Final mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Sold) == - FPDF_ANNOT_NAME_Stamp_Sold, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Sold) == + FPDF_ANNOT_NAME_Stamp_Sold, "Icon::kStamp_Sold mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Departmental) == - FPDF_ANNOT_NAME_Stamp_Departmental, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Departmental) == + FPDF_ANNOT_NAME_Stamp_Departmental, "Icon::kStamp_Departmental mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_ForComment) == - FPDF_ANNOT_NAME_Stamp_ForComment, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_ForComment) == + FPDF_ANNOT_NAME_Stamp_ForComment, "Icon::kStamp_ForComment mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_TopSecret) == - FPDF_ANNOT_NAME_Stamp_TopSecret, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_TopSecret) == + FPDF_ANNOT_NAME_Stamp_TopSecret, "Icon::kStamp_TopSecret mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_Draft) == - FPDF_ANNOT_NAME_Stamp_Draft, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_Draft) == + FPDF_ANNOT_NAME_Stamp_Draft, "Icon::kStamp_Draft mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kStamp_ForPublicRelease) == - FPDF_ANNOT_NAME_Stamp_ForPublicRelease, +static_assert(static_cast(CPDF_Annot::Icon::kStamp_ForPublicRelease) == + FPDF_ANNOT_NAME_Stamp_ForPublicRelease, "Icon::kStamp_ForPublicRelease mismatch"); static_assert(static_cast(CPDF_Annot::Icon::kStamp_Completed) == FPDF_ANNOT_NAME_Stamp_Completed, @@ -485,11 +485,11 @@ static_assert(static_cast(CPDF_Annot::Icon::kStamp_Custom) == static_assert(static_cast(CPDF_Annot::Icon::kStamp_Image) == FPDF_ANNOT_NAME_Stamp_Image, "Icon::kStamp_Image mismatch"); -static_assert(static_cast(CPDF_Annot::Icon::kLast) == - FPDF_ANNOT_NAME_LAST, +static_assert(static_cast(CPDF_Annot::Icon::kLast) == FPDF_ANNOT_NAME_LAST, "Icon::kLast mismatch"); -// These checks ensure the consistency of reply type values across core/ and public. +// These checks ensure the consistency of reply type values across core/ and +// public. static_assert(static_cast(CPDF_Annot::ReplyType::kUnknown) == FPDF_ANNOT_RT_UNKNOWN, "ReplyType::kUnknown mismatch"); @@ -501,39 +501,45 @@ static_assert(static_cast(CPDF_Annot::ReplyType::kGroup) == "ReplyType::kGroup mismatch"); class RawAnnotContext final : public CPDF_AnnotContext { - public: - // Takes ownership of |unparsed_page| by value (RetainPtr). - RawAnnotContext(RetainPtr dict, - RetainPtr unparsed_page) - : CPDF_AnnotContext(dict, unparsed_page.Get()), - owned_page_(std::move(unparsed_page)) {} - - private: - // Keeps the page alive as long as the annot context lives. - const RetainPtr owned_page_; - }; + public: + // Takes ownership of |unparsed_page| by value (RetainPtr). + RawAnnotContext(RetainPtr dict, + RetainPtr unparsed_page, + int annot_index) + : CPDF_AnnotContext(dict, unparsed_page.Get(), annot_index), + owned_page_(std::move(unparsed_page)) {} + + private: + // Keeps the page alive as long as the annot context lives. + const RetainPtr owned_page_; +}; class AnnotAppearanceExporter final : public CPDF_PageOrganizer { public: AnnotAppearanceExporter(CPDF_Document* dest_doc, CPDF_Document* src_doc) : CPDF_PageOrganizer(dest_doc, src_doc) {} - RetainPtr ExportFormXObject(RetainPtr src_stream) { - if (!src_stream || !Init()) + RetainPtr ExportFormXObject( + RetainPtr src_stream) { + if (!src_stream || !Init()) { return nullptr; + } RetainPtr cloned_object = src_stream->Clone(); RetainPtr cloned_stream = ToStream(cloned_object); - if (!cloned_stream) + if (!cloned_stream) { return nullptr; + } const uint32_t src_obj_num = src_stream->GetObjNum(); const uint32_t dest_obj_num = dest()->AddIndirectObject(cloned_object); - if (src_obj_num) + if (src_obj_num) { AddObjectMapping(src_obj_num, dest_obj_num); + } - if (!UpdateReference(cloned_object)) + if (!UpdateReference(cloned_object)) { return nullptr; + } return cloned_stream; } @@ -569,7 +575,7 @@ bool IsNameValidForSubtype(FPDF_ANNOT_NAME name, } } -bool HasAPStream(CPDF_Dictionary* pAnnotDict) { +bool HasAPStream(const CPDF_Dictionary* pAnnotDict) { return !!GetAnnotAP(pAnnotDict, CPDF_Annot::AppearanceMode::kNormal); } @@ -631,41 +637,49 @@ void UpdateBBox(CPDF_Dictionary* annot_dict) { } BlendMode GetEffectiveAnnotBlendMode(CPDF_AnnotContext* ctx) { - if (!ctx) + if (!ctx) { return BlendMode::kNormal; + } - RetainPtr annot_dict = ctx->GetMutableAnnotDict(); - if (!annot_dict) + CPDF_DocumentViewScope document_view(ctx->GetPage()->GetDocument()); + const CPDF_Dictionary* annot_dict = ctx->GetAnnotDict(); + if (!annot_dict) { return BlendMode::kNormal; + } // Get (or detect absence of) normal appearance stream. RetainPtr ap_stream = - GetAnnotAP(annot_dict.Get(), CPDF_Annot::AppearanceMode::kNormal); + GetAnnotAP(annot_dict, CPDF_Annot::AppearanceMode::kNormal); if (!ap_stream) { // Heuristic: highlight annotations without AP are effectively Multiply. const CPDF_Annot::Subtype subtype = CPDF_Annot::StringToAnnotSubtype( annot_dict->GetNameFor(pdfium::annotation::kSubtype)); - if (subtype == CPDF_Annot::Subtype::HIGHLIGHT) + if (subtype == CPDF_Annot::Subtype::HIGHLIGHT) { return BlendMode::kMultiply; + } return BlendMode::kNormal; } // Ensure form is parsed. - if (!ctx->HasForm()) + if (!ctx->HasForm()) { ctx->SetForm(ap_stream); + } CPDF_Form* form = ctx->GetForm(); - if (!form) + if (!form) { return BlendMode::kNormal; + } // Iterate objects in creation order; pick first non-Normal encountered. for (const auto& obj : *form) { - if (!obj) + if (!obj) { continue; + } const CPDF_GeneralState& gs = obj->general_state(); BlendMode bm = gs.GetBlendType(); - if (bm != BlendMode::kNormal) + if (bm != BlendMode::kNormal) { return bm; + } } return BlendMode::kNormal; } @@ -682,11 +696,58 @@ RetainPtr GetMutableAnnotDictFromFPDFAnnotation( return context ? context->GetMutableAnnotDict() : nullptr; } +constexpr char kEmbedMetadataKey[] = "EMBD_Metadata"; +constexpr char kEmbedMetadataCustomJSONKey[] = "CustomJSON"; + +RetainPtr GetEmbedMetadataDict( + const CPDF_Dictionary* annot_dict) { + return annot_dict ? annot_dict->GetDictFor(kEmbedMetadataKey) : nullptr; +} + +RetainPtr GetOrCreateEmbedMetadataDict( + RetainPtr annot_dict) { + if (!annot_dict) { + return nullptr; + } + + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (!metadata) { + metadata = annot_dict->SetNewFor(kEmbedMetadataKey); + } + return metadata; +} + +bool EmbedMetadataIsEmpty(const CPDF_Dictionary* metadata) { + return !metadata || metadata->size() == 0; +} + +void RemoveEmbedMetadataIfEmpty(RetainPtr annot_dict) { + RetainPtr metadata = + GetEmbedMetadataDict(annot_dict.Get()); + if (EmbedMetadataIsEmpty(metadata.Get())) { + annot_dict->RemoveFor(kEmbedMetadataKey); + } +} + +float GetEmbedMetadataFloatFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetFloatFor(key) : 0.0f; +} + +CFX_FloatRect GetEmbedMetadataRectFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetRectFor(key) : CFX_FloatRect(); +} + static uint32_t EnsureIndirect(CPDF_Document* doc, RetainPtr dict) { uint32_t objnum = dict->GetObjNum(); - if (objnum == 0) + if (objnum == 0) { objnum = doc->AddIndirectObject(dict); + } return objnum; } @@ -743,42 +804,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, @@ -840,7 +865,13 @@ std::optional GetFreetextFontColor( RetainPtr acroform_dict = root_dict ? root_dict->GetDictFor("AcroForm") : nullptr; CPDF_DefaultAppearance default_appearance(annot_dict, acroform_dict); - return default_appearance.GetColorARGB(); + std::optional color = + default_appearance.GetColorARGB(); + if (color.has_value()) { + return color; + } + return CFX_Color::TypeAndARGB(CFX_Color::Type::kGray, + ArgbEncode(255, 0, 0, 0)); } std::optional GetWidgetFontColor(FPDF_FORMHANDLE handle, @@ -853,19 +884,28 @@ enum class EPDFStampFitCpp { kContain = 0, kCover = 1, kStretch = 2 }; inline EPDFStampFitCpp ToCpp(EPDF_STAMP_FIT v) { switch (v) { - case EPDF_STAMP_FIT_COVER: return EPDFStampFitCpp::kCover; - case EPDF_STAMP_FIT_STRETCH: return EPDFStampFitCpp::kStretch; - case EPDF_STAMP_FIT_CONTAIN: return EPDFStampFitCpp::kContain; + case EPDF_STAMP_FIT_COVER: + return EPDFStampFitCpp::kCover; + case EPDF_STAMP_FIT_STRETCH: + return EPDFStampFitCpp::kStretch; + case EPDF_STAMP_FIT_CONTAIN: + return EPDFStampFitCpp::kContain; } return EPDFStampFitCpp::kContain; } -static bool FitImageIntoBox(float box_w, float box_h, - float img_w, float img_h, +static bool FitImageIntoBox(float box_w, + float box_h, + float img_w, + float img_h, EPDFStampFitCpp fit, - float* out_drawn_w, float* out_drawn_h, - float* out_dx, float* out_dy) { - if (box_w <= 0 || box_h <= 0 || img_w <= 0 || img_h <= 0) return false; + float* out_drawn_w, + float* out_drawn_h, + float* out_dx, + float* out_dy) { + if (box_w <= 0 || box_h <= 0 || img_w <= 0 || img_h <= 0) { + return false; + } const float sx = box_w / img_w; const float sy = box_h / img_h; @@ -902,13 +942,16 @@ static bool FitImageIntoBox(float box_w, float box_h, // CalcBoundingBox() already returns bounds in the post-Matrix display space. // We therefore must NOT apply the Matrix again here. // Falls back to the raw /BBox if the form has no parseable page objects. -static CFX_FloatRect GetPaintedFormBounds(CPDF_Document* doc, CPDF_Stream* stream) { - if (!doc || !stream) +static CFX_FloatRect GetPaintedFormBounds(CPDF_Document* doc, + CPDF_Stream* stream) { + if (!doc || !stream) { return CFX_FloatRect(); + } RetainPtr stream_dict = stream->GetMutableDict(); - if (!stream_dict) + if (!stream_dict) { return CFX_FloatRect(); + } auto form = std::make_unique( doc, stream_dict->GetMutableDictFor("Resources"), @@ -921,8 +964,9 @@ static CFX_FloatRect GetPaintedFormBounds(CPDF_Document* doc, CPDF_Stream* strea bounds = stream_dict->GetRectFor("BBox"); bounds.Normalize(); } - if (bounds.IsEmpty()) + if (bounds.IsEmpty()) { return CFX_FloatRect(); + } return bounds; } @@ -930,13 +974,15 @@ static CFX_FloatRect GetPaintedFormBounds(CPDF_Document* doc, CPDF_Stream* strea // has no parseable page objects, or all objects are outside the BBox clip). // Computes the display box by applying the stream's /Matrix to its /BBox. static CFX_FloatRect GetFormDisplayBox(const CPDF_Dictionary* stream_dict) { - if (!stream_dict) + if (!stream_dict) { return CFX_FloatRect(); + } CFX_FloatRect bbox = stream_dict->GetRectFor("BBox"); bbox.Normalize(); - if (bbox.IsEmpty()) + if (bbox.IsEmpty()) { return CFX_FloatRect(); + } CFX_Matrix matrix = stream_dict->GetMatrixFor("Matrix"); if (!matrix.IsIdentity()) { @@ -950,12 +996,11 @@ static CFX_FloatRect GetFormDisplayBox(const CPDF_Dictionary* stream_dict) { // Resources/XObject/EPDFWRAP, so the outer AP content can be a simple // "q ... cm /EPDFWRAP Do Q" that handles all scaling. Returns false on // failure; on success the caller must write the new wrapper content stream. -static bool WrapAPContentIntoFormXObject( - CPDF_Stream* ap, - CPDF_Document* doc) { +static bool WrapAPContentIntoFormXObject(CPDF_Stream* ap, CPDF_Document* doc) { RetainPtr ap_dict = ap->GetMutableDict(); - if (!ap_dict) + if (!ap_dict) { return false; + } // Build the child Form XObject dictionary. auto child_dict = doc->New(); @@ -967,17 +1012,20 @@ static bool WrapAPContentIntoFormXObject( // Fallback to Matrix-transformed BBox if the raw BBox is missing/empty. CFX_FloatRect bbox = ap_dict->GetRectFor("BBox"); bbox.Normalize(); - if (bbox.IsEmpty()) + if (bbox.IsEmpty()) { bbox = GetFormDisplayBox(ap_dict.Get()); - if (bbox.IsEmpty()) + } + if (bbox.IsEmpty()) { return false; + } child_dict->SetRectFor("BBox", bbox); CFX_Matrix child_matrix = ap_dict->GetMatrixFor("Matrix"); - if (!child_matrix.IsIdentity()) + if (!child_matrix.IsIdentity()) { child_dict->SetMatrixFor("Matrix", child_matrix); - else + } else { child_dict->RemoveFor("Matrix"); + } // Move Resources to the child (avoids deep-clone cost). RetainPtr res = ap_dict->GetMutableDictFor("Resources"); @@ -1000,8 +1048,7 @@ static bool WrapAPContentIntoFormXObject( ap_dict->SetNewFor("Resources"); RetainPtr xobj = new_res->SetNewFor("XObject"); - xobj->SetNewFor("EPDFWRAP", doc, - child_stream->GetObjNum()); + xobj->SetNewFor("EPDFWRAP", doc, child_stream->GetObjNum()); return true; } } // namespace @@ -1028,6 +1075,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; @@ -1068,24 +1119,27 @@ FPDF_EXPORT int FPDF_CALLCONV FPDFPage_GetAnnotCount(FPDF_PAGE page) { FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV FPDFPage_GetAnnot(FPDF_PAGE page, int index) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage || index < 0) { + ScopedFPDFPageView page_view(page); + if (!page_view || index < 0) { return nullptr; } + const CPDF_Page* pPage = page_view.Get(); - RetainPtr pAnnots = pPage->GetMutableAnnotsArray(); + RetainPtr pAnnots = pPage->GetAnnotsArray(); if (!pAnnots || static_cast(index) >= pAnnots->size()) { return nullptr; } + RetainPtr const_dict = + ToDictionary(pAnnots->GetDirectObjectAt(index)); RetainPtr dict = - ToDictionary(pAnnots->GetMutableDirectObjectAt(index)); + pdfium::WrapRetain(const_cast(const_dict.Get())); if (!dict) { return nullptr; } auto pNewAnnot = std::make_unique( - std::move(dict), IPDFPageFromFPDFPage(page)); + std::move(dict), IPDFPageFromFPDFPage(page), index); // Caller takes ownership. return FPDFAnnotationFromCPDFAnnotContext(pNewAnnot.release()); @@ -1093,15 +1147,17 @@ FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV FPDFPage_GetAnnot(FPDF_PAGE page, FPDF_EXPORT int FPDF_CALLCONV FPDFPage_GetAnnotIndex(FPDF_PAGE page, FPDF_ANNOTATION annot) { - const CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return -1; } + const CPDF_Page* pPage = page_view.Get(); - const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) { + ScopedFPDFAnnotationView annot_view(annot); + if (!annot_view) { return -1; } + const CPDF_Dictionary* pAnnotDict = annot_view.Get()->GetAnnotDict(); RetainPtr pAnnots = pPage->GetAnnotsArray(); if (!pAnnots) { @@ -1288,10 +1344,11 @@ FPDF_EXPORT int FPDF_CALLCONV FPDFAnnot_GetObjectCount(FPDF_ANNOTATION annot) { return 0; } + CPDF_DocumentViewScope document_view(pAnnot->GetPage()->GetDocument()); if (!pAnnot->HasForm()) { - RetainPtr dict = pAnnot->GetMutableAnnotDict(); + const CPDF_Dictionary* dict = pAnnot->GetAnnotDict(); RetainPtr pStream = - GetAnnotAP(dict.Get(), CPDF_Annot::AppearanceMode::kNormal); + GetAnnotAP(dict, CPDF_Annot::AppearanceMode::kNormal); if (!pStream) { return 0; } @@ -1308,10 +1365,11 @@ FPDFAnnot_GetObject(FPDF_ANNOTATION annot, int index) { return nullptr; } + CPDF_DocumentViewScope document_view(pAnnot->GetPage()->GetDocument()); if (!pAnnot->HasForm()) { - RetainPtr pAnnotDict = pAnnot->GetMutableAnnotDict(); + const CPDF_Dictionary* pAnnotDict = pAnnot->GetAnnotDict(); RetainPtr pStream = - GetAnnotAP(pAnnotDict.Get(), CPDF_Annot::AppearanceMode::kNormal); + GetAnnotAP(pAnnotDict, CPDF_Annot::AppearanceMode::kNormal); if (!pStream) { return nullptr; } @@ -1397,8 +1455,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_GetColor(FPDF_ANNOTATION annot, unsigned int* G, unsigned int* B, unsigned int* A) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); if (!pAnnotDict || !R || !G || !B || !A) { return false; @@ -1407,7 +1464,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_GetColor(FPDF_ANNOTATION annot, // For annotations with their appearance streams already defined, the path // stream's own color definitions take priority over the annotation color // definitions retrieved by this method, hence this method will simply fail. - if (HasAPStream(pAnnotDict.Get())) { + if (HasAPStream(pAnnotDict)) { return false; } @@ -1811,6 +1868,267 @@ EPDFAnnot_SetNumberValue(FPDF_ANNOTATION annot, return true; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_HasEmbedMetadata(FPDF_ANNOTATION annot) { + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); + return !!GetEmbedMetadataDict(annot_dict); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadata(FPDF_ANNOTATION annot) { + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return false; + } + + annot_dict->RemoveFor(kEmbedMetadataKey); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadataKey(FPDF_ANNOTATION annot, FPDF_BYTESTRING key) { + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict || !key) { + return false; + } + + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (!metadata) { + return true; + } + + metadata->RemoveFor(key); + RemoveEmbedMetadataIfEmpty(annot_dict); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor( + key, UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (!key) { + return 0; + } + + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return 0; + } + + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + metadata ? metadata->GetUnicodeTextFor(key) : WideString(), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float* value) { + if (!key || !value) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kNumber) { + return false; + } + + *value = object->GetNumber(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor(key, !!value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL* value) { + if (!key || !value) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kBoolean) { + return false; + } + + *value = object->GetInteger() != 0; + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + const FS_RECTF* rect) { + if (!key) { + return false; + } + + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return false; + } + + if (!rect) { + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (metadata) { + metadata->RemoveFor(key); + RemoveEmbedMetadataIfEmpty(annot_dict); + } + return true; + } + + RetainPtr metadata = + GetOrCreateEmbedMetadataDict(annot_dict); + if (!metadata) { + return false; + } + + metadata->SetRectFor(key, CFXFloatRectFromFSRectF(*rect)); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FS_RECTF* rect) { + if (!key || !rect) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kArray) { + return false; + } + + *rect = FSRectFFromCFXFloatRect(metadata->GetRectFor(key)); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataJSON(FPDF_ANNOTATION annot, FPDF_WIDESTRING json) { + return EPDFAnnot_SetEmbedMetadataString(annot, kEmbedMetadataCustomJSONKey, + json); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataJSON(FPDF_ANNOTATION annot, + FPDF_WCHAR* buffer, + unsigned long buflen) { + return EPDFAnnot_GetEmbedMetadataString(annot, kEmbedMetadataCustomJSONKey, + buffer, buflen); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDocument_ClearEmbedMetadata(FPDF_DOCUMENT document) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + if (!pdf) { + return false; + } + + for (int page_index = 0; page_index < pdf->GetPageCount(); ++page_index) { + RetainPtr page_dict = + pdf->GetMutablePageDictionary(page_index); + if (!page_dict) { + continue; + } + + RetainPtr annots = page_dict->GetMutableArrayFor("Annots"); + if (!annots) { + continue; + } + + for (size_t annot_index = 0; annot_index < annots->size(); ++annot_index) { + RetainPtr annot_dict = + annots->GetMutableDictAt(annot_index); + if (annot_dict) { + annot_dict->RemoveFor(kEmbedMetadataKey); + } + } + } + + return true; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetAP(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode, @@ -1897,8 +2215,7 @@ FPDFAnnot_GetAP(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode, FPDF_WCHAR* buffer, unsigned long buflen) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); if (!pAnnotDict) { return 0; } @@ -1910,7 +2227,7 @@ FPDFAnnot_GetAP(FPDF_ANNOTATION annot, CPDF_Annot::AppearanceMode mode = static_cast(appearanceMode); - RetainPtr pStream = GetAnnotAPNoFallback(pAnnotDict.Get(), mode); + RetainPtr pStream = GetAnnotAPNoFallback(pAnnotDict, mode); // SAFETY: required from caller. return Utf16EncodeMaybeCopyAndReturnLength( pStream ? pStream->GetUnicodeText() : WideString(), @@ -1924,8 +2241,12 @@ FPDFAnnot_GetLinkedAnnot(FPDF_ANNOTATION annot, FPDF_BYTESTRING key) { return nullptr; } + CPDF_DocumentViewScope document_view(pAnnot->GetPage()->GetDocument()); + const CPDF_Dictionary* annot_dict = pAnnot->GetAnnotDict(); + RetainPtr const_linked_dict = + annot_dict ? annot_dict->GetDictFor(key) : nullptr; RetainPtr pLinkedDict = - pAnnot->GetMutableAnnotDict()->GetMutableDictFor(key); + pdfium::WrapRetain(const_cast(const_linked_dict.Get())); if (!pLinkedDict || pLinkedDict->GetNameFor("Type") != "Annot") { return nullptr; } @@ -2173,19 +2494,15 @@ FPDFAnnot_SetFontColor(FPDF_FORMHANDLE handle, return false; } - bool generated = CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( - form->GetInteractiveForm()->document(), annot_dict, CFX_Color(R, G, B)); - if (!generated) { + CPDF_Document* doc = form->GetInteractiveForm()->document(); + bool updated = CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( + doc, annot_dict, CFX_Color(R, G, B)); + if (!updated) { return false; } - // Remove the appearance stream. Otherwise PDF viewers will render that and - // not use the new color. - // - // TODO(thestig) When GenerateDefaultAppearanceWithColor() properly updates - // the annotation's appearance stream, remove this. - annot_dict->RemoveFor(pdfium::annotation::kAP); - return true; + return CPDF_GenerateAP::GenerateAnnotAP(doc, annot_dict.Get(), + CPDF_Annot::Subtype::FREETEXT); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV @@ -2309,9 +2626,14 @@ FPDF_EXPORT FPDF_LINK FPDF_CALLCONV FPDFAnnot_GetLink(FPDF_ANNOTATION annot) { return nullptr; } + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return nullptr; + } + // Unretained reference in public API. NOLINTNEXTLINE return FPDFLinkFromCPDFDictionary( - CPDFAnnotContextFromFPDFAnnotation(annot)->GetMutableAnnotDict()); + const_cast(context->GetAnnotDict())); } FPDF_EXPORT int FPDF_CALLCONV @@ -2369,48 +2691,91 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetURI(FPDF_ANNOTATION annot, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action) { - if (!action || FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINK) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAction(FPDF_ANNOTATION annot, + FPDF_ACTION action) { + if (!action || FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINK) { return false; + } CPDF_AnnotContext* pAnnotContext = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!pAnnotContext) + if (!pAnnotContext) { return false; + } RetainPtr annot_dict = pAnnotContext->GetMutableAnnotDict(); - if (!annot_dict) + if (!annot_dict) { return false; + } CPDF_Dictionary* act_dict = CPDFDictionaryFromFPDFAction(action); - if (!act_dict) + if (!act_dict) { return false; + } // Require the action to be indirect so we can reference it. - if (act_dict->GetObjNum() == 0) + if (act_dict->GetObjNum() == 0) { return false; + } 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) { return nullptr; } - RetainPtr annot_dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return nullptr; + } + CPDF_DocumentViewScope document_view(context->GetPage()->GetDocument()); + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); if (!annot_dict) { return nullptr; } + RetainPtr file_spec = annot_dict->GetDirectObjectFor("FS"); return FPDFAttachmentFromCPDFObject( - annot_dict->GetMutableDirectObjectFor("FS")); + const_cast(file_spec.Get())); } FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV @@ -2456,15 +2821,16 @@ static ByteString GetColorKeyForType(FPDFANNOT_COLORTYPE type) { return "C"; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B) { - RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict || R > 255 || G > 255 || B > 255) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B) { + RetainPtr pAnnotDict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!pAnnotDict || R > 255 || G > 255 || B > 255) { return false; + } // OverlayColor (OC) is only valid for Redact annotations. if (type == FPDFANNOT_COLORTYPE_OverlayColor && @@ -2473,7 +2839,8 @@ EPDFAnnot_SetColor(FPDF_ANNOTATION annot, } ByteString key = GetColorKeyForType(type); - RetainPtr pColor = pAnnotDict->GetMutableArrayFor(key.AsStringView()); + RetainPtr pColor = + pAnnotDict->GetMutableArrayFor(key.AsStringView()); if (pColor) { pColor->Clear(); } else { @@ -2487,18 +2854,19 @@ EPDFAnnot_SetColor(FPDF_ANNOTATION annot, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B) { - if (!R || !G || !B) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B) { + if (!R || !G || !B) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } // OverlayColor (OC) is only valid for Redact annotations. if (type == FPDFANNOT_COLORTYPE_OverlayColor && @@ -2508,8 +2876,9 @@ EPDFAnnot_GetColor(FPDF_ANNOTATION annot, ByteString key = GetColorKeyForType(type); RetainPtr pColor = dict->GetArrayFor(key.AsStringView()); - if (!pColor) - return false; // "no colour set" + if (!pColor) { + return false; // "no colour set" + } CFX_Color color = fpdfdoc::CFXColorFromArray(*pColor); switch (color.nColorType) { @@ -2521,7 +2890,7 @@ EPDFAnnot_GetColor(FPDF_ANNOTATION annot, case CFX_Color::Type::kGray: *R = *G = *B = color.fColor1 * 255.f; break; - case CFX_Color::Type::kCMYK: // convert roughly + case CFX_Color::Type::kCMYK: // convert roughly *R = 255.f * (1 - color.fColor1) * (1 - color.fColor4); *G = 255.f * (1 - color.fColor2) * (1 - color.fColor4); *B = 255.f * (1 - color.fColor3) * (1 - color.fColor4); @@ -2534,9 +2903,11 @@ EPDFAnnot_GetColor(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type) { - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } // OverlayColor (OC) is only valid for Redact annotations. if (type == FPDFANNOT_COLORTYPE_OverlayColor && @@ -2550,11 +2921,13 @@ EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type) { return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, unsigned int alpha) { - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict || alpha > 255) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, + unsigned int alpha) { + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict || alpha > 255) { return false; + } if (alpha == 255) { dict->RemoveFor("CA"); @@ -2564,13 +2937,15 @@ EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, unsigned int alpha) { return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, unsigned int* alpha) { - if (!alpha) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, + unsigned int* alpha) { + if (!alpha) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } float ca = dict->KeyExist("CA") ? dict->GetFloatFor("CA") : 1.0f; *alpha = std::clamp(ca, 0.f, 1.f) * 255.f + 0.5f; @@ -2597,14 +2972,14 @@ EPDFAnnot_GetBorderEffect(FPDF_ANNOTATION annot, float* intensity) { // The style must be 'Cloudy' for the intensity to be meaningful. if (pBEDict->GetNameFor("S") != "C") { - return false; + return false; } // The intensity is in the /I key. Default is 1 if not present. if (pBEDict->KeyExist("I")) { *intensity = pBEDict->GetFloatFor("I"); } else { - *intensity = 1.0f; // Default intensity for cloudy border + *intensity = 1.0f; // Default intensity for cloudy border } return true; @@ -2623,7 +2998,8 @@ EPDFAnnot_SetBorderEffect(FPDF_ANNOTATION annot, float intensity) { return false; } - RetainPtr pBEDict = pAnnotDict->SetNewFor("BE"); + RetainPtr pBEDict = + pAnnotDict->SetNewFor("BE"); pBEDict->SetNewFor("S", "C"); pBEDict->SetNewFor("I", intensity); @@ -2640,8 +3016,9 @@ EPDFAnnot_ClearBorderEffect(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("BE"); return true; @@ -2688,10 +3065,10 @@ EPDFAnnot_GetRectangleDifferences(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRectangleDifferences(FPDF_ANNOTATION annot, - float left, - float bottom, - float right, - float top) { + float left, + float bottom, + float right, + float top) { FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); if (subtype != FPDF_ANNOT_SQUARE && subtype != FPDF_ANNOT_CIRCLE && subtype != FPDF_ANNOT_CARET && subtype != FPDF_ANNOT_FREETEXT && @@ -2724,38 +3101,48 @@ EPDFAnnot_ClearRectangleDifferences(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("RD"); return true; } -FPDF_EXPORT unsigned long FPDF_CALLCONV -EPDFAnnot_GetBorderDashPatternCount(FPDF_ANNOTATION annot) { - const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) { - return 0; - } +namespace { - // The dash pattern is inside the /BS dictionary. +RetainPtr GetExplicitBorderDashArray( + const CPDF_Dictionary* pAnnotDict) { RetainPtr pBSDict = pAnnotDict->GetDictFor("BS"); - if (!pBSDict) { - return 0; + if (pBSDict) { + // /BS takes precedence over /Border. A missing or unrecognised /S uses + // the solid default, so no dash pattern applies. + if (pBSDict->GetNameFor("S") != "D") { + return nullptr; + } + return pBSDict->GetArrayFor("D"); } - - // The border style must be dashed. - if (pBSDict->GetNameFor("S") != "D") { - return 0; + + RetainPtr pBorderArray = + pAnnotDict->GetArrayFor(pdfium::annotation::kBorder); + if (!pBorderArray || pBorderArray->size() < 4) { + return nullptr; } + return pBorderArray->GetArrayAt(3); +} + +} // namespace - // The dash pattern is defined by the /D array. - RetainPtr pDashArray = pBSDict->GetArrayFor("D"); - if (!pDashArray) { +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetBorderDashPatternCount(FPDF_ANNOTATION annot) { + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); + if (!pAnnotDict) { return 0; } - return pDashArray->size(); + RetainPtr pDashArray = + GetExplicitBorderDashArray(pAnnotDict); + return pDashArray ? pDashArray->size() : 0; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV @@ -2767,12 +3154,8 @@ EPDFAnnot_GetBorderDashPattern(FPDF_ANNOTATION annot, return false; } - RetainPtr pBSDict = pAnnotDict->GetDictFor("BS"); - if (!pBSDict || pBSDict->GetNameFor("S") != "D") { - return false; - } - - RetainPtr pDashArray = pBSDict->GetArrayFor("D"); + RetainPtr pDashArray = + GetExplicitBorderDashArray(pAnnotDict); if (!pDashArray || pDashArray->size() < count) { return false; } @@ -2788,25 +3171,29 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, const float* dash_array, unsigned long count) { - if (!annot) + if (!annot) { return false; + } RetainPtr annot_dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) + if (!annot_dict) { return false; + } RetainPtr bs_dict = annot_dict->GetMutableDictFor("BS"); - if (!bs_dict) + if (!bs_dict) { bs_dict = annot_dict->SetNewFor("BS"); + } // --- Removal branch (PDFium style) --- if (!dash_array || count == 0) { bs_dict->RemoveFor("D"); // Optional: if style was dashed only because of the array, you can revert. // Leaving it unchanged matches PDFium's permissive style. - if (bs_dict->size() == 0) + if (bs_dict->size() == 0) { annot_dict->RemoveFor("BS"); + } return true; } @@ -2814,14 +3201,16 @@ EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, bs_dict->SetNewFor("S", "D"); RetainPtr d_array = bs_dict->GetMutableArrayFor("D"); - if (d_array) + if (d_array) { d_array->Clear(); - else + } else { d_array = bs_dict->SetNewFor("D"); + } // SAFETY: caller guarantees `dash_array` has `count` elements. - for (unsigned long i = 0; i < count; ++i) + for (unsigned long i = 0; i < count; ++i) { d_array->AppendNew(dash_array[i]); + } return true; } @@ -2830,7 +3219,9 @@ FPDF_EXPORT FPDF_ANNOT_BORDER_STYLE FPDF_CALLCONV EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width) { const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); if (!pAnnotDict) { - if (width) *width = 0; + if (width) { + *width = 0; + } return FPDF_ANNOT_BS_UNKNOWN; } @@ -2839,13 +3230,29 @@ EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width) { if (width) { *width = pBSDict->KeyExist("W") ? pBSDict->GetFloatFor("W") : 1.0f; } - // Use our new internal helper function and cast the result - return static_cast( - CPDF_Annot::StringToBorderStyle(pBSDict->GetNameFor("S"))); + + CPDF_Annot::BorderStyle nStyle = + CPDF_Annot::StringToBorderStyle(pBSDict->GetNameFor("S")); + if (nStyle == CPDF_Annot::BorderStyle::kUnknown) { + nStyle = CPDF_Annot::BorderStyle::kSolid; + } + return static_cast(nStyle); + } + + RetainPtr pBorderArray = + pAnnotDict->GetArrayFor(pdfium::annotation::kBorder); + if (width) { + *width = pBorderArray && pBorderArray->size() >= 3 + ? pBorderArray->GetFloatAt(2) + : 1.0f; + } + + RetainPtr pDashArray = + GetExplicitBorderDashArray(pAnnotDict); + if (pDashArray && !pDashArray->IsEmpty()) { + return FPDF_ANNOT_BS_DASHED; } - - if (width) *width = 0; - return FPDF_ANNOT_BS_UNKNOWN; + return FPDF_ANNOT_BS_SOLID; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV @@ -2915,16 +3322,19 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GenerateAppearanceWithBlend(FPDF_ANNOTATION annot, FPDF_BLENDMODE blend) { CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!ctx) + if (!ctx) { return false; + } RetainPtr annot_dict = ctx->GetMutableAnnotDict(); - if (!annot_dict) + if (!annot_dict) { return false; + } CPDF_Document* doc = ctx->GetPage()->GetDocument(); - if (!doc) + if (!doc) { return false; + } const CPDF_Annot::Subtype subtype = CPDF_Annot::StringToAnnotSubtype( annot_dict->GetNameFor(pdfium::annotation::kSubtype)); @@ -2939,8 +3349,9 @@ EPDFAnnot_GenerateAppearanceWithBlend(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_BLENDMODE FPDF_CALLCONV EPDFAnnot_GetBlendMode(FPDF_ANNOTATION annot) { CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!ctx) + if (!ctx) { return FPDF_BLENDMODE_Normal; + } BlendMode bm = GetEffectiveAnnotBlendMode(ctx); // Safe cast due to static_asserts above. @@ -2949,20 +3360,24 @@ EPDFAnnot_GetBlendMode(FPDF_ANNOTATION annot) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetIntent(FPDF_ANNOTATION annot, FPDF_BYTESTRING intent) { - if (!annot || !intent || !*intent) + if (!annot || !intent || !*intent) { return false; + } RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } // Allow leading slash from caller; strip it. - if (intent[0] == '/') + if (intent[0] == '/') { ++intent; + } - if (!*intent) + if (!*intent) { return false; + } // Minimal validation (PDFium typically trusts caller). Could reject spaces / // delimiters (),<>[]{}/%# if you want to be stricter. Keeping permissive. @@ -2975,12 +3390,14 @@ EPDFAnnot_GetIntent(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen) { const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return 0; + } ByteString name = dict->GetNameFor("IT"); - if (name.IsEmpty()) + if (name.IsEmpty()) { return 0; + } // Name objects are ASCII (or PDF name syntax). For normal ASCII we can // construct a WideString directly. (If you later want to decode #XX escapes @@ -2997,21 +3414,23 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen) { const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return 0; + } // /RC may be a text string or a stream (PDF 2.0 §12.5.6.5). RetainPtr rc_obj = dict->GetObjectFor("RC"); - if (!rc_obj) + if (!rc_obj) { return 0; + } WideString ws; if (rc_obj->IsString() || rc_obj->IsName()) { - ws = rc_obj->GetUnicodeText(); // handles PDFDocEncoding / UTF‑16BE + ws = rc_obj->GetUnicodeText(); // handles PDFDocEncoding / UTF‑16BE } else if (rc_obj->IsStream()) { ws = rc_obj->AsStream()->GetUnicodeText(); } else { - return 0; // some exotic type we don't handle + return 0; // some exotic type we don't handle } // SAFETY: same pattern as other getters. @@ -3025,12 +3444,15 @@ EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, FPDF_ANNOT_LINE_END end_style) { FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); if (subtype != FPDF_ANNOT_LINE && subtype != FPDF_ANNOT_POLYLINE && - subtype != FPDF_ANNOT_FREETEXT) + subtype != FPDF_ANNOT_FREETEXT) { return false; + } - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } auto to_core = [](FPDF_ANNOT_LINE_END v) { return static_cast(v); @@ -3049,23 +3471,27 @@ EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, if (subtype == FPDF_ANNOT_FREETEXT) { // FreeText uses a single name for /LE (Acrobat convention). ByteString e_name = CPDF_Annot::LineEndingToString(e); - if (e_name.IsEmpty()) + if (e_name.IsEmpty()) { e_name = "None"; + } dict->SetNewFor("LE", e_name); } else { ByteString s_name = CPDF_Annot::LineEndingToString(s); ByteString e_name = CPDF_Annot::LineEndingToString(e); - if (s_name.IsEmpty()) + if (s_name.IsEmpty()) { s_name = "None"; - if (e_name.IsEmpty()) + } + if (e_name.IsEmpty()) { e_name = "None"; + } RetainPtr le = dict->GetMutableArrayFor("LE"); - if (le) + if (le) { le->Clear(); - else + } else { le = dict->SetNewFor("LE"); + } le->AppendNew(s_name); le->AppendNew(e_name); @@ -3076,33 +3502,39 @@ EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, static ByteString ReadLineEndingToken(const CPDF_Array* le, size_t idx) { RetainPtr obj = le->GetDirectObjectAt(idx); - if (!obj) + if (!obj) { return ByteString(); + } - if (const CPDF_Name* n = obj->AsName()) - return n->GetString(); // e.g. "OpenArrow" + if (const CPDF_Name* n = obj->AsName()) { + return n->GetString(); // e.g. "OpenArrow" + } - if (const CPDF_String* s = obj->AsString()) - return s->GetString(); // tolerate a stray string + if (const CPDF_String* s = obj->AsString()) { + return s->GetString(); // tolerate a stray string + } - return ByteString(); // anything else -> empty + return ByteString(); // anything else -> empty } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, FPDF_ANNOT_LINE_END* start_style, FPDF_ANNOT_LINE_END* end_style) { - if (!start_style || !end_style) + if (!start_style || !end_style) { return false; + } FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); if (subtype != FPDF_ANNOT_LINE && subtype != FPDF_ANNOT_POLYLINE && - subtype != FPDF_ANNOT_FREETEXT) + subtype != FPDF_ANNOT_FREETEXT) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } // Try reading as a 2-element array first (spec-compliant for all types). RetainPtr le = dict->GetArrayFor("LE"); @@ -3116,18 +3548,19 @@ EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, CPDF_Annot::StringToLineEnding(e_name.IsEmpty() ? "None" : e_name); *start_style = static_cast(s); - *end_style = static_cast(e); + *end_style = static_cast(e); return true; } // Fall back to single name (Acrobat FreeText convention). ByteString name = dict->GetNameFor("LE"); - if (name.IsEmpty()) + if (name.IsEmpty()) { return false; + } *start_style = FPDF_ANNOT_LE_None; - *end_style = static_cast( - CPDF_Annot::StringToLineEnding(name)); + *end_style = + static_cast(CPDF_Annot::StringToLineEnding(name)); return true; } @@ -3137,12 +3570,15 @@ EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, unsigned long count) { // Accept only Polygon / Polyline annotations. FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); - if (subtype != FPDF_ANNOT_POLYGON && subtype != FPDF_ANNOT_POLYLINE) + if (subtype != FPDF_ANNOT_POLYGON && subtype != FPDF_ANNOT_POLYLINE) { return false; + } - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } // ───── Removal branch ──────────────────────────────────────────────── // If caller passes nullptr or zero, delete the /Vertices array. @@ -3154,10 +3590,11 @@ EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, // ───── Replacement branch ─────────────────────────────────────────── RetainPtr verts = dict->GetMutableArrayFor(pdfium::annotation::kVertices); - if (verts) + if (verts) { verts->Clear(); - else + } else { verts = dict->SetNewFor(pdfium::annotation::kVertices); + } // SAFETY: caller guarantees |points| has |count| entries. auto pts = UNSAFE_BUFFERS(pdfium::span(points, count)); @@ -3169,28 +3606,31 @@ EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetLine(FPDF_ANNOTATION annot, - const FS_POINTF* start, - const FS_POINTF* end) { - if (!annot || !start || !end) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLine(FPDF_ANNOTATION annot, + const FS_POINTF* start, + const FS_POINTF* end) { + if (!annot || !start || !end) { return false; + } - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINE) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_LINE) { return false; + } RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } // (Re‑)create the /L array: [ x1 y1 x2 y2 ] RetainPtr line_arr = dict->GetMutableArrayFor(pdfium::annotation::kL); - if (line_arr) + if (line_arr) { line_arr->Clear(); - else + } else { line_arr = dict->SetNewFor(pdfium::annotation::kL); + } line_arr->AppendNew(start->x); line_arr->AppendNew(start->y); @@ -3217,10 +3657,10 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, return false; } - // Allow FREETEXT, WIDGET, and REDACT annotations (all use DA for text styling) + // Allow FREETEXT, WIDGET, and REDACT annotations (all use DA for text + // styling) FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); - if (subtype != FPDF_ANNOT_FREETEXT && - subtype != FPDF_ANNOT_WIDGET && + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && subtype != FPDF_ANNOT_REDACT) { return false; } @@ -3230,7 +3670,8 @@ EPDFAnnot_SetDefaultAppearance(FPDF_ANNOTATION annot, return false; } - // Validate parameters. Allow FPDF_FONT_UNKNOWN to preserve non-standard fonts. + // Validate parameters. Allow FPDF_FONT_UNKNOWN to preserve non-standard + // fonts. if (font != FPDF_FONT_UNKNOWN && (font < FPDF_FONT_COURIER || font > FPDF_FONT_ZAPFDINGBATS)) { return false; @@ -3244,6 +3685,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, @@ -3259,15 +3740,11 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, if (!context) { return false; } - RetainPtr annot_dict = context->GetMutableAnnotDict(); - if (!annot_dict) { - return false; - } - // Allow FREETEXT, WIDGET, and REDACT annotations (all use DA for text styling) + // Allow FREETEXT, WIDGET, and REDACT annotations (all use DA for text + // styling) FPDF_ANNOTATION_SUBTYPE subtype = FPDFAnnot_GetSubtype(annot); - if (subtype != FPDF_ANNOT_FREETEXT && - subtype != FPDF_ANNOT_WIDGET && + if (subtype != FPDF_ANNOT_FREETEXT && subtype != FPDF_ANNOT_WIDGET && subtype != FPDF_ANNOT_REDACT) { return false; } @@ -3276,19 +3753,25 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, if (!doc) { return false; } + CPDF_DocumentViewScope document_view(doc); + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return false; + } // Get AcroForm for inherited /DA and /DR. - RetainPtr root_dict = doc->GetMutableRoot(); + const CPDF_Dictionary* root_dict = doc->GetRoot(); RetainPtr acroform_dict = root_dict ? root_dict->GetDictFor("AcroForm") : nullptr; // Parse the /DA string, inheriting from AcroForm if necessary. - CPDF_DefaultAppearance da(annot_dict.Get(), acroform_dict.Get()); + CPDF_DefaultAppearance da(annot_dict, acroform_dict.Get()); // Get Font and Font Size - std::optional font_info = da.GetFont(); + std::optional font_info = + da.GetFont(); if (!font_info.has_value()) { - return false; // Hard fail: /DA string must specify a font. + return false; // Hard fail: /DA string must specify a font. } *font_size = font_info.value().size; ByteString font_name = font_info.value().name; @@ -3319,7 +3802,8 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment) { +EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, + FPDF_TEXT_ALIGNMENT alignment) { RetainPtr annot_dict = GetMutableAnnotDictFromFPDFAnnotation(annot); if (!annot_dict) { @@ -3333,11 +3817,13 @@ EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment) } // Validate the enum range to ensure a valid value is passed. - if (alignment < FPDF_TEXT_ALIGNMENT_LEFT || alignment > FPDF_TEXT_ALIGNMENT_RIGHT) { + if (alignment < FPDF_TEXT_ALIGNMENT_LEFT || + alignment > FPDF_TEXT_ALIGNMENT_RIGHT) { return false; } - // Set the /Q key in the annotation dictionary to the integer value of the enum. + // Set the /Q key in the annotation dictionary to the integer value of the + // enum. annot_dict->SetNewFor("Q", static_cast(alignment)); return true; @@ -3372,73 +3858,33 @@ EPDFAnnot_GetTextAlignment(FPDF_ANNOTATION annot) { return kDefaultAlignment; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, FPDF_VERTICAL_ALIGNMENT alignment) { - RetainPtr annot_dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return false; - } - - // This property is only valid for FreeText annotations. - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { - return false; - } - - // Validate the enum range to ensure a valid value is passed. - if (alignment < FPDF_VERTICAL_ALIGNMENT_TOP || alignment > FPDF_VERTICAL_ALIGNMENT_BOTTOM) { - return false; - } - - // Set the /EPDF:VerticalAlignment key in the annotation dictionary to the integer value of the enum. - annot_dict->SetNewFor("EPDF:VerticalAlignment", static_cast(alignment)); - - return true; -} - -FPDF_EXPORT FPDF_VERTICAL_ALIGNMENT FPDF_CALLCONV -EPDFAnnot_GetVerticalAlignment(FPDF_ANNOTATION annot) { - RetainPtr annot_dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return FPDF_VERTICAL_ALIGNMENT_TOP; +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { + if (!page || !nm || !*nm) { + return nullptr; } - - // This property is only valid for FreeText annotations. - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { - return FPDF_VERTICAL_ALIGNMENT_TOP; + ScopedFPDFPageView page_view(page); + if (!page_view) { + return nullptr; } + const CPDF_Page* pPage = page_view.Get(); - // GetIntegerFor() conveniently returns 0 if the key doesn't exist, - // which matches the PDF specification's default. - int alignment_value = annot_dict->GetIntegerFor("EPDF:VerticalAlignment"); - - // Validate the value is within the known enum range before casting. - if (alignment_value >= FPDF_VERTICAL_ALIGNMENT_TOP && - alignment_value <= FPDF_VERTICAL_ALIGNMENT_BOTTOM) { - return static_cast(alignment_value); + RetainPtr annots = pPage->GetAnnotsArray(); + if (!annots) { + return nullptr; } - return FPDF_VERTICAL_ALIGNMENT_TOP; -} - -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV -EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { - if (!page || !nm || !*nm) return nullptr; - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) return nullptr; - - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) return nullptr; - WideString target = UNSAFE_BUFFERS(WideStringFromFPDFWideString(nm)); for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr const_dict = + ToDictionary(annots->GetDirectObjectAt(i)); RetainPtr d = - ToDictionary(annots->GetMutableDirectObjectAt(i)); + pdfium::WrapRetain(const_cast(const_dict.Get())); if (d && d->GetUnicodeTextFor("NM") == target) { auto ctx = std::make_unique( - std::move(d), IPDFPageFromFPDFPage(page)); + std::move(d), IPDFPageFromFPDFPage(page), + pdfium::checked_cast(i)); return FPDFAnnotationFromCPDFAnnotContext(ctx.release()); } } @@ -3447,16 +3893,19 @@ EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { - if (!page || !nm || !*nm) + if (!page || !nm || !*nm) { return false; + } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return false; + } RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) + if (!annots) { return false; + } WideString target = UNSAFE_BUFFERS(WideStringFromFPDFWideString(nm)); @@ -3467,8 +3916,9 @@ EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { // Resolve to a dictionary to compare /NM. RetainPtr dict = ToDictionary(entry ? entry->GetMutableDirect() : nullptr); - if (!dict || dict->GetUnicodeTextFor("NM") != target) + if (!dict || dict->GetUnicodeTextFor("NM") != target) { continue; + } // Determine indirect object number, if any. uint32_t objnum = 0; @@ -3484,8 +3934,9 @@ EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { annots->RemoveAt(i); // If it was indirect, delete the object to avoid leaving an orphan. - if (objnum) + if (objnum) { pPage->GetDocument()->DeleteIndirectObject(objnum); + } return true; } @@ -3496,45 +3947,67 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, FPDF_ANNOTATION linked_annot) { - if (!annot || !key) return false; + if (!annot || !key) { + return false; + } CPDF_AnnotContext* src = CPDFAnnotContextFromFPDFAnnotation(annot); CPDF_AnnotContext* dst = CPDFAnnotContextFromFPDFAnnotation(linked_annot); - if (!src) return false; + if (!src) { + return false; + } RetainPtr src_dict = src->GetMutableAnnotDict(); - if (!src_dict) return false; + if (!src_dict) { + return false; + } - if (!linked_annot) { src_dict->RemoveFor(key); return true; } + if (!linked_annot) { + src_dict->RemoveFor(key); + return true; + } - if (!dst) return false; + if (!dst) { + return false; + } IPDF_Page* sp = src->GetPage(); IPDF_Page* dp = dst->GetPage(); - if (!sp || !dp) return false; + if (!sp || !dp) { + return false; + } CPDF_Document* doc = sp->GetDocument(); - if (doc != dp->GetDocument()) return false; + if (doc != dp->GetDocument()) { + return false; + } RetainPtr dst_dict = dst->GetMutableAnnotDict(); - if (!dst_dict) return false; + if (!dst_dict) { + return false; + } const uint32_t objnum = EnsureIndirect(doc, dst_dict); - if (objnum == 0) return false; + if (objnum == 0) { + return false; + } src_dict->SetNewFor(key, doc, objnum); return true; } -FPDF_EXPORT int FPDF_CALLCONV -EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, int page_index) { +FPDF_EXPORT int FPDF_CALLCONV EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, + int page_index) { CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc); - if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount()) + if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount()) { return 0; + } + CPDF_DocumentViewScope document_view(pdf); RetainPtr page_dict = pdf->GetPageDictionary(page_index); - if (!page_dict) + if (!page_dict) { return 0; + } RetainPtr annots = page_dict->GetArrayFor("Annots"); return annots ? fxcrt::CollectionSize(*annots) : 0; } @@ -3546,20 +4019,25 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { page_index >= pdf->GetPageCount()) { return nullptr; } + CPDF_DocumentViewScope document_view(pdf); + RetainPtr const_page_dict = + pdf->GetPageDictionary(page_index); RetainPtr page_dict = - pdf->GetMutablePageDictionary(page_index); + pdfium::WrapRetain(const_cast(const_page_dict.Get())); if (!page_dict) { return nullptr; } - RetainPtr annots = page_dict->GetMutableArrayFor("Annots"); + RetainPtr annots = page_dict->GetArrayFor("Annots"); if (!annots || static_cast(index) >= annots->size()) { return nullptr; } + RetainPtr const_annot_dict = + ToDictionary(annots->GetDirectObjectAt(index)); RetainPtr annot_dict = - ToDictionary(annots->GetMutableDirectObjectAt(index)); + pdfium::WrapRetain(const_cast(const_annot_dict.Get())); if (!annot_dict) { return nullptr; } @@ -3569,25 +4047,32 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { auto page = pdfium::MakeRetain(pdf, page_dict); // Create the context, which now takes the RetainPtr directly. - auto ctx = std::make_unique(std::move(annot_dict), std::move(page)); + auto ctx = std::make_unique(std::move(annot_dict), + std::move(page), index); // The lifetime is now perfectly managed by smart pointers. return FPDFAnnotationFromCPDFAnnotContext(ctx.release()); } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, + int page_index, + int index) { CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(doc); - if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount() || index < 0) + if (!pdf || page_index < 0 || page_index >= pdf->GetPageCount() || + index < 0) { return false; + } - RetainPtr page_dict = pdf->GetMutablePageDictionary(page_index); - if (!page_dict) + RetainPtr page_dict = + pdf->GetMutablePageDictionary(page_index); + if (!page_dict) { return false; + } RetainPtr annots = page_dict->GetMutableArrayFor("Annots"); - if (!annots || static_cast(index) >= annots->size()) + if (!annots || static_cast(index) >= annots->size()) { return false; + } // Keep original entry so we can determine if it was indirect. RetainPtr entry = annots->GetMutableObjectAt(index); @@ -3607,14 +4092,15 @@ EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { annots->RemoveAt(index); // If it was indirect, delete the annot object to avoid leaving orphans. - if (objnum) + if (objnum) { pdf->DeleteIndirectObject(objnum); + } return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetName(FPDF_ANNOTATION annot, FPDF_ANNOT_NAME name) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetName(FPDF_ANNOTATION annot, + FPDF_ANNOT_NAME name) { RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); if (!dict) { @@ -3672,30 +4158,35 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { EPDFStampFitCpp fit_cpp = ToCpp(fit); CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!ctx) + if (!ctx) { return false; + } - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_STAMP) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_STAMP) { return false; + } RetainPtr ad = ctx->GetMutableAnnotDict(); - if (!ad) + if (!ad) { return false; + } - // 1) Check for EPDFRotate + EPDFUnrotatedRect first. - float rotate_deg = ad->GetFloatFor("EPDFRotate"); + // 1) Check for /EMBD_Metadata rotation + unrotated rect first. + float rotate_deg = GetEmbedMetadataFloatFor(ad.Get(), "Rotation"); rotate_deg = fmod(fmod(rotate_deg, 360.0f) + 360.0f, 360.0f); bool has_rotation = (rotate_deg > 0.01f && rotate_deg < 359.99f); - CFX_FloatRect unrotated = ad->GetRectFor("EPDFUnrotatedRect"); + CFX_FloatRect unrotated = GetEmbedMetadataRectFor(ad.Get(), "UnrotatedRect"); bool use_rotation = has_rotation && !unrotated.IsEmpty(); // Use unrotated rect for image fitting when rotated, otherwise /Rect. - CFX_FloatRect rect = use_rotation ? unrotated : ad->GetRectFor(pdfium::annotation::kRect); + CFX_FloatRect rect = + use_rotation ? unrotated : ad->GetRectFor(pdfium::annotation::kRect); const float box_w = std::max(0.f, rect.Width()); const float box_h = std::max(0.f, rect.Height()); - if (box_w <= 0 || box_h <= 0) + if (box_w <= 0 || box_h <= 0) { return false; + } // 2) Fetch/create AP(N). RetainPtr ap = @@ -3703,18 +4194,21 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { if (!ap) { CPDF_GenerateAP::GenerateEmptyAP(ctx->GetPage()->GetDocument(), ad.Get()); ap = GetAnnotAP(ad.Get(), CPDF_Annot::AppearanceMode::kNormal); - if (!ap) + if (!ap) { return false; + } } // 3) Get the AP dict. RetainPtr ap_dict = ap->GetMutableDict(); - if (!ap_dict) + if (!ap_dict) { return false; + } CPDF_Document* doc = ctx->GetPage()->GetDocument(); - if (!doc) + if (!doc) { return false; + } // 4) On first call, wrap the original AP content into a child Form XObject // and record the painted content rect in EPDFOrigContentRect. This rect @@ -3724,37 +4218,40 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { CFX_FloatRect content_rect = ap_dict->GetRectFor("EPDFOrigContentRect"); if (content_rect.IsEmpty()) { content_rect = GetFormDisplayBox(ap_dict.Get()); - if (content_rect.IsEmpty()) + if (content_rect.IsEmpty()) { content_rect = GetPaintedFormBounds(doc, ap.Get()); + } content_rect.Normalize(); - if (content_rect.IsEmpty() || - content_rect.Width() <= 0 || content_rect.Height() <= 0) { + if (content_rect.IsEmpty() || content_rect.Width() <= 0 || + content_rect.Height() <= 0) { return false; } ap_dict->SetRectFor("EPDFOrigContentRect", content_rect); - if (!WrapAPContentIntoFormXObject(ap.Get(), doc)) + if (!WrapAPContentIntoFormXObject(ap.Get(), doc)) { return false; + } } const float orig_w = content_rect.Width(); const float orig_h = content_rect.Height(); - if (orig_w <= 0 || orig_h <= 0) + if (orig_w <= 0 || orig_h <= 0) { return false; + } // 5) Compute placement matrix using the same fit logic as before. float drawn_w, drawn_h, dx, dy; - if (!FitImageIntoBox(box_w, box_h, orig_w, orig_h, fit_cpp, - &drawn_w, &drawn_h, &dx, &dy)) { + if (!FitImageIntoBox(box_w, box_h, orig_w, orig_h, fit_cpp, &drawn_w, + &drawn_h, &dx, &dy)) { return false; } // 6) Write the wrapper content stream: q sx 0 0 sy tx ty cm /EPDFWRAP Do Q // Form XObjects render in their own coordinate space. content_rect.left // and .bottom are the offset of the painted content within the child form, - // so the translation compensates for that offset after scaling to align the - // visible content's origin with the target placement box. + // so the translation compensates for that offset after scaling to align + // the visible content's origin with the target placement box. { const float sx = drawn_w / orig_w; const float sy = drawn_h / orig_h; @@ -3780,10 +4277,10 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { const float cx = (unrotated.left + unrotated.right) / 2.0f; const float cy = (unrotated.bottom + unrotated.top) / 2.0f; // M = T(cx,cy) * R(theta) * T(-cx,-cy) - ap_dict->SetMatrixFor("Matrix", CFX_Matrix( - cos_t, sin_t, -sin_t, cos_t, - cx * (1.0f - cos_t) + cy * sin_t, - cy * (1.0f - cos_t) - cx * sin_t)); + ap_dict->SetMatrixFor("Matrix", + CFX_Matrix(cos_t, sin_t, -sin_t, cos_t, + cx * (1.0f - cos_t) + cy * sin_t, + cy * (1.0f - cos_t) - cx * sin_t)); } else { ap_dict->RemoveFor("Matrix"); } @@ -3794,32 +4291,36 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_CreateAnnot(FPDF_PAGE page, FPDF_ANNOTATION_SUBTYPE subtype) { CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage || !FPDFAnnot_IsSupportedSubtype(subtype)) + if (!pPage || !FPDFAnnot_IsSupportedSubtype(subtype)) { return nullptr; + } CPDF_Document* doc = pPage->GetDocument(); // Create the annotation dictionary as an INDIRECT object RetainPtr dict = doc->NewIndirect(); dict->SetNewFor(pdfium::annotation::kType, "Annot"); - dict->SetNewFor( - pdfium::annotation::kSubtype, - CPDF_Annot::AnnotSubtypeToString(static_cast(subtype))); + dict->SetNewFor(pdfium::annotation::kSubtype, + CPDF_Annot::AnnotSubtypeToString( + static_cast(subtype))); // Append a REFERENCE to /Annots instead of the direct dict RetainPtr annots = pPage->GetOrCreateAnnotsArray(); annots->AppendNew(doc, dict->GetObjNum()); // Build the public handle - auto ctx = std::make_unique(dict, IPDFPageFromFPDFPage(page)); + auto ctx = + std::make_unique(dict, IPDFPageFromFPDFPage(page)); return FPDFAnnotationFromCPDFAnnotContext(ctx.release()); } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation) { - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, + float rotation) { + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } if (rotation == 0.0f) { // 0 is the default, so remove the key to keep the PDF clean. @@ -3830,18 +4331,20 @@ EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation) { return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, float* rotation) { - if (!rotation) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, + float* rotation) { + if (!rotation) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } *rotation = dict->GetFloatFor("Rotate"); return true; -} +} FPDF_EXPORT FPDF_ANNOT_REPLY_TYPE FPDF_CALLCONV EPDFAnnot_GetReplyType(FPDF_ANNOTATION annot) { @@ -3859,7 +4362,8 @@ EPDFAnnot_GetReplyType(FPDF_ANNOTATION annot) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetReplyType(FPDF_ANNOTATION annot, FPDF_ANNOT_REPLY_TYPE rt) { - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); if (!dict) { return false; } @@ -3883,12 +4387,15 @@ EPDFAnnot_SetReplyType(FPDF_ANNOTATION annot, FPDF_ANNOT_REPLY_TYPE rt) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetOverlayText(FPDF_ANNOTATION annot, FPDF_WIDESTRING text) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return false; + } - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } if (!text || !*text) { dict->RemoveFor("OverlayText"); @@ -3905,12 +4412,14 @@ FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetOverlayText(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return 0; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return 0; + } WideString text = dict->GetUnicodeTextFor("OverlayText"); // SAFETY: required from caller. @@ -3920,12 +4429,15 @@ EPDFAnnot_GetOverlayText(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetOverlayTextRepeat(FPDF_ANNOTATION annot, FPDF_BOOL repeat) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return false; + } - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } if (repeat) { dict->SetNewFor("Repeat", true); @@ -3937,291 +4449,16 @@ EPDFAnnot_SetOverlayTextRepeat(FPDF_ANNOTATION annot, FPDF_BOOL repeat) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) - return false; - - const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) - return false; - - return dict->GetBooleanFor("Repeat", false); -} - -namespace { - -// Helper to extract redaction rectangles from a REDACT annotation. -// Returns QuadPoints if present, otherwise falls back to Rect. -std::vector GetRedactRectsFromAnnotDict( - const CPDF_Dictionary* annot_dict) { - std::vector rects; - if (!annot_dict) - return rects; - - // Try QuadPoints first (for text-based redactions) - RetainPtr quad_points_array = - annot_dict->GetArrayFor("QuadPoints"); - if (quad_points_array && quad_points_array->size() >= 8) { - 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()) - rects.push_back(rect); - } - if (!rects.empty()) - return rects; - } - - // Fall back to Rect (for area-based redactions) - CFX_FloatRect rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - rect.Normalize(); - if (!rect.IsEmpty()) - rects.push_back(rect); - - return rects; -} - -// Internal helper to flatten any Form XObject stream to page content. -// Used by EPDFAnnot_Flatten (for AP/N) and EPDFAnnot_ApplyRedaction (for RO). -void FlattenFormXObjectToPage(CPDF_Page* page, - RetainPtr form_stream, - const CFX_FloatRect& target_rect) { - if (!page || !form_stream) - return; - - CPDF_Document* doc = page->GetDocument(); - if (!doc) - return; - - // Get the form dictionary from the stream - RetainPtr form_dict = form_stream->GetDict(); - if (!form_dict) - return; - - // Get the BBox from the form stream - CFX_FloatRect form_bbox = form_dict->GetRectFor("BBox"); - form_bbox.Normalize(); - if (form_bbox.IsEmpty()) - form_bbox = target_rect; - - // Calculate the transformation matrix to position the form at the target rect - // The form's content is defined in BBox coordinates, we need to map it to target_rect - float scale_x = 1.0f; - float scale_y = 1.0f; - if (form_bbox.Width() > 0) - scale_x = target_rect.Width() / form_bbox.Width(); - if (form_bbox.Height() > 0) - scale_y = target_rect.Height() / form_bbox.Height(); - - CFX_Matrix form_matrix; - form_matrix.a = scale_x; - form_matrix.d = scale_y; - form_matrix.e = target_rect.left - form_bbox.left * scale_x; - form_matrix.f = target_rect.bottom - form_bbox.bottom * scale_y; - - // Create a CPDF_Form from the stream - auto form = std::make_unique( - doc, - page->GetMutableResources(), - pdfium::WrapRetain(const_cast(form_stream.Get()))); - form->ParseContent(); - - // Create a FormObject that wraps the form - auto form_obj = std::make_unique( - CPDF_PageObject::kNoContentStream, - std::move(form), - form_matrix); - - form_obj->CalcBoundingBox(); - form_obj->SetDirty(true); - - page->AppendPageObject(std::move(form_obj)); -} - -// Find the index of an annotation in the page's annotation array. -// Returns -1 if not found. -int GetAnnotIndexOnPage(CPDF_Page* page, const CPDF_Dictionary* annot_dict) { - if (!page || !annot_dict) - return -1; - - RetainPtr annots = page->GetMutableAnnotsArray(); - 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_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) - return false; - - // Must be a REDACT annotation - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) - return false; - - const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) - return false; - - // 1. Extract redaction rectangles from QuadPoints or Rect - std::vector rects = GetRedactRectsFromAnnotDict(annot_dict); - if (rects.empty()) - return false; - - // 2. Remove content using existing redactor (no black boxes - we use RO) - RedactTextInRects(pPage, pdfium::span(rects), - /*recurse_forms=*/true, - /*draw_black_boxes=*/false); - - // 3. Flatten RO stream if present - RetainPtr ro_stream = annot_dict->GetStreamFor("RO"); - if (ro_stream) { - CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - FlattenFormXObjectToPage(pPage, ro_stream, annot_rect); - } - // If no RO: content is removed but no overlay is added - - // 4. Remove the annotation from the page - int annot_index = GetAnnotIndexOnPage(pPage, annot_dict); - if (annot_index >= 0) { - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (annots) { - RetainPtr entry = - annots->GetMutableObjectAt(annot_index); - uint32_t objnum = 0; - if (entry && entry->IsReference()) { - objnum = entry->AsReference()->GetRefObjNum(); - } else if (entry) { - objnum = entry->GetObjNum(); - } - annots->RemoveAt(annot_index); - if (objnum) - pPage->GetDocument()->DeleteIndirectObject(objnum); - } - } - - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactions(FPDF_PAGE page) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) - return false; - - // First pass: collect all redaction areas, RO streams, indices, and objnums - std::vector all_rects; - std::vector, CFX_FloatRect>> ro_streams; - std::vector> redact_index_objnums; - - RetainPtr annot_list = pPage->GetMutableAnnotsArray(); - if (!annot_list || annot_list->IsEmpty()) - return false; - - for (size_t i = 0; i < annot_list->size(); ++i) { - RetainPtr entry = annot_list->GetMutableObjectAt(i); - RetainPtr annot_dict = - ToDictionary(entry ? entry->GetMutableDirect() : nullptr); - if (!annot_dict) - continue; - - // Check if this is a REDACT annotation - ByteString subtype = annot_dict->GetNameFor(pdfium::annotation::kSubtype); - if (subtype != "Redact") - continue; - - // Track index and indirect object number for later removal - uint32_t objnum = 0; - if (entry && entry->IsReference()) { - objnum = entry->AsReference()->GetRefObjNum(); - } else if (annot_dict) { - objnum = annot_dict->GetObjNum(); - } - redact_index_objnums.push_back({i, objnum}); - - // Extract rectangles - std::vector rects = GetRedactRectsFromAnnotDict(annot_dict.Get()); - for (const auto& rect : rects) { - all_rects.push_back(rect); - } - - // Collect RO stream if present - RetainPtr ro_stream = annot_dict->GetStreamFor("RO"); - if (ro_stream) { - CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - ro_streams.push_back({ro_stream, annot_rect}); - } - } - - if (all_rects.empty()) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return false; - - // Remove content for all redaction areas at once - RedactTextInRects(pPage, pdfium::span(all_rects), - /*recurse_forms=*/true, - /*draw_black_boxes=*/false); - - // Flatten all RO streams - for (const auto& [ro_stream, annot_rect] : ro_streams) { - FlattenFormXObjectToPage(pPage, ro_stream, annot_rect); - } - - // Remove all REDACT annotations (in reverse order to maintain indices) - // and delete the underlying indirect objects to avoid orphans in the xref. - for (auto it = redact_index_objnums.rbegin(); it != redact_index_objnums.rend(); ++it) { - annot_list->RemoveAt(it->first); - if (it->second) - pPage->GetDocument()->DeleteIndirectObject(it->second); } - return true; -} - -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) + const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; - - CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - - FlattenFormXObjectToPage(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; + return dict->GetBooleanFor("Repeat", false); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV @@ -4229,44 +4466,51 @@ EPDFAnnot_SetAppearanceFromPage(FPDF_ANNOTATION annot, FPDF_DOCUMENT src_doc_handle, int page_index) { CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!ctx) + if (!ctx) { return false; + } RetainPtr annot_dict = ctx->GetMutableAnnotDict(); IPDF_Page* annot_page = ctx->GetPage(); CPDF_Document* dest_doc = annot_page ? annot_page->GetDocument() : nullptr; - if (!annot_dict || !dest_doc) + if (!annot_dict || !dest_doc) { return false; + } CPDF_Document* src_doc = CPDFDocumentFromFPDFDocument(src_doc_handle); - if (!src_doc) + if (!src_doc) { return false; + } RetainPtr src_page_dict = src_doc->GetMutablePageDictionary(page_index); - if (!src_page_dict) + if (!src_page_dict) { return false; + } CFX_FloatRect media_box = src_page_dict->GetRectFor("MediaBox"); media_box.Normalize(); - if (media_box.IsEmpty()) + if (media_box.IsEmpty()) { return false; + } // Collect page content bytes (Contents can be a stream or an array of // streams). RetainPtr contents_obj = src_page_dict->GetObjectFor("Contents"); - if (!contents_obj) + if (!contents_obj) { return false; + } DataVector content_data; const CPDF_Object* direct = contents_obj->GetDirect(); - if (!direct) + if (!direct) { return false; + } if (direct->IsStream()) { - auto acc = - pdfium::MakeRetain(pdfium::WrapRetain(direct->AsStream())); + auto acc = pdfium::MakeRetain( + pdfium::WrapRetain(direct->AsStream())); acc->LoadAllDataFiltered(); auto span = acc->GetSpan(); content_data.assign(span.begin(), span.end()); @@ -4274,18 +4518,21 @@ EPDFAnnot_SetAppearanceFromPage(FPDF_ANNOTATION annot, const CPDF_Array* arr = direct->AsArray(); for (size_t i = 0; i < arr->size(); ++i) { RetainPtr stream = arr->GetStreamAt(i); - if (!stream) + if (!stream) { continue; + } auto acc = pdfium::MakeRetain(std::move(stream)); acc->LoadAllDataFiltered(); auto span = acc->GetSpan(); - if (!content_data.empty()) + if (!content_data.empty()) { content_data.push_back(' '); + } content_data.insert(content_data.end(), span.begin(), span.end()); } } - if (content_data.empty()) + if (content_data.empty()) { return false; + } // Build a Form XObject stream in the source document so that // AnnotAppearanceExporter can deep-clone it with all resource dependencies. @@ -4296,47 +4543,49 @@ EPDFAnnot_SetAppearanceFromPage(FPDF_ANNOTATION annot, RetainPtr src_resources = src_page_dict->GetDictFor("Resources"); - if (src_resources) + if (src_resources) { xobj_dict->SetFor("Resources", src_resources->Clone()); + } - auto src_stream = - src_doc->NewIndirect(std::move(xobj_dict)); + auto src_stream = src_doc->NewIndirect(std::move(xobj_dict)); src_stream->SetData(content_data); // Clone the stream (and all its resource references) into dest_doc. AnnotAppearanceExporter exporter(dest_doc, src_doc); - RetainPtr cloned_stream = - exporter.ExportFormXObject(src_stream); + RetainPtr cloned_stream = exporter.ExportFormXObject(src_stream); // Clean up temporary object from source document. src_doc->DeleteIndirectObject(src_stream->GetObjNum()); - if (!cloned_stream) + if (!cloned_stream) { return false; + } RetainPtr cloned_dict = cloned_stream->GetMutableDict(); - if (!cloned_dict) + if (!cloned_dict) { return false; + } // Persist the imported appearance's painted content rect before any later // annotation resize mutates /Rect. This captures where the visible content // lives inside the child form's coordinate space so the wrapper can align it. CFX_FloatRect content_rect = GetFormDisplayBox(cloned_dict.Get()); - if (content_rect.IsEmpty()) + if (content_rect.IsEmpty()) { content_rect = GetPaintedFormBounds(dest_doc, cloned_stream.Get()); + } content_rect.Normalize(); if (!content_rect.IsEmpty()) { cloned_dict->SetRectFor("EPDFOrigContentRect", content_rect); - if (!WrapAPContentIntoFormXObject(cloned_stream.Get(), dest_doc)) + if (!WrapAPContentIntoFormXObject(cloned_stream.Get(), dest_doc)) { return false; + } } // Set cloned stream as AP/N on the annotation. RetainPtr ap_dict = annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - ap_dict->SetNewFor("N", dest_doc, - cloned_stream->GetObjNum()); + ap_dict->SetNewFor("N", dest_doc, cloned_stream->GetObjNum()); return true; } @@ -4344,19 +4593,23 @@ EPDFAnnot_SetAppearanceFromPage(FPDF_ANNOTATION annot, FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV EPDFAnnot_ExportAppearanceAsDocument(FPDF_ANNOTATION annot) { CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!ctx) + if (!ctx) { return nullptr; + } - RetainPtr annot_dict = ctx->GetMutableAnnotDict(); IPDF_Page* src_page = ctx->GetPage(); CPDF_Document* src_doc = src_page ? src_page->GetDocument() : nullptr; - if (!annot_dict || !src_doc) + CPDF_DocumentViewScope document_view(src_doc); + const CPDF_Dictionary* annot_dict = ctx->GetAnnotDict(); + if (!annot_dict || !src_doc) { return nullptr; + } RetainPtr ap_stream = - GetAnnotAP(annot_dict.Get(), CPDF_Annot::AppearanceMode::kNormal); - if (!ap_stream) + GetAnnotAP(annot_dict, CPDF_Annot::AppearanceMode::kNormal); + if (!ap_stream) { return nullptr; + } CFX_FloatRect bbox = ap_stream->GetDict()->GetRectFor("BBox"); bbox.Normalize(); @@ -4364,17 +4617,20 @@ EPDFAnnot_ExportAppearanceAsDocument(FPDF_ANNOTATION annot) { bbox = annot_dict->GetRectFor(pdfium::annotation::kRect); bbox.Normalize(); } - if (bbox.IsEmpty()) + if (bbox.IsEmpty()) { return nullptr; + } const float page_width = bbox.Width(); const float page_height = bbox.Height(); - if (page_width <= 0 || page_height <= 0) + if (page_width <= 0 || page_height <= 0) { return nullptr; + } FPDF_DOCUMENT exported_doc = FPDF_CreateNewDocument(); - if (!exported_doc) + if (!exported_doc) { return nullptr; + } CPDF_Document* dest_doc = CPDFDocumentFromFPDFDocument(exported_doc); if (!dest_doc) { @@ -4389,7 +4645,8 @@ EPDFAnnot_ExportAppearanceAsDocument(FPDF_ANNOTATION annot) { return nullptr; } - FPDF_PAGE exported_page = FPDFPage_New(exported_doc, 0, page_width, page_height); + FPDF_PAGE exported_page = + FPDFPage_New(exported_doc, 0, page_width, page_height); if (!exported_page) { FPDF_CloseDocument(exported_doc); return nullptr; @@ -4404,12 +4661,12 @@ EPDFAnnot_ExportAppearanceAsDocument(FPDF_ANNOTATION annot) { CFX_Matrix form_matrix = cloned_stream->GetDict()->GetMatrixFor("Matrix"); - auto form = std::make_unique(dest_doc, dest_page->GetMutableResources(), - std::move(cloned_stream)); + auto form = std::make_unique( + dest_doc, dest_page->GetMutableResources(), std::move(cloned_stream)); form->ParseContent(); - CFX_PointF mapped_origin = form_matrix.Transform( - CFX_PointF(bbox.left, bbox.bottom)); + CFX_PointF mapped_origin = + form_matrix.Transform(CFX_PointF(bbox.left, bbox.bottom)); auto form_obj = std::make_unique( CPDF_PageObject::kNoContentStream, std::move(form), @@ -4431,18 +4688,22 @@ EPDFAnnot_ExportAppearanceAsDocument(FPDF_ANNOTATION annot) { FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, int annot_count) { - if (!annots || annot_count <= 0) + if (!annots || annot_count <= 0) { return nullptr; + } // Validate first annotation and extract source page/document. CPDF_AnnotContext* first_ctx = CPDFAnnotContextFromFPDFAnnotation(annots[0]); - if (!first_ctx) + if (!first_ctx) { return nullptr; + } IPDF_Page* src_page = first_ctx->GetPage(); CPDF_Document* src_doc = src_page ? src_page->GetDocument() : nullptr; - if (!src_doc) + if (!src_doc) { return nullptr; + } + CPDF_DocumentViewScope document_view(src_doc); struct AnnotInfo { RetainPtr ap_stream; @@ -4458,22 +4719,26 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, for (int i = 0; i < annot_count; i++) { CPDF_AnnotContext* ctx = CPDFAnnotContextFromFPDFAnnotation(annots[i]); - if (!ctx) + if (!ctx) { return nullptr; + } // All annotations must share the same source document. IPDF_Page* page_i = ctx->GetPage(); - if (!page_i || page_i->GetDocument() != src_doc) + if (!page_i || page_i->GetDocument() != src_doc) { return nullptr; + } - RetainPtr annot_dict = ctx->GetMutableAnnotDict(); - if (!annot_dict) + const CPDF_Dictionary* annot_dict = ctx->GetAnnotDict(); + if (!annot_dict) { return nullptr; + } RetainPtr ap_stream = - GetAnnotAP(annot_dict.Get(), CPDF_Annot::AppearanceMode::kNormal); - if (!ap_stream) + GetAnnotAP(annot_dict, CPDF_Annot::AppearanceMode::kNormal); + if (!ap_stream) { return nullptr; + } CFX_FloatRect ap_bbox = ap_stream->GetDict()->GetRectFor("BBox"); ap_bbox.Normalize(); @@ -4481,14 +4746,16 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, ap_bbox = annot_dict->GetRectFor(pdfium::annotation::kRect); ap_bbox.Normalize(); } - if (ap_bbox.IsEmpty()) + if (ap_bbox.IsEmpty()) { return nullptr; + } CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); annot_rect.Normalize(); - if (annot_rect.IsEmpty()) + if (annot_rect.IsEmpty()) { return nullptr; + } if (first) { combined_rect = annot_rect; @@ -4502,12 +4769,14 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, const float page_width = combined_rect.Width(); const float page_height = combined_rect.Height(); - if (page_width <= 0 || page_height <= 0) + if (page_width <= 0 || page_height <= 0) { return nullptr; + } FPDF_DOCUMENT exported_doc = FPDF_CreateNewDocument(); - if (!exported_doc) + if (!exported_doc) { return nullptr; + } CPDF_Document* dest_doc = CPDFDocumentFromFPDFDocument(exported_doc); if (!dest_doc) { @@ -4543,8 +4812,7 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, CFX_Matrix form_matrix = cloned_stream->GetDict()->GetMatrixFor("Matrix"); auto form = std::make_unique( - dest_doc, dest_page->GetMutableResources(), - std::move(cloned_stream)); + dest_doc, dest_page->GetMutableResources(), std::move(cloned_stream)); form->ParseContent(); CFX_PointF mapped_origin = form_matrix.Transform( @@ -4552,10 +4820,10 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, const float sx = info.annot_rect.Width() / info.ap_bbox.Width(); const float sy = info.annot_rect.Height() / info.ap_bbox.Height(); - const float tx = (info.annot_rect.left - combined_rect.left) - - mapped_origin.x * sx; - const float ty = (info.annot_rect.bottom - combined_rect.bottom) - - mapped_origin.y * sy; + const float tx = + (info.annot_rect.left - combined_rect.left) - mapped_origin.x * sx; + const float ty = + (info.annot_rect.bottom - combined_rect.bottom) - mapped_origin.y * sy; auto form_obj = std::make_unique( CPDF_PageObject::kNoContentStream, std::move(form), @@ -4575,112 +4843,56 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, return exported_doc; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation) { - RetainPtr dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRect(FPDF_ANNOTATION annot, + FS_RECTF* rect) { + if (!FPDFAnnot_GetRect(annot, rect)) { return false; - - if (rotation == 0.0f) { - dict->RemoveFor("EPDFRotate"); - } else { - dict->SetNewFor("EPDFRotate", rotation); } + + // Normalize: upstream FPDFAnnot_GetRect does not normalize the rect read + // from the dictionary. PDFs may store Rect as [x1,y1,x2,y2] with y1>y2, + // resulting in inverted top/bottom. CFX_FloatRect::Normalize() fixes this. + CFX_FloatRect float_rect = CFXFloatRectFromFSRectF(*rect); + float_rect.Normalize(); + *rect = FSRectFFromCFXFloatRect(float_rect); return true; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetExtendedRotation(FPDF_ANNOTATION annot, float* rotation) { - if (!rotation) +EPDFAnnot_SetAPMatrix(FPDF_ANNOTATION annot, + FPDF_ANNOT_APPEARANCEMODE appearanceMode, + const FS_MATRIX* matrix) { + if (!matrix) { return false; - - const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + } + if (appearanceMode < 0 || appearanceMode >= FPDF_ANNOT_APPEARANCEMODE_COUNT) { return false; + } - *rotation = dict->GetFloatFor("EPDFRotate"); - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetUnrotatedRect(FPDF_ANNOTATION annot, const FS_RECTF* rect) { RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; - - if (!rect) { - dict->RemoveFor("EPDFUnrotatedRect"); - return true; } - CFX_FloatRect float_rect = CFXFloatRectFromFSRectF(*rect); - if (float_rect.IsEmpty()) { - dict->RemoveFor("EPDFUnrotatedRect"); - } else { - dict->SetRectFor("EPDFUnrotatedRect", float_rect); + // Map mode to AP stream key: N, R, D + static constexpr auto kModeKey = std::to_array({"N", "R", "D"}); + RetainPtr ap = + dict->GetMutableDictFor(pdfium::annotation::kAP); + if (!ap) { + return false; } - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect) { - if (!rect) - return false; - - const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) - return false; - - *rect = FSRectFFromCFXFloatRect(dict->GetRectFor("EPDFUnrotatedRect")); - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRect(FPDF_ANNOTATION annot, FS_RECTF* rect) { - if (!FPDFAnnot_GetRect(annot, rect)) - return false; - - // Normalize: upstream FPDFAnnot_GetRect does not normalize the rect read - // from the dictionary. PDFs may store Rect as [x1,y1,x2,y2] with y1>y2, - // resulting in inverted top/bottom. CFX_FloatRect::Normalize() fixes this. - CFX_FloatRect float_rect = CFXFloatRectFromFSRectF(*rect); - float_rect.Normalize(); - *rect = FSRectFFromCFXFloatRect(float_rect); - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetAPMatrix(FPDF_ANNOTATION annot, - FPDF_ANNOT_APPEARANCEMODE appearanceMode, - const FS_MATRIX* matrix) { - if (!matrix) - return false; - if (appearanceMode < 0 || appearanceMode >= FPDF_ANNOT_APPEARANCEMODE_COUNT) - return false; - - RetainPtr dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) - return false; - - // Map mode to AP stream key: N, R, D - static constexpr auto kModeKey = - std::to_array({"N", "R", "D"}); - RetainPtr ap = - dict->GetMutableDictFor(pdfium::annotation::kAP); - if (!ap) - return false; RetainPtr stream = ap->GetMutableStreamFor(kModeKey[appearanceMode]); - if (!stream) + if (!stream) { return false; + } RetainPtr stream_dict = stream->GetMutableDict(); - if (!stream_dict) + if (!stream_dict) { return false; + } stream_dict->SetMatrixFor("Matrix", CFXMatrixFromFSMatrix(*matrix)); return true; @@ -4690,31 +4902,36 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetAPMatrix(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode, FS_MATRIX* matrix) { - if (!matrix) + if (!matrix) { return false; - if (appearanceMode < 0 || appearanceMode >= FPDF_ANNOT_APPEARANCEMODE_COUNT) + } + if (appearanceMode < 0 || appearanceMode >= FPDF_ANNOT_APPEARANCEMODE_COUNT) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } // Map mode to AP stream key: N, R, D - static constexpr auto kModeKey = - std::to_array({"N", "R", "D"}); + static constexpr auto kModeKey = std::to_array({"N", "R", "D"}); RetainPtr ap = dict->GetDictFor(pdfium::annotation::kAP); - if (!ap) + if (!ap) { return false; + } RetainPtr stream = ap->GetStreamFor(kModeKey[appearanceMode]); - if (!stream) + if (!stream) { return false; + } RetainPtr stream_dict = stream->GetDict(); - if (!stream_dict) + if (!stream_dict) { return false; + } *matrix = FSMatrixFromCFXMatrix(stream_dict->GetMatrixFor("Matrix")); return true; @@ -4722,36 +4939,40 @@ EPDFAnnot_GetAPMatrix(FPDF_ANNOTATION annot, FPDF_EXPORT int FPDF_CALLCONV EPDFAnnot_GetAvailableAppearanceModes(FPDF_ANNOTATION annot) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); + if (!pAnnotDict) { return 0; + } RetainPtr pAP = pAnnotDict->GetDictFor(pdfium::annotation::kAP); - if (!pAP) + if (!pAP) { return 0; + } int modes = 0; - if (pAP->KeyExist("N")) + if (pAP->KeyExist("N")) { modes |= 1; // bit 0 = Normal - if (pAP->KeyExist("R")) + } + if (pAP->KeyExist("R")) { modes |= 2; // bit 1 = Rollover - if (pAP->KeyExist("D")) + } + if (pAP->KeyExist("D")) { modes |= 4; // bit 2 = Down + } return modes; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_HasAppearanceStream(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); + if (!pAnnotDict) { return false; + } auto mode = static_cast(appearanceMode); - return !!GetAnnotAP(pAnnotDict.Get(), mode); + return !!GetAnnotAP(pAnnotDict, mode); } static ByteString GetMKColorKey(EPDF_MK_COLORTYPE type) { @@ -4764,16 +4985,16 @@ static ByteString GetMKColorKey(EPDF_MK_COLORTYPE type) { return "BC"; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict || R > 255 || G > 255 || B > 255) + if (!pAnnotDict || R > 255 || G > 255 || B > 255) { return false; + } RetainPtr pMK = pAnnotDict->GetOrCreateDictFor("MK"); ByteString key = GetMKColorKey(type); @@ -4792,27 +5013,30 @@ EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B) { - if (!R || !G || !B) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B) { + if (!R || !G || !B) { return false; + } const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } RetainPtr pMK = pAnnotDict->GetDictFor("MK"); - if (!pMK) + if (!pMK) { return false; + } ByteString key = GetMKColorKey(type); RetainPtr pColor = pMK->GetArrayFor(key.AsStringView()); - if (!pColor || pColor->size() < 3) + if (!pColor || pColor->size() < 3) { return false; + } *R = static_cast(pColor->GetFloatAt(0) * 255.f + 0.5f); *G = static_cast(pColor->GetFloatAt(1) * 255.f + 0.5f); @@ -4825,12 +5049,14 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearMKColor(FPDF_ANNOTATION annot, EPDF_MK_COLORTYPE type) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } RetainPtr pMK = pAnnotDict->GetMutableDictFor("MK"); - if (!pMK) + if (!pMK) { return true; + } ByteString key = GetMKColorKey(type); pMK->RemoveFor(key.AsStringView()); @@ -4838,124 +5064,22 @@ 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); - if (!pContext) + if (!pContext) { return false; + } RetainPtr pAnnotDict = pContext->GetMutableAnnotDict(); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } CPDF_Document* pDoc = pContext->GetPage()->GetDocument(); - if (!pDoc) + if (!pDoc) { return false; + } // Look for /FT on this dict or on /Parent ByteString ft; @@ -4969,8 +5093,9 @@ EPDFAnnot_GenerateFormFieldAP(FPDF_ANNOTATION annot) { pLookup = pLookup->GetDictFor("Parent"); } - if (ft.IsEmpty()) + if (ft.IsEmpty()) { return false; + } uint32_t ff = 0; RetainPtr pFfLookup = pAnnotDict; @@ -5012,206 +5137,30 @@ 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) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { return 0; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return 0; + } RetainPtr cl = dict->GetArrayFor("CL"); - if (!cl) + if (!cl) { return 0; + } // /CL must have 4 (2 points) or 6 (3 points) numbers. const size_t sz = cl->size(); - if (sz == 4) + if (sz == 4) { return 2; - if (sz >= 6) + } + if (sz >= 6) { return 3; + } return 0; } @@ -5219,25 +5168,29 @@ FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetCalloutLine(FPDF_ANNOTATION annot, FS_POINTF* buffer, unsigned long length) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { return 0; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return 0; + } RetainPtr cl = dict->GetArrayFor("CL"); - if (!cl) + if (!cl) { return 0; + } const size_t sz = cl->size(); unsigned long points_len = 0; - if (sz == 4) + if (sz == 4) { points_len = 2; - else if (sz >= 6) + } else if (sz >= 6) { points_len = 3; - else + } else { return 0; + } if (buffer && length >= points_len) { auto buffer_span = UNSAFE_BUFFERS(pdfium::span(buffer, length)); @@ -5253,12 +5206,15 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetCalloutLine(FPDF_ANNOTATION annot, const FS_POINTF* points, unsigned long count) { - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { return false; + } - RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { return false; + } if (!points || count == 0) { dict->RemoveFor("CL"); @@ -5266,14 +5222,16 @@ EPDFAnnot_SetCalloutLine(FPDF_ANNOTATION annot, } // /CL must be 2 points (4 numbers) or 3 points (6 numbers). - if (count != 2 && count != 3) + if (count != 2 && count != 3) { return false; + } RetainPtr cl = dict->GetMutableArrayFor("CL"); - if (cl) + if (cl) { cl->Clear(); - else + } else { cl = dict->SetNewFor("CL"); + } auto pts = UNSAFE_BUFFERS(pdfium::span(points, count)); for (unsigned long i = 0; i < count; ++i) { @@ -5284,59 +5242,41 @@ 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); - if (!pCtx) + if (!pCtx) { return 0; + } const CPDF_Dictionary* dict = pCtx->GetAnnotDict(); - if (!dict) + if (!dict) { return 0; + } return dict->GetObjNum(); } FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) { - if (!page || obj_num == 0) + if (!page || obj_num == 0) { return nullptr; + } - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + ScopedFPDFPageView page_view(page); + if (!page_view) { return nullptr; + } + const CPDF_Page* pPage = page_view.Get(); - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) + RetainPtr annots = pPage->GetAnnotsArray(); + if (!annots) { return nullptr; + } for (size_t i = 0; i < annots->size(); ++i) { + RetainPtr const_dict = + ToDictionary(annots->GetDirectObjectAt(i)); RetainPtr d = - ToDictionary(annots->GetMutableDirectObjectAt(i)); + pdfium::WrapRetain(const_cast(const_dict.Get())); if (d && d->GetObjNum() == obj_num) { auto ctx = std::make_unique( std::move(d), IPDFPageFromFPDFPage(page)); @@ -5346,15 +5286,17 @@ EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) { return nullptr; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnot(FPDF_PAGE page, int index) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnot(FPDF_PAGE page, + int index) { CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage || index < 0) + if (!pPage || index < 0) { return false; + } RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots || static_cast(index) >= annots->size()) + if (!annots || static_cast(index) >= annots->size()) { return false; + } RetainPtr entry = annots->GetMutableObjectAt(index); RetainPtr dict = @@ -5369,31 +5311,36 @@ EPDFPage_RemoveAnnot(FPDF_PAGE page, int index) { annots->RemoveAt(index); - if (objnum) + if (objnum) { pPage->GetDocument()->DeleteIndirectObject(objnum); + } return true; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) { - if (!page || obj_num == 0) + if (!page || obj_num == 0) { return false; + } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return false; + } RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) + if (!annots) { return false; + } for (size_t i = 0; i < annots->size(); ++i) { RetainPtr entry = annots->GetMutableObjectAt(i); RetainPtr dict = ToDictionary(entry ? entry->GetMutableDirect() : nullptr); - if (!dict) + if (!dict) { continue; + } uint32_t entry_objnum = 0; if (entry && entry->IsReference()) { @@ -5401,13 +5348,15 @@ EPDFPage_RemoveAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) { } else { entry_objnum = dict->GetObjNum(); } - if (entry_objnum != obj_num) + if (entry_objnum != obj_num) { continue; + } annots->RemoveAt(i); - if (entry_objnum) + if (entry_objnum) { pPage->GetDocument()->DeleteIndirectObject(entry_objnum); + } return true; } @@ -5428,21 +5377,23 @@ EPDFPage_RemoveAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num) { // Any failure returns false without touching the array. The indirect // objects backing each annotation are never destroyed, so durable // identity (objectNumber, /NM) is preserved across the move. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_MoveAnnots(FPDF_PAGE page, - const int* from_indices, - int from_indices_len, - int to_index) { - if (!page || !from_indices || from_indices_len <= 0) +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_MoveAnnots(FPDF_PAGE page, + const int* from_indices, + int from_indices_len, + int to_index) { + if (!page || !from_indices || from_indices_len <= 0) { return false; + } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return false; + } RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) + if (!annots) { return false; + } const int count = static_cast(annots->size()); @@ -5453,17 +5404,20 @@ EPDFPage_MoveAnnots(FPDF_PAGE page, seen.reserve(static_cast(from_indices_len)); for (int i = 0; i < from_indices_len; ++i) { int idx = from_indices[i]; - if (idx < 0 || idx >= count) + if (idx < 0 || idx >= count) { return false; + } for (int s : seen) { - if (s == idx) + if (s == idx) { return false; + } } seen.push_back(idx); } const int post_count = count - from_indices_len; - if (to_index < 0 || to_index > post_count) + if (to_index < 0 || to_index > post_count) { return false; + } // 2. Detach each entry in caller order. RetainPtr keeps the underlying // CPDF_Object alive across the subsequent RemoveAt calls so the @@ -5488,8 +5442,7 @@ EPDFPage_MoveAnnots(FPDF_PAGE page, // a fresh reference to each entry; our local RetainPtr drops at // scope exit. for (int i = 0; i < from_indices_len; ++i) { - annots->InsertAt(static_cast(to_index + i), - std::move(entries[i])); + annots->InsertAt(static_cast(to_index + i), std::move(entries[i])); } return true; } diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index d23c3e35f5..ca5907d293 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -3,22 +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" @@ -27,15 +42,20 @@ #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" #include "testing/embedder_test_constants.h" #include "testing/fx_string_testhelpers.h" #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" +#include "testing/utils/file_util.h" #include "testing/utils/hash.h" +#include "testing/utils/path_service.h" using pdfium::kAnnotationStampWithApPng; +using testing::HasSubstr; namespace { @@ -46,6 +66,375 @@ const wchar_t kStreamData[] = L"223.7 732.4 c 232.6 729.9 242.0 730.8 251.2 730.8 c 257.5 730.8 " L"263.0 732.9 269.0 734.4 c S"; +std::wstring ExtractPageText(FPDF_PAGE page) { + ScopedFPDFTextPage text_page(FPDFText_LoadPage(page)); + if (!text_page) { + ADD_FAILURE() << "Failed to load text page"; + return L""; + } + + const int char_count = FPDFText_CountChars(text_page.get()); + std::vector buffer(char_count + 1); + EXPECT_GT(FPDFText_GetText(text_page.get(), 0, char_count, buffer.data()), 0); + return GetPlatformWString(buffer.data()); +} + +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(); } +}; + +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; + } + + std::copy_n(file_access->data_.data() + pos, size, buf); + return 1; + } + + 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(); + } + + 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())); +} + +ByteString GetNormalAppearanceStreamBytes(FPDF_ANNOTATION annot) { + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot); + if (!context) { + return ByteString(); + } + + const CPDF_Dictionary* annot_dict = context->GetAnnotDict(); + if (!annot_dict) { + return ByteString(); + } + + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr normal_stream = + ap_dict ? ap_dict->GetStreamFor("N") : nullptr; + if (!normal_stream) { + return ByteString(); + } + + 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( FPDF_FORMHANDLE form_handle, pdfium::span expected_subtypes) { @@ -345,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); @@ -413,6 +859,583 @@ TEST_F(FPDFAnnotEmbedderTest, RenderMultilineMarkupAnnotWithoutAP) { "annotation_markup_multiline_no_ap"); } +TEST_F(FPDFAnnotEmbedderTest, ReadPurityRenderMarkupAnnotWithoutAP) { + ASSERT_TRUE(OpenDocument("annotation_markup_multiline_no_ap.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + const uint32_t last_obj_num = doc->GetLastObjNum(); + + { + CPDF_ReadOnlyGraphGuard guard; + EXPECT_GT(FPDFPage_GetAnnotCount(page.get()), 0); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + ScopedFPDFBitmap bitmap = RenderLoadedPageWithFlags(page.get(), FPDF_ANNOT); + ASSERT_TRUE(bitmap); + } + + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); +} + +TEST_F(FPDFAnnotEmbedderTest, ExplicitGenerateAppearanceAllowed) { + ASSERT_TRUE(OpenDocument("annotation_markup_multiline_no_ap.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + const uint32_t before = doc->GetLastObjNum(); + + EXPECT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + + 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")); @@ -1516,6 +2539,71 @@ TEST_F(FPDFAnnotEmbedderTest, GetNumberValue) { } } +TEST_F(FPDFAnnotEmbedderTest, EmbedMetadata) { + ASSERT_TRUE(OpenDocument("text_form_multiple.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + + EXPECT_FALSE(EPDFAnnot_HasEmbedMetadata(annot.get())); + + ScopedFPDFWideString user_id = GetFPDFWideString(L"44"); + EXPECT_TRUE( + EPDFAnnot_SetEmbedMetadataString(annot.get(), "UserID", user_id.get())); + + unsigned long length_bytes = + EPDFAnnot_GetEmbedMetadataString(annot.get(), "UserID", nullptr, 0); + ASSERT_EQ(6u, length_bytes); + std::vector string_buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + EPDFAnnot_GetEmbedMetadataString( + annot.get(), "UserID", string_buffer.data(), length_bytes)); + EXPECT_EQ(L"44", GetPlatformWString(string_buffer.data())); + + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataNumber(annot.get(), "Rotation", 12.5f)); + float number_value = 0.0f; + EXPECT_TRUE( + EPDFAnnot_GetEmbedMetadataNumber(annot.get(), "Rotation", &number_value)); + EXPECT_FLOAT_EQ(12.5f, number_value); + + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataBoolean(annot.get(), "Archived", true)); + FPDF_BOOL boolean_value = false; + EXPECT_TRUE(EPDFAnnot_GetEmbedMetadataBoolean(annot.get(), "Archived", + &boolean_value)); + EXPECT_TRUE(boolean_value); + + const FS_RECTF rect{1.0f, 2.0f, 3.0f, 4.0f}; + EXPECT_TRUE( + EPDFAnnot_SetEmbedMetadataRect(annot.get(), "UnrotatedRect", &rect)); + FS_RECTF rect_value; + EXPECT_TRUE(EPDFAnnot_GetEmbedMetadataRect(annot.get(), "UnrotatedRect", + &rect_value)); + EXPECT_FLOAT_EQ(rect.left, rect_value.left); + EXPECT_FLOAT_EQ(rect.bottom, rect_value.bottom); + EXPECT_FLOAT_EQ(rect.right, rect_value.right); + EXPECT_FLOAT_EQ(rect.top, rect_value.top); + + ScopedFPDFWideString json = GetFPDFWideString(L"{\"source\":\"test\"}"); + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataJSON(annot.get(), json.get())); + length_bytes = EPDFAnnot_GetEmbedMetadataJSON(annot.get(), nullptr, 0); + ASSERT_EQ(36u, length_bytes); + string_buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, EPDFAnnot_GetEmbedMetadataJSON( + annot.get(), string_buffer.data(), length_bytes)); + EXPECT_EQ(L"{\"source\":\"test\"}", GetPlatformWString(string_buffer.data())); + + EXPECT_TRUE(EPDFAnnot_HasEmbedMetadata(annot.get())); + EXPECT_TRUE(EPDFAnnot_ClearEmbedMetadataKey(annot.get(), "UserID")); + EXPECT_TRUE(EPDFAnnot_HasEmbedMetadata(annot.get())); + EXPECT_EQ( + 2u, EPDFAnnot_GetEmbedMetadataString(annot.get(), "UserID", nullptr, 0)); + + EXPECT_TRUE(EPDFAnnot_ClearEmbedMetadata(annot.get())); + EXPECT_FALSE(EPDFAnnot_HasEmbedMetadata(annot.get())); +} + TEST_F(FPDFAnnotEmbedderTest, GetSetAP) { // Open a file with four annotations and load its first page. ASSERT_TRUE(OpenDocument("annotation_stamp_with_ap.pdf")); @@ -1960,8 +3048,6 @@ TEST_F(FPDFAnnotEmbedderTest, GetFormAnnotAndCheckFlagsComboBox) { } TEST_F(FPDFAnnotEmbedderTest, Bug1206) { - static constexpr size_t kExpectedMinimumOriginalSize = 1601; - ASSERT_TRUE(OpenDocument("bug_1206.pdf")); ScopedPage page = LoadScopedPage(0); @@ -1969,7 +3055,7 @@ TEST_F(FPDFAnnotEmbedderTest, Bug1206) { ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); const size_t original_size = GetString().size(); - EXPECT_LE(kExpectedMinimumOriginalSize, original_size); // Sanity check. + ASSERT_GT(original_size, 0u); ClearString(); for (size_t i = 0; i < 10; ++i) { @@ -1977,9 +3063,16 @@ TEST_F(FPDFAnnotEmbedderTest, Bug1206) { CompareBitmapWithExpectationSuffix(bitmap.get(), "bug_1206"); ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); - // TODO(https://crbug.com/42270200): This is wrong. The size should be - // equal, not bigger. - EXPECT_GT(GetString().size(), original_size); + EXPECT_EQ(original_size, GetString().size()); + + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + ScopedFPDFBitmap saved_bitmap = + RenderSavedPageWithFlags(saved_page.get(), FPDF_ANNOT); + CompareBitmapWithExpectationSuffix(saved_bitmap.get(), "bug_1206"); + ClearString(); } } @@ -2548,6 +3641,27 @@ TEST_F(FPDFAnnotEmbedderTest, GetFontSizeNegative) { } } +TEST_F(FPDFAnnotEmbedderTest, + DirectAnnotationObjectNumberStaysZeroAfterRender) { + ASSERT_TRUE(OpenDocument("freetext_annotation_without_da.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get())); + EXPECT_FALSE(EPDFPage_GetAnnotByObjectNumber(page.get(), 0)); + + ScopedFPDFBitmap bitmap = RenderLoadedPageWithFlags(page.get(), FPDF_ANNOT); + ASSERT_TRUE(bitmap); + EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get())); + + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 10, 20, 30)); + EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get())); +} + TEST_F(FPDFAnnotEmbedderTest, SetFontColor) { ASSERT_TRUE(OpenDocument("freetext_annotation_without_da.pdf")); ScopedPage page = LoadScopedPage(0); @@ -3204,6 +4318,424 @@ TEST_F(FPDFAnnotEmbedderTest, Redactannotation) { } } +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionRemovesTextInMiddleOfSentence) { + ASSERT_TRUE(OpenDocument("redact_text_middle.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + std::wstring before = ExtractPageText(page.get()); + EXPECT_NE(std::wstring::npos, before.find(L"hello")); + EXPECT_NE(std::wstring::npos, before.find(L"secret")); + EXPECT_NE(std::wstring::npos, before.find(L"world")); + ScopedFPDFBitmap before_bitmap = RenderLoadedPage(page.get()); + ASSERT_TRUE(before_bitmap); + const std::string before_hash = HashBitmap(before_bitmap.get()); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + ASSERT_TRUE(EPDFAnnot_ApplyRedaction(page.get(), annot.get(), nullptr)); + } + + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + 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); + ScopedFPDFBitmap after_bitmap = RenderSavedPage(saved_page); + ASSERT_TRUE(after_bitmap); + const std::string after_hash = HashBitmap(after_bitmap.get()); + CloseSavedPage(saved_page); + EXPECT_NE(std::wstring::npos, after.find(L"hello")); + EXPECT_EQ(std::wstring::npos, after.find(L"secret")); + EXPECT_NE(std::wstring::npos, after.find(L"world")); + EXPECT_NE(before_hash, after_hash); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionCountsIntersectingAnnotation) { + ASSERT_TRUE(OpenDocument("redact_remove_annots.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(2, FPDFPage_GetAnnotCount(page.get())); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + // 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())); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionPreservesSiblingRedactions) { + ASSERT_TRUE(OpenDocument("redact_preserve_sibling.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(3, FPDFPage_GetAnnotCount(page.get())); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + // 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())); + ScopedFPDFAnnotation remaining(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(remaining); + EXPECT_EQ(FPDF_ANNOT_REDACT, FPDFAnnot_GetSubtype(remaining.get())); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionDoesNotRemoveTouchOnlyAnnotation) { + ASSERT_TRUE(OpenDocument("redact_touch_only.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(2, FPDFPage_GetAnnotCount(page.get())); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + // 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())); + ScopedFPDFAnnotation remaining(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(remaining); + EXPECT_EQ(FPDF_ANNOT_SQUARE, FPDFAnnot_GetSubtype(remaining.get())); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyRedactionCascadesPopupRemoval) { + ASSERT_TRUE(OpenDocument("redact_popup_cascade.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(3, FPDFPage_GetAnnotCount(page.get())); + + { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + // 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, ApplyPageRedactionsCountsRemovedAnnotations) { + ASSERT_TRUE(OpenDocument("redact_apply_all_visible.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(4, FPDFPage_GetAnnotCount(page.get())); + + // 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); @@ -3455,6 +4987,93 @@ TEST_F(FPDFAnnotEmbedderTest, AnnotationBorder) { } } +TEST_F(FPDFAnnotEmbedderTest, BorderStyleResolutionUsesISOPrecedence) { + ScopedFPDFDocument doc(FPDF_CreateNewDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 100, 100)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_CIRCLE)); + ASSERT_TRUE(annot); + + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get()); + ASSERT_TRUE(context); + RetainPtr annot_dict = context->GetMutableAnnotDict(); + ASSERT_TRUE(annot_dict); + annot_dict->RemoveFor("BS"); + annot_dict->RemoveFor(pdfium::annotation::kBorder); + + auto expect_style = [&](FPDF_ANNOT_BORDER_STYLE expected_style, + float expected_width) { + float width = -1.0f; + EXPECT_EQ(expected_style, EPDFAnnot_GetBorderStyle(annot.get(), &width)); + EXPECT_FLOAT_EQ(expected_width, width); + }; + + // ISO 32000-2, 12.5.4: if neither /BS nor /Border is present, the + // effective border is a solid 1-point line. + expect_style(FPDF_ANNOT_BS_SOLID, 1.0f); + EXPECT_EQ(0u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + + // The legacy /Border array supplies the width and optional dash pattern + // when /BS is absent. + RetainPtr border = + annot_dict->SetNewFor(pdfium::annotation::kBorder); + border->AppendNew(0); + border->AppendNew(0); + border->AppendNew(0); + expect_style(FPDF_ANNOT_BS_SOLID, 0.0f); + + RetainPtr border_dash = border->AppendNew(); + border_dash->AppendNew(3); + border_dash->AppendNew(2); + expect_style(FPDF_ANNOT_BS_DASHED, 0.0f); + ASSERT_EQ(2u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + std::array dash_values; + ASSERT_TRUE(EPDFAnnot_GetBorderDashPattern(annot.get(), dash_values.data(), + dash_values.size())); + EXPECT_FLOAT_EQ(3.0f, dash_values[0]); + EXPECT_FLOAT_EQ(2.0f, dash_values[1]); + + // /BS suppresses /Border completely. Missing /W and /S in /BS use their + // defaults (1 point and solid), rather than falling back to /Border. + RetainPtr border_style = + annot_dict->SetNewFor("BS"); + expect_style(FPDF_ANNOT_BS_SOLID, 1.0f); + EXPECT_EQ(0u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + + const FS_RECTF rect{/*left=*/10.0f, /*top=*/90.0f, /*right=*/90.0f, + /*bottom=*/10.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + ByteString appearance = GetNormalAppearanceStreamBytes(annot.get()); + EXPECT_TRUE(appearance.Find("1 w ").has_value()); + EXPECT_FALSE(appearance.Find("] 0 d").has_value()); + + // Explicit /BS values, including zero width, remain authoritative. + border_style->SetNewFor("W", 0); + border_style->SetNewFor("S", "D"); + RetainPtr bs_dash = border_style->SetNewFor("D"); + bs_dash->AppendNew(4); + bs_dash->AppendNew(1); + expect_style(FPDF_ANNOT_BS_DASHED, 0.0f); + ASSERT_EQ(2u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + ASSERT_TRUE(EPDFAnnot_GetBorderDashPattern(annot.get(), dash_values.data(), + dash_values.size())); + EXPECT_FLOAT_EQ(4.0f, dash_values[0]); + EXPECT_FLOAT_EQ(1.0f, dash_values[1]); + + // Unknown /BS styles are required to use the solid default. + border_style->SetNewFor("S", "Unknown"); + expect_style(FPDF_ANNOT_BS_SOLID, 0.0f); + EXPECT_EQ(0u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + + float invalid_width = 42.0f; + EXPECT_EQ(FPDF_ANNOT_BS_UNKNOWN, + EPDFAnnot_GetBorderStyle(nullptr, &invalid_width)); + EXPECT_FLOAT_EQ(0.0f, invalid_width); +} + TEST_F(FPDFAnnotEmbedderTest, AnnotationJavaScript) { ASSERT_TRUE(OpenDocument("annot_javascript.pdf")); ScopedPage page = LoadScopedPage(0); @@ -3772,3 +5391,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 5f0d908d41..a2d06025ed 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,22 +42,119 @@ 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 FPDFDoc_GetAttachmentCount(FPDF_DOCUMENT document) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc) { return 0; } - auto name_tree = CPDF_NameTree::Create(doc, "EmbeddedFiles"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "EmbeddedFiles"); return name_tree ? pdfium::checked_cast(name_tree->GetCount()) : 0; } FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV FPDFDoc_AddAttachment(FPDF_DOCUMENT document, FPDF_WIDESTRING name) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc) { return nullptr; } @@ -82,12 +187,13 @@ FPDFDoc_AddAttachment(FPDF_DOCUMENT document, FPDF_WIDESTRING name) { FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV FPDFDoc_GetAttachment(FPDF_DOCUMENT document, int index) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc || index < 0) { return nullptr; } - auto name_tree = CPDF_NameTree::Create(doc, "EmbeddedFiles"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "EmbeddedFiles"); if (!name_tree || static_cast(index) >= name_tree->GetCount()) { return nullptr; } @@ -99,9 +205,61 @@ 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc || index < 0) { return false; } @@ -232,10 +390,18 @@ FPDFAttachment_SetFile(FPDF_ATTACHMENT attachment, } CPDF_Object* pFile = CPDFObjectFromFPDFAttachment(attachment); - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!pFile || !pFile->IsDictionary() || !doc || len > INT_MAX) { return false; } + RetainPtr effective_file; + if (pFile->GetObjNum() != 0) { + effective_file = doc->GetOrParseIndirectObject(pFile->GetObjNum()); + if (effective_file) { + pFile = effective_file.Get(); + } + } // Create a dictionary for the new embedded file stream. auto pFileStreamDict = pdfium::MakeRetain(); @@ -334,38 +500,44 @@ FPDFAttachment_GetSubtype(FPDF_ATTACHMENT attachment, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAttachment_SetSubtype(FPDF_ATTACHMENT attachment, FPDF_BYTESTRING subtype) { CPDF_Object* file = CPDFObjectFromFPDFAttachment(attachment); - if (!file) + if (!file) { return false; + } CPDF_FileSpec spec(pdfium::WrapRetain(file)); RetainPtr file_stream = spec.GetFileStream(); - if (!file_stream) + if (!file_stream) { return false; + } CPDF_Stream* s = const_cast(file_stream.Get()); CPDF_Dictionary* dict = s->GetMutableDict(); - if (!dict) + if (!dict) { return false; + } // Ensure /Type is present (defensive). - if (dict->GetNameFor("Type").IsEmpty()) + if (dict->GetNameFor("Type").IsEmpty()) { dict->SetNewFor("Type", "EmbeddedFile"); + } // Convert to ByteString. ByteString bs = subtype ? ByteString(subtype) : ByteString(); if (bs.IsEmpty()) { dict->RemoveFor("Subtype"); } else { - dict->SetNewFor("Subtype", bs); + dict->SetNewFor("Subtype", bs); } return true; } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAttachment_SetDescription(FPDF_ATTACHMENT attachment, FPDF_WIDESTRING desc) { +EPDFAttachment_SetDescription(FPDF_ATTACHMENT attachment, + FPDF_WIDESTRING desc) { CPDF_Object* file = CPDFObjectFromFPDFAttachment(attachment); - if (!file || !file->IsDictionary()) + if (!file || !file->IsDictionary()) { return false; + } // SAFETY: required from caller. WideString ws = UNSAFE_BUFFERS(WideStringFromFPDFWideString(desc)); @@ -384,42 +556,136 @@ EPDFAttachment_GetDescription(FPDF_ATTACHMENT attachment, FPDF_WCHAR* buffer, unsigned long buflen) { CPDF_Object* file = CPDFObjectFromFPDFAttachment(attachment); - if (!file || !file->IsDictionary()) + if (!file || !file->IsDictionary()) { return 0; + } - RetainPtr obj = - file->AsDictionary()->GetObjectFor("Desc"); - if (!obj || !obj->IsString()) - return Utf16EncodeMaybeCopyAndReturnLength(WideString(), - UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); + RetainPtr obj = file->AsDictionary()->GetObjectFor("Desc"); + if (!obj || !obj->IsString()) { + return Utf16EncodeMaybeCopyAndReturnLength( + WideString(), UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); + } - return Utf16EncodeMaybeCopyAndReturnLength(obj->GetUnicodeText(), - UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); + return Utf16EncodeMaybeCopyAndReturnLength( + obj->GetUnicodeText(), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAttachment_GetIntegerValue(FPDF_ATTACHMENT attachment, FPDF_BYTESTRING key, int* out_value) { - if (!out_value) + if (!out_value) { return false; + } CPDF_Object* file = CPDFObjectFromFPDFAttachment(attachment); - if (!file) + if (!file) { return false; + } CPDF_FileSpec spec(pdfium::WrapRetain(file)); RetainPtr params = spec.GetParamsDict(); - if (!params) + if (!params) { return false; + } ByteStringView k(key); RetainPtr obj = params->GetObjectFor(k); - if (!obj || !obj->IsNumber()) + if (!obj || !obj->IsNumber()) { return false; + } const CPDF_Number* num = obj->AsNumber(); - *out_value = num->IsInteger() ? num->GetInteger() - : static_cast(num->GetNumber()); + *out_value = + 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; -} \ No newline at end of file +} + +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 799c5a592a..e8d5ed90e1 100644 --- a/fpdfsdk/fpdf_doc.cpp +++ b/fpdfsdk/fpdf_doc.cpp @@ -95,26 +95,29 @@ using pdfium::metadata::kNameTrue; using pdfium::metadata::kNameUnknown; constexpr const char* kReservedInfoKeys[] = { - kInfoTitle, kInfoAuthor, kInfoSubject, kInfoKeywords, - kInfoProducer, kInfoCreator, kInfoCreationDate, kInfoModDate, - kInfoTrapped, + kInfoTitle, kInfoAuthor, kInfoSubject, kInfoKeywords, kInfoProducer, + kInfoCreator, kInfoCreationDate, kInfoModDate, kInfoTrapped, }; bool IsReservedInfoKey(ByteStringView key) { for (const char* r : kReservedInfoKeys) { - if (key == r) + if (key == r) { return true; + } } return false; } inline FPDF_TRAPPED_STATUS TrappedNameToStatus(ByteStringView name) { - if (name == kNameTrue) + if (name == kNameTrue) { return PDFTRAPPED_TRUE; - if (name == kNameFalse) + } + if (name == kNameFalse) { return PDFTRAPPED_FALSE; - if (name == kNameUnknown) + } + if (name == kNameUnknown) { return PDFTRAPPED_UNKNOWN; + } // Be forgiving on odd values. return PDFTRAPPED_UNKNOWN; } @@ -333,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) { @@ -381,10 +401,11 @@ FPDFDest_GetLocationInPage(FPDF_DEST dest, FPDF_EXPORT FPDF_LINK FPDF_CALLCONV FPDFLink_GetLinkAtPoint(FPDF_PAGE page, double x, double y) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return nullptr; } + CPDF_Page* pPage = page_view.Get(); CPDF_LinkList* pLinkList = GetLinkList(pPage); if (!pLinkList) { @@ -401,10 +422,11 @@ FPDF_EXPORT FPDF_LINK FPDF_CALLCONV FPDFLink_GetLinkAtPoint(FPDF_PAGE page, FPDF_EXPORT int FPDF_CALLCONV FPDFLink_GetLinkZOrderAtPoint(FPDF_PAGE page, double x, double y) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return -1; } + CPDF_Page* pPage = page_view.Get(); CPDF_LinkList* pLinkList = GetLinkList(pPage); if (!pLinkList) { @@ -423,11 +445,20 @@ FPDF_EXPORT FPDF_DEST FPDF_CALLCONV FPDFLink_GetDest(FPDF_DOCUMENT document, if (!link) { return nullptr; } - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); - if (!doc) { + ScopedFPDFDocumentView document_view(document); + if (!document_view) { return nullptr; } - CPDF_Link cLink(pdfium::WrapRetain(CPDFDictionaryFromFPDFLink(link))); + CPDF_Document* doc = document_view.Get(); + RetainPtr link_dict(CPDFDictionaryFromFPDFLink(link)); + if (link_dict->GetObjNum() != 0) { + RetainPtr effective = + ToDictionary(doc->GetOrParseIndirectObject(link_dict->GetObjNum())); + if (effective) { + link_dict = std::move(effective); + } + } + CPDF_Link cLink(std::move(link_dict)); FPDF_DEST dest = FPDFDestFromCPDFArray(cLink.GetDest(doc).GetArray()); if (dest) { return dest; @@ -455,23 +486,25 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFLink_Enumerate(FPDF_PAGE page, if (!start_pos || !link_annot) { return false; } - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return false; } - RetainPtr pAnnots = pPage->GetMutableAnnotsArray(); + const CPDF_Page* pPage = page_view.Get(); + RetainPtr pAnnots = pPage->GetAnnotsArray(); if (!pAnnots) { return false; } for (size_t i = *start_pos; i < pAnnots->size(); i++) { - RetainPtr dict = - ToDictionary(pAnnots->GetMutableDirectObjectAt(i)); + RetainPtr dict = + ToDictionary(pAnnots->GetDirectObjectAt(i)); if (!dict) { continue; } if (dict->GetByteStringFor("Subtype") == "Link") { *start_pos = static_cast(i + 1); - *link_annot = FPDFLinkFromCPDFDictionary(dict.Get()); + *link_annot = + FPDFLinkFromCPDFDictionary(const_cast(dict.Get())); return true; } } @@ -480,11 +513,19 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFLink_Enumerate(FPDF_PAGE page, FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV FPDFLink_GetAnnot(FPDF_PAGE page, FPDF_LINK link_annot) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); + ScopedFPDFPageView page_view(page); + CPDF_Page* pPage = page_view.Get(); RetainPtr pAnnotDict(CPDFDictionaryFromFPDFLink(link_annot)); if (!pPage || !pAnnotDict) { return nullptr; } + if (pAnnotDict->GetObjNum() != 0) { + RetainPtr effective = ToDictionary( + pPage->GetDocument()->GetOrParseIndirectObject(pAnnotDict->GetObjNum())); + if (effective) { + pAnnotDict = std::move(effective); + } + } auto pAnnotContext = std::make_unique( std::move(pAnnotDict), IPDFPageFromFPDFPage(page)); @@ -633,22 +674,23 @@ FPDF_GetPageLabel(FPDF_DOCUMENT document, str.value(), UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); } -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) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc || !tag) + if (!pDoc || !tag) { return false; + } // Create /Info if it does not exist. RetainPtr info = pDoc->GetOrCreateInfo(); - if (!info) + if (!info) { return false; + } ByteString key(tag); - WideString wide = - value ? UNSAFE_BUFFERS(WideStringFromFPDFWideString(value)) : WideString(); + WideString wide = value ? UNSAFE_BUFFERS(WideStringFromFPDFWideString(value)) + : WideString(); if (wide.IsEmpty()) { // RemoveFor() expects ByteStringView. @@ -661,42 +703,53 @@ EPDF_SetMetaText(FPDF_DOCUMENT document, return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDF_HasMetaText(FPDF_DOCUMENT document, FPDF_BYTESTRING tag) { - if (!tag) return false; +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_HasMetaText(FPDF_DOCUMENT document, + FPDF_BYTESTRING tag) { + if (!tag) { + return false; + } CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - - if (!pDoc) return false; + + if (!pDoc) { + return false; + } RetainPtr info = pDoc->GetInfo(); - if (!info) return false; + if (!info) { + return false; + } return info->KeyExist(tag); } FPDF_EXPORT FPDF_TRAPPED_STATUS FPDF_CALLCONV EPDF_GetMetaTrapped(FPDF_DOCUMENT document) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc) + if (!pDoc) { return PDFTRAPPED_UNKNOWN; + } RetainPtr info = pDoc->GetInfo(); - if (!info) + if (!info) { return PDFTRAPPED_NOTSET; + } // SetNewFor() wants ByteString keys; RemoveFor() wants ByteStringView. ByteString key_trapped(kInfoTrapped); RetainPtr obj = info->GetDirectObjectFor(key_trapped.AsStringView()); - if (!obj) + if (!obj) { return PDFTRAPPED_NOTSET; + } - if (const CPDF_Name* pName = ToName(obj.Get())) + if (const CPDF_Name* pName = ToName(obj.Get())) { return TrappedNameToStatus(pName->GetString().AsStringView()); + } // Lenient: some PDFs incorrectly store a boolean; read via the dict helper. if (obj->IsBoolean()) { - const bool b = info->GetBooleanFor(key_trapped.AsStringView(), /*bDefault=*/false); + const bool b = + info->GetBooleanFor(key_trapped.AsStringView(), /*bDefault=*/false); return b ? PDFTRAPPED_TRUE : PDFTRAPPED_FALSE; } @@ -706,12 +759,14 @@ EPDF_GetMetaTrapped(FPDF_DOCUMENT document) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_SetMetaTrapped(FPDF_DOCUMENT document, FPDF_TRAPPED_STATUS status) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc) + if (!pDoc) { return false; + } RetainPtr info = pDoc->GetOrCreateInfo(); - if (!info) + if (!info) { return false; + } ByteString key_trapped(kInfoTrapped); @@ -721,28 +776,33 @@ EPDF_SetMetaTrapped(FPDF_DOCUMENT document, FPDF_TRAPPED_STATUS status) { } ByteStringView name = StatusToTrappedName(status); - if (name.IsEmpty()) + if (name.IsEmpty()) { return false; // invalid enum + } - info->SetNewFor(key_trapped, ByteString(name)); // expects ByteString key + info->SetNewFor(key_trapped, + ByteString(name)); // expects ByteString key return true; } -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) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc) + if (!pDoc) { return 0; + } RetainPtr info = pDoc->GetInfo(); - if (!info) + if (!info) { return 0; + } int count = 0; std::vector keys = info->GetKeys(); for (const ByteString& key : keys) { - if (custom_only && IsReservedInfoKey(key.AsStringView())) + if (custom_only && IsReservedInfoKey(key.AsStringView())) { continue; + } ++count; } return count; @@ -755,22 +815,25 @@ EPDF_GetMetaKeyName(FPDF_DOCUMENT document, void* buffer, unsigned long buflen) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc || index < 0) + if (!pDoc || index < 0) { return 0; + } RetainPtr info = pDoc->GetInfo(); - if (!info) + if (!info) { return 0; + } int seen = 0; std::vector keys = info->GetKeys(); for (const ByteString& key : keys) { - if (custom_only && IsReservedInfoKey(key.AsStringView())) + if (custom_only && IsReservedInfoKey(key.AsStringView())) { continue; + } if (seen++ == index) { return NulTerminateMaybeCopyAndReturnLength( key, UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); } } return 0; -} \ No newline at end of file +} diff --git a/fpdfsdk/fpdf_doc_embeddertest.cpp b/fpdfsdk/fpdf_doc_embeddertest.cpp index aa61b5d4aa..1ae6e07161 100644 --- a/fpdfsdk/fpdf_doc_embeddertest.cpp +++ b/fpdfsdk/fpdf_doc_embeddertest.cpp @@ -9,6 +9,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fxcrt/bytestring.h" #include "core/fxcrt/fx_safe_types.h" @@ -75,6 +76,25 @@ int CountStreamEntries(const std::string& data) { class FPDFDocEmbedderTest : public EmbedderTest {}; +TEST_F(FPDFDocEmbedderTest, ReadPurityLinkEnumeration) { + ASSERT_TRUE(OpenDocument("links_highlights_annots.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + const uint32_t last_obj_num = doc->GetLastObjNum(); + + { + CPDF_ReadOnlyGraphGuard guard; + int start_pos = 0; + FPDF_LINK link = nullptr; + while (FPDFLink_Enumerate(page.get(), &start_pos, &link)) { + } + } + + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); +} + TEST_F(FPDFDocEmbedderTest, MultipleSamePage) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); @@ -122,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 c5a9555760..666e2b5719 100644 --- a/fpdfsdk/fpdf_javascript.cpp +++ b/fpdfsdk/fpdf_javascript.cpp @@ -14,50 +14,65 @@ #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::Create(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) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); +RetainPtr GetNamedJavaScriptActionDictionary( + CPDF_Document* doc, + int index, + WideString* name, + std::optional* script) { if (!doc || index < 0) { return nullptr; } - auto name_tree = CPDF_NameTree::Create(doc, "JavaScript"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "JavaScript"); if (!name_tree || static_cast(index) >= name_tree->GetCount()) { 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) { + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); + 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) { + ScopedFPDFDocumentView document_view(document); + WideString name; + std::optional script; + if (!GetNamedJavaScriptActionDictionary(document_view.Get(), index, &name, + &script)) { return nullptr; } @@ -67,6 +82,20 @@ 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) { + ScopedFPDFDocumentView document_view(document); + WideString name; + std::optional script; + RetainPtr dictionary = + GetNamedJavaScriptActionDictionary(document_view.Get(), index, &name, + &script); + return dictionary ? epdf::MakeActionModelHandle(epdf::BuildActionModel( + CPDF_Action(std::move(dictionary)), + document_view.Get())) + : 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_progressive.cpp b/fpdfsdk/fpdf_progressive.cpp index b43d23e244..abe2cfecb7 100644 --- a/fpdfsdk/fpdf_progressive.cpp +++ b/fpdfsdk/fpdf_progressive.cpp @@ -53,10 +53,11 @@ FPDF_RenderPageBitmapWithColorScheme_Start(FPDF_BITMAP bitmap, return FPDF_RENDER_FAILED; } - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return FPDF_RENDER_FAILED; } + CPDF_Page* pPage = page_view.Get(); RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); if (!pBitmap) { @@ -134,10 +135,11 @@ FPDF_EXPORT int FPDF_CALLCONV FPDF_RenderPage_Continue(FPDF_PAGE page, return FPDF_RENDER_FAILED; } - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return FPDF_RENDER_FAILED; } + CPDF_Page* pPage = page_view.Get(); auto* context = static_cast(pPage->GetRenderContext()); diff --git a/fpdfsdk/fpdf_save.cpp b/fpdfsdk/fpdf_save.cpp index a6eb68ca5f..3ed555cdd2 100644 --- a/fpdfsdk/fpdf_save.cpp +++ b/fpdfsdk/fpdf_save.cpp @@ -6,10 +6,13 @@ #include "public/fpdf_save.h" -#include #include +#include +#include +#include #include +#include #include #include @@ -27,7 +30,6 @@ #include "core/fxcrt/stl_util.h" #include "fpdfsdk/cpdfsdk_filewriteadapter.h" #include "fpdfsdk/cpdfsdk_helpers.h" -#include "fpdfsdk/fpdfsdk_pending_security.h" #include "public/fpdf_edit.h" #ifdef PDF_ENABLE_XFA @@ -195,23 +197,19 @@ bool DoDocSave(FPDF_DOCUMENT document, CPDF_Creator file_maker( doc, pdfium::MakeRetain(file_write)); - // Apply pending security state - { - std::lock_guard lock(GetPendingSecurityMutex()); - auto& pending_security = GetPendingSecurityMap(); - auto it = pending_security.find(document); - if (it != pending_security.end()) { - if (it->second.mode == PendingSecurityMode::kRemove) { - flags |= FPDF_REMOVE_SECURITY; - } else if (it->second.mode == PendingSecurityMode::kEncrypt) { - // SetEncryption sets both: - // 1. encrypt_dict_ - so /Encrypt reference is written to trailer - // 2. security_handler_ - so GetCryptoHandler() encrypts streams/strings - file_maker.SetEncryption(it->second.encrypt_dict, - it->second.security_handler); - } - // Don't erase from map here - leave for cleanup on doc close - // This allows multiple saves of the same encrypted doc + // Apply document-owned pending security state. It stays on the document so + // repeated saves use the same requested encryption/removal policy until the + // caller changes it or closes the document. + if (const CPDF_Document::PendingSecurity* pending = + doc->GetPendingSecurity()) { + if (pending->mode == CPDF_Document::PendingSecurityMode::kRemove) { + flags |= FPDF_REMOVE_SECURITY; + } else if (pending->mode == CPDF_Document::PendingSecurityMode::kEncrypt) { + // SetEncryption sets both: + // 1. encrypt_dict_ - so /Encrypt reference is written to trailer + // 2. security_handler_ - so GetCryptoHandler() encrypts streams/strings + file_maker.SetEncryption(pending->encrypt_dict, + pending->security_handler); } } @@ -229,8 +227,54 @@ bool DoDocSave(FPDF_DOCUMENT document, return create_result; } +struct MemoryFileWriter : public FPDF_FILEWRITE { + std::string data; + + MemoryFileWriter() { + version = 1; + WriteBlock = [](FPDF_FILEWRITE* self, const void* buf, + unsigned long size) -> int { + static_cast(self)->data.append( + static_cast(buf), size); + return 1; + }; + } +}; + +void* SaveToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_DWORD flags, + unsigned long* out_size, + std::optional version) { + if (!out_size) { + return nullptr; + } + *out_size = 0; + + MemoryFileWriter writer; + const bool ok = version.has_value() + ? DoDocSave(document, &writer, flags, version.value()) + : DoDocSave(document, &writer, flags, {}); + if (!ok || writer.data.empty() || + writer.data.size() > std::numeric_limits::max()) { + return nullptr; + } + + void* buffer = malloc(writer.data.size()); + if (!buffer) { + return nullptr; + } + + memcpy(buffer, writer.data.data(), writer.data.size()); + *out_size = static_cast(writer.data.size()); + return buffer; +} + } // namespace +FPDF_EXPORT void FPDF_CALLCONV EPDF_FreeBuffer(void* buffer) { + free(buffer); +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDF_SaveAsCopy(FPDF_DOCUMENT document, FPDF_FILEWRITE* file_write, FPDF_DWORD flags) { @@ -244,3 +288,18 @@ FPDF_SaveWithVersion(FPDF_DOCUMENT document, int fileVersion) { return DoDocSave(document, file_write, flags, fileVersion); } + +FPDF_EXPORT void* FPDF_CALLCONV +EPDF_SaveDocumentToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_DWORD flags, + unsigned long* out_size) { + return SaveToOwnedBuffer(document, flags, out_size, {}); +} + +FPDF_EXPORT void* FPDF_CALLCONV +EPDF_SaveDocumentToOwnedBufferWithVersion(FPDF_DOCUMENT document, + FPDF_DWORD flags, + unsigned long* out_size, + int file_version) { + return SaveToOwnedBuffer(document, flags, out_size, file_version); +} diff --git a/fpdfsdk/fpdf_save_embeddertest.cpp b/fpdfsdk/fpdf_save_embeddertest.cpp index ea45c40180..f880ab3477 100644 --- a/fpdfsdk/fpdf_save_embeddertest.cpp +++ b/fpdfsdk/fpdf_save_embeddertest.cpp @@ -6,8 +6,13 @@ #include #include +#include "core/fpdfapi/parser/cpdf_cross_ref_table.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_parser.h" #include "core/fxcrt/fx_string.h" +#include "fpdfsdk/cpdfsdk_helpers.h" #include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_annot.h" #include "public/fpdf_edit.h" #include "public/fpdf_ppo.h" #include "public/fpdf_save.h" @@ -24,6 +29,23 @@ using testing::HasSubstr; using testing::Not; using testing::StartsWith; +namespace { + +bool HasSavedXRefEntryForObject(FPDF_DOCUMENT document, uint32_t objnum) { + CPDF_Document* cpdf_doc = CPDFDocumentFromFPDFDocument(document); + if (!cpdf_doc || !cpdf_doc->GetParser() || + !cpdf_doc->GetParser()->GetCrossRefTable()) { + return false; + } + + const CPDF_CrossRefTable::ObjectInfo* info = + cpdf_doc->GetParser()->GetCrossRefTable()->GetObjectInfo(objnum); + return info && (info->type == CPDF_CrossRefTable::ObjectType::kNormal || + info->type == CPDF_CrossRefTable::ObjectType::kCompressed); +} + +} // namespace + class FPDFSaveEmbedderTest : public EmbedderTest {}; TEST_F(FPDFSaveEmbedderTest, SaveSimpleDoc) { @@ -40,6 +62,27 @@ TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocWithVersion) { EXPECT_EQ(805u, GetString().size()); } +TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocToOwnedBuffer) { + EPDF_FreeBuffer(nullptr); + + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + unsigned long size = 0; + void* buffer = EPDF_SaveDocumentToOwnedBuffer(document(), 0, &size); + ASSERT_TRUE(buffer); + EXPECT_EQ(805u, size); + std::string saved(static_cast(buffer), size); + EPDF_FreeBuffer(buffer); + EXPECT_THAT(saved, StartsWith("%PDF-1.7\r\n")); + + size = 0; + buffer = EPDF_SaveDocumentToOwnedBufferWithVersion(document(), 0, &size, 14); + ASSERT_TRUE(buffer); + EXPECT_EQ(805u, size); + saved.assign(static_cast(buffer), size); + EPDF_FreeBuffer(buffer); + EXPECT_THAT(saved, StartsWith("%PDF-1.4\r\n")); +} + TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocWithBadVersion) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); EXPECT_TRUE(FPDF_SaveWithVersion(document(), this, 0, -1)); @@ -64,6 +107,119 @@ TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocIncremental) { EXPECT_GT(GetString().size(), 985u); } +TEST_F(FPDFSaveEmbedderTest, SaveAsCopyPrunesUnlinkedNewAnnotation) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_GT(annot_objnum, 0u); + ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + + annot.reset(); + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 0)); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + EXPECT_FALSE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum)); + + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(saved_page.get())); +} + +TEST_F(FPDFSaveEmbedderTest, IncrementalSavePrunesUnlinkedNewAnnotation) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_GT(annot_objnum, 0u); + + annot.reset(); + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 0)); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, FPDF_INCREMENTAL)); + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + EXPECT_FALSE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum)); + + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(saved_page.get())); +} + +TEST_F(FPDFSaveEmbedderTest, SaveAsCopyKeepsLinkedNewAnnotation) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + const uint32_t annot_objnum = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_GT(annot_objnum, 0u); + ASSERT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + EXPECT_TRUE(HasSavedXRefEntryForObject(saved_doc.get(), annot_objnum)); + + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + ASSERT_EQ(1, FPDFPage_GetAnnotCount(saved_page.get())); + ScopedFPDFAnnotation saved_annot(FPDFPage_GetAnnot(saved_page.get(), 0)); + ASSERT_TRUE(saved_annot); + EXPECT_EQ(FPDF_ANNOT_TEXT, FPDFAnnot_GetSubtype(saved_annot.get())); +} + +TEST_F(FPDFSaveEmbedderTest, SetEncryptionRoundTripsWithInlineEncryptDict) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + ASSERT_TRUE(EPDF_SetEncryption(document(), "user", "owner", + EPDF_PERM_PRINT | EPDF_PERM_COPY)); + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ScopedSavedDoc saved_doc = OpenScopedSavedDocumentWithPassword("user"); + ASSERT_TRUE(saved_doc); + EXPECT_TRUE(EPDF_IsEncrypted(saved_doc.get())); + + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + ScopedFPDFBitmap bitmap = RenderSavedPage(saved_page.get()); + ASSERT_TRUE(bitmap); +} + +TEST_F(FPDFSaveEmbedderTest, RemoveEncryptionUsesDocumentOwnedPendingState) { + ASSERT_TRUE(OpenDocument("hello_world.pdf")); + ASSERT_TRUE(EPDF_SetEncryption(document(), "user", "owner", + EPDF_PERM_PRINT | EPDF_PERM_COPY)); + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + + { + ScopedSavedDoc encrypted_doc = OpenScopedSavedDocumentWithPassword("user"); + ASSERT_TRUE(encrypted_doc); + ASSERT_TRUE(EPDF_IsEncrypted(encrypted_doc.get())); + + ClearString(); + ASSERT_TRUE(EPDF_RemoveEncryption(encrypted_doc.get())); + ASSERT_TRUE(FPDF_SaveAsCopy(encrypted_doc.get(), this, 0)); + } + + ScopedSavedDoc decrypted_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(decrypted_doc); + EXPECT_FALSE(EPDF_IsEncrypted(decrypted_doc.get())); +} + TEST_F(FPDFSaveEmbedderTest, SaveSimpleDocNoIncremental) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); EXPECT_TRUE(FPDF_SaveWithVersion(document(), this, FPDF_NO_INCREMENTAL, 14)); diff --git a/fpdfsdk/fpdf_text.cpp b/fpdfsdk/fpdf_text.cpp index 960b2aefce..a6e8376a12 100644 --- a/fpdfsdk/fpdf_text.cpp +++ b/fpdfsdk/fpdf_text.cpp @@ -41,10 +41,11 @@ CPDF_TextPage* GetTextPageForValidIndex(FPDF_TEXTPAGE text_page, int index) { } // namespace FPDF_EXPORT FPDF_TEXTPAGE FPDF_CALLCONV FPDFText_LoadPage(FPDF_PAGE page) { - CPDF_Page* pPDFPage = CPDFPageFromFPDFPage(page); - if (!pPDFPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return nullptr; } + CPDF_Page* pPDFPage = page_view.Get(); CPDF_ViewerPreferences viewRef(pPDFPage->GetDocument()); auto textpage = diff --git a/fpdfsdk/fpdf_text_embeddertest.cpp b/fpdfsdk/fpdf_text_embeddertest.cpp index f77c04efc1..5e6bb8cc4f 100644 --- a/fpdfsdk/fpdf_text_embeddertest.cpp +++ b/fpdfsdk/fpdf_text_embeddertest.cpp @@ -9,8 +9,11 @@ #include #include "build/build_config.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fxcrt/notreached.h" #include "core/fxge/fx_font.h" +#include "fpdfsdk/cpdfsdk_helpers.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_doc.h" #include "public/fpdf_text.h" @@ -2113,6 +2116,23 @@ TEST_F(FPDFTextEmbedderTest, SmallType3Glyph) { EXPECT_DOUBLE_EQ(61.520000457763672, top); } +TEST_F(FPDFTextEmbedderTest, ReadPurityRenderType3Font) { + ASSERT_TRUE(OpenDocument("bug_1591.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + const uint32_t last_obj_num = doc->GetLastObjNum(); + + { + CPDF_ReadOnlyGraphGuard guard; + ScopedFPDFBitmap bitmap = RenderLoadedPage(page.get()); + ASSERT_TRUE(bitmap); + } + + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); +} + TEST_F(FPDFTextEmbedderTest, BigtableTextExtraction) { static constexpr char kExpectedText[] = "{fay,jeff,sanjay,wilsonh,kerr,m3b,tushar,\x02k es,gruber}@google.com"; diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index 6a4278cf8e..f9c8224024 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -8,28 +8,26 @@ #include #include -#include #include -#include #include #include #include "build/build_config.h" #include "constants/page_object.h" +#include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/page/cpdf_form.h" #include "core/fpdfapi/page/cpdf_occontext.h" #include "core/fpdfapi/page/cpdf_page.h" #include "core/fpdfapi/page/cpdf_pageimagecache.h" #include "core/fpdfapi/page/cpdf_pagemodule.h" -#include "core/fpdfdoc/cpdf_annot.h" -#include "core/fpdfapi/page/cpdf_annotcontext.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_number.h" #include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_document_view_scope.h" #include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" #include "core/fpdfapi/parser/cpdf_parser.h" #include "core/fpdfapi/parser/cpdf_security_handler.h" #include "core/fpdfapi/parser/cpdf_stream.h" @@ -39,6 +37,7 @@ #include "core/fpdfapi/render/cpdf_pagerendercontext.h" #include "core/fpdfapi/render/cpdf_rendercontext.h" #include "core/fpdfapi/render/cpdf_renderoptions.h" +#include "core/fpdfdoc/cpdf_annot.h" #include "core/fpdfdoc/cpdf_nametree.h" #include "core/fpdfdoc/cpdf_viewerpreferences.h" #include "core/fxcrt/cfx_fileaccess_stream.h" @@ -46,6 +45,7 @@ #include "core/fxcrt/cfx_timer.h" #include "core/fxcrt/check_op.h" #include "core/fxcrt/compiler_specific.h" +#include "core/fxcrt/epdf_tls.h" #include "core/fxcrt/fx_extension.h" #include "core/fxcrt/fx_memcpy_wrappers.h" #include "core/fxcrt/fx_safe_types.h" @@ -57,6 +57,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" @@ -66,7 +67,6 @@ #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/cpdfsdk_pageview.h" #include "fpdfsdk/cpdfsdk_renderpage.h" -#include "fpdfsdk/fpdfsdk_pending_security.h" #include "fxjs/ijs_runtime.h" #include "public/fpdf_formfill.h" @@ -117,29 +117,11 @@ static_assert(static_cast(CFX_DefaultRenderDevice::RendererType::kSkia) == FPDF_RENDERERTYPE_SKIA); #endif // defined(PDF_USE_SKIA) -// Storage for pending security state (declared in fpdfsdk_pending_security.h). -// Both the mutex and the map are intentionally leaked behind function-local -// statics to avoid the static destruction order fiasco. -std::mutex& GetPendingSecurityMutex() { - static std::mutex* mutex = new std::mutex; - return *mutex; -} - -std::unordered_map& GetPendingSecurityMap() { - static auto* pending_security = - new std::unordered_map; - return *pending_security; -} - -// Helper to cleanup pending security on document close -void EPDF_CleanupPendingSecurity(FPDF_DOCUMENT doc) { - std::lock_guard lock(GetPendingSecurityMutex()); - GetPendingSecurityMap().erase(doc); -} - namespace { -bool g_bLibraryInitialized = false; +// EmbedPDF: thread-confined runtime - each worker thread tracks its own +// library-initialized state so per-thread Init/Destroy don't race. +EPDF_TLS bool g_bLibraryInitialized = false; void SetRendererType(FPDF_RENDERER_TYPE public_type) { // Internal definition of renderer types must stay updated with respect to @@ -300,6 +282,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(); @@ -477,8 +462,8 @@ FPDF_GetSecurityHandlerRevision(FPDF_DOCUMENT document) { namespace { // Build P value with correct reserved bits for R>=3 (including R=4 and R=6) -// Input: allowed_flags - OR'd combination of permission bits user wants to ALLOW -// Output: proper P value with reserved bits set correctly +// Input: allowed_flags - OR'd combination of permission bits user wants to +// ALLOW Output: proper P value with reserved bits set correctly uint32_t BuildPermissionsForRevision(uint32_t allowed_flags) { // Enforce: PrintHighQuality implies Print (bit 12 requires bit 3) // Some readers interpret oddly if PRINT_HIGH is set without PRINT @@ -524,8 +509,10 @@ EPDF_SetEncryption(FPDF_DOCUMENT document, int32_t permissions = static_cast(BuildPermissionsForRevision(allowed_flags_32)); - // Create encrypt dictionary as indirect object - auto pEncryptDict = pDoc->NewIndirect(); + // Create the encrypt dictionary inline. CPDF_Creator::SetEncryption() owns + // the trailer-only reference and writes inline encrypt dictionaries as + // indirect objects during save. + auto pEncryptDict = pDoc->New(); pEncryptDict->SetNewFor("Filter", "Standard"); pEncryptDict->SetNewFor("V", 5); pEncryptDict->SetNewFor("R", 6); @@ -552,20 +539,14 @@ EPDF_SetEncryption(FPDF_DOCUMENT document, // Returns false if LoadDict fails or R != 6 if (!pSecurityHandler->OnCreateWithPasswords(pEncryptDict.Get(), user_pwd, owner_pwd)) { - // Cleanup the indirect object we created - pDoc->DeleteIndirectObject(pEncryptDict->GetObjNum()); return false; } - // Store (OVERWRITE if exists - allows changing password multiple times) - { - std::lock_guard lock(GetPendingSecurityMutex()); - PendingSecurity pending; - pending.mode = PendingSecurityMode::kEncrypt; - pending.encrypt_dict = std::move(pEncryptDict); - pending.security_handler = std::move(pSecurityHandler); - GetPendingSecurityMap()[document] = std::move(pending); - } + CPDF_Document::PendingSecurity pending; + pending.mode = CPDF_Document::PendingSecurityMode::kEncrypt; + pending.encrypt_dict = std::move(pEncryptDict); + pending.security_handler = std::move(pSecurityHandler); + pDoc->SetPendingSecurity(std::move(pending)); return true; } @@ -576,13 +557,14 @@ EPDF_RemoveEncryption(FPDF_DOCUMENT document) { return false; } - // Set pending security to removal mode - // This will cause RemoveSecurity() to be called during save - std::lock_guard lock(GetPendingSecurityMutex()); - PendingSecurity pending; - pending.mode = PendingSecurityMode::kRemove; - // encrypt_dict and security_handler remain null for removal - GetPendingSecurityMap()[document] = std::move(pending); + CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + CPDF_Document::PendingSecurity pending; + pending.mode = CPDF_Document::PendingSecurityMode::kRemove; + pDoc->SetPendingSecurity(std::move(pending)); return true; } @@ -612,7 +594,93 @@ EPDF_UnlockOwnerPermissions(FPDF_DOCUMENT document, } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDF_IsEncrypted(FPDF_DOCUMENT document) { +EPDF_CheckPasswordPermissions(FPDF_DOCUMENT document, + FPDF_BYTESTRING password, + int* out_kind, + unsigned int* out_user_permissions, + unsigned int* out_effective_permissions, + int* out_security_handler_revision) { + if (!out_kind || !out_user_permissions || !out_effective_permissions || + !out_security_handler_revision) { + return false; + } + + *out_kind = EPDF_PASSWORD_PERMISSION_INVALID; + *out_user_permissions = 0; + *out_effective_permissions = 0; + *out_security_handler_revision = -1; + + CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + CPDF_Parser* pParser = pDoc->GetParser(); + if (!pParser) { + return false; + } + + const auto& security_handler = pParser->GetSecurityHandler(); + if (!security_handler) { + *out_kind = EPDF_PASSWORD_PERMISSION_NONE; + *out_user_permissions = 0xFFFFFFFF; + *out_effective_permissions = 0xFFFFFFFF; + return true; + } + + RetainPtr encrypt_dict = pParser->GetEncryptDict(); + *out_security_handler_revision = + encrypt_dict ? encrypt_dict->GetIntegerFor("R") : -1; + + const unsigned int user_permissions = static_cast( + security_handler->GetPermissionsForPasswordProbe(/*owner=*/false)); + *out_user_permissions = user_permissions; + + ByteString raw_password(password ? password : ""); + + // This is a password probe, not a document-state probe. Do not use + // IsOwnerUnlocked(), UnlockOwner(), or FPDF_GetDocPermissions() here; those + // depend on or mutate the current handle state. Match PDFium's open path by + // checking a non-empty password against owner credentials first. + if (!raw_password.IsEmpty() && + security_handler->CheckPasswordNoMutate(raw_password, /*bOwner=*/true)) { + *out_kind = EPDF_PASSWORD_PERMISSION_OWNER; + *out_effective_permissions = static_cast( + security_handler->GetPermissionsForPasswordProbe(/*owner=*/true)); + return true; + } + + if (security_handler->CheckPasswordNoMutate(raw_password, /*bOwner=*/false)) { + *out_kind = EPDF_PASSWORD_PERMISSION_USER; + *out_effective_permissions = user_permissions; + return true; + } + + return false; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_SetRuntimeOwnerPermissions(FPDF_DOCUMENT document, FPDF_BOOL enabled) { + CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + CPDF_Parser* pParser = pDoc->GetParser(); + if (!pParser) { + return false; + } + + const auto& security_handler = pParser->GetSecurityHandler(); + if (!security_handler) { + return false; + } + + security_handler->SetRuntimeOwnerUnlocked(!!enabled); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_IsEncrypted(FPDF_DOCUMENT document) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); if (!pDoc) { return false; @@ -660,10 +728,14 @@ namespace { // Shared body of FPDF_LoadPage and EPDFDoc_LoadPageByObjectNumber. Validates // `page_index` against the document's page count, then constructs and returns -// a leaked page handle. Returns nullptr on any failure. +// a leaked page handle. When `normalize` is true, the page's rotation is +// overridden to 0 so all subsequent operations use normalized 0-degree +// coordinates (the intrinsic rotation is surfaced separately via +// EPDF_GetPageRotationByIndex). Returns nullptr on any failure. FPDF_PAGE LoadPageByValidatedIndex(FPDF_DOCUMENT document, CPDF_Document* doc, - int page_index) { + int page_index, + bool normalize) { if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) { return nullptr; } @@ -675,7 +747,10 @@ FPDF_PAGE LoadPageByValidatedIndex(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 nullptr; } @@ -684,6 +759,12 @@ FPDF_PAGE LoadPageByValidatedIndex(FPDF_DOCUMENT document, pPage->AddPageImageCache(); pPage->ParseContent(); + // Force rotation to 0 - this re-runs UpdateDimensions() so page_size_ and + // page_matrix_ are calculated as if rotation=0. + if (normalize) { + pPage->SetRotationOverride(0); + } + return FPDFPageFromIPDFPage(pPage.Leak()); } @@ -695,7 +776,8 @@ FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV FPDF_LoadPage(FPDF_DOCUMENT document, if (!doc) { return nullptr; } - return LoadPageByValidatedIndex(document, doc, page_index); + return LoadPageByValidatedIndex(document, doc, page_index, + /*normalize=*/false); } FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV @@ -704,7 +786,100 @@ EPDFDoc_LoadPageByObjectNumber(FPDF_DOCUMENT document, unsigned int obj_num) { if (!doc || obj_num == 0) { return nullptr; } - return LoadPageByValidatedIndex(document, doc, doc->GetPageIndex(obj_num)); + return LoadPageByValidatedIndex(document, doc, doc->GetPageIndex(obj_num), + /*normalize=*/false); +} + +FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV +EPDFDoc_LoadPageByObjectNumberNormalized(FPDF_DOCUMENT document, + unsigned int obj_num) { + auto* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || obj_num == 0) { + return nullptr; + } + return LoadPageByValidatedIndex(document, doc, doc->GetPageIndex(obj_num), + /*normalize=*/true); +} + +FPDF_EXPORT unsigned int FPDF_CALLCONV +EPDFDoc_GetPageObjectNumberByIndex(FPDF_DOCUMENT document, int page_index) { + auto* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || page_index < 0 || page_index >= FPDF_GetPageCount(document)) { + return 0; + } + +#ifdef PDF_ENABLE_XFA + // XFA pages do not have CPDF_Page dictionaries. Match + // EPDFPage_GetObjectNumber()'s documented XFA behavior and return 0. + if (doc->GetExtension()) { + return 0; + } +#endif // PDF_ENABLE_XFA + + RetainPtr dict = doc->GetPageDictionary(page_index); + return dict ? dict->GetObjNum() : 0; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_DeletePageByObjectNumber(FPDF_DOCUMENT document, unsigned int obj_num) { + auto* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || obj_num == 0) { + return false; + } + +#ifdef PDF_ENABLE_XFA + // XFA pages do not have CPDF_Page dictionaries. Match the other + // EPDFDoc_*ByObjectNumber APIs and reject object-number page mutations for + // XFA-backed documents. + if (doc->GetExtension()) { + return false; + } +#endif // PDF_ENABLE_XFA + + const int page_index = doc->GetPageIndex(obj_num); + if (page_index < 0) { + return false; + } + + const uint32_t deleted_obj_num = doc->DeletePage(page_index); + if (deleted_obj_num == 0) { + return false; + } + + doc->SetPageToNullObject(deleted_obj_num); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPageRotationByObjectNumber(FPDF_DOCUMENT document, + unsigned int obj_num, + int rotate) { + auto* doc = CPDFDocumentFromFPDFDocument(document); + if (!doc || obj_num == 0 || rotate < 0 || rotate > 3) { + return false; + } + +#ifdef PDF_ENABLE_XFA + // XFA pages do not have CPDF_Page dictionaries. Match the other + // EPDFDoc_*ByObjectNumber APIs and reject object-number page mutations for + // XFA-backed documents. + if (doc->GetExtension()) { + return false; + } +#endif // PDF_ENABLE_XFA + + if (doc->GetPageIndex(obj_num) < 0) { + return false; + } + + RetainPtr page_dict = + ToDictionary(doc->GetMutableIndirectObject(obj_num)); + if (!page_dict) { + return false; + } + + page_dict->SetNewFor(pdfium::page_object::kRotate, rotate * 90); + return true; } FPDF_EXPORT unsigned int FPDF_CALLCONV @@ -712,11 +887,13 @@ EPDFPage_GetObjectNumber(FPDF_PAGE page) { // Note: CPDFPageFromFPDFPage() returns null for XFA pages, so this function // returns 0 for XFA pages (documented in the header). CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return 0; + } const CPDF_Dictionary* dict = pPage->GetDict().Get(); - if (!dict) + if (!dict) { return 0; + } return dict->GetObjNum(); } @@ -851,10 +1028,11 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDF_RenderPage(HDC dc, int size_y, int rotate, int flags) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return false; } + CPDF_Page* pPage = page_view.Get(); auto owned_context = std::make_unique(); CPDF_PageRenderContext* context = owned_context.get(); @@ -976,10 +1154,11 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_RenderPageBitmap(FPDF_BITMAP bitmap, int size_y, int rotate, int flags) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return; } + CPDF_Page* pPage = page_view.Get(); RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); if (!pBitmap) { @@ -1012,10 +1191,11 @@ FPDF_RenderPageBitmapWithMatrix(FPDF_BITMAP bitmap, const FS_MATRIX* matrix, const FS_RECTF* clipping, int flags) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return; } + CPDF_Page* pPage = page_view.Get(); RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); if (!pBitmap) { @@ -1059,30 +1239,38 @@ EPDF_RenderAnnotBitmap(FPDF_BITMAP bitmap, int flags) { // Guards CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!bitmap || !pPage || !annot) + if (!bitmap || !pPage || !annot) { return false; + } CPDF_AnnotContext* pAnnotContext = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!pAnnotContext) + if (!pAnnotContext) { return false; + } + + CPDF_DocumentViewScope document_view(pPage->GetDocument()); // Get the annotation's dictionary from the context. - RetainPtr pAnnotDict = pAnnotContext->GetMutableAnnotDict(); - if (!pAnnotDict) + const CPDF_Dictionary* pAnnotDict = pAnnotContext->GetAnnotDict(); + if (!pAnnotDict) { return false; + } // Get the document from the page. The CPDF_Annot constructor needs it. CPDF_Document* pDoc = pPage->GetDocument(); - if (!pDoc) + if (!pDoc) { return false; + } // Instantiate CPDF_Annot using its public constructor. - auto pAnnot = std::make_unique(std::move(pAnnotDict), pDoc); + auto pAnnot = std::make_unique( + pdfium::WrapRetain(const_cast(pAnnotDict)), pDoc); // ---------------------------------------------------------------- bitmaps RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); - if (!pBitmap) + if (!pBitmap) { return false; + } ValidateBitmapPremultiplyState(pBitmap); #if defined(PDF_USE_SKIA) @@ -1095,8 +1283,9 @@ EPDF_RenderAnnotBitmap(FPDF_BITMAP bitmap, // CTM = DisplayMatrix * userMatrix * Translate(bbox.left, bbox.bottom) CFX_Matrix ctm = pPage->GetDisplayMatrix(); - if (matrix) + if (matrix) { ctm.Concat(CFXMatrixFromFSMatrix(*matrix)); + } // Draw appearance const bool ok = pAnnot->DrawAppearance( @@ -1115,39 +1304,50 @@ EPDF_RenderAnnotBitmapUnrotated(FPDF_BITMAP bitmap, int flags) { // Guards (same as EPDF_RenderAnnotBitmap) CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!bitmap || !pPage || !annot) + if (!bitmap || !pPage || !annot) { return false; + } CPDF_AnnotContext* pAnnotContext = CPDFAnnotContextFromFPDFAnnotation(annot); - if (!pAnnotContext) + if (!pAnnotContext) { return false; + } - RetainPtr pAnnotDict = pAnnotContext->GetMutableAnnotDict(); - if (!pAnnotDict) + CPDF_DocumentViewScope document_view(pPage->GetDocument()); + const CPDF_Dictionary* pAnnotDict = pAnnotContext->GetAnnotDict(); + if (!pAnnotDict) { return false; + } CPDF_Document* pDoc = pPage->GetDocument(); - if (!pDoc) + if (!pDoc) { return false; + } - auto pAnnot = std::make_unique(std::move(pAnnotDict), pDoc); + auto pAnnot = std::make_unique( + pdfium::WrapRetain(const_cast(pAnnotDict)), pDoc); // Get the AP form for the requested mode. // Note: we skip ShouldDrawAnnotation/GenerateAPIfNeeded (private) because // the AP stream is expected to already exist when rendering stamps. auto mode = static_cast(appearanceMode); CPDF_Form* pForm = pAnnot->GetAPForm(pPage, mode); - if (!pForm) + if (!pForm) { return false; + } // Read the raw BBox WITHOUT applying the AP Matrix. CFX_FloatRect form_bbox = pForm->GetDict()->GetRectFor("BBox"); - // Use EPDFUnrotatedRect as the target rect for MatchRect. - // Falls back to /Rect if EPDFUnrotatedRect is not set. - CFX_FloatRect target = pAnnot->GetAnnotDict()->GetRectFor("EPDFUnrotatedRect"); - if (target.IsEmpty()) + // Use /EMBD_Metadata /UnrotatedRect as the target rect for MatchRect. + // Falls back to /Rect if EmbedPDF metadata is not set. + RetainPtr metadata = + pAnnot->GetAnnotDict()->GetDictFor("EMBD_Metadata"); + CFX_FloatRect target = + metadata ? metadata->GetRectFor("UnrotatedRect") : CFX_FloatRect(); + if (target.IsEmpty()) { target = pAnnot->GetRect(); + } // The form's Matrix (rotation) was baked into the content objects during // parsing by CPDF_ContentParser. We must undo it so the bitmap is unrotated. @@ -1161,16 +1361,18 @@ EPDF_RenderAnnotBitmapUnrotated(FPDF_BITMAP bitmap, // Build CTM = displayMatrix * userMatrix CFX_Matrix ctm = pPage->GetDisplayMatrix(); - if (matrix) + if (matrix) { ctm.Concat(CFXMatrixFromFSMatrix(*matrix)); + } // Combine: form -> page -> device mtForm2Page.Concat(ctm); // ---- Bitmap setup (same as EPDF_RenderAnnotBitmap) ---- RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); - if (!pBitmap) + if (!pBitmap) { return false; + } ValidateBitmapPremultiplyState(pBitmap); #if defined(PDF_USE_SKIA) @@ -1183,7 +1385,8 @@ EPDF_RenderAnnotBitmapUnrotated(FPDF_BITMAP bitmap, // Render the AP form with our custom matrix (no AP Matrix distortion). CPDF_RenderContext context(pDoc, - pPage->GetMutablePageResources(), + pdfium::WrapRetain(const_cast( + pPage->GetPageResources().Get())), pPage->GetPageImageCache()); context.AppendLayer(pForm, mtForm2Page); context.Render(device.get(), nullptr, nullptr, nullptr); @@ -1200,10 +1403,11 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_RenderPageSkia(FPDF_SKIA_CANVAS canvas, return; } - CPDF_Page* cpdf_page = CPDFPageFromFPDFPage(page); - if (!cpdf_page) { + ScopedFPDFPageView page_view(page); + if (!page_view) { return; } + CPDF_Page* cpdf_page = page_view.Get(); auto owned_context = std::make_unique(); CPDF_PageRenderContext* context = owned_context.get(); @@ -1242,9 +1446,6 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_ClosePage(FPDF_PAGE page) { } FPDF_EXPORT void FPDF_CALLCONV FPDF_CloseDocument(FPDF_DOCUMENT document) { - // Cleanup pending security BEFORE deletion - EPDF_CleanupPendingSecurity(document); - // Take it back across the API and throw it away, std::unique_ptr(CPDFDocumentFromFPDFDocument(document)); } @@ -1500,7 +1701,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; } @@ -1512,70 +1716,121 @@ FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, return true; } +static RetainPtr GetPageDictionaryByIndex( + FPDF_DOCUMENT document, + CPDF_Document* doc, + int page_index); +static int GetInheritedPageRotation(const CPDF_Dictionary* page_dict); + FPDF_EXPORT int FPDF_CALLCONV EPDF_GetPageRotationByIndex(FPDF_DOCUMENT document, int page_index) { auto* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc) + if (!pDoc) { return -1; + } - if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) + RetainPtr dict = + GetPageDictionaryByIndex(document, pDoc, page_index); + if (!dict) { return -1; + } + return GetInheritedPageRotation(dict.Get()); +} - // Cheap: no ParseContent(). - RetainPtr dict = pDoc->GetMutablePageDictionary(page_index); - if (!dict) - return -1; - auto page = pdfium::MakeRetain(pDoc, std::move(dict)); - return page->GetPageRotation(); +static RetainPtr GetPageDictionaryByIndex( + FPDF_DOCUMENT document, + CPDF_Document* doc, + int page_index) { + if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) { + return nullptr; + } + return doc->GetPageDictionary(page_index); +} + +static ByteStringView GetPageBoxKey(EPDF_PAGE_BOX_TYPE box_type) { + switch (box_type) { + case EPDF_PAGE_BOX_MEDIA: + return pdfium::page_object::kMediaBox; + case EPDF_PAGE_BOX_CROP: + return pdfium::page_object::kCropBox; + case EPDF_PAGE_BOX_BLEED: + return pdfium::page_object::kBleedBox; + case EPDF_PAGE_BOX_TRIM: + return pdfium::page_object::kTrimBox; + case EPDF_PAGE_BOX_ART: + return pdfium::page_object::kArtBox; + } + return ByteStringView(); } // Walk the page tree (/Parent chain) to resolve an inherited rectangle // attribute. Mirrors the logic of CPDF_Page::GetPageAttr + GetBox but works // directly on a dictionary pointer so we can avoid constructing a CPDF_Page. -static CFX_FloatRect GetInheritedRect(const CPDF_Dictionary* pPageDict, +static CFX_FloatRect GetInheritedRect(const CPDF_Dictionary* page_dict, ByteStringView name) { std::set visited; - const CPDF_Dictionary* pDict = pPageDict; - while (pDict && !visited.contains(pDict)) { - RetainPtr pObj = pDict->GetDirectObjectFor(name); - if (pObj) { - RetainPtr pArray = ToArray(std::move(pObj)); - if (pArray) { - CFX_FloatRect rect = pArray->GetRect(); + const CPDF_Dictionary* dict = page_dict; + while (dict && !visited.contains(dict)) { + RetainPtr object = dict->GetDirectObjectFor(name); + if (object) { + RetainPtr array = ToArray(std::move(object)); + if (array) { + CFX_FloatRect rect = array->GetRect(); rect.Normalize(); return rect; } } - visited.insert(pDict); - pDict = pDict->GetDictFor(pdfium::page_object::kParent).Get(); + visited.insert(dict); + dict = dict->GetDictFor(pdfium::page_object::kParent).Get(); } return CFX_FloatRect(); } +static int GetInheritedPageRotation(const CPDF_Dictionary* page_dict) { + std::set visited; + const CPDF_Dictionary* dict = page_dict; + while (dict && !visited.contains(dict)) { + if (dict->KeyExist(pdfium::page_object::kRotate)) { + int rotation = + (dict->GetIntegerFor(pdfium::page_object::kRotate) / 90) % 4; + return rotation < 0 ? rotation + 4 : rotation; + } + visited.insert(dict); + dict = dict->GetDictFor(pdfium::page_object::kParent).Get(); + } + return 0; +} + +static CFX_FloatRect GetEffectiveMediaBox(const CPDF_Dictionary* page_dict) { + CFX_FloatRect media_box = + GetInheritedRect(page_dict, pdfium::page_object::kMediaBox); + if (media_box.IsEmpty()) { + media_box = CFX_FloatRect(0, 0, 612, 792); + } + return media_box; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_GetPageSizeByIndexNormalized(FPDF_DOCUMENT document, - int page_index, - FS_SIZEF* size) { - if (!size) + int page_index, + FS_SIZEF* size) { + if (!size) { return false; + } auto* pDoc = CPDFDocumentFromFPDFDocument(document); - if (!pDoc) - return false; - - if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) + if (!pDoc) { return false; + } - RetainPtr dict = pDoc->GetMutablePageDictionary(page_index); - if (!dict) + RetainPtr dict = + GetPageDictionaryByIndex(document, pDoc, page_index); + if (!dict) { return false; + } // Resolve MediaBox/CropBox via page tree inheritance (not just the page dict) - CFX_FloatRect mediabox = - GetInheritedRect(dict.Get(), pdfium::page_object::kMediaBox); - if (mediabox.IsEmpty()) - mediabox = CFX_FloatRect(0, 0, 612, 792); - + CFX_FloatRect mediabox = GetEffectiveMediaBox(dict.Get()); CFX_FloatRect cropbox = GetInheritedRect(dict.Get(), pdfium::page_object::kCropBox); CFX_FloatRect bbox = cropbox.IsEmpty() ? mediabox : cropbox; @@ -1587,14 +1842,88 @@ EPDF_GetPageSizeByIndexNormalized(FPDF_DOCUMENT document, return true; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_GetPageBoxByIndex(FPDF_DOCUMENT document, + int page_index, + EPDF_PAGE_BOX_TYPE box_type, + FS_RECTF* box) { + if (!box) { + return false; + } + + auto* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + RetainPtr dict = + GetPageDictionaryByIndex(document, pDoc, page_index); + if (!dict) { + return false; + } + + CFX_FloatRect rect; + switch (box_type) { + case EPDF_PAGE_BOX_MEDIA: + rect = GetEffectiveMediaBox(dict.Get()); + break; + case EPDF_PAGE_BOX_CROP: + rect = GetInheritedRect(dict.Get(), pdfium::page_object::kCropBox); + if (rect.IsEmpty()) { + rect = GetEffectiveMediaBox(dict.Get()); + } + break; + case EPDF_PAGE_BOX_BLEED: + case EPDF_PAGE_BOX_TRIM: + case EPDF_PAGE_BOX_ART: + rect = GetInheritedRect(dict.Get(), GetPageBoxKey(box_type)); + if (rect.IsEmpty()) { + return false; + } + break; + } + + if (rect.IsEmpty()) { + return false; + } + + *box = FSRectFFromCFXFloatRect(rect); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_GetPageUserUnitByIndex(FPDF_DOCUMENT document, + int page_index, + float* user_unit) { + if (!user_unit) { + return false; + } + + auto* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + RetainPtr dict = + GetPageDictionaryByIndex(document, pDoc, page_index); + if (!dict) { + return false; + } + + float value = dict->GetFloatFor("UserUnit"); + *user_unit = value > 0 ? value : 1.0f; + return true; +} + FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV EPDF_LoadPageNormalized(FPDF_DOCUMENT document, - int page_index, - int* out_original_rotation) { + int page_index, + int* out_original_rotation) { // Load page normally first FPDF_PAGE page = FPDF_LoadPage(document, page_index); - if (!page) + if (!page) { return nullptr; + } CPDF_Page* pPage = CPDFPageFromFPDFPage(page); if (!pPage) { @@ -1722,7 +2051,8 @@ FPDF_VIEWERREF_GetName(FPDF_DOCUMENT document, FPDF_EXPORT FPDF_DWORD FPDF_CALLCONV FPDF_CountNamedDests(FPDF_DOCUMENT document) { - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc) { return 0; } @@ -1732,7 +2062,7 @@ FPDF_CountNamedDests(FPDF_DOCUMENT document) { return 0; } - auto name_tree = CPDF_NameTree::Create(doc, "Dests"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "Dests"); FX_SAFE_UINT32 count = name_tree ? name_tree->GetCount() : 0; RetainPtr pOldStyleDests = pRoot->GetDictFor("Dests"); if (pOldStyleDests) { @@ -1747,7 +2077,8 @@ FPDF_GetNamedDestByName(FPDF_DOCUMENT document, FPDF_BYTESTRING name) { return nullptr; } - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc) { return nullptr; } @@ -1844,7 +2175,8 @@ FPDF_EXPORT FPDF_DEST FPDF_CALLCONV FPDF_GetNamedDest(FPDF_DOCUMENT document, return nullptr; } - CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document); + ScopedFPDFDocumentView document_view(document); + CPDF_Document* doc = document_view.Get(); if (!doc) { return nullptr; } @@ -1854,7 +2186,7 @@ FPDF_EXPORT FPDF_DEST FPDF_CALLCONV FPDF_GetNamedDest(FPDF_DOCUMENT document, return nullptr; } - auto name_tree = CPDF_NameTree::Create(doc, "Dests"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "Dests"); size_t name_tree_count = name_tree ? name_tree->GetCount() : 0; RetainPtr pDestObj; WideString wsName; diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index a4f21974cf..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,534 +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(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(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/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 56b87842e4..f0603e9d99 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -13,11 +14,22 @@ #include #include "build/build_config.h" +#include "constants/page_object.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/cpdf_reference.h" #include "core/fxge/cfx_defaultrenderdevice.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/fpdf_view_c_api_test.h" #include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_attachment.h" +#include "public/fpdf_doc.h" +#include "public/fpdf_edit.h" +#include "public/fpdf_javascript.h" +#include "public/fpdf_save.h" +#include "public/fpdf_text.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" @@ -46,6 +58,20 @@ namespace { constexpr char kFirstAlternate[] = "FirstAlternate"; constexpr char kLastAlternate[] = "LastAlternate"; +uint32_t GetReferencedObjectNumber(const CPDF_Dictionary* dict, + ByteStringView key) { + if (!dict) { + return 0; + } + + RetainPtr ref = ToReference(dict->GetObjectFor(key)); + return ref ? ref->GetRefObjNum() : 0; +} + +bool PdfBytesContainObject(const std::string& pdf, uint32_t objnum) { + return pdf.find(std::to_string(objnum) + " 0 obj") != std::string::npos; +} + #if BUILDFLAG(IS_WIN) const char kExpectedRectanglePostScript[] = R"( save @@ -108,6 +134,48 @@ class MockDownloadHints final : public FX_DOWNLOADHINTS { ~MockDownloadHints() = default; }; +struct CountingStringFileAccess { + explicit CountingStringFileAccess(std::string data) : data(std::move(data)) { + file_access.m_FileLen = this->data.size(); + file_access.m_GetBlock = &CountingStringFileAccess::GetBlock; + file_access.m_Param = this; + } + + FPDF_FILEACCESS* get() { return &file_access; } + void ResetCounts() { + read_count = 0; + read_bytes = 0; + } + + static int GetBlock(void* param, + unsigned long pos, + unsigned char* buf, + unsigned long size) { + CountingStringFileAccess* file = + static_cast(param); + if (!file || pos > file->data.size() || size > file->data.size() - pos) { + return 0; + } + memcpy(buf, file->data.data() + pos, size); + ++file->read_count; + file->read_bytes += size; + return 1; + } + + std::string data; + FPDF_FILEACCESS file_access = {}; + size_t read_count = 0; + size_t read_bytes = 0; +}; + +uint64_t ReadUint64LEForTest(const char* data) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(static_cast(data[i])) << (i * 8); + } + return value; +} + #if defined(PDF_USE_SKIA) ScopedFPDFBitmap SkImageToPdfiumBitmap(const SkImage& image) { ScopedFPDFBitmap bitmap( @@ -158,6 +226,393 @@ TEST(fpdf, CApiTest) { class FPDFViewEmbedderTest : public EmbedderTest { protected: + void CheckReadOnlyLayerWorkflowProducesEmptyDelta(const char* file_name) { + FileAccessForTesting base_access(file_name); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base) << file_name; + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer) << file_name; + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status) << file_name; + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())) << file_name; + + const int page_count = FPDF_GetPageCount(layer.get()); + ASSERT_GT(page_count, 0) << file_name; + for (int page_index = 0; page_index < page_count; ++page_index) { + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), page_index)); + ASSERT_TRUE(page) << file_name << " page " << page_index; + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + ASSERT_TRUE(bitmap) << file_name << " page " << page_index; + + const int annot_count = FPDFPage_GetAnnotCount(page.get()); + for (int annot_index = 0; annot_index < annot_count; ++annot_index) { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), annot_index)); + ASSERT_TRUE(annot) << file_name << " annot " << annot_index; + + (void)FPDFAnnot_GetFlags(annot.get()); + (void)EPDFAnnot_GetBlendMode(annot.get()); + FPDF_STANDARD_FONT font = FPDF_FONT_UNKNOWN; + float font_size = 0; + unsigned int red = 0; + unsigned int green = 0; + unsigned int blue = 0; + (void)EPDFAnnot_GetDefaultAppearance( + annot.get(), &font, &font_size, &red, &green, &blue); + const int object_count = FPDFAnnot_GetObjectCount(annot.get()); + if (object_count > 0) { + EXPECT_TRUE(FPDFAnnot_GetObject(annot.get(), 0)) + << file_name << " annot " << annot_index; + } + + if (FPDFAnnot_GetSubtype(annot.get()) == FPDF_ANNOT_LINK) { + EXPECT_TRUE(FPDFAnnot_GetLink(annot.get())) + << file_name << " annot " << annot_index; + } + if (FPDFAnnot_GetSubtype(annot.get()) == FPDF_ANNOT_FILEATTACHMENT) { + EXPECT_TRUE(FPDFAnnot_GetFileAttachment(annot.get())) + << file_name << " annot " << annot_index; + } + + ScopedFPDFAnnotation linked( + FPDFAnnot_GetLinkedAnnot(annot.get(), "IRT")); + } + + int link_pos = 0; + FPDF_LINK link = nullptr; + while (FPDFLink_Enumerate(page.get(), &link_pos, &link)) { + ASSERT_TRUE(link) << file_name << " page " << page_index; + } + } + + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())) << file_name; + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)) + << file_name; + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status) << file_name; + EXPECT_TRUE(GetString().empty()) << file_name; + + EPDF_ReleaseBaseDocument(base); + } + + void CheckReadOnlyLayerParityProducesEmptyDelta(const char* file_name) { + FileAccessForTesting plain_access(file_name); + ScopedFPDFDocument plain(FPDF_LoadCustomDocument(&plain_access, nullptr)); + ASSERT_TRUE(plain) << file_name; + + FileAccessForTesting base_access(file_name); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base) << file_name; + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer) << file_name; + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status) << file_name; + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())) << file_name; + + CompareDocumentReadApis(plain.get(), layer.get(), file_name); + + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())) << file_name; + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)) + << file_name; + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status) << file_name; + EXPECT_TRUE(GetString().empty()) << file_name; + + EPDF_ReleaseBaseDocument(base); + } + + void CompareDocumentReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + const char* file_name) { + const int plain_page_count = FPDF_GetPageCount(plain); + EXPECT_EQ(plain_page_count, FPDF_GetPageCount(layer)) << file_name; + + CompareNamedDestReadApis(plain, layer, file_name); + CompareAttachmentReadApis(plain, layer, file_name); + CompareJavaScriptReadApis(plain, layer, file_name); + CompareBookmarkReadApis(plain, layer, file_name); + + for (int page_index = -1; page_index <= plain_page_count; ++page_index) { + ComparePageLabelReadApi(plain, layer, page_index, file_name); + } + + for (int page_index = 0; page_index < plain_page_count; ++page_index) { + FS_SIZEF plain_size; + FS_SIZEF layer_size; + const bool plain_has_size = + FPDF_GetPageSizeByIndexF(plain, page_index, &plain_size); + const bool layer_has_size = + FPDF_GetPageSizeByIndexF(layer, page_index, &layer_size); + EXPECT_EQ(plain_has_size, layer_has_size) + << file_name << " page " << page_index; + if (plain_has_size && layer_has_size) { + EXPECT_FLOAT_EQ(plain_size.width, layer_size.width) + << file_name << " page " << page_index; + EXPECT_FLOAT_EQ(plain_size.height, layer_size.height) + << file_name << " page " << page_index; + } + + ScopedFPDFPage plain_page(FPDF_LoadPage(plain, page_index)); + ScopedFPDFPage layer_page(FPDF_LoadPage(layer, page_index)); + EXPECT_EQ(!!plain_page, !!layer_page) + << file_name << " page " << page_index; + if (!plain_page || !layer_page) { + continue; + } + + CompareLoadedPageReadApis(plain, plain_page.get(), layer, + layer_page.get(), file_name, page_index); + } + } + + void CompareNamedDestReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + const char* file_name) { + const unsigned long plain_count = FPDF_CountNamedDests(plain); + ASSERT_EQ(plain_count, FPDF_CountNamedDests(layer)) << file_name; + + static constexpr const char* kNamesToProbe[] = { + "", "First", "Next", kFirstAlternate, kLastAlternate, "NoSuchName"}; + for (const char* name : kNamesToProbe) { + EXPECT_EQ(!!FPDF_GetNamedDestByName(plain, name), + !!FPDF_GetNamedDestByName(layer, name)) + << file_name << " named dest " << name; + } + + for (unsigned long index = 0; index < plain_count; ++index) { + char plain_buffer[512]; + char layer_buffer[512]; + long plain_size = sizeof(plain_buffer); + long layer_size = sizeof(layer_buffer); + const bool plain_has_dest = + FPDF_GetNamedDest(plain, index, plain_buffer, &plain_size); + const bool layer_has_dest = + FPDF_GetNamedDest(layer, index, layer_buffer, &layer_size); + EXPECT_EQ(plain_has_dest, layer_has_dest) + << file_name << " named dest index " << index; + EXPECT_EQ(plain_size, layer_size) + << file_name << " named dest index " << index; + if (plain_has_dest && layer_has_dest) { + EXPECT_EQ( + GetPlatformString(reinterpret_cast(plain_buffer)), + GetPlatformString(reinterpret_cast(layer_buffer))) + << file_name << " named dest index " << index; + } + } + } + + void CompareAttachmentReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + const char* file_name) { + const int plain_count = FPDFDoc_GetAttachmentCount(plain); + ASSERT_EQ(plain_count, FPDFDoc_GetAttachmentCount(layer)) << file_name; + for (int index = -1; index <= plain_count; ++index) { + EXPECT_EQ(!!FPDFDoc_GetAttachment(plain, index), + !!FPDFDoc_GetAttachment(layer, index)) + << file_name << " attachment " << index; + } + } + + void CompareJavaScriptReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + const char* file_name) { + const int plain_count = FPDFDoc_GetJavaScriptActionCount(plain); + ASSERT_EQ(plain_count, FPDFDoc_GetJavaScriptActionCount(layer)) + << file_name; + for (int index = -1; index <= plain_count; ++index) { + ScopedFPDFJavaScriptAction plain_js( + FPDFDoc_GetJavaScriptAction(plain, index)); + ScopedFPDFJavaScriptAction layer_js( + FPDFDoc_GetJavaScriptAction(layer, index)); + EXPECT_EQ(!!plain_js, !!layer_js) + << file_name << " JavaScript action " << index; + if (!plain_js || !layer_js) { + continue; + } + EXPECT_EQ(FPDFJavaScriptAction_GetName(plain_js.get(), nullptr, 0), + FPDFJavaScriptAction_GetName(layer_js.get(), nullptr, 0)) + << file_name << " JavaScript action " << index; + EXPECT_EQ(FPDFJavaScriptAction_GetScript(plain_js.get(), nullptr, 0), + FPDFJavaScriptAction_GetScript(layer_js.get(), nullptr, 0)) + << file_name << " JavaScript action " << index; + } + } + + void CompareBookmarkReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + const char* file_name) { + CompareBookmarkSubtreeReadApis( + plain, layer, FPDFBookmark_GetFirstChild(plain, nullptr), + FPDFBookmark_GetFirstChild(layer, nullptr), file_name, 0); + } + + void CompareBookmarkSubtreeReadApis(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + FPDF_BOOKMARK plain_bookmark, + FPDF_BOOKMARK layer_bookmark, + const char* file_name, + int depth) { + EXPECT_EQ(!!plain_bookmark, !!layer_bookmark) + << file_name << " bookmark depth " << depth; + if (!plain_bookmark || !layer_bookmark || depth >= 4) { + return; + } + + EXPECT_EQ(FPDFBookmark_GetTitle(plain_bookmark, nullptr, 0), + FPDFBookmark_GetTitle(layer_bookmark, nullptr, 0)) + << file_name << " bookmark depth " << depth; + EXPECT_EQ(FPDFBookmark_GetCount(plain_bookmark), + FPDFBookmark_GetCount(layer_bookmark)) + << file_name << " bookmark depth " << depth; + EXPECT_EQ(!!FPDFBookmark_GetDest(plain, plain_bookmark), + !!FPDFBookmark_GetDest(layer, layer_bookmark)) + << file_name << " bookmark depth " << depth; + EXPECT_EQ(!!FPDFBookmark_GetAction(plain_bookmark), + !!FPDFBookmark_GetAction(layer_bookmark)) + << file_name << " bookmark depth " << depth; + + CompareBookmarkSubtreeReadApis( + plain, layer, FPDFBookmark_GetFirstChild(plain, plain_bookmark), + FPDFBookmark_GetFirstChild(layer, layer_bookmark), file_name, + depth + 1); + CompareBookmarkSubtreeReadApis( + plain, layer, FPDFBookmark_GetNextSibling(plain, plain_bookmark), + FPDFBookmark_GetNextSibling(layer, layer_bookmark), file_name, depth); + } + + void ComparePageLabelReadApi(FPDF_DOCUMENT plain, + FPDF_DOCUMENT layer, + int page_index, + const char* file_name) { + char plain_buffer[128]; + char layer_buffer[128]; + const unsigned long plain_size = FPDF_GetPageLabel( + plain, page_index, plain_buffer, sizeof(plain_buffer)); + const unsigned long layer_size = FPDF_GetPageLabel( + layer, page_index, layer_buffer, sizeof(layer_buffer)); + EXPECT_EQ(plain_size, layer_size) + << file_name << " page label " << page_index; + if (plain_size > 0 && plain_size <= sizeof(plain_buffer)) { + EXPECT_EQ( + GetPlatformString(reinterpret_cast(plain_buffer)), + GetPlatformString(reinterpret_cast(layer_buffer))) + << file_name << " page label " << page_index; + } + } + + void CompareLoadedPageReadApis(FPDF_DOCUMENT plain_doc, + FPDF_PAGE plain_page, + FPDF_DOCUMENT layer_doc, + FPDF_PAGE layer_page, + const char* file_name, + int page_index) { + EXPECT_FLOAT_EQ(FPDF_GetPageWidthF(plain_page), + FPDF_GetPageWidthF(layer_page)) + << file_name << " page " << page_index; + EXPECT_FLOAT_EQ(FPDF_GetPageHeightF(plain_page), + FPDF_GetPageHeightF(layer_page)) + << file_name << " page " << page_index; + + CompareAnnotationReadApis(plain_page, layer_page, file_name, page_index); + CompareLinkReadApis(plain_doc, plain_page, layer_doc, layer_page, file_name, + page_index); + CompareTextReadApis(plain_page, layer_page, file_name, page_index); + + ScopedFPDFBitmap bitmap = RenderPage(layer_page); + ASSERT_TRUE(bitmap) << file_name << " page " << page_index; + } + + void CompareAnnotationReadApis(FPDF_PAGE plain_page, + FPDF_PAGE layer_page, + const char* file_name, + int page_index) { + const int plain_annot_count = FPDFPage_GetAnnotCount(plain_page); + ASSERT_EQ(plain_annot_count, FPDFPage_GetAnnotCount(layer_page)) + << file_name << " page " << page_index; + for (int annot_index = -1; annot_index <= plain_annot_count; + ++annot_index) { + ScopedFPDFAnnotation plain_annot( + FPDFPage_GetAnnot(plain_page, annot_index)); + ScopedFPDFAnnotation layer_annot( + FPDFPage_GetAnnot(layer_page, annot_index)); + EXPECT_EQ(!!plain_annot, !!layer_annot) + << file_name << " page " << page_index << " annot " << annot_index; + } + } + + void CompareLinkReadApis(FPDF_DOCUMENT plain_doc, + FPDF_PAGE plain_page, + FPDF_DOCUMENT layer_doc, + FPDF_PAGE layer_page, + const char* file_name, + int page_index) { + std::vector plain_links; + std::vector layer_links; + int pos = 0; + FPDF_LINK link = nullptr; + while (FPDFLink_Enumerate(plain_page, &pos, &link)) { + plain_links.push_back(link); + } + pos = 0; + link = nullptr; + while (FPDFLink_Enumerate(layer_page, &pos, &link)) { + layer_links.push_back(link); + } + ASSERT_EQ(plain_links.size(), layer_links.size()) + << file_name << " page " << page_index; + for (size_t i = 0; i < plain_links.size(); ++i) { + EXPECT_EQ(!!FPDFLink_GetDest(plain_doc, plain_links[i]), + !!FPDFLink_GetDest(layer_doc, layer_links[i])) + << file_name << " page " << page_index << " link " << i; + EXPECT_EQ(!!FPDFLink_GetAction(plain_links[i]), + !!FPDFLink_GetAction(layer_links[i])) + << file_name << " page " << page_index << " link " << i; + EXPECT_EQ(FPDFLink_CountQuadPoints(plain_links[i]), + FPDFLink_CountQuadPoints(layer_links[i])) + << file_name << " page " << page_index << " link " << i; + EXPECT_EQ(!!FPDFLink_GetAnnot(plain_page, plain_links[i]), + !!FPDFLink_GetAnnot(layer_page, layer_links[i])) + << file_name << " page " << page_index << " link " << i; + } + } + + void CompareTextReadApis(FPDF_PAGE plain_page, + FPDF_PAGE layer_page, + const char* file_name, + int page_index) { + ScopedFPDFTextPage plain_text(FPDFText_LoadPage(plain_page)); + ScopedFPDFTextPage layer_text(FPDFText_LoadPage(layer_page)); + EXPECT_EQ(!!plain_text, !!layer_text) + << file_name << " page " << page_index; + if (!plain_text || !layer_text) { + return; + } + + const int plain_char_count = FPDFText_CountChars(plain_text.get()); + ASSERT_EQ(plain_char_count, FPDFText_CountChars(layer_text.get())) + << file_name << " page " << page_index; + if (plain_char_count <= 0) { + return; + } + + EXPECT_EQ(FPDFText_GetUnicode(plain_text.get(), 0), + FPDFText_GetUnicode(layer_text.get(), 0)) + << file_name << " page " << page_index; + EXPECT_EQ(FPDFText_GetUnicode(plain_text.get(), plain_char_count - 1), + FPDFText_GetUnicode(layer_text.get(), plain_char_count - 1)) + << file_name << " page " << page_index; + EXPECT_EQ(FPDFText_CountRects(plain_text.get(), 0, plain_char_count), + FPDFText_CountRects(layer_text.get(), 0, plain_char_count)) + << file_name << " page " << page_index; + } + void TestRenderPageBitmapWithMatrix(FPDF_PAGE page, int bitmap_width, int bitmap_height, @@ -310,6 +765,47 @@ class FPDFViewEmbedderTest : public EmbedderTest { } }; +TEST_F(FPDFViewEmbedderTest, LoadMemBaseDocument) { + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + std::vector file_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(file_bytes.empty()); + + EPDF_BASE_DOCUMENT base = EPDF_LoadMemBaseDocument( + file_bytes.data(), static_cast(file_bytes.size()), nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + ASSERT_TRUE(layer); + + EXPECT_EQ(1, FPDF_GetPageCount(layer.get())); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LoadMemBaseDocument64) { + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + std::vector file_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(file_bytes.empty()); + + EPDF_BASE_DOCUMENT base = + EPDF_LoadMemBaseDocument64(file_bytes.data(), file_bytes.size(), nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + EXPECT_EQ(1, FPDF_GetPageCount(layer.get())); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + EPDF_ReleaseBaseDocument(base); +} + // Test for conversion of a point in device coordinates to page coordinates TEST_F(FPDFViewEmbedderTest, DeviceCoordinatesToPageCoordinates) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); @@ -659,6 +1155,1447 @@ TEST_F(FPDFViewEmbedderTest, LoadCustomDocumentWithShortLivedFileAccess) { EXPECT_FLOAT_EQ(300.0f, FPDF_GetPageHeightF(page.get())); } +TEST_F(FPDFViewEmbedderTest, OpenFreshLayerRendersWithEmptyOverlay) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, 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(base, EPDFLayer_GetBaseDocument(layer.get())); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(layer.get(), 1)); + EXPECT_EQ(1, FPDF_GetPageCount(layer.get())); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + EXPECT_FLOAT_EQ(200.0f, FPDF_GetPageWidthF(page.get())); + EXPECT_FLOAT_EQ(300.0f, FPDF_GetPageHeightF(page.get())); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + ASSERT_TRUE(bitmap); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + } + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, OpenFreshLayerAnnotHandleDoesNotPromote) { + FileAccessForTesting base_access("annotation_stamp_with_ap.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, 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())); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ASSERT_GT(FPDFPage_GetAnnotCount(page.get()), 0); + + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + const int object_count = FPDFAnnot_GetObjectCount(annot.get()); + EXPECT_GT(object_count, 0); + EXPECT_TRUE(FPDFAnnot_GetObject(annot.get(), 0)); + (void)EPDFAnnot_GetBlendMode(annot.get()); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + ScopedFPDFBitmap bitmap( + FPDFBitmap_Create(/*width=*/612, /*height=*/792, /*alpha=*/1)); + ASSERT_TRUE(bitmap); + ASSERT_TRUE(FPDFBitmap_FillRect(bitmap.get(), 0, 0, 612, 792, 0xFFFFFFFF)); + const FS_MATRIX identity = {1, 0, 0, 1, 0, 0}; + EXPECT_TRUE(EPDF_RenderAnnotBitmap(bitmap.get(), page.get(), annot.get(), + FPDF_ANNOT_APPEARANCEMODE_NORMAL, + &identity, 0)); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + } + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, + ExistingAnnotHandlesAndDeltaReplaySeeEffectiveLayerGeometry) { + FileAccessForTesting base_access("polygon_annot.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status_a = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_a( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_a)); + ASSERT_TRUE(layer_a); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, status_a); + + EPDFLayerOpenStatus status_b = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_b( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_b)); + ASSERT_TRUE(layer_b); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, status_b); + + ScopedFPDFPage writer_page(FPDF_LoadPage(layer_a.get(), 0)); + ScopedFPDFPage reader_page(FPDF_LoadPage(layer_a.get(), 0)); + ScopedFPDFPage sibling_page(FPDF_LoadPage(layer_b.get(), 0)); + ASSERT_TRUE(writer_page); + ASSERT_TRUE(reader_page); + ASSERT_TRUE(sibling_page); + + ScopedFPDFAnnotation writer(FPDFPage_GetAnnot(writer_page.get(), 0)); + ScopedFPDFAnnotation reader(FPDFPage_GetAnnot(reader_page.get(), 0)); + ScopedFPDFAnnotation sibling(FPDFPage_GetAnnot(sibling_page.get(), 0)); + ASSERT_TRUE(writer); + ASSERT_TRUE(reader); + ASSERT_TRUE(sibling); + + ScopedFPDFBitmap original_bitmap = + RenderPageWithFlags(sibling_page.get(), nullptr, FPDF_ANNOT); + ASSERT_TRUE(original_bitmap); + const std::string original_hash = HashBitmap(original_bitmap.get()); + + FS_RECTF original_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(writer.get(), &original_rect)); + const FS_RECTF moved_rect = { + 160.0f, + 440.0f, + 500.0f, + 250.0f, + }; + const FS_POINTF moved_vertices[] = { + {176.0f, 319.0f}, + {367.0f, 434.0f}, + {489.0f, 266.42f}, + }; + + ASSERT_TRUE(FPDFAnnot_SetRect(writer.get(), &moved_rect)); + ASSERT_TRUE(EPDFAnnot_SetVertices(writer.get(), moved_vertices, + std::size(moved_vertices))); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(writer.get())); + ASSERT_TRUE(FPDFPage_GenerateContent(writer_page.get())); + + FS_RECTF observed_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(reader.get(), &observed_rect)); + EXPECT_FLOAT_EQ(moved_rect.left, observed_rect.left); + EXPECT_FLOAT_EQ(moved_rect.top, observed_rect.top); + EXPECT_FLOAT_EQ(moved_rect.right, observed_rect.right); + EXPECT_FLOAT_EQ(moved_rect.bottom, observed_rect.bottom); + + std::vector observed_vertices(std::size(moved_vertices)); + ASSERT_EQ(observed_vertices.size(), + FPDFAnnot_GetVertices(reader.get(), observed_vertices.data(), + observed_vertices.size())); + for (size_t i = 0; i < observed_vertices.size(); ++i) { + EXPECT_FLOAT_EQ(moved_vertices[i].x, observed_vertices[i].x); + EXPECT_FLOAT_EQ(moved_vertices[i].y, observed_vertices[i].y); + } + + FS_RECTF sibling_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(sibling.get(), &sibling_rect)); + EXPECT_FLOAT_EQ(original_rect.left, sibling_rect.left); + EXPECT_FLOAT_EQ(original_rect.top, sibling_rect.top); + EXPECT_FLOAT_EQ(original_rect.right, sibling_rect.right); + EXPECT_FLOAT_EQ(original_rect.bottom, sibling_rect.bottom); + + ScopedFPDFBitmap writer_bitmap = + RenderPageWithFlags(writer_page.get(), nullptr, FPDF_ANNOT); + ScopedFPDFBitmap reader_bitmap = + RenderPageWithFlags(reader_page.get(), nullptr, FPDF_ANNOT); + ScopedFPDFBitmap sibling_bitmap = + RenderPageWithFlags(sibling_page.get(), nullptr, FPDF_ANNOT); + ASSERT_TRUE(writer_bitmap); + ASSERT_TRUE(reader_bitmap); + ASSERT_TRUE(sibling_bitmap); + const std::string moved_hash = HashBitmap(writer_bitmap.get()); + EXPECT_EQ(moved_hash, HashBitmap(reader_bitmap.get())); + EXPECT_EQ(original_hash, HashBitmap(sibling_bitmap.get())); + EXPECT_NE(original_hash, moved_hash); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_a.get(), this, &save_status)); + ASSERT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string 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 replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + ScopedFPDFAnnotation replayed_annot( + FPDFPage_GetAnnot(replayed_page.get(), 0)); + ASSERT_TRUE(replayed_annot); + + FS_RECTF replayed_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(replayed_annot.get(), &replayed_rect)); + EXPECT_FLOAT_EQ(moved_rect.left, replayed_rect.left); + EXPECT_FLOAT_EQ(moved_rect.top, replayed_rect.top); + EXPECT_FLOAT_EQ(moved_rect.right, replayed_rect.right); + EXPECT_FLOAT_EQ(moved_rect.bottom, replayed_rect.bottom); + + std::vector replayed_vertices(std::size(moved_vertices)); + ASSERT_EQ( + replayed_vertices.size(), + FPDFAnnot_GetVertices(replayed_annot.get(), replayed_vertices.data(), + replayed_vertices.size())); + for (size_t i = 0; i < replayed_vertices.size(); ++i) { + EXPECT_FLOAT_EQ(moved_vertices[i].x, replayed_vertices[i].x); + EXPECT_FLOAT_EQ(moved_vertices[i].y, replayed_vertices[i].y); + } + + ScopedFPDFBitmap replayed_bitmap = + RenderPageWithFlags(replayed_page.get(), nullptr, FPDF_ANNOT); + ASSERT_TRUE(replayed_bitmap); + EXPECT_EQ(moved_hash, HashBitmap(replayed_bitmap.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, + PromotedBaseAnnotReopensByObjectNumberFromEffectiveLayer) { + FileAccessForTesting base_access("polygon_annot.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation writer(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(writer); + const uint32_t object_number = EPDFAnnot_GetObjectNumber(writer.get()); + ASSERT_GT(object_number, 0u); + + const FS_RECTF moved_rect = { + 160.0f, + 440.0f, + 500.0f, + 250.0f, + }; + ASSERT_TRUE(FPDFAnnot_SetRect(writer.get(), &moved_rect)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(writer.get())); + ASSERT_TRUE(FPDFPage_GenerateContent(page.get())); + writer.reset(); + + ScopedFPDFAnnotation reopened( + EPDFPage_GetAnnotByObjectNumber(page.get(), object_number)); + ASSERT_TRUE(reopened); + + FS_RECTF observed_rect; + ASSERT_TRUE(FPDFAnnot_GetRect(reopened.get(), &observed_rect)); + EXPECT_FLOAT_EQ(moved_rect.left, observed_rect.left); + EXPECT_FLOAT_EQ(moved_rect.top, observed_rect.top); + EXPECT_FLOAT_EQ(moved_rect.right, observed_rect.right); + EXPECT_FLOAT_EQ(moved_rect.bottom, observed_rect.bottom); + EXPECT_EQ(0, FPDFPage_GetAnnotIndex(page.get(), reopened.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LinkCacheRebuildsAfterOverlayEpochChanges) { + FileAccessForTesting base_access("annots.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); + ASSERT_TRUE(layer); + ASSERT_EQ(EPDFLayerOpenStatus_kSuccess, status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + + FPDF_LINK initial = FPDFLink_GetLinkAtPoint(page.get(), 69.0, 653.0); + ASSERT_TRUE(initial); + CPDF_Dictionary* initial_dict = CPDFDictionaryFromFPDFLink(initial); + ASSERT_TRUE(initial_dict); + ASSERT_GT(initial_dict->GetObjNum(), 0u); + + CPDF_Document* layer_doc = CPDFDocumentFromFPDFDocument(layer.get()); + ASSERT_TRUE(layer_doc); + RetainPtr promoted = ToDictionary( + layer_doc->GetMutableIndirectObject(initial_dict->GetObjNum())); + ASSERT_TRUE(promoted); + promoted->SetRectFor("Rect", CFX_FloatRect(300.0f, 300.0f, 350.0f, 350.0f)); + + EXPECT_FALSE(FPDFLink_GetLinkAtPoint(page.get(), 69.0, 653.0)); + EXPECT_TRUE(FPDFLink_GetLinkAtPoint(page.get(), 325.0, 325.0)); + + layer.reset(); + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerWorkflowsProduceEmptyDelta) { + static constexpr const char* kFiles[] = { + "links_highlights_annots.pdf", + "multiple_form_types.pdf", + "embedded_images.pdf", + "annotation_stamp_with_ap.pdf", + }; + + for (const char* file_name : kFiles) { + CheckReadOnlyLayerWorkflowProducesEmptyDelta(file_name); + } +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerRenderCacheMatrixProducesEmptyDelta) { + static constexpr const char* kFiles[] = { + "form_object.pdf", + "form_object_with_text.pdf", + "form_object_with_image.pdf", + "form_object_with_path.pdf", + "shared_form_xobject_matrix.pdf", + "hello_world_2_pages_shared_resources_dict.pdf", + "jpx_lzw.pdf", + "rotated_image.pdf", + "simple_thumbnail.pdf", + "thumbnail_with_no_filters.pdf", + }; + + for (const char* file_name : kFiles) { + CheckReadOnlyLayerWorkflowProducesEmptyDelta(file_name); + } +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerAnnotMatrixProducesEmptyDelta) { + static constexpr const char* kFiles[] = { + "annots.pdf", + "annotation_markup_multiline_no_ap.pdf", + "annotation_highlight_rollover_ap.pdf", + "annotation_ink_multiple.pdf", + "polygon_annot.pdf", + "line_annot.pdf", + "redact_annot.pdf", + "annotation_fileattachment.pdf", + }; + + for (const char* file_name : kFiles) { + CheckReadOnlyLayerWorkflowProducesEmptyDelta(file_name); + } +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerCatalogMatrixProducesEmptyDelta) { + static constexpr const char* kFiles[] = { + "embedded_attachments.pdf", "named_dests.pdf", "page_labels.pdf", + "tagged_mcr_multipage.pdf", "two_signatures.pdf", + }; + + for (const char* file_name : kFiles) { + CheckReadOnlyLayerWorkflowProducesEmptyDelta(file_name); + } +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerFixtureParityProducesEmptyDelta) { + static constexpr const char* kFiles[] = { + "annots_action_handling.pdf", + "bug_679649.pdf", + "calculate.pdf", + "document_aactions.pdf", + "embedded_attachments.pdf", + "embedded_images.pdf", + "find_text_consecutive.pdf", + "font_weight.pdf", + "hello_world_2_pages_split_streams.pdf", + "links_highlights_annots.pdf", + "multiple_form_types.pdf", + "named_dests_old_style.pdf", + "page_labels.pdf", + "tagged_actual_text.pdf", + "tagged_mcr_multipage.pdf", + "text_font.pdf", + "use_outlines.pdf", + "zero_length_stream.pdf", + }; + + for (const char* file_name : kFiles) { + CheckReadOnlyLayerParityProducesEmptyDelta(file_name); + } +} + +TEST_F(FPDFViewEmbedderTest, + ReadOnlyLayerMalformedOldStyleNamedDestsProducesEmptyDelta) { + FileAccessForTesting plain_access("named_dests_old_style.pdf"); + ScopedFPDFDocument plain(FPDF_LoadCustomDocument(&plain_access, nullptr)); + ASSERT_TRUE(plain); + EXPECT_EQ(2, FPDF_GetPageCount(plain.get())); + ScopedFPDFPage plain_page_0(FPDF_LoadPage(plain.get(), 0)); + EXPECT_TRUE(plain_page_0); + ScopedFPDFPage plain_page_1(FPDF_LoadPage(plain.get(), 1)); + EXPECT_FALSE(plain_page_1); + + FileAccessForTesting base_access("named_dests_old_style.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + EXPECT_EQ(2, FPDF_GetPageCount(layer.get())); + + EXPECT_EQ(2u, FPDF_CountNamedDests(layer.get())); + EXPECT_FALSE(FPDF_GetNamedDestByName(layer.get(), nullptr)); + EXPECT_FALSE(FPDF_GetNamedDestByName(layer.get(), "")); + EXPECT_FALSE(FPDF_GetNamedDestByName(layer.get(), "NoSuchName")); + EXPECT_TRUE(FPDF_GetNamedDestByName(layer.get(), kFirstAlternate)); + EXPECT_TRUE(FPDF_GetNamedDestByName(layer.get(), kLastAlternate)); + + char buffer[512]; + long size = sizeof(buffer); + ASSERT_TRUE(FPDF_GetNamedDest(layer.get(), 0, buffer, &size)); + ASSERT_EQ(static_cast(sizeof(kFirstAlternate) * 2), size); + EXPECT_EQ(kFirstAlternate, + GetPlatformString(reinterpret_cast(buffer))); + + ScopedFPDFPage layer_page_0(FPDF_LoadPage(layer.get(), 0)); + EXPECT_TRUE(layer_page_0); + ScopedFPDFPage layer_page_1(FPDF_LoadPage(layer.get(), 1)); + EXPECT_FALSE(layer_page_1); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_TRUE(GetString().empty()); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerNameTreeApisProduceEmptyDelta) { + { + FileAccessForTesting base_access("embedded_attachments.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ASSERT_EQ(2, FPDFDoc_GetAttachmentCount(layer.get())); + EXPECT_TRUE(FPDFDoc_GetAttachment(layer.get(), 0)); + EXPECT_TRUE(FPDFDoc_GetAttachment(layer.get(), 1)); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_TRUE(GetString().empty()); + + EPDF_ReleaseBaseDocument(base); + } + + { + FileAccessForTesting base_access("bug_679649.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ASSERT_EQ(1, FPDFDoc_GetJavaScriptActionCount(layer.get())); + ScopedFPDFJavaScriptAction js(FPDFDoc_GetJavaScriptAction(layer.get(), 0)); + EXPECT_TRUE(js); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_TRUE(GetString().empty()); + + EPDF_ReleaseBaseDocument(base); + } +} + +TEST_F(FPDFViewEmbedderTest, ReadOnlySiblingLayersStayEmptyAfterPeerMutates) { + FileAccessForTesting base_access("embedded_images.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status_a = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_a( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_a)); + ASSERT_TRUE(layer_a); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_a); + + EPDFLayerOpenStatus status_b = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_b( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_b)); + ASSERT_TRUE(layer_b); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_b); + + EPDFLayerOpenStatus status_c = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_c( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_c)); + ASSERT_TRUE(layer_c); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_c); + + for (FPDF_DOCUMENT layer : {layer_a.get(), layer_b.get(), layer_c.get()}) { + ScopedFPDFPage page(FPDF_LoadPage(layer, 0)); + ASSERT_TRUE(page); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + ASSERT_TRUE(bitmap); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer)); + } + + { + ScopedFPDFPage page_b(FPDF_LoadPage(layer_b.get(), 0)); + ASSERT_TRUE(page_b); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page_b.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + EXPECT_GT(EPDFLayer_GetPromotedObjectCount(layer_b.get()), 0u); + } + + for (FPDF_DOCUMENT read_only_layer : {layer_a.get(), layer_c.get()}) { + ScopedFPDFPage page(FPDF_LoadPage(read_only_layer, 0)); + ASSERT_TRUE(page); + ScopedFPDFBitmap bitmap = RenderPage(page.get()); + ASSERT_TRUE(bitmap); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(read_only_layer)); + } + + ClearString(); + EPDFLayerSaveStatus save_status_a = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_a.get(), this, &save_status_a)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status_a); + EXPECT_TRUE(GetString().empty()); + + ClearString(); + EPDFLayerSaveStatus save_status_b = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_b.get(), this, &save_status_b)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status_b); + EXPECT_FALSE(GetString().empty()); + + ClearString(); + EPDFLayerSaveStatus save_status_c = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_c.get(), this, &save_status_c)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status_c); + EXPECT_TRUE(GetString().empty()); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerMetadataUpdatePromotesInfoAndSavesDelta) { + FileAccessForTesting base_access("bug_601362.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status_a = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_a( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_a)); + ASSERT_TRUE(layer_a); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_a); + + EPDFLayerOpenStatus status_b = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_b( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_b)); + ASSERT_TRUE(layer_b); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_b); + + unsigned short buf[128]; + ASSERT_EQ(30u, FPDF_GetMetaText(layer_a.get(), "Creator", buf, sizeof(buf))); + EXPECT_EQ(L"Microsoft Word", GetPlatformWString(buf)); + ASSERT_EQ(30u, FPDF_GetMetaText(layer_b.get(), "Creator", buf, sizeof(buf))); + EXPECT_EQ(L"Microsoft Word", GetPlatformWString(buf)); + + ScopedFPDFWideString layer_creator = GetFPDFWideString(L"Layer A Creator"); + ASSERT_TRUE(EPDF_SetMetaText(layer_a.get(), "Creator", layer_creator.get())); + EXPECT_EQ(1u, EPDFLayer_GetPromotedObjectCount(layer_a.get())); + + ASSERT_EQ(32u, FPDF_GetMetaText(layer_a.get(), "Creator", buf, sizeof(buf))); + EXPECT_EQ(L"Layer A Creator", GetPlatformWString(buf)); + ASSERT_EQ(30u, FPDF_GetMetaText(layer_b.get(), "Creator", buf, sizeof(buf))); + EXPECT_EQ(L"Microsoft Word", GetPlatformWString(buf)); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer_b.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_a.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string delta = GetString(); + EXPECT_FALSE(delta.empty()); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ASSERT_EQ(32u, FPDF_GetMetaText(replayed.get(), "Creator", buf, sizeof(buf))); + EXPECT_EQ(L"Layer A Creator", GetPlatformWString(buf)); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerMetadataUpdateCreatesInfoWhenBaseHasNone) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + unsigned short buf[128]; + EXPECT_EQ(0u, FPDF_GetMetaText(layer.get(), "Title", buf, sizeof(buf))); + + ScopedFPDFWideString title = GetFPDFWideString(L"Layer Title"); + ASSERT_TRUE(EPDF_SetMetaText(layer.get(), "Title", title.get())); + EXPECT_EQ(1u, EPDFLayer_GetPromotedObjectCount(layer.get())); + ASSERT_EQ(24u, FPDF_GetMetaText(layer.get(), "Title", buf, sizeof(buf))); + EXPECT_EQ(L"Layer Title", GetPlatformWString(buf)); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string delta = GetString(); + EXPECT_FALSE(delta.empty()); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ASSERT_EQ(24u, FPDF_GetMetaText(replayed.get(), "Title", buf, sizeof(buf))); + EXPECT_EQ(L"Layer Title", GetPlatformWString(buf)); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, + LayerDeleteBasePageSavesPromotedPageTreeWithoutNullReplacement) { + FileAccessForTesting base_access("rectangles_multi_pages.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status_a = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_a( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_a)); + ASSERT_TRUE(layer_a); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_a); + ASSERT_EQ(5, FPDF_GetPageCount(layer_a.get())); + + EPDFLayerOpenStatus status_b = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_b( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_b)); + ASSERT_TRUE(layer_b); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_b); + ASSERT_EQ(5, FPDF_GetPageCount(layer_b.get())); + + const unsigned int deleted_page_objnum = + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 1); + ASSERT_NE(0u, deleted_page_objnum); + + FPDFPage_Delete(layer_a.get(), 1); + EXPECT_EQ(4, FPDF_GetPageCount(layer_a.get())); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(layer_a.get(), deleted_page_objnum)); + + EXPECT_EQ(5, FPDF_GetPageCount(layer_b.get())); + EXPECT_EQ(deleted_page_objnum, + EPDFDoc_GetPageObjectNumberByIndex(layer_b.get(), 1)); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer_b.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_a.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string delta = GetString(); + EXPECT_FALSE(delta.empty()); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ASSERT_EQ(4, FPDF_GetPageCount(replayed.get())); + for (int i = 0; i < FPDF_GetPageCount(replayed.get()); ++i) { + EXPECT_NE(deleted_page_objnum, + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), i)); + } + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, + LayerFullSavePrunesDeletedBasePageButKeepsSharedObjects) { + FileAccessForTesting base_access( + "hello_world_2_pages_shared_resources_dict.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + ASSERT_EQ(2, FPDF_GetPageCount(layer.get())); + + CPDF_Document* cpdf_layer = CPDFDocumentFromFPDFDocument(layer.get()); + RetainPtr surviving_page = + cpdf_layer->GetPageDictionary(0); + RetainPtr deleted_page = + cpdf_layer->GetPageDictionary(1); + ASSERT_TRUE(surviving_page); + ASSERT_TRUE(deleted_page); + + const uint32_t surviving_page_objnum = surviving_page->GetObjNum(); + const uint32_t deleted_page_objnum = deleted_page->GetObjNum(); + const uint32_t shared_resources_objnum = + GetReferencedObjectNumber(surviving_page.Get(), "Resources"); + const uint32_t deleted_page_resources_objnum = + GetReferencedObjectNumber(deleted_page.Get(), "Resources"); + const uint32_t shared_contents_objnum = + GetReferencedObjectNumber(surviving_page.Get(), "Contents"); + const uint32_t deleted_page_contents_objnum = + GetReferencedObjectNumber(deleted_page.Get(), "Contents"); + + ASSERT_NE(0u, surviving_page_objnum); + ASSERT_NE(0u, deleted_page_objnum); + ASSERT_NE(surviving_page_objnum, deleted_page_objnum); + ASSERT_NE(0u, shared_resources_objnum); + ASSERT_EQ(shared_resources_objnum, deleted_page_resources_objnum); + ASSERT_NE(0u, shared_contents_objnum); + ASSERT_EQ(shared_contents_objnum, deleted_page_contents_objnum); + + FPDFPage_Delete(layer.get(), 1); + ASSERT_EQ(1, FPDF_GetPageCount(layer.get())); + + ClearString(); + ASSERT_TRUE(FPDF_SaveAsCopy(layer.get(), this, 0)); + std::string saved_pdf = GetString(); + EXPECT_FALSE(saved_pdf.empty()); + + EXPECT_TRUE(PdfBytesContainObject(saved_pdf, surviving_page_objnum)); + EXPECT_FALSE(PdfBytesContainObject(saved_pdf, deleted_page_objnum)); + EXPECT_TRUE(PdfBytesContainObject(saved_pdf, shared_resources_objnum)); + EXPECT_TRUE(PdfBytesContainObject(saved_pdf, shared_contents_objnum)); + + ScopedSavedDoc saved_doc = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_doc); + EXPECT_EQ(1, FPDF_GetPageCount(saved_doc.get())); + EXPECT_EQ(surviving_page_objnum, + EPDFDoc_GetPageObjectNumberByIndex(saved_doc.get(), 0)); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerMovePagesPromotesMovedPageAndReplays) { + FileAccessForTesting base_access("rectangles_multi_pages.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus status_a = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_a( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_a)); + ASSERT_TRUE(layer_a); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_a); + ASSERT_EQ(5, FPDF_GetPageCount(layer_a.get())); + + std::vector original_order; + for (int i = 0; i < FPDF_GetPageCount(layer_a.get()); ++i) { + original_order.push_back( + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), i)); + } + ASSERT_EQ(5u, original_order.size()); + + EPDFLayerOpenStatus status_b = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer_b( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status_b)); + ASSERT_TRUE(layer_b); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status_b); + + int page_to_move = 2; + ASSERT_TRUE(FPDF_MovePages(layer_a.get(), &page_to_move, 1, 1)); + EXPECT_EQ(5, FPDF_GetPageCount(layer_a.get())); + EXPECT_EQ(original_order[0], + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 0)); + EXPECT_EQ(original_order[2], + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 1)); + EXPECT_EQ(original_order[1], + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 2)); + EXPECT_EQ(original_order[3], + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 3)); + EXPECT_EQ(original_order[4], + EPDFDoc_GetPageObjectNumberByIndex(layer_a.get(), 4)); + EXPECT_TRUE(EPDFLayer_IsObjectPromoted(layer_a.get(), original_order[2])); + + for (int i = 0; i < FPDF_GetPageCount(layer_b.get()); ++i) { + EXPECT_EQ(original_order[i], + EPDFDoc_GetPageObjectNumberByIndex(layer_b.get(), i)); + } + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer_b.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer_a.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string delta = GetString(); + EXPECT_FALSE(delta.empty()); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ASSERT_EQ(5, FPDF_GetPageCount(replayed.get())); + EXPECT_EQ(original_order[0], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 0)); + EXPECT_EQ(original_order[2], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 1)); + EXPECT_EQ(original_order[1], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 2)); + EXPECT_EQ(original_order[3], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 3)); + EXPECT_EQ(original_order[4], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 4)); + + page_to_move = 2; + ASSERT_TRUE(FPDF_MovePages(replayed.get(), &page_to_move, 1, 1)); + EXPECT_EQ(original_order[0], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 0)); + EXPECT_EQ(original_order[1], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 1)); + EXPECT_EQ(original_order[2], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 2)); + EXPECT_EQ(original_order[3], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 3)); + EXPECT_EQ(original_order[4], + EPDFDoc_GetPageObjectNumberByIndex(replayed.get(), 4)); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, CreateAnnotOnFreshLayerPromotesOverlayOnly) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, 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())); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + EXPECT_EQ(2u, EPDFLayer_GetPromotedObjectCount(layer.get())); + } + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, SaveLayerDeltaMaterializesWithBaseBytes) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + std::string materialized; + { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + const std::string delta = GetString(); + EXPECT_FALSE(delta.empty()); + EXPECT_FALSE(delta.starts_with("%PDF-")); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = const_cast(&delta); + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ASSERT_TRUE(replayed); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(replayed_page.get())); + + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector base_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(base_bytes.empty()); + EXPECT_LT(delta.size(), base_bytes.size()); + materialized.assign(reinterpret_cast(base_bytes.data()), + base_bytes.size()); + materialized += delta; + } + + ScopedFPDFDocument reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + ASSERT_TRUE(reopened); + ScopedFPDFPage reopened_page(FPDF_LoadPage(reopened.get(), 0)); + ASSERT_TRUE(reopened_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(reopened_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerDeltaReplayWithLeadingBytesInBase) { + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector file_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(file_bytes.empty()); + + std::string base_bytes = "leading junk\n"; + base_bytes.append(reinterpret_cast(file_bytes.data()), + file_bytes.size()); + CountingStringFileAccess base_access(base_bytes); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(base_access.get(), nullptr); + ASSERT_TRUE(base); + + std::string delta; + { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + delta = GetString(); + + unsigned long artifact_size = 0; + void* artifact_buffer = EPDFLayer_SaveLayerArtifactToOwnedBuffer( + layer.get(), &artifact_size, &save_status); + ASSERT_TRUE(artifact_buffer); + std::string artifact(static_cast(artifact_buffer), + artifact_size); + EPDF_FreeBuffer(artifact_buffer); + ASSERT_GE(artifact.size(), 40u); + EXPECT_EQ(base_bytes.size(), ReadUint64LEForTest(artifact.data() + 16)); + EXPECT_LT(ReadUint64LEForTest(artifact.data() + 24), + ReadUint64LEForTest(artifact.data() + 16)); + } + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(replayed_page.get())); + + const std::string materialized = base_bytes + delta; + ScopedFPDFDocument stock_reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + ASSERT_TRUE(stock_reopened); + ScopedFPDFPage stock_page(FPDF_LoadPage(stock_reopened.get(), 0)); + ASSERT_TRUE(stock_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(stock_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerDeltaReplayWithTrailingBytesInBase) { + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector file_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(file_bytes.empty()); + + std::string base_bytes(reinterpret_cast(file_bytes.data()), + file_bytes.size()); + base_bytes += "\n% harmless trailing bytes\n"; + CountingStringFileAccess base_access(base_bytes); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(base_access.get(), nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + const std::string delta = GetString(); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = const_cast(&delta); + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(replayed_page.get())); + + const std::string materialized = base_bytes + delta; + ScopedFPDFDocument stock_reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + ASSERT_TRUE(stock_reopened); + ScopedFPDFPage stock_page(FPDF_LoadPage(stock_reopened.get(), 0)); + ASSERT_TRUE(stock_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(stock_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerArtifactSaveUsesCachedBaseIdentity) { + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector file_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(file_bytes.empty()); + std::string base_bytes(reinterpret_cast(file_bytes.data()), + file_bytes.size()); + + CountingStringFileAccess base_access(base_bytes); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(base_access.get(), nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + + base_access.ResetCounts(); + unsigned long artifact_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* artifact_buffer = EPDFLayer_SaveLayerArtifactToOwnedBuffer( + layer.get(), &artifact_size, &save_status); + ASSERT_TRUE(artifact_buffer); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + EXPECT_EQ(0u, base_access.read_bytes); + EPDF_FreeBuffer(artifact_buffer); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerDiagnosticsRejectPlainDocuments) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + EXPECT_FALSE(EPDFLayer_GetBaseDocument(document())); + EXPECT_FALSE(EPDFLayer_IsObjectPromoted(document(), 1)); + EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(document())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSuccess; + EXPECT_FALSE(EPDFLayer_SaveDelta(document(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSaveFailed, save_status); +} + +TEST_F(FPDFViewEmbedderTest, LayerOwnedBufferAndArtifactReplay) { + EPDF_FreeBuffer(nullptr); + + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(page.get())); + + unsigned long delta_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* delta_buffer = + EPDFLayer_SaveDeltaToOwnedBuffer(layer.get(), &delta_size, &save_status); + ASSERT_TRUE(delta_buffer); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string delta(static_cast(delta_buffer), delta_size); + EPDF_FreeBuffer(delta_buffer); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = δ + EPDFLayerOpenStatus delta_open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &delta_open_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, delta_open_status); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(replayed_page.get())); + + unsigned long artifact_size = 0; + save_status = EPDFLayerSaveStatus_kSaveFailed; + void* artifact_buffer = EPDFLayer_SaveLayerArtifactToOwnedBuffer( + layer.get(), &artifact_size, &save_status); + ASSERT_TRUE(artifact_buffer); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string artifact(static_cast(artifact_buffer), + artifact_size); + EPDF_FreeBuffer(artifact_buffer); + + ClearString(); + save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveLayerArtifact(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string streamed_artifact = GetString(); + ASSERT_FALSE(streamed_artifact.empty()); + + // Independent saves may produce different trailer /ID values. Layer + // artifact correctness is replay validity, not byte-for-byte equality + // between separately generated artifacts. + FPDF_FILEACCESS artifact_access = {}; + artifact_access.m_FileLen = artifact.size(); + artifact_access.m_GetBlock = GetBlockFromString; + artifact_access.m_Param = &artifact; + EPDFLayerOpenStatus artifact_open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument artifact_replayed(EPDFLayer_OpenLayerArtifact( + base, &artifact_access, nullptr, &artifact_open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, artifact_open_status); + ASSERT_TRUE(artifact_replayed); + ScopedFPDFPage artifact_page(FPDF_LoadPage(artifact_replayed.get(), 0)); + ASSERT_TRUE(artifact_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(artifact_page.get())); + + FPDF_FILEACCESS streamed_artifact_access = {}; + streamed_artifact_access.m_FileLen = streamed_artifact.size(); + streamed_artifact_access.m_GetBlock = GetBlockFromString; + streamed_artifact_access.m_Param = &streamed_artifact; + EPDFLayerOpenStatus streamed_artifact_open_status = + EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument streamed_artifact_replayed( + EPDFLayer_OpenLayerArtifact(base, &streamed_artifact_access, nullptr, + &streamed_artifact_open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, streamed_artifact_open_status); + ASSERT_TRUE(streamed_artifact_replayed); + ScopedFPDFPage streamed_artifact_page( + FPDF_LoadPage(streamed_artifact_replayed.get(), 0)); + ASSERT_TRUE(streamed_artifact_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(streamed_artifact_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerArtifactIncludesNewAnnotObjectBodies) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + std::vector annot_objnums; + for (int i = 0; i < 5; ++i) { + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + const uint32_t objnum = EPDFAnnot_GetObjectNumber(annot.get()); + ASSERT_GT(objnum, 0u); + annot_objnums.push_back(objnum); + } + EXPECT_EQ(5, FPDFPage_GetAnnotCount(page.get())); + + unsigned long artifact_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* artifact_buffer = EPDFLayer_SaveLayerArtifactToOwnedBuffer( + layer.get(), &artifact_size, &save_status); + ASSERT_TRUE(artifact_buffer); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string artifact(static_cast(artifact_buffer), + artifact_size); + EPDF_FreeBuffer(artifact_buffer); + ASSERT_FALSE(artifact.empty()); + + for (uint32_t objnum : annot_objnums) { + const std::string object_reference = std::to_string(objnum) + " 0 R"; + const std::string object_header = std::to_string(objnum) + " 0 obj"; + EXPECT_NE(std::string::npos, artifact.find(object_reference)) + << "Layer artifact should reference newly created annotation object " + << objnum << "."; + EXPECT_NE(std::string::npos, artifact.find(object_header)) + << "Layer artifact references annotation object " << objnum + << " but does not contain its object body."; + } + + EPDF_ReleaseBaseDocument(base); + + FileAccessForTesting replay_base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT replay_base = + EPDF_LoadBaseDocument(&replay_base_access, nullptr); + ASSERT_TRUE(replay_base); + + FPDF_FILEACCESS artifact_access = {}; + artifact_access.m_FileLen = artifact.size(); + artifact_access.m_GetBlock = GetBlockFromString; + artifact_access.m_Param = &artifact; + EPDFLayerOpenStatus artifact_open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument artifact_replayed(EPDFLayer_OpenLayerArtifact( + replay_base, &artifact_access, nullptr, &artifact_open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, artifact_open_status); + ASSERT_TRUE(artifact_replayed); + ScopedFPDFPage artifact_page(FPDF_LoadPage(artifact_replayed.get(), 0)); + ASSERT_TRUE(artifact_page); + ASSERT_EQ(5, FPDFPage_GetAnnotCount(artifact_page.get())); + for (int i = 0; i < 5; ++i) { + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(artifact_page.get(), i)); + ASSERT_TRUE(annot); + EXPECT_EQ(FPDF_ANNOT_TEXT, FPDFAnnot_GetSubtype(annot.get())); + } + + EPDF_ReleaseBaseDocument(replay_base); +} + +TEST_F(FPDFViewEmbedderTest, LayerReplaySoakSmoke) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector base_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(base_bytes.empty()); + + for (int expected_annots = 1; expected_annots <= 3; ++expected_annots) { + std::string materialized; + { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + for (int i = 0; i < expected_annots; ++i) { + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + } + EXPECT_EQ(expected_annots, FPDFPage_GetAnnotCount(page.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + materialized.assign(reinterpret_cast(base_bytes.data()), + base_bytes.size()); + materialized += GetString(); + } + + ScopedFPDFDocument reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + ASSERT_TRUE(reopened); + ScopedFPDFPage reopened_page(FPDF_LoadPage(reopened.get(), 0)); + ASSERT_TRUE(reopened_page); + EXPECT_EQ(expected_annots, FPDFPage_GetAnnotCount(reopened_page.get())); + } + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerDeltaReplaysMultipleAnnotRemoval) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + const std::string pdf_path = PathService::GetTestFilePath("rectangles.pdf"); + ASSERT_FALSE(pdf_path.empty()); + std::vector base_bytes = GetFileContents(pdf_path.c_str()); + ASSERT_FALSE(base_bytes.empty()); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + for (int i = 0; i < 3; ++i) { + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + } + ASSERT_EQ(3, FPDFPage_GetAnnotCount(page.get())); + + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 1)); + ASSERT_EQ(2, FPDFPage_GetAnnotCount(page.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + const std::string delta = GetString(); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = const_cast(&delta); + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(2, FPDFPage_GetAnnotCount(replayed_page.get())); + + std::string materialized(reinterpret_cast(base_bytes.data()), + base_bytes.size()); + materialized += delta; + ScopedFPDFDocument stock_reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + ASSERT_TRUE(stock_reopened); + ScopedFPDFPage stock_page(FPDF_LoadPage(stock_reopened.get(), 0)); + ASSERT_TRUE(stock_page); + EXPECT_EQ(2, FPDFPage_GetAnnotCount(stock_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + +TEST_F(FPDFViewEmbedderTest, LayerDeltaReplaysRemoveAllAnnots) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); + + ScopedFPDFPage page(FPDF_LoadPage(layer.get(), 0)); + ASSERT_TRUE(page); + for (int i = 0; i < 3; ++i) { + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ASSERT_TRUE(annot); + } + ASSERT_EQ(3, FPDFPage_GetAnnotCount(page.get())); + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 2)); + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 1)); + ASSERT_TRUE(FPDFPage_RemoveAnnot(page.get(), 0)); + ASSERT_EQ(0, FPDFPage_GetAnnotCount(page.get())); + + ClearString(); + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveDelta(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + const std::string delta = GetString(); + + FPDF_FILEACCESS delta_access = {}; + delta_access.m_FileLen = delta.size(); + delta_access.m_GetBlock = GetBlockFromString; + delta_access.m_Param = const_cast(&delta); + EPDFLayerOpenStatus replay_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument replayed( + EPDFLayer_OpenLayer(base, &delta_access, nullptr, &replay_status)); + ASSERT_TRUE(replayed); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, replay_status); + ScopedFPDFPage replayed_page(FPDF_LoadPage(replayed.get(), 0)); + ASSERT_TRUE(replayed_page); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(replayed_page.get())); + + EPDF_ReleaseBaseDocument(base); +} + TEST_F(FPDFViewEmbedderTest, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); @@ -1212,6 +3149,205 @@ TEST_F(FPDFViewEmbedderTest, FPDFGetPageSizeByIndexF) { EXPECT_EQ(1u, doc->GetParsedPageCountForTesting()); } +TEST_F(FPDFViewEmbedderTest, EPDFDocGetPageObjectNumberByIndex) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + + EXPECT_EQ(0u, EPDFDoc_GetPageObjectNumberByIndex(nullptr, 0)); + + // Page -1 doesn't exist. + EXPECT_EQ(0u, EPDFDoc_GetPageObjectNumberByIndex(document(), -1)); + + // Page 1 doesn't exist. + EXPECT_EQ(0u, EPDFDoc_GetPageObjectNumberByIndex(document(), 1)); + + const unsigned int objnum = EPDFDoc_GetPageObjectNumberByIndex(document(), 0); + EXPECT_NE(0u, objnum); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + EXPECT_EQ(0u, doc->GetParsedPageCountForTesting()); + + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + EXPECT_EQ(objnum, EPDFPage_GetObjectNumber(page.get())); + EXPECT_EQ(1u, doc->GetParsedPageCountForTesting()); +} + +TEST_F(FPDFViewEmbedderTest, EPDFDocSetPageRotationByObjectNumber) { + constexpr char kRotatedPng[] = "rectangles_rotated"; + + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + + const unsigned int objnum = EPDFDoc_GetPageObjectNumberByIndex(document(), 0); + ASSERT_NE(0u, objnum); + + EXPECT_FALSE(EPDFDoc_SetPageRotationByObjectNumber(nullptr, objnum, 1)); + EXPECT_FALSE(EPDFDoc_SetPageRotationByObjectNumber(document(), 0, 1)); + EXPECT_FALSE(EPDFDoc_SetPageRotationByObjectNumber(document(), objnum, -1)); + EXPECT_FALSE(EPDFDoc_SetPageRotationByObjectNumber(document(), objnum, 4)); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + EXPECT_EQ(0u, doc->GetParsedPageCountForTesting()); + + EXPECT_TRUE(EPDFDoc_SetPageRotationByObjectNumber(document(), objnum, 1)); + EXPECT_EQ(0u, doc->GetParsedPageCountForTesting()); + + { + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + EXPECT_EQ(1, FPDFPage_GetRotation(page.get())); + EXPECT_EQ(300, static_cast(FPDF_GetPageWidth(page.get()))); + EXPECT_EQ(200, static_cast(FPDF_GetPageHeight(page.get()))); + ScopedFPDFBitmap bitmap = RenderLoadedPage(page.get()); + CompareBitmapWithExpectationSuffix(bitmap.get(), kRotatedPng); + } + + ASSERT_TRUE(FPDF_SaveAsCopy(document(), this, 0)); + ScopedSavedDoc saved_document = OpenScopedSavedDocument(); + ASSERT_TRUE(saved_document); + ScopedSavedPage saved_page = LoadScopedSavedPage(0); + ASSERT_TRUE(saved_page); + EXPECT_EQ(1, FPDFPage_GetRotation(saved_page.get())); +} + +TEST_F(FPDFViewEmbedderTest, EPDFDocDeletePageByObjectNumber) { + ASSERT_TRUE(OpenDocument("rectangles_multi_pages.pdf")); + ASSERT_EQ(5, FPDF_GetPageCount(document())); + + const unsigned int original_page_1 = + EPDFDoc_GetPageObjectNumberByIndex(document(), 1); + ASSERT_NE(0u, original_page_1); + + int page_to_move = 1; + ASSERT_TRUE(FPDF_MovePages(document(), &page_to_move, 1, 4)); + ASSERT_EQ(5, FPDF_GetPageCount(document())); + EXPECT_EQ(original_page_1, EPDFDoc_GetPageObjectNumberByIndex(document(), 4)); + + EXPECT_FALSE(EPDFDoc_DeletePageByObjectNumber(nullptr, original_page_1)); + EXPECT_FALSE(EPDFDoc_DeletePageByObjectNumber(document(), 0)); + EXPECT_TRUE(EPDFDoc_DeletePageByObjectNumber(document(), original_page_1)); + EXPECT_EQ(4, FPDF_GetPageCount(document())); + + for (int i = 0; i < FPDF_GetPageCount(document()); ++i) { + EXPECT_NE(original_page_1, + EPDFDoc_GetPageObjectNumberByIndex(document(), i)); + } + + EXPECT_FALSE(EPDFDoc_DeletePageByObjectNumber(document(), original_page_1)); +} + +TEST_F(FPDFViewEmbedderTest, + EPDFDocDeletePageByObjectNumberDeletesDuplicatePageObjectOccurrence) { + // This malformed compatibility fixture references the same /Page object from + // multiple visible page positions. Delete-by-object-number removes the first + // visible occurrence resolved by PDFium. + ASSERT_TRUE(OpenDocument("bug_1229106.pdf")); + ASSERT_EQ(4, FPDF_GetPageCount(document())); + + const unsigned int duplicate_objnum = + EPDFDoc_GetPageObjectNumberByIndex(document(), 0); + ASSERT_NE(0u, duplicate_objnum); + ASSERT_EQ(duplicate_objnum, + EPDFDoc_GetPageObjectNumberByIndex(document(), 1)); + + EXPECT_TRUE(EPDFDoc_DeletePageByObjectNumber(document(), duplicate_objnum)); + EXPECT_EQ(3, FPDF_GetPageCount(document())); + EXPECT_EQ(duplicate_objnum, + EPDFDoc_GetPageObjectNumberByIndex(document(), 0)); + + EXPECT_TRUE(EPDFDoc_DeletePageByObjectNumber(document(), duplicate_objnum)); + EXPECT_EQ(2, FPDF_GetPageCount(document())); + + for (int i = 0; i < FPDF_GetPageCount(document()); ++i) { + EXPECT_NE(duplicate_objnum, + EPDFDoc_GetPageObjectNumberByIndex(document(), i)); + } +} + +TEST_F(FPDFViewEmbedderTest, EPDFGetPageBoxByIndex) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + + auto expect_rect = [](const FS_RECTF& rect, float left, float top, + float right, float bottom) { + EXPECT_FLOAT_EQ(left, rect.left); + EXPECT_FLOAT_EQ(top, rect.top); + EXPECT_FLOAT_EQ(right, rect.right); + EXPECT_FLOAT_EQ(bottom, rect.bottom); + }; + + FS_RECTF box = {-1.0f, -1.0f, -1.0f, -1.0f}; + EXPECT_FALSE(EPDF_GetPageBoxByIndex(nullptr, 0, EPDF_PAGE_BOX_MEDIA, &box)); + EXPECT_FALSE( + EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_MEDIA, nullptr)); + EXPECT_FALSE( + EPDF_GetPageBoxByIndex(document(), -1, EPDF_PAGE_BOX_MEDIA, &box)); + EXPECT_FALSE( + EPDF_GetPageBoxByIndex(document(), 1, EPDF_PAGE_BOX_MEDIA, &box)); + EXPECT_FALSE(EPDF_GetPageBoxByIndex( + document(), 0, static_cast(999), &box)); + + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_MEDIA, &box)); + expect_rect(box, 0.0f, 300.0f, 200.0f, 0.0f); + + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_CROP, &box)); + expect_rect(box, 0.0f, 300.0f, 200.0f, 0.0f); + + box = {-1.0f, -1.0f, -1.0f, -1.0f}; + EXPECT_FALSE( + EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_BLEED, &box)); + expect_rect(box, -1.0f, -1.0f, -1.0f, -1.0f); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + RetainPtr page_dict = doc->GetMutablePageDictionary(0); + ASSERT_TRUE(page_dict); + page_dict->SetRectFor(pdfium::page_object::kCropBox, + CFX_FloatRect(10.0f, 20.0f, 110.0f, 120.0f)); + page_dict->SetRectFor(pdfium::page_object::kBleedBox, + CFX_FloatRect(20.0f, 30.0f, 100.0f, 110.0f)); + page_dict->SetRectFor(pdfium::page_object::kTrimBox, + CFX_FloatRect(30.0f, 40.0f, 90.0f, 100.0f)); + page_dict->SetRectFor(pdfium::page_object::kArtBox, + CFX_FloatRect(40.0f, 50.0f, 80.0f, 90.0f)); + + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_CROP, &box)); + expect_rect(box, 10.0f, 120.0f, 110.0f, 20.0f); + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_BLEED, &box)); + expect_rect(box, 20.0f, 110.0f, 100.0f, 30.0f); + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_TRIM, &box)); + expect_rect(box, 30.0f, 100.0f, 90.0f, 40.0f); + ASSERT_TRUE(EPDF_GetPageBoxByIndex(document(), 0, EPDF_PAGE_BOX_ART, &box)); + expect_rect(box, 40.0f, 90.0f, 80.0f, 50.0f); + + EXPECT_EQ(0u, doc->GetParsedPageCountForTesting()); +} + +TEST_F(FPDFViewEmbedderTest, EPDFGetPageUserUnitByIndex) { + ASSERT_TRUE(OpenDocument("rectangles.pdf")); + + float user_unit = 0.0f; + EXPECT_FALSE(EPDF_GetPageUserUnitByIndex(nullptr, 0, &user_unit)); + EXPECT_FALSE(EPDF_GetPageUserUnitByIndex(document(), 0, nullptr)); + EXPECT_FALSE(EPDF_GetPageUserUnitByIndex(document(), -1, &user_unit)); + EXPECT_FALSE(EPDF_GetPageUserUnitByIndex(document(), 1, &user_unit)); + + ASSERT_TRUE(EPDF_GetPageUserUnitByIndex(document(), 0, &user_unit)); + EXPECT_FLOAT_EQ(1.0f, user_unit); + + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + RetainPtr page_dict = doc->GetMutablePageDictionary(0); + ASSERT_TRUE(page_dict); + page_dict->SetNewFor("UserUnit", 2.5f); + + ASSERT_TRUE(EPDF_GetPageUserUnitByIndex(document(), 0, &user_unit)); + EXPECT_FLOAT_EQ(2.5f, user_unit); + + page_dict->SetNewFor("UserUnit", -10.0f); + ASSERT_TRUE(EPDF_GetPageUserUnitByIndex(document(), 0, &user_unit)); + EXPECT_FLOAT_EQ(1.0f, user_unit); + + EXPECT_EQ(0u, doc->GetParsedPageCountForTesting()); +} + TEST_F(FPDFViewEmbedderTest, FPDFGetPageSizeByIndex) { ASSERT_TRUE(OpenDocument("rectangles.pdf")); diff --git a/fpdfsdk/fpdfsdk_pending_security.h b/fpdfsdk/fpdfsdk_pending_security.h deleted file mode 100644 index b8e85d1a98..0000000000 --- a/fpdfsdk/fpdfsdk_pending_security.h +++ /dev/null @@ -1,37 +0,0 @@ -// 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. - -#ifndef FPDFSDK_FPDFSDK_PENDING_SECURITY_H_ -#define FPDFSDK_FPDFSDK_PENDING_SECURITY_H_ - -#include -#include - -#include "core/fpdfapi/parser/cpdf_dictionary.h" -#include "core/fpdfapi/parser/cpdf_security_handler.h" -#include "core/fxcrt/retain_ptr.h" -#include "public/fpdfview.h" - -// Experimental EmbedPDF Extension: pending security state shared between -// fpdf_view.cpp (writers: EPDF_SetEncryption / EPDF_RemoveEncryption / -// EPDF_CleanupPendingSecurity) and fpdf_save.cpp (reader: DoDocSave). -// -// The storage is keyed by FPDF_DOCUMENT handle and protected by a mutex. -// Both the mutex and the map are intentionally leaked (heap-allocated behind -// function-local statics) to avoid the static destruction order fiasco -- the -// map holds RetainPtr<> handles to PDFium objects whose destructors must not -// race with process shutdown of other globals. - -enum class PendingSecurityMode { kNone, kEncrypt, kRemove }; - -struct PendingSecurity { - PendingSecurityMode mode = PendingSecurityMode::kNone; - RetainPtr encrypt_dict; // Only for kEncrypt - RetainPtr security_handler; // Only for kEncrypt -}; - -std::mutex& GetPendingSecurityMutex(); -std::unordered_map& GetPendingSecurityMap(); - -#endif // FPDFSDK_FPDFSDK_PENDING_SECURITY_H_ diff --git a/pdfium.gni b/pdfium.gni index 73ea9f46bd..6c681945de 100644 --- a/pdfium.gni +++ b/pdfium.gni @@ -91,6 +91,14 @@ declare_args() { # Don't build against bundled zlib. use_system_zlib = false + + # EmbedPDF: thread-confined runtime. When true, PDFium's process-global + # singletons become per-thread (thread_local via EPDF_TLS), so each worker + # thread owns its own PDFium state and N threads can render shared-nothing in + # one process. Default off keeps globals process-wide; enable only for native + # server targets that follow the per-thread init/shutdown contract. Never + # makes arbitrary PDFium usage thread-safe; handles must not cross threads. + embedpdf_thread_local_globals = false } assert(!pdf_is_complete_lib || !is_component_build, diff --git a/public/epdf_action.h b/public/epdf_action.h new file mode 100644 index 0000000000..726f8ea75d --- /dev/null +++ b/public/epdf_action.h @@ -0,0 +1,216 @@ +// 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. Type, subtype, script, chain, destination, +// URI, file path, and named-action payloads are copied at build time and stay +// valid after the document is closed or mutated. The getters that take an +// FPDF_DOCUMENT still require the originating document to be open: it +// validates destination page identity and is used to resolve a named +// destination when EPDFAction_LoadModel() had no document owner available. +// +// 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 new file mode 100644 index 0000000000..52b3ce97d6 --- /dev/null +++ b/public/epdf_redact.h @@ -0,0 +1,71 @@ +// 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_REDACT_H_ +#define PUBLIC_EPDF_REDACT_H_ + +#include + +// NOLINTNEXTLINE(build/include) +#include "fpdfview.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Experimental EmbedPDF Extension API. +// Apply a redact annotation, permanently removing content underneath. +// +// 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 +// 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, + uint32_t* out_removed_annot_count); + +// Experimental EmbedPDF Extension API. +// Apply all redact annotations on a page, permanently removing content +// 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 +// 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, uint32_t* out_removed_annot_count); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_REDACT_H_ diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h index db493ca3d0..76b0f73c8e 100644 --- a/public/fpdf_annot.h +++ b/public/fpdf_annot.h @@ -6,13 +6,18 @@ #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" + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -100,13 +105,13 @@ typedef enum FPDFANNOT_COLORTYPE { } FPDFANNOT_COLORTYPE; typedef enum FPDF_ANNOT_BORDER_STYLE { - FPDF_ANNOT_BS_UNKNOWN = 0, - FPDF_ANNOT_BS_SOLID, - FPDF_ANNOT_BS_DASHED, - FPDF_ANNOT_BS_BEVELED, - FPDF_ANNOT_BS_INSET, - FPDF_ANNOT_BS_UNDERLINE, - FPDF_ANNOT_BS_CLOUDY + FPDF_ANNOT_BS_UNKNOWN = 0, + FPDF_ANNOT_BS_SOLID, + FPDF_ANNOT_BS_DASHED, + FPDF_ANNOT_BS_BEVELED, + FPDF_ANNOT_BS_INSET, + FPDF_ANNOT_BS_UNDERLINE, + FPDF_ANNOT_BS_CLOUDY } FPDF_ANNOT_BORDER_STYLE; typedef enum FPDF_BLENDMODE { @@ -228,7 +233,7 @@ typedef enum FPDF_ANNOT_NAME { typedef enum EPDF_STAMP_FIT { EPDF_STAMP_FIT_CONTAIN = 0, // preserve aspect, fully visible - EPDF_STAMP_FIT_COVER = 1, // preserve aspect, fill box, might crop + EPDF_STAMP_FIT_COVER = 1, // preserve aspect, fill box, might crop EPDF_STAMP_FIT_STRETCH = 2 // ignore aspect, fill box } EPDF_STAMP_FIT; @@ -754,6 +759,97 @@ EPDFAnnot_SetNumberValue(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, float value); +// Experimental EmbedPDF Extension API. +// Returns true if |annot| has an /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_HasEmbedMetadata(FPDF_ANNOTATION annot); + +// Experimental EmbedPDF Extension API. +// Removes the /EMBD_Metadata dictionary from |annot|. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadata(FPDF_ANNOTATION annot); + +// Experimental EmbedPDF Extension API. +// Removes |key| from |annot|'s /EMBD_Metadata dictionary. If the metadata +// dictionary becomes empty, /EMBD_Metadata is removed too. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadataKey(FPDF_ANNOTATION annot, FPDF_BYTESTRING key); + +// Experimental EmbedPDF Extension API. +// Set a UTF-16LE string in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value); + +// Experimental EmbedPDF Extension API. +// Get a UTF-16LE string from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Set a number in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float value); + +// Experimental EmbedPDF Extension API. +// Get a number from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float* value); + +// Experimental EmbedPDF Extension API. +// Set a boolean in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL value); + +// Experimental EmbedPDF Extension API. +// Get a boolean from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL* value); + +// Experimental EmbedPDF Extension API. +// Set a rect in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + const FS_RECTF* rect); + +// Experimental EmbedPDF Extension API. +// Get a rect from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FS_RECTF* rect); + +// Experimental EmbedPDF Extension API. +// Set the arbitrary app-specific JSON bucket in /EMBD_Metadata /CustomJSON. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataJSON(FPDF_ANNOTATION annot, FPDF_WIDESTRING json); + +// Experimental EmbedPDF Extension API. +// Get the arbitrary app-specific JSON bucket from /EMBD_Metadata /CustomJSON. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataJSON(FPDF_ANNOTATION annot, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Removes /EMBD_Metadata from every annotation in |document|. This does not +// remove standard annotation fields or appearance streams. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDocument_ClearEmbedMetadata(FPDF_DOCUMENT document); + // Experimental API. // Set the AP (appearance string) in |annot|'s dictionary for a given // |appearanceMode|. @@ -834,8 +930,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetFlags(FPDF_ANNOTATION annot, // // Returns the annotation flags specific to interactive forms. FPDF_EXPORT int FPDF_CALLCONV -FPDFAnnot_GetFormFieldFlags(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot); +FPDFAnnot_GetFormFieldFlags(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot); // Experimental API. // Sets the form field flags for an interactive form annotation. @@ -1192,7 +1287,6 @@ FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot); FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name); - // Experimental EmbedPDF Extension API. // Get the color of an annotation. If no color is specified, default to yellow // for highlight annotation, black for all else. @@ -1202,12 +1296,11 @@ FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name); // R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B); // Experimental EmbedPDF Extension API. // Set the color of an annotation. @@ -1217,12 +1310,11 @@ EPDFAnnot_GetColor(FPDF_ANNOTATION annot, // R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B); // Experimental EmbedPDF Extension API. // Set the opacity of an annotaion. @@ -1243,8 +1335,7 @@ EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, // // Returns true if succesful. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, - unsigned int* alpha /* 0-255 */); +EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, unsigned int* alpha /* 0-255 */); // Experimental EmbedPDF Extension API. // Clear the color of an annotation. @@ -1253,7 +1344,8 @@ EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, // type - type of the color to be cleared. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); // Experimental EmbedPDF Extension API. // Get the border style and width of an annotation. This function handles both @@ -1273,7 +1365,10 @@ EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width); // width - the border width to be set. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetBorderStyle(FPDF_ANNOTATION annot, FPDF_ANNOT_BORDER_STYLE style, float width); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetBorderStyle(FPDF_ANNOTATION annot, + FPDF_ANNOT_BORDER_STYLE style, + float width); // Experimental EmbedPDF Extension API. // Get the intensity of a cloudy border effect. @@ -1333,10 +1428,10 @@ EPDFAnnot_GetRectangleDifferences(FPDF_ANNOTATION annot, // Returns true on success, false on failure. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRectangleDifferences(FPDF_ANNOTATION annot, - float left, - float bottom, - float right, - float top); + float left, + float bottom, + float right, + float top); // Experimental EmbedPDF Extension API. // Remove the rectangle differences (/RD) entry from a supported annotation. @@ -1349,7 +1444,9 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearRectangleDifferences(FPDF_ANNOTATION annot); // Experimental EmbedPDF Extension API. -// Get the number of entries in the dash pattern for a dashed border. +// Get the number of entries in the dash pattern for a dashed border. This +// function handles both the modern /BS dictionary and the legacy /Border +// array. // // annot - handle to an annotation. // @@ -1359,7 +1456,8 @@ FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetBorderDashPatternCount(FPDF_ANNOTATION annot); // Experimental EmbedPDF Extension API. -// Get the dash pattern for a dashed border. +// Get the dash pattern for a dashed border. This function handles both the +// modern /BS dictionary and the legacy /Border array. // // annot - handle to an annotation. // dash_array - a buffer to receive the dash pattern values. @@ -1381,10 +1479,10 @@ EPDFAnnot_GetBorderDashPattern(FPDF_ANNOTATION annot, // EPDFAnnot_GetBorderDashPatternCount(). // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, - const float* dash_array, - unsigned long count); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, + const float* dash_array, + unsigned long count); // Experimental EmbedPDF Extension API. // Generates or regenerates the appearance stream for a given annotation @@ -1423,20 +1521,20 @@ EPDFAnnot_GetBlendMode(FPDF_ANNOTATION annot); // ASCII byte string *without* the leading slash. If the caller includes a // leading '/', it is stripped. Returns false on invalid input or failure. // Succeeds regardless of subtype (permissive; /IT is just metadata). -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetIntent(FPDF_ANNOTATION annot, FPDF_BYTESTRING intent); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetIntent(FPDF_ANNOTATION annot, + FPDF_BYTESTRING intent); // Retrieves the Intent (/IT) name of an annotation as UTF-16 (without a // leading slash). Returns the number of 16-bit code units required (excluding // terminating NUL). If `buffer` is non-null and `buflen` large enough, copies -// the UTF-16 data and NUL-terminates it (same pattern as FPDFAnnot_GetStringValue). -// Returns 0 if annotation invalid, no /IT entry, or empty. +// the UTF-16 data and NUL-terminates it (same pattern as +// FPDFAnnot_GetStringValue). Returns 0 if annotation invalid, no /IT entry, or +// empty. FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetIntent(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen); - // Get the rich (formatted) text stored in the annotation’s /RC entry. // Returns the number of 16‑bit characters required (including the // terminating NUL). Call once with `buffer == nullptr` to get the size. @@ -1448,8 +1546,8 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, // Experimental EmbedPDF Extension API. // Set the line endings of a Line, Polyline, or FreeText annotation. // For Line/Polyline: writes /LE as a 2-element array [start_style, end_style]. -// For FreeText: writes /LE as a single name using end_style (Acrobat convention); -// start_style is ignored. +// For FreeText: writes /LE as a single name using end_style (Acrobat +// convention); start_style is ignored. // // annot - handle to an annotation. // start_style - the start line ending style (ignored for FreeText). @@ -1457,8 +1555,8 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, // FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, - FPDF_ANNOT_LINE_END start_style, - FPDF_ANNOT_LINE_END end_style); + FPDF_ANNOT_LINE_END start_style, + FPDF_ANNOT_LINE_END end_style); // Experimental EmbedPDF Extension API. // Get the line endings of a Line, Polyline, or FreeText annotation. @@ -1471,8 +1569,8 @@ EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, // FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, - FPDF_ANNOT_LINE_END* start_style, - FPDF_ANNOT_LINE_END* end_style); + FPDF_ANNOT_LINE_END* start_style, + FPDF_ANNOT_LINE_END* end_style); // Experimental EmbedPDF Extension API. // Replace every vertex in the /Vertices array with the |points| supplied. @@ -1483,10 +1581,10 @@ EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, // count - the number of vertices to be set. // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, - const FS_POINTF* points, - unsigned long count); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, + const FS_POINTF* points, + unsigned long count); // Experimental EmbedPDF Extension API. // Set (or create) the two end‑points of a **Line** annotation @@ -1497,12 +1595,11 @@ EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, // end - pointer to an `FS_POINTF` holding the new end‑point. // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetLine(FPDF_ANNOTATION annot, - const FS_POINTF* start, - const FS_POINTF* end); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLine(FPDF_ANNOTATION annot, + const FS_POINTF* start, + const FS_POINTF* end); -// Experimental EmbedPDF Extension API. +// Experimental EmbedPDF Extension API. // Set the default appearance of a FreeText annotation. // // annot - handle to an annotation. @@ -1519,6 +1616,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. // @@ -1543,8 +1658,8 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, // alignment - the text alignment to be set. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment); // Experimental EmbedPDF Extension API. @@ -1553,37 +1668,16 @@ EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, // annot - handle to an annotation. // // Returns the text alignment. -FPDF_EXPORT FPDF_TEXT_ALIGNMENT FPDF_CALLCONV +FPDF_EXPORT FPDF_TEXT_ALIGNMENT FPDF_CALLCONV EPDFAnnot_GetTextAlignment(FPDF_ANNOTATION annot); -// Experimental EmbedPDF Extension API. -// Set the vertical alignment of a FreeText annotation. -// -// annot - handle to an annotation. -// alignment - the vertical alignment to be set. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, - FPDF_VERTICAL_ALIGNMENT alignment); - -// Experimental EmbedPDF Extension API. -// Get the vertical alignment of a FreeText annotation. -// -// annot - handle to an annotation. -// -// Returns the vertical alignment. -FPDF_EXPORT FPDF_VERTICAL_ALIGNMENT FPDF_CALLCONV -EPDFAnnot_GetVerticalAlignment(FPDF_ANNOTATION annot); - - // Get the annotation by name. // // page - handle to a page. // nm - the name of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // Remove the annotation by name. @@ -1592,7 +1686,7 @@ EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // nm - the name of the annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // Set the linked annotation. @@ -1602,23 +1696,58 @@ EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // linked_annot - the linked annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, FPDF_ANNOTATION linked_annot); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_ANNOTATION linked_annot); // Experimental EmbedPDF Extension API. // Set the action of a Link annotation. // // annot - handle to a link annotation. -// action - handle to an action dictionary (e.g. from EPDFAction_CreateGoTo()). +// action - handle to an action dictionary (e.g. from +// EPDFAction_CreateGoTo()). // // Returns true on success. // // Notes: // * Only valid for FPDF_ANNOT_LINK annotations. // * The action must be an indirect object. -// * Any existing /A entry will be replaced. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action); +// * 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. @@ -1627,8 +1756,8 @@ EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action); // page_index - the index of the page. // // Returns the annotation count. -FPDF_EXPORT int FPDF_CALLCONV -EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, int page_index); +FPDF_EXPORT int FPDF_CALLCONV EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, + int page_index); // Experimental EmbedPDF Extension API. // Get the annotation by index. @@ -1638,7 +1767,7 @@ EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, int page_index); // index - the index of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // Experimental EmbedPDF Extension API. @@ -1649,8 +1778,9 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // index - the index of the annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, + int page_index, + int index); // Experimental EmbedPDF Extension API. // Set the /Name entry of an annotation (icon name for text/file/sound, @@ -1660,8 +1790,8 @@ EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // name - the name to be set. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetName(FPDF_ANNOTATION annot, FPDF_ANNOT_NAME name); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetName(FPDF_ANNOTATION annot, + FPDF_ANNOT_NAME name); // Experimental EmbedPDF Extension API. // Get the /Name entry of an annotation. @@ -1673,8 +1803,9 @@ FPDF_EXPORT FPDF_ANNOT_NAME FPDF_CALLCONV EPDFAnnot_GetName(FPDF_ANNOTATION annot); // Experimental EmbedPDF Extension API. -// Resize the normal appearance (/AP/N) of a Stamp to match the annotation's /Rect -// using the specified fit policy. Updates the AP /BBox and the image's CTM. +// Resize the normal appearance (/AP/N) of a Stamp to match the annotation's +// /Rect using the specified fit policy. Updates the AP /BBox and the image's +// CTM. // // annot - handle to a Stamp annotation. // fit - one of EPDF_STAMP_FIT_*. @@ -1684,24 +1815,26 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit); // Experimental EmbedPDF Extension API. -// Create an annotation. (the difference from FPDFPage_CreateAnnot is that it creates an indirect object) +// Create an annotation. (the difference from FPDFPage_CreateAnnot is that it +// creates an indirect object) // // page - handle to a page. // subtype - the subtype of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_CreateAnnot(FPDF_PAGE page, FPDF_ANNOTATION_SUBTYPE subtype); // Experimental EmbedPDF Extension API. // Set the rotation of an annotation in degrees. // // annot - handle to an annotation. -// rotation - the rotation in degrees (any value, e.g. 0, 45.5, 90, 180, etc.). +// rotation - the rotation in degrees (any value, e.g. 0, 45.5, 90, 180, +// etc.). // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, + float rotation); // Experimental EmbedPDF Extension API. // Get the rotation of an annotation in degrees. @@ -1710,8 +1843,8 @@ EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation); // rotation - receives the rotation value in degrees. // // Returns true on success, false if annot is invalid or rotation is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, float* rotation); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, + float* rotation); // Experimental EmbedPDF Extension API. // Get the reply type (RT) of an annotation. This specifies how an annotation @@ -1782,56 +1915,6 @@ EPDFAnnot_SetOverlayTextRepeat(FPDF_ANNOTATION annot, FPDF_BOOL repeat); FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot); -// 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. -// -// 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 -// -// 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. -// 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. -// -// The caller is responsible for: -// 1. Calling FPDFPage_GenerateContent to persist changes -// -// page - handle to a page -// -// Returns TRUE if any redactions were applied, FALSE otherwise. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactions(FPDF_PAGE page); - -// 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 @@ -1876,53 +1959,6 @@ FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, int annot_count); -// Experimental EmbedPDF Extension API. -// Set the EmbedPDF extended rotation on an annotation. This stores a custom -// /EPDFRotate entry (not the standard /Rotate) for non-widget annotations. -// A value of 0 removes the key to keep the PDF clean. -// -// annot - handle to an annotation. -// rotation - the rotation in degrees. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation); - -// Experimental EmbedPDF Extension API. -// Get the EmbedPDF extended rotation from an annotation. -// Reads the custom /EPDFRotate entry. -// -// annot - handle to an annotation. -// rotation - receives the rotation value in degrees. 0 if not set. -// -// Returns true on success, false if annot is invalid or rotation is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetExtendedRotation(FPDF_ANNOTATION annot, float* rotation); - -// Experimental EmbedPDF Extension API. -// Set the EmbedPDF unrotated rect on an annotation. This stores a custom -// /EPDFUnrotatedRect array representing the annotation's rect before rotation -// was applied. Follows the same FS_RECTF convention as FPDFAnnot_SetRect. -// -// annot - handle to an annotation. -// rect - pointer to the unrotated rect. Pass NULL to remove. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetUnrotatedRect(FPDF_ANNOTATION annot, const FS_RECTF* rect); - -// Experimental EmbedPDF Extension API. -// Get the EmbedPDF unrotated rect from an annotation. -// Reads the custom /EPDFUnrotatedRect array. -// Follows the same FS_RECTF convention as FPDFAnnot_GetRect. -// -// annot - handle to an annotation. -// rect - receives the unrotated rect. All zeros if not set. -// -// Returns true on success, false if annot is invalid or rect is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect); - // Experimental EmbedPDF Extension API. // Get the annotation rectangle with normalization applied. // Wraps FPDFAnnot_GetRect and ensures the returned rect is normalized @@ -1932,8 +1968,8 @@ EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect); // rect - receives the normalized rectangle; must not be NULL. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRect(FPDF_ANNOTATION annot, FS_RECTF* rect); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRect(FPDF_ANNOTATION annot, + FS_RECTF* rect); // Experimental EmbedPDF Extension API. // Set the Matrix on an annotation's appearance stream for the given mode. @@ -2016,12 +2052,11 @@ typedef enum { // R,G,B - color components in 0..255. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B); // Experimental EmbedPDF Extension API. // Get a color from the widget annotation's /MK dictionary. @@ -2031,12 +2066,11 @@ EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, // R,G,B - pointers to receive color components in 0..255. // // Returns true on success, false if no MK color is set or annot is invalid. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B); // Experimental EmbedPDF Extension API. // Remove a color from the widget annotation's /MK dictionary. @@ -2048,101 +2082,6 @@ 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. @@ -2158,42 +2097,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. // @@ -2264,8 +2167,8 @@ EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num); // index - the index of the annotation in the /Annots array. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnot(FPDF_PAGE page, int index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnot(FPDF_PAGE page, + int index); // Experimental EmbedPDF Extension API. // Remove an annotation on a page by its PDF indirect object number. @@ -2300,11 +2203,10 @@ EPDFPage_RemoveAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num); // (objectNumber, /NM) is preserved across the move. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_MoveAnnots(FPDF_PAGE page, - const int* from_indices, - int from_indices_len, - int to_index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_MoveAnnots(FPDF_PAGE page, + const int* from_indices, + int from_indices_len, + int to_index); #ifdef __cplusplus } // extern "C" 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_ext.h b/public/fpdf_ext.h index 068a977c12..877788bfbe 100644 --- a/public/fpdf_ext.h +++ b/public/fpdf_ext.h @@ -84,7 +84,10 @@ FPDF_EXPORT void FPDF_CALLCONV FSDK_SetTimeFunction(time_t (*func)()); // behave poorly in production environments. // // func - Function pointer to alternate implementation of localtime(), or -// NULL to restore to actual localtime() call itself. +// NULL to restore the thread-safe default localtime wrapper (which +// fills a thread-local tm via localtime_r/localtime_s). EmbedPDF: the +// default is no longer plain localtime(), which returned shared static +// storage and raced across threads. FPDF_EXPORT void FPDF_CALLCONV FSDK_SetLocaltimeFunction(struct tm* (*func)(const time_t*)); 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/public/fpdf_save.h b/public/fpdf_save.h index 093b889ad1..ea84444703 100644 --- a/public/fpdf_save.h +++ b/public/fpdf_save.h @@ -86,6 +86,92 @@ FPDF_SaveWithVersion(FPDF_DOCUMENT document, FPDF_DWORD flags, int file_version); +// Function: EPDF_FreeBuffer +// Releases a buffer returned by an EPDF_*ToOwnedBuffer() API. +// Parameters: +// buffer - Buffer returned by an EPDF owned-buffer API, or +// NULL. +FPDF_EXPORT void FPDF_CALLCONV EPDF_FreeBuffer(void* buffer); + +// Function: EPDF_SaveDocumentToOwnedBuffer +// Saves the copy of specified document into an owned memory buffer. +// The caller must release the returned buffer with EPDF_FreeBuffer(). +// Parameters: +// document - Handle to document. +// flags - Flags above that affect how the PDF gets saved. +// out_size - Receives the size of the returned buffer. +// Return value: +// Owned buffer if successful, NULL if failed. +FPDF_EXPORT void* FPDF_CALLCONV +EPDF_SaveDocumentToOwnedBuffer(FPDF_DOCUMENT document, + FPDF_DWORD flags, + unsigned long* out_size); + +// Function: EPDF_SaveDocumentToOwnedBufferWithVersion +// Same as EPDF_SaveDocumentToOwnedBuffer(), except the file version of +// the saved document can be specified by the caller. +FPDF_EXPORT void* FPDF_CALLCONV +EPDF_SaveDocumentToOwnedBufferWithVersion(FPDF_DOCUMENT document, + FPDF_DWORD flags, + unsigned long* out_size, + int file_version); + +// Runtime-side status for saving a layer delta. +typedef enum { + EPDFLayerSaveStatus_kSuccess = 0, + EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge = 1, + EPDFLayerSaveStatus_kSaveFailed = 2, +} EPDFLayerSaveStatus; + +// Function: EPDFLayer_SaveDelta +// Saves only a layer document's current overlay delta. The caller can +// materialize the layer as base bytes followed by the written delta. +// Parameters: +// layer - A layer document returned by EPDFLayer_OpenLayer(). +// file_write - A pointer to a custom file write structure. +// out_status - Optional detailed save status. +// Return value: +// TRUE if succeed, FALSE if failed. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveDelta(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status); + +// Function: EPDFLayer_SaveLayerArtifact +// Saves a server-facing layer artifact containing base identity +// metadata and the raw layer delta. Unlike the owned-buffer variant, +// this writes to the caller-supplied FPDF_FILEWRITE and is suitable +// for native/server paths that should avoid materializing the whole +// artifact in memory. +// Parameters: +// layer - A layer document returned by EPDFLayer_OpenLayer(). +// file_write - A pointer to a custom file write structure. +// out_status - Optional detailed save status. +// Return value: +// TRUE if succeed, FALSE if failed. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveLayerArtifact(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status); + +// Function: EPDFLayer_SaveDeltaToOwnedBuffer +// Saves only a layer document's current overlay delta into an owned +// memory buffer. The caller must release the returned buffer with +// EPDF_FreeBuffer(). +FPDF_EXPORT void* FPDF_CALLCONV +EPDFLayer_SaveDeltaToOwnedBuffer(FPDF_DOCUMENT layer, + unsigned long* out_size, + EPDFLayerSaveStatus* out_status); + +// Function: EPDFLayer_SaveLayerArtifactToOwnedBuffer +// Saves a server-facing layer artifact containing base identity +// metadata and the raw layer delta. The caller must release the +// returned buffer with EPDF_FreeBuffer(). +FPDF_EXPORT void* FPDF_CALLCONV +EPDFLayer_SaveLayerArtifactToOwnedBuffer(FPDF_DOCUMENT layer, + unsigned long* out_size, + EPDFLayerSaveStatus* out_status); + #ifdef __cplusplus } #endif diff --git a/public/fpdfview.h b/public/fpdfview.h index 2af9a0ac41..fb1816dff3 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -67,6 +67,7 @@ typedef struct fpdf_bitmap_t__* FPDF_BITMAP; typedef struct fpdf_bookmark_t__* FPDF_BOOKMARK; typedef struct fpdf_clippath_t__* FPDF_CLIPPATH; typedef struct fpdf_dest_t__* FPDF_DEST; +typedef struct fpdf_base_document_t__* EPDF_BASE_DOCUMENT; typedef struct fpdf_document_t__* FPDF_DOCUMENT; typedef struct fpdf_font_t__* FPDF_FONT; typedef struct fpdf_form_handle_t__* FPDF_FORMHANDLE; @@ -319,6 +320,24 @@ FPDF_EXPORT void FPDF_CALLCONV FPDF_InitLibrary(); // closing the library with this function. FPDF_EXPORT void FPDF_CALLCONV FPDF_DestroyLibrary(); +// Experimental EmbedPDF Extension API. +// Function: EPDF_InitThread / EPDF_ShutdownThread +// Thread-confined runtime lifecycle. Initialize / tear down PDFium for +// the CALLING thread. +// Comments: +// When the runtime is built with per-thread globals +// (embedpdf_thread_local_globals), each worker thread owns its own +// PDFium state, so every thread that uses PDFium MUST call +// EPDF_InitThread before creating any handle and EPDF_ShutdownThread +// after closing every handle it created. Handles must never cross +// threads. When per-thread globals are disabled, these are exact +// aliases of FPDF_InitLibrary / FPDF_DestroyLibrary and are safe to +// call once on a single thread. EPDF_ShutdownThread is lifecycle- +// strict: only legal after all handles on the calling thread are +// closed. +FPDF_EXPORT void FPDF_CALLCONV EPDF_InitThread(); +FPDF_EXPORT void FPDF_CALLCONV EPDF_ShutdownThread(); + // Policy for accessing the local machine time. #define FPDF_POLICY_MACHINETIME_ACCESS 0 @@ -562,6 +581,120 @@ typedef struct FPDF_FILEHANDLER_ { FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV FPDF_LoadCustomDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password); +// Function: EPDF_LoadBaseDocument +// Load and freeze a shareable base PDF document from a custom access +// descriptor. The returned handle is distinct from FPDF_DOCUMENT and +// cannot be used with ordinary document APIs such as FPDF_LoadPage(). +// Parameters: +// pFileAccess - A structure for accessing the file. +// password - Optional password for decrypting the PDF file. +// Return value: +// A handle to the loaded base document, or NULL on failure. +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password); + +// Function: EPDF_LoadMemBaseDocument +// Load and freeze a shareable base PDF document from memory. The +// returned handle is distinct from FPDF_DOCUMENT and cannot be used +// with ordinary document APIs such as FPDF_LoadPage(). +// Parameters: +// data_buf - Pointer to a buffer containing the PDF document. +// size - Number of bytes in the PDF document. +// password - Optional password for decrypting the PDF file. +// Return value: +// A handle to the loaded base document, or NULL on failure. +// Comments: +// The memory buffer must remain valid until the returned base document +// is released with EPDF_ReleaseBaseDocument(). +// +// See the comments for FPDF_LoadDocument() regarding the encoding for +// |password|. +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadMemBaseDocument(const void* data_buf, + int size, + FPDF_BYTESTRING password); + +// Function: EPDF_LoadMemBaseDocument64 +// Load and freeze a shareable base PDF document from memory. The +// returned handle is distinct from FPDF_DOCUMENT and cannot be used +// with ordinary document APIs such as FPDF_LoadPage(). +// Parameters: +// data_buf - Pointer to a buffer containing the PDF document. +// size - Number of bytes in the PDF document. +// password - Optional password for decrypting the PDF file. +// Return value: +// A handle to the loaded base document, or NULL on failure. +// Comments: +// The memory buffer must remain valid until the returned base document +// is released with EPDF_ReleaseBaseDocument(). +// +// See the comments for FPDF_LoadDocument() regarding the encoding for +// |password|. +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadMemBaseDocument64(const void* data_buf, + size_t size, + FPDF_BYTESTRING password); + +// Function: EPDF_ReleaseBaseDocument +// Release a base document returned by EPDF_LoadBaseDocument() or +// EPDF_LoadMemBaseDocument()/EPDF_LoadMemBaseDocument64(). +FPDF_EXPORT void FPDF_CALLCONV +EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base); + +// Runtime-side status for opening a layer on top of a base document. +typedef enum { + EPDFLayerOpenStatus_kSuccess = 0, + EPDFLayerOpenStatus_kPasswordRequired = 1, + EPDFLayerOpenStatus_kMalformedDelta = 2, + EPDFLayerOpenStatus_kBaseLayerMismatch = 3, + EPDFLayerOpenStatus_kOpenFailed = 4, +} EPDFLayerOpenStatus; + +// Function: EPDFLayer_OpenLayer +// Open a layer view on top of a previously loaded base document. +// The returned handle is an ordinary FPDF_DOCUMENT and must be closed +// with FPDF_CloseDocument(). +// Parameters: +// base - A base document returned by EPDF_LoadBaseDocument(). +// pFileAccess - Optional raw layer delta bytes as returned by +// EPDFLayer_SaveDelta(). NULL or zero-length opens a +// fresh empty layer. +// password - Reserved for future delta-password handling. +// out_status - Optional detailed open status. +// Return value: +// A layer document handle, or NULL on failure. +FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV +EPDFLayer_OpenLayer(EPDF_BASE_DOCUMENT base, + FPDF_FILEACCESS* pFileAccess, + FPDF_BYTESTRING password, + EPDFLayerOpenStatus* out_status); + +// Function: EPDFLayer_OpenLayerArtifact +// Open a layer view from a layer artifact produced by +// EPDFLayer_SaveLayerArtifactToOwnedBuffer(). Validates that the +// artifact belongs to |base| before ingesting its raw delta. +FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV +EPDFLayer_OpenLayerArtifact(EPDF_BASE_DOCUMENT base, + FPDF_FILEACCESS* pFileAccess, + FPDF_BYTESTRING password, + EPDFLayerOpenStatus* out_status); + +// Function: EPDFLayer_IsObjectPromoted +// Return whether the object exists in the layer overlay. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_IsObjectPromoted(FPDF_DOCUMENT layer, unsigned long obj_num); + +// Function: EPDFLayer_GetPromotedObjectCount +// Return the number of objects currently stored in the layer overlay. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFLayer_GetPromotedObjectCount(FPDF_DOCUMENT layer); + +// Function: EPDFLayer_GetBaseDocument +// Return the layer's borrowed base document handle. The caller MUST +// NOT release the returned handle. +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDFLayer_GetBaseDocument(FPDF_DOCUMENT layer); + // Function: FPDF_GetFileVersion // Get the file version of the given PDF document. // Parameters: @@ -712,6 +845,42 @@ EPDF_SetEncryption(FPDF_DOCUMENT document, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_RemoveEncryption(FPDF_DOCUMENT document); +#define EPDF_PASSWORD_PERMISSION_INVALID 0 +#define EPDF_PASSWORD_PERMISSION_NONE 1 +#define EPDF_PASSWORD_PERMISSION_USER 2 +#define EPDF_PASSWORD_PERMISSION_OWNER 3 + +// Experimental EmbedPDF Extension API. +// Checks what permissions the given password proves without changing the +// document's current owner-unlocked state. +// +// document - handle to a document +// password - raw password to check +// out_kind - receives EPDF_PASSWORD_PERMISSION_* +// out_user_permissions - receives the document's user permission bits +// out_effective_permissions - receives permissions implied by |password| +// out_security_handler_revision - receives the security handler revision +// +// Returns TRUE if: +// - document is not encrypted (kind NONE, full permissions, revision -1) +// - password is valid as user or owner +// +// Returns FALSE if: +// - document is NULL +// - any out pointer is NULL +// - document has no parser +// - password is invalid for an encrypted document +// +// This is a password probe, not a runtime permission override. It must not be +// used as evidence that the current document handle is owner-unlocked. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_CheckPasswordPermissions(FPDF_DOCUMENT document, + FPDF_BYTESTRING password, + int* out_kind, + unsigned int* out_user_permissions, + unsigned int* out_effective_permissions, + int* out_security_handler_revision); + // Experimental EmbedPDF Extension API. // Attempts to unlock owner permissions on an already-opened encrypted document. // @@ -730,6 +899,20 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_UnlockOwnerPermissions(FPDF_DOCUMENT document, FPDF_BYTESTRING owner_password); +// Experimental EmbedPDF Extension API. +// Runtime policy override for EmbedPDF-managed sessions. +// +// document - handle to a document +// enabled - TRUE to make PDFium internals behave as owner-unlocked, FALSE to +// restore the normal user-permission state for this handle +// +// This does not prove an owner password and does not authorize the caller. +// Callers must enforce JWT/PDF permission policy outside PDFium. This exists so +// high-level PDFium subsystems that consult permissions (notably forms) do not +// silently block operations after app-level authorization has already happened. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_SetRuntimeOwnerPermissions(FPDF_DOCUMENT document, FPDF_BOOL enabled); + // Experimental EmbedPDF Extension API. // Checks if a document is encrypted. // @@ -859,6 +1042,15 @@ FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, int page_index, FS_SIZEF* size); +// Page box type used by EPDF_GetPageBoxByIndex(). +typedef enum EPDF_PAGE_BOX_TYPE { + EPDF_PAGE_BOX_MEDIA = 0, + EPDF_PAGE_BOX_CROP = 1, + EPDF_PAGE_BOX_BLEED = 2, + EPDF_PAGE_BOX_TRIM = 3, + EPDF_PAGE_BOX_ART = 4, +} EPDF_PAGE_BOX_TYPE; + // Experimental EmbedPDF API. // Function: EPDF_GetPageRotationByIndex // Get the rotation of the page at the given index without parsing @@ -867,7 +1059,7 @@ FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, // document - Handle to document. Returned by FPDF_LoadDocument(). // page_index - Page index, zero for the first page. // Return value: -// The rotation in degrees (must be one of 0, 90, 180, 270). +// The rotation as quarter-turns (must be one of 0, 1, 2, 3). // Returns -1 on error (document or page not found). FPDF_EXPORT int FPDF_CALLCONV EPDF_GetPageRotationByIndex(FPDF_DOCUMENT document, int page_index); @@ -888,6 +1080,43 @@ EPDF_GetPageSizeByIndexNormalized(FPDF_DOCUMENT document, int page_index, FS_SIZEF* size); +// Experimental EmbedPDF API. +// Function: EPDF_GetPageBoxByIndex +// Get a page box at the given index without loading or parsing the page. +// Parameters: +// document - Handle to document. Returned by FPDF_LoadDocument(). +// page_index - Page index, zero for the first page. +// box_type - The page box to query. +// box - Pointer to a FS_RECTF to receive the box (in points). +// Return value: +// Non-zero for success. 0 for error or absent optional box. +// Comments: +// MediaBox is resolved through page-tree inheritance and falls back to +// PDFium's default page size when absent. CropBox falls back to +// MediaBox. BleedBox, TrimBox, and ArtBox return false when absent. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_GetPageBoxByIndex(FPDF_DOCUMENT document, + int page_index, + EPDF_PAGE_BOX_TYPE box_type, + FS_RECTF* box); + +// Experimental EmbedPDF API. +// Function: EPDF_GetPageUserUnitByIndex +// Get /UserUnit at the given page index without loading or parsing the +// page. +// Parameters: +// document - Handle to document. Returned by FPDF_LoadDocument(). +// page_index - Page index, zero for the first page. +// user_unit - Pointer to a float to receive the user unit. +// Return value: +// Non-zero for success. 0 for error. +// Comments: +// Missing or invalid /UserUnit values return the PDF default 1.0. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_GetPageUserUnitByIndex(FPDF_DOCUMENT document, + int page_index, + float* user_unit); + // Experimental EmbedPDF API. // Function: EPDF_LoadPageNormalized // Load a page with rotation normalized to 0 degrees. @@ -1585,9 +1814,9 @@ EPDF_RenderAnnotBitmap(FPDF_BITMAP bitmap, // Experimental EmbedPDF Extension API. // Renders the annotation's AP form content WITHOUT the AP stream's rotation -// Matrix applied, using /EPDFUnrotatedRect (falling back to /Rect) for the -// MatchRect mapping. This produces an unrotated bitmap suitable for UI layers -// that apply CSS rotation separately. +// Matrix applied, using /EMBD_Metadata /UnrotatedRect (falling back to /Rect) +// for the MatchRect mapping. This produces an unrotated bitmap suitable for UI +// layers that apply CSS rotation separately. // // Same parameters as EPDF_RenderAnnotBitmap. // @@ -1661,6 +1890,74 @@ FPDF_EXPORT FPDF_RESULT FPDF_CALLCONV FPDF_BStr_Clear(FPDF_BSTR* bstr); FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV EPDFDoc_LoadPageByObjectNumber(FPDF_DOCUMENT document, unsigned int obj_num); +// Experimental EmbedPDF Extension API. +// Load a page by its PDF indirect object number with rotation normalized to +// 0 degrees. Like EPDFDoc_LoadPageByObjectNumber(), but forces the page's +// rotation override to 0 so all subsequent operations (GetPageWidth, +// annotations, text, rendering) use normalized coordinates as if the page had +// no rotation. The intrinsic rotation is surfaced separately via +// EPDF_GetPageRotationByIndex(). +// +// document - handle to the document. +// obj_num - the indirect object number of the page dictionary. +// +// Returns a handle to the loaded page, or NULL if the object number +// does not correspond to a page. The caller must close the returned +// handle with FPDF_ClosePage(). +FPDF_EXPORT FPDF_PAGE FPDF_CALLCONV +EPDFDoc_LoadPageByObjectNumberNormalized(FPDF_DOCUMENT document, + unsigned int obj_num); + +// Experimental EmbedPDF Extension API. +// Get the PDF indirect object number of a page's dictionary by page index. +// Unlike FPDF_LoadPage(), this does not construct a page object or parse page +// content. +// +// document - handle to the document. +// page_index - zero-based page index. +// +// Returns the page dictionary object number (> 0) on success, or 0 if the +// document/index is invalid, the page dictionary is a direct object, or the +// page is an XFA page. +FPDF_EXPORT unsigned int FPDF_CALLCONV +EPDFDoc_GetPageObjectNumberByIndex(FPDF_DOCUMENT document, int page_index); + +// Experimental EmbedPDF Extension API. +// Delete the first visible page whose page dictionary has |obj_num| as its PDF +// indirect object number. Unlike FPDFPage_Delete(), this remains stable across +// page moves because it is keyed by page identity rather than page position. +// +// document - handle to the document. +// obj_num - the indirect object number of the page dictionary. +// +// Returns TRUE if a matching page was found and deleted, or FALSE if |document| +// is invalid, |obj_num| is 0, or |obj_num| does not correspond to a visible +// page. If a malformed PDF references the same /Page object more than once, +// the first visible occurrence resolved by PDFium is deleted. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_DeletePageByObjectNumber(FPDF_DOCUMENT document, unsigned int obj_num); + +// Experimental EmbedPDF Extension API. +// Set the rotation of the page whose page dictionary has |obj_num| as its PDF +// indirect object number. Unlike FPDFPage_SetRotation(), this does not require +// loading or parsing the page. +// +// document - handle to the document. +// obj_num - the indirect object number of the page dictionary. +// rotate - the rotation value, one of: +// 0 - No rotation. +// 1 - Rotated 90 degrees clockwise. +// 2 - Rotated 180 degrees clockwise. +// 3 - Rotated 270 degrees clockwise. +// +// Returns TRUE if a matching page was found and updated, or FALSE if |document| +// is invalid, |obj_num| is 0, |rotate| is outside 0..3, or |obj_num| does not +// correspond to a visible page. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDoc_SetPageRotationByObjectNumber(FPDF_DOCUMENT document, + unsigned int obj_num, + int rotate); + // Experimental EmbedPDF Extension API. // Get the PDF indirect object number of a page's dictionary. // diff --git a/scripts/embedpdf-runtime/build-target.sh b/scripts/embedpdf-runtime/build-target.sh index 0326635369..84b4662113 100755 --- a/scripts/embedpdf-runtime/build-target.sh +++ b/scripts/embedpdf-runtime/build-target.sh @@ -4,6 +4,10 @@ set -euo pipefail SOURCE_DIR="${PDF_RUNTIME_SOURCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" TARGET="${1:-}" PDF_IS_COMPLETE_LIB=true +# EmbedPDF: thread-confined runtime. Off by default; enabled per-target below +# for the native server builds. wasm isolates globals per-instance, so it stays +# off there. +EMBEDPDF_TLS_GLOBALS=false if [[ -z "$TARGET" ]]; then echo "usage: $0 " >&2 @@ -63,6 +67,13 @@ case "$TARGET" in ;; esac +# EmbedPDF: enable per-thread PDFium globals for the native server targets so +# the server worker pool can render in parallel in-process. wasm stays off +# (each instance already isolates globals via its own linear memory). +if [[ "$TARGET" != "wasm32" ]]; then + EMBEDPDF_TLS_GLOBALS=true +fi + PDF_RUNTIME_TARGET_OS_LIST="${PDF_RUNTIME_TARGET_OS_LIST:-$GN_TARGET_OS}" \ "$SOURCE_DIR/scripts/embedpdf-runtime/ensure-deps.sh" @@ -91,6 +102,7 @@ pdf_is_standalone=true use_debug_fission=false pdf_is_complete_lib=$PDF_IS_COMPLETE_LIB pdf_use_partition_alloc=false +embedpdf_thread_local_globals=$EMBEDPDF_TLS_GLOBALS symbol_level=0 target_os="$GN_TARGET_OS" target_cpu="$GN_TARGET_CPU"${EXTRA_ARGS:-} diff --git a/scripts/embedpdf-runtime/test-target.sh b/scripts/embedpdf-runtime/test-target.sh index 07c25687b0..0c1321cbbc 100755 --- a/scripts/embedpdf-runtime/test-target.sh +++ b/scripts/embedpdf-runtime/test-target.sh @@ -4,6 +4,11 @@ set -euo pipefail SOURCE_DIR="${PDF_RUNTIME_SOURCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" TARGET="${1:-}" TEST_SUITE="${PDFIUM_TEST_SUITE:-all}" +# EmbedPDF: thread-confined runtime. test-target.sh only builds native host +# targets, which is exactly where we ship the flag on, so default it on here to +# exercise the same variant we ship. Override with EMBEDPDF_TLS_GLOBALS=false to +# build the baseline (process-global) variant for comparison/regression. +EMBEDPDF_TLS_GLOBALS="${EMBEDPDF_TLS_GLOBALS:-true}" if [[ -z "$TARGET" ]]; then echo "usage: $0 " >&2 @@ -71,6 +76,7 @@ pdf_is_standalone=true use_debug_fission=false pdf_is_complete_lib=false pdf_use_partition_alloc=false +embedpdf_thread_local_globals=$EMBEDPDF_TLS_GLOBALS symbol_level=1 target_os="$GN_TARGET_OS" target_cpu="$GN_TARGET_CPU"${EXTRA_ARGS:-} diff --git a/scripts/embedpdf-runtime/thread-soak-target.sh b/scripts/embedpdf-runtime/thread-soak-target.sh new file mode 100755 index 0000000000..b2fe0b2b43 --- /dev/null +++ b/scripts/embedpdf-runtime/thread-soak-target.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +# EmbedPDF: build and run the thread-confined runtime soak harness +# (testing/tools:epdf_thread_soak). This is the gate for the thread_local +# globals work: run it (ideally under TSAN) before lifting the server pool cap. +# +# Usage: +# thread-soak-target.sh [-- ] +# +# Env: +# EMBEDPDF_TLS_GLOBALS default "true". Set "false" to build the baseline +# (process-global) variant for an A/B comparison. +# EMBEDPDF_TSAN default "0". Set "1" to build with is_tsan=true. + +SOURCE_DIR="${PDF_RUNTIME_SOURCE_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +TARGET="${1:-}" +PDF_PATH="${2:-}" +EMBEDPDF_TLS_GLOBALS="${EMBEDPDF_TLS_GLOBALS:-true}" +EMBEDPDF_TSAN="${EMBEDPDF_TSAN:-0}" + +if [[ -z "$TARGET" || -z "$PDF_PATH" ]]; then + echo "usage: $0 [-- ]" >&2 + exit 1 +fi +shift 2 || true +if [[ "${1:-}" == "--" ]]; then + shift +fi + +case "$TARGET" in + darwin-arm64) + GN_TARGET_OS="mac" + GN_TARGET_CPU="arm64" + ;; + darwin-x64) + GN_TARGET_OS="mac" + GN_TARGET_CPU="x64" + ;; + linux-x64) + GN_TARGET_OS="linux" + GN_TARGET_CPU="x64" + ;; + linux-arm64) + GN_TARGET_OS="linux" + GN_TARGET_CPU="arm64" + EXTRA_ARGS=$'\narm_control_flow_integrity="none"' + ;; + *) + echo "thread-soak-target.sh only supports host-native targets: darwin-arm64, darwin-x64, linux-x64, linux-arm64" >&2 + exit 1 + ;; +esac + +TSAN_ARG="" +if [[ "$EMBEDPDF_TSAN" == "1" ]]; then + TSAN_ARG=$'\nis_tsan=true' +fi + +PDF_RUNTIME_TARGET_OS_LIST="${PDF_RUNTIME_TARGET_OS_LIST:-$GN_TARGET_OS}" \ + "$SOURCE_DIR/scripts/embedpdf-runtime/ensure-deps.sh" + +"$SOURCE_DIR/scripts/embedpdf-runtime/apply-patches.sh" "$TARGET" + +if [[ "$TARGET" == linux-* ]]; then + ( + cd "$SOURCE_DIR" + build/install-build-deps.sh --no-prompt + build/linux/sysroot_scripts/install-sysroot.py "--arch=$GN_TARGET_CPU" + ) +fi + +OUT="$SOURCE_DIR/out/embedpdf-runtime-thread-soak-$TARGET" +mkdir -p "$OUT" + +cat > "$OUT/args.gn" <> +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/pixel/icc_profile_bad_component.pdf b/testing/resources/pixel/icc_profile_bad_component.pdf new file mode 100644 index 0000000000..03ead69421 Binary files /dev/null and b/testing/resources/pixel/icc_profile_bad_component.pdf differ diff --git a/testing/resources/pixel/icc_profile_bad_value.pdf b/testing/resources/pixel/icc_profile_bad_value.pdf new file mode 100644 index 0000000000..030ed3e0b6 Binary files /dev/null and b/testing/resources/pixel/icc_profile_bad_value.pdf differ diff --git a/testing/resources/redact_annot.in b/testing/resources/redact_annot.in index 99b7b4e8e9..e3e1de1341 100644 --- a/testing/resources/redact_annot.in +++ b/testing/resources/redact_annot.in @@ -15,6 +15,11 @@ endobj /Parent 2 0 R /Contents 4 0 R /MediaBox [0 0 612 792] + /Resources << + /Font << + /F1 6 0 R + >> + >> /Annots [ 5 0 R ] @@ -25,6 +30,22 @@ endobj {{streamlen}} >> stream +q +0.97 0.98 1 rg +0 0 612 792 re f +0.9 0.94 1 rg +285 526 72 20 re f +Q +BT +/F1 18 Tf +72 700 Td +(Visible redaction annotation fixture) Tj +/F1 12 Tf +72 660 Td +(The red annotation below marks the area that would be redacted.) Tj +293 533 Td +(target) Tj +ET endstream endobj {{object 5 0}} << @@ -32,12 +53,20 @@ endobj /Subtype /Redact /NM (Redact-1) /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) /QuadPoints [293 542 349 542 293 530 349 530] /P 3 0 R - /C [1 0.90196 0] /Rect [293 530 349 542] >> endobj +{{object 6 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj {{xref}} {{trailer}} {{startxref}} diff --git a/testing/resources/redact_annot.pdf b/testing/resources/redact_annot.pdf index 76c26078b2..a433bb6624 100644 Binary files a/testing/resources/redact_annot.pdf and b/testing/resources/redact_annot.pdf differ diff --git a/testing/resources/redact_apply_all_visible.in b/testing/resources/redact_apply_all_visible.in new file mode 100644 index 0000000000..18cbf69b67 --- /dev/null +++ b/testing/resources/redact_apply_all_visible.in @@ -0,0 +1,100 @@ +{{header}} +{{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 160 120] + /Resources << + /Font << + /F1 9 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R 7 0 R 8 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +0.97 0.97 1 rg +0 0 160 120 re f +1 0.9 0.9 rg +20 20 30 25 re f +0.9 1 0.9 rg +95 20 30 25 re f +Q +BT +/F1 9 Tf +10 98 Td +(apply all redactions removes both targets) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-left) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (LEFT) + /QuadPoints [15 50 55 50 15 15 55 15] + /Rect [15 15 55 50] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-left) + /F 4 + /C [0.8 0 0] + /IC [1 0.9 0.9] + /Border [0 0 2] + /Rect [20 20 50 45] +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-right) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (RIGHT) + /QuadPoints [90 50 130 50 90 15 130 15] + /Rect [90 15 130 50] +>> +endobj +{{object 8 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-right) + /F 4 + /C [0 0.55 0] + /IC [0.9 1 0.9] + /Border [0 0 2] + /Rect [95 20 125 45] +>> +endobj +{{object 9 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_apply_all_visible.pdf b/testing/resources/redact_apply_all_visible.pdf new file mode 100644 index 0000000000..5725b06a85 Binary files /dev/null and b/testing/resources/redact_apply_all_visible.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/redact_popup_cascade.in b/testing/resources/redact_popup_cascade.in new file mode 100644 index 0000000000..e52830b8a1 --- /dev/null +++ b/testing/resources/redact_popup_cascade.in @@ -0,0 +1,83 @@ +{{header}} +{{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 120 120] + /Resources << + /Font << + /F1 8 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R 7 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +1 0.98 0.9 rg +0 0 120 120 re f +1 0.9 0.2 rg +20 20 15 15 re f +Q +BT +/F1 8 Tf +5 104 Td +(popup cascades with note) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-popup-parent) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (POPUP) + /QuadPoints [10 50 50 50 10 10 50 10] + /Rect [10 10 50 50] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Text + /NM (Text-parent) + /F 4 + /C [1 0.75 0] + /Rect [20 20 35 35] + /Contents (comment) + /Popup 7 0 R +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Popup + /Rect [70 70 100 100] + /Parent 6 0 R + /Open true +>> +endobj +{{object 8 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_popup_cascade.pdf b/testing/resources/redact_popup_cascade.pdf new file mode 100644 index 0000000000..4e68070121 Binary files /dev/null and b/testing/resources/redact_popup_cascade.pdf differ diff --git a/testing/resources/redact_preserve_sibling.in b/testing/resources/redact_preserve_sibling.in new file mode 100644 index 0000000000..8a405cb793 --- /dev/null +++ b/testing/resources/redact_preserve_sibling.in @@ -0,0 +1,87 @@ +{{header}} +{{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 100 100] + /Resources << + /Font << + /F1 8 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R 7 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +0.98 0.98 0.94 rg +0 0 100 100 re f +0.9 0.95 1 rg +20 20 30 30 re f +Q +BT +/F1 8 Tf +5 86 Td +(sibling redaction stays visible) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-primary) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (PRIMARY) + /QuadPoints [10 40 40 40 10 10 40 10] + /Rect [10 10 40 40] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-sibling) + /F 4 + /C [1 0.5 0] + /IC [0 0 0] + /OverlayText (SIBLING) + /QuadPoints [15 35 35 35 15 15 35 15] + /Rect [15 15 35 35] +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-target) + /F 4 + /C [0 0.25 1] + /IC [0.85 0.93 1] + /Border [0 0 2] + /Rect [20 20 50 50] +>> +endobj +{{object 8 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_preserve_sibling.pdf b/testing/resources/redact_preserve_sibling.pdf new file mode 100644 index 0000000000..7524d56d1f Binary files /dev/null and b/testing/resources/redact_preserve_sibling.pdf differ diff --git a/testing/resources/redact_remove_annots.in b/testing/resources/redact_remove_annots.in new file mode 100644 index 0000000000..7f0d277fb5 --- /dev/null +++ b/testing/resources/redact_remove_annots.in @@ -0,0 +1,75 @@ +{{header}} +{{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 100 100] + /Resources << + /Font << + /F1 7 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +0.96 0.98 1 rg +0 0 100 100 re f +0.85 0.92 1 rg +20 20 30 30 re f +Q +BT +/F1 8 Tf +5 86 Td +(intersecting square is removed) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-target) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) + /QuadPoints [10 40 40 40 10 10 40 10] + /Rect [10 10 40 40] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-target) + /F 4 + /C [0 0.25 1] + /IC [0.85 0.93 1] + /Border [0 0 2] + /Rect [20 20 50 50] +>> +endobj +{{object 7 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_remove_annots.pdf b/testing/resources/redact_remove_annots.pdf new file mode 100644 index 0000000000..d30988ccab Binary files /dev/null and b/testing/resources/redact_remove_annots.pdf differ diff --git a/testing/resources/redact_text_middle.in b/testing/resources/redact_text_middle.in new file mode 100644 index 0000000000..b60f20b855 --- /dev/null +++ b/testing/resources/redact_text_middle.in @@ -0,0 +1,58 @@ +{{header}} +{{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 200] + /Resources << + /Font << + /F1 4 0 R + >> + >> + /Contents 5 0 R + /Annots [6 0 R] +>> +endobj +{{object 4 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Courier +>> +endobj +{{object 5 0}} << + {{streamlen}} +>> +stream +BT +/F1 20 Tf +50 100 Td +(hello secret world) Tj +ET +endstream +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-secret) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) + /QuadPoints [120 122 195 122 120 94 195 94] + /Rect [120 94 195 122] +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_text_middle.pdf b/testing/resources/redact_text_middle.pdf new file mode 100644 index 0000000000..d2e2d844cf Binary files /dev/null and b/testing/resources/redact_text_middle.pdf differ diff --git a/testing/resources/redact_touch_only.in b/testing/resources/redact_touch_only.in new file mode 100644 index 0000000000..c0ce81ad1a --- /dev/null +++ b/testing/resources/redact_touch_only.in @@ -0,0 +1,75 @@ +{{header}} +{{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 100 100] + /Resources << + /Font << + /F1 7 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +0.98 0.98 0.98 rg +0 0 100 100 re f +0.88 1 0.88 rg +30 10 30 30 re f +Q +BT +/F1 8 Tf +5 86 Td +(edge touch is not overlap) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-touch) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (TOUCH) + /QuadPoints [10 40 30 40 10 10 30 10] + /Rect [10 10 30 40] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-touching) + /F 4 + /C [0 0.55 0] + /IC [0.88 1 0.88] + /Border [0 0 2] + /Rect [30 10 60 40] +>> +endobj +{{object 7 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_touch_only.pdf b/testing/resources/redact_touch_only.pdf new file mode 100644 index 0000000000..908dfd3e3c Binary files /dev/null and b/testing/resources/redact_touch_only.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 diff --git a/testing/tools/BUILD.gn b/testing/tools/BUILD.gn index 15d841bcfe..7e3153c179 100644 --- a/testing/tools/BUILD.gn +++ b/testing/tools/BUILD.gn @@ -4,6 +4,51 @@ import("../../pdfium.gni") +source_set("epdf_layer_tool_support") { + testonly = true + sources = [ "epdf_layer_tool_common.h" ] + deps = [ "../../:pdfium_public_headers" ] + configs += [ "../../:pdfium_common_config" ] +} + +executable("epdf_layer_memory_benchmark") { + testonly = true + sources = [ "epdf_layer_memory_benchmark.cpp" ] + deps = [ + ":epdf_layer_tool_support", + "../../:pdfium_public_headers", + "../../fpdfsdk", + "//build/win:default_exe_manifest", + ] + configs += [ "../../:pdfium_common_config" ] +} + +executable("epdf_layer_replay_soak") { + testonly = true + sources = [ "epdf_layer_replay_soak.cpp" ] + deps = [ + ":epdf_layer_tool_support", + "../../:pdfium_public_headers", + "../../fpdfsdk", + "//build/win:default_exe_manifest", + ] + configs += [ "../../:pdfium_common_config" ] +} + +# EmbedPDF: thread-confined runtime soak. Multi-thread init/open/render/save/ +# close harness used (under TSAN) to validate the thread_local globals work. +executable("epdf_thread_soak") { + testonly = true + sources = [ "epdf_thread_soak.cpp" ] + deps = [ + ":epdf_layer_tool_support", + "../../:pdfium_public_headers", + "../../fpdfsdk", + "//build/win:default_exe_manifest", + ] + configs += [ "../../:pdfium_common_config" ] +} + if (pdf_is_standalone) { # Generates the list of inputs required by `test_runner.py` tests. action("test_runner_py") { diff --git a/testing/tools/epdf_layer_memory_benchmark.cpp b/testing/tools/epdf_layer_memory_benchmark.cpp new file mode 100644 index 0000000000..bfcc25f3d8 --- /dev/null +++ b/testing/tools/epdf_layer_memory_benchmark.cpp @@ -0,0 +1,170 @@ +// 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 "testing/tools/epdf_layer_tool_common.h" + +#include + +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#elif defined(__linux__) +#include + +#include +#endif + +#include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_annot.h" +#include "public/fpdfview.h" + +namespace { + +size_t CurrentRssBytes() { +#if defined(__APPLE__) + mach_task_basic_info_data_t info; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, + reinterpret_cast(&info), &count) != KERN_SUCCESS) { + return 0; + } + return static_cast(info.resident_size); +#elif defined(__linux__) + std::ifstream statm("/proc/self/statm"); + size_t total_pages = 0; + size_t resident_pages = 0; + statm >> total_pages >> resident_pages; + const long page_size = sysconf(_SC_PAGESIZE); + if (!statm || page_size <= 0) { + return 0; + } + return resident_pages * static_cast(page_size); +#else + return 0; +#endif +} + +std::vector ParseLayerCounts(const std::string& spec) { + std::vector result; + size_t start = 0; + while (start <= spec.size()) { + const size_t comma = spec.find(',', start); + const std::string token = spec.substr( + start, comma == std::string::npos ? std::string::npos : comma - start); + if (!token.empty()) { + result.push_back( + static_cast(std::strtoull(token.c_str(), nullptr, 10))); + } + if (comma == std::string::npos) { + break; + } + start = comma + 1; + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +bool AddTextAnnotation(FPDF_DOCUMENT layer) { + ScopedFPDFPage page(FPDF_LoadPage(layer, 0)); + if (!page) { + return false; + } + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + return !!annot; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + epdf_layer_tool::PrintUsage(argv[0], "[--layers=1,10,100,1000]"); + return 2; + } + + std::string path = argv[1]; + std::vector layer_counts = {1, 10, 100, 1000}; + for (int i = 2; i < argc; ++i) { + const std::string arg = argv[i]; + constexpr char kLayersPrefix[] = "--layers="; + if (arg.rfind(kLayersPrefix, 0) == 0) { + layer_counts = ParseLayerCounts(arg.substr(sizeof(kLayersPrefix) - 1)); + } else { + epdf_layer_tool::PrintUsage(argv[0], "[--layers=1,10,100,1000]"); + return 2; + } + } + if (layer_counts.empty() || layer_counts.front() == 0) { + std::fprintf(stderr, "Layer counts must be positive.\n"); + return 2; + } + + std::vector base_bytes; + if (!epdf_layer_tool::ReadFile(path, &base_bytes)) { + std::fprintf(stderr, "Failed to read %s\n", path.c_str()); + return 1; + } + + FPDF_InitLibrary(); + epdf_layer_tool::MemoryFile base_file(&base_bytes); + EPDF_BASE_DOCUMENT base = + EPDF_LoadBaseDocument(base_file.file_access(), nullptr); + if (!base) { + std::fprintf(stderr, "Failed to load base document.\n"); + FPDF_DestroyLibrary(); + return 1; + } + + const size_t baseline_rss = CurrentRssBytes(); + std::vector layers; + layers.reserve(layer_counts.back()); + + std::puts( + "layers,rss_bytes,delta_from_base_rss,bytes_per_layer," + "promoted_objects"); + size_t next_report = 0; + for (size_t i = 1; i <= layer_counts.back(); ++i) { + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, /*pFileAccess=*/nullptr, nullptr, &status)); + if (!layer || status != EPDFLayerOpenStatus_kSuccess) { + std::fprintf(stderr, "Failed to open layer %zu.\n", i); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + if (!AddTextAnnotation(layer.get())) { + std::fprintf(stderr, "Failed to mutate layer %zu.\n", i); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + layers.push_back(std::move(layer)); + + if (i == layer_counts[next_report]) { + size_t promoted_objects = 0; + for (const auto& live_layer : layers) { + promoted_objects += EPDFLayer_GetPromotedObjectCount(live_layer.get()); + } + const size_t rss = CurrentRssBytes(); + const size_t delta = rss > baseline_rss ? rss - baseline_rss : 0; + std::printf("%zu,%zu,%zu,%zu,%zu\n", i, rss, delta, delta / i, + promoted_objects); + ++next_report; + if (next_report == layer_counts.size()) { + break; + } + } + } + + layers.clear(); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 0; +} diff --git a/testing/tools/epdf_layer_replay_soak.cpp b/testing/tools/epdf_layer_replay_soak.cpp new file mode 100644 index 0000000000..e5c060f923 --- /dev/null +++ b/testing/tools/epdf_layer_replay_soak.cpp @@ -0,0 +1,245 @@ +// 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 "testing/tools/epdf_layer_tool_common.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_save.h" +#include "public/fpdfview.h" + +namespace { + +size_t ParseSizeArg(const std::string& arg, + const char* prefix, + size_t fallback) { + const std::string prefix_string(prefix); + if (arg.rfind(prefix_string, 0) != 0) { + return fallback; + } + return static_cast( + std::strtoull(arg.substr(prefix_string.size()).c_str(), nullptr, 10)); +} + +bool AddTextAnnotations(FPDF_DOCUMENT layer, size_t count) { + ScopedFPDFPage page(FPDF_LoadPage(layer, 0)); + if (!page) { + return false; + } + for (size_t i = 0; i < count; ++i) { + ScopedFPDFAnnotation annot( + EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + if (!annot) { + return false; + } + } + return static_cast(FPDFPage_GetAnnotCount(page.get())) == count; +} + +bool VerifyMaterializedAnnotCount(const std::vector& materialized, + size_t expected_count) { + ScopedFPDFDocument reopened(FPDF_LoadMemDocument64( + materialized.data(), materialized.size(), nullptr)); + if (!reopened) { + return false; + } + ScopedFPDFPage page(FPDF_LoadPage(reopened.get(), 0)); + if (!page) { + return false; + } + return static_cast(FPDFPage_GetAnnotCount(page.get())) == + expected_count; +} + +bool VerifyLayerAnnotCount(FPDF_DOCUMENT layer, size_t expected_count) { + ScopedFPDFPage page(FPDF_LoadPage(layer, 0)); + if (!page) { + return false; + } + return static_cast(FPDFPage_GetAnnotCount(page.get())) == + expected_count; +} + +bool VerifyRawDeltaReplay(EPDF_BASE_DOCUMENT base, + const std::string& delta, + size_t expected_count) { + const std::vector delta_bytes(delta.begin(), delta.end()); + epdf_layer_tool::MemoryFile delta_file(&delta_bytes); + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument reopened(EPDFLayer_OpenLayer( + base, delta_file.file_access(), nullptr, &open_status)); + return reopened && open_status == EPDFLayerOpenStatus_kSuccess && + VerifyLayerAnnotCount(reopened.get(), expected_count); +} + +bool VerifyArtifactReplay(EPDF_BASE_DOCUMENT base, + FPDF_DOCUMENT layer, + size_t expected_count) { + unsigned long artifact_size = 0; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + void* artifact = EPDFLayer_SaveLayerArtifactToOwnedBuffer( + layer, &artifact_size, &save_status); + if (!artifact || artifact_size == 0 || + save_status != EPDFLayerSaveStatus_kSuccess) { + EPDF_FreeBuffer(artifact); + return false; + } + + std::vector artifact_bytes( + static_cast(artifact), + static_cast(artifact) + artifact_size); + EPDF_FreeBuffer(artifact); + + epdf_layer_tool::MemoryFile artifact_file(&artifact_bytes); + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument reopened(EPDFLayer_OpenLayerArtifact( + base, artifact_file.file_access(), nullptr, &open_status)); + return reopened && open_status == EPDFLayerOpenStatus_kSuccess && + VerifyLayerAnnotCount(reopened.get(), expected_count); +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + epdf_layer_tool::PrintUsage( + argv[0], + "[--layers=100] [--rounds=60] [--sleep-seconds=60] [--seed=N]"); + return 2; + } + + std::string path = argv[1]; + size_t layer_count = 100; + size_t rounds = 60; + size_t sleep_seconds = 60; + uint32_t seed = 0xE7DF750u; + + for (int i = 2; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.rfind("--layers=", 0) == 0) { + layer_count = ParseSizeArg(arg, "--layers=", layer_count); + } else if (arg.rfind("--rounds=", 0) == 0) { + rounds = ParseSizeArg(arg, "--rounds=", rounds); + } else if (arg.rfind("--sleep-seconds=", 0) == 0) { + sleep_seconds = ParseSizeArg(arg, "--sleep-seconds=", sleep_seconds); + } else if (arg.rfind("--seed=", 0) == 0) { + seed = static_cast(ParseSizeArg(arg, "--seed=", seed)); + } else { + epdf_layer_tool::PrintUsage( + argv[0], + "[--layers=100] [--rounds=60] [--sleep-seconds=60] [--seed=N]"); + return 2; + } + } + + if (layer_count == 0 || rounds == 0) { + std::fprintf(stderr, "Layer count and rounds must be positive.\n"); + return 2; + } + + std::vector base_bytes; + if (!epdf_layer_tool::ReadFile(path, &base_bytes)) { + std::fprintf(stderr, "Failed to read %s\n", path.c_str()); + return 1; + } + + FPDF_InitLibrary(); + epdf_layer_tool::MemoryFile base_file(&base_bytes); + EPDF_BASE_DOCUMENT base = + EPDF_LoadBaseDocument(base_file.file_access(), nullptr); + if (!base) { + std::fprintf(stderr, "Failed to load base document.\n"); + FPDF_DestroyLibrary(); + return 1; + } + + std::mt19937 rng(seed); + std::uniform_int_distribution layer_dist(0, layer_count - 1); + std::uniform_int_distribution edit_dist(1, 3); + std::vector expected_annots(layer_count, 0); + + for (size_t round = 0; round < rounds; ++round) { + const size_t edited_layer = layer_dist(rng); + expected_annots[edited_layer] += edit_dist(rng); + + for (size_t layer_index = 0; layer_index < layer_count; ++layer_index) { + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); + if (!layer || open_status != EPDFLayerOpenStatus_kSuccess) { + std::fprintf(stderr, "Round %zu layer %zu: open failed.\n", round, + layer_index); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + if (!AddTextAnnotations(layer.get(), expected_annots[layer_index])) { + std::fprintf(stderr, "Round %zu layer %zu: mutation failed.\n", round, + layer_index); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + + epdf_layer_tool::StringWriter writer; + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + if (!EPDFLayer_SaveDelta(layer.get(), &writer, &save_status) || + save_status != EPDFLayerSaveStatus_kSuccess) { + std::fprintf(stderr, "Round %zu layer %zu: save failed (%d).\n", round, + layer_index, save_status); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + + const std::vector materialized = + epdf_layer_tool::MaterializeLayerBytes(base_bytes, writer.data); + if (!VerifyMaterializedAnnotCount(materialized, + expected_annots[layer_index])) { + std::fprintf(stderr, + "Round %zu layer %zu: materialized verification failed.\n", + round, layer_index); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + if (!VerifyRawDeltaReplay(base, writer.data, + expected_annots[layer_index])) { + std::fprintf(stderr, "Round %zu layer %zu: raw delta replay failed.\n", + round, layer_index); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + if (!VerifyArtifactReplay(base, layer.get(), + expected_annots[layer_index])) { + std::fprintf(stderr, "Round %zu layer %zu: artifact replay failed.\n", + round, layer_index); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + } + + std::printf("round=%zu layers=%zu edited_layer=%zu ok\n", round + 1, + layer_count, edited_layer); + if (round + 1 < rounds && sleep_seconds > 0) { + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds)); + } + } + + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 0; +} diff --git a/testing/tools/epdf_layer_tool_common.h b/testing/tools/epdf_layer_tool_common.h new file mode 100644 index 0000000000..d78a95238b --- /dev/null +++ b/testing/tools/epdf_layer_tool_common.h @@ -0,0 +1,108 @@ +// 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 TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ +#define TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "public/fpdf_save.h" +#include "public/fpdfview.h" + +namespace epdf_layer_tool { + +struct MemoryFile { + explicit MemoryFile(std::vector input) + : owned_bytes(std::move(input)), bytes(&owned_bytes) { + InitAccess(); + } + + explicit MemoryFile(const std::vector* input) : bytes(input) { + InitAccess(); + } + + void InitAccess() { + access.m_FileLen = static_cast(bytes->size()); + access.m_GetBlock = &MemoryFile::GetBlock; + access.m_Param = this; + } + + FPDF_FILEACCESS* file_access() { return &access; } + + static int GetBlock(void* param, + unsigned long pos, + unsigned char* buf, + unsigned long size) { + MemoryFile* file = static_cast(param); + if (!file || !file->bytes || pos > file->bytes->size() || + size > file->bytes->size() - pos) { + return 0; + } + memcpy(buf, file->bytes->data() + pos, size); + return 1; + } + + std::vector owned_bytes; + const std::vector* bytes = nullptr; + FPDF_FILEACCESS access = {}; +}; + +struct StringWriter : FPDF_FILEWRITE { + StringWriter() { + version = 1; + WriteBlock = &StringWriter::WriteBlockCallback; + } + + void Clear() { data.clear(); } + + static int WriteBlockCallback(FPDF_FILEWRITE* file_write, + const void* buffer, + unsigned long size) { + StringWriter* writer = static_cast(file_write); + writer->data.append(static_cast(buffer), size); + return 1; + } + + std::string data; +}; + +inline bool ReadFile(const std::string& path, std::vector* out) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return false; + } + file.seekg(0, std::ios::end); + const std::streamoff length = file.tellg(); + if (length < 0) { + return false; + } + file.seekg(0, std::ios::beg); + out->resize(static_cast(length)); + return out->empty() || + file.read(reinterpret_cast(out->data()), length).good(); +} + +inline std::vector MaterializeLayerBytes( + const std::vector& base_bytes, + const std::string& delta) { + std::vector materialized = base_bytes; + materialized.insert(materialized.end(), delta.begin(), delta.end()); + return materialized; +} + +inline void PrintUsage(const char* argv0, const char* extra) { + std::fprintf(stderr, "Usage: %s %s\n", argv0, extra); +} + +} // namespace epdf_layer_tool + +#endif // TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ diff --git a/testing/tools/epdf_thread_soak.cpp b/testing/tools/epdf_thread_soak.cpp new file mode 100644 index 0000000000..afa5195bf8 --- /dev/null +++ b/testing/tools/epdf_thread_soak.cpp @@ -0,0 +1,187 @@ +// 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. + +// EmbedPDF: thread-confined runtime soak harness. +// +// Spawns N worker threads that each run an independent PDFium lifecycle in a +// loop: FPDF_InitLibrary -> load -> render page 0 -> (optional) encrypted save +// -> close -> FPDF_DestroyLibrary. Each worker only ever touches handles it +// created, matching the thread-confined contract. +// +// With embedpdf_thread_local_globals OFF this exercises (and is expected to +// trip) the shared process-global init/render races. With the flag ON every +// thread owns its own PDFium state and the soak should pass cleanly. The +// encrypted-save path is included on purpose: pending security is now stored on +// CPDF_Document, so it must survive concurrent SetEncryption -> save across +// threads. Intended to be run under ThreadSanitizer as the gate before the +// server worker-pool cap is lifted. + +#include "testing/tools/epdf_layer_tool_common.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "public/cpp/fpdf_scopers.h" +#include "public/fpdf_save.h" +#include "public/fpdfview.h" + +namespace { + +// Fixed render surface keeps per-thread memory bounded while still exercising +// the rasterizer, font cache, and stock colorspaces. +constexpr int kRenderWidth = 300; +constexpr int kRenderHeight = 400; + +size_t ParseSizeArg(const std::string& arg, + const char* prefix, + size_t fallback) { + const std::string prefix_string(prefix); + if (arg.rfind(prefix_string, 0) != 0) { + return fallback; + } + return static_cast( + std::strtoull(arg.substr(prefix_string.size()).c_str(), nullptr, 10)); +} + +struct Options { + size_t threads = 4; + size_t iterations = 50; + bool render = true; + bool encrypt = true; +}; + +const char* kUsageExtra = + "[--threads=4] [--iterations=50] [--no-render] [--no-encrypt]"; + +bool RenderFirstPage(FPDF_DOCUMENT doc) { + ScopedFPDFPage page(FPDF_LoadPage(doc, 0)); + if (!page) { + return false; + } + ScopedFPDFBitmap bitmap(FPDFBitmap_Create(kRenderWidth, kRenderHeight, 0)); + if (!bitmap) { + return false; + } + FPDFBitmap_FillRect(bitmap.get(), 0, 0, kRenderWidth, kRenderHeight, + 0xFFFFFFFF); + FPDF_RenderPageBitmap(bitmap.get(), page.get(), 0, 0, kRenderWidth, + kRenderHeight, 0, FPDF_ANNOT); + // Touch the buffer so the render isn't optimized away. + return FPDFBitmap_GetBuffer(bitmap.get()) != nullptr; +} + +bool EncryptAndSave(FPDF_DOCUMENT doc) { + if (!EPDF_SetEncryption(doc, "user", "owner", + EPDF_PERM_PRINT | EPDF_PERM_COPY)) { + return false; + } + unsigned long out_size = 0; + void* buffer = EPDF_SaveDocumentToOwnedBuffer(doc, 0, &out_size); + const bool ok = buffer != nullptr && out_size > 0; + EPDF_FreeBuffer(buffer); + return ok; +} + +bool RunWorker(const std::vector& bytes, + const Options& opts, + size_t worker_index) { + for (size_t i = 0; i < opts.iterations; ++i) { + FPDF_InitLibrary(); + { + ScopedFPDFDocument doc(FPDF_LoadMemDocument64( + bytes.data(), bytes.size(), nullptr)); + if (!doc) { + std::fprintf(stderr, "worker %zu iter %zu: load failed\n", worker_index, + i); + FPDF_DestroyLibrary(); + return false; + } + if (opts.render && !RenderFirstPage(doc.get())) { + std::fprintf(stderr, "worker %zu iter %zu: render failed\n", + worker_index, i); + FPDF_DestroyLibrary(); + return false; + } + if (opts.encrypt && !EncryptAndSave(doc.get())) { + std::fprintf(stderr, "worker %zu iter %zu: encrypt-save failed\n", + worker_index, i); + FPDF_DestroyLibrary(); + return false; + } + } // Document is closed here, before tearing down the library. + FPDF_DestroyLibrary(); + } + return true; +} + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + epdf_layer_tool::PrintUsage(argv[0], kUsageExtra); + return 2; + } + + std::string path = argv[1]; + Options opts; + for (int i = 2; i < argc; ++i) { + const std::string arg = argv[i]; + if (arg.rfind("--threads=", 0) == 0) { + opts.threads = ParseSizeArg(arg, "--threads=", opts.threads); + } else if (arg.rfind("--iterations=", 0) == 0) { + opts.iterations = ParseSizeArg(arg, "--iterations=", opts.iterations); + } else if (arg == "--no-render") { + opts.render = false; + } else if (arg == "--no-encrypt") { + opts.encrypt = false; + } else { + epdf_layer_tool::PrintUsage(argv[0], kUsageExtra); + return 2; + } + } + + if (opts.threads == 0 || opts.iterations == 0) { + std::fprintf(stderr, "Thread count and iterations must be positive.\n"); + return 2; + } + + std::vector bytes; + if (!epdf_layer_tool::ReadFile(path, &bytes)) { + std::fprintf(stderr, "Failed to read %s\n", path.c_str()); + return 1; + } + + std::atomic failures{0}; + std::vector workers; + workers.reserve(opts.threads); + for (size_t t = 0; t < opts.threads; ++t) { + workers.emplace_back([&bytes, &opts, &failures, t]() { + if (!RunWorker(bytes, opts, t)) { + failures.fetch_add(1, std::memory_order_relaxed); + } + }); + } + for (auto& worker : workers) { + worker.join(); + } + + const size_t failed = failures.load(std::memory_order_relaxed); + if (failed != 0) { + std::fprintf(stderr, "thread soak FAILED: %zu/%zu workers errored\n", failed, + opts.threads); + return 1; + } + + std::printf( + "thread soak OK: threads=%zu iterations=%zu render=%d encrypt=%d\n", + opts.threads, opts.iterations, opts.render ? 1 : 0, + opts.encrypt ? 1 : 0); + return 0; +} diff --git a/third_party/BUILD.gn b/third_party/BUILD.gn index dfb74ec3ed..6384408d1e 100644 --- a/third_party/BUILD.gn +++ b/third_party/BUILD.gn @@ -306,6 +306,12 @@ source_set("fx_lcms2") { "lcms/src/cmswtpnt.c", "lcms/src/cmsxform.c", ] + if (is_linux || is_chromeos || is_android || is_mac || is_ios) { + defines = [ "HAVE_GMTIME_R" ] + } + if (is_win) { + defines = [ "HAVE_GMTIME_S" ] + } deps = [ "../core/fxcrt" ] } diff --git a/third_party/lcms/src/cmsplugin.c b/third_party/lcms/src/cmsplugin.c index 3876506dac..ce7fb2eac7 100644 --- a/third_party/lcms/src/cmsplugin.c +++ b/third_party/lcms/src/cmsplugin.c @@ -1057,7 +1057,13 @@ cmsBool _cmsGetTime(struct tm* ptr_time) _cmsEnterCriticalSectionPrimitive(&_cmsContextPoolHeadMutex); t = gmtime(&now); + // EmbedPDF: copy gmtime()'s shared static result while still holding the + // LCMS mutex. ThreadSanitizer flags copying it after unlock when ICC + // profiles are created concurrently. + if (t != NULL) + *ptr_time = *t; _cmsLeaveCriticalSectionPrimitive(&_cmsContextPoolHeadMutex); + return t != NULL; #endif if (t == NULL) diff --git a/third_party/lcms/src/cmswtpnt.c b/third_party/lcms/src/cmswtpnt.c index a73eaa7e36..490035e9d5 100644 --- a/third_party/lcms/src/cmswtpnt.c +++ b/third_party/lcms/src/cmswtpnt.c @@ -30,16 +30,23 @@ // D50 - Widely used const cmsCIEXYZ* CMSEXPORT cmsD50_XYZ(void) { - static cmsCIEXYZ D50XYZ = {cmsD50X, cmsD50Y, cmsD50Z}; + // EmbedPDF: immutable shared D50 constant is safe for concurrent readers. + static const cmsCIEXYZ D50XYZ = {cmsD50X, cmsD50Y, cmsD50Z}; return &D50XYZ; } const cmsCIExyY* CMSEXPORT cmsD50_xyY(void) { - static cmsCIExyY D50xyY; - - cmsXYZ2xyY(&D50xyY, cmsD50_XYZ()); + // EmbedPDF: thread-confined runtime. + // This is equivalent to cmsXYZ2xyY(cmsD50_XYZ()), but avoids writing to a + // shared static on every call. ThreadSanitizer flags the old lazy + // recomputation when multiple documents create ICC transforms in parallel. + static const cmsCIExyY D50xyY = { + cmsD50X / (cmsD50X + cmsD50Y + cmsD50Z), + cmsD50Y / (cmsD50X + cmsD50Y + cmsD50Z), + cmsD50Y + }; return &D50xyY; } @@ -349,5 +356,3 @@ cmsBool CMSEXPORT cmsAdaptToIlluminant(cmsCIEXYZ* Result, return TRUE; } - -