diff --git a/core/fpdfapi/edit/BUILD.gn b/core/fpdfapi/edit/BUILD.gn index fd0b1eac7..b5a8ab45b 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 612502aba..025c9b1ff 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,6 +233,8 @@ bool CPDF_Creator::WriteOldObjs() { return true; } + // WriteOldObjs() only runs for full saves. Layer saves are incremental and + // use CollectSaveReachableObjects() when writing their overlay objects. const std::set objects_with_refs = GetObjectsWithReferences(document_); uint32_t last_object_number_written = 0; @@ -210,8 +256,15 @@ bool CPDF_Creator::WriteOldObjs() { } bool CPDF_Creator::WriteNewObjs() { + const std::set objects_with_refs = + CollectSaveReachableObjects(document_, encrypt_dict_.Get()); + 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 +274,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 +332,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 +414,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 +468,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 +513,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 +531,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; @@ -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; @@ -629,7 +721,11 @@ bool CPDF_Creator::Create(Mask flags, int32_t file_version) { new_obj_num_array_.clear(); InitID(); - return Continue(); + 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 f96e708a7..06d2fe120 100644 --- a/core/fpdfapi/edit/cpdf_creator.h +++ b/core/fpdfapi/edit/cpdf_creator.h @@ -13,6 +13,7 @@ #include #include +#include "core/fxcrt/fx_string.h" #include "core/fxcrt/fx_stream.h" #include "core/fxcrt/mask.h" #include "core/fxcrt/retain_ptr.h" @@ -36,6 +37,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,6 +52,9 @@ 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) @@ -83,6 +95,7 @@ class CPDF_Creator { bool WriteOldObjs(); bool WriteNewObjs(); bool WriteIndirectObj(uint32_t objnum, const CPDF_Object* pObj); + bool CheckEmittedOffset(FX_FILESIZE offset); void RemoveSecurity(); @@ -106,6 +119,8 @@ class CPDF_Creator { 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 000000000..4ea0c8ba1 --- /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/font/cpdf_type3font.cpp b/core/fpdfapi/font/cpdf_type3font.cpp index 00d95298c..44b8ca7d0 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 dcaf97354..75fa27610 100644 --- a/core/fpdfapi/page/cpdf_annotcontext.cpp +++ b/core/fpdfapi/page/cpdf_annotcontext.cpp @@ -10,13 +10,20 @@ #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_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()); @@ -27,7 +34,10 @@ CPDF_AnnotContext::~CPDF_AnnotContext() = default; void CPDF_AnnotContext::SetForm(RetainPtr pStream) { CHECK(pStream); 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 +48,35 @@ void CPDF_AnnotContext::SetForm(RetainPtr pStream) { pStream->GetDict()->GetMatrixFor("Matrix").GetInverse(); annot_form_->ParseContent(nullptr, &inverse_stream_matrix, nullptr); } + +RetainPtr CPDF_AnnotContext::GetMutableAnnotDict() { + 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()); + } + return annot_dict_; + } + + if (annot_dict_->IsFrozen()) { + EnsureMutableBackingForAnnotDict(); + } + return annot_dict_; +} + +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 b1d008bf2..ebc16c97a 100644 --- a/core/fpdfapi/page/cpdf_annotcontext.h +++ b/core/fpdfapi/page/cpdf_annotcontext.h @@ -19,7 +19,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 +29,19 @@ class CPDF_AnnotContext { CPDF_Form* GetForm() const { return annot_form_.get(); } // Never nullptr. - RetainPtr GetMutableAnnotDict() { return annot_dict_; } + RetainPtr GetMutableAnnotDict(); const CPDF_Dictionary* GetAnnotDict() const { return annot_dict_.Get(); } // Never nullptr. IPDF_Page* GetPage() const { return page_; } private: + void EnsureMutableBackingForAnnotDict(); + std::unique_ptr annot_form_; - RetainPtr const annot_dict_; + RetainPtr annot_dict_; UnownedPtr const page_; + const int annot_index_ = -1; }; #endif // CORE_FPDFAPI_PAGE_CPDF_ANNOTCONTEXT_H_ diff --git a/core/fpdfapi/page/cpdf_contentparser.cpp b/core/fpdfapi/page/cpdf_contentparser.cpp index 1d6bd7b67..80e7c9a62 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 2c3f841df..a40e1e936 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 a46ff6ecc..ebaeb4721 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 2f55ac308..76daf765b 100644 --- a/core/fpdfapi/page/cpdf_form.cpp +++ b/core/fpdfapi/page/cpdf_form.cpp @@ -14,6 +14,8 @@ #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_object.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fxcrt/check_op.h" #include "core/fxge/dib/cfx_dibitmap.h" @@ -23,10 +25,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,15 +47,15 @@ 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, + pdfium::WrapRetain(const_cast(ChooseResourcesDict( + pFormStream->GetDict()->GetDictFor("Resources").Get(), + pParentResources, + pPageResources.Get())))), form_stream_(std::move(pFormStream)) { LoadTransparencyInfo(); } @@ -118,9 +120,27 @@ CFX_FloatRect CPDF_Form::CalcBoundingBox() const { } RetainPtr CPDF_Form::GetMutableFormStream() { + CPDF_Document* doc = GetDocument(); + if (!doc || !form_stream_) { + return form_stream_; + } + + const uint32_t objnum = form_stream_->GetObjNum(); + DCHECK(objnum); + RetainPtr live = doc->GetMutableIndirectObject(objnum); + if (live && live.Get() != form_stream_.Get()) { + form_stream_ = pdfium::WrapRetain(live->AsMutableStream()); + } return form_stream_; } +void CPDF_Form::EnsureMutableBackingObjectForDict() { + RetainPtr live_stream = GetMutableFormStream(); + if (live_stream) { + dict_ = live_stream->GetMutableDict(); + } +} + RetainPtr CPDF_Form::GetStream() const { return form_stream_; } diff --git a/core/fpdfapi/page/cpdf_form.h b/core/fpdfapi/page/cpdf_form.h index 354c50d05..80ab06eee 100644 --- a/core/fpdfapi/page/cpdf_form.h +++ b/core/fpdfapi/page/cpdf_form.h @@ -32,9 +32,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 +65,16 @@ class CPDF_Form final : public CPDF_PageObjectHolder, RetainPtr GetStream() const; private: + // CPDF_PageObjectHolder: + void EnsureMutableBackingObjectForDict() override; + void ParseContentInternal(const CPDF_AllStates* pGraphicStates, const CFX_Matrix* pParentMatrix, CPDF_Type3Char* pType3Char, RecursionState* recursion_state); RecursionState recursion_state_; - RetainPtr const form_stream_; + RetainPtr form_stream_; }; #endif // CORE_FPDFAPI_PAGE_CPDF_FORM_H_ diff --git a/core/fpdfapi/page/cpdf_image.cpp b/core/fpdfapi/page/cpdf_image.cpp index 6cb86bbe6..384cb91b3 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 b547578f2..f9137ff1e 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 f0f7c2599..2490f624a 100644 --- a/core/fpdfapi/page/cpdf_page.cpp +++ b/core/fpdfapi/page/cpdf_page.cpp @@ -15,6 +15,7 @@ #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_object.h" #include "core/fxcrt/check.h" #include "core/fxcrt/check_op.h" @@ -28,9 +29,10 @@ CPDF_Page::CPDF_Page(CPDF_Document* 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(); @@ -81,6 +83,21 @@ 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_; +} + RetainPtr CPDF_Page::GetPageAttr(ByteStringView name) const { std::set> visited; RetainPtr pPageDict = GetDict(); diff --git a/core/fpdfapi/page/cpdf_page.h b/core/fpdfapi/page/cpdf_page.h index ba1da8dac..ae9b829f3 100644 --- a/core/fpdfapi/page/cpdf_page.h +++ b/core/fpdfapi/page/cpdf_page.h @@ -106,6 +106,10 @@ 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; + 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 cc7ba1e17..9f05687b3 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 f05f01a61..4e44212c6 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 { @@ -61,6 +63,136 @@ RetainPtr CPDF_PageObjectHolder::GetMutableFormStream() { return nullptr; } +RetainPtr CPDF_PageObjectHolder::GetDict() const { + if (!document_) { + return dict_; + } + + const uint32_t objnum = dict_->GetObjNum(); + if (objnum == 0) { + return dict_; + } + + RetainPtr live = document_->FindPromotedObject(objnum); + if (live && live.Get() != dict_.Get()) { + const_cast(this)->dict_ = + pdfium::WrapRetain(live->AsMutableDictionary()); + } + 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()); + } + return dict_; + } + + if (dict_->IsFrozen()) { + EnsureMutableBackingObjectForDict(); + } + return dict_; +} + +RetainPtr CPDF_PageObjectHolder::GetResources() const { + if (!document_ || !resources_) { + return resources_; + } + + const uint32_t objnum = resources_->GetObjNum(); + if (objnum == 0) { + return resources_; + } + + RetainPtr live = document_->FindPromotedObject(objnum); + if (live && live.Get() != resources_.Get()) { + const_cast(this)->resources_ = + pdfium::WrapRetain(live->AsMutableDictionary()); + } + 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()); + } + return resources_; + } + + if (resources_->IsFrozen()) { + EnsureMutableBackingObjectForResources(); + } + return resources_; +} + +RetainPtr CPDF_PageObjectHolder::GetPageResources() + const { + if (!document_ || !page_resources_) { + return page_resources_; + } + + const uint32_t objnum = page_resources_->GetObjNum(); + if (objnum == 0) { + return page_resources_; + } + + RetainPtr live = document_->FindPromotedObject(objnum); + if (live && live.Get() != page_resources_.Get()) { + const_cast(this)->page_resources_ = + pdfium::WrapRetain(live->AsMutableDictionary()); + } + 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()); + } + return page_resources_; + } + + if (page_resources_->IsFrozen()) { + EnsureMutableBackingObjectForPageResources(); + } + 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); diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.h b/core/fpdfapi/page/cpdf_pageobjectholder.h index 898a3d1c0..5ac3a6cc7 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,15 @@ 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_; } + RetainPtr GetDict() const; + RetainPtr GetMutableDict(); + RetainPtr GetResources() const; + RetainPtr GetMutableResources(); void SetResources(RetainPtr dict) { resources_ = std::move(dict); } - RetainPtr GetPageResources() const { - return page_resources_; - } - RetainPtr GetMutablePageResources() { - return page_resources_; - } + 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 +156,13 @@ class CPDF_PageObjectHolder { protected: void LoadTransparencyInfo(); + virtual void EnsureMutableBackingObjectForDict(); + virtual void EnsureMutableBackingObjectForResources(); + virtual void EnsureMutableBackingObjectForPageResources(); RetainPtr page_resources_; RetainPtr resources_; + RetainPtr dict_; std::map graphics_map_; std::map fonts_map_; std::map fonts_by_objnum_; @@ -172,7 +173,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 e503fbf16..64f366c2f 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" @@ -187,6 +188,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 +230,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 +401,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 +628,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 +786,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 +956,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 +1208,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 +1223,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 +1297,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 +1309,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 38e9977c2..91f457536 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 94ab93ee7..a66441b47 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", @@ -33,6 +37,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 +59,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 +124,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 +138,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 4939e24be..5665b4ebc 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 473588dfc..053d6cb9a 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 000000000..92abccff4 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.cpp @@ -0,0 +1,154 @@ +// 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_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); +} diff --git a/core/fpdfapi/parser/cpdf_base_document.h b/core/fpdfapi/parser/cpdf_base_document.h new file mode 100644 index 000000000..abd434efe --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.h @@ -0,0 +1,49 @@ +// 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 "core/fpdfapi/parser/cpdf_document.h" +#include "core/fxcrt/fx_types.h" +#include "core/fxcrt/retain_ptr.h" + +class IFX_SeekableReadStream; + +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 std::array& GetRawBaseSha256() const { + return raw_base_sha256_; + } + + private: + CPDF_BaseDocument(); + ~CPDF_BaseDocument() override; + + bool CacheBaseIdentity(); + + 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 000000000..c1a637df1 --- /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 b3ebb9553..4674bc763 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 000000000..fe0cac734 --- /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 000000000..537040e8e --- /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 de3542774..fa9e44745 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 a6c8653bf..0f8b33a52 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 166ef94f8..e51bf4f8e 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" @@ -170,6 +171,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 +232,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 +266,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 +320,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 +345,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 +384,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 +420,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 +436,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 +474,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 +496,34 @@ bool CPDF_Document::IsModifiedAPStream(const CPDF_Stream* stream) const { pdfium::Contains(modified_apstream_ids_, stream->GetObjNum()); } +RetainPtr CPDF_Document::FindPromotedObject( + uint32_t objnum) const { + return nullptr; +} + +bool CPDF_Document::IsObjectPromoted(uint32_t objnum) const { + return !!FindPromotedObject(objnum); +} + +bool CPDF_Document::IsLayerDocument() const { + return false; +} + +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 +536,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 +567,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 +586,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) { @@ -594,7 +695,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,35 +704,43 @@ 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::GetMutableInfo() { + return GetInfo(); +} + RetainPtr CPDF_Document::GetOrCreateInfo() { - if (info_dict_) + if (info_dict_) { return info_dict_; + } // If parser already has an Info object, reuse it (this populates info_dict_). - if (RetainPtr existing = GetInfo()) + if (RetainPtr existing = GetInfo()) { 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) { @@ -655,22 +764,24 @@ 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)); } - if (pdfium::Contains(page_list_, page_obj_num)) { - return; + for (size_t i = 0; i < GetPageListSize(); ++i) { + if (GetPageObjNumAt(i) == page_obj_num) { + return; + } } // If `page_dict` is no longer in the page tree, replace it with an object of @@ -685,7 +796,7 @@ void CPDF_Document::SetPageToNullObject(uint32_t page_obj_num) { } 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 +880,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 48b01270c..0a02dfcea 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -94,10 +94,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 +112,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 +145,13 @@ class CPDF_Document : public Observable, // Returns whether CreateModifiedAPStream() created `stream`. bool IsModifiedAPStream(const CPDF_Stream* stream) const; + // 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; + virtual bool IsLayerDocument() const; + virtual FX_FILESIZE GetLayerAppendBaseOffset() const; + // CPDF_Parser::ParsedObjectsHolder: bool TryInit() override; RetainPtr ParseIndirectObject(uint32_t objnum) override; @@ -170,6 +178,19 @@ 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(); + private: class StockFontClearer { public: diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 6e02e258a..b9de488ec 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 46b9a7b40..51529e016 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 000000000..11b232118 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -0,0 +1,376 @@ +// 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_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()); + InitializeFromBase(); + IngestCurrentDelta(); +} + +CPDF_LayerDocument::~CPDF_LayerDocument() = default; + +// 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(); +} + +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() { + CPDF_Parser* parser = base_->GetParser(); + const uint32_t info_objnum = parser ? parser->GetInfoObjNum() : 0; + if (!info_objnum || info_objnum == CPDF_Object::kInvalidObjNum) { + return nullptr; + } + 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)); +} + +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); +} + +bool CPDF_LayerDocument::IsLayerDocument() const { + return true; +} + +FX_FILESIZE CPDF_LayerDocument::GetLayerAppendBaseOffset() const { + return base_->GetLayerAppendBaseOffset(); +} + +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); + } +} + +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)); + ++selected_delta_object_count; + } + + if (FindLocalIndirectObject(base_parser->GetRootObjNum())) { + InvalidateCachedRootDict(); + } + if (FindLocalIndirectObject(base_parser->GetInfoObjNum())) { + InvalidateCachedInfoDict(); + } + 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); + + 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 000000000..6d22237dc --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -0,0 +1,79 @@ +// 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; + CPDF_BaseDocument* GetBaseDocument() const { return base_.Get(); } + + // CPDF_Document: + CPDF_Parser* GetParser() const override; + RetainPtr GetMutableRoot() override; + RetainPtr GetMutableInfo() override; + RetainPtr GetPageDictionary(int iPage) override; + uint32_t GetUserPermissions(bool get_owner_perms) const override; + RetainPtr FindPromotedObject(uint32_t objnum) const override; + bool IsLayerDocument() const override; + FX_FILESIZE GetLayerAppendBaseOffset() 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_; + 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 000000000..614bd8b06 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -0,0 +1,428 @@ +// 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_page.h" +#include "core/fpdfapi/page/cpdf_pagemodule.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_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/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 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, 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); + ASSERT_TRUE(page); + RetainPtr resources = page->GetMutableResources(); + ASSERT_TRUE(resources); + EXPECT_EQ(1u, layer->GetPromotedObjectCount()); + + resources->SetNewFor("Tier3ResourceMarker", 5); + EXPECT_EQ(5, page->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 8fd027aa4..95204b943 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 cd5a9ed53..7179d0dd0 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 1f901acd0..81c02a882 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 8de91f644..a954b6473 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 30dcb1108..be440b584 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 613a1d750..1a3168d6c 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 ecfd50f23..0c4301b3f 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 000000000..61e3c99bd --- /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 000000000..ee3cae76e --- /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 000000000..7e761faf9 --- /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 c8665416e..8056b508b 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 8dc56d528..06e4d35cf 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 63baff1b0..b0828a0f9 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_stream.cpp b/core/fpdfapi/parser/cpdf_stream.cpp index d6700e8b3..057404aff 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 ffeea79ca..b291a724d 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 8bdba046c..2b4553d79 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 749e59c07..f765a10ea 100644 --- a/core/fpdfapi/parser/cpdf_syntax_parser.cpp +++ b/core/fpdfapi/parser/cpdf_syntax_parser.cpp @@ -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 79fc68d63..a3c27a934 100644 --- a/core/fpdfapi/parser/cpdf_syntax_parser.h +++ b/core/fpdfapi/parser/cpdf_syntax_parser.h @@ -70,6 +70,7 @@ class CPDF_SyntaxParser { ByteString PeekNextWord(); RetainPtr GetValidator() const; + RetainPtr GetFileAccess() const; uint32_t GetDirectNum(); bool GetNextChar(uint8_t& ch); diff --git a/core/fpdfapi/parser/object_tree_traversal_util.cpp b/core/fpdfapi/parser/object_tree_traversal_util.cpp index eebcff1e4..331437d00 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 e9db96dce..1ef6f0624 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 c35e952d7..100d9199a 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 f11be0cae..691a97c75 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 6f8ec8e54..8494f1c61 100644 --- a/core/fpdfapi/render/cpdf_renderstatus.cpp +++ b/core/fpdfapi/render/cpdf_renderstatus.cpp @@ -1438,7 +1438,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 94ad43f0c..f258efae3 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -106,6 +106,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 1d40c9e63..3d8926c83 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 1071f28f9..9285f1b6a 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 5e2f582b3..547c025ff 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,51 @@ 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)); + EXPECT_EQ(CFX_FloatRect(-2, -2, 12, 12), annot.GetRect()); +} diff --git a/core/fpdfdoc/cpdf_annotlist.cpp b/core/fpdfdoc/cpdf_annotlist.cpp index db8be1603..8b53e3039 100644 --- a/core/fpdfdoc/cpdf_annotlist.cpp +++ b/core/fpdfdoc/cpdf_annotlist.cpp @@ -13,7 +13,6 @@ #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" @@ -26,9 +25,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 +121,33 @@ 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(); + 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(); diff --git a/core/fpdfdoc/cpdf_annotlist_unittest.cpp b/core/fpdfdoc/cpdf_annotlist_unittest.cpp index a59f9595d..2d7260265 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_formcontrol.cpp b/core/fpdfdoc/cpdf_formcontrol.cpp index 814a1fa97..2a5187a52 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 00a54f39c..220941d37 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 f1c42bd87..23ab5f077 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" @@ -41,9 +43,9 @@ #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_renderdevice.h" -#include "core/fxcrt/fx_system.h" namespace { @@ -67,45 +69,49 @@ 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 << " " + out << "q " << cos_a << " " << sin_a << " " << -sin_a << " " << cos_a << " " << pos.x << " " << pos.y << " cm\n"; emitter(); out << "Q\n"; @@ -118,22 +124,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 +146,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 +174,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 +219,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, @@ -406,9 +439,9 @@ AnnotationDimensionsAndColor GetAnnotationDimensionsAndColor( // Rotation info for shape annotations (Square, Circle) using EmbedPDF's // custom /EPDFRotate and /EPDFUnrotatedRect entries. 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) { @@ -440,10 +473,9 @@ ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict) { // 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; } @@ -761,7 +793,7 @@ inline CPDF_Annot::VerticalAlignment GetVerticalAlign( annot_dict ? annot_dict->GetIntegerFor("EPDF:VerticalAlignment") : 0; 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 +860,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 +896,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 +921,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 +951,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 +965,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 +1011,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 +1021,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 +1056,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 @@ -1055,72 +1143,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); - auto normal_stream = doc->NewIndirect(std::move(stream_dict)); - normal_stream->SetDataFromStringstream(app_stream); + target->normal_stream = + target->IsPersistent() + ? target->doc->NewIndirect(std::move(stream_dict)) + : pdfium::MakeRetain(std::move(stream_dict)); + target->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)); - - auto normal_stream = doc->NewIndirect(std::move(stream_dict)); - normal_stream->SetDataFromStringstream(app_stream); - - RetainPtr ap_dict = - annot_dict->GetOrCreateDictFor(pdfium::annotation::kAP); - ap_dict->SetNewFor("N", doc, normal_stream->GetObjNum()); +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); +} + +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); +} + +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 +1280,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; @@ -1388,7 +1533,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 +1567,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 +1592,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 +1688,15 @@ 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); // (c) Border width and colors. const float border_w = GetBorderWidth(annot_dict); @@ -1546,8 +1708,9 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B appearance_stream << GenerateColorAP(fill, 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 @@ -1560,10 +1723,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); @@ -1682,10 +1845,10 @@ bool GenerateFreeTextAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const B // Finalize AP dict. auto graphics_state_dict = GenerateExtGStateDict(*annot_dict, blend_name); auto resource_font_dict = - GenerateResourceFontDict(doc, font_name, font_dict->GetObjNum()); + GenerateResourceFontDict(doc, font_name, font_dict.Get()); 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 { @@ -1761,15 +1924,15 @@ 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()); + GenerateResourceFontDict(doc, font_name, font_dict.Get()); 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); } @@ -1777,7 +1940,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 "; @@ -1801,35 +1966,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; @@ -1846,32 +2012,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)}); @@ -1883,12 +2054,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) { @@ -1896,27 +2067,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) { @@ -1929,12 +2102,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) { @@ -1953,13 +2126,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; @@ -1988,7 +2164,9 @@ bool GenerateInkAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteSt // 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); + if (target->IsPersistent()) { + annot_dict->SetRectFor(pdfium::annotation::kRect, rect); + } for (size_t i = 0; i < ink_list->size(); i++) { RetainPtr coordinates_array = ink_list->GetArrayAt(i); @@ -2011,13 +2189,22 @@ bool GenerateInkAP(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const ByteSt } 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 "; @@ -2036,7 +2223,9 @@ 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 GenerateUnderlineAP(APGenerationTarget* target, + CPDF_Dictionary* annot_dict, + const ByteString& blend_name) { fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; @@ -2060,13 +2249,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"; @@ -2107,7 +2300,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 "; @@ -2142,8 +2337,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 "; } @@ -2152,21 +2348,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 "; @@ -2210,13 +2409,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 "; @@ -2241,8 +2444,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; } @@ -2252,8 +2457,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(); @@ -2271,7 +2477,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: { @@ -2320,7 +2527,7 @@ void GenerateRedactAPDicts(CPDF_Document* doc, normal_stream_dict->SetRectFor("BBox", rect); normal_stream_dict->SetFor("Resources", resource_dict->Clone()); - auto normal_pdf_stream = + auto normal_pdf_stream = doc->NewIndirect(std::move(normal_stream_dict)); normal_pdf_stream->SetDataFromStringstream(normal_stream); @@ -2334,7 +2541,7 @@ void GenerateRedactAPDicts(CPDF_Document* doc, rollover_stream_dict->SetRectFor("BBox", rect); rollover_stream_dict->SetFor("Resources", resource_dict->Clone()); - auto rollover_pdf_stream = + auto rollover_pdf_stream = doc->NewIndirect(std::move(rollover_stream_dict)); rollover_pdf_stream->SetDataFromStringstream(rollover_stream); @@ -2348,13 +2555,13 @@ void GenerateRedactAPDicts(CPDF_Document* doc, 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 + // 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, +bool GenerateRedactAP(CPDF_Document* doc, + CPDF_Dictionary* annot_dict, const ByteString& blend_name) { fxcrt::ostringstream normal_stream; fxcrt::ostringstream rollover_stream; @@ -2364,7 +2571,7 @@ bool GenerateRedactAP(CPDF_Document* doc, // 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 = + 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(); @@ -2410,10 +2617,10 @@ bool GenerateRedactAP(CPDF_Document* doc, // 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"; + 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"; } } } else { @@ -2432,8 +2639,8 @@ bool GenerateRedactAP(CPDF_Document* doc, // 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"; + rollover_stream << rect.left << " " << rect.bottom << " " << rect.Width() + << " " << rect.Height() << " re f\n"; } } @@ -2447,14 +2654,13 @@ bool GenerateRedactAP(CPDF_Document* doc, resources_dict, has_quad_points); 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 AppearanceCharacteristics mk = GetAppearanceCharacteristics(annot_dict->GetDictFor("MK")); const bool has_bg = @@ -2511,19 +2717,18 @@ 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 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"; @@ -2542,19 +2747,18 @@ void GenerateComboBoxFormAP( da_info.font_size, provider); } -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"; @@ -2620,8 +2824,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"; } @@ -2670,73 +2874,113 @@ 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) { + 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"); if (!form_dict) { - return; + return false; } std::optional default_appearance_info = - GetDefaultAppearanceInfo(annot_dict, form_dict); + GetDefaultAppearanceInfo(annot_dict, form_dict.Get()); if (!default_appearance_info.has_value()) { - return; + return false; } - RetainPtr dr_dict = form_dict->GetMutableDictFor("DR"); + RetainPtr dr_dict = form_dict->GetDictFor("DR"); if (!dr_dict) { - return; + 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; + 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) { - return; + return false; } 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)); } + 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); @@ -2757,18 +3001,48 @@ void CPDF_GenerateAP::GenerateFormAP(CPDF_Document* doc, break; } + 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); +} + +// 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)) { + return std::nullopt; + } + return GeneratedAP{std::move(target.normal_stream)}; } // static @@ -2815,11 +3089,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); @@ -2975,14 +3249,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); @@ -3002,7 +3276,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 "; @@ -3038,8 +3314,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) << " " @@ -3055,6 +3331,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, @@ -3068,34 +3375,19 @@ 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::LINK: return GenerateLinkAP(doc, annot_dict, blend_name); case CPDF_Annot::Subtype::REDACT: @@ -3107,6 +3399,56 @@ 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 bool CPDF_GenerateAP::GenerateDefaultAppearanceWithColor( CPDF_Document* doc, @@ -3130,6 +3472,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); @@ -3150,20 +3499,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 @@ -3173,8 +3520,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(); diff --git a/core/fpdfdoc/cpdf_generateap.h b/core/fpdfdoc/cpdf_generateap.h index 865315e04..c0b4cc1f4 100644 --- a/core/fpdfdoc/cpdf_generateap.h +++ b/core/fpdfdoc/cpdf_generateap.h @@ -7,6 +7,8 @@ #ifndef CORE_FPDFDOC_CPDF_GENERATEAP_H_ #define CORE_FPDFDOC_CPDF_GENERATEAP_H_ +#include + #include "core/fpdfdoc/cpdf_annot.h" class CPDF_Dictionary; @@ -39,6 +41,28 @@ 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); + static bool GenerateDefaultAppearanceWithColor(CPDF_Document* doc, CPDF_Dictionary* annot_dict, const CFX_Color& color); diff --git a/core/fpdfdoc/cpdf_generateap_unittest.cpp b/core/fpdfdoc/cpdf_generateap_unittest.cpp new file mode 100644 index 000000000..635e1533c --- /dev/null +++ b/core/fpdfdoc/cpdf_generateap_unittest.cpp @@ -0,0 +1,276 @@ +// 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)); + EXPECT_EQ(CFX_FloatRect(-2, -2, 12, 12), + 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 24bf93ad4..4f85fde2d 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,8 +856,9 @@ CPDF_InteractiveForm::GetControlsForField(const CPDF_FormField* field) { return control_lists_[pdfium::WrapUnowned(field)]; } -void CPDF_InteractiveForm::LoadField(RetainPtr field_dict, - int nLevel) { +void CPDF_InteractiveForm::LoadField( + RetainPtr field_dict, + int nLevel) { if (nLevel > kMaxRecursion) { return; } @@ -861,8 +867,8 @@ void CPDF_InteractiveForm::LoadField(RetainPtr 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,21 +885,22 @@ 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); } @@ -901,81 +908,59 @@ void CPDF_InteractiveForm::FixPageFields(CPDF_Page* page) { } 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)) { + 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) { + RetainPtr kid = 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_dict.Get()); + WideString field_name = + CPDF_FormField::GetFullNameForDict(field_storage_dict.Get()); if (field_name.IsEmpty()) { 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; - } - } - - 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 (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()); - } - } - } - - auto new_field = std::make_unique(this, std::move(pParent)); + 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); + RetainPtr kid = kids->GetDictAt(i); if (kid && kid->GetNameFor("Subtype") == "Widget") { - AddControl(field, std::move(kid)); + AddControl(field, + pdfium::WrapRetain(const_cast(kid.Get()))); } } } diff --git a/core/fpdfdoc/cpdf_interactiveform.h b/core/fpdfdoc/cpdf_interactiveform.h index 33c09170e..3de7645fd 100644 --- a/core/fpdfdoc/cpdf_interactiveform.h +++ b/core/fpdfdoc/cpdf_interactiveform.h @@ -108,8 +108,8 @@ class CPDF_InteractiveForm { CPDF_Document* document() { return document_; } private: - void LoadField(RetainPtr field_dict, int nLevel); - void AddTerminalField(RetainPtr field_dict); + 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 986668738..405497b07 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 c80270e7f..a7296bc7c 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,8 @@ 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) { uint32_t objnum = pPage->GetDict()->GetObjNum(); if (objnum == 0) { return nullptr; @@ -60,13 +61,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 b259aaf88..d9d125774 100644 --- a/core/fpdfdoc/cpdf_linklist.h +++ b/core/fpdfdoc/cpdf_linklist.h @@ -29,9 +29,10 @@ 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_; + 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 b4f65c7a2..967a300e4 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 a6505a65c..de6b4043f 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/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index b903c2b31..6132162e3 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -7,9 +7,14 @@ import("../testing/test.gni") source_set("fpdfsdk") { sources = [ + "epdf_base_document.cpp", + "epdf_layer.cpp", "epdf_outline.cpp", + "epdf_page_content_helpers.cpp", + "epdf_page_content_helpers.h", "epdf_png_shim.cpp", "epdf_jpeg_shim.cpp", + "epdf_redact.cpp", "cpdfsdk_annot.cpp", "cpdfsdk_annot.h", "cpdfsdk_annotiteration.cpp", diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp index bf887c579..5c91858b6 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_pageview.cpp b/fpdfsdk/cpdfsdk_pageview.cpp index 339201cae..c4e8bc395 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 4def06a07..39ef4d1cb 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_base_document.cpp b/fpdfsdk/epdf_base_document.cpp new file mode 100644 index 000000000..2f62be0c2 --- /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_layer.cpp b/fpdfsdk/epdf_layer.cpp new file mode 100644 index 000000000..56fb0856a --- /dev/null +++ b/fpdfsdk/epdf_layer.cpp @@ -0,0 +1,473 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "public/fpdfview.h" + +#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; + }; + } +}; + +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))); + } +} + +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 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; + artifact.reserve(kLayerArtifactHeaderSize + delta_writer.data.size()); + 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, static_cast(delta_writer.data.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()); + 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 000000000..857a65f54 --- /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 000000000..bf49df510 --- /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_redact.cpp b/fpdfsdk/epdf_redact.cpp new file mode 100644 index 000000000..d02504302 --- /dev/null +++ b/fpdfsdk/epdf_redact.cpp @@ -0,0 +1,613 @@ +// 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 + +#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_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; + ByteString nm_utf8; + RetainPtr dict; +}; + +struct RedactionReportBuffers { + EPDF_RemovedAnnotInfo* removed = nullptr; + uint32_t removed_capacity = 0; + char* nm_utf8_pool = nullptr; + uint32_t nm_utf8_pool_capacity = 0; + uint32_t* written_count = nullptr; + uint32_t* total_count = nullptr; + uint32_t* nm_utf8_bytes_used = nullptr; +}; + +uint32_t GetAnnotObjectNumber(const CPDF_Object* entry, + const CPDF_Dictionary* dict) { + if (entry && entry->IsReference()) { + return entry->AsReference()->GetRefObjNum(); + } + return dict ? dict->GetObjNum() : 0; +} + +ByteString GetAnnotNMUtf8(const CPDF_Dictionary* dict) { + if (!dict || !dict->KeyExist("NM")) { + return ByteString(); + } + return dict->GetUnicodeTextFor("NM").ToUTF8(); +} + +bool RectsIntersectWithPositiveArea(CFX_FloatRect a, CFX_FloatRect b) { + a.Normalize(); + b.Normalize(); + 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.nm_utf8 = GetAnnotNMUtf8(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); + } +} + +void WriteRemovalReport(const std::vector& candidates, + const RedactionReportBuffers* report) { + if (!report) { + return; + } + + const uint32_t total = + pdfium::checked_cast(candidates.size()); + uint32_t written = 0; + uint32_t nm_bytes_used = 0; + const uint32_t capacity = report->removed ? report->removed_capacity : 0; + const uint32_t limit = std::min(total, capacity); + + for (; written < limit; ++written) { + const RemovedAnnotCandidate& candidate = candidates[written]; + EPDF_RemovedAnnotInfo& out = report->removed[written]; + out.object_number = candidate.object_number; + out.index_at_removal = + pdfium::checked_cast(candidate.index); + out.nm_utf8_offset = 0; + out.nm_utf8_len = 0; + + const uint32_t nm_len = + pdfium::checked_cast(candidate.nm_utf8.GetLength()); + if (nm_len == 0) { + continue; + } + if (!report->nm_utf8_pool || + nm_len > report->nm_utf8_pool_capacity - nm_bytes_used) { + out.nm_utf8_len = EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW; + continue; + } + + out.nm_utf8_offset = nm_bytes_used; + out.nm_utf8_len = nm_len; + memcpy(report->nm_utf8_pool + nm_bytes_used, candidate.nm_utf8.c_str(), + nm_len); + nm_bytes_used += nm_len; + } + + if (report->written_count) { + *report->written_count = written; + } + if (report->total_count) { + *report->total_count = total; + } + if (report->nm_utf8_bytes_used) { + *report->nm_utf8_bytes_used = nm_bytes_used; + } +} + +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; }); +} + +bool ApplySingleRedactionCore(CPDF_Page* page, + const CPDF_Dictionary* redact_dict, + const RedactionReportBuffers* report) { + if (!page || !redact_dict) { + return false; + } + + 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 ro_stream = redact_dict->GetStreamFor("RO"); + if (ro_stream) { + CFX_FloatRect annot_rect = + redact_dict->GetRectFor(pdfium::annotation::kRect); + annot_rect.Normalize(); + EpdfAppendFormXObjectToPage(page, ro_stream, annot_rect); + } + + DetachWidgetsFromAcroForm(page, removals); + WriteRemovalReport(removals, report); + RemoveCandidatesFromPage(page, removals); + return true; +} + +bool ApplyAllRedactionsCore(CPDF_Page* page, + const RedactionReportBuffers* report) { + if (!page) { + return false; + } + + RetainPtr annots = page->GetMutableAnnotsArray(); + if (!annots || annots->IsEmpty()) { + return false; + } + + std::vector all_rects; + std::vector, CFX_FloatRect>> + ro_streams; + 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 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()) { + 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& [ro_stream, annot_rect] : ro_streams) { + EpdfAppendFormXObjectToPage(page, ro_stream, annot_rect); + } + + DetachWidgetsFromAcroForm(page, removals); + WriteRemovalReport(removals, report); + RemoveCandidatesFromPage(page, removals); + return true; +} + +} // namespace + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { + return EPDFAnnot_ApplyRedactionWithReport( + page, annot, nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ApplyRedactionWithReport( + FPDF_PAGE page, + FPDF_ANNOTATION annot, + EPDF_RemovedAnnotInfo* out_removed, + uint32_t out_removed_capacity, + char* nm_utf8_pool, + uint32_t nm_utf8_pool_capacity, + uint32_t* out_written_count, + uint32_t* out_total_count, + uint32_t* out_nm_utf8_bytes_used) { + if (out_written_count) { + *out_written_count = 0; + } + if (out_total_count) { + *out_total_count = 0; + } + if (out_nm_utf8_bytes_used) { + *out_nm_utf8_bytes_used = 0; + } + + 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; + } + + RedactionReportBuffers report = { + out_removed, + out_removed_capacity, + nm_utf8_pool, + nm_utf8_pool_capacity, + out_written_count, + out_total_count, + out_nm_utf8_bytes_used, + }; + return ApplySingleRedactionCore(pPage, annot_dict, &report); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_ApplyRedactions(FPDF_PAGE page) { + return EPDFPage_ApplyRedactionsWithReport(page, nullptr, 0, nullptr, 0, + nullptr, nullptr, nullptr); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_ApplyRedactionsWithReport( + FPDF_PAGE page, + EPDF_RemovedAnnotInfo* out_removed, + uint32_t out_removed_capacity, + char* nm_utf8_pool, + uint32_t nm_utf8_pool_capacity, + uint32_t* out_written_count, + uint32_t* out_total_count, + uint32_t* out_nm_utf8_bytes_used) { + if (out_written_count) { + *out_written_count = 0; + } + if (out_total_count) { + *out_total_count = 0; + } + if (out_nm_utf8_bytes_used) { + *out_nm_utf8_bytes_used = 0; + } + + CPDF_Page* pPage = CPDFPageFromFPDFPage(page); + if (!pPage) { + return false; + } + + RedactionReportBuffers report = { + out_removed, + out_removed_capacity, + nm_utf8_pool, + nm_utf8_pool_capacity, + out_written_count, + out_total_count, + out_nm_utf8_bytes_used, + }; + return ApplyAllRedactionsCore(pPage, &report); +} diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 5603622e1..2d8d1c253 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,14 +16,13 @@ #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" @@ -53,6 +53,7 @@ #include "fpdfsdk/cpdfsdk_formfillenvironment.h" #include "fpdfsdk/cpdfsdk_helpers.h" #include "fpdfsdk/cpdfsdk_interactiveform.h" +#include "fpdfsdk/epdf_page_content_helpers.h" namespace { @@ -213,57 +214,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 +293,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 +484,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 +500,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 +574,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,12 +636,14 @@ 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) + if (!annot_dict) { return BlendMode::kNormal; + } // Get (or detect absence of) normal appearance stream. RetainPtr ap_stream = @@ -645,27 +652,32 @@ BlendMode GetEffectiveAnnotBlendMode(CPDF_AnnotContext* ctx) { // 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; } @@ -685,8 +697,9 @@ RetainPtr GetMutableAnnotDictFromFPDFAnnotation( 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; } @@ -840,7 +853,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 +872,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 +930,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 +952,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 +962,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 +984,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 +1000,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 +1036,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 @@ -1068,24 +1103,26 @@ 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); + const CPDF_Page* pPage = CPDFPageFromFPDFPage(page); if (!pPage || index < 0) { return nullptr; } - 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()); @@ -1397,8 +1434,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 +1443,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; } @@ -1897,8 +1933,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 +1945,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(), @@ -2173,19 +2208,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 @@ -2369,26 +2400,31 @@ 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(); @@ -2403,14 +2439,14 @@ FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot) { return nullptr; } - RetainPtr annot_dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); 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 +2492,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 +2510,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 +2525,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 +2547,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 +2561,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 +2574,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 +2592,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 +2608,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 +2643,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 +2669,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 +2687,9 @@ EPDFAnnot_ClearBorderEffect(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("BE"); return true; @@ -2688,10 +2736,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,8 +2772,9 @@ EPDFAnnot_ClearRectangleDifferences(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("RD"); return true; @@ -2743,10 +2792,10 @@ EPDFAnnot_GetBorderDashPatternCount(FPDF_ANNOTATION annot) { if (!pBSDict) { return 0; } - + // The border style must be dashed. if (pBSDict->GetNameFor("S") != "D") { - return 0; + return 0; } // The dash pattern is defined by the /D array. @@ -2788,25 +2837,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 +2867,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 +2885,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; } @@ -2843,8 +2900,10 @@ EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width) { return static_cast( CPDF_Annot::StringToBorderStyle(pBSDict->GetNameFor("S"))); } - - if (width) *width = 0; + + if (width) { + *width = 0; + } return FPDF_ANNOT_BS_UNKNOWN; } @@ -2915,16 +2974,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 +3001,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 +3012,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 +3042,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 +3066,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 +3096,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 +3123,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 +3154,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 +3200,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 +3222,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 +3242,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 +3258,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 +3309,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 +3322,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; @@ -3264,10 +3357,10 @@ EPDFAnnot_GetDefaultAppearance(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; } @@ -3286,9 +3379,10 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, CPDF_DefaultAppearance da(annot_dict.Get(), 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 +3413,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 +3428,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; @@ -3373,7 +3470,8 @@ EPDFAnnot_GetTextAlignment(FPDF_ANNOTATION annot) { } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, FPDF_VERTICAL_ALIGNMENT alignment) { +EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, + FPDF_VERTICAL_ALIGNMENT alignment) { RetainPtr annot_dict = GetMutableAnnotDictFromFPDFAnnotation(annot); if (!annot_dict) { @@ -3386,20 +3484,22 @@ EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, FPDF_VERTICAL_ALIGNMENT al } // Validate the enum range to ensure a valid value is passed. - if (alignment < FPDF_VERTICAL_ALIGNMENT_TOP || alignment > FPDF_VERTICAL_ALIGNMENT_BOTTOM) { + 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)); + // 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); + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); if (!annot_dict) { return FPDF_VERTICAL_ALIGNMENT_TOP; } @@ -3424,21 +3524,30 @@ EPDFAnnot_GetVerticalAlignment(FPDF_ANNOTATION annot) { 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; + if (!page || !nm || !*nm) { + return nullptr; + } + const CPDF_Page* pPage = CPDFPageFromFPDFPage(page); + if (!pPage) { + return nullptr; + } - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (!annots) return nullptr; + RetainPtr annots = pPage->GetAnnotsArray(); + 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 +3556,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 +3579,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 +3597,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 +3610,66 @@ 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; + } 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; } @@ -3547,19 +3682,23 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { return nullptr; } + 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 +3708,33 @@ 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 +3754,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,15 +3820,18 @@ 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"); @@ -3691,11 +3842,13 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { 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 +3856,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 +3880,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 +3939,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 +3953,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 +3993,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 +4024,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 +4049,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 +4074,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 +4091,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,280 +4111,71 @@ 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) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + 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) +int GetAnnotIndexOnPage(const CPDF_Page* page, + const CPDF_Dictionary* annot_dict) { + if (!page || !annot_dict) { return -1; + } - RetainPtr annots = page->GetMutableAnnotsArray(); - if (!annots) + RetainPtr annots = page->GetAnnotsArray(); + if (!annots) { return -1; + } for (size_t i = 0; i < annots->size(); ++i) { - if (annots->GetDictAt(i) == annot_dict) + 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) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_Flatten(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 (!pPage) { 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) + 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) + if (!ap_dict) { return false; + } RetainPtr ap_stream = ap_dict->GetStreamFor("N"); - if (!ap_stream) + if (!ap_stream) { return false; + } CFX_FloatRect annot_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); annot_rect.Normalize(); - FlattenFormXObjectToPage(pPage, ap_stream, annot_rect); + EpdfAppendFormXObjectToPage(pPage, ap_stream, annot_rect); // Remove the annotation from the page int annot_index = GetAnnotIndexOnPage(pPage, annot_dict); @@ -4229,44 +4194,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 +4246,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 +4271,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 +4321,22 @@ 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) + if (!annot_dict || !src_doc) { return nullptr; + } RetainPtr ap_stream = GetAnnotAP(annot_dict.Get(), CPDF_Annot::AppearanceMode::kNormal); - if (!ap_stream) + if (!ap_stream) { return nullptr; + } CFX_FloatRect bbox = ap_stream->GetDict()->GetRectFor("BBox"); bbox.Normalize(); @@ -4364,17 +4344,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 +4372,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 +4388,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 +4415,21 @@ 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; + } struct AnnotInfo { RetainPtr ap_stream; @@ -4458,22 +4445,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) + if (!annot_dict) { return nullptr; + } RetainPtr ap_stream = GetAnnotAP(annot_dict.Get(), CPDF_Annot::AppearanceMode::kNormal); - if (!ap_stream) + if (!ap_stream) { return nullptr; + } CFX_FloatRect ap_bbox = ap_stream->GetDict()->GetRectFor("BBox"); ap_bbox.Normalize(); @@ -4481,14 +4472,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 +4495,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 +4538,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 +4546,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), @@ -4579,8 +4573,9 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation) { RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } if (rotation == 0.0f) { dict->RemoveFor("EPDFRotate"); @@ -4592,12 +4587,14 @@ EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetExtendedRotation(FPDF_ANNOTATION annot, float* rotation) { - if (!rotation) + if (!rotation) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + if (!dict) { return false; + } *rotation = dict->GetFloatFor("EPDFRotate"); return true; @@ -4607,8 +4604,9 @@ 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"); @@ -4626,21 +4624,24 @@ EPDFAnnot_SetUnrotatedRect(FPDF_ANNOTATION annot, const FS_RECTF* rect) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect) { - if (!rect) + if (!rect) { return false; + } const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) + 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)) +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, @@ -4655,32 +4656,37 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAPMatrix(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode, const 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; + } RetainPtr dict = GetMutableAnnotDictFromFPDFAnnotation(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->GetMutableDictFor(pdfium::annotation::kAP); - if (!ap) + 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 +4696,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 +4733,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 +4779,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 +4807,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 +4843,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()); @@ -4844,12 +4864,14 @@ EPDFPage_CreateFormField(FPDF_PAGE page, int field_type, FPDF_WIDESTRING field_name) { CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return nullptr; + } CPDFSDK_InteractiveForm* pSDKForm = FormHandleToInteractiveForm(handle); - if (!pSDKForm) + if (!pSDKForm) { return nullptr; + } // Validate field_type switch (field_type) { @@ -4896,14 +4918,16 @@ EPDFPage_CreateFormField(FPDF_PAGE page, // Create the parent field dictionary (indirect) RetainPtr pFieldDict = pDoc->NewIndirect(); pFieldDict->SetNewFor("FT", ft_value); - if (base_flags != 0) + 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()) + if (!ws_name.IsEmpty()) { pFieldDict->SetNewFor("T", ws_name.ToUTF8()); + } } // Create the widget annotation dictionary (indirect) @@ -4912,7 +4936,8 @@ EPDFPage_CreateFormField(FPDF_PAGE page, pAnnotDict->SetNewFor(pdfium::annotation::kSubtype, "Widget"); // Link widget -> parent via /Parent - pAnnotDict->SetNewFor("Parent", pDoc, pFieldDict->GetObjNum()); + pAnnotDict->SetNewFor("Parent", pDoc, + pFieldDict->GetObjNum()); // Link parent -> widget via /Kids RetainPtr pKids = pFieldDict->SetNewFor("Kids"); @@ -4920,8 +4945,9 @@ EPDFPage_CreateFormField(FPDF_PAGE page, // Ensure /AcroForm exists on document root RetainPtr pRoot = pDoc->GetMutableRoot(); - if (!pRoot) + if (!pRoot) { return nullptr; + } RetainPtr pAcroForm = pRoot->GetOrCreateDictFor("AcroForm"); @@ -4946,16 +4972,19 @@ EPDFPage_CreateFormField(FPDF_PAGE page, 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 +4998,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; @@ -5017,17 +5047,20 @@ EPDFAnnot_GetButtonExportValue(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen) { const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return 0; + } RetainPtr pAP = pAnnotDict->GetDictFor(pdfium::annotation::kAP); - if (!pAP) + if (!pAP) { return 0; + } RetainPtr pN = pAP->GetDictFor("N"); - if (!pN) + if (!pN) { return 0; + } ByteString on_state; CPDF_DictionaryLocker locker(pN); @@ -5038,8 +5071,9 @@ EPDFAnnot_GetButtonExportValue(FPDF_ANNOTATION annot, } } - if (on_state.IsEmpty()) + if (on_state.IsEmpty()) { return 0; + } return Utf16EncodeMaybeCopyAndReturnLength( WideString::FromUTF8(on_state.AsStringView()), @@ -5066,8 +5100,9 @@ EPDFAnnot_SetFormFieldValue(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot, FPDF_WIDESTRING value) { CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) + if (!pFormField) { return false; + } return pFormField->SetValue(WideStringFromFPDFWideString(value), NotificationOption::kDoNotNotify); @@ -5078,14 +5113,15 @@ EPDFAnnot_SetFormFieldName(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot, FPDF_WIDESTRING name) { CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) + if (!pFormField) { return false; + } - RetainPtr pFieldDict = - pdfium::WrapRetain(const_cast( - pFormField->GetFieldDict().Get())); - if (!pFieldDict) + RetainPtr pFieldDict = pdfium::WrapRetain( + const_cast(pFormField->GetFieldDict().Get())); + if (!pFieldDict) { return false; + } WideString ws_name = WideStringFromFPDFWideString(name); pFieldDict->SetNewFor("T", ws_name.ToUTF8()); @@ -5093,8 +5129,10 @@ EPDFAnnot_SetFormFieldName(FPDF_FORMHANDLE handle, } FPDF_EXPORT int FPDF_CALLCONV -EPDFAnnot_GetFormFieldObjectNumber(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot) { - RetainPtr pFieldDict = GetMutableFieldDict(GetFormField(handle, annot)); +EPDFAnnot_GetFormFieldObjectNumber(FPDF_FORMHANDLE handle, + FPDF_ANNOTATION annot) { + RetainPtr pFieldDict = + GetMutableFieldDict(GetFormField(handle, annot)); if (!pFieldDict) { return 0; } @@ -5117,8 +5155,10 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, return false; } - RetainPtr pSourceFieldDict = GetMutableFieldDict(pSourceField); - RetainPtr pTargetFieldDict = GetMutableFieldDict(pTargetField); + RetainPtr pSourceFieldDict = + GetMutableFieldDict(pSourceField); + RetainPtr pTargetFieldDict = + GetMutableFieldDict(pTargetField); if (!pSourceFieldDict || !pTargetFieldDict) { return false; } @@ -5131,8 +5171,10 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, return false; } - CPDF_AnnotContext* pSourceContext = CPDFAnnotContextFromFPDFAnnotation(source_annot); - CPDF_AnnotContext* pTargetContext = CPDFAnnotContextFromFPDFAnnotation(target_annot); + CPDF_AnnotContext* pSourceContext = + CPDFAnnotContextFromFPDFAnnotation(source_annot); + CPDF_AnnotContext* pTargetContext = + CPDFAnnotContextFromFPDFAnnotation(target_annot); if (!pSourceContext || !pTargetContext) { return false; } @@ -5148,8 +5190,10 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, return false; } - RetainPtr pSourceKids = pSourceFieldDict->GetMutableArrayFor("Kids"); - RetainPtr pTargetKids = pTargetFieldDict->GetOrCreateArrayFor("Kids"); + RetainPtr pSourceKids = + pSourceFieldDict->GetMutableArrayFor("Kids"); + RetainPtr pTargetKids = + pTargetFieldDict->GetOrCreateArrayFor("Kids"); if (!pSourceKids || !pTargetKids) { return false; } @@ -5160,9 +5204,11 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, continue; } - pKidDict->SetNewFor("Parent", pDoc, pTargetFieldDict->GetObjNum()); + pKidDict->SetNewFor("Parent", pDoc, + pTargetFieldDict->GetObjNum()); - if (!ArrayContainsDictWithObjNum(pTargetKids.Get(), pKidDict->GetObjNum())) { + if (!ArrayContainsDictWithObjNum(pTargetKids.Get(), + pKidDict->GetObjNum())) { pTargetKids->AppendNew(pDoc, pKidDict->GetObjNum()); } } @@ -5175,7 +5221,8 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, if (pAcroForm) { RetainPtr pFields = pAcroForm->GetMutableArrayFor("Fields"); if (pFields) { - RemoveDictWithObjNumFromArray(pFields.Get(), pSourceFieldDict->GetObjNum()); + RemoveDictWithObjNumFromArray(pFields.Get(), + pSourceFieldDict->GetObjNum()); } } } @@ -5195,23 +5242,28 @@ EPDFAnnot_ShareFormField(FPDF_FORMHANDLE handle, 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 +5271,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 +5309,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 +5325,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) { @@ -5289,18 +5350,20 @@ EPDFAnnot_SetFormFieldOptions(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot, const FPDF_WIDESTRING* labels, int count) { - if (count < 0 || (count > 0 && !labels)) + if (count < 0 || (count > 0 && !labels)) { return false; + } CPDF_FormField* pFormField = GetFormField(handle, annot); - if (!pFormField) + if (!pFormField) { return false; + } - RetainPtr pFieldDict = - pdfium::WrapRetain(const_cast( - pFormField->GetFieldDict().Get())); - if (!pFieldDict) + 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++) { @@ -5313,30 +5376,37 @@ EPDFAnnot_SetFormFieldOptions(FPDF_FORMHANDLE handle, 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) + const CPDF_Page* pPage = CPDFPageFromFPDFPage(page); + if (!pPage) { return nullptr; + } - 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 +5416,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 +5441,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 +5478,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 +5507,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 +5534,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 +5572,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 d23c3e35f..19ad4ec52 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -16,6 +18,8 @@ #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_read_only_graph_guard.h" #include "core/fxcrt/compiler_specific.h" #include "core/fxcrt/containers/contains.h" #include "core/fxcrt/fx_memcpy_wrappers.h" @@ -27,6 +31,7 @@ #include "public/fpdf_attachment.h" #include "public/fpdf_edit.h" #include "public/fpdf_formfill.h" +#include "public/fpdf_text.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" @@ -46,6 +51,58 @@ 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()); +} + +struct RedactionReport { + std::vector object_numbers; + uint32_t written_count = 0; + uint32_t total_count = 0; + uint32_t nm_utf8_bytes_used = 0; +}; + +RedactionReport ApplyRedactionWithReport(FPDF_PAGE page, + FPDF_ANNOTATION annot) { + std::array removed = {}; + std::array nm_utf8_pool = {}; + RedactionReport report; + + EXPECT_TRUE(EPDFAnnot_ApplyRedactionWithReport( + page, annot, removed.data(), removed.size(), nm_utf8_pool.data(), + nm_utf8_pool.size(), &report.written_count, &report.total_count, + &report.nm_utf8_bytes_used)); + + for (uint32_t i = 0; i < report.written_count; ++i) + report.object_numbers.push_back(removed[i].object_number); + return report; +} + +RedactionReport ApplyPageRedactionsWithReport(FPDF_PAGE page) { + std::array removed = {}; + std::array nm_utf8_pool = {}; + RedactionReport report; + + EXPECT_TRUE(EPDFPage_ApplyRedactionsWithReport( + page, removed.data(), removed.size(), nm_utf8_pool.data(), + nm_utf8_pool.size(), &report.written_count, &report.total_count, + &report.nm_utf8_bytes_used)); + + for (uint32_t i = 0; i < report.written_count; ++i) + report.object_numbers.push_back(removed[i].object_number); + return report; +} + void VerifyFocusableAnnotSubtypes( FPDF_FORMHANDLE form_handle, pdfium::span expected_subtypes) { @@ -413,6 +470,41 @@ 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, ExtractHighlightLongContent) { // Open a file with one annotation and load its first page. ASSERT_TRUE(OpenDocument("annotation_highlight_long_content.pdf")); @@ -1960,8 +2052,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 +2059,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 +2067,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 +2645,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 +3322,137 @@ 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())); + } + + 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, ApplyRedactionReportsIntersectingAnnotation) { + 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); + RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); + EXPECT_EQ(2u, report.written_count); + EXPECT_EQ(2u, report.total_count); + EXPECT_THAT(report.object_numbers, testing::UnorderedElementsAre(5u, 6u)); + } + + 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); + RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); + EXPECT_EQ(2u, report.written_count); + EXPECT_EQ(2u, report.total_count); + EXPECT_THAT(report.object_numbers, testing::UnorderedElementsAre(5u, 7u)); + } + + 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); + RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); + EXPECT_EQ(1u, report.written_count); + EXPECT_EQ(1u, report.total_count); + EXPECT_THAT(report.object_numbers, testing::ElementsAre(5u)); + } + + 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); + RedactionReport report = ApplyRedactionWithReport(page.get(), annot.get()); + EXPECT_EQ(3u, report.written_count); + EXPECT_EQ(3u, report.total_count); + EXPECT_THAT(report.object_numbers, + testing::UnorderedElementsAre(5u, 6u, 7u)); + } + + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); +} + +TEST_F(FPDFAnnotEmbedderTest, ApplyPageRedactionsReportsAllRemovedAnnotations) { + ASSERT_TRUE(OpenDocument("redact_apply_all_visible.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + ASSERT_EQ(4, FPDFPage_GetAnnotCount(page.get())); + + RedactionReport report = ApplyPageRedactionsWithReport(page.get()); + EXPECT_EQ(4u, report.written_count); + EXPECT_EQ(4u, report.total_count); + EXPECT_THAT(report.object_numbers, + testing::UnorderedElementsAre(5u, 6u, 7u, 8u)); + EXPECT_EQ(0, FPDFPage_GetAnnotCount(page.get())); +} + TEST_F(FPDFAnnotEmbedderTest, PolygonAnnotation) { ASSERT_TRUE(OpenDocument("polygon_annot.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/fpdfsdk/fpdf_attachment.cpp b/fpdfsdk/fpdf_attachment.cpp index 5f0d908d4..40ae66bae 100644 --- a/fpdfsdk/fpdf_attachment.cpp +++ b/fpdfsdk/fpdf_attachment.cpp @@ -43,7 +43,7 @@ FPDFDoc_GetAttachmentCount(FPDF_DOCUMENT document) { 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; } @@ -87,7 +87,7 @@ FPDFDoc_GetAttachment(FPDF_DOCUMENT document, int index) { 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; } @@ -334,38 +334,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 +390,48 @@ 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; -} \ No newline at end of file +} diff --git a/fpdfsdk/fpdf_doc.cpp b/fpdfsdk/fpdf_doc.cpp index 799c5a592..f0ba7f4c5 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; } @@ -455,23 +458,24 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFLink_Enumerate(FPDF_PAGE page, if (!start_pos || !link_annot) { return false; } - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); + const CPDF_Page* pPage = CPDFPageFromFPDFPage(page); if (!pPage) { return false; } - RetainPtr pAnnots = pPage->GetMutableAnnotsArray(); + 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; } } @@ -633,22 +637,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 +666,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 +722,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 +739,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 +778,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 aa61b5d4a..c76a91986 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()); diff --git a/fpdfsdk/fpdf_javascript.cpp b/fpdfsdk/fpdf_javascript.cpp index c5a955576..9ee17d61c 100644 --- a/fpdfsdk/fpdf_javascript.cpp +++ b/fpdfsdk/fpdf_javascript.cpp @@ -27,7 +27,7 @@ FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document) { return -1; } - auto name_tree = CPDF_NameTree::Create(doc, "JavaScript"); + auto name_tree = CPDF_NameTree::CreateForReading(doc, "JavaScript"); return name_tree ? pdfium::checked_cast(name_tree->GetCount()) : 0; } @@ -38,7 +38,7 @@ FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, int index) { 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; } diff --git a/fpdfsdk/fpdf_save.cpp b/fpdfsdk/fpdf_save.cpp index a6eb68ca5..d82567c8e 100644 --- a/fpdfsdk/fpdf_save.cpp +++ b/fpdfsdk/fpdf_save.cpp @@ -6,10 +6,14 @@ #include "public/fpdf_save.h" -#include #include +#include +#include +#include +#include #include +#include #include #include @@ -229,8 +233,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 +294,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 ea45c4018..30583b56e 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,98 @@ 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, SaveSimpleDocNoIncremental) { ASSERT_TRUE(OpenDocument("hello_world.pdf")); EXPECT_TRUE(FPDF_SaveWithVersion(document(), this, FPDF_NO_INCREMENTAL, 14)); diff --git a/fpdfsdk/fpdf_text_embeddertest.cpp b/fpdfsdk/fpdf_text_embeddertest.cpp index f77c04efc..5e6bb8cc4 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 6a4278cf8..8a4adfb56 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -16,20 +16,19 @@ #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_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 +38,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" @@ -477,8 +477,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 +524,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,8 +554,6 @@ 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; } @@ -611,8 +611,7 @@ EPDF_UnlockOwnerPermissions(FPDF_DOCUMENT document, return security_handler->UnlockOwner(password); } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDF_IsEncrypted(FPDF_DOCUMENT document) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_IsEncrypted(FPDF_DOCUMENT document) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); if (!pDoc) { return false; @@ -675,7 +674,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; } @@ -707,16 +709,37 @@ EPDFDoc_LoadPageByObjectNumber(FPDF_DOCUMENT document, unsigned int obj_num) { return LoadPageByValidatedIndex(document, doc, doc->GetPageIndex(obj_num)); } +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 unsigned int FPDF_CALLCONV 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(); } @@ -1059,30 +1082,35 @@ 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; + } // Get the annotation's dictionary from the context. RetainPtr pAnnotDict = pAnnotContext->GetMutableAnnotDict(); - if (!pAnnotDict) + 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); // ---------------------------------------------------------------- bitmaps RetainPtr pBitmap(CFXDIBitmapFromFPDFBitmap(bitmap)); - if (!pBitmap) + if (!pBitmap) { return false; + } ValidateBitmapPremultiplyState(pBitmap); #if defined(PDF_USE_SKIA) @@ -1095,8 +1123,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,20 +1144,24 @@ 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) + if (!pAnnotDict) { return false; + } CPDF_Document* pDoc = pPage->GetDocument(); - if (!pDoc) + if (!pDoc) { return false; + } auto pAnnot = std::make_unique(std::move(pAnnotDict), pDoc); @@ -1137,17 +1170,20 @@ EPDF_RenderAnnotBitmapUnrotated(FPDF_BITMAP bitmap, // 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()) + CFX_FloatRect target = + pAnnot->GetAnnotDict()->GetRectFor("EPDFUnrotatedRect"); + 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 +1197,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 +1221,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); @@ -1515,16 +1554,19 @@ FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, 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)) + if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) { return -1; + } // Cheap: no ParseContent(). RetainPtr dict = pDoc->GetMutablePageDictionary(page_index); - if (!dict) + if (!dict) { return -1; + } auto page = pdfium::MakeRetain(pDoc, std::move(dict)); return page->GetPageRotation(); } @@ -1554,27 +1596,32 @@ static CFX_FloatRect GetInheritedRect(const CPDF_Dictionary* pPageDict, 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) + if (!pDoc) { return false; + } - if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) + if (page_index < 0 || page_index >= FPDF_GetPageCount(document)) { return false; + } RetainPtr dict = pDoc->GetMutablePageDictionary(page_index); - if (!dict) + 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()) + if (mediabox.IsEmpty()) { mediabox = CFX_FloatRect(0, 0, 612, 792); + } CFX_FloatRect cropbox = GetInheritedRect(dict.Get(), pdfium::page_object::kCropBox); @@ -1589,12 +1636,13 @@ EPDF_GetPageSizeByIndexNormalized(FPDF_DOCUMENT document, 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) { @@ -1732,7 +1780,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) { @@ -1854,7 +1902,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 a4f21974c..a20af2e31 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -534,6 +534,22 @@ int CheckPDFiumCApi() { CHK(FPDF_GetXFAPacketName); CHK(FPDF_InitLibrary); CHK(FPDF_InitLibraryWithConfig); + CHK(EPDF_LoadBaseDocument); + CHK(EPDF_LoadMemBaseDocument); + CHK(EPDF_LoadMemBaseDocument64); + CHK(EPDFDoc_GetPageObjectNumberByIndex); + 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_SaveLayerArtifactToOwnedBuffer); + CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); CHK(FPDF_LoadDocument); CHK(FPDF_LoadMemDocument); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 56b87842e..c4d296dfc 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,12 @@ #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_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" @@ -108,6 +115,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 +207,366 @@ 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; + } + + 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 +719,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 +1109,875 @@ 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())); + } + + 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, 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); + + 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())); + + 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 +2531,30 @@ 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, FPDFGetPageSizeByIndex) { ASSERT_TRUE(OpenDocument("rectangles.pdf")); diff --git a/public/epdf_redact.h b/public/epdf_redact.h new file mode 100644 index 000000000..e0f62b68a --- /dev/null +++ b/public/epdf_redact.h @@ -0,0 +1,107 @@ +// 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. +// Report entry produced by redaction APIs that delete annotations. +// +// `object_number` is 0 when the removed annotation was a direct object. +// `nm_utf8_len` is 0 when no /NM was present. It is +// EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW when /NM existed but the caller's +// UTF-8 byte pool had no room for it. +#define EPDF_REMOVED_ANNOT_NM_UTF8_OVERFLOW 0xFFFFFFFFu + +typedef struct { + uint32_t object_number; + uint32_t index_at_removal; + uint32_t nm_utf8_offset; + uint32_t nm_utf8_len; +} EPDF_RemovedAnnotInfo; + +// Experimental EmbedPDF Extension API. +// Apply a redact annotation, permanently removing content underneath. +// If the annotation has an RO (Redact Overlay) stream, it will be flattened +// as page content (filled rectangles with overlay text). +// If no RO stream exists, content is simply removed with no overlay. +// The annotation is automatically removed from the page after applying. +// +// 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. +// Same as EPDFAnnot_ApplyRedaction(), but also reports every annotation that +// was removed. This includes annotations whose /Rect intersects the redaction +// area, popup annotations cascaded from removed parents, and the originating +// REDACT annotation itself. Sibling REDACT annotations are preserved. +// +// The caller owns both output buffers. `out_written_count` is the number of +// records safely written to `out_removed`; `out_total_count` is the total +// number of annotations removed. If total > written, the report was truncated. +// /NM values are normalized to UTF-8 and written into `nm_utf8_pool`. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ApplyRedactionWithReport( + FPDF_PAGE page, + FPDF_ANNOTATION annot, + EPDF_RemovedAnnotInfo* out_removed, + uint32_t out_removed_capacity, + char* nm_utf8_pool, + uint32_t nm_utf8_pool_capacity, + uint32_t* out_written_count, + uint32_t* out_total_count, + uint32_t* out_nm_utf8_bytes_used); + +// 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. +// Same as EPDFPage_ApplyRedactions(), but reports removed annotations using +// the same buffer contract as EPDFAnnot_ApplyRedactionWithReport(). +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFPage_ApplyRedactionsWithReport( + FPDF_PAGE page, + EPDF_RemovedAnnotInfo* out_removed, + uint32_t out_removed_capacity, + char* nm_utf8_pool, + uint32_t nm_utf8_pool_capacity, + uint32_t* out_written_count, + uint32_t* out_total_count, + uint32_t* out_nm_utf8_bytes_used); + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus + +#endif // PUBLIC_EPDF_REDACT_H_ diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h index db493ca3d..a767537cf 100644 --- a/public/fpdf_annot.h +++ b/public/fpdf_annot.h @@ -13,6 +13,9 @@ // NOLINTNEXTLINE(build/include) #include "fpdf_formfill.h" +// NOLINTNEXTLINE(build/include) +#include "epdf_redact.h" + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -1782,40 +1785,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. diff --git a/public/fpdf_save.h b/public/fpdf_save.h index 093b889ad..72a5dfcfa 100644 --- a/public/fpdf_save.h +++ b/public/fpdf_save.h @@ -86,6 +86,75 @@ 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_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 2af9a0ac4..e5ec75ab0 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; @@ -562,6 +563,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: @@ -1661,6 +1776,20 @@ 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. +// 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. // Get the PDF indirect object number of a page's dictionary. // diff --git a/testing/resources/redact_annot.in b/testing/resources/redact_annot.in index 99b7b4e8e..e3e1de134 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 76c26078b..a433bb662 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 000000000..18cbf69b6 --- /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 000000000..5725b06a8 Binary files /dev/null and b/testing/resources/redact_apply_all_visible.pdf differ diff --git a/testing/resources/redact_popup_cascade.in b/testing/resources/redact_popup_cascade.in new file mode 100644 index 000000000..e52830b8a --- /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 000000000..4e6807012 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 000000000..8a405cb79 --- /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 000000000..7524d56d1 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 000000000..7f0d277fb --- /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 000000000..d30988cca 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 000000000..b60f20b85 --- /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 000000000..d2e2d844c 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 000000000..c0ce81ad1 --- /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 000000000..908dfd3e3 Binary files /dev/null and b/testing/resources/redact_touch_only.pdf differ diff --git a/testing/tools/BUILD.gn b/testing/tools/BUILD.gn index 15d841bcf..a90c5718e 100644 --- a/testing/tools/BUILD.gn +++ b/testing/tools/BUILD.gn @@ -4,6 +4,37 @@ 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" ] +} + 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 000000000..bfcc25f3d --- /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 000000000..e5c060f92 --- /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 000000000..d78a95238 --- /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_