From 94383feb181fbeb54171877a9f25a15be1bda480 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 8 May 2026 23:07:07 +0300 Subject: [PATCH 01/28] Read-only walks to const accessors across content parsing --- core/fpdfapi/font/cpdf_type3font.cpp | 14 +- core/fpdfapi/page/cpdf_contentparser.cpp | 21 +- core/fpdfapi/page/cpdf_form.cpp | 18 +- .../fpdfapi/page/cpdf_streamcontentparser.cpp | 39 +- core/fpdfapi/page/cpdf_streamcontentparser.h | 6 +- core/fpdfapi/parser/BUILD.gn | 3 + .../parser/cpdf_read_only_graph_guard.cpp | 25 + .../parser/cpdf_read_only_graph_guard.h | 30 + .../cpdf_read_only_graph_guard_unittest.cpp | 30 + core/fpdfdoc/cpdf_annot.cpp | 57 +- core/fpdfdoc/cpdf_linklist.cpp | 15 +- core/fpdfdoc/cpdf_linklist.h | 5 +- fpdfsdk/fpdf_annot.cpp | 1591 ++++++++++------- fpdfsdk/fpdf_doc.cpp | 114 +- 14 files changed, 1222 insertions(+), 746 deletions(-) create mode 100644 core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp create mode 100644 core/fpdfapi/parser/cpdf_read_only_graph_guard.h create mode 100644 core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp diff --git a/core/fpdfapi/font/cpdf_type3font.cpp b/core/fpdfapi/font/cpdf_type3font.cpp index 00d95298c8..44b8ca7d03 100644 --- a/core/fpdfapi/font/cpdf_type3font.cpp +++ b/core/fpdfapi/font/cpdf_type3font.cpp @@ -60,7 +60,10 @@ void CPDF_Type3Font::WillBeDestroyed() { } bool CPDF_Type3Font::Load() { - font_resources_ = font_dict_->GetMutableDictFor("Resources"); + RetainPtr font_resources = + font_dict_->GetDictFor("Resources"); + font_resources_ = + pdfium::WrapRetain(const_cast(font_resources.Get())); RetainPtr pMatrix = font_dict_->GetArrayFor("FontMatrix"); float xscale = 1.0f; float yscale = 1.0f; @@ -93,7 +96,10 @@ bool CPDF_Type3Font::Load() { } } } - char_procs_ = font_dict_->GetMutableDictFor("CharProcs"); + RetainPtr char_procs = + font_dict_->GetDictFor("CharProcs"); + char_procs_ = + pdfium::WrapRetain(const_cast(char_procs.Get())); if (font_dict_->GetDirectObjectFor("Encoding")) { LoadPDFEncoding(false, false); } @@ -125,8 +131,10 @@ CPDF_Type3Char* CPDF_Type3Font::LoadChar(uint32_t charcode) { return nullptr; } + RetainPtr const_stream = + ToStream(char_procs_->GetDirectObjectFor(name)); RetainPtr pStream = - ToStream(char_procs_->GetMutableDirectObjectFor(name)); + pdfium::WrapRetain(const_cast(const_stream.Get())); if (!pStream) { return nullptr; } diff --git a/core/fpdfapi/page/cpdf_contentparser.cpp b/core/fpdfapi/page/cpdf_contentparser.cpp index 1d6bd7b678..4e74180fcf 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,15 @@ 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); + 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 +214,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_form.cpp b/core/fpdfapi/page/cpdf_form.cpp index 2f55ac308c..c363862ace 100644 --- a/core/fpdfapi/page/cpdf_form.cpp +++ b/core/fpdfapi/page/cpdf_form.cpp @@ -45,15 +45,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, + pFormStream->GetMutableDict(), + pPageResources, + pdfium::WrapRetain(ChooseResourcesDict( + const_cast( + pFormStream->GetDict()->GetDictFor("Resources").Get()), + pParentResources, + pPageResources.Get()))), form_stream_(std::move(pFormStream)) { LoadTransparencyInfo(); } diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.cpp b/core/fpdfapi/page/cpdf_streamcontentparser.cpp index e503fbf164..35cac51d74 100644 --- a/core/fpdfapi/page/cpdf_streamcontentparser.cpp +++ b/core/fpdfapi/page/cpdf_streamcontentparser.cpp @@ -624,7 +624,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 +782,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 +952,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 +1204,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 +1219,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 +1293,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 +1305,9 @@ RetainPtr CPDF_StreamContentParser::FindPattern( RetainPtr CPDF_StreamContentParser::FindShading( const ByteString& name) { - RetainPtr pPattern = FindResourceObj("Shading", name); + RetainPtr const_pattern = FindResourceObj("Shading", name); + RetainPtr pPattern = + pdfium::WrapRetain(const_cast(const_pattern.Get())); if (!pPattern || (!pPattern->IsDictionary() && !pPattern->IsStream())) { return nullptr; } diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.h b/core/fpdfapi/page/cpdf_streamcontentparser.h index 38e9977c20..91f4575364 100644 --- a/core/fpdfapi/page/cpdf_streamcontentparser.h +++ b/core/fpdfapi/page/cpdf_streamcontentparser.h @@ -125,9 +125,9 @@ class CPDF_StreamContentParser { RetainPtr FindColorSpace(const ByteString& name); RetainPtr FindPattern(const ByteString& name); RetainPtr FindShading(const ByteString& name); - RetainPtr FindResourceHolder(ByteStringView type); - RetainPtr FindResourceObj(ByteStringView type, - const ByteString& name); + RetainPtr FindResourceHolder(ByteStringView type); + RetainPtr FindResourceObj(ByteStringView type, + const ByteString& name); // Takes ownership of |pImageObj|, returns unowned pointer to it. CPDF_ImageObject* AddImageObject(std::unique_ptr pImageObj); diff --git a/core/fpdfapi/parser/BUILD.gn b/core/fpdfapi/parser/BUILD.gn index 94ab93ee7b..475c093aa0 100644 --- a/core/fpdfapi/parser/BUILD.gn +++ b/core/fpdfapi/parser/BUILD.gn @@ -53,6 +53,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", @@ -128,6 +130,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_read_only_graph_guard.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp new file mode 100644 index 0000000000..607bc3bf85 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp @@ -0,0 +1,25 @@ +// 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; + +} // 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; +} diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard.h b/core/fpdfapi/parser/cpdf_read_only_graph_guard.h new file mode 100644 index 0000000000..64e1630af6 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.h @@ -0,0 +1,30 @@ +// 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(); + + private: + const bool previous_; +}; + +#if DCHECK_IS_ON() +#define DCHECK_PDF_GRAPH_MUTABLE_FOR(obj) \ + DCHECK(!CPDF_ReadOnlyGraphGuard::IsActive() || (obj)->GetObjNum() == 0) +#define DCHECK_PDF_HOLDER_MUTABLE() DCHECK(!CPDF_ReadOnlyGraphGuard::IsActive()) +#else +#define DCHECK_PDF_GRAPH_MUTABLE_FOR(obj) ((void)0) +#define DCHECK_PDF_HOLDER_MUTABLE() ((void)0) +#endif + +#endif // CORE_FPDFAPI_PARSER_CPDF_READ_ONLY_GRAPH_GUARD_H_ diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp new file mode 100644 index 0000000000..2a87202f86 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp @@ -0,0 +1,30 @@ +// 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()); +} diff --git a/core/fpdfdoc/cpdf_annot.cpp b/core/fpdfdoc/cpdf_annot.cpp index 1d40c9e63d..18a455617d 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 @@ -574,8 +575,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 +702,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 +754,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 +808,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 +841,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; } diff --git a/core/fpdfdoc/cpdf_linklist.cpp b/core/fpdfdoc/cpdf_linklist.cpp index c80270e7fb..a7296bc7c9 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 b259aaf88a..d9d1257741 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/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 5603622e10..3d262c6a3b 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -15,14 +15,14 @@ #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_pageorganizer.h" #include "core/fpdfapi/edit/cpdf_text_redactor.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" @@ -213,57 +213,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 +292,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 +483,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 +499,44 @@ 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) + : 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_; +}; 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; } @@ -631,12 +634,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 +650,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 +695,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; } @@ -853,19 +864,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 +922,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 +944,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 +954,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 +976,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 +992,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 +1028,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,18 +1095,20 @@ 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; } @@ -2369,26 +2398,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(); @@ -2456,15 +2490,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 +2508,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 +2523,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 +2545,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 +2559,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 +2572,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 +2590,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 +2606,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 +2641,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 +2667,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 +2685,9 @@ EPDFAnnot_ClearBorderEffect(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("BE"); return true; @@ -2688,10 +2734,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 +2770,9 @@ EPDFAnnot_ClearRectangleDifferences(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } pAnnotDict->RemoveFor("RD"); return true; @@ -2743,10 +2790,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 +2835,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 +2865,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 +2883,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 +2898,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 +2972,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 +2999,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 +3010,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 +3040,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 +3064,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 +3094,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 +3121,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 +3152,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 +3198,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 +3220,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 +3240,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 +3256,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 +3307,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 +3320,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 +3355,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 +3377,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 +3411,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 +3426,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 +3468,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,12 +3482,15 @@ 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; } @@ -3424,18 +3523,26 @@ 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)); @@ -3447,16 +3554,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 +3577,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 +3595,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 +3608,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 +3680,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 +3706,32 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { auto page = pdfium::MakeRetain(pdf, page_dict); // Create the context, which now takes the RetainPtr directly. - auto ctx = std::make_unique(std::move(annot_dict), std::move(page)); + auto ctx = + std::make_unique(std::move(annot_dict), std::move(page)); // 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 +3751,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 +3817,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 +3839,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 +3853,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 +3877,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 +3936,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 +3950,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 +3990,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 +4021,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 +4046,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 +4071,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 +4088,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,12 +4108,14 @@ 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); } @@ -3954,8 +4127,9 @@ namespace { std::vector GetRedactRectsFromAnnotDict( const CPDF_Dictionary* annot_dict) { std::vector rects; - if (!annot_dict) + if (!annot_dict) { return rects; + } // Try QuadPoints first (for text-based redactions) RetainPtr quad_points_array = @@ -3965,18 +4139,21 @@ std::vector GetRedactRectsFromAnnotDict( for (size_t i = 0; i < quad_count; ++i) { CFX_FloatRect rect = CPDF_Annot::RectFromQuadPoints(annot_dict, i); rect.Normalize(); - if (!rect.IsEmpty()) + if (!rect.IsEmpty()) { rects.push_back(rect); + } } - if (!rects.empty()) + 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()) + if (!rect.IsEmpty()) { rects.push_back(rect); + } return rects; } @@ -3986,32 +4163,39 @@ std::vector GetRedactRectsFromAnnotDict( void FlattenFormXObjectToPage(CPDF_Page* page, RetainPtr form_stream, const CFX_FloatRect& target_rect) { - if (!page || !form_stream) + if (!page || !form_stream) { return; + } CPDF_Document* doc = page->GetDocument(); - if (!doc) + if (!doc) { return; + } // Get the form dictionary from the stream RetainPtr form_dict = form_stream->GetDict(); - if (!form_dict) + 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()) + 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 + // 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) + if (form_bbox.Width() > 0) { scale_x = target_rect.Width() / form_bbox.Width(); - if (form_bbox.Height() > 0) + } + if (form_bbox.Height() > 0) { scale_y = target_rect.Height() / form_bbox.Height(); + } CFX_Matrix form_matrix; form_matrix.a = scale_x; @@ -4021,16 +4205,13 @@ void FlattenFormXObjectToPage(CPDF_Page* page, // Create a CPDF_Form from the stream auto form = std::make_unique( - doc, - page->GetMutableResources(), + 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); + CPDF_PageObject::kNoContentStream, std::move(form), form_matrix); form_obj->CalcBoundingBox(); form_obj->SetDirty(true); @@ -4040,17 +4221,21 @@ void FlattenFormXObjectToPage(CPDF_Page* page, // 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; } @@ -4060,21 +4245,25 @@ int GetAnnotIndexOnPage(CPDF_Page* page, const CPDF_Dictionary* annot_dict) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return false; + } // Must be a REDACT annotation - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) + if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { return false; + } const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) + if (!annot_dict) { return false; + } // 1. Extract redaction rectangles from QuadPoints or Rect std::vector rects = GetRedactRectsFromAnnotDict(annot_dict); - if (rects.empty()) + if (rects.empty()) { return false; + } // 2. Remove content using existing redactor (no black boxes - we use RO) RedactTextInRects(pPage, pdfium::span(rects), @@ -4084,7 +4273,8 @@ EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { // 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); + CFX_FloatRect annot_rect = + annot_dict->GetRectFor(pdfium::annotation::kRect); annot_rect.Normalize(); FlattenFormXObjectToPage(pPage, ro_stream, annot_rect); } @@ -4095,8 +4285,7 @@ EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { if (annot_index >= 0) { RetainPtr annots = pPage->GetMutableAnnotsArray(); if (annots) { - RetainPtr entry = - annots->GetMutableObjectAt(annot_index); + RetainPtr entry = annots->GetMutableObjectAt(annot_index); uint32_t objnum = 0; if (entry && entry->IsReference()) { objnum = entry->AsReference()->GetRefObjNum(); @@ -4104,40 +4293,45 @@ EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { objnum = entry->GetObjNum(); } annots->RemoveAt(annot_index); - if (objnum) + if (objnum) { pPage->GetDocument()->DeleteIndirectObject(objnum); + } } } return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_ApplyRedactions(FPDF_PAGE page) { +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_ApplyRedactions(FPDF_PAGE page) { CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + 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, CFX_FloatRect>> + ro_streams; std::vector> redact_index_objnums; RetainPtr annot_list = pPage->GetMutableAnnotsArray(); - if (!annot_list || annot_list->IsEmpty()) + 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) + if (!annot_dict) { continue; + } // Check if this is a REDACT annotation ByteString subtype = annot_dict->GetNameFor(pdfium::annotation::kSubtype); - if (subtype != "Redact") + if (subtype != "Redact") { continue; + } // Track index and indirect object number for later removal uint32_t objnum = 0; @@ -4149,7 +4343,8 @@ EPDFPage_ApplyRedactions(FPDF_PAGE page) { redact_index_objnums.push_back({i, objnum}); // Extract rectangles - std::vector rects = GetRedactRectsFromAnnotDict(annot_dict.Get()); + std::vector rects = + GetRedactRectsFromAnnotDict(annot_dict.Get()); for (const auto& rect : rects) { all_rects.push_back(rect); } @@ -4157,14 +4352,16 @@ EPDFPage_ApplyRedactions(FPDF_PAGE page) { // 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); + 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 (all_rects.empty()) { return false; + } // Remove content for all redaction areas at once RedactTextInRects(pPage, pdfium::span(all_rects), @@ -4178,34 +4375,40 @@ EPDFPage_ApplyRedactions(FPDF_PAGE page) { // 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) { + for (auto it = redact_index_objnums.rbegin(); + it != redact_index_objnums.rend(); ++it) { annot_list->RemoveAt(it->first); - if (it->second) + if (it->second) { pPage->GetDocument()->DeleteIndirectObject(it->second); + } } return true; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_Flatten(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) + 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(); @@ -4229,44 +4432,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 +4484,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 +4509,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 +4559,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 +4582,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 +4610,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 +4626,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 +4653,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 +4683,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 +4710,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 +4733,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 +4776,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 +4784,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 +4811,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 +4825,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 +4842,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 +4862,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 +4894,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 +4934,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; @@ -4724,21 +4973,26 @@ FPDF_EXPORT int FPDF_CALLCONV EPDFAnnot_GetAvailableAppearanceModes(FPDF_ANNOTATION annot) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + 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; } @@ -4747,8 +5001,9 @@ EPDFAnnot_HasAppearanceStream(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode) { RetainPtr pAnnotDict = GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!pAnnotDict) + if (!pAnnotDict) { return false; + } auto mode = static_cast(appearanceMode); return !!GetAnnotAP(pAnnotDict.Get(), mode); @@ -4764,16 +5019,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 +5047,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 +5083,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 +5104,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 +5158,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 +5176,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 +5185,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 +5212,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 +5238,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 +5287,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 +5311,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 +5340,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 +5353,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 +5369,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 +5395,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 +5411,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 +5430,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 +5444,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 +5461,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 +5482,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 +5511,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 +5549,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 +5565,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 +5590,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 +5616,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 +5656,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 +5681,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 +5718,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 +5747,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 +5774,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 +5812,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_doc.cpp b/fpdfsdk/fpdf_doc.cpp index 799c5a592a..f0ba7f4c51 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 +} From 7bfcd90d3f99f197c4eb2090ec9d67eaac594b34 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 8 May 2026 23:58:06 +0300 Subject: [PATCH 02/28] AP becomes ephemeral --- core/fpdfdoc/BUILD.gn | 1 + core/fpdfdoc/cpdf_annot.cpp | 81 +- core/fpdfdoc/cpdf_annot.h | 17 +- core/fpdfdoc/cpdf_annot_unittest.cpp | 55 ++ core/fpdfdoc/cpdf_annotlist.cpp | 64 -- core/fpdfdoc/cpdf_generateap.cpp | 1012 ++++++++++++++------- core/fpdfdoc/cpdf_generateap.h | 24 + core/fpdfdoc/cpdf_generateap_unittest.cpp | 276 ++++++ fpdfsdk/cpdfsdk_pageview.cpp | 3 - fpdfsdk/fpdf_annot.cpp | 24 +- 10 files changed, 1123 insertions(+), 434 deletions(-) create mode 100644 core/fpdfdoc/cpdf_generateap_unittest.cpp diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index 94ad43f0c2..f258efae31 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 18a455617d..8a65fcdbf6 100644 --- a/core/fpdfdoc/cpdf_annot.cpp +++ b/core/fpdfdoc/cpdf_annot.cpp @@ -133,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() { @@ -166,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; @@ -175,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 { @@ -182,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) { @@ -218,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; } @@ -890,12 +939,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); @@ -919,12 +970,14 @@ bool CPDF_Annot::DrawInContext(CPDF_Page* pPage, return false; } - // It might happen that by the time this annotation instance was created, - // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided - // to not "generate" its AP. - // If for a reason the object is no longer hidden, but still does not have - // its "AP" generated, generate it now. - GenerateAPIfNeeded(); + if (!CanGenerateEphemeralAP()) { + // It might happen that by the time this annotation instance was created, + // it was flagged as "hidden" (e.g. /F 2), and hence CPDF_GenerateAP decided + // to not "generate" its AP. + // If for a reason the object is no longer hidden, but still does not have + // its "AP" generated, generate it now. + GenerateAPIfNeeded(); + } CFX_Matrix matrix; CPDF_Form* pForm = AnnotGetMatrix(pPage, this, mode, mtUser2Device, &matrix); diff --git a/core/fpdfdoc/cpdf_annot.h b/core/fpdfdoc/cpdf_annot.h index 1071f28f9b..5a41be69d2 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,12 +232,15 @@ 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_; }; diff --git a/core/fpdfdoc/cpdf_annot_unittest.cpp b/core/fpdfdoc/cpdf_annot_unittest.cpp index 5e2f582b35..547c025ff3 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 db8be16033..32e7b6aa77 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,57 +121,6 @@ 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) @@ -185,10 +130,6 @@ CPDF_AnnotList::CPDF_AnnotList(CPDF_Page* pPage) 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)); @@ -204,11 +145,6 @@ CPDF_AnnotList::CPDF_AnnotList(CPDF_Page* pPage) } pAnnots->ConvertToIndirectObjectAt(i, document_); 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_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp index f1c42bd876..23ab5f0773 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 865315e04c..c0b4cc1f4c 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 0000000000..635e1533c3 --- /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/fpdfsdk/cpdfsdk_pageview.cpp b/fpdfsdk/cpdfsdk_pageview.cpp index 339201caec..c4e8bc395e 100644 --- a/fpdfsdk/cpdfsdk_pageview.cpp +++ b/fpdfsdk/cpdfsdk_pageview.cpp @@ -107,9 +107,6 @@ std::unique_ptr CPDFSDK_PageView::NewAnnot(CPDF_Annot* annot) { auto ret = std::make_unique(annot, this, form); form->AddMap(form_control, ret.get()); - if (pdf_form->NeedConstructAP()) { - ret->ResetAppearance(std::nullopt, CPDFSDK_Widget::kValueUnchanged); - } return ret; } diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 3d262c6a3b..b893959c5b 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -851,7 +851,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, @@ -2202,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 From 71516c3898e1c72887f833b08e2143d70f250d0e Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 00:06:44 +0300 Subject: [PATCH 03/28] CPDF_AnnotList preserves direct annotations --- core/fpdfdoc/cpdf_annotlist.cpp | 15 ++++++++------ core/fpdfdoc/cpdf_annotlist_unittest.cpp | 25 ++++++++++++++++++++++++ fpdfsdk/fpdf_annot_embeddertest.cpp | 20 +++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/core/fpdfdoc/cpdf_annotlist.cpp b/core/fpdfdoc/cpdf_annotlist.cpp index 32e7b6aa77..8b53e30398 100644 --- a/core/fpdfdoc/cpdf_annotlist.cpp +++ b/core/fpdfdoc/cpdf_annotlist.cpp @@ -125,25 +125,28 @@ std::unique_ptr CreatePopupAnnot(CPDF_Document* document, CPDF_AnnotList::CPDF_AnnotList(CPDF_Page* pPage) : page_(pPage), document_(page_->GetDocument()) { - RetainPtr pAnnots = page_->GetMutableAnnotsArray(); + RetainPtr pAnnots = page_->GetAnnotsArray(); if (!pAnnots) { return; } 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_)); } diff --git a/core/fpdfdoc/cpdf_annotlist_unittest.cpp b/core/fpdfdoc/cpdf_annotlist_unittest.cpp index a59f9595d1..2f93c93dc9 100644 --- a/core/fpdfdoc/cpdf_annotlist_unittest.cpp +++ b/core/fpdfdoc/cpdf_annotlist_unittest.cpp @@ -52,6 +52,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 +131,21 @@ 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(); + + CPDF_AnnotList list(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/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index d23c3e35f5..7cfdd97b14 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -2548,6 +2548,26 @@ 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); From 2dd7caa5505922b93a88ce18a8173f01097bc20b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 00:30:19 +0300 Subject: [PATCH 04/28] CPDF_InteractiveForm const walk + explicit normalize --- core/fpdfdoc/cpdf_formcontrol.cpp | 29 ++-- core/fpdfdoc/cpdf_formfield.cpp | 14 +- core/fpdfdoc/cpdf_interactiveform.cpp | 137 ++++++++---------- core/fpdfdoc/cpdf_interactiveform.h | 4 +- .../fpdfdoc/cpdf_interactiveform_unittest.cpp | 61 ++++++-- 5 files changed, 142 insertions(+), 103 deletions(-) diff --git a/core/fpdfdoc/cpdf_formcontrol.cpp b/core/fpdfdoc/cpdf_formcontrol.cpp index 814a1fa97f..2a5187a527 100644 --- a/core/fpdfdoc/cpdf_formcontrol.cpp +++ b/core/fpdfdoc/cpdf_formcontrol.cpp @@ -222,16 +222,16 @@ RetainPtr CPDF_FormControl::GetDefaultControlFont() const { } const ByteString& font_name = maybe_font_name_and_size.value().name; - RetainPtr pDRDict = ToDictionary( - CPDF_FormField::GetMutableFieldAttrForDict(widget_dict_.Get(), "DR")); + RetainPtr pDRDict = ToDictionary( + CPDF_FormField::GetFieldAttrForDict(widget_dict_.Get(), "DR")); if (pDRDict) { - RetainPtr fonts = pDRDict->GetMutableDictFor("Font"); + RetainPtr fonts = pDRDict->GetDictFor("Font"); if (ValidateFontResourceDict(fonts.Get())) { - RetainPtr pElement = - fonts->GetMutableDictFor(font_name.AsStringView()); + RetainPtr pElement = + fonts->GetDictFor(font_name.AsStringView()); if (pElement) { - RetainPtr font = - form_->GetFontForElement(std::move(pElement)); + RetainPtr font = form_->GetFontForElement( + pdfium::WrapRetain(const_cast(pElement.Get()))); if (font) { return font; } @@ -243,25 +243,26 @@ RetainPtr CPDF_FormControl::GetDefaultControlFont() const { return pFormFont; } - RetainPtr pPageDict = widget_dict_->GetMutableDictFor("P"); - RetainPtr dict = ToDictionary( - CPDF_FormField::GetMutableFieldAttrForDict(pPageDict.Get(), "Resources")); + RetainPtr pPageDict = widget_dict_->GetDictFor("P"); + RetainPtr dict = ToDictionary( + CPDF_FormField::GetFieldAttrForDict(pPageDict.Get(), "Resources")); if (!dict) { return nullptr; } - RetainPtr fonts = dict->GetMutableDictFor("Font"); + RetainPtr fonts = dict->GetDictFor("Font"); if (!ValidateFontResourceDict(fonts.Get())) { return nullptr; } - RetainPtr pElement = - fonts->GetMutableDictFor(font_name.AsStringView()); + RetainPtr pElement = + fonts->GetDictFor(font_name.AsStringView()); if (!pElement) { return nullptr; } - return form_->GetFontForElement(std::move(pElement)); + return form_->GetFontForElement( + pdfium::WrapRetain(const_cast(pElement.Get()))); } int CPDF_FormControl::GetControlAlignment() const { diff --git a/core/fpdfdoc/cpdf_formfield.cpp b/core/fpdfdoc/cpdf_formfield.cpp index 00a54f39c9..220941d37b 100644 --- a/core/fpdfdoc/cpdf_formfield.cpp +++ b/core/fpdfdoc/cpdf_formfield.cpp @@ -19,6 +19,7 @@ #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_name.h" #include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/fpdf_parser_decode.h" @@ -62,6 +63,17 @@ bool IsComboOrListField(CPDF_FormField::Type type) { } } +WideString GetFieldNameComponent(const CPDF_Dictionary* dict) { + RetainPtr t_obj = + dict->GetObjectFor(pdfium::form_fields::kT); + if (ToReference(t_obj)) { + RetainPtr direct_obj = t_obj->GetDirect(); + return direct_obj && direct_obj->IsString() ? direct_obj->GetUnicodeText() + : WideString(); + } + return dict->GetUnicodeTextFor(pdfium::form_fields::kT); +} + } // namespace // static @@ -96,7 +108,7 @@ WideString CPDF_FormField::GetFullNameForDict( const CPDF_Dictionary* pLevel = pFieldDict; while (pLevel) { visited.insert(pLevel); - WideString short_name = pLevel->GetUnicodeTextFor(pdfium::form_fields::kT); + WideString short_name = GetFieldNameComponent(pLevel); if (!short_name.IsEmpty()) { if (full_name.IsEmpty()) { full_name = std::move(short_name); diff --git a/core/fpdfdoc/cpdf_interactiveform.cpp b/core/fpdfdoc/cpdf_interactiveform.cpp index 24bf93ad41..4f85fde2db 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 33c09170e7..3de7645fd2 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 9866687384..9c6a3d97c6 100644 --- a/core/fpdfdoc/cpdf_interactiveform_unittest.cpp +++ b/core/fpdfdoc/cpdf_interactiveform_unittest.cpp @@ -10,6 +10,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_number.h" #include "core/fpdfapi/parser/cpdf_reference.h" #include "core/fpdfapi/parser/cpdf_stream.h" #include "core/fpdfapi/parser/cpdf_string.h" @@ -57,18 +58,58 @@ 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. + const uint32_t last_obj_num = doc->GetLastObjNum(); + + // Let `interactive_form` parse the dictionaries above. CPDF_InteractiveForm interactive_form(doc.get()); - 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()); + 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"))); + + 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 bad_name_field_t = bad_name_field_dict->GetStringFor("T"); - ASSERT_TRUE(bad_name_field_t); - EXPECT_TRUE(bad_name_field_t->GetString().IsEmpty()); + 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(); + + CPDF_InteractiveForm interactive_form(doc.get()); - 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(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()); } From b50c87f6a168fd98e64f7d3178c391820abc2ad7 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 00:41:36 +0300 Subject: [PATCH 05/28] CPDF_Image and CPDF_DocPageData cache hardening --- core/fpdfapi/page/cpdf_docpagedata.cpp | 3 + core/fpdfapi/page/cpdf_image.cpp | 138 +++++++++++------- core/fpdfapi/page/cpdf_image.h | 9 +- .../page/cpdf_pageimagecache_unittest.cpp | 122 ++++++++++++++++ core/fpdfapi/parser/cpdf_document.cpp | 4 + core/fpdfapi/parser/cpdf_document.h | 4 + 6 files changed, 222 insertions(+), 58 deletions(-) diff --git a/core/fpdfapi/page/cpdf_docpagedata.cpp b/core/fpdfapi/page/cpdf_docpagedata.cpp index 2c3f841df9..902d253bd2 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.cpp +++ b/core/fpdfapi/page/cpdf_docpagedata.cpp @@ -427,6 +427,9 @@ 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; } diff --git a/core/fpdfapi/page/cpdf_image.cpp b/core/fpdfapi/page/cpdf_image.cpp index 6cb86bbe65..384cb91b31 100644 --- a/core/fpdfapi/page/cpdf_image.cpp +++ b/core/fpdfapi/page/cpdf_image.cpp @@ -42,67 +42,74 @@ namespace { - // Internal helper that overwrites an existing stream's dict + bytes - // and purges any cached image. - bool OverwriteStreamData(CPDF_Stream* s, - CPDF_Document* doc, - DataVector new_data, - RetainPtr new_dict, - bool data_is_decoded) { - if (!s || !new_dict) - return false; - - // Replace dictionary entries (no streams allowed as values). - RetainPtr old = s->GetMutableDict(); - if (!old) - return false; - - // Clear existing keys. - for (const ByteString& k : old->GetKeys()) - old->RemoveFor(k.AsStringView()); - - // Deep-copy all entries from new_dict into old. - CPDF_DictionaryLocker lock(new_dict); - for (auto it = lock.begin(); it != lock.end(); ++it) - old->SetFor(it->first, it->second->Clone()); - - // Swap in the bytes. - if (data_is_decoded) { - // Decoded pixels: also removes Filter/DecodeParms from the stream dict. - s->SetDataAndRemoveFilter(pdfium::span(new_data)); - } else { - // Already filtered (e.g., JPEG with /Filter /DCTDecode). - s->TakeData(std::move(new_data)); - } - - if (doc) - doc->MaybePurgeImage(s->GetObjNum()); - - return true; +// Internal helper that overwrites an existing stream's dict + bytes +// and purges any cached image. +bool OverwriteStreamData(CPDF_Stream* s, + CPDF_Document* doc, + DataVector new_data, + RetainPtr new_dict, + bool data_is_decoded) { + if (!s || !new_dict) { + return false; + } + + // Replace dictionary entries (no streams allowed as values). + RetainPtr old = s->GetMutableDict(); + if (!old) { + return false; + } + + // Clear existing keys. + for (const ByteString& k : old->GetKeys()) { + old->RemoveFor(k.AsStringView()); } - + + // Deep-copy all entries from new_dict into old. + CPDF_DictionaryLocker lock(new_dict); + for (auto it = lock.begin(); it != lock.end(); ++it) { + old->SetFor(it->first, it->second->Clone()); + } + + // Swap in the bytes. + if (data_is_decoded) { + // Decoded pixels: also removes Filter/DecodeParms from the stream dict. + s->SetDataAndRemoveFilter(pdfium::span(new_data)); + } else { + // Already filtered (e.g., JPEG with /Filter /DCTDecode). + s->TakeData(std::move(new_data)); + } + + if (doc) { + doc->MaybePurgeImage(s->GetObjNum()); + } + + return true; +} + } // namespace bool CPDF_Image::OverwriteStreamInPlace(DataVector new_data, RetainPtr new_dict, bool data_is_decoded) { // Ensure we can mutate the underlying stream. - if (stream_->IsInline()) + if (stream_->IsInline()) { ConvertStreamToIndirectObject(); + } RetainPtr s_const = GetStream(); - if (!s_const) + if (!s_const) { return false; + } // Get a mutable stream by objnum. RetainPtr s = ToStream(document_->GetMutableIndirectObject(s_const->GetObjNum())); - if (!s) + if (!s) { return false; + } - const bool ok = - OverwriteStreamData(s.Get(), document_, std::move(new_data), - std::move(new_dict), data_is_decoded); + const bool ok = OverwriteStreamData(s.Get(), document_, std::move(new_data), + std::move(new_dict), data_is_decoded); if (ok) { // Refresh cached flags/size from the new dictionary. FinishInitialization(); @@ -132,7 +139,7 @@ CPDF_Image::CPDF_Image(CPDF_Document* doc, RetainPtr pStream) CPDF_Image::CPDF_Image(CPDF_Document* doc, uint32_t dwStreamObjNum) : document_(doc), - stream_(ToStream(doc->GetMutableIndirectObject(dwStreamObjNum))) { + stream_(ToStream(doc->GetIndirectObject(dwStreamObjNum))) { DCHECK(document_); FinishInitialization(); } @@ -151,7 +158,11 @@ void CPDF_Image::FinishInitialization() { void CPDF_Image::ConvertStreamToIndirectObject() { CHECK(stream_->IsInline()); - document_->AddIndirectObject(stream_); + document_->AddIndirectObject(AcquireMutableStreamForEdit()); +} + +RetainPtr CPDF_Image::AcquireMutableStreamForEdit() { + return pdfium::WrapRetain(const_cast(stream_.Get())); } RetainPtr CPDF_Image::GetDict() const { @@ -166,6 +177,22 @@ RetainPtr CPDF_Image::GetOC() const { return oc_; } +bool CPDF_Image::RebindStreamIfPromoted() { + if (!stream_ || stream_->GetObjNum() == 0) { + return false; + } + + RetainPtr current_stream = + ToStream(document_->GetIndirectObject(stream_->GetObjNum())); + if (!current_stream || current_stream.Get() == stream_.Get()) { + return false; + } + + stream_ = std::move(current_stream); + FinishInitialization(); + return true; +} + RetainPtr CPDF_Image::InitJPEG( pdfium::span src_span) { std::optional info_opt = @@ -254,7 +281,6 @@ void CPDF_Image::SetJpegImageInline(RetainPtr pFile) { stream_ = pdfium::MakeRetain(std::move(data), std::move(dict)); } - void CPDF_Image::SetImage(const RetainPtr& pBitmap) { int32_t BitmapWidth = pBitmap->GetWidth(); int32_t BitmapHeight = pBitmap->GetHeight(); @@ -393,10 +419,11 @@ void CPDF_Image::SetImage(const RetainPtr& pBitmap) { } void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { - auto decoded = PngModule::Decode( - pdfium::span(png_data, png_size)); - if (!decoded.has_value()) + auto decoded = + PngModule::Decode(pdfium::span(png_data, png_size)); + if (!decoded.has_value()) { return; + } const uint32_t w = decoded->width; const uint32_t h = decoded->height; @@ -431,13 +458,12 @@ void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { parms->SetNewFor("BitsPerComponent", 8); parms->SetNewFor("Columns", static_cast(w)); - dict->SetNewFor( - "Length", pdfium::checked_cast(compressed.size())); + dict->SetNewFor("Length", + pdfium::checked_cast(compressed.size())); // Build alpha (SMask) stream if the PNG had transparency. if (!decoded->alpha.empty()) { - DataVector alpha_compressed = - FlateModule::Encode(decoded->alpha); + DataVector alpha_compressed = FlateModule::Encode(decoded->alpha); RetainPtr mask_dict = CreateXObjectImageDict(w, h); mask_dict->SetNewFor("ColorSpace", "DeviceGray"); @@ -452,8 +478,8 @@ void CPDF_Image::SetPng(const uint8_t* png_data, size_t png_size) { mask_stream->GetObjNum()); } - stream_ = pdfium::MakeRetain( - std::move(compressed), std::move(dict)); + stream_ = + pdfium::MakeRetain(std::move(compressed), std::move(dict)); is_mask_ = false; width_ = w; height_ = h; diff --git a/core/fpdfapi/page/cpdf_image.h b/core/fpdfapi/page/cpdf_image.h index b547578f2d..f9137ff1e9 100644 --- a/core/fpdfapi/page/cpdf_image.h +++ b/core/fpdfapi/page/cpdf_image.h @@ -10,10 +10,10 @@ #include #include "core/fpdfapi/page/cpdf_colorspace.h" +#include "core/fxcrt/data_vector.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/span.h" #include "core/fxcrt/unowned_ptr.h" -#include "core/fxcrt/data_vector.h" class CFX_DIBBase; class CFX_DIBitmap; @@ -53,6 +53,9 @@ class CPDF_Image final : public Retainable { RetainPtr CreateNewDIB() const; RetainPtr LoadDIBBase() const; + // Rebinds the image to the current stream object for its object number. + bool RebindStreamIfPromoted(); + void SetImage(const RetainPtr& pBitmap); void SetJpegImage(RetainPtr pFile); void SetJpegImageInline(RetainPtr pFile); @@ -88,6 +91,8 @@ class CPDF_Image final : public Retainable { ~CPDF_Image() override; void FinishInitialization(); + // Used only by explicit edit paths that need to promote this stream. + RetainPtr AcquireMutableStreamForEdit(); RetainPtr InitJPEG(pdfium::span src_span); RetainPtr CreateXObjectImageDict(int width, int height); @@ -101,7 +106,7 @@ class CPDF_Image final : public Retainable { UnownedPtr const document_; RetainPtr dibbase_; RetainPtr mask_; - RetainPtr stream_; + RetainPtr stream_; RetainPtr oc_; }; diff --git a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp index cc7ba1e177..e90836552a 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp @@ -13,13 +13,58 @@ #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_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; } + + bool IsObjectPromoted(uint32_t objnum) const override { + return objnum == promoted_objnum_; + } + + 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 +129,81 @@ 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 = page_data->GetImage(stream_objnum); + + EXPECT_EQ(last_objnum, document.GetLastObjNum()); + EXPECT_EQ(stream.Get(), image->GetStream().Get()); + + RetainPtr 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/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp index 166ef94f83..ab8967572b 100644 --- a/core/fpdfapi/parser/cpdf_document.cpp +++ b/core/fpdfapi/parser/cpdf_document.cpp @@ -416,6 +416,10 @@ bool CPDF_Document::IsModifiedAPStream(const CPDF_Stream* stream) const { pdfium::Contains(modified_apstream_ids_, stream->GetObjNum()); } +bool CPDF_Document::IsObjectPromoted(uint32_t objnum) const { + return false; +} + int CPDF_Document::GetPageIndex(uint32_t objnum) { uint32_t skip_count = 0; bool bSkipped = false; diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 48b01270c9..1c5988d6fc 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -144,6 +144,10 @@ 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 bool IsObjectPromoted(uint32_t objnum) const; + // CPDF_Parser::ParsedObjectsHolder: bool TryInit() override; RetainPtr ParseIndirectObject(uint32_t objnum) override; From 3cdfb37616a1110c25c8b65ba3063741dcec4c3b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 01:03:19 +0300 Subject: [PATCH 06/28] Install macros --- .../page/cpdf_pageimagecache_unittest.cpp | 13 ++++++- .../fpdfapi/page/cpdf_streamcontentparser.cpp | 3 ++ core/fpdfapi/parser/cpdf_array.cpp | 7 ++++ core/fpdfapi/parser/cpdf_dictionary.cpp | 5 +++ .../parser/cpdf_indirect_object_holder.cpp | 4 ++ .../parser/cpdf_read_only_graph_guard.cpp | 14 +++++++ .../parser/cpdf_read_only_graph_guard.h | 11 +++++- .../cpdf_read_only_graph_guard_unittest.cpp | 14 +++++++ core/fpdfapi/parser/cpdf_stream.cpp | 7 ++++ core/fpdfapi/parser/cpdf_string.cpp | 2 + core/fpdfdoc/cpdf_annotlist_unittest.cpp | 11 ++++-- .../fpdfdoc/cpdf_interactiveform_unittest.cpp | 26 +++++++++---- fpdfsdk/fpdf_annot_embeddertest.cpp | 37 +++++++++++++++++++ fpdfsdk/fpdf_doc_embeddertest.cpp | 20 ++++++++++ fpdfsdk/fpdf_text_embeddertest.cpp | 20 ++++++++++ 15 files changed, 180 insertions(+), 14 deletions(-) diff --git a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp index e90836552a..7e833205db 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp @@ -17,6 +17,7 @@ #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" @@ -139,12 +140,20 @@ TEST(CPDFDocPageDataTest, GetImageDoesNotMutateDocument) { const uint32_t last_objnum = document.GetLastObjNum(); CPDF_DocPageData* page_data = CPDF_DocPageData::FromDocument(&document); - RetainPtr image = page_data->GetImage(stream_objnum); + 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 = page_data->GetImage(stream_objnum); + 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()); } diff --git a/core/fpdfapi/page/cpdf_streamcontentparser.cpp b/core/fpdfapi/page/cpdf_streamcontentparser.cpp index 35cac51d74..49412ec67a 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()) { diff --git a/core/fpdfapi/parser/cpdf_array.cpp b/core/fpdfapi/parser/cpdf_array.cpp index 4939e24be0..605a96107b 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" @@ -204,12 +205,14 @@ RetainPtr CPDF_Array::GetStringAt(size_t index) const { void CPDF_Array::Clear() { CHECK(!IsLocked()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); objects_.clear(); } void CPDF_Array::RemoveAt(size_t index) { CHECK(!IsLocked()); if (index < objects_.size()) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); objects_.erase(objects_.begin() + index); } } @@ -225,6 +228,7 @@ void CPDF_Array::ConvertToIndirectObjectAt(size_t index, return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); pHolder->AddIndirectObject(objects_[index]); objects_[index] = objects_[index]->MakeReference(pHolder); } @@ -251,6 +255,7 @@ CPDF_Object* CPDF_Array::SetAtInternal(size_t index, return nullptr; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); CPDF_Object* pRet = pObj.Get(); objects_[index] = std::move(pObj); return pRet; @@ -266,6 +271,7 @@ CPDF_Object* CPDF_Array::InsertAtInternal(size_t index, return nullptr; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); CPDF_Object* pRet = pObj.Get(); objects_.insert(objects_.begin() + index, std::move(pObj)); return pRet; @@ -276,6 +282,7 @@ CPDF_Object* CPDF_Array::AppendInternal(RetainPtr pObj) { CHECK(pObj); CHECK(pObj->IsInline()); CHECK(!pObj->IsStream()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); CPDF_Object* pRet = pObj.Get(); objects_.push_back(std::move(pObj)); return pRet; diff --git a/core/fpdfapi/parser/cpdf_dictionary.cpp b/core/fpdfapi/parser/cpdf_dictionary.cpp index de35427745..29abba7910 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" @@ -277,6 +278,7 @@ void CPDF_Dictionary::SetFor(const ByteString& key, CPDF_Object* CPDF_Dictionary::SetForInternal(const ByteString& key, RetainPtr pObj) { CHECK(!IsLocked()); + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); if (!pObj) { map_.erase(key); return nullptr; @@ -297,6 +299,7 @@ void CPDF_Dictionary::ConvertToIndirectObjectFor( return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); pHolder->AddIndirectObject(it->second); it->second = it->second->MakeReference(pHolder); } @@ -307,6 +310,7 @@ RetainPtr CPDF_Dictionary::RemoveFor(ByteStringView key) { if (it == map_.end()) { return RetainPtr(); } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); auto node = map_.extract(it); return std::move(node.mapped()); } @@ -324,6 +328,7 @@ void CPDF_Dictionary::ReplaceKey(const ByteString& oldkey, return; } + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); map_[MaybeIntern(newkey)] = std::move(old_it->second); map_.erase(old_it); } diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 6e02e258af..9aa7cabd13 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 { @@ -88,6 +89,7 @@ RetainPtr CPDF_IndirectObjectHolder::ParseIndirectObject( uint32_t CPDF_IndirectObjectHolder::AddIndirectObject( RetainPtr pObj) { + DCHECK_PDF_HOLDER_MUTABLE(); CHECK(!pObj->GetObjNum()); pObj->SetObjNum(++last_obj_num_); indirect_objs_[last_obj_num_] = std::move(pObj); @@ -108,6 +110,7 @@ bool CPDF_IndirectObjectHolder::ReplaceIndirectObjectIfHigherGeneration( return false; } + DCHECK_PDF_HOLDER_MUTABLE(); pObj->SetObjNum(objnum); obj_holder = std::move(pObj); last_obj_num_ = std::max(last_obj_num_, objnum); @@ -120,5 +123,6 @@ void CPDF_IndirectObjectHolder::DeleteIndirectObject(uint32_t objnum) { return; } + DCHECK_PDF_HOLDER_MUTABLE(); indirect_objs_.erase(it); } diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp index 607bc3bf85..61e3c99bdd 100644 --- a/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.cpp @@ -7,6 +7,7 @@ namespace { thread_local bool g_read_only_graph_guard_active = false; +thread_local int g_inline_rewrite_depth = 0; } // namespace @@ -23,3 +24,16 @@ CPDF_ReadOnlyGraphGuard::~CPDF_ReadOnlyGraphGuard() { 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 index 64e1630af6..ee3cae76ea 100644 --- a/core/fpdfapi/parser/cpdf_read_only_graph_guard.h +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard.h @@ -13,14 +13,23 @@ class 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() || (obj)->GetObjNum() == 0) + 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) diff --git a/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp index 2a87202f86..7e761faf91 100644 --- a/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_read_only_graph_guard_unittest.cpp @@ -28,3 +28,17 @@ TEST(CPDFReadOnlyGraphGuardTest, AllowsInlineObjects) { 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_stream.cpp b/core/fpdfapi/parser/cpdf_stream.cpp index d6700e8b3f..461f6b5de3 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,7 @@ CPDF_Stream* CPDF_Stream::AsMutableStream() { } void CPDF_Stream::InitStreamFromFile(RetainPtr file) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); const int size = pdfium::checked_cast(file->GetSize()); data_ = std::move(file); dict_ = pdfium::MakeRetain(); @@ -115,6 +117,7 @@ RetainPtr CPDF_Stream::CloneNonCyclic( } void CPDF_Stream::SetDataAndRemoveFilter(pdfium::span pData) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); SetData(pData); dict_->RemoveFor("Filter"); dict_->RemoveFor(pdfium::stream::kDecodeParms); @@ -131,17 +134,20 @@ void CPDF_Stream::SetDataFromStringstreamAndRemoveFilter( } void CPDF_Stream::SetData(pdfium::span pData) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); DataVector data_copy(pData.begin(), pData.end()); TakeData(std::move(data_copy)); } void CPDF_Stream::TakeData(DataVector data) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); 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); if (stream->tellp() <= 0) { SetData({}); return; @@ -216,5 +222,6 @@ pdfium::span CPDF_Stream::GetInMemoryRawData() const { } void CPDF_Stream::SetLengthInDict(int length) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); dict_->SetNewFor("Length", length); } diff --git a/core/fpdfapi/parser/cpdf_string.cpp b/core/fpdfapi/parser/cpdf_string.cpp index 8bdba046ce..39434349f9 100644 --- a/core/fpdfapi/parser/cpdf_string.cpp +++ b/core/fpdfapi/parser/cpdf_string.cpp @@ -11,6 +11,7 @@ #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/data_vector.h" #include "core/fxcrt/fx_stream.h" @@ -56,6 +57,7 @@ ByteString CPDF_String::GetString() const { } void CPDF_String::SetString(const ByteString& str) { + DCHECK_PDF_GRAPH_MUTABLE_FOR(this); data_ = str; } diff --git a/core/fpdfdoc/cpdf_annotlist_unittest.cpp b/core/fpdfdoc/cpdf_annotlist_unittest.cpp index 2f93c93dc9..2d7260265c 100644 --- a/core/fpdfdoc/cpdf_annotlist_unittest.cpp +++ b/core/fpdfdoc/cpdf_annotlist_unittest.cpp @@ -15,6 +15,7 @@ #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fpdfapi/parser/cpdf_string.h" #include "core/fpdfapi/parser/cpdf_test_document.h" #include "core/fpdfdoc/cpdf_annot.h" @@ -141,10 +142,14 @@ TEST_F(CPDFAnnotListTest, ConstructionPreservesDirectAnnotations) { ASSERT_EQ(0u, annotation->GetObjNum()); const uint32_t last_obj_num = document_->GetLastObjNum(); - CPDF_AnnotList list(page_); + 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()); + 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_interactiveform_unittest.cpp b/core/fpdfdoc/cpdf_interactiveform_unittest.cpp index 9c6a3d97c6..405497b070 100644 --- a/core/fpdfdoc/cpdf_interactiveform_unittest.cpp +++ b/core/fpdfdoc/cpdf_interactiveform_unittest.cpp @@ -11,6 +11,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" @@ -61,18 +62,22 @@ TEST_F(CPDFInteractiveFormTest, LoadFieldsWithReferencedNames) { const uint32_t last_obj_num = doc->GetLastObjNum(); // Let `interactive_form` parse the dictionaries above. - CPDF_InteractiveForm interactive_form(doc.get()); + std::unique_ptr interactive_form; + { + CPDF_ReadOnlyGraphGuard guard; + interactive_form = std::make_unique(doc.get()); + } 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"))); - EXPECT_EQ(1u, - interactive_form.CountFields(WideString::FromASCII("good_string"))); + 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())); + interactive_form->CountFields(WideString::FromASCII("bad_name"))); + EXPECT_EQ(1u, interactive_form->CountFields(WideString())); } TEST_F(CPDFInteractiveFormTest, LoadFieldDoesNotCopyInheritedTypeToParent) { @@ -101,14 +106,19 @@ TEST_F(CPDFInteractiveFormTest, LoadFieldDoesNotCopyInheritedTypeToParent) { ASSERT_FALSE(parent_dict->KeyExist("Ff")); const uint32_t last_obj_num = doc->GetLastObjNum(); - CPDF_InteractiveForm interactive_form(doc.get()); + 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"))); + EXPECT_EQ(1u, + interactive_form->CountFields(WideString::FromASCII("Parent"))); CPDF_FormField* field = - interactive_form.GetField(0, WideString::FromASCII("Parent")); + interactive_form->GetField(0, WideString::FromASCII("Parent")); ASSERT_TRUE(field); EXPECT_EQ(FormFieldType::kTextField, field->GetFieldType()); EXPECT_EQ(1, field->CountControls()); diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 7cfdd97b14..44a0415b4e 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -16,6 +16,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" @@ -413,6 +415,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")); diff --git a/fpdfsdk/fpdf_doc_embeddertest.cpp b/fpdfsdk/fpdf_doc_embeddertest.cpp index aa61b5d4aa..c76a91986c 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_text_embeddertest.cpp b/fpdfsdk/fpdf_text_embeddertest.cpp index f77c04efc1..5e6bb8cc4f 100644 --- a/fpdfsdk/fpdf_text_embeddertest.cpp +++ b/fpdfsdk/fpdf_text_embeddertest.cpp @@ -9,8 +9,11 @@ #include #include "build/build_config.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_read_only_graph_guard.h" #include "core/fxcrt/notreached.h" #include "core/fxge/fx_font.h" +#include "fpdfsdk/cpdfsdk_helpers.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_doc.h" #include "public/fpdf_text.h" @@ -2113,6 +2116,23 @@ TEST_F(FPDFTextEmbedderTest, SmallType3Glyph) { EXPECT_DOUBLE_EQ(61.520000457763672, top); } +TEST_F(FPDFTextEmbedderTest, ReadPurityRenderType3Font) { + ASSERT_TRUE(OpenDocument("bug_1591.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); + ASSERT_TRUE(doc); + const uint32_t last_obj_num = doc->GetLastObjNum(); + + { + CPDF_ReadOnlyGraphGuard guard; + ScopedFPDFBitmap bitmap = RenderLoadedPage(page.get()); + ASSERT_TRUE(bitmap); + } + + EXPECT_EQ(last_obj_num, doc->GetLastObjNum()); +} + TEST_F(FPDFTextEmbedderTest, BigtableTextExtraction) { static constexpr char kExpectedText[] = "{fay,jeff,sanjay,wilsonh,kerr,m3b,tushar,\x02k es,gruber}@google.com"; From 87aec2eaf7ea7d14303985ec9796ba0690036c14 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 22:54:00 +0300 Subject: [PATCH 07/28] Widen all internal seams --- core/fpdfapi/page/cpdf_docpagedata.cpp | 41 +++++ core/fpdfapi/page/cpdf_docpagedata.h | 6 + .../page/cpdf_pageimagecache_unittest.cpp | 7 +- core/fpdfapi/parser/cpdf_document.cpp | 170 +++++++++++++----- core/fpdfapi/parser/cpdf_document.h | 31 +++- .../parser/cpdf_indirect_object_holder.cpp | 6 + .../parser/cpdf_indirect_object_holder.h | 15 +- core/fpdfapi/render/cpdf_docrenderdata.cpp | 22 +++ core/fpdfapi/render/cpdf_docrenderdata.h | 6 + 9 files changed, 239 insertions(+), 65 deletions(-) diff --git a/core/fpdfapi/page/cpdf_docpagedata.cpp b/core/fpdfapi/page/cpdf_docpagedata.cpp index 902d253bd2..8356c69300 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()); @@ -433,6 +452,10 @@ RetainPtr CPDF_DocPageData::GetImage(uint32_t dwStreamObjNum) { return it->second; } + if (fallback_ && !GetDocument()->IsObjectPromoted(dwStreamObjNum)) { + return fallback_->GetImage(dwStreamObjNum); + } + auto pImage = pdfium::MakeRetain(GetDocument(), dwStreamObjNum); image_map_[dwStreamObjNum] = pImage; return pImage; @@ -455,6 +478,10 @@ RetainPtr CPDF_DocPageData::GetIccProfile( return it->second; } + if (fallback_ && CanUseFallbackForObject(pProfileStream.Get())) { + return fallback_->GetIccProfile(pProfileStream); + } + auto pAccessor = pdfium::MakeRetain(pProfileStream); pAccessor->LoadAllDataFiltered(); @@ -489,6 +516,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"); @@ -525,6 +556,16 @@ void CPDF_DocPageData::MaybePurgeFontFileStreamAcc( } } +bool CPDF_DocPageData::CanUseFallbackForObject( + const CPDF_Object* object) const { + if (!object) { + return false; + } + + const uint32_t objnum = object->GetObjNum(); + return objnum != 0 && !GetDocument()->IsObjectPromoted(objnum); +} + std::unique_ptr CPDF_DocPageData::CreateForm( CPDF_Document* document, RetainPtr pPageResources, diff --git a/core/fpdfapi/page/cpdf_docpagedata.h b/core/fpdfapi/page/cpdf_docpagedata.h index a46ff6ecc0..ebaeb47210 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.h +++ b/core/fpdfapi/page/cpdf_docpagedata.h @@ -19,6 +19,7 @@ #include "core/fxcrt/fx_codepage_forward.h" #include "core/fxcrt/fx_coordinates.h" #include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" class CFX_Font; class CPDF_Dictionary; @@ -36,8 +37,11 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, static CPDF_DocPageData* FromDocument(const CPDF_Document* doc); CPDF_DocPageData(); + explicit CPDF_DocPageData(CPDF_DocPageData* fallback); ~CPDF_DocPageData() override; + void SetFallback(CPDF_DocPageData* fallback) { fallback_ = fallback; } + // CPDF_Document::PageDataIface: void ClearStockFont() override; RetainPtr GetFontFileStreamAcc( @@ -109,6 +113,7 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, const CPDF_Dictionary* pResources, std::set* pVisited, std::set* pVisitedInternal); + bool CanUseFallbackForObject(const CPDF_Object* object) const; size_t CalculateEncodingDict(FX_Charset charset, CPDF_Dictionary* pBaseDict); RetainPtr ProcessbCJK( @@ -118,6 +123,7 @@ class CPDF_DocPageData final : public CPDF_Document::PageDataIface, std::function Insert); bool force_clear_ = false; + UnownedPtr fallback_; // Specific destruction order may be required between maps. std::map> diff --git a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp index 7e833205db..9f05687b32 100644 --- a/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp +++ b/core/fpdfapi/page/cpdf_pageimagecache_unittest.cpp @@ -57,8 +57,11 @@ class PromotedImageDocument final : public CPDF_Document { void SetPromotedObject(uint32_t objnum) { promoted_objnum_ = objnum; } - bool IsObjectPromoted(uint32_t objnum) const override { - return objnum == promoted_objnum_; + RetainPtr FindPromotedObject(uint32_t objnum) const override { + return objnum == promoted_objnum_ + ? const_cast(this) + ->GetMutableIndirectObject(objnum) + : nullptr; } private: diff --git a/core/fpdfapi/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp index ab8967572b..59506c882e 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" @@ -195,17 +196,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 +230,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 +284,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 +309,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) { @@ -340,7 +355,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 +371,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 +409,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,20 +431,29 @@ 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; } 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; } } @@ -442,19 +466,19 @@ 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() { @@ -472,7 +496,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( @@ -490,17 +515,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) { @@ -598,7 +624,7 @@ bool CPDF_Document::InsertNewPage(int iPage, return false; } } - page_list_.insert(page_list_.begin() + iPage, pPageDict->GetObjNum()); + InsertPageObjNum(iPage, pPageDict->GetObjNum()); return true; } @@ -607,20 +633,25 @@ 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_) return info_dict_; @@ -630,12 +661,13 @@ RetainPtr CPDF_Document::GetOrCreateInfo() { 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) { @@ -659,22 +691,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 @@ -689,7 +723,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, @@ -773,9 +807,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 1c5988d6fc..0493f6e524 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( @@ -146,7 +147,9 @@ class CPDF_Document : public Observable, // Returns whether `objnum` has been promoted from its base storage into a // document overlay. Always false for ordinary documents. - virtual bool IsObjectPromoted(uint32_t objnum) const; + virtual RetainPtr FindPromotedObject(uint32_t objnum) const; + bool IsObjectPromoted(uint32_t objnum) const; + virtual bool IsLayerDocument() const; // CPDF_Parser::ParsedObjectsHolder: bool TryInit() override; @@ -174,6 +177,18 @@ 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; + + 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 9aa7cabd13..a477a82fd0 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -51,6 +51,12 @@ const CPDF_Object* CPDF_IndirectObjectHolder::GetIndirectObjectInternal( return FilterInvalidObjNum(it->second.Get()); } +RetainPtr CPDF_IndirectObjectHolder::FindLocalIndirectObject( + uint32_t objnum) const { + return pdfium::WrapRetain( + const_cast(GetIndirectObjectInternal(objnum))); +} + RetainPtr CPDF_IndirectObjectHolder::GetOrParseIndirectObject( uint32_t objnum) { return pdfium::WrapRetain(GetOrParseIndirectObjectInternal(objnum)); diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.h b/core/fpdfapi/parser/cpdf_indirect_object_holder.h index 46b9a7b405..c2f35d20f0 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. @@ -74,13 +74,14 @@ 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; 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; std::map> indirect_objs_; WeakPtr byte_string_pool_; diff --git a/core/fpdfapi/render/cpdf_docrenderdata.cpp b/core/fpdfapi/render/cpdf_docrenderdata.cpp index c35e952d74..100d9199a5 100644 --- a/core/fpdfapi/render/cpdf_docrenderdata.cpp +++ b/core/fpdfapi/render/cpdf_docrenderdata.cpp @@ -40,6 +40,9 @@ CPDF_DocRenderData* CPDF_DocRenderData::FromDocument(const CPDF_Document* doc) { CPDF_DocRenderData::CPDF_DocRenderData() = default; +CPDF_DocRenderData::CPDF_DocRenderData(CPDF_DocRenderData* fallback) + : fallback_(fallback) {} + CPDF_DocRenderData::~CPDF_DocRenderData() = default; RetainPtr CPDF_DocRenderData::GetCachedType3( @@ -50,6 +53,11 @@ RetainPtr CPDF_DocRenderData::GetCachedType3( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && font->GetFontDictObjNum() != 0 && + !GetDocument()->IsObjectPromoted(font->GetFontDictObjNum())) { + return fallback_->GetCachedType3(font); + } + auto cache = pdfium::MakeRetain(font); type3_face_map_[font].Reset(cache.Get()); return cache; @@ -63,11 +71,25 @@ RetainPtr CPDF_DocRenderData::GetTransferFunc( return pdfium::WrapRetain(it->second.Get()); } + if (fallback_ && CanUseFallbackForObject(obj.Get())) { + return fallback_->GetTransferFunc(obj); + } + auto func = CreateTransferFunc(obj); transfer_func_map_[obj].Reset(func.Get()); return func; } +bool CPDF_DocRenderData::CanUseFallbackForObject( + const CPDF_Object* object) const { + if (!object) { + return false; + } + + const uint32_t objnum = object->GetObjNum(); + return objnum != 0 && !GetDocument()->IsObjectPromoted(objnum); +} + #if BUILDFLAG(IS_WIN) CFX_PSFontTracker* CPDF_DocRenderData::GetPSFontTracker() { if (!psfont_tracker_) { diff --git a/core/fpdfapi/render/cpdf_docrenderdata.h b/core/fpdfapi/render/cpdf_docrenderdata.h index f11be0caec..691a97c751 100644 --- a/core/fpdfapi/render/cpdf_docrenderdata.h +++ b/core/fpdfapi/render/cpdf_docrenderdata.h @@ -14,6 +14,7 @@ #include "core/fpdfapi/parser/cpdf_document.h" #include "core/fxcrt/observed_ptr.h" #include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/unowned_ptr.h" #if BUILDFLAG(IS_WIN) #include @@ -34,6 +35,7 @@ class CPDF_DocRenderData : public CPDF_Document::RenderDataIface { static CPDF_DocRenderData* FromDocument(const CPDF_Document* doc); CPDF_DocRenderData(); + explicit CPDF_DocRenderData(CPDF_DocRenderData* fallback); ~CPDF_DocRenderData() override; CPDF_DocRenderData(const CPDF_DocRenderData&) = delete; @@ -53,7 +55,11 @@ class CPDF_DocRenderData : public CPDF_Document::RenderDataIface { RetainPtr CreateTransferFunc( RetainPtr pObj) const; + bool CanUseFallbackForObject(const CPDF_Object* object) const; + private: + UnownedPtr fallback_; + // TODO(tsepez): investigate this map outliving its font keys. std::map> type3_face_map_; std::map, From 4e5781ec3ffa80f6561db88866e876d96b142620 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 23:10:27 +0300 Subject: [PATCH 08/28] CPDF_BaseDocument + eager parse + freeze --- core/fpdfapi/parser/BUILD.gn | 3 + core/fpdfapi/parser/cpdf_array.cpp | 12 ++ core/fpdfapi/parser/cpdf_array.h | 1 + core/fpdfapi/parser/cpdf_base_document.cpp | 102 ++++++++++ core/fpdfapi/parser/cpdf_base_document.h | 28 +++ .../parser/cpdf_base_document_unittest.cpp | 186 ++++++++++++++++++ core/fpdfapi/parser/cpdf_boolean.cpp | 2 + core/fpdfapi/parser/cpdf_dictionary.cpp | 11 ++ core/fpdfapi/parser/cpdf_dictionary.h | 1 + .../parser/cpdf_indirect_object_holder.cpp | 17 ++ .../parser/cpdf_indirect_object_holder.h | 3 + core/fpdfapi/parser/cpdf_name.cpp | 2 + core/fpdfapi/parser/cpdf_number.cpp | 2 + core/fpdfapi/parser/cpdf_object.cpp | 17 ++ core/fpdfapi/parser/cpdf_object.h | 7 + core/fpdfapi/parser/cpdf_stream.cpp | 10 + core/fpdfapi/parser/cpdf_stream.h | 1 + core/fpdfapi/parser/cpdf_string.cpp | 2 + fpdfsdk/BUILD.gn | 1 + fpdfsdk/epdf_base_document.cpp | 48 +++++ fpdfsdk/fpdf_view_c_api_test.c | 2 + public/fpdfview.h | 18 ++ 22 files changed, 476 insertions(+) create mode 100644 core/fpdfapi/parser/cpdf_base_document.cpp create mode 100644 core/fpdfapi/parser/cpdf_base_document.h create mode 100644 core/fpdfapi/parser/cpdf_base_document_unittest.cpp create mode 100644 fpdfsdk/epdf_base_document.cpp diff --git a/core/fpdfapi/parser/BUILD.gn b/core/fpdfapi/parser/BUILD.gn index 475c093aa0..12787998fe 100644 --- a/core/fpdfapi/parser/BUILD.gn +++ b/core/fpdfapi/parser/BUILD.gn @@ -11,6 +11,8 @@ 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_cross_ref_avail.cpp", @@ -118,6 +120,7 @@ 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", diff --git a/core/fpdfapi/parser/cpdf_array.cpp b/core/fpdfapi/parser/cpdf_array.cpp index 605a96107b..70a67b24f4 100644 --- a/core/fpdfapi/parser/cpdf_array.cpp +++ b/core/fpdfapi/parser/cpdf_array.cpp @@ -63,6 +63,12 @@ RetainPtr CPDF_Array::CloneNonCyclic( 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) { @@ -205,6 +211,7 @@ RetainPtr CPDF_Array::GetStringAt(size_t index) const { void CPDF_Array::Clear() { CHECK(!IsLocked()); + DCHECK(!IsFrozen()); DCHECK_PDF_GRAPH_MUTABLE_FOR(this); objects_.clear(); } @@ -212,6 +219,7 @@ void CPDF_Array::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); } @@ -229,6 +237,7 @@ void CPDF_Array::ConvertToIndirectObjectAt(size_t index, } DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); pHolder->AddIndirectObject(objects_[index]); objects_[index] = objects_[index]->MakeReference(pHolder); } @@ -256,6 +265,7 @@ CPDF_Object* CPDF_Array::SetAtInternal(size_t index, } DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_[index] = std::move(pObj); return pRet; @@ -272,6 +282,7 @@ CPDF_Object* CPDF_Array::InsertAtInternal(size_t index, } DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_.insert(objects_.begin() + index, std::move(pObj)); return pRet; @@ -283,6 +294,7 @@ CPDF_Object* CPDF_Array::AppendInternal(RetainPtr pObj) { CHECK(pObj->IsInline()); CHECK(!pObj->IsStream()); DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); CPDF_Object* pRet = pObj.Get(); objects_.push_back(std::move(pObj)); return pRet; diff --git a/core/fpdfapi/parser/cpdf_array.h b/core/fpdfapi/parser/cpdf_array.h index 473588dfc8..5b71bf5175 100644 --- a/core/fpdfapi/parser/cpdf_array.h +++ b/core/fpdfapi/parser/cpdf_array.h @@ -166,6 +166,7 @@ class CPDF_Array final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, std::set* pVisited) const override; + void FreezeChildren(std::set* visited) override; std::vector> objects_; WeakPtr pool_; diff --git a/core/fpdfapi/parser/cpdf_base_document.cpp b/core/fpdfapi/parser/cpdf_base_document.cpp new file mode 100644 index 0000000000..1e3fa6fe3f --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.cpp @@ -0,0 +1,102 @@ +// 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 "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" + +namespace { + +void PushIfNew(RetainPtr object, + std::set* visited, + std::queue>* worklist) { + if (!object || !visited->insert(object.Get()).second) { + return; + } + worklist->push(std::move(object)); +} + +} // 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; + } + return EagerlyParseAllReachable() ? CPDF_Parser::SUCCESS + : CPDF_Parser::FORMAT_ERROR; +} + +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 0000000000..693dd2d565 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document.h @@ -0,0 +1,28 @@ +// 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 "core/fpdfapi/parser/cpdf_document.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; + + private: + CPDF_BaseDocument(); + ~CPDF_BaseDocument() override; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_BASE_DOCUMENT_H_ diff --git a/core/fpdfapi/parser/cpdf_base_document_unittest.cpp b/core/fpdfapi/parser/cpdf_base_document_unittest.cpp new file mode 100644 index 0000000000..c1a637df16 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_base_document_unittest.cpp @@ -0,0 +1,186 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fpdfapi/parser/cpdf_base_document.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "core/fpdfapi/page/cpdf_pagemodule.h" +#include "core/fpdfapi/parser/cpdf_array.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_string.h" +#include "core/fxcrt/cfx_read_only_span_stream.h" +#include "core/fxcrt/check.h" +#include "core/fxcrt/data_vector.h" +#include "core/fxcrt/retain_ptr.h" +#include "core/fxcrt/span.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class CPDFBaseDocumentTest : public testing::Test { + protected: + static void SetUpTestSuite() { pdfium::InitializePageModule(); } + static void TearDownTestSuite() { pdfium::DestroyPageModule(); } +}; + +RetainPtr LoadBaseDocumentFromString( + const std::string& data) { + RetainPtr document = + pdfium::MakeRetain(); + auto stream = pdfium::MakeRetain( + pdfium::span(reinterpret_cast(data.data()), + data.size())); + if (document->LoadBaseDoc(std::move(stream), "") != CPDF_Parser::SUCCESS) { + return nullptr; + } + return document; +} + +size_t CountIndirectObjects(const CPDF_IndirectObjectHolder& holder) { + return static_cast(std::distance(holder.begin(), holder.end())); +} + +std::string BuildPdfWithOrphanObject() { + const std::vector objects = { + "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n", + "2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n", + "3 0 obj\n" + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>\n" + "endobj\n", + "4 0 obj\n<< /Orphan true >>\nendobj\n", + }; + + std::ostringstream pdf; + pdf << "%PDF-1.7\n"; + std::vector offsets; + for (const std::string& object : objects) { + offsets.push_back(pdf.tellp()); + pdf << object; + } + + const size_t xref_offset = pdf.tellp(); + pdf << "xref\n0 " << (objects.size() + 1) + << "\n0000000000 65535 f \n"; + for (size_t offset : offsets) { + pdf << std::setw(10) << std::setfill('0') << offset << " 00000 n \n"; + } + pdf << "trailer\n<< /Size " << (objects.size() + 1) + << " /Root 1 0 R >>\nstartxref\n" + << xref_offset << "\n%%EOF\n"; + return pdf.str(); +} + +} // namespace + +TEST_F(CPDFBaseDocumentTest, LoadFreezesReachableGraph) { + const std::string pdf = BuildPdfWithOrphanObject(); + RetainPtr document = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(document); + + const size_t initial_object_count = CountIndirectObjects(*document); + EXPECT_TRUE(document->IsHolderFrozen()); + EXPECT_GT(initial_object_count, 0u); + + ASSERT_TRUE(document->GetPageDictionary(0)); + EXPECT_EQ(initial_object_count, CountIndirectObjects(*document)); + ASSERT_TRUE(document->GetPageDictionary(0)); + EXPECT_EQ(initial_object_count, CountIndirectObjects(*document)); +} + +TEST_F(CPDFBaseDocumentTest, IsFrozenVisibleThroughConstObject) { + auto object = pdfium::MakeRetain(); + object->Freeze(); + const CPDF_Object* const_object = object.Get(); + EXPECT_TRUE(const_object->IsFrozen()); +} + +TEST_F(CPDFBaseDocumentTest, CloneOfFrozenObjectIsMutable) { + auto dict = pdfium::MakeRetain(); + RetainPtr nested = dict->SetNewFor("Kids"); + nested->AppendNew("child"); + dict->Freeze(); + + RetainPtr clone = ToDictionary(dict->Clone()); + ASSERT_TRUE(clone); + EXPECT_FALSE(clone->IsFrozen()); + + RetainPtr cloned_kids = clone->GetMutableArrayFor("Kids"); + ASSERT_TRUE(cloned_kids); + EXPECT_FALSE(cloned_kids->IsFrozen()); + EXPECT_FALSE(cloned_kids->GetMutableObjectAt(0)->IsFrozen()); + + clone->SetNewFor("Mutable", 1); + cloned_kids->AppendNew(2); +} + +TEST_F(CPDFBaseDocumentTest, RetainableRefCountSanity) { + RetainPtr document = + pdfium::MakeRetain(); + EXPECT_TRUE(document->HasOneRef()); + RetainPtr second_reference = document; + EXPECT_FALSE(document->HasOneRef()); + second_reference.Reset(); + EXPECT_TRUE(document->HasOneRef()); +} + +#if DCHECK_IS_ON() +TEST_F(CPDFBaseDocumentTest, HolderMutatorsDcheckAfterFreeze) { + CPDF_IndirectObjectHolder holder; + holder.NewIndirect(); + holder.Freeze(); + + EXPECT_DEATH_IF_SUPPORTED(holder.NewIndirect(), ""); + auto replacement = pdfium::MakeRetain(); + replacement->SetGenNum(1); + EXPECT_DEATH_IF_SUPPORTED(holder.ReplaceIndirectObjectIfHigherGeneration( + 1, std::move(replacement)), + ""); + EXPECT_DEATH_IF_SUPPORTED(holder.DeleteIndirectObject(1), ""); +} + +TEST_F(CPDFBaseDocumentTest, ObjectMutatorsDcheckAfterFreeze) { + auto dict = pdfium::MakeRetain(); + RetainPtr array = dict->SetNewFor("Array"); + array->AppendNew(0); + RetainPtr string = + dict->SetNewFor("String", "value"); + RetainPtr stream = + pdfium::MakeRetain(pdfium::span()); + dict->Freeze(); + stream->Freeze(); + + EXPECT_DEATH_IF_SUPPORTED(dict->SetNewFor("New", 1), ""); + EXPECT_DEATH_IF_SUPPORTED(dict->RemoveFor("String"), ""); + EXPECT_DEATH_IF_SUPPORTED(array->AppendNew(1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->InsertNewAt(0, 1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->SetNewAt(0, 1), ""); + EXPECT_DEATH_IF_SUPPORTED(array->RemoveAt(0), ""); + EXPECT_DEATH_IF_SUPPORTED(array->Clear(), ""); + EXPECT_DEATH_IF_SUPPORTED(stream->SetData(pdfium::span()), + ""); + EXPECT_DEATH_IF_SUPPORTED( + stream->SetDataAndRemoveFilter(pdfium::span()), ""); + EXPECT_DEATH_IF_SUPPORTED(stream->TakeData(DataVector()), ""); + EXPECT_DEATH_IF_SUPPORTED(string->SetString("changed"), ""); +} + +TEST_F(CPDFBaseDocumentTest, ReadMissAfterFreezeDchecks) { + const std::string pdf = BuildPdfWithOrphanObject(); + RetainPtr document = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(document); + EXPECT_TRUE(document->IsHolderFrozen()); + EXPECT_FALSE(document->GetFrozenObjectForLayer(4)); + + EXPECT_DEATH_IF_SUPPORTED(document->GetOrParseIndirectObject(4), ""); +} +#endif // DCHECK_IS_ON() diff --git a/core/fpdfapi/parser/cpdf_boolean.cpp b/core/fpdfapi/parser/cpdf_boolean.cpp index b3ebb95530..4674bc763c 100644 --- a/core/fpdfapi/parser/cpdf_boolean.cpp +++ b/core/fpdfapi/parser/cpdf_boolean.cpp @@ -6,6 +6,7 @@ #include "core/fpdfapi/parser/cpdf_boolean.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" CPDF_Boolean::CPDF_Boolean() = default; @@ -31,6 +32,7 @@ int CPDF_Boolean::GetInteger() const { } void CPDF_Boolean::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); value_ = (str == "true"); } diff --git a/core/fpdfapi/parser/cpdf_dictionary.cpp b/core/fpdfapi/parser/cpdf_dictionary.cpp index 29abba7910..9d23fe69ba 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.cpp +++ b/core/fpdfapi/parser/cpdf_dictionary.cpp @@ -70,6 +70,13 @@ RetainPtr CPDF_Dictionary::CloneNonCyclic( 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); @@ -278,6 +285,7 @@ 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); @@ -300,6 +308,7 @@ void CPDF_Dictionary::ConvertToIndirectObjectFor( } DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); pHolder->AddIndirectObject(it->second); it->second = it->second->MakeReference(pHolder); } @@ -310,6 +319,7 @@ 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()); @@ -329,6 +339,7 @@ void CPDF_Dictionary::ReplaceKey(const ByteString& oldkey, } DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); map_[MaybeIntern(newkey)] = std::move(old_it->second); map_.erase(old_it); } diff --git a/core/fpdfapi/parser/cpdf_dictionary.h b/core/fpdfapi/parser/cpdf_dictionary.h index a6c8653bfa..2b7b13362e 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.h +++ b/core/fpdfapi/parser/cpdf_dictionary.h @@ -148,6 +148,7 @@ class CPDF_Dictionary final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, 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_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index a477a82fd0..7b2c5abc09 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -62,6 +62,19 @@ RetainPtr CPDF_IndirectObjectHolder::GetOrParseIndirectObject( 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) { @@ -74,6 +87,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); @@ -96,6 +110,7 @@ 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); @@ -117,6 +132,7 @@ bool CPDF_IndirectObjectHolder::ReplaceIndirectObjectIfHigherGeneration( } DCHECK_PDF_HOLDER_MUTABLE(); + DCHECK(!frozen_); pObj->SetObjNum(objnum); obj_holder = std::move(pObj); last_obj_num_ = std::max(last_obj_num_, objnum); @@ -130,5 +146,6 @@ void CPDF_IndirectObjectHolder::DeleteIndirectObject(uint32_t objnum) { } 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 c2f35d20f0..d2d51d8e12 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.h +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.h @@ -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_; @@ -83,6 +85,7 @@ class CPDF_IndirectObjectHolder { friend class CPDF_Reference; uint32_t last_obj_num_ = 0; + bool frozen_ = false; std::map> indirect_objs_; WeakPtr byte_string_pool_; }; diff --git a/core/fpdfapi/parser/cpdf_name.cpp b/core/fpdfapi/parser/cpdf_name.cpp index 8fd027aa4b..95204b943f 100644 --- a/core/fpdfapi/parser/cpdf_name.cpp +++ b/core/fpdfapi/parser/cpdf_name.cpp @@ -8,6 +8,7 @@ #include "core/fpdfapi/parser/fpdf_parser_decode.h" #include "core/fpdfapi/parser/fpdf_parser_utility.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" CPDF_Name::CPDF_Name(WeakPtr pPool, const ByteString& str) @@ -32,6 +33,7 @@ ByteString CPDF_Name::GetString() const { } void CPDF_Name::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); name_ = str; } diff --git a/core/fpdfapi/parser/cpdf_number.cpp b/core/fpdfapi/parser/cpdf_number.cpp index cd5a9ed53a..7179d0dd03 100644 --- a/core/fpdfapi/parser/cpdf_number.cpp +++ b/core/fpdfapi/parser/cpdf_number.cpp @@ -9,6 +9,7 @@ #include #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" +#include "core/fxcrt/check.h" #include "core/fxcrt/fx_stream.h" #include "core/fxcrt/fx_string_wrappers.h" @@ -55,6 +56,7 @@ CPDF_Number* CPDF_Number::AsMutableNumber() { } void CPDF_Number::SetString(const ByteString& str) { + DCHECK(!IsFrozen()); number_ = FX_Number(str.AsStringView()); } diff --git a/core/fpdfapi/parser/cpdf_object.cpp b/core/fpdfapi/parser/cpdf_object.cpp index 1f901acd09..68cbeef0bd 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())); } diff --git a/core/fpdfapi/parser/cpdf_object.h b/core/fpdfapi/parser/cpdf_object.h index 8de91f6441..a8a7ab5fac 100644 --- a/core/fpdfapi/parser/cpdf_object.h +++ b/core/fpdfapi/parser/cpdf_object.h @@ -74,6 +74,10 @@ class CPDF_Object : public Retainable { // Create a deep copy of the object. virtual RetainPtr Clone() const = 0; + 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; @@ -115,6 +119,8 @@ class CPDF_Object : public Retainable { virtual RetainPtr MakeReference( CPDF_IndirectObjectHolder* holder) const; + virtual void FreezeChildren(std::set* visited); + RetainPtr GetDirect() const; // Wraps virtual method. RetainPtr GetMutableDirect(); // Wraps virtual method. RetainPtr GetDict() const; // Wraps virtual method. @@ -156,6 +162,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_stream.cpp b/core/fpdfapi/parser/cpdf_stream.cpp index 461f6b5de3..a98bec1353 100644 --- a/core/fpdfapi/parser/cpdf_stream.cpp +++ b/core/fpdfapi/parser/cpdf_stream.cpp @@ -89,6 +89,7 @@ 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(); @@ -116,8 +117,13 @@ RetainPtr CPDF_Stream::CloneNonCyclic( 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); @@ -135,12 +141,14 @@ 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); @@ -148,6 +156,7 @@ void CPDF_Stream::TakeData(DataVector data) { void CPDF_Stream::SetDataFromStringstream(fxcrt::ostringstream* stream) { DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); if (stream->tellp() <= 0) { SetData({}); return; @@ -223,5 +232,6 @@ pdfium::span CPDF_Stream::GetInMemoryRawData() const { void CPDF_Stream::SetLengthInDict(int length) { DCHECK_PDF_GRAPH_MUTABLE_FOR(this); + DCHECK(!IsFrozen()); dict_->SetNewFor("Length", length); } diff --git a/core/fpdfapi/parser/cpdf_stream.h b/core/fpdfapi/parser/cpdf_stream.h index ffeea79cac..a618df5b85 100644 --- a/core/fpdfapi/parser/cpdf_stream.h +++ b/core/fpdfapi/parser/cpdf_stream.h @@ -90,6 +90,7 @@ class CPDF_Stream final : public CPDF_Object { RetainPtr CloneNonCyclic( bool bDirect, 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 39434349f9..2b4553d792 100644 --- a/core/fpdfapi/parser/cpdf_string.cpp +++ b/core/fpdfapi/parser/cpdf_string.cpp @@ -13,6 +13,7 @@ #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" @@ -58,6 +59,7 @@ 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/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index b903c2b31f..6fbabc94d1 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -7,6 +7,7 @@ import("../testing/test.gni") source_set("fpdfsdk") { sources = [ + "epdf_base_document.cpp", "epdf_outline.cpp", "epdf_png_shim.cpp", "epdf_jpeg_shim.cpp", diff --git a/fpdfsdk/epdf_base_document.cpp b/fpdfsdk/epdf_base_document.cpp new file mode 100644 index 0000000000..212b329c11 --- /dev/null +++ b/fpdfsdk/epdf_base_document.cpp @@ -0,0 +1,48 @@ +// 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 "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fxcrt/retain_ptr.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); +} + +} // namespace + +FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV +EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password) { + if (!pFileAccess) { + return nullptr; + } + + RetainPtr base = pdfium::MakeRetain(); + CPDF_Parser::Error error = base->LoadBaseDoc( + pdfium::MakeRetain(pFileAccess), password); + if (error != CPDF_Parser::SUCCESS) { + ProcessParseError(error); + return nullptr; + } + + return EPDFBaseDocumentFromCPDFBaseDocument(base.Leak()); +} + +FPDF_EXPORT void FPDF_CALLCONV +EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base) { + RetainPtr retained; + retained.Unleak(CPDFBaseDocumentFromEPDFBaseDocument(base)); +} diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index a4f21974cf..88210ef78b 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -534,6 +534,8 @@ int CheckPDFiumCApi() { CHK(FPDF_GetXFAPacketName); CHK(FPDF_InitLibrary); CHK(FPDF_InitLibraryWithConfig); + CHK(EPDF_LoadBaseDocument); + CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); CHK(FPDF_LoadDocument); CHK(FPDF_LoadMemDocument); diff --git a/public/fpdfview.h b/public/fpdfview.h index 2af9a0ac41..e9b13000a7 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,23 @@ 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_ReleaseBaseDocument +// Release a base document returned by EPDF_LoadBaseDocument(). +FPDF_EXPORT void FPDF_CALLCONV +EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base); + // Function: FPDF_GetFileVersion // Get the file version of the given PDF document. // Parameters: From ef257bad4c3c520cd5b6e3167065e93003ca8da7 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 23:25:50 +0300 Subject: [PATCH 09/28] CPDF_LayerDocument (read fall-through) --- core/fpdfapi/parser/BUILD.gn | 3 + .../parser/cpdf_indirect_object_holder.cpp | 7 +- core/fpdfapi/parser/cpdf_layer_document.cpp | 173 ++++++++++++++++++ core/fpdfapi/parser/cpdf_layer_document.h | 73 ++++++++ .../parser/cpdf_layer_document_unittest.cpp | 139 ++++++++++++++ fpdfsdk/BUILD.gn | 1 + fpdfsdk/epdf_layer.cpp | 102 +++++++++++ fpdfsdk/fpdf_view_c_api_test.c | 4 + fpdfsdk/fpdf_view_embeddertest.cpp | 31 ++++ public/fpdfview.h | 44 +++++ 10 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 core/fpdfapi/parser/cpdf_layer_document.cpp create mode 100644 core/fpdfapi/parser/cpdf_layer_document.h create mode 100644 core/fpdfapi/parser/cpdf_layer_document_unittest.cpp create mode 100644 fpdfsdk/epdf_layer.cpp diff --git a/core/fpdfapi/parser/BUILD.gn b/core/fpdfapi/parser/BUILD.gn index 12787998fe..c3ccf34648 100644 --- a/core/fpdfapi/parser/BUILD.gn +++ b/core/fpdfapi/parser/BUILD.gn @@ -35,6 +35,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", @@ -126,6 +128,7 @@ pdfium_unittest_source_set("unittests") { "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", diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 7b2c5abc09..9c8f87b836 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -53,8 +53,13 @@ const CPDF_Object* CPDF_IndirectObjectHolder::GetIndirectObjectInternal( 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(GetIndirectObjectInternal(objnum))); + const_cast(FilterInvalidObjNum(it->second.Get()))); } RetainPtr CPDF_IndirectObjectHolder::GetOrParseIndirectObject( diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp new file mode 100644 index 0000000000..18e7626c54 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -0,0 +1,173 @@ +// 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 "core/fpdfapi/page/cpdf_docpagedata.h" +#include "core/fpdfapi/parser/cpdf_base_document.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" + +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(); +} + +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; +} + +RetainPtr CPDF_LayerDocument::ParseIndirectObject( + uint32_t objnum) { + NOTREACHED(); + return nullptr; +} + +RetainPtr CPDF_LayerDocument::GetMutableIndirectObject( + uint32_t objnum) { + NOTREACHED(); + return nullptr; +} + +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_) { + ingest_status_ = OpenStatus::kOpenFailed; + return; + } + + const FX_FILESIZE base_end = base_parser->GetDocumentSize(); + const FX_FILESIZE layer_size = file_access_->GetSize(); + if (layer_size < base_end) { + ingest_status_ = OpenStatus::kBaseLayerMismatch; + return; + } + if (layer_size > base_end) { + // Full appended-xref ingest lands with the delta parser. Until then, fail + // closed instead of silently ignoring a caller-provided delta. + ingest_status_ = OpenStatus::kMalformedDelta; + } +} diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h new file mode 100644 index 0000000000..442f9e2f16 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -0,0 +1,73 @@ +// 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; + uint32_t GetUserPermissions(bool get_owner_perms) const override; + RetainPtr FindPromotedObject(uint32_t objnum) const override; + bool IsLayerDocument() 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(); + + RetainPtr const base_; + RetainPtr const 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 0000000000..4444b49067 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -0,0 +1,139 @@ +// 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 "core/fpdfapi/page/cpdf_pagemodule.h" +#include "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_object.h" +#include "core/fpdfapi/parser/cpdf_parser.h" +#include "core/fxcrt/cfx_read_only_span_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(); } +}; + +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(); +} + +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; +} + +} // namespace + +TEST_F(CPDFLayerDocumentTest, FreshLayerFallsThroughToFrozenBase) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + + auto layer = + std::make_unique(base, MakeStreamForString(pdf)); + + 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, MakeStreamForString(pdf)); + + ASSERT_TRUE(layer->GetIndirectObject(1)); + layer->DeleteIndirectObject(1); + EXPECT_TRUE(layer->GetIndirectObject(1)); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +TEST_F(CPDFLayerDocumentTest, AppendedBytesFailClosedUntilDeltaIngestLands) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + const std::string layer_bytes = pdf + "\n% appended delta placeholder\n"; + + auto layer = std::make_unique( + base, MakeStreamForString(layer_bytes)); + + EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kMalformedDelta, + layer->ingest_status()); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); +} + +#if DCHECK_IS_ON() +TEST_F(CPDFLayerDocumentTest, MutatorsDcheckUntilCowSliceLands) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = + std::make_unique(base, MakeStreamForString(pdf)); + + EXPECT_DEATH_IF_SUPPORTED(layer->GetMutableIndirectObject(1), ""); + EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); +} +#endif // DCHECK_IS_ON() diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 6fbabc94d1..99aacb3175 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -8,6 +8,7 @@ import("../testing/test.gni") source_set("fpdfsdk") { sources = [ "epdf_base_document.cpp", + "epdf_layer.cpp", "epdf_outline.cpp", "epdf_png_shim.cpp", "epdf_jpeg_shim.cpp", diff --git a/fpdfsdk/epdf_layer.cpp b/fpdfsdk/epdf_layer.cpp new file mode 100644 index 0000000000..738b724a00 --- /dev/null +++ b/fpdfsdk/epdf_layer.cpp @@ -0,0 +1,102 @@ +// 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 "core/fpdfapi/parser/cpdf_base_document.h" +#include "core/fpdfapi/parser/cpdf_layer_document.h" +#include "core/fxcrt/retain_ptr.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); +} + +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; + } +} + +} // namespace + +FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV +EPDFLayer_OpenLayer(EPDF_BASE_DOCUMENT base, + FPDF_FILEACCESS* pFileAccess, + FPDF_BYTESTRING password, + EPDFLayerOpenStatus* out_status) { + if (out_status) { + *out_status = EPDFLayerOpenStatus_kOpenFailed; + } + if (!base || !pFileAccess) { + return nullptr; + } + + // Slice 7.2 layers share the base parser/security state; password handling is + // already complete when the base is loaded. + (void)password; + + CPDF_BaseDocument* base_doc = CPDFBaseDocumentFromEPDFBaseDocument(base); + RetainPtr retained_base = pdfium::WrapRetain(base_doc); + auto layer = std::make_unique( + std::move(retained_base), + pdfium::MakeRetain(pFileAccess)); + + const EPDFLayerOpenStatus status = ToPublicStatus(layer->ingest_status()); + if (out_status) { + *out_status = status; + } + if (status != EPDFLayerOpenStatus_kSuccess) { + return nullptr; + } + + return FPDFDocumentFromCPDFDocument(layer.release()); +} + +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; +} diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index 88210ef78b..f51db07ece 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -535,6 +535,10 @@ int CheckPDFiumCApi() { CHK(FPDF_InitLibrary); CHK(FPDF_InitLibraryWithConfig); CHK(EPDF_LoadBaseDocument); + CHK(EPDFLayer_GetBaseDocument); + CHK(EPDFLayer_GetPromotedObjectCount); + CHK(EPDFLayer_IsObjectPromoted); + CHK(EPDFLayer_OpenLayer); CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); CHK(FPDF_LoadDocument); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 56b87842e4..1131e8e967 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -18,6 +18,7 @@ #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/fpdfview.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" @@ -659,6 +660,36 @@ 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); + + { + FileAccessForTesting layer_access("rectangles.pdf"); + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, &layer_access, 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, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/public/fpdfview.h b/public/fpdfview.h index e9b13000a7..dad1f1246c 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -580,6 +580,50 @@ EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password); 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 - A structure for accessing the materialized layer +// bytes. For Slice 7.2 this must be the base bytes +// with no appended delta. +// 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_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: From b345d990874d06fbcbc07001b702f5079066a8c4 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 23:44:47 +0300 Subject: [PATCH 10/28] COW on actual mutable handle USE --- core/fpdfapi/page/cpdf_annotcontext.cpp | 48 ++++++- core/fpdfapi/page/cpdf_annotcontext.h | 11 +- core/fpdfapi/page/cpdf_contentparser.cpp | 8 +- core/fpdfapi/page/cpdf_form.cpp | 38 +++-- core/fpdfapi/page/cpdf_form.h | 12 +- core/fpdfapi/page/cpdf_page.cpp | 23 ++- core/fpdfapi/page/cpdf_page.h | 4 + core/fpdfapi/page/cpdf_pageobjectholder.cpp | 132 ++++++++++++++++++ core/fpdfapi/page/cpdf_pageobjectholder.h | 26 ++-- .../fpdfapi/page/cpdf_streamcontentparser.cpp | 7 +- core/fpdfapi/parser/cpdf_array.cpp | 19 ++- core/fpdfapi/parser/cpdf_dictionary.cpp | 21 ++- .../parser/cpdf_indirect_object_holder.cpp | 2 +- core/fpdfapi/parser/cpdf_layer_document.cpp | 22 +++ core/fpdfapi/parser/cpdf_layer_document.h | 2 + .../parser/cpdf_layer_document_unittest.cpp | 9 ++ core/fpdfapi/parser/cpdf_object.h | 5 +- core/fpdfapi/parser/cpdf_reference.cpp | 5 + core/fpdfapi/parser/cpdf_reference.h | 1 + core/fpdfapi/render/cpdf_renderstatus.cpp | 4 +- core/fpdfdoc/cpdf_annot.cpp | 8 +- fpdfsdk/cpdfsdk_renderpage.cpp | 5 +- fpdfsdk/fpdf_annot.cpp | 5 +- fpdfsdk/fpdf_view.cpp | 107 ++++++++------ fpdfsdk/fpdf_view_embeddertest.cpp | 26 ++++ 25 files changed, 449 insertions(+), 101 deletions(-) diff --git a/core/fpdfapi/page/cpdf_annotcontext.cpp b/core/fpdfapi/page/cpdf_annotcontext.cpp index dcaf973548..75fa276107 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 b1d008bf26..ebc16c97a1 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 4e74180fcf..80e7c9a620 100644 --- a/core/fpdfapi/page/cpdf_contentparser.cpp +++ b/core/fpdfapi/page/cpdf_contentparser.cpp @@ -97,9 +97,11 @@ CPDF_ContentParser::CPDF_ContentParser( 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_, + 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); diff --git a/core/fpdfapi/page/cpdf_form.cpp b/core/fpdfapi/page/cpdf_form.cpp index c363862ace..76daf765ba 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; } @@ -47,13 +49,13 @@ CPDF_Form::CPDF_Form(CPDF_Document* doc, CPDF_Dictionary* pParentResources) : CPDF_PageObjectHolder( doc, - pFormStream->GetMutableDict(), + pdfium::WrapRetain( + const_cast(pFormStream->GetDict().Get())), pPageResources, - pdfium::WrapRetain(ChooseResourcesDict( - const_cast( - pFormStream->GetDict()->GetDictFor("Resources").Get()), + pdfium::WrapRetain(const_cast(ChooseResourcesDict( + pFormStream->GetDict()->GetDictFor("Resources").Get(), pParentResources, - pPageResources.Get()))), + 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 354c50d051..80ab06eeec 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_page.cpp b/core/fpdfapi/page/cpdf_page.cpp index f0f7c25993..2490f624a4 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 ba1da8dac4..ae9b829f3c 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_pageobjectholder.cpp b/core/fpdfapi/page/cpdf_pageobjectholder.cpp index f05f01a61a..4e44212c61 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 898a3d1c00..5ac3a6cc77 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 49412ec67a..64f366c2fc 100644 --- a/core/fpdfapi/page/cpdf_streamcontentparser.cpp +++ b/core/fpdfapi/page/cpdf_streamcontentparser.cpp @@ -401,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), diff --git a/core/fpdfapi/parser/cpdf_array.cpp b/core/fpdfapi/parser/cpdf_array.cpp index 70a67b24f4..4849c27698 100644 --- a/core/fpdfapi/parser/cpdf_array.cpp +++ b/core/fpdfapi/parser/cpdf_array.cpp @@ -109,7 +109,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) { @@ -121,7 +121,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) { @@ -182,7 +183,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) { @@ -190,7 +199,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) { @@ -198,7 +207,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 { diff --git a/core/fpdfapi/parser/cpdf_dictionary.cpp b/core/fpdfapi/parser/cpdf_dictionary.cpp index 9d23fe69ba..4967f5b850 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.cpp +++ b/core/fpdfapi/parser/cpdf_dictionary.cpp @@ -107,8 +107,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 { @@ -177,8 +177,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( @@ -201,7 +209,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) { @@ -224,8 +232,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( diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 9c8f87b836..0ed55f4591 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -38,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( diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index 18e7626c54..770e2cdeca 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -58,6 +58,28 @@ 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; +} + uint32_t CPDF_LayerDocument::GetUserPermissions(bool get_owner_perms) const { return base_->GetUserPermissions(get_owner_perms); } diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h index 442f9e2f16..3d21378338 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -38,6 +38,8 @@ class CPDF_LayerDocument final : public CPDF_Document { // CPDF_Document: CPDF_Parser* GetParser() const override; + RetainPtr GetMutableRoot() override; + RetainPtr GetMutableInfo() override; uint32_t GetUserPermissions(bool get_owner_perms) const override; RetainPtr FindPromotedObject(uint32_t objnum) const override; bool IsLayerDocument() const override; diff --git a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp index 4444b49067..74ff894e49 100644 --- a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -11,6 +11,7 @@ #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_dictionary.h" @@ -135,5 +136,13 @@ TEST_F(CPDFLayerDocumentTest, MutatorsDcheckUntilCowSliceLands) { EXPECT_DEATH_IF_SUPPORTED(layer->GetMutableIndirectObject(1), ""); EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); + + RetainPtr page_dict = layer->GetPageDictionary(0); + ASSERT_TRUE(page_dict); + auto page = pdfium::MakeRetain( + layer.get(), + pdfium::WrapRetain(const_cast(page_dict.Get()))); + EXPECT_EQ(0u, layer->GetPromotedObjectCount()); + EXPECT_DEATH_IF_SUPPORTED(page->GetMutableDict(), ""); } #endif // DCHECK_IS_ON() diff --git a/core/fpdfapi/parser/cpdf_object.h b/core/fpdfapi/parser/cpdf_object.h index a8a7ab5fac..4915d04ef4 100644 --- a/core/fpdfapi/parser/cpdf_object.h +++ b/core/fpdfapi/parser/cpdf_object.h @@ -121,8 +121,9 @@ class CPDF_Object : public Retainable { virtual void FreezeChildren(std::set* visited); - RetainPtr GetDirect() const; // Wraps virtual method. - RetainPtr GetMutableDirect(); // Wraps virtual method. + RetainPtr GetDirect() const; // Wraps virtual method. + virtual RetainPtr GetMutableDirect(); + // Wraps virtual method. RetainPtr GetDict() const; // Wraps virtual method. RetainPtr GetMutableDict(); // Wraps virtual method. diff --git a/core/fpdfapi/parser/cpdf_reference.cpp b/core/fpdfapi/parser/cpdf_reference.cpp index 8dc56d5281..c0e6fd8e35 100644 --- a/core/fpdfapi/parser/cpdf_reference.cpp +++ b/core/fpdfapi/parser/cpdf_reference.cpp @@ -49,6 +49,11 @@ RetainPtr CPDF_Reference::Clone() const { return CloneObjectNonCyclic(false); } +RetainPtr CPDF_Reference::GetMutableDirect() { + return obj_list_ ? obj_list_->GetMutableIndirectObject(ref_obj_num_) + : nullptr; +} + RetainPtr CPDF_Reference::CloneNonCyclic( bool bDirect, std::set* pVisited) const { diff --git a/core/fpdfapi/parser/cpdf_reference.h b/core/fpdfapi/parser/cpdf_reference.h index 63baff1b0a..45e04d48d1 100644 --- a/core/fpdfapi/parser/cpdf_reference.h +++ b/core/fpdfapi/parser/cpdf_reference.h @@ -22,6 +22,7 @@ class CPDF_Reference final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr GetMutableDirect() override; ByteString GetString() const override; float GetNumber() const override; int GetInteger() const override; diff --git a/core/fpdfapi/render/cpdf_renderstatus.cpp b/core/fpdfapi/render/cpdf_renderstatus.cpp index 6f8ec8e54b..8494f1c615 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/cpdf_annot.cpp b/core/fpdfdoc/cpdf_annot.cpp index 8a65fcdbf6..d8bc6a2e25 100644 --- a/core/fpdfdoc/cpdf_annot.cpp +++ b/core/fpdfdoc/cpdf_annot.cpp @@ -277,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(); @@ -955,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); diff --git a/fpdfsdk/cpdfsdk_renderpage.cpp b/fpdfsdk/cpdfsdk_renderpage.cpp index 4def06a07c..39ef4d1cbb 100644 --- a/fpdfsdk/cpdfsdk_renderpage.cpp +++ b/fpdfsdk/cpdfsdk_renderpage.cpp @@ -11,6 +11,7 @@ #include "build/build_config.h" #include "core/fpdfapi/page/cpdf_pageimagecache.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/render/cpdf_pagerendercontext.h" #include "core/fpdfapi/render/cpdf_progressiverenderer.h" #include "core/fpdfapi/render/cpdf_renderoptions.h" @@ -62,7 +63,9 @@ void RenderPageImpl(CPDF_PageRenderContext* context, context->device_->SetBaseClip(clipping_rect); context->device_->SetClip_Rect(clipping_rect); context->context_ = std::make_unique( - pPage->GetDocument(), pPage->GetMutablePageResources(), + pPage->GetDocument(), + pdfium::WrapRetain( + const_cast(pPage->GetPageResources().Get())), pPage->GetPageImageCache()); context->context_->AppendLayer(pPage, matrix); diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index b893959c5b..4db7fa00b7 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -1120,7 +1120,7 @@ FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV FPDFPage_GetAnnot(FPDF_PAGE page, } auto pNewAnnot = std::make_unique( - std::move(dict), IPDFPageFromFPDFPage(page)); + std::move(dict), IPDFPageFromFPDFPage(page), index); // Caller takes ownership. return FPDFAnnotationFromCPDFAnnotContext(pNewAnnot.release()); @@ -3547,7 +3547,8 @@ EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { 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()); } } diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index 6a4278cf8e..2cef39bfe8 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 @@ -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; } @@ -712,11 +714,13 @@ EPDFPage_GetObjectNumber(FPDF_PAGE page) { // Note: CPDFPageFromFPDFPage() returns null for XFA pages, so this function // returns 0 for XFA pages (documented in the header). CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) + if (!pPage) { return 0; + } const CPDF_Dictionary* dict = pPage->GetDict().Get(); - if (!dict) + if (!dict) { return 0; + } return dict->GetObjNum(); } @@ -1059,30 +1063,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 +1104,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 +1125,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 +1151,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 +1178,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 +1202,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 +1535,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 +1577,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 +1617,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) { diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 1131e8e967..f9d2a108d5 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -690,6 +690,32 @@ TEST_F(FPDFViewEmbedderTest, OpenFreshLayerRendersWithEmptyOverlay) { 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); + + { + FileAccessForTesting layer_access("annotation_stamp_with_ap.pdf"); + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, &layer_access, 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, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); From a59180571428588a4f35fdc05ea89e0d2fd4a02d Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 May 2026 23:57:39 +0300 Subject: [PATCH 11/28] CloneForHolder + PromoteFromBase --- core/fpdfapi/parser/cpdf_array.cpp | 22 +++ core/fpdfapi/parser/cpdf_array.h | 5 + core/fpdfapi/parser/cpdf_dictionary.cpp | 24 +++ core/fpdfapi/parser/cpdf_dictionary.h | 5 + .../parser/cpdf_indirect_object_holder.cpp | 14 ++ .../parser/cpdf_indirect_object_holder.h | 1 + core/fpdfapi/parser/cpdf_layer_document.cpp | 40 +++- core/fpdfapi/parser/cpdf_layer_document.h | 1 + .../parser/cpdf_layer_document_unittest.cpp | 176 +++++++++++++++++- core/fpdfapi/parser/cpdf_object.cpp | 12 ++ core/fpdfapi/parser/cpdf_object.h | 10 + core/fpdfapi/parser/cpdf_reference.cpp | 12 ++ core/fpdfapi/parser/cpdf_reference.h | 5 + core/fpdfapi/parser/cpdf_stream.cpp | 23 +++ core/fpdfapi/parser/cpdf_stream.h | 5 + fpdfsdk/fpdf_view_embeddertest.cpp | 28 +++ 16 files changed, 371 insertions(+), 12 deletions(-) diff --git a/core/fpdfapi/parser/cpdf_array.cpp b/core/fpdfapi/parser/cpdf_array.cpp index 4849c27698..5665b4ebca 100644 --- a/core/fpdfapi/parser/cpdf_array.cpp +++ b/core/fpdfapi/parser/cpdf_array.cpp @@ -47,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 { @@ -63,6 +69,22 @@ 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); diff --git a/core/fpdfapi/parser/cpdf_array.h b/core/fpdfapi/parser/cpdf_array.h index 5b71bf5175..053d6cb9ad 100644 --- a/core/fpdfapi/parser/cpdf_array.h +++ b/core/fpdfapi/parser/cpdf_array.h @@ -33,6 +33,8 @@ class CPDF_Array final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; CPDF_Array* AsMutableArray() override; bool WriteTo(IFX_ArchiveStream* archive, const CPDF_Encryptor* encryptor) const override; @@ -166,6 +168,9 @@ 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_; diff --git a/core/fpdfapi/parser/cpdf_dictionary.cpp b/core/fpdfapi/parser/cpdf_dictionary.cpp index 4967f5b850..fa9e447458 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.cpp +++ b/core/fpdfapi/parser/cpdf_dictionary.cpp @@ -52,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 { @@ -70,6 +76,24 @@ 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) { diff --git a/core/fpdfapi/parser/cpdf_dictionary.h b/core/fpdfapi/parser/cpdf_dictionary.h index 2b7b13362e..0f8b33a52a 100644 --- a/core/fpdfapi/parser/cpdf_dictionary.h +++ b/core/fpdfapi/parser/cpdf_dictionary.h @@ -36,6 +36,8 @@ class CPDF_Dictionary final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; CPDF_Dictionary* AsMutableDictionary() override; bool WriteTo(IFX_ArchiveStream* archive, const CPDF_Encryptor* encryptor) const override; @@ -148,6 +150,9 @@ 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; diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp index 0ed55f4591..b9de488ec3 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.cpp @@ -62,6 +62,20 @@ RetainPtr CPDF_IndirectObjectHolder::FindLocalIndirectObject( 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)); diff --git a/core/fpdfapi/parser/cpdf_indirect_object_holder.h b/core/fpdfapi/parser/cpdf_indirect_object_holder.h index d2d51d8e12..51529e016c 100644 --- a/core/fpdfapi/parser/cpdf_indirect_object_holder.h +++ b/core/fpdfapi/parser/cpdf_indirect_object_holder.h @@ -80,6 +80,7 @@ class CPDF_IndirectObjectHolder { 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; diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index 770e2cdeca..e0a146aab9 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -101,8 +101,10 @@ RetainPtr CPDF_LayerDocument::ParseIndirectObject( RetainPtr CPDF_LayerDocument::GetMutableIndirectObject( uint32_t objnum) { - NOTREACHED(); - return nullptr; + if (RetainPtr local = FindLocalIndirectObject(objnum)) { + return local; + } + return PromoteFromBase(objnum); } void CPDF_LayerDocument::DeleteIndirectObject(uint32_t objnum) { @@ -193,3 +195,37 @@ void CPDF_LayerDocument::IngestCurrentDelta() { ingest_status_ = OpenStatus::kMalformedDelta; } } + +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 index 3d21378338..2cfc44a6e6 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -65,6 +65,7 @@ class CPDF_LayerDocument final : public CPDF_Document { private: void InitializeFromBase(); void IngestCurrentDelta(); + RetainPtr PromoteFromBase(uint32_t objnum); RetainPtr const base_; RetainPtr const file_access_; diff --git a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp index 74ff894e49..421638720b 100644 --- a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -15,8 +15,10 @@ #include "core/fpdfapi/page/cpdf_pagemodule.h" #include "core/fpdfapi/parser/cpdf_base_document.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/retain_ptr.h" #include "core/fxcrt/span.h" @@ -58,6 +60,35 @@ std::string BuildSimplePdf() { 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(); +} + RetainPtr MakeStreamForString(const std::string& data) { return pdfium::MakeRetain( pdfium::span(reinterpret_cast(data.data()), data.size())); @@ -74,6 +105,17 @@ RetainPtr LoadBaseDocumentFromString( 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_F(CPDFLayerDocumentTest, FreshLayerFallsThroughToFrozenBase) { @@ -126,23 +168,137 @@ TEST_F(CPDFLayerDocumentTest, AppendedBytesFailClosedUntilDeltaIngestLands) { EXPECT_EQ(0u, layer->GetPromotedObjectCount()); } -#if DCHECK_IS_ON() -TEST_F(CPDFLayerDocumentTest, MutatorsDcheckUntilCowSliceLands) { +TEST_F(CPDFLayerDocumentTest, GetMutableIndirectObjectPromotesFromBase) { const std::string pdf = BuildSimplePdf(); RetainPtr base = LoadBaseDocumentFromString(pdf); ASSERT_TRUE(base); auto layer = std::make_unique(base, MakeStreamForString(pdf)); - EXPECT_DEATH_IF_SUPPORTED(layer->GetMutableIndirectObject(1), ""); - EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); + 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()); +} - RetainPtr page_dict = layer->GetPageDictionary(0); - ASSERT_TRUE(page_dict); - auto page = pdfium::MakeRetain( - layer.get(), - pdfium::WrapRetain(const_cast(page_dict.Get()))); +TEST_F(CPDFLayerDocumentTest, PageMutableDictPromotesAndLeavesBaseFrozen) { + const std::string pdf = BuildSimplePdf(); + RetainPtr base = LoadBaseDocumentFromString(pdf); + ASSERT_TRUE(base); + auto layer = + std::make_unique(base, MakeStreamForString(pdf)); + + auto page = MakeLayerPage(layer.get(), 0); + ASSERT_TRUE(page); EXPECT_EQ(0u, layer->GetPromotedObjectCount()); - EXPECT_DEATH_IF_SUPPORTED(page->GetMutableDict(), ""); + + 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, MakeStreamForString(pdf)); + + 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, MakeStreamForString(pdf)); + + 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, MakeStreamForString(pdf)); + + 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, MakeStreamForString(pdf)); + + EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); } #endif // DCHECK_IS_ON() diff --git a/core/fpdfapi/parser/cpdf_object.cpp b/core/fpdfapi/parser/cpdf_object.cpp index 68cbeef0bd..81c02a882b 100644 --- a/core/fpdfapi/parser/cpdf_object.cpp +++ b/core/fpdfapi/parser/cpdf_object.cpp @@ -72,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 4915d04ef4..a954b6473e 100644 --- a/core/fpdfapi/parser/cpdf_object.h +++ b/core/fpdfapi/parser/cpdf_object.h @@ -74,6 +74,12 @@ 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_; } @@ -114,6 +120,10 @@ 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( diff --git a/core/fpdfapi/parser/cpdf_reference.cpp b/core/fpdfapi/parser/cpdf_reference.cpp index c0e6fd8e35..06e4d35cf8 100644 --- a/core/fpdfapi/parser/cpdf_reference.cpp +++ b/core/fpdfapi/parser/cpdf_reference.cpp @@ -49,6 +49,11 @@ 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; @@ -67,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 45e04d48d1..b0828a0f97 100644 --- a/core/fpdfapi/parser/cpdf_reference.h +++ b/core/fpdfapi/parser/cpdf_reference.h @@ -22,6 +22,8 @@ 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; @@ -47,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 a98bec1353..057404aff2 100644 --- a/core/fpdfapi/parser/cpdf_stream.cpp +++ b/core/fpdfapi/parser/cpdf_stream.cpp @@ -100,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 { @@ -117,6 +123,23 @@ 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); } diff --git a/core/fpdfapi/parser/cpdf_stream.h b/core/fpdfapi/parser/cpdf_stream.h index a618df5b85..b291a724d1 100644 --- a/core/fpdfapi/parser/cpdf_stream.h +++ b/core/fpdfapi/parser/cpdf_stream.h @@ -29,6 +29,8 @@ class CPDF_Stream final : public CPDF_Object { // CPDF_Object: Type GetType() const override; RetainPtr Clone() const override; + RetainPtr CloneForHolder( + CPDF_IndirectObjectHolder* holder) const override; WideString GetUnicodeText() const override; CPDF_Stream* AsMutableStream() override; bool WriteTo(IFX_ArchiveStream* archive, @@ -90,6 +92,9 @@ 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/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index f9d2a108d5..650d21d50d 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -716,6 +716,34 @@ TEST_F(FPDFViewEmbedderTest, OpenFreshLayerAnnotHandleDoesNotPromote) { EPDF_ReleaseBaseDocument(base); } +TEST_F(FPDFViewEmbedderTest, CreateAnnotOnFreshLayerPromotesOverlayOnly) { + FileAccessForTesting base_access("rectangles.pdf"); + EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); + ASSERT_TRUE(base); + + { + FileAccessForTesting layer_access("rectangles.pdf"); + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, &layer_access, 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, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); From 502f817327ae21f22b7f2133d5f352cd38a8bdc1 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 May 2026 00:07:00 +0300 Subject: [PATCH 12/28] EPDFLayer_SaveDeltaToBuffer + creator append-only mode --- core/fpdfapi/edit/BUILD.gn | 1 + core/fpdfapi/edit/cpdf_creator.cpp | 65 ++++++++++++++++++--- core/fpdfapi/edit/cpdf_creator.h | 15 +++++ core/fpdfapi/edit/cpdf_creator_unittest.cpp | 23 ++++++++ fpdfsdk/epdf_layer.cpp | 54 +++++++++++++++++ fpdfsdk/fpdf_view_c_api_test.c | 1 + fpdfsdk/fpdf_view_embeddertest.cpp | 50 ++++++++++++++++ public/fpdf_save.h | 21 +++++++ 8 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 core/fpdfapi/edit/cpdf_creator_unittest.cpp diff --git a/core/fpdfapi/edit/BUILD.gn b/core/fpdfapi/edit/BUILD.gn index fd0b1eac7f..b5a8ab45bd 100644 --- a/core/fpdfapi/edit/BUILD.gn +++ b/core/fpdfapi/edit/BUILD.gn @@ -53,6 +53,7 @@ source_set("contentstream_write_utils") { pdfium_unittest_source_set("unittests") { sources = [ + "cpdf_creator_unittest.cpp", "cpdf_npagetooneexporter_unittest.cpp", "cpdf_pagecontentgenerator_unittest.cpp", ] diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index 612502abab..0499f22eb6 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -44,10 +45,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 +59,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 +132,10 @@ bool OutputIndex(IFX_ArchiveStream* archive, FX_FILESIZE offset) { archive->WriteByte(0); } +ByteString FormatXrefOffset10(FX_FILESIZE offset) { + return ByteString::Format("%010" PRId64, static_cast(offset)); +} + } // namespace CPDF_Creator::CPDF_Creator(CPDF_Document* doc, @@ -141,6 +149,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; @@ -225,6 +238,14 @@ bool CPDF_Creator::WriteNewObjs() { 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; @@ -274,8 +295,9 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage1() { } } 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; } } @@ -401,7 +423,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 +468,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; } @@ -562,6 +592,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 +614,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 +639,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 +654,17 @@ 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(parser_->GetDocumentSize()); + } if (file_version >= 10 && file_version <= 17) { file_version_ = file_version; @@ -629,7 +676,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 f96e708a7c..06d2fe1205 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 0000000000..4ea0c8ba10 --- /dev/null +++ b/core/fpdfapi/edit/cpdf_creator_unittest.cpp @@ -0,0 +1,23 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fpdfapi/edit/cpdf_creator.h" + +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CPDFCreatorTest, FormatXrefOffset10Is64BitClean) { + ByteString small = CPDF_Creator::FormatXrefOffset10ForTesting(42); + EXPECT_EQ("0000000042", small); + EXPECT_EQ(10u, small.GetLength()); + + ByteString mid = + CPDF_Creator::FormatXrefOffset10ForTesting(2500000000LL); + EXPECT_EQ("2500000000", mid); + EXPECT_EQ(10u, mid.GetLength()); + + ByteString max = + CPDF_Creator::FormatXrefOffset10ForTesting(0xffffffff); + EXPECT_EQ("4294967295", max); + EXPECT_EQ(10u, max.GetLength()); +} diff --git a/fpdfsdk/epdf_layer.cpp b/fpdfsdk/epdf_layer.cpp index 738b724a00..04d2b2130f 100644 --- a/fpdfsdk/epdf_layer.cpp +++ b/fpdfsdk/epdf_layer.cpp @@ -8,14 +8,22 @@ #include #include +#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/retain_ptr.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; + CPDF_BaseDocument* CPDFBaseDocumentFromEPDFBaseDocument( EPDF_BASE_DOCUMENT base) { return reinterpret_cast(base); @@ -100,3 +108,49 @@ EPDFLayer_GetBaseDocument(FPDF_DOCUMENT layer) { layer_doc->GetBaseDocument()) : nullptr; } + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveDeltaToBuffer(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status) { + if (out_status) { + *out_status = EPDFLayerSaveStatus_kSaveFailed; + } + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + if (!layer_doc || !file_write) { + return false; + } + + CPDF_Parser* parser = layer_doc->GetParser(); + if (!parser) { + return false; + } + if (parser->GetDocumentSize() > kSafeNotionalStartOffsetMax) { + if (out_status) { + *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) { + if (out_status) { + *out_status = EPDFLayerSaveStatus_kSuccess; + } + return true; + } + + if (out_status && + creator.GetFailureReason() == + CPDF_Creator::FailureReason::kAppendOnlyOffsetTooLarge) { + *out_status = EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge; + } + return false; +} diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index f51db07ece..3b7f6cd032 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -539,6 +539,7 @@ int CheckPDFiumCApi() { CHK(EPDFLayer_GetPromotedObjectCount); CHK(EPDFLayer_IsObjectPromoted); CHK(EPDFLayer_OpenLayer); + CHK(EPDFLayer_SaveDeltaToBuffer); CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); CHK(FPDF_LoadDocument); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 650d21d50d..1cafe29f17 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -19,6 +19,7 @@ #include "fpdfsdk/fpdf_view_c_api_test.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_annot.h" +#include "public/fpdf_save.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" #include "testing/embedder_test_constants.h" @@ -744,6 +745,55 @@ TEST_F(FPDFViewEmbedderTest, CreateAnnotOnFreshLayerPromotesOverlayOnly) { 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; + { + FileAccessForTesting layer_access("rectangles.pdf"); + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, &layer_access, 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_SaveDeltaToBuffer(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-")); + + 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, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/public/fpdf_save.h b/public/fpdf_save.h index 093b889ad1..d388ae11fd 100644 --- a/public/fpdf_save.h +++ b/public/fpdf_save.h @@ -86,6 +86,27 @@ FPDF_SaveWithVersion(FPDF_DOCUMENT document, FPDF_DWORD flags, 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_SaveDeltaToBuffer +// Saves only a layer document's current overlay delta. The caller can +// materialize the layer as base bytes followed by this returned 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_SaveDeltaToBuffer(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status); + #ifdef __cplusplus } #endif From 2cde580c996a2a8aeaccde00c07e5630e71a0ac5 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 May 2026 00:18:14 +0300 Subject: [PATCH 13/28] Introspection + benchmarks + soak --- fpdfsdk/fpdf_view_embeddertest.cpp | 61 ++++++ testing/tools/BUILD.gn | 31 +++ testing/tools/epdf_layer_memory_benchmark.cpp | 174 +++++++++++++++++ testing/tools/epdf_layer_replay_soak.cpp | 182 ++++++++++++++++++ testing/tools/epdf_layer_tool_common.h | 108 +++++++++++ 5 files changed, 556 insertions(+) create mode 100644 testing/tools/epdf_layer_memory_benchmark.cpp create mode 100644 testing/tools/epdf_layer_replay_soak.cpp create mode 100644 testing/tools/epdf_layer_tool_common.h diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 1cafe29f17..77c9c69820 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -794,6 +794,67 @@ TEST_F(FPDFViewEmbedderTest, SaveLayerDeltaMaterializesWithBaseBytes) { 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_SaveDeltaToBuffer(document(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSaveFailed, save_status); +} + +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; + { + FileAccessForTesting layer_access("rectangles.pdf"); + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer( + EPDFLayer_OpenLayer(base, &layer_access, 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_SaveDeltaToBuffer(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, Page) { ASSERT_TRUE(OpenDocument("about_blank.pdf")); ScopedPage page = LoadScopedPage(0); diff --git a/testing/tools/BUILD.gn b/testing/tools/BUILD.gn index 15d841bcfe..a90c5718ea 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 0000000000..7b8930c11d --- /dev/null +++ b/testing/tools/epdf_layer_memory_benchmark.cpp @@ -0,0 +1,174 @@ +// 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::vector layer_files; + layer_files.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) { + layer_files.emplace_back(&base_bytes); + EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer(EPDFLayer_OpenLayer( + base, layer_files.back().file_access(), nullptr, &status)); + if (!layer || status != EPDFLayerOpenStatus_kSuccess) { + std::fprintf(stderr, "Failed to open layer %zu.\n", i); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + if (!AddTextAnnotation(layer.get())) { + std::fprintf(stderr, "Failed to mutate layer %zu.\n", i); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 1; + } + layers.push_back(std::move(layer)); + + if (i == layer_counts[next_report]) { + size_t promoted_objects = 0; + for (const auto& live_layer : layers) { + promoted_objects += EPDFLayer_GetPromotedObjectCount(live_layer.get()); + } + const size_t rss = CurrentRssBytes(); + const size_t delta = rss > baseline_rss ? rss - baseline_rss : 0; + std::printf("%zu,%zu,%zu,%zu,%zu\n", i, rss, delta, delta / i, + promoted_objects); + ++next_report; + if (next_report == layer_counts.size()) { + break; + } + } + } + + layers.clear(); + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 0; +} diff --git a/testing/tools/epdf_layer_replay_soak.cpp b/testing/tools/epdf_layer_replay_soak.cpp new file mode 100644 index 0000000000..abd63f291d --- /dev/null +++ b/testing/tools/epdf_layer_replay_soak.cpp @@ -0,0 +1,182 @@ +// 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; +} + +} // 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) { + epdf_layer_tool::MemoryFile layer_file(&base_bytes); + EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument layer(EPDFLayer_OpenLayer( + base, layer_file.file_access(), 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_SaveDeltaToBuffer(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; + } + } + + std::printf("round=%zu layers=%zu edited_layer=%zu ok\n", round + 1, + layer_count, edited_layer); + if (round + 1 < rounds && sleep_seconds > 0) { + std::this_thread::sleep_for(std::chrono::seconds(sleep_seconds)); + } + } + + EPDF_ReleaseBaseDocument(base); + FPDF_DestroyLibrary(); + return 0; +} diff --git a/testing/tools/epdf_layer_tool_common.h b/testing/tools/epdf_layer_tool_common.h new file mode 100644 index 0000000000..d78a95238b --- /dev/null +++ b/testing/tools/epdf_layer_tool_common.h @@ -0,0 +1,108 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ +#define TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "public/fpdf_save.h" +#include "public/fpdfview.h" + +namespace epdf_layer_tool { + +struct MemoryFile { + explicit MemoryFile(std::vector input) + : owned_bytes(std::move(input)), bytes(&owned_bytes) { + InitAccess(); + } + + explicit MemoryFile(const std::vector* input) : bytes(input) { + InitAccess(); + } + + void InitAccess() { + access.m_FileLen = static_cast(bytes->size()); + access.m_GetBlock = &MemoryFile::GetBlock; + access.m_Param = this; + } + + FPDF_FILEACCESS* file_access() { return &access; } + + static int GetBlock(void* param, + unsigned long pos, + unsigned char* buf, + unsigned long size) { + MemoryFile* file = static_cast(param); + if (!file || !file->bytes || pos > file->bytes->size() || + size > file->bytes->size() - pos) { + return 0; + } + memcpy(buf, file->bytes->data() + pos, size); + return 1; + } + + std::vector owned_bytes; + const std::vector* bytes = nullptr; + FPDF_FILEACCESS access = {}; +}; + +struct StringWriter : FPDF_FILEWRITE { + StringWriter() { + version = 1; + WriteBlock = &StringWriter::WriteBlockCallback; + } + + void Clear() { data.clear(); } + + static int WriteBlockCallback(FPDF_FILEWRITE* file_write, + const void* buffer, + unsigned long size) { + StringWriter* writer = static_cast(file_write); + writer->data.append(static_cast(buffer), size); + return 1; + } + + std::string data; +}; + +inline bool ReadFile(const std::string& path, std::vector* out) { + std::ifstream file(path, std::ios::binary); + if (!file) { + return false; + } + file.seekg(0, std::ios::end); + const std::streamoff length = file.tellg(); + if (length < 0) { + return false; + } + file.seekg(0, std::ios::beg); + out->resize(static_cast(length)); + return out->empty() || + file.read(reinterpret_cast(out->data()), length).good(); +} + +inline std::vector MaterializeLayerBytes( + const std::vector& base_bytes, + const std::string& delta) { + std::vector materialized = base_bytes; + materialized.insert(materialized.end(), delta.begin(), delta.end()); + return materialized; +} + +inline void PrintUsage(const char* argv0, const char* extra) { + std::fprintf(stderr, "Usage: %s %s\n", argv0, extra); +} + +} // namespace epdf_layer_tool + +#endif // TESTING_TOOLS_EPDF_LAYER_TOOL_COMMON_H_ From 986cd2eb2c485bd2f1c543b514b7ab8b4211a37d Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 May 2026 16:35:33 +0300 Subject: [PATCH 14/28] Finish opening file with delta --- core/fpdfapi/edit/cpdf_creator.cpp | 17 +- core/fpdfapi/parser/BUILD.gn | 2 + core/fpdfapi/parser/cpdf_base_document.cpp | 52 +++ core/fpdfapi/parser/cpdf_base_document.h | 21 + .../parser/cpdf_concat_read_stream.cpp | 63 +++ core/fpdfapi/parser/cpdf_concat_read_stream.h | 31 ++ core/fpdfapi/parser/cpdf_document.cpp | 79 +++- core/fpdfapi/parser/cpdf_document.h | 2 + core/fpdfapi/parser/cpdf_layer_document.cpp | 151 ++++++- core/fpdfapi/parser/cpdf_layer_document.h | 4 +- .../parser/cpdf_layer_document_unittest.cpp | 180 ++++++-- core/fpdfapi/parser/cpdf_parser.cpp | 14 +- core/fpdfapi/parser/cpdf_parser.h | 6 +- core/fpdfapi/parser/cpdf_parser_unittest.cpp | 2 + core/fpdfapi/parser/cpdf_read_validator.h | 1 + core/fpdfapi/parser/cpdf_syntax_parser.cpp | 4 + core/fpdfapi/parser/cpdf_syntax_parser.h | 1 + fpdfsdk/epdf_layer.cpp | 392 +++++++++++++++-- fpdfsdk/fpdf_save.cpp | 67 ++- fpdfsdk/fpdf_save_embeddertest.cpp | 21 + fpdfsdk/fpdf_view_c_api_test.c | 8 +- fpdfsdk/fpdf_view_embeddertest.cpp | 410 +++++++++++++++++- public/fpdf_save.h | 58 ++- public/fpdfview.h | 16 +- testing/tools/epdf_layer_memory_benchmark.cpp | 24 +- testing/tools/epdf_layer_replay_soak.cpp | 73 +++- 26 files changed, 1562 insertions(+), 137 deletions(-) create mode 100644 core/fpdfapi/parser/cpdf_concat_read_stream.cpp create mode 100644 core/fpdfapi/parser/cpdf_concat_read_stream.h diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index 0499f22eb6..fe806bf2c7 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp @@ -8,9 +8,9 @@ #include +#include #include #include -#include #include #include @@ -290,7 +290,9 @@ 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; } } @@ -370,7 +372,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) @@ -486,7 +489,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; @@ -654,8 +658,7 @@ bool CPDF_Creator::Create(Mask flags, int32_t file_version) { } is_incremental_ = !!(flags & CreateFlags::kIncremental); - is_incremental_append_only_ = - !!(flags & CreateFlags::kIncrementalAppendOnly); + is_incremental_append_only_ = !!(flags & CreateFlags::kIncrementalAppendOnly); if (is_incremental_append_only_ && !is_incremental_) { failure_reason_ = FailureReason::kOther; return false; @@ -663,7 +666,7 @@ bool CPDF_Creator::Create(Mask flags, int32_t file_version) { is_original_ = !(flags & CreateFlags::kNoOriginal); if (is_incremental_append_only_ && parser_) { static_cast(archive_.get()) - ->SetNotionalStartOffset(parser_->GetDocumentSize()); + ->SetNotionalStartOffset(document_->GetLayerAppendBaseOffset()); } if (file_version >= 10 && file_version <= 17) { diff --git a/core/fpdfapi/parser/BUILD.gn b/core/fpdfapi/parser/BUILD.gn index c3ccf34648..a66441b47e 100644 --- a/core/fpdfapi/parser/BUILD.gn +++ b/core/fpdfapi/parser/BUILD.gn @@ -15,6 +15,8 @@ source_set("parser") { "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", diff --git a/core/fpdfapi/parser/cpdf_base_document.cpp b/core/fpdfapi/parser/cpdf_base_document.cpp index 1e3fa6fe3f..92abccff44 100644 --- a/core/fpdfapi/parser/cpdf_base_document.cpp +++ b/core/fpdfapi/parser/cpdf_base_document.cpp @@ -4,11 +4,14 @@ #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" @@ -16,9 +19,13 @@ #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) { @@ -28,6 +35,32 @@ void PushIfNew(RetainPtr object, 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() @@ -43,10 +76,29 @@ CPDF_Parser::Error CPDF_BaseDocument::LoadBaseDoc( 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; diff --git a/core/fpdfapi/parser/cpdf_base_document.h b/core/fpdfapi/parser/cpdf_base_document.h index 693dd2d565..abd434efe4 100644 --- a/core/fpdfapi/parser/cpdf_base_document.h +++ b/core/fpdfapi/parser/cpdf_base_document.h @@ -5,7 +5,12 @@ #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; @@ -19,10 +24,26 @@ class CPDF_BaseDocument final : public CPDF_Document, public Retainable { 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_concat_read_stream.cpp b/core/fpdfapi/parser/cpdf_concat_read_stream.cpp new file mode 100644 index 0000000000..fe0cac734d --- /dev/null +++ b/core/fpdfapi/parser/cpdf_concat_read_stream.cpp @@ -0,0 +1,63 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "core/fpdfapi/parser/cpdf_concat_read_stream.h" + +#include +#include + +#include "core/fxcrt/fx_safe_types.h" +#include "core/fxcrt/numerics/safe_conversions.h" + +CPDF_ConcatReadStream::CPDF_ConcatReadStream( + RetainPtr first, + RetainPtr second) + : first_(std::move(first)), + second_(std::move(second)), + first_size_(first_ ? first_->GetSize() : 0), + second_size_(second_ ? second_->GetSize() : 0) {} + +CPDF_ConcatReadStream::~CPDF_ConcatReadStream() = default; + +FX_FILESIZE CPDF_ConcatReadStream::GetSize() { + FX_SAFE_FILESIZE size = first_size_; + size += second_size_; + return size.ValueOrDefault(0); +} + +bool CPDF_ConcatReadStream::ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) { + if (offset < 0) { + return false; + } + if (buffer.empty()) { + return offset <= GetSize(); + } + + FX_SAFE_FILESIZE safe_end = offset; + safe_end += buffer.size(); + if (!safe_end.IsValid() || safe_end.ValueOrDie() > GetSize()) { + return false; + } + + if (offset < first_size_) { + const FX_FILESIZE remaining_first_size = first_size_ - offset; + const size_t first_read_size = + pdfium::IsValueInRangeForNumericType(remaining_first_size) + ? std::min(buffer.size(), + pdfium::checked_cast(remaining_first_size)) + : buffer.size(); + if (!first_ || + !first_->ReadBlockAtOffset(buffer.first(first_read_size), offset)) { + return false; + } + buffer = buffer.subspan(first_read_size); + offset = 0; + } else { + offset -= first_size_; + } + + return buffer.empty() || + (second_ && second_->ReadBlockAtOffset(buffer, offset)); +} diff --git a/core/fpdfapi/parser/cpdf_concat_read_stream.h b/core/fpdfapi/parser/cpdf_concat_read_stream.h new file mode 100644 index 0000000000..537040e8e9 --- /dev/null +++ b/core/fpdfapi/parser/cpdf_concat_read_stream.h @@ -0,0 +1,31 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ +#define CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ + +#include "core/fxcrt/fx_stream.h" +#include "core/fxcrt/retain_ptr.h" + +class CPDF_ConcatReadStream final : public IFX_SeekableReadStream { + public: + CONSTRUCT_VIA_MAKE_RETAIN; + + // IFX_SeekableReadStream: + FX_FILESIZE GetSize() override; + bool ReadBlockAtOffset(pdfium::span buffer, + FX_FILESIZE offset) override; + + private: + CPDF_ConcatReadStream(RetainPtr first, + RetainPtr second); + ~CPDF_ConcatReadStream() override; + + RetainPtr const first_; + RetainPtr const second_; + FX_FILESIZE const first_size_; + FX_FILESIZE const second_size_; +}; + +#endif // CORE_FPDFAPI_PARSER_CPDF_CONCAT_READ_STREAM_H_ diff --git a/core/fpdfapi/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp index 59506c882e..e51bf4f8e0 100644 --- a/core/fpdfapi/parser/cpdf_document.cpp +++ b/core/fpdfapi/parser/cpdf_document.cpp @@ -171,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, @@ -348,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); @@ -444,6 +509,11 @@ 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; @@ -466,7 +536,8 @@ 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 (found_index < 0 || static_cast(found_index) >= GetPageListSize()) { + if (found_index < 0 || + static_cast(found_index) >= GetPageListSize()) { return -1; } @@ -653,12 +724,14 @@ RetainPtr CPDF_Document::GetMutableInfo() { } 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. SetCachedInfoDict(NewIndirect()); diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h index 0493f6e524..0a02dfcea0 100644 --- a/core/fpdfapi/parser/cpdf_document.h +++ b/core/fpdfapi/parser/cpdf_document.h @@ -150,6 +150,7 @@ class CPDF_Document : public Observable, 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; @@ -183,6 +184,7 @@ class CPDF_Document : public Observable, 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(); diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index e0a146aab9..44747cef02 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -5,11 +5,12 @@ #include "core/fpdfapi/parser/cpdf_layer_document.h" #include -#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" @@ -17,6 +18,56 @@ #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, @@ -93,6 +144,10 @@ bool CPDF_LayerDocument::IsLayerDocument() const { return true; } +FX_FILESIZE CPDF_LayerDocument::GetLayerAppendBaseOffset() const { + return base_->GetLayerAppendBaseOffset(); +} + RetainPtr CPDF_LayerDocument::ParseIndirectObject( uint32_t objnum) { NOTREACHED(); @@ -179,21 +234,97 @@ void CPDF_LayerDocument::IngestCurrentDelta() { CPDF_Parser* base_parser = base_->GetParser(); if (!base_parser || !file_access_) { - ingest_status_ = OpenStatus::kOpenFailed; + 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 FX_FILESIZE base_end = base_parser->GetDocumentSize(); - const FX_FILESIZE layer_size = file_access_->GetSize(); - if (layer_size < base_end) { - ingest_status_ = OpenStatus::kBaseLayerMismatch; + const CPDF_CrossRefTable* table = parser.GetCrossRefTable(); + if (!table) { + FailDeltaIngest(OpenStatus::kMalformedDelta); return; } - if (layer_size > base_end) { - // Full appended-xref ingest lands with the delta parser. Until then, fail - // closed instead of silently ignoring a caller-provided delta. - ingest_status_ = OpenStatus::kMalformedDelta; + + 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) { diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h index 2cfc44a6e6..1d4df51299 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -43,6 +43,7 @@ class CPDF_LayerDocument final : public CPDF_Document { 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; @@ -65,10 +66,11 @@ class CPDF_LayerDocument final : public CPDF_Document { private: void InitializeFromBase(); void IngestCurrentDelta(); + void FailDeltaIngest(OpenStatus status); RetainPtr PromoteFromBase(uint32_t objnum); RetainPtr const base_; - RetainPtr const file_access_; + RetainPtr file_access_; std::vector layer_page_list_; OpenStatus ingest_status_ = OpenStatus::kSuccess; }; diff --git a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp index 421638720b..614bd8b069 100644 --- a/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document_unittest.cpp @@ -4,7 +4,9 @@ #include "core/fpdfapi/parser/cpdf_layer_document.h" +#include #include +#include #include #include #include @@ -14,12 +16,14 @@ #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" @@ -32,6 +36,40 @@ class CPDFLayerDocumentTest : public testing::Test { 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", @@ -89,6 +127,41 @@ std::string BuildPdfWithDirectResources() { 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())); @@ -112,19 +185,48 @@ RetainPtr MakeLayerPage(CPDF_LayerDocument* layer, int page_index) { return nullptr; } return pdfium::MakeRetain( - layer, - pdfium::WrapRetain(const_cast(page_dict.Get()))); + 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, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); EXPECT_EQ(CPDF_LayerDocument::OpenStatus::kSuccess, layer->ingest_status()); EXPECT_TRUE(layer->IsLayerDocument()); @@ -145,8 +247,7 @@ TEST_F(CPDFLayerDocumentTest, DeleteBaseObjectIsNoOp) { const std::string pdf = BuildSimplePdf(); RetainPtr base = LoadBaseDocumentFromString(pdf); ASSERT_TRUE(base); - auto layer = - std::make_unique(base, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); ASSERT_TRUE(layer->GetIndirectObject(1)); layer->DeleteIndirectObject(1); @@ -154,26 +255,51 @@ TEST_F(CPDFLayerDocumentTest, DeleteBaseObjectIsNoOp) { EXPECT_EQ(0u, layer->GetPromotedObjectCount()); } -TEST_F(CPDFLayerDocumentTest, AppendedBytesFailClosedUntilDeltaIngestLands) { +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); - const std::string layer_bytes = pdf + "\n% appended delta placeholder\n"; auto layer = std::make_unique( - base, MakeStreamForString(layer_bytes)); + 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, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); RetainPtr promoted = layer->GetMutableIndirectObject(1); ASSERT_TRUE(promoted); @@ -189,8 +315,7 @@ TEST_F(CPDFLayerDocumentTest, PageMutableDictPromotesAndLeavesBaseFrozen) { const std::string pdf = BuildSimplePdf(); RetainPtr base = LoadBaseDocumentFromString(pdf); ASSERT_TRUE(base); - auto layer = - std::make_unique(base, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); auto page = MakeLayerPage(layer.get(), 0); ASSERT_TRUE(page); @@ -206,16 +331,15 @@ TEST_F(CPDFLayerDocumentTest, PageMutableDictPromotesAndLeavesBaseFrozen) { 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")); + 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, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); auto page = MakeLayerPage(layer.get(), 0); ASSERT_TRUE(page); @@ -236,9 +360,10 @@ TEST_F(CPDFLayerDocumentTest, PromotedReferencesResolveThroughLayerHolder) { "Tier3ParentMarker")); parent->SetNewFor("Tier3ParentMarker", 91); - EXPECT_EQ(91, layer->GetMutableIndirectObject(2) - ->AsMutableDictionary() - ->GetIntegerFor("Tier3ParentMarker")); + EXPECT_EQ( + 91, + layer->GetMutableIndirectObject(2)->AsMutableDictionary()->GetIntegerFor( + "Tier3ParentMarker")); EXPECT_FALSE(base->GetFrozenObjectForLayer(2)->AsDictionary()->KeyExist( "Tier3ParentMarker")); } @@ -247,8 +372,7 @@ TEST_F(CPDFLayerDocumentTest, CrossHandleReadRefreshesAfterPagePromotion) { const std::string pdf = BuildSimplePdf(); RetainPtr base = LoadBaseDocumentFromString(pdf); ASSERT_TRUE(base); - auto layer = - std::make_unique(base, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); auto page_a = MakeLayerPage(layer.get(), 0); auto page_b = MakeLayerPage(layer.get(), 0); @@ -262,16 +386,17 @@ TEST_F(CPDFLayerDocumentTest, CrossHandleReadRefreshesAfterPagePromotion) { 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")); + 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, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); auto page = MakeLayerPage(layer.get(), 0); ASSERT_TRUE(page); @@ -296,8 +421,7 @@ TEST_F(CPDFLayerDocumentTest, ParseIndirectObjectStillUnsupportedOnLayer) { const std::string pdf = BuildSimplePdf(); RetainPtr base = LoadBaseDocumentFromString(pdf); ASSERT_TRUE(base); - auto layer = - std::make_unique(base, MakeStreamForString(pdf)); + auto layer = std::make_unique(base, nullptr); EXPECT_DEATH_IF_SUPPORTED(layer->ParseIndirectObject(1), ""); } diff --git a/core/fpdfapi/parser/cpdf_parser.cpp b/core/fpdfapi/parser/cpdf_parser.cpp index 30dcb11085..be440b584f 100644 --- a/core/fpdfapi/parser/cpdf_parser.cpp +++ b/core/fpdfapi/parser/cpdf_parser.cpp @@ -591,6 +591,11 @@ bool CPDF_Parser::ParseAndAppendCrossRefSubsectionData( pdfium::span pEntry = pdfium::span(buf).subspan(i * kEntrySize); + // TODO(art-snake): The info.gennum is uint16_t, but version may be + // greater than max. Need to solve this issue. + const int32_t version = + StringToInt(ByteStringView(pEntry.subspan<11u>())); + info.gennum = version; if (pEntry[17] == 'f') { info.pos = 0; info.type = ObjectType::kFree; @@ -610,11 +615,6 @@ bool CPDF_Parser::ParseAndAppendCrossRefSubsectionData( info.pos = offset.ValueOrDie(); - // TODO(art-snake): The info.gennum is uint16_t, but version may be - // greater than max. Need to solve this issue. - const int32_t version = - StringToInt(ByteStringView(pEntry.subspan<11u>())); - info.gennum = version; info.type = ObjectType::kNormal; } } @@ -1166,6 +1166,10 @@ FX_FILESIZE CPDF_Parser::GetDocumentSize() const { return syntax_->GetDocumentSize(); } +RetainPtr CPDF_Parser::GetFileAccess() const { + return syntax_ ? syntax_->GetFileAccess() : nullptr; +} + uint32_t CPDF_Parser::GetFirstPageNo() const { return linearized_ ? linearized_->GetFirstPageNo() : 0; } diff --git a/core/fpdfapi/parser/cpdf_parser.h b/core/fpdfapi/parser/cpdf_parser.h index 613a1d750d..1a3168d6c8 100644 --- a/core/fpdfapi/parser/cpdf_parser.h +++ b/core/fpdfapi/parser/cpdf_parser.h @@ -109,6 +109,7 @@ class CPDF_Parser { bool IsXRefStream() const { return xref_stream_; } FX_FILESIZE GetDocumentSize() const; + RetainPtr GetFileAccess() const; uint32_t GetFirstPageNo() const; const CPDF_LinearizedHeader* GetLinearizedHeader() const { return linearized_.get(); @@ -119,9 +120,12 @@ class CPDF_Parser { std::vector GetTrailerEnds(); bool WriteToArchive(IFX_ArchiveStream* archive, FX_FILESIZE src_size); - const CPDF_CrossRefTable* GetCrossRefTableForTesting() const { + const CPDF_CrossRefTable* GetCrossRefTable() const { return cross_ref_table_.get(); } + const CPDF_CrossRefTable* GetCrossRefTableForTesting() const { + return GetCrossRefTable(); + } CPDF_Dictionary* GetMutableTrailerForTesting(); diff --git a/core/fpdfapi/parser/cpdf_parser_unittest.cpp b/core/fpdfapi/parser/cpdf_parser_unittest.cpp index ecfd50f236..0c4301b3fd 100644 --- a/core/fpdfapi/parser/cpdf_parser_unittest.cpp +++ b/core/fpdfapi/parser/cpdf_parser_unittest.cpp @@ -204,6 +204,8 @@ TEST(ParserTest, LoadCrossRefTable) { EXPECT_EQ(kExpected[i].offset, GetObjInfo(parser, i).pos); EXPECT_EQ(kExpected[i].type, GetObjInfo(parser, i).type); } + EXPECT_EQ(65535u, GetObjInfo(parser, 0).gennum); + EXPECT_EQ(7u, GetObjInfo(parser, 3).gennum); } { static const unsigned char kXrefTable[] = diff --git a/core/fpdfapi/parser/cpdf_read_validator.h b/core/fpdfapi/parser/cpdf_read_validator.h index c8665416e3..8056b508bb 100644 --- a/core/fpdfapi/parser/cpdf_read_validator.h +++ b/core/fpdfapi/parser/cpdf_read_validator.h @@ -43,6 +43,7 @@ class CPDF_ReadValidator : public IFX_SeekableReadStream { bool IsWholeFileAvailable(); bool CheckDataRangeAndRequestIfUnavailable(FX_FILESIZE offset, size_t size); bool CheckWholeFileAndRequestIfUnavailable(); + RetainPtr GetFileAccess() const { return file_read_; } // IFX_SeekableReadStream overrides: bool ReadBlockAtOffset(pdfium::span buffer, diff --git a/core/fpdfapi/parser/cpdf_syntax_parser.cpp b/core/fpdfapi/parser/cpdf_syntax_parser.cpp index 749e59c07b..f765a10ead 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 79fc68d635..a3c27a9342 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/fpdfsdk/epdf_layer.cpp b/fpdfsdk/epdf_layer.cpp index 04d2b2130f..89eae4c787 100644 --- a/fpdfsdk/epdf_layer.cpp +++ b/fpdfsdk/epdf_layer.cpp @@ -4,15 +4,25 @@ #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" @@ -23,6 +33,55 @@ 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) { @@ -47,6 +106,132 @@ EPDFLayerOpenStatus ToPublicStatus(CPDF_LayerDocument::OpenStatus status) { } } +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 @@ -54,32 +239,99 @@ EPDFLayer_OpenLayer(EPDF_BASE_DOCUMENT base, FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password, EPDFLayerOpenStatus* out_status) { - if (out_status) { - *out_status = EPDFLayerOpenStatus_kOpenFailed; - } - if (!base || !pFileAccess) { - return nullptr; - } - // 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); - RetainPtr retained_base = pdfium::WrapRetain(base_doc); - auto layer = std::make_unique( - std::move(retained_base), - pdfium::MakeRetain(pFileAccess)); + if (!base_doc) { + return nullptr; + } - const EPDFLayerOpenStatus status = ToPublicStatus(layer->ingest_status()); - if (out_status) { - *out_status = status; + RetainPtr artifact_stream = + pdfium::MakeRetain(pFileAccess); + DataVector artifact = ReadStreamToVector(artifact_stream.Get()); + if (artifact.size() < kLayerArtifactHeaderSize) { + SetOpenStatus(out_status, EPDFLayerOpenStatus_kMalformedDelta); + return nullptr; } - if (status != EPDFLayerOpenStatus_kSuccess) { + + 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; - return FPDFDocumentFromCPDFDocument(layer.release()); + 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 @@ -110,47 +362,107 @@ EPDFLayer_GetBaseDocument(FPDF_DOCUMENT layer) { } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFLayer_SaveDeltaToBuffer(FPDF_DOCUMENT layer, - FPDF_FILEWRITE* file_write, - EPDFLayerSaveStatus* out_status) { - if (out_status) { - *out_status = EPDFLayerSaveStatus_kSaveFailed; - } +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; } - CPDF_Parser* parser = layer_doc->GetParser(); - if (!parser) { + if (!layer_doc->GetParser()) { return false; } - if (parser->GetDocumentSize() > kSafeNotionalStartOffsetMax) { - if (out_status) { - *out_status = EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge; - } + 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); + const bool ok = + creator.Create(Mask( + CPDF_Creator::CreateFlags::kIncremental, + CPDF_Creator::CreateFlags::kIncrementalAppendOnly), + /*file_version=*/0); if (ok) { - if (out_status) { - *out_status = EPDFLayerSaveStatus_kSuccess; - } + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); return true; } - if (out_status && - creator.GetFailureReason() == - CPDF_Creator::FailureReason::kAppendOnlyOffsetTooLarge) { - *out_status = EPDFLayerSaveStatus_kAppendOnlyOffsetTooLarge; + 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/fpdf_save.cpp b/fpdfsdk/fpdf_save.cpp index a6eb68ca5f..d82567c8e2 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 ea45c40180..471756c918 100644 --- a/fpdfsdk/fpdf_save_embeddertest.cpp +++ b/fpdfsdk/fpdf_save_embeddertest.cpp @@ -40,6 +40,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)); diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index 3b7f6cd032..4957427151 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -535,11 +535,17 @@ int CheckPDFiumCApi() { CHK(FPDF_InitLibrary); CHK(FPDF_InitLibraryWithConfig); CHK(EPDF_LoadBaseDocument); + CHK(EPDF_FreeBuffer); + CHK(EPDF_SaveDocumentToOwnedBuffer); + CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); CHK(EPDFLayer_GetBaseDocument); CHK(EPDFLayer_GetPromotedObjectCount); CHK(EPDFLayer_IsObjectPromoted); CHK(EPDFLayer_OpenLayer); - CHK(EPDFLayer_SaveDeltaToBuffer); + CHK(EPDFLayer_OpenLayerArtifact); + CHK(EPDFLayer_SaveDelta); + CHK(EPDFLayer_SaveDeltaToOwnedBuffer); + CHK(EPDFLayer_SaveLayerArtifactToOwnedBuffer); CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); CHK(FPDF_LoadDocument); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 77c9c69820..aa86677a6d 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -110,6 +111,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( @@ -667,10 +710,9 @@ TEST_F(FPDFViewEmbedderTest, OpenFreshLayerRendersWithEmptyOverlay) { ASSERT_TRUE(base); { - FileAccessForTesting layer_access("rectangles.pdf"); EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; ScopedFPDFDocument layer( - EPDFLayer_OpenLayer(base, &layer_access, nullptr, &status)); + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); ASSERT_TRUE(layer); EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); EXPECT_EQ(base, EPDFLayer_GetBaseDocument(layer.get())); @@ -697,10 +739,9 @@ TEST_F(FPDFViewEmbedderTest, OpenFreshLayerAnnotHandleDoesNotPromote) { ASSERT_TRUE(base); { - FileAccessForTesting layer_access("annotation_stamp_with_ap.pdf"); EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; ScopedFPDFDocument layer( - EPDFLayer_OpenLayer(base, &layer_access, nullptr, &status)); + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); ASSERT_TRUE(layer); EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); @@ -723,10 +764,9 @@ TEST_F(FPDFViewEmbedderTest, CreateAnnotOnFreshLayerPromotesOverlayOnly) { ASSERT_TRUE(base); { - FileAccessForTesting layer_access("rectangles.pdf"); EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; ScopedFPDFDocument layer( - EPDFLayer_OpenLayer(base, &layer_access, nullptr, &status)); + EPDFLayer_OpenLayer(base, nullptr, nullptr, &status)); ASSERT_TRUE(layer); EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, status); EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())); @@ -752,10 +792,9 @@ TEST_F(FPDFViewEmbedderTest, SaveLayerDeltaMaterializesWithBaseBytes) { std::string materialized; { - FileAccessForTesting layer_access("rectangles.pdf"); EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; ScopedFPDFDocument layer( - EPDFLayer_OpenLayer(base, &layer_access, nullptr, &open_status)); + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); ASSERT_TRUE(layer); EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); @@ -768,12 +807,25 @@ TEST_F(FPDFViewEmbedderTest, SaveLayerDeltaMaterializesWithBaseBytes) { ClearString(); EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; - ASSERT_TRUE(EPDFLayer_SaveDeltaToBuffer(layer.get(), this, &save_status)); + 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()); @@ -794,6 +846,166 @@ TEST_F(FPDFViewEmbedderTest, SaveLayerDeltaMaterializesWithBaseBytes) { 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())); @@ -802,10 +1014,77 @@ TEST_F(FPDFViewEmbedderTest, LayerDiagnosticsRejectPlainDocuments) { ClearString(); EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSuccess; - EXPECT_FALSE(EPDFLayer_SaveDeltaToBuffer(document(), this, &save_status)); + 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, LayerReplaySoakSmoke) { FileAccessForTesting base_access("rectangles.pdf"); EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); @@ -819,10 +1098,9 @@ TEST_F(FPDFViewEmbedderTest, LayerReplaySoakSmoke) { for (int expected_annots = 1; expected_annots <= 3; ++expected_annots) { std::string materialized; { - FileAccessForTesting layer_access("rectangles.pdf"); EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; ScopedFPDFDocument layer( - EPDFLayer_OpenLayer(base, &layer_access, nullptr, &open_status)); + EPDFLayer_OpenLayer(base, nullptr, nullptr, &open_status)); ASSERT_TRUE(layer); EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); @@ -837,7 +1115,7 @@ TEST_F(FPDFViewEmbedderTest, LayerReplaySoakSmoke) { ClearString(); EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; - ASSERT_TRUE(EPDFLayer_SaveDeltaToBuffer(layer.get(), this, &save_status)); + 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()); @@ -855,6 +1133,112 @@ TEST_F(FPDFViewEmbedderTest, LayerReplaySoakSmoke) { 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); diff --git a/public/fpdf_save.h b/public/fpdf_save.h index d388ae11fd..72a5dfcfa2 100644 --- a/public/fpdf_save.h +++ b/public/fpdf_save.h @@ -86,6 +86,36 @@ 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, @@ -93,9 +123,9 @@ typedef enum { EPDFLayerSaveStatus_kSaveFailed = 2, } EPDFLayerSaveStatus; -// Function: EPDFLayer_SaveDeltaToBuffer +// Function: EPDFLayer_SaveDelta // Saves only a layer document's current overlay delta. The caller can -// materialize the layer as base bytes followed by this returned delta. +// 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. @@ -103,9 +133,27 @@ typedef enum { // Return value: // TRUE if succeed, FALSE if failed. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFLayer_SaveDeltaToBuffer(FPDF_DOCUMENT layer, - FPDF_FILEWRITE* file_write, - EPDFLayerSaveStatus* out_status); +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 } diff --git a/public/fpdfview.h b/public/fpdfview.h index dad1f1246c..47c4143dbd 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -595,9 +595,9 @@ typedef enum { // with FPDF_CloseDocument(). // Parameters: // base - A base document returned by EPDF_LoadBaseDocument(). -// pFileAccess - A structure for accessing the materialized layer -// bytes. For Slice 7.2 this must be the base bytes -// with no appended delta. +// 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: @@ -608,6 +608,16 @@ EPDFLayer_OpenLayer(EPDF_BASE_DOCUMENT base, 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 diff --git a/testing/tools/epdf_layer_memory_benchmark.cpp b/testing/tools/epdf_layer_memory_benchmark.cpp index 7b8930c11d..bfcc25f3d8 100644 --- a/testing/tools/epdf_layer_memory_benchmark.cpp +++ b/testing/tools/epdf_layer_memory_benchmark.cpp @@ -55,12 +55,11 @@ std::vector ParseLayerCounts(const std::string& spec) { 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); + 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))); + result.push_back( + static_cast(std::strtoull(token.c_str(), nullptr, 10))); } if (comma == std::string::npos) { break; @@ -77,8 +76,7 @@ bool AddTextAnnotation(FPDF_DOCUMENT layer) { if (!page) { return false; } - ScopedFPDFAnnotation annot( - EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); + ScopedFPDFAnnotation annot(EPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_TEXT)); return !!annot; } @@ -126,17 +124,15 @@ int main(int argc, char** argv) { const size_t baseline_rss = CurrentRssBytes(); std::vector layers; layers.reserve(layer_counts.back()); - std::vector layer_files; - layer_files.reserve(layer_counts.back()); - std::puts("layers,rss_bytes,delta_from_base_rss,bytes_per_layer," - "promoted_objects"); + 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) { - layer_files.emplace_back(&base_bytes); EPDFLayerOpenStatus status = EPDFLayerOpenStatus_kOpenFailed; - ScopedFPDFDocument layer(EPDFLayer_OpenLayer( - base, layer_files.back().file_access(), nullptr, &status)); + 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); diff --git a/testing/tools/epdf_layer_replay_soak.cpp b/testing/tools/epdf_layer_replay_soak.cpp index abd63f291d..e5c060f923 100644 --- a/testing/tools/epdf_layer_replay_soak.cpp +++ b/testing/tools/epdf_layer_replay_soak.cpp @@ -62,12 +62,60 @@ bool VerifyMaterializedAnnotCount(const std::vector& materialized, 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]"); + argv[0], + "[--layers=100] [--rounds=60] [--sleep-seconds=60] [--seed=N]"); return 2; } @@ -126,10 +174,9 @@ int main(int argc, char** argv) { expected_annots[edited_layer] += edit_dist(rng); for (size_t layer_index = 0; layer_index < layer_count; ++layer_index) { - epdf_layer_tool::MemoryFile layer_file(&base_bytes); EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; - ScopedFPDFDocument layer(EPDFLayer_OpenLayer( - base, layer_file.file_access(), nullptr, &open_status)); + 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); @@ -147,7 +194,7 @@ int main(int argc, char** argv) { epdf_layer_tool::StringWriter writer; EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; - if (!EPDFLayer_SaveDeltaToBuffer(layer.get(), &writer, &save_status) || + 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); @@ -167,6 +214,22 @@ int main(int argc, char** argv) { 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, From 7d468aff8daef7452844335b6593ab4ff217077b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 May 2026 21:45:36 +0300 Subject: [PATCH 15/28] ReadOnlyLayerWorkflowsProduceEmptyDelta --- core/fpdfapi/page/cpdf_docpagedata.cpp | 7 ++-- fpdfsdk/epdf_layer.cpp | 5 +++ fpdfsdk/fpdf_view_embeddertest.cpp | 55 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/core/fpdfapi/page/cpdf_docpagedata.cpp b/core/fpdfapi/page/cpdf_docpagedata.cpp index 8356c69300..a40e1e9369 100644 --- a/core/fpdfapi/page/cpdf_docpagedata.cpp +++ b/core/fpdfapi/page/cpdf_docpagedata.cpp @@ -452,10 +452,9 @@ RetainPtr CPDF_DocPageData::GetImage(uint32_t dwStreamObjNum) { return it->second; } - if (fallback_ && !GetDocument()->IsObjectPromoted(dwStreamObjNum)) { - return fallback_->GetImage(dwStreamObjNum); - } - + // 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; diff --git a/fpdfsdk/epdf_layer.cpp b/fpdfsdk/epdf_layer.cpp index 89eae4c787..56fb0856a6 100644 --- a/fpdfsdk/epdf_layer.cpp +++ b/fpdfsdk/epdf_layer.cpp @@ -372,6 +372,11 @@ EPDFLayer_SaveDelta(FPDF_DOCUMENT layer, return false; } + if (layer_doc->GetPromotedObjectCount() == 0) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return true; + } + if (!layer_doc->GetParser()) { return false; } diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index aa86677a6d..81feb76221 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -20,6 +20,7 @@ #include "fpdfsdk/fpdf_view_c_api_test.h" #include "public/cpp/fpdf_scopers.h" #include "public/fpdf_annot.h" +#include "public/fpdf_doc.h" #include "public/fpdf_save.h" #include "public/fpdfview.h" #include "testing/embedder_test.h" @@ -758,6 +759,60 @@ TEST_F(FPDFViewEmbedderTest, OpenFreshLayerAnnotHandleDoesNotPromote) { 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) { + 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); + } +} + TEST_F(FPDFViewEmbedderTest, CreateAnnotOnFreshLayerPromotesOverlayOnly) { FileAccessForTesting base_access("rectangles.pdf"); EPDF_BASE_DOCUMENT base = EPDF_LoadBaseDocument(&base_access, nullptr); From 8191aebc55b2230664ca256a2364b54989d3d5ee Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 May 2026 22:05:57 +0300 Subject: [PATCH 16/28] Keep layer read paths from promoting page and name-tree state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harden layer read-purity by preventing layer page lookup and name-tree read APIs from entering mutable PDF graph paths. This change: - Adds a CPDF_LayerDocument::GetPageDictionary() override so unresolved layer page slots fail closed instead of running PDFium’s mutable page-tree fallback. - Adds CPDF_NameTree::CreateForReading() and moves named destinations, attachments, JavaScript actions, and form-fill JavaScript scanning onto const traversal. - Keeps mutating name-tree operations on the existing mutable creation path. - Expands read-only layer canaries across render/cache, annotation, catalog, name-tree, malformed page-tree, and sibling-layer isolation cases. - Verifies read-only layer workflows save empty deltas while peer layer mutations remain isolated. --- core/fpdfapi/parser/cpdf_layer_document.cpp | 14 + core/fpdfapi/parser/cpdf_layer_document.h | 1 + core/fpdfdoc/cpdf_nametree.cpp | 41 ++- core/fpdfdoc/cpdf_nametree.h | 6 +- fpdfsdk/cpdfsdk_formfillenvironment.cpp | 2 +- fpdfsdk/fpdf_attachment.cpp | 60 +++-- fpdfsdk/fpdf_javascript.cpp | 4 +- fpdfsdk/fpdf_view.cpp | 4 +- fpdfsdk/fpdf_view_embeddertest.cpp | 282 ++++++++++++++++++-- 9 files changed, 350 insertions(+), 64 deletions(-) diff --git a/core/fpdfapi/parser/cpdf_layer_document.cpp b/core/fpdfapi/parser/cpdf_layer_document.cpp index 44747cef02..11b232118c 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.cpp +++ b/core/fpdfapi/parser/cpdf_layer_document.cpp @@ -131,6 +131,20 @@ RetainPtr CPDF_LayerDocument::GetMutableInfo() { 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); } diff --git a/core/fpdfapi/parser/cpdf_layer_document.h b/core/fpdfapi/parser/cpdf_layer_document.h index 1d4df51299..6d22237dcd 100644 --- a/core/fpdfapi/parser/cpdf_layer_document.h +++ b/core/fpdfapi/parser/cpdf_layer_document.h @@ -40,6 +40,7 @@ class CPDF_LayerDocument final : public 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; diff --git a/core/fpdfdoc/cpdf_nametree.cpp b/core/fpdfdoc/cpdf_nametree.cpp index b4f65c7a28..967a300e4a 100644 --- a/core/fpdfdoc/cpdf_nametree.cpp +++ b/core/fpdfdoc/cpdf_nametree.cpp @@ -455,8 +455,8 @@ RetainPtr LookupOldStyleNamedDest(CPDF_Document* doc, } // namespace -CPDF_NameTree::CPDF_NameTree(RetainPtr pRoot) - : root_(std::move(pRoot)) { +CPDF_NameTree::CPDF_NameTree(RetainPtr pRoot, bool read_only) + : root_(std::move(pRoot)), read_only_(read_only) { DCHECK(root_); } @@ -481,7 +481,34 @@ std::unique_ptr CPDF_NameTree::Create(CPDF_Document* doc, } return pdfium::WrapUnique( - new CPDF_NameTree(std::move(pCategory))); // Private ctor. + new CPDF_NameTree(std::move(pCategory), /*read_only=*/false)); +} + +// static +std::unique_ptr CPDF_NameTree::CreateForReading( + CPDF_Document* doc, + ByteStringView category) { + const CPDF_Dictionary* root = doc->GetRoot(); + if (!root) { + return nullptr; + } + + RetainPtr names = root->GetDictFor("Names"); + if (!names) { + return nullptr; + } + + RetainPtr category_dict = names->GetDictFor(category); + if (!category_dict) { + return nullptr; + } + + // CPDF_NameTree stores a mutable pointer because the same class also backs + // AddValueAndName() / DeleteValueAndName(). `read_only_` prevents mutation + // through trees constructed by this const traversal path. + return pdfium::WrapUnique(new CPDF_NameTree( + pdfium::WrapRetain(const_cast(category_dict.Get())), + /*read_only=*/true)); } // static @@ -509,14 +536,14 @@ std::unique_ptr CPDF_NameTree::CreateWithRootNameArray( pCategory->GetObjNum()); } - return pdfium::WrapUnique(new CPDF_NameTree(pCategory)); // Private ctor. + return pdfium::WrapUnique(new CPDF_NameTree(pCategory, /*read_only=*/false)); } // static std::unique_ptr CPDF_NameTree::CreateForTesting( CPDF_Dictionary* pRoot) { return pdfium::WrapUnique( - new CPDF_NameTree(pdfium::WrapRetain(pRoot))); // Private ctor. + new CPDF_NameTree(pdfium::WrapRetain(pRoot), /*read_only=*/false)); } // static @@ -524,7 +551,7 @@ RetainPtr CPDF_NameTree::LookupNamedDest( CPDF_Document* doc, const ByteString& name) { RetainPtr dest_array; - std::unique_ptr name_tree = Create(doc, "Dests"); + std::unique_ptr name_tree = CreateForReading(doc, "Dests"); if (name_tree) { dest_array = name_tree->LookupNewStyleNamedDest(name); } @@ -541,6 +568,7 @@ size_t CPDF_NameTree::GetCount() const { bool CPDF_NameTree::AddValueAndName(RetainPtr pObj, const WideString& name) { + CHECK(!read_only_); NodeToInsert node_to_insert; // Handle the corner case where the root node is empty. i.e. No kids and no // names. In which case, just insert into it and skip all the searches. @@ -601,6 +629,7 @@ bool CPDF_NameTree::AddValueAndName(RetainPtr pObj, } bool CPDF_NameTree::DeleteValueAndName(size_t nIndex) { + CHECK(!read_only_); std::optional result = SearchNameNodeByIndex(root_.Get(), nIndex); if (!result) { diff --git a/core/fpdfdoc/cpdf_nametree.h b/core/fpdfdoc/cpdf_nametree.h index a6505a65cd..de6b4043f8 100644 --- a/core/fpdfdoc/cpdf_nametree.h +++ b/core/fpdfdoc/cpdf_nametree.h @@ -27,6 +27,9 @@ class CPDF_NameTree { static std::unique_ptr Create(CPDF_Document* doc, ByteStringView category); + static std::unique_ptr CreateForReading( + CPDF_Document* doc, + ByteStringView category); // If necessary, create missing Names dictionary in |doc|, and/or missing // Names array in the dictionary that corresponds to |category|, if necessary. @@ -52,11 +55,12 @@ class CPDF_NameTree { CPDF_Dictionary* GetRootForTesting() const { return root_.Get(); } private: - explicit CPDF_NameTree(RetainPtr pRoot); + CPDF_NameTree(RetainPtr pRoot, bool read_only); RetainPtr LookupNewStyleNamedDest(const ByteString& name); RetainPtr const root_; + const bool read_only_; }; #endif // CORE_FPDFDOC_CPDF_NAMETREE_H_ diff --git a/fpdfsdk/cpdfsdk_formfillenvironment.cpp b/fpdfsdk/cpdfsdk_formfillenvironment.cpp index bf887c5797..5c91858b62 100644 --- a/fpdfsdk/cpdfsdk_formfillenvironment.cpp +++ b/fpdfsdk/cpdfsdk_formfillenvironment.cpp @@ -685,7 +685,7 @@ CPDFSDK_PageView* CPDFSDK_FormFillEnvironment::GetPageViewAtIndex(int nIndex) { } void CPDFSDK_FormFillEnvironment::ProcJavascriptAction() { - auto name_tree = CPDF_NameTree::Create(cpdfdoc_, "JavaScript"); + auto name_tree = CPDF_NameTree::CreateForReading(cpdfdoc_, "JavaScript"); if (!name_tree) { return; } diff --git a/fpdfsdk/fpdf_attachment.cpp b/fpdfsdk/fpdf_attachment.cpp index 5f0d908d41..40ae66baef 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_javascript.cpp b/fpdfsdk/fpdf_javascript.cpp index c5a9555760..9ee17d61c5 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_view.cpp b/fpdfsdk/fpdf_view.cpp index 2cef39bfe8..a423735d47 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -1761,7 +1761,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) { @@ -1883,7 +1883,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_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 81feb76221..e36b7945a1 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -20,7 +20,9 @@ #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/fpdfview.h" #include "testing/embedder_test.h" @@ -204,6 +206,51 @@ 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 TestRenderPageBitmapWithMatrix(FPDF_PAGE page, int bitmap_width, int bitmap_height, @@ -768,51 +815,230 @@ TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerWorkflowsProduceEmptyDelta) { }; for (const char* file_name : kFiles) { - FileAccessForTesting base_access(file_name); + 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, + 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) << file_name; + ASSERT_TRUE(base); 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; + ASSERT_TRUE(layer); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, open_status); - 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; + 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())); - 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; - } + 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()); - int link_pos = 0; - FPDF_LINK link = nullptr; - while (FPDFLink_Enumerate(page.get(), &link_pos, &link)) { - ASSERT_TRUE(link) << file_name << " page " << page_index; - } - } + EPDF_ReleaseBaseDocument(base); + } - EXPECT_EQ(0u, EPDFLayer_GetPromotedObjectCount(layer.get())) << file_name; + { + 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)) - << file_name; - EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status) << file_name; - EXPECT_TRUE(GetString().empty()) << file_name; + 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); From efe597b367f648d068c2e379faef3e2ea3d3079a Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 12 May 2026 11:36:31 +0300 Subject: [PATCH 17/28] Add some more tests --- fpdfsdk/fpdf_view_embeddertest.cpp | 343 +++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index e36b7945a1..5359fe0999 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -24,6 +24,7 @@ #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" @@ -251,6 +252,321 @@ class FPDFViewEmbedderTest : public EmbedderTest { 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, @@ -866,6 +1182,33 @@ TEST_F(FPDFViewEmbedderTest, ReadOnlyLayerCatalogMatrixProducesEmptyDelta) { } } +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"); From a3f16c270f8b199397c2427beaee06825ff04b2b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 12 May 2026 12:57:45 +0300 Subject: [PATCH 18/28] Prune unreachable new PDF objects during save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make `CPDF_Creator::WriteNewObjs()` reachability-aware so newly allocated indirect objects that are no longer referenced do not get written into saved PDFs. The save reachability set now includes normal document graph references plus trailer-owned roots such as `/Info` and non-inline `/Encrypt`, avoiding dangling trailer references while still pruning true orphans. Also refactor `EPDF_SetEncryption()` to use an inline encryption dictionary so it follows CPDF_Creator’s existing trailer-owned encrypt path, and update Bug1206 to assert render-only saves remain stable while reopened rendering still matches. --- core/fpdfapi/edit/cpdf_creator.cpp | 32 ++++++++ fpdfsdk/fpdf_annot_embeddertest.cpp | 24 +++--- fpdfsdk/fpdf_save_embeddertest.cpp | 114 ++++++++++++++++++++++++++++ fpdfsdk/fpdf_view.cpp | 8 +- 4 files changed, 165 insertions(+), 13 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index fe806bf2c7..07c86bbe7e 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_crypto_handler.h" @@ -136,6 +137,28 @@ ByteString FormatXrefOffset10(FX_FILESIZE offset) { return ByteString::Format("%010" PRId64, static_cast(offset)); } +std::set CollectSaveReachableObjects( + CPDF_Document* document, + const CPDF_Dictionary* encrypt_dict) { + std::set objects = GetObjectsWithReferences(document); + + // `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, @@ -223,8 +246,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; @@ -234,7 +264,9 @@ 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; } diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 44a0415b4e..5aa484fe4c 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -1997,8 +1997,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); @@ -2006,7 +2004,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) { @@ -2014,9 +2012,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(); } } @@ -2585,7 +2590,8 @@ TEST_F(FPDFAnnotEmbedderTest, GetFontSizeNegative) { } } -TEST_F(FPDFAnnotEmbedderTest, DirectAnnotationObjectNumberStaysZeroAfterRender) { +TEST_F(FPDFAnnotEmbedderTest, + DirectAnnotationObjectNumberStaysZeroAfterRender) { ASSERT_TRUE(OpenDocument("freetext_annotation_without_da.pdf")); ScopedPage page = LoadScopedPage(0); ASSERT_TRUE(page); @@ -2600,8 +2606,8 @@ TEST_F(FPDFAnnotEmbedderTest, DirectAnnotationObjectNumberStaysZeroAfterRender) ASSERT_TRUE(bitmap); EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get())); - ASSERT_TRUE(EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 10, - 20, 30)); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 10, 20, 30)); EXPECT_EQ(0u, EPDFAnnot_GetObjectNumber(annot.get())); } diff --git a/fpdfsdk/fpdf_save_embeddertest.cpp b/fpdfsdk/fpdf_save_embeddertest.cpp index 471756c918..30583b56ee 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) { @@ -85,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_view.cpp b/fpdfsdk/fpdf_view.cpp index a423735d47..b78c80a7c3 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -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; } From f769de77a8ec348b6a5dc19d068d8c520a14c349 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 15 May 2026 11:09:46 +0300 Subject: [PATCH 19/28] Add EPDF_LoadMemBaseDocument API Introduce EPDF_LoadMemBaseDocument to load a shareable base PDF document from a memory buffer. Refactor loading logic into LoadBaseDocumentImpl that accepts a RetainPtr so both file-access and in-memory paths share parsing code. Implement the in-memory path using CFX_ReadOnlySpanStream and UNSAFE_BUFFERS, and update public/fpdfview.h with the new API and documentation. Add test coverage: register the C API symbol in fpdf_view_c_api_test and add an embedder test that loads a PDF from memory. Also add the necessary include headers. --- fpdfsdk/epdf_base_document.cpp | 44 +++++++++++++++++++++++++----- fpdfsdk/fpdf_view_c_api_test.c | 1 + fpdfsdk/fpdf_view_embeddertest.cpp | 20 ++++++++++++++ public/fpdfview.h | 24 +++++++++++++++- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/fpdfsdk/epdf_base_document.cpp b/fpdfsdk/epdf_base_document.cpp index 212b329c11..6aeba4f850 100644 --- a/fpdfsdk/epdf_base_document.cpp +++ b/fpdfsdk/epdf_base_document.cpp @@ -4,9 +4,17 @@ #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" @@ -22,17 +30,16 @@ EPDF_BASE_DOCUMENT EPDFBaseDocumentFromCPDFBaseDocument( return reinterpret_cast(base); } -} // namespace - -FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV -EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password) { - if (!pFileAccess) { +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( - pdfium::MakeRetain(pFileAccess), password); + CPDF_Parser::Error error = + base->LoadBaseDoc(std::move(file_access), password); if (error != CPDF_Parser::SUCCESS) { ProcessParseError(error); return nullptr; @@ -41,6 +48,29 @@ EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password) { return EPDFBaseDocumentFromCPDFBaseDocument(base.Leak()); } +} // 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, + 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); +} + FPDF_EXPORT void FPDF_CALLCONV EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base) { RetainPtr retained; diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index 4957427151..c08cf2aaf9 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -535,6 +535,7 @@ int CheckPDFiumCApi() { CHK(FPDF_InitLibrary); CHK(FPDF_InitLibraryWithConfig); CHK(EPDF_LoadBaseDocument); + CHK(EPDF_LoadMemBaseDocument); CHK(EPDF_FreeBuffer); CHK(EPDF_SaveDocumentToOwnedBuffer); CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 5359fe0999..4b789cc9c7 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -719,6 +719,26 @@ 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(), 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")); diff --git a/public/fpdfview.h b/public/fpdfview.h index 47c4143dbd..2270aa920a 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -575,8 +575,30 @@ FPDF_LoadCustomDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password); 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, + size_t size, + FPDF_BYTESTRING password); + // Function: EPDF_ReleaseBaseDocument -// Release a base document returned by EPDF_LoadBaseDocument(). +// Release a base document returned by EPDF_LoadBaseDocument() or +// EPDF_LoadMemBaseDocument(). FPDF_EXPORT void FPDF_CALLCONV EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base); From df004474365b641417c1eb0f276399cae0cd0006 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 15 May 2026 11:43:23 +0300 Subject: [PATCH 20/28] Add EPDF_LoadMemBaseDocument64 and helpers Introduce a size_t-based API for loading base documents from memory: add internal LoadMemBaseDocumentImpl and a public EPDF_LoadMemBaseDocument64 that accepts size_t. Preserve the existing EPDF_LoadMemBaseDocument(int) for backward compatibility but add a guard to reject negative sizes. Update tests to exercise the new API and register it in the C API test. Also add documentation for EPDF_LoadMemBaseDocument64 in the public header and update the release comment to reference both variants. --- fpdfsdk/epdf_base_document.cpp | 28 ++++++++++++++++++++++------ fpdfsdk/fpdf_view_c_api_test.c | 1 + fpdfsdk/fpdf_view_embeddertest.cpp | 23 ++++++++++++++++++++++- public/fpdfview.h | 25 +++++++++++++++++++++++-- 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/fpdfsdk/epdf_base_document.cpp b/fpdfsdk/epdf_base_document.cpp index 6aeba4f850..2f62be0c2f 100644 --- a/fpdfsdk/epdf_base_document.cpp +++ b/fpdfsdk/epdf_base_document.cpp @@ -48,6 +48,16 @@ EPDF_BASE_DOCUMENT LoadBaseDocumentImpl( 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 @@ -62,13 +72,19 @@ EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password) { FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV EPDF_LoadMemBaseDocument(const void* data_buf, - size_t size, + int 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); + 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 diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index c08cf2aaf9..ab1d8d8e3a 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -536,6 +536,7 @@ int CheckPDFiumCApi() { CHK(FPDF_InitLibraryWithConfig); CHK(EPDF_LoadBaseDocument); CHK(EPDF_LoadMemBaseDocument); + CHK(EPDF_LoadMemBaseDocument64); CHK(EPDF_FreeBuffer); CHK(EPDF_SaveDocumentToOwnedBuffer); CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 4b789cc9c7..7d1f7cb4be 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -724,8 +724,29 @@ TEST_F(FPDFViewEmbedderTest, LoadMemBaseDocument) { 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_LoadMemBaseDocument(file_bytes.data(), file_bytes.size(), nullptr); + EPDF_LoadMemBaseDocument64(file_bytes.data(), file_bytes.size(), nullptr); ASSERT_TRUE(base); EPDFLayerOpenStatus open_status = EPDFLayerOpenStatus_kOpenFailed; diff --git a/public/fpdfview.h b/public/fpdfview.h index 2270aa920a..ea96a4aa99 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -593,12 +593,33 @@ EPDF_LoadBaseDocument(FPDF_FILEACCESS* pFileAccess, FPDF_BYTESTRING password); // |password|. FPDF_EXPORT EPDF_BASE_DOCUMENT FPDF_CALLCONV EPDF_LoadMemBaseDocument(const void* data_buf, - size_t size, + 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_LoadMemBaseDocument()/EPDF_LoadMemBaseDocument64(). FPDF_EXPORT void FPDF_CALLCONV EPDF_ReleaseBaseDocument(EPDF_BASE_DOCUMENT base); From f66d083262e0cd562ef59dc25439abc0e75eda84 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 15 May 2026 14:17:25 +0300 Subject: [PATCH 21/28] Add GetPageObjectNumberByIndex API and tests Introduce EPDFDoc_GetPageObjectNumberByIndex to return a page dictionary's indirect object number by zero-based page index without constructing a CPDF_Page (returns 0 for invalid indices, XFA pages, or direct objects). Add the function declaration to public/fpdfview.h, implement it in fpdfsdk/fpdf_view.cpp (with PDF_ENABLE_XFA guard), register it in the C API test, and add an embedder test verifying null/invalid inputs, non-parsing behavior, and consistency with EPDFPage_GetObjectNumber after loading the page. --- fpdfsdk/fpdf_view.cpp | 19 +++++++++++++++++++ fpdfsdk/fpdf_view_c_api_test.c | 1 + fpdfsdk/fpdf_view_embeddertest.cpp | 24 ++++++++++++++++++++++++ public/fpdfview.h | 14 ++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index b78c80a7c3..8a4adfb560 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -709,6 +709,25 @@ 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 diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index ab1d8d8e3a..a20af2e314 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -537,6 +537,7 @@ int CheckPDFiumCApi() { CHK(EPDF_LoadBaseDocument); CHK(EPDF_LoadMemBaseDocument); CHK(EPDF_LoadMemBaseDocument64); + CHK(EPDFDoc_GetPageObjectNumberByIndex); CHK(EPDF_FreeBuffer); CHK(EPDF_SaveDocumentToOwnedBuffer); CHK(EPDF_SaveDocumentToOwnedBufferWithVersion); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 7d1f7cb4be..78371d327b 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -2457,6 +2457,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/fpdfview.h b/public/fpdfview.h index ea96a4aa99..e5ec75ab07 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -1776,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. // From 1bdc674caf62dbb086cace7d5c2814b34120010c Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 17 May 2026 10:59:30 +0300 Subject: [PATCH 22/28] Const-correct annot APIs; add annot index Make annotation dictionary parameters const-correct by changing GetAnnotAP, GetAnnotAPNoFallback, and HasAPStream to accept const CPDF_Dictionary*. Update FPDFAnnot_GetColor to use the const-returning GetAnnotDictFromFPDFAnnotation and pass the const dict to HasAPStream. Add an annot_index parameter to RawAnnotContext, forward it to the CPDF_AnnotContext base, and update the creation site to pass the index. These changes improve const-safety and propagate the annotation index into the annot context. --- core/fpdfdoc/cpdf_annot.cpp | 4 ++-- core/fpdfdoc/cpdf_annot.h | 4 ++-- fpdfsdk/fpdf_annot.cpp | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/fpdfdoc/cpdf_annot.cpp b/core/fpdfdoc/cpdf_annot.cpp index d8bc6a2e25..3d8926c83c 100644 --- a/core/fpdfdoc/cpdf_annot.cpp +++ b/core/fpdfdoc/cpdf_annot.cpp @@ -250,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); diff --git a/core/fpdfdoc/cpdf_annot.h b/core/fpdfdoc/cpdf_annot.h index 5a41be69d2..9285f1b6a2 100644 --- a/core/fpdfdoc/cpdf_annot.h +++ b/core/fpdfdoc/cpdf_annot.h @@ -247,12 +247,12 @@ class CPDF_Annot { // 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/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 4db7fa00b7..4de355a23d 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -502,8 +502,9 @@ 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()), + RetainPtr unparsed_page, + int annot_index) + : CPDF_AnnotContext(dict, unparsed_page.Get(), annot_index), owned_page_(std::move(unparsed_page)) {} private: @@ -572,7 +573,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); } @@ -1432,8 +1433,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; @@ -1442,7 +1442,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; } @@ -3710,7 +3710,8 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index) { // Create the context, which now takes the RetainPtr directly. auto ctx = - std::make_unique(std::move(annot_dict), std::move(page)); + std::make_unique(std::move(annot_dict), std::move(page), + index); // The lifetime is now perfectly managed by smart pointers. return FPDFAnnotationFromCPDFAnnotContext(ctx.release()); From ac54d56e0bc6961ec551c2e642952df02b795ecb Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 17 May 2026 11:06:32 +0300 Subject: [PATCH 23/28] Fix some readonly paths --- fpdfsdk/fpdf_annot.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 4de355a23d..5b09d34b80 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -1932,8 +1932,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; } @@ -1945,7 +1944,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(), @@ -2439,14 +2438,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 @@ -3499,8 +3498,7 @@ EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, 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; } @@ -4975,8 +4973,7 @@ EPDFAnnot_GetAPMatrix(FPDF_ANNOTATION annot, FPDF_EXPORT int FPDF_CALLCONV EPDFAnnot_GetAvailableAppearanceModes(FPDF_ANNOTATION annot) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + const CPDF_Dictionary* pAnnotDict = GetAnnotDictFromFPDFAnnotation(annot); if (!pAnnotDict) { return 0; } @@ -5003,14 +5000,13 @@ EPDFAnnot_GetAvailableAppearanceModes(FPDF_ANNOTATION annot) { FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_HasAppearanceStream(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode) { - RetainPtr pAnnotDict = - GetMutableAnnotDictFromFPDFAnnotation(annot); + 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) { From 19fbb6d780a6dc0635ab69e82da1a627a2c800df Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 17 May 2026 23:19:22 +0300 Subject: [PATCH 24/28] Honor layer overlays when collecting objects Add ObjectTreeReferenceResolveMode to control how CPDF_Reference targets are resolved (through their holder or via the traversed document). Update ObjectTreeTraverser and GetObjectsWithReferences to accept this mode (defaulting to the previous behavior). Use kEffectiveDocument when collecting reachable objects for layer documents so overlay/promoted objects from a layer override the frozen base graph and are included in saves. Also add a unit test (LayerArtifactIncludesNewAnnotObjectBodies) to verify layer artifacts include newly created annotation object bodies, and add a clarifying comment in CPDF_Creator::WriteOldObjs about incremental vs full saves. --- core/fpdfapi/edit/cpdf_creator.cpp | 12 ++- .../parser/object_tree_traversal_util.cpp | 29 +++++--- .../parser/object_tree_traversal_util.h | 25 ++++++- fpdfsdk/fpdf_view_embeddertest.cpp | 74 +++++++++++++++++++ 4 files changed, 128 insertions(+), 12 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_creator.cpp b/core/fpdfapi/edit/cpdf_creator.cpp index 07c86bbe7e..025c9b1ff2 100644 --- a/core/fpdfapi/edit/cpdf_creator.cpp +++ b/core/fpdfapi/edit/cpdf_creator.cpp @@ -140,7 +140,15 @@ ByteString FormatXrefOffset10(FX_FILESIZE offset) { std::set CollectSaveReachableObjects( CPDF_Document* document, const CPDF_Dictionary* encrypt_dict) { - std::set objects = GetObjectsWithReferences(document); + // 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. @@ -225,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; diff --git a/core/fpdfapi/parser/object_tree_traversal_util.cpp b/core/fpdfapi/parser/object_tree_traversal_util.cpp index eebcff1e4e..331437d001 100644 --- a/core/fpdfapi/parser/object_tree_traversal_util.cpp +++ b/core/fpdfapi/parser/object_tree_traversal_util.cpp @@ -25,8 +25,9 @@ namespace { class ObjectTreeTraverser { public: - explicit ObjectTreeTraverser(const CPDF_Document* document) - : document_(document) { + ObjectTreeTraverser(const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) + : document_(document), resolve_mode_(resolve_mode) { const CPDF_Parser* parser = document_->GetParser(); const CPDF_Dictionary* trailer = parser ? parser->GetTrailer() : nullptr; const CPDF_Dictionary* root = trailer ? trailer : document_->GetRoot(); @@ -83,11 +84,17 @@ class ObjectTreeTraverser { const uint32_t referenced_object_number = ref_object->GetRefObjNum(); RetainPtr referenced_object; - if (ref_object->HasIndirectObjectHolder()) { - // Calling GetIndirectObject() does not work for normal references. + if (resolve_mode_ == + ObjectTreeReferenceResolveMode::kEffectiveDocument) { + referenced_object = + document_->GetIndirectObject(referenced_object_number); + } else if (ref_object->HasIndirectObjectHolder()) { + // In kReferenceHolder mode, go through the reference's holder so + // lazy parsing can pull the referenced object off disk on demand. referenced_object = ref_object->GetDirect(); } else { - // Calling GetDirect() does not work for references from trailers. + // Inlined trailer references have no holder, so GetDirect() cannot + // resolve them. referenced_object = document_->GetIndirectObject(referenced_object_number); } @@ -171,6 +178,7 @@ class ObjectTreeTraverser { } UnownedPtr const document_; + const ObjectTreeReferenceResolveMode resolve_mode_; // Queue of objects to traverse. // - Pointers in the queue are non-null. @@ -196,8 +204,10 @@ class ObjectTreeTraverser { } // namespace -std::set GetObjectsWithReferences(const CPDF_Document* document) { - ObjectTreeTraverser traverser(document); +std::set GetObjectsWithReferences( + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) { + ObjectTreeTraverser traverser(document, resolve_mode); traverser.Traverse(); std::set results; @@ -208,8 +218,9 @@ std::set GetObjectsWithReferences(const CPDF_Document* document) { } std::set GetObjectsWithMultipleReferences( - const CPDF_Document* document) { - ObjectTreeTraverser traverser(document); + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode) { + ObjectTreeTraverser traverser(document, resolve_mode); traverser.Traverse(); std::set results; diff --git a/core/fpdfapi/parser/object_tree_traversal_util.h b/core/fpdfapi/parser/object_tree_traversal_util.h index e9db96dce9..1ef6f0624d 100644 --- a/core/fpdfapi/parser/object_tree_traversal_util.h +++ b/core/fpdfapi/parser/object_tree_traversal_util.h @@ -11,12 +11,31 @@ class CPDF_Document; +enum class ObjectTreeReferenceResolveMode { + // Resolve references through the holder stored on each CPDF_Reference. This + // preserves the historical traversal behavior for ordinary documents. + kReferenceHolder, + + // Resolve all references through the document being traversed. This is needed + // when the document can override referenced objects, such as a layer document + // whose overlay should take precedence over the frozen base graph. + kEffectiveDocument, +}; + // Traverses `document` starting with its trailer, if it has one, or starting at // the catalog, which always exists. The trailer should have a reference to the // catalog. The traversal avoids cycles. +// +// In `kReferenceHolder` mode, references are followed through their +// CPDF_IndirectObjectHolder. In `kEffectiveDocument` mode, references are +// resolved through `document` so any overlay it provides is honored. +// // Returns all the PDF objects (not CPDF_Objects) the traversal reached as a set // of object numbers. -std::set GetObjectsWithReferences(const CPDF_Document* document); +std::set GetObjectsWithReferences( + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode = + ObjectTreeReferenceResolveMode::kReferenceHolder); // Same as GetObjectsWithReferences(), but only returns the objects with // multiple references. References that would create a cycle are ignored. @@ -41,6 +60,8 @@ std::set GetObjectsWithReferences(const CPDF_Document* document); // references (B). Since (B) -> (C) -> (B) creates a cycle, the (C) -> (B) // reference does not count. std::set GetObjectsWithMultipleReferences( - const CPDF_Document* document); + const CPDF_Document* document, + ObjectTreeReferenceResolveMode resolve_mode = + ObjectTreeReferenceResolveMode::kReferenceHolder); #endif // CORE_FPDFAPI_PARSER_OBJECT_TREE_TRAVERSAL_UTIL_H_ diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index 78371d327b..c4d296dfc0 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -1750,6 +1750,80 @@ TEST_F(FPDFViewEmbedderTest, LayerOwnedBufferAndArtifactReplay) { 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); From b57a70cbac80e3efbe67b3e3a098c626cb17a031 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Mon, 18 May 2026 16:32:16 +0300 Subject: [PATCH 25/28] Add redaction API and form XObject helper Introduce EmbedPDF redaction support and a reusable helper to append Form XObjects to a page. - Add public API (public/epdf_redact.h) exposing EPDFAnnot_ApplyRedaction, EPDFAnnot_ApplyRedactionWithReport, EPDFPage_ApplyRedactions, and EPDFPage_ApplyRedactionsWithReport to apply redaction annotations and report removed annotations. - Implement redaction logic in fpdfsdk/epdf_redact.cpp: collects redaction areas, removes text, flattens optional RO streams, detaches widget entries from AcroForm, cascades popup removals, and writes removal reports with /NM UTF-8 handling. - Add page content helper (fpdfsdk/epdf_page_content_helpers.*) to append Form XObjects to pages and use it from fpdfsdk/fpdf_annot.cpp (replacing inlined flattening code). - Update BUILD.gn to include new sources and update public/fpdf_annot.h to expose the new API header. - Add tests and resources to cover redaction behaviors (removal, reporting, popup cascade, widget handling, preservation of sibling REDACTs, and touch-only annotations). These changes centralize redaction behavior, provide reporting for removed annotations, and factor Form XObject flattening into a reusable helper. --- fpdfsdk/BUILD.gn | 3 + fpdfsdk/epdf_page_content_helpers.cpp | 66 ++ fpdfsdk/epdf_page_content_helpers.h | 18 + fpdfsdk/epdf_redact.cpp | 613 ++++++++++++++++++ fpdfsdk/fpdf_annot.cpp | 246 +------ fpdfsdk/fpdf_annot_embeddertest.cpp | 186 ++++++ public/epdf_redact.h | 107 +++ public/fpdf_annot.h | 37 +- testing/resources/redact_annot.in | 31 +- testing/resources/redact_annot.pdf | Bin 683 -> 1119 bytes testing/resources/redact_apply_all_visible.in | 100 +++ .../resources/redact_apply_all_visible.pdf | Bin 0 -> 1588 bytes testing/resources/redact_popup_cascade.in | 83 +++ testing/resources/redact_popup_cascade.pdf | Bin 0 -> 1272 bytes testing/resources/redact_preserve_sibling.in | 87 +++ testing/resources/redact_preserve_sibling.pdf | Bin 0 -> 1379 bytes testing/resources/redact_remove_annots.in | 75 +++ testing/resources/redact_remove_annots.pdf | Bin 0 -> 1146 bytes testing/resources/redact_text_middle.in | 58 ++ testing/resources/redact_text_middle.pdf | Bin 0 -> 889 bytes testing/resources/redact_touch_only.in | 75 +++ testing/resources/redact_touch_only.pdf | Bin 0 -> 1144 bytes 22 files changed, 1507 insertions(+), 278 deletions(-) create mode 100644 fpdfsdk/epdf_page_content_helpers.cpp create mode 100644 fpdfsdk/epdf_page_content_helpers.h create mode 100644 fpdfsdk/epdf_redact.cpp create mode 100644 public/epdf_redact.h create mode 100644 testing/resources/redact_apply_all_visible.in create mode 100644 testing/resources/redact_apply_all_visible.pdf create mode 100644 testing/resources/redact_popup_cascade.in create mode 100644 testing/resources/redact_popup_cascade.pdf create mode 100644 testing/resources/redact_preserve_sibling.in create mode 100644 testing/resources/redact_preserve_sibling.pdf create mode 100644 testing/resources/redact_remove_annots.in create mode 100644 testing/resources/redact_remove_annots.pdf create mode 100644 testing/resources/redact_text_middle.in create mode 100644 testing/resources/redact_text_middle.pdf create mode 100644 testing/resources/redact_touch_only.in create mode 100644 testing/resources/redact_touch_only.pdf diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 99aacb3175..6132162e33 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -10,8 +10,11 @@ source_set("fpdfsdk") { "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/epdf_page_content_helpers.cpp b/fpdfsdk/epdf_page_content_helpers.cpp new file mode 100644 index 0000000000..857a65f544 --- /dev/null +++ b/fpdfsdk/epdf_page_content_helpers.cpp @@ -0,0 +1,66 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "fpdfsdk/epdf_page_content_helpers.h" + +#include + +#include "core/fpdfapi/page/cpdf_form.h" +#include "core/fpdfapi/page/cpdf_formobject.h" +#include "core/fpdfapi/page/cpdf_page.h" +#include "core/fpdfapi/page/cpdf_pageobject.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_stream.h" + +void EpdfAppendFormXObjectToPage(CPDF_Page* page, + RetainPtr form_stream, + const CFX_FloatRect& target_rect) { + if (!page || !form_stream) { + return; + } + + CPDF_Document* doc = page->GetDocument(); + if (!doc) { + return; + } + + RetainPtr form_dict = form_stream->GetDict(); + if (!form_dict) { + return; + } + + CFX_FloatRect form_bbox = form_dict->GetRectFor("BBox"); + form_bbox.Normalize(); + if (form_bbox.IsEmpty()) { + form_bbox = target_rect; + } + + float scale_x = 1.0f; + float scale_y = 1.0f; + if (form_bbox.Width() > 0) { + scale_x = target_rect.Width() / form_bbox.Width(); + } + if (form_bbox.Height() > 0) { + scale_y = target_rect.Height() / form_bbox.Height(); + } + + CFX_Matrix form_matrix; + form_matrix.a = scale_x; + form_matrix.d = scale_y; + form_matrix.e = target_rect.left - form_bbox.left * scale_x; + form_matrix.f = target_rect.bottom - form_bbox.bottom * scale_y; + + auto form = std::make_unique( + doc, page->GetMutableResources(), + pdfium::WrapRetain(const_cast(form_stream.Get()))); + form->ParseContent(); + + auto form_obj = std::make_unique( + CPDF_PageObject::kNoContentStream, std::move(form), form_matrix); + + form_obj->CalcBoundingBox(); + form_obj->SetDirty(true); + page->AppendPageObject(std::move(form_obj)); +} diff --git a/fpdfsdk/epdf_page_content_helpers.h b/fpdfsdk/epdf_page_content_helpers.h new file mode 100644 index 0000000000..bf49df5101 --- /dev/null +++ b/fpdfsdk/epdf_page_content_helpers.h @@ -0,0 +1,18 @@ +// Copyright 2026 The PDFium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FPDFSDK_EPDF_PAGE_CONTENT_HELPERS_H_ +#define FPDFSDK_EPDF_PAGE_CONTENT_HELPERS_H_ + +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/retain_ptr.h" + +class CPDF_Page; +class CPDF_Stream; + +void EpdfAppendFormXObjectToPage(CPDF_Page* page, + RetainPtr form_stream, + const CFX_FloatRect& target_rect); + +#endif // FPDFSDK_EPDF_PAGE_CONTENT_HELPERS_H_ diff --git a/fpdfsdk/epdf_redact.cpp b/fpdfsdk/epdf_redact.cpp new file mode 100644 index 0000000000..d025043021 --- /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 5b09d34b80..2d8d1c2534 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,6 @@ #include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" #include "core/fpdfapi/edit/cpdf_pagecontentgenerator.h" #include "core/fpdfapi/edit/cpdf_pageorganizer.h" -#include "core/fpdfapi/edit/cpdf_text_redactor.h" #include "core/fpdfapi/page/cpdf_annotcontext.h" #include "core/fpdfapi/page/cpdf_form.h" #include "core/fpdfapi/page/cpdf_formobject.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 { @@ -4124,103 +4125,6 @@ EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot) { 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(const CPDF_Page* page, @@ -4244,150 +4148,6 @@ int GetAnnotIndexOnPage(const CPDF_Page* page, } // namespace -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_ApplyRedaction(FPDF_PAGE page, FPDF_ANNOTATION annot) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { - return false; - } - - // Must be a REDACT annotation - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { - return false; - } - - const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return false; - } - - // 1. Extract redaction rectangles from QuadPoints or Rect - std::vector rects = GetRedactRectsFromAnnotDict(annot_dict); - if (rects.empty()) { - return false; - } - - // 2. Remove content using existing redactor (no black boxes - we use RO) - RedactTextInRects(pPage, pdfium::span(rects), - /*recurse_forms=*/true, - /*draw_black_boxes=*/false); - - // 3. Flatten RO stream if present - RetainPtr ro_stream = annot_dict->GetStreamFor("RO"); - if (ro_stream) { - CFX_FloatRect annot_rect = - annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - FlattenFormXObjectToPage(pPage, ro_stream, annot_rect); - } - // If no RO: content is removed but no overlay is added - - // 4. Remove the annotation from the page - int annot_index = GetAnnotIndexOnPage(pPage, annot_dict); - if (annot_index >= 0) { - RetainPtr annots = pPage->GetMutableAnnotsArray(); - if (annots) { - RetainPtr entry = annots->GetMutableObjectAt(annot_index); - uint32_t objnum = 0; - if (entry && entry->IsReference()) { - objnum = entry->AsReference()->GetRefObjNum(); - } else if (entry) { - objnum = entry->GetObjNum(); - } - annots->RemoveAt(annot_index); - if (objnum) { - pPage->GetDocument()->DeleteIndirectObject(objnum); - } - } - } - - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_ApplyRedactions(FPDF_PAGE page) { - CPDF_Page* pPage = CPDFPageFromFPDFPage(page); - if (!pPage) { - return false; - } - - // First pass: collect all redaction areas, RO streams, indices, and objnums - std::vector all_rects; - std::vector, CFX_FloatRect>> - ro_streams; - std::vector> redact_index_objnums; - - RetainPtr annot_list = pPage->GetMutableAnnotsArray(); - if (!annot_list || annot_list->IsEmpty()) { - return false; - } - - for (size_t i = 0; i < annot_list->size(); ++i) { - RetainPtr entry = annot_list->GetMutableObjectAt(i); - RetainPtr annot_dict = - ToDictionary(entry ? entry->GetMutableDirect() : nullptr); - if (!annot_dict) { - continue; - } - - // Check if this is a REDACT annotation - ByteString subtype = annot_dict->GetNameFor(pdfium::annotation::kSubtype); - if (subtype != "Redact") { - continue; - } - - // Track index and indirect object number for later removal - uint32_t objnum = 0; - if (entry && entry->IsReference()) { - objnum = entry->AsReference()->GetRefObjNum(); - } else if (annot_dict) { - objnum = annot_dict->GetObjNum(); - } - redact_index_objnums.push_back({i, objnum}); - - // Extract rectangles - std::vector rects = - GetRedactRectsFromAnnotDict(annot_dict.Get()); - for (const auto& rect : rects) { - all_rects.push_back(rect); - } - - // Collect RO stream if present - RetainPtr ro_stream = annot_dict->GetStreamFor("RO"); - if (ro_stream) { - CFX_FloatRect annot_rect = - annot_dict->GetRectFor(pdfium::annotation::kRect); - annot_rect.Normalize(); - ro_streams.push_back({ro_stream, annot_rect}); - } - } - - if (all_rects.empty()) { - 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); @@ -4415,7 +4175,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, 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); diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 5aa484fe4c..19ad4ec52d 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -7,6 +7,8 @@ #include #include +#include +#include #include #include #include @@ -29,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" @@ -48,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) { @@ -3267,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/public/epdf_redact.h b/public/epdf_redact.h new file mode 100644 index 0000000000..e0f62b68a3 --- /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 db493ca3d0..a767537cfe 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/testing/resources/redact_annot.in b/testing/resources/redact_annot.in index 99b7b4e8e9..e3e1de1341 100644 --- a/testing/resources/redact_annot.in +++ b/testing/resources/redact_annot.in @@ -15,6 +15,11 @@ endobj /Parent 2 0 R /Contents 4 0 R /MediaBox [0 0 612 792] + /Resources << + /Font << + /F1 6 0 R + >> + >> /Annots [ 5 0 R ] @@ -25,6 +30,22 @@ endobj {{streamlen}} >> stream +q +0.97 0.98 1 rg +0 0 612 792 re f +0.9 0.94 1 rg +285 526 72 20 re f +Q +BT +/F1 18 Tf +72 700 Td +(Visible redaction annotation fixture) Tj +/F1 12 Tf +72 660 Td +(The red annotation below marks the area that would be redacted.) Tj +293 533 Td +(target) Tj +ET endstream endobj {{object 5 0}} << @@ -32,12 +53,20 @@ endobj /Subtype /Redact /NM (Redact-1) /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) /QuadPoints [293 542 349 542 293 530 349 530] /P 3 0 R - /C [1 0.90196 0] /Rect [293 530 349 542] >> endobj +{{object 6 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj {{xref}} {{trailer}} {{startxref}} diff --git a/testing/resources/redact_annot.pdf b/testing/resources/redact_annot.pdf index 76c26078b2c614ac8740ee497fc3a9e9bfdf9fd9..a433bb6624c96d1cb0b5fadb4a5ce060cdf472bc 100644 GIT binary patch delta 551 zcmZ8ey-ve05LS_pkl%pq7!V*N;y5%(B~-*u5lo0;Ap~M@nrjJ!1RR%^0r3i;JO&TL z$iTpBFmiT)0T1?l_xU^fzI&bhoO}Lg_pvI+dVm#dY%rkZ?UjZ31s3`c0*AgMyInfE z-Iw>?lUR=#!OCbXoR8VExq2*B5p%lgb)c3yt#11 zwk5OA`7y{LBPQY;WXhILvMNh*q-&`fn5-;76;DNJl#7Si)QmN*fV^tt;zu5aja*(j zDT)3x3FhSlZj`>Rz!0sd2?~^fNjc6_NPZu~bj=kOodDWF;J#y&9^ssE`JSwoT4&x4 zocq);zaV?O146d_1|8m`&eem&se$EwZ+B})uGEjM&HAmiAg!}ya@E-F$%k8P{I_L} z$BA(XoAzyHTUF?$> +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 0000000000000000000000000000000000000000..5725b06a855fb1e63b6fb6c8cf47a2e7eab15daa GIT binary patch literal 1588 zcmbVMO>g2b5WV|X%mt}E6cQkONL8h!3)^bjZ40@f9$aE*sx+pKsY-v%9`+Zm_6O`6 zn-ZXCkv0M+^X#$TyctjB_UqszthPnv=kH&CL`VVYhbKBc6-2doIagFWmsVza#=xzd zseSty{zPnXt&&vs z^c#%>c!e$e>%l#*9I8Su%mnltcOU}|&y~SuNL@Y}i-|yfHGp7laYjMS)okrco@?0S zhyiN3vF*kVkDJ`tr{sR6@|k@Etrmw@Sfk{#co9Lh3wItuGBdHkoQ$F=i(Uma{4rY( z2)_o#$j>tFS75dq?abbZUMzfUF?I^5+o3oSM{+*TmL#(bnJUBY4symj-cOeUAHdb4zN)gQ#p?`|QJTb*)N#$iNJK#?2q%_fhlq))6{;n4_t zmSxF_I~>61Wg(6F2*6wW0tl*{{4#`f(j1YVHio~AZ+O9Ag<)*zTf*A{vml3~*eme9D4=c?sVya5SN=aG_E#dr zb=lUKFZ(V#ZV8;~b@_5xyWZ53g>ns}cA>IYWz&h=$k&@uQ{3Ay5W)JPRunZNn$jAB zc5asP5^F&uem1bMf!f)@8X@TJU{T|s$I@1nTZ=#KUVAjYqL&f?= hKhzpM8iH|mtc}ul#j&~`CXIE`gLOw#D*c;*_zQN*hynlr literal 0 HcmV?d00001 diff --git a/testing/resources/redact_popup_cascade.in b/testing/resources/redact_popup_cascade.in new file mode 100644 index 0000000000..e52830b8a1 --- /dev/null +++ b/testing/resources/redact_popup_cascade.in @@ -0,0 +1,83 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 120 120] + /Resources << + /Font << + /F1 8 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R 7 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +1 0.98 0.9 rg +0 0 120 120 re f +1 0.9 0.2 rg +20 20 15 15 re f +Q +BT +/F1 8 Tf +5 104 Td +(popup cascades with note) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-popup-parent) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (POPUP) + /QuadPoints [10 50 50 50 10 10 50 10] + /Rect [10 10 50 50] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Text + /NM (Text-parent) + /F 4 + /C [1 0.75 0] + /Rect [20 20 35 35] + /Contents (comment) + /Popup 7 0 R +>> +endobj +{{object 7 0}} << + /Type /Annot + /Subtype /Popup + /Rect [70 70 100 100] + /Parent 6 0 R + /Open true +>> +endobj +{{object 8 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_popup_cascade.pdf b/testing/resources/redact_popup_cascade.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4e68070121658911e3f17ab9eb811b3c4ff936c8 GIT binary patch literal 1272 zcmah}&2HL25WeqI%ms-Y0$#BBk)lY22DNHa9O9l(4jX%+j=<~M>y-3qdgv2YeSpr) z8nE0}vHY|1&CJd>KkH8Tc5x|tW6}BX>*sGF!2|R71XouApdY>D8v1i()y`}Q2-Q}X zFhHY~xV{!TOVC)5+;(D1a+#Ydvmj~tAx%nH56SzX;ZDH6++Dyzqq~cUTiog-RWoyd zwTE7Ez|R9&u5@XtB1U@59g&4W&src+!h}MjF#wXUUq~>s7^4Kg*7o0InL&9S5C9{7 zkLlfHP1a}DA9c31UqH$UC05!(tG)P2p<(0Hs2z|NbD?wsx>YbxhRb-S+~ zEw_246BS#6KHWmsNtYb!GFQ}v3(_sXr_UVLG%6mK{yj>Ro0FE$y4b0gNFOY8!)5q6 zWXHQoC80^F&}->IaJ+HtMCsgDI<{> +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 0000000000000000000000000000000000000000..7524d56d1f9ad52c196f8ba5919063ffcdb3e142 GIT binary patch literal 1379 zcmbVM-EP`26u$RUoEs#1L5K;VAXSwLD;rvAX~@lldSQrRsc76frh+}qF7|}e9$@F# zfka_arA5a0d_Kqi&Yv5zPvaK%E?MK}?_Yly2Ore(8LqAvKs!FXN@$OS5nHt(U?w&) zhYl)5?E0F?EJ0;Pa^1*klF3NzG6S5JU(+OqML^b16>|dqW$rH263yL>c*ImDsTis~ zEPS-$zWqIsW+Zd9(<`KBxg#=GXjup(a_H05s0@JQiwOy42JgtBTkN)PvP_}82nm3m zoiAywvL@}4^_|Q%<~#7tfE>$>mg0rIG2a{DoyF7zyMWnMx_(3+_YrIB@Mm?hc6L%2(S)N9L2^ z-Q@oBxg#M2$H6g`cFn`0Fs&m;G(fa$9V)Aj9htxFgqFvAIH_fX-bhpP1H@H*_)gJ_76a!V#z-lA(=aylnzZNX4~cHs#uwUHKRGgFjmGW6nEeIJXjbO{ literal 0 HcmV?d00001 diff --git a/testing/resources/redact_remove_annots.in b/testing/resources/redact_remove_annots.in new file mode 100644 index 0000000000..7f0d277fb5 --- /dev/null +++ b/testing/resources/redact_remove_annots.in @@ -0,0 +1,75 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 100 100] + /Resources << + /Font << + /F1 7 0 R + >> + >> + /Contents 4 0 R + /Annots [5 0 R 6 0 R] +>> +endobj +{{object 4 0}} << + {{streamlen}} +>> +stream +q +0.96 0.98 1 rg +0 0 100 100 re f +0.85 0.92 1 rg +20 20 30 30 re f +Q +BT +/F1 8 Tf +5 86 Td +(intersecting square is removed) Tj +ET +endstream +endobj +{{object 5 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-target) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) + /QuadPoints [10 40 40 40 10 10 40 10] + /Rect [10 10 40 40] +>> +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Square + /NM (Square-target) + /F 4 + /C [0 0.25 1] + /IC [0.85 0.93 1] + /Border [0 0 2] + /Rect [20 20 50 50] +>> +endobj +{{object 7 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_remove_annots.pdf b/testing/resources/redact_remove_annots.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d30988ccab083ce36d90fc14abf611b59c4bb35d GIT binary patch literal 1146 zcmah}OK#gR5Z&_>voVk?B8mD%27&;wW1~fz#<8_CvS2J)Rp5vXMH@~|(?w5M^Z=b9 zWm`}KRFGxO;|%%Ud^DKdPA|B3$p$}v|N6r?1fZUt;p&P3`1#up3I13Zu~8)fGf~P0 zLUfw5>uVQWXs>BZ_|PgcI;lxI0iw40k8u7I(6!#7OO7 z8DJC-?C+60=dw{-y+V4HJ0eqsk*z@{hf6vdodJ-1`#^%V!800&+g1Ojt`*wLgaAnC z+2u=qC%#{0U7GK}!;~a7MoaO+URmG`G0+=kz(JR+!xprJHLhp4y*CWqdKduSk-hEu z0~_UxIA@TrSpr!KdBM&qSSW=q=HtnC$B5}^T-$}g@Z@JtX z++cEhGtO<~!&VeCh2$}-Jb?Icv3@E=A8=he-R6tl!FLoeJlb=dtn0n zq@%4#)M6v03#pyHux&Ep=ZcjCkRa>R7*Kch`B4uNmt&48_jVXYqLH>k{ZDeUlcrjU z&R%;hu}2_d!SRALNumVSaKt#4l`g1HSepHV@d(S+^NRvx+rwh)u|Ae&IGsJeEbd@N ni)w>OavzMjQaJ0F2bwZ}QN2r)V8=peV>J!=kPQZt`ziYi0bv~s literal 0 HcmV?d00001 diff --git a/testing/resources/redact_text_middle.in b/testing/resources/redact_text_middle.in new file mode 100644 index 0000000000..b60f20b855 --- /dev/null +++ b/testing/resources/redact_text_middle.in @@ -0,0 +1,58 @@ +{{header}} +{{object 1 0}} << + /Type /Catalog + /Pages 2 0 R +>> +endobj +{{object 2 0}} << + /Type /Pages + /Count 1 + /Kids [3 0 R] +>> +endobj +{{object 3 0}} << + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 300 200] + /Resources << + /Font << + /F1 4 0 R + >> + >> + /Contents 5 0 R + /Annots [6 0 R] +>> +endobj +{{object 4 0}} << + /Type /Font + /Subtype /Type1 + /BaseFont /Courier +>> +endobj +{{object 5 0}} << + {{streamlen}} +>> +stream +BT +/F1 20 Tf +50 100 Td +(hello secret world) Tj +ET +endstream +endobj +{{object 6 0}} << + /Type /Annot + /Subtype /Redact + /NM (Redact-secret) + /F 4 + /C [1 0 0] + /IC [0 0 0] + /OverlayText (REDACT) + /QuadPoints [120 122 195 122 120 94 195 94] + /Rect [120 94 195 122] +>> +endobj +{{xref}} +{{trailer}} +{{startxref}} +%%EOF diff --git a/testing/resources/redact_text_middle.pdf b/testing/resources/redact_text_middle.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d2e2d844cfd0f9e1d85dff770876d6f8f836659c GIT binary patch literal 889 zcmZuw%TD7!5WLS<^d%A;ATxd>L_$a;BogA44Zc|%hPV@qj0cS;5crxs>=zDlB!9ri`%_s5@4p+JJUe}v;>0np9fo)Nku?Q~_9oS5n*HV_eL zCQeQS)&+qD$GaqtWXi~_Y6ps6FUq2UJZARY7M&A6(L+OvL=PRQ#1$5$9-1}el31~n z5J`E*$Qd?fW#=T%Yp2Y_5VH+9RFH6M0t1lv_C^A0M?D%y+ppczx;7N&Jx**ZW&W;G z?!x_VSNG1_#isdjhq^(pfjzJ#+O3(n*QU}X)=T#Q$$$}!v#6iMFcT~}l8`M#DnZe# zSt0fwu&N9+IJfBF)!3@ohwM>|Gu}qaZTG_ZH%-Da{cQi~onfKpeuV2Q*bC7?sK3v^ z2_*a&LC%LGn0-zWFCauWFK8?Mmf_k_f$`btDD%u;D_u-Y$=>FQ22oK2YLJE}#e>9$ zgQQ*H+yz{lC-}DcYl{mZp)cgu1-&$lQ&_-PPLZ*ELA}ZKj=tqoOtb9ZV);X^|Gi$) sCtv?@oz-Q9w*6mb#?VnGU-*!9Z_9TixqhYEI> +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 0000000000000000000000000000000000000000..908dfd3e3c981ae4490d674ffa5cf42cb004e385 GIT binary patch literal 1144 zcmah}!EV|>5WVkL%ms-Y0yfyjNKvFhgHp9A1l$wKVS@+95%nhPbwv6#J@gB!en4h+ z9c;OkYQfm^cEkBf7EFcH0-)MczT7WG5?E0;k zC4ykK!ufuGBQ@{;-aQuxqN-$aULP z_r|t)jj7xjhQ8iF-+AYDpECv*5b$jdTb>cm*8CHBEX#RIOXD@cqk(?1{5HR~iMN$Z zS1RWUZ%JTa1(Ky_r`LDsYup(d;n0B4X2iG&|Cg|NuB66y3C?p8@*?Y~Al8GBD$@vQ z+F`Mo(kW^ua8!>NGjx55SV@~9&sLWqPav~7)tGB0ekJwBBd5%nwu~ i_qkXr8b+QDK4Zg8{sT#ZJrq(Kt7+=fho*P6T!=TC2OEt5 literal 0 HcmV?d00001 From 66cb1475104c86c27625dd012443f776185c4a94 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 24 May 2026 18:33:05 +0300 Subject: [PATCH 26/28] Add EPDFLayer_SaveLayerArtifact API Introduce a streaming API to write layer artifacts without materializing the whole artifact in memory. - Add HashingTempFileWriter, a temporary-file-based FPDF_FILEWRITE that computes SHA-256 while writing, can finalize to get the digest, and can replay its contents to another FPDF_FILEWRITE. - Add BuildLayerArtifactHeader and WriteBytes helpers to assemble the artifact header and safely write byte spans to an FPDF_FILEWRITE. - Implement EPDFLayer_SaveLayerArtifact which saves a layer delta into a temp writer, finalizes the SHA-256, builds the layer artifact header, and streams header+delta to the provided FPDF_FILEWRITE while reporting status. - Refactor EPDFLayer_SaveLayerArtifactToOwnedBuffer to reuse BuildLayerArtifactHeader. - Add the public declaration and docs for EPDFLayer_SaveLayerArtifact to public/fpdf_save.h. - Update tests and test registry: add check for EPDFLayer_SaveLayerArtifact in fpdf_view_c_api_test.c and extend LayerOwnedBufferAndArtifactReplay test to exercise streaming save and replay validation; also a minor formatting tweak in another test. - Add include where needed. This change enables native/server code paths to write artifacts without holding the full artifact in memory and ensures the delta is integrity-protected via SHA-256. --- fpdfsdk/epdf_layer.cpp | 180 +++++++++++++++++++++++++++-- fpdfsdk/fpdf_view_c_api_test.c | 1 + fpdfsdk/fpdf_view_embeddertest.cpp | 29 ++++- public/fpdf_save.h | 17 +++ 4 files changed, 213 insertions(+), 14 deletions(-) diff --git a/fpdfsdk/epdf_layer.cpp b/fpdfsdk/epdf_layer.cpp index 56fb0856a6..ed3d881ce1 100644 --- a/fpdfsdk/epdf_layer.cpp +++ b/fpdfsdk/epdf_layer.cpp @@ -5,6 +5,7 @@ #include "public/fpdfview.h" #include +#include #include #include #include @@ -83,6 +84,92 @@ struct MemoryFileWriter : public FPDF_FILEWRITE { } }; +struct HashingTempFileWriter : public FPDF_FILEWRITE { + FILE* file = nullptr; + CRYPT_sha2_context sha_context = {}; + uint64_t size = 0; + bool failed = false; + bool finalized = false; + + HashingTempFileWriter() { + version = 1; + file = std::tmpfile(); + CRYPT_SHA256Start(&sha_context); + WriteBlock = [](FPDF_FILEWRITE* self, const void* buf, + unsigned long block_size) -> int { + auto* writer = static_cast(self); + if (!writer || !writer->file || writer->failed || writer->finalized) { + return 0; + } + if (writer->size + block_size < writer->size) { + writer->failed = true; + return 0; + } + if (block_size == 0) { + return 1; + } + const size_t written = fwrite(buf, 1, block_size, writer->file); + if (written != block_size) { + writer->failed = true; + return 0; + } + CRYPT_SHA256Update(&writer->sha_context, + UNSAFE_BUFFERS(pdfium::span( + static_cast(buf), block_size))); + writer->size += block_size; + return 1; + }; + } + + ~HashingTempFileWriter() { + if (file) { + fclose(file); + } + } + + bool IsValid() const { return file && !failed; } + + std::optional> FinishSha256() { + if (!IsValid() || finalized) { + return std::nullopt; + } + if (fflush(file) != 0) { + failed = true; + return std::nullopt; + } + finalized = true; + std::array digest = {}; + CRYPT_SHA256Finish(&sha_context, digest); + return digest; + } + + bool ReplayTo(FPDF_FILEWRITE* out) { + if (!out || !IsValid() || !finalized) { + return false; + } + if (fseek(file, 0, SEEK_SET) != 0) { + return false; + } + + std::array buffer = {}; + uint64_t remaining = size; + while (remaining > 0) { + const size_t chunk_size = + static_cast(std::min(buffer.size(), remaining)); + const size_t read = fread(buffer.data(), 1, chunk_size, file); + if (read != chunk_size) { + return false; + } + if (!out->WriteBlock(out, buffer.data(), + static_cast(chunk_size))) { + return false; + } + remaining -= chunk_size; + } + return true; + } +}; + CPDF_BaseDocument* CPDFBaseDocumentFromEPDFBaseDocument( EPDF_BASE_DOCUMENT base) { return reinterpret_cast(base); @@ -182,6 +269,40 @@ void AppendUint64LE(std::vector* buffer, uint64_t value) { } } +std::vector BuildLayerArtifactHeader( + CPDF_BaseDocument* base_doc, + uint64_t delta_size, + const std::array& delta_sha) { + std::vector artifact; + artifact.reserve(kLayerArtifactHeaderSize); + artifact.insert(artifact.end(), kLayerArtifactMagic, kLayerArtifactMagic + 8); + AppendUint32LE(&artifact, kLayerArtifactVersion); + AppendUint32LE(&artifact, kLayerArtifactHeaderSize); + AppendUint64LE(&artifact, static_cast(base_doc->GetRawBaseSize())); + AppendUint64LE(&artifact, + static_cast(base_doc->GetLayerAppendBaseOffset())); + AppendUint64LE(&artifact, delta_size); + const std::array& base_sha = + base_doc->GetRawBaseSha256(); + artifact.insert(artifact.end(), base_sha.begin(), base_sha.end()); + artifact.insert(artifact.end(), delta_sha.begin(), delta_sha.end()); + return artifact; +} + +bool WriteBytes(FPDF_FILEWRITE* file_write, pdfium::span bytes) { + if (!file_write) { + return false; + } + if (bytes.empty()) { + return true; + } + if (bytes.size() > std::numeric_limits::max()) { + return false; + } + return file_write->WriteBlock(file_write, bytes.data(), + static_cast(bytes.size())); +} + uint32_t ReadUint32LE(const uint8_t* data) { return static_cast(data[0]) | (static_cast(data[1]) << 8) | @@ -418,6 +539,51 @@ EPDFLayer_SaveDeltaToOwnedBuffer(FPDF_DOCUMENT layer, return CopyToOwnedBuffer(pdfium::as_byte_span(writer.data), out_size); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveLayerArtifact(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status) { + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSaveFailed); + if (!file_write) { + return false; + } + + CPDF_Document* document = CPDFDocumentFromFPDFDocument(layer); + CPDF_LayerDocument* layer_doc = CPDF_LayerDocument::FromDocument(document); + CPDF_BaseDocument* base_doc = + layer_doc ? layer_doc->GetBaseDocument() : nullptr; + if (!layer_doc || !base_doc) { + return false; + } + + HashingTempFileWriter delta_writer; + if (!delta_writer.IsValid()) { + return false; + } + + EPDFLayerSaveStatus save_status = EPDFLayerSaveStatus_kSaveFailed; + if (!EPDFLayer_SaveDelta(layer, &delta_writer, &save_status)) { + SetSaveStatus(out_status, save_status); + return false; + } + + std::optional> delta_sha = + delta_writer.FinishSha256(); + if (!delta_sha) { + return false; + } + + const std::vector header = + BuildLayerArtifactHeader(base_doc, delta_writer.size, *delta_sha); + if (!WriteBytes(file_write, pdfium::span(header)) || + !delta_writer.ReplayTo(file_write)) { + return false; + } + + SetSaveStatus(out_status, EPDFLayerSaveStatus_kSuccess); + return true; +} + FPDF_EXPORT void* FPDF_CALLCONV EPDFLayer_SaveLayerArtifactToOwnedBuffer(FPDF_DOCUMENT layer, unsigned long* out_size, @@ -452,19 +618,9 @@ EPDFLayer_SaveLayerArtifactToOwnedBuffer(FPDF_DOCUMENT layer, return nullptr; } - std::vector artifact; + std::vector artifact = BuildLayerArtifactHeader( + base_doc, static_cast(delta_writer.data.size()), *delta_sha); artifact.reserve(kLayerArtifactHeaderSize + delta_writer.data.size()); - artifact.insert(artifact.end(), 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()); diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index a20af2e314..b9f19dd83e 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -548,6 +548,7 @@ int CheckPDFiumCApi() { CHK(EPDFLayer_OpenLayerArtifact); CHK(EPDFLayer_SaveDelta); CHK(EPDFLayer_SaveDeltaToOwnedBuffer); + CHK(EPDFLayer_SaveLayerArtifact); CHK(EPDFLayer_SaveLayerArtifactToOwnedBuffer); CHK(EPDF_ReleaseBaseDocument); CHK(FPDF_LoadCustomDocument); diff --git a/fpdfsdk/fpdf_view_embeddertest.cpp b/fpdfsdk/fpdf_view_embeddertest.cpp index c4d296dfc0..6968fc4841 100644 --- a/fpdfsdk/fpdf_view_embeddertest.cpp +++ b/fpdfsdk/fpdf_view_embeddertest.cpp @@ -1734,6 +1734,16 @@ TEST_F(FPDFViewEmbedderTest, LayerOwnedBufferAndArtifactReplay) { artifact_size); EPDF_FreeBuffer(artifact_buffer); + ClearString(); + save_status = EPDFLayerSaveStatus_kSaveFailed; + ASSERT_TRUE(EPDFLayer_SaveLayerArtifact(layer.get(), this, &save_status)); + EXPECT_EQ(EPDFLayerSaveStatus_kSuccess, save_status); + std::string streamed_artifact = GetString(); + ASSERT_FALSE(streamed_artifact.empty()); + + // Independent saves may produce different trailer /ID values. Layer + // artifact correctness is replay validity, not byte-for-byte equality + // between separately generated artifacts. FPDF_FILEACCESS artifact_access = {}; artifact_access.m_FileLen = artifact.size(); artifact_access.m_GetBlock = GetBlockFromString; @@ -1747,6 +1757,22 @@ TEST_F(FPDFViewEmbedderTest, LayerOwnedBufferAndArtifactReplay) { ASSERT_TRUE(artifact_page); EXPECT_EQ(1, FPDFPage_GetAnnotCount(artifact_page.get())); + FPDF_FILEACCESS streamed_artifact_access = {}; + streamed_artifact_access.m_FileLen = streamed_artifact.size(); + streamed_artifact_access.m_GetBlock = GetBlockFromString; + streamed_artifact_access.m_Param = &streamed_artifact; + EPDFLayerOpenStatus streamed_artifact_open_status = + EPDFLayerOpenStatus_kOpenFailed; + ScopedFPDFDocument streamed_artifact_replayed( + EPDFLayer_OpenLayerArtifact(base, &streamed_artifact_access, nullptr, + &streamed_artifact_open_status)); + EXPECT_EQ(EPDFLayerOpenStatus_kSuccess, streamed_artifact_open_status); + ASSERT_TRUE(streamed_artifact_replayed); + ScopedFPDFPage streamed_artifact_page( + FPDF_LoadPage(streamed_artifact_replayed.get(), 0)); + ASSERT_TRUE(streamed_artifact_page); + EXPECT_EQ(1, FPDFPage_GetAnnotCount(streamed_artifact_page.get())); + EPDF_ReleaseBaseDocument(base); } @@ -2542,8 +2568,7 @@ TEST_F(FPDFViewEmbedderTest, EPDFDocGetPageObjectNumberByIndex) { // Page 1 doesn't exist. EXPECT_EQ(0u, EPDFDoc_GetPageObjectNumberByIndex(document(), 1)); - const unsigned int objnum = - EPDFDoc_GetPageObjectNumberByIndex(document(), 0); + const unsigned int objnum = EPDFDoc_GetPageObjectNumberByIndex(document(), 0); EXPECT_NE(0u, objnum); CPDF_Document* doc = CPDFDocumentFromFPDFDocument(document()); diff --git a/public/fpdf_save.h b/public/fpdf_save.h index 72a5dfcfa2..ea84444703 100644 --- a/public/fpdf_save.h +++ b/public/fpdf_save.h @@ -137,6 +137,23 @@ EPDFLayer_SaveDelta(FPDF_DOCUMENT layer, FPDF_FILEWRITE* file_write, EPDFLayerSaveStatus* out_status); +// Function: EPDFLayer_SaveLayerArtifact +// Saves a server-facing layer artifact containing base identity +// metadata and the raw layer delta. Unlike the owned-buffer variant, +// this writes to the caller-supplied FPDF_FILEWRITE and is suitable +// for native/server paths that should avoid materializing the whole +// artifact in memory. +// Parameters: +// layer - A layer document returned by EPDFLayer_OpenLayer(). +// file_write - A pointer to a custom file write structure. +// out_status - Optional detailed save status. +// Return value: +// TRUE if succeed, FALSE if failed. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFLayer_SaveLayerArtifact(FPDF_DOCUMENT layer, + FPDF_FILEWRITE* file_write, + EPDFLayerSaveStatus* out_status); + // Function: EPDFLayer_SaveDeltaToOwnedBuffer // Saves only a layer document's current overlay delta into an owned // memory buffer. The caller must release the returned buffer with From 211d129a09feface541138c5c2480775a4ac8c6a Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 27 May 2026 14:05:04 +0300 Subject: [PATCH 27/28] Add password-probe and runtime owner APIs Introduce non-mutating password probe and a per-handle runtime owner-unlocked override. Adds CPDF_SecurityHandler::GetPermissionsForPasswordProbe, CheckPasswordNoMutate, and SetRuntimeOwnerUnlocked, plus public C APIs EPDF_CheckPasswordPermissions and EPDF_SetRuntimeOwnerPermissions. Implementations in fpdf_view.cpp and header declarations in public/fpdfview.h; tests updated/added in cpdf_security_handler_embeddertest.cpp and fpdf_view_c_api_test.c. These changes let embedders verify what permissions a password proves without changing the document's owner-unlocked state and allow embedder-managed sessions to temporarily treat a handle as owner-unlocked. --- core/fpdfapi/parser/cpdf_security_handler.cpp | 25 ++++++ core/fpdfapi/parser/cpdf_security_handler.h | 10 +++ .../cpdf_security_handler_embeddertest.cpp | 71 +++++++++++++++ fpdfsdk/fpdf_view.cpp | 87 +++++++++++++++++++ fpdfsdk/fpdf_view_c_api_test.c | 2 + public/fpdfview.h | 50 +++++++++++ 6 files changed, 245 insertions(+) diff --git a/core/fpdfapi/parser/cpdf_security_handler.cpp b/core/fpdfapi/parser/cpdf_security_handler.cpp index debcaddfca..be1a71e11c 100644 --- a/core/fpdfapi/parser/cpdf_security_handler.cpp +++ b/core/fpdfapi/parser/cpdf_security_handler.cpp @@ -230,6 +230,17 @@ uint32_t CPDF_SecurityHandler::GetPermissions(bool get_owner_perms) const { return dwPermission; } +uint32_t CPDF_SecurityHandler::GetPermissionsForPasswordProbe( + bool owner) const { + uint32_t dwPermission = owner ? 0xFFFFFFFF : permissions_; + if (encrypt_dict_ && + encrypt_dict_->GetByteStringFor("Filter") == "Standard") { + dwPermission &= 0xFFFFFFFC; + dwPermission |= 0xFFFFF0C0; + } + return dwPermission; +} + bool CPDF_SecurityHandler::UnlockOwner(const ByteString& password) { if (owner_unlocked_) { return true; // Already unlocked @@ -246,6 +257,20 @@ bool CPDF_SecurityHandler::UnlockOwner(const ByteString& password) { return false; } +bool CPDF_SecurityHandler::CheckPasswordNoMutate(const ByteString& password, + bool bOwner) { + const PasswordEncodingConversion saved_conversion = + password_encoding_conversion_; + const std::array saved_key = encrypt_key_; + + password_encoding_conversion_ = kUnknown; + const bool valid = CheckPassword(password, bOwner); + + password_encoding_conversion_ = saved_conversion; + encrypt_key_ = saved_key; + return valid; +} + static bool LoadCryptInfo(const CPDF_Dictionary* pEncryptDict, const ByteString& name, CPDF_CryptoHandler::Cipher* cipher, diff --git a/core/fpdfapi/parser/cpdf_security_handler.h b/core/fpdfapi/parser/cpdf_security_handler.h index 702dcb447e..e15985a266 100644 --- a/core/fpdfapi/parser/cpdf_security_handler.h +++ b/core/fpdfapi/parser/cpdf_security_handler.h @@ -40,6 +40,7 @@ class CPDF_SecurityHandler final : public Retainable { // When `get_owner_perms` is true, returns full permissions if unlocked by // owner. uint32_t GetPermissions(bool get_owner_perms) const; + uint32_t GetPermissionsForPasswordProbe(bool owner) const; bool IsMetadataEncrypted() const; CPDF_CryptoHandler* GetCryptoHandler() const { return crypto_handler_.get(); } @@ -54,6 +55,15 @@ class CPDF_SecurityHandler final : public Retainable { // Returns false if password is invalid, empty, or document isn't encrypted. bool UnlockOwner(const ByteString& password); + // Checks a password without changing the document's current unlock state. + // This is for app-level authorization probes; it must not be used to decrypt + // content for normal document operation. + bool CheckPasswordNoMutate(const ByteString& password, bool bOwner); + + // Runtime policy override used by EmbedPDF after app-level authorization has + // been handled outside PDFium. This changes only this opened document handle. + void SetRuntimeOwnerUnlocked(bool enabled) { owner_unlocked_ = enabled; } + // Returns true if owner permissions are currently unlocked. bool IsOwnerUnlocked() const { return owner_unlocked_; } diff --git a/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp b/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp index 5cbf93f2b2..4c8789a8e7 100644 --- a/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp +++ b/core/fpdfapi/parser/cpdf_security_handler_embeddertest.cpp @@ -138,6 +138,77 @@ TEST_F(CPDFSecurityHandlerEmbedderTest, OwnerPassword) { EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocUserPermissions(document())); } +TEST_F(CPDFSecurityHandlerEmbedderTest, + CheckPasswordPermissionsIgnoresCurrentOwnerUnlockState) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "5678")); + ASSERT_TRUE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFFFFC, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "1234", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_USER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFF2C0u, effective_permissions); + EXPECT_GE(revision, 0); + + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "5678", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_OWNER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFFFFCu, effective_permissions); +} + +TEST_F(CPDFSecurityHandlerEmbedderTest, + CheckPasswordPermissionsDoesNotUnlockOwner) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "1234")); + ASSERT_FALSE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "5678", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_OWNER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFFFFCu, effective_permissions); + EXPECT_FALSE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); +} + +TEST_F(CPDFSecurityHandlerEmbedderTest, SetRuntimeOwnerPermissions) { + ASSERT_TRUE(OpenDocumentWithPassword("encrypted.pdf", "1234")); + ASSERT_FALSE(EPDF_IsOwnerUnlocked(document())); + ASSERT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); + + EXPECT_TRUE(EPDF_SetRuntimeOwnerPermissions(document(), true)); + EXPECT_TRUE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFFFFC, FPDF_GetDocPermissions(document())); + + int kind = -1; + unsigned int user_permissions = 0; + unsigned int effective_permissions = 0; + int revision = -1; + EXPECT_TRUE(EPDF_CheckPasswordPermissions(document(), "1234", &kind, + &user_permissions, + &effective_permissions, &revision)); + EXPECT_EQ(EPDF_PASSWORD_PERMISSION_USER, kind); + EXPECT_EQ(0xFFFFF2C0u, user_permissions); + EXPECT_EQ(0xFFFFF2C0u, effective_permissions); + + EXPECT_TRUE(EPDF_SetRuntimeOwnerPermissions(document(), false)); + EXPECT_FALSE(EPDF_IsOwnerUnlocked(document())); + EXPECT_EQ(0xFFFFF2C0, FPDF_GetDocPermissions(document())); +} + TEST_F(CPDFSecurityHandlerEmbedderTest, PasswordAfterGenerateSave) { constexpr char kBasename[] = "encrypted"; { diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index 8a4adfb560..70a128e029 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -611,6 +611,93 @@ EPDF_UnlockOwnerPermissions(FPDF_DOCUMENT document, return security_handler->UnlockOwner(password); } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_CheckPasswordPermissions(FPDF_DOCUMENT document, + FPDF_BYTESTRING password, + int* out_kind, + unsigned int* out_user_permissions, + unsigned int* out_effective_permissions, + int* out_security_handler_revision) { + if (!out_kind || !out_user_permissions || !out_effective_permissions || + !out_security_handler_revision) { + return false; + } + + *out_kind = EPDF_PASSWORD_PERMISSION_INVALID; + *out_user_permissions = 0; + *out_effective_permissions = 0; + *out_security_handler_revision = -1; + + CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + CPDF_Parser* pParser = pDoc->GetParser(); + if (!pParser) { + return false; + } + + const auto& security_handler = pParser->GetSecurityHandler(); + if (!security_handler) { + *out_kind = EPDF_PASSWORD_PERMISSION_NONE; + *out_user_permissions = 0xFFFFFFFF; + *out_effective_permissions = 0xFFFFFFFF; + return true; + } + + RetainPtr encrypt_dict = pParser->GetEncryptDict(); + *out_security_handler_revision = + encrypt_dict ? encrypt_dict->GetIntegerFor("R") : -1; + + const unsigned int user_permissions = static_cast( + security_handler->GetPermissionsForPasswordProbe(/*owner=*/false)); + *out_user_permissions = user_permissions; + + ByteString raw_password(password ? password : ""); + + // This is a password probe, not a document-state probe. Do not use + // IsOwnerUnlocked(), UnlockOwner(), or FPDF_GetDocPermissions() here; those + // depend on or mutate the current handle state. Match PDFium's open path by + // checking a non-empty password against owner credentials first. + if (!raw_password.IsEmpty() && + security_handler->CheckPasswordNoMutate(raw_password, /*bOwner=*/true)) { + *out_kind = EPDF_PASSWORD_PERMISSION_OWNER; + *out_effective_permissions = static_cast( + security_handler->GetPermissionsForPasswordProbe(/*owner=*/true)); + return true; + } + + if (security_handler->CheckPasswordNoMutate(raw_password, /*bOwner=*/false)) { + *out_kind = EPDF_PASSWORD_PERMISSION_USER; + *out_effective_permissions = user_permissions; + return true; + } + + return false; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_SetRuntimeOwnerPermissions(FPDF_DOCUMENT document, FPDF_BOOL enabled) { + CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); + if (!pDoc) { + return false; + } + + CPDF_Parser* pParser = pDoc->GetParser(); + if (!pParser) { + return false; + } + + const auto& security_handler = pParser->GetSecurityHandler(); + if (!security_handler) { + return false; + } + + security_handler->SetRuntimeOwnerUnlocked(!!enabled); + return true; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_IsEncrypted(FPDF_DOCUMENT document) { CPDF_Document* pDoc = CPDFDocumentFromFPDFDocument(document); if (!pDoc) { diff --git a/fpdfsdk/fpdf_view_c_api_test.c b/fpdfsdk/fpdf_view_c_api_test.c index b9f19dd83e..558f340f0b 100644 --- a/fpdfsdk/fpdf_view_c_api_test.c +++ b/fpdfsdk/fpdf_view_c_api_test.c @@ -510,6 +510,8 @@ int CheckPDFiumCApi() { #ifdef PDF_ENABLE_V8 CHK(FPDF_GetArrayBufferAllocatorSharedInstance); #endif + CHK(EPDF_CheckPasswordPermissions); + CHK(EPDF_SetRuntimeOwnerPermissions); CHK(FPDF_GetDocPermissions); CHK(FPDF_GetDocUserPermissions); CHK(FPDF_GetFileVersion); diff --git a/public/fpdfview.h b/public/fpdfview.h index e5ec75ab07..4b79a39617 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -827,6 +827,42 @@ EPDF_SetEncryption(FPDF_DOCUMENT document, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_RemoveEncryption(FPDF_DOCUMENT document); +#define EPDF_PASSWORD_PERMISSION_INVALID 0 +#define EPDF_PASSWORD_PERMISSION_NONE 1 +#define EPDF_PASSWORD_PERMISSION_USER 2 +#define EPDF_PASSWORD_PERMISSION_OWNER 3 + +// Experimental EmbedPDF Extension API. +// Checks what permissions the given password proves without changing the +// document's current owner-unlocked state. +// +// document - handle to a document +// password - raw password to check +// out_kind - receives EPDF_PASSWORD_PERMISSION_* +// out_user_permissions - receives the document's user permission bits +// out_effective_permissions - receives permissions implied by |password| +// out_security_handler_revision - receives the security handler revision +// +// Returns TRUE if: +// - document is not encrypted (kind NONE, full permissions, revision -1) +// - password is valid as user or owner +// +// Returns FALSE if: +// - document is NULL +// - any out pointer is NULL +// - document has no parser +// - password is invalid for an encrypted document +// +// This is a password probe, not a runtime permission override. It must not be +// used as evidence that the current document handle is owner-unlocked. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_CheckPasswordPermissions(FPDF_DOCUMENT document, + FPDF_BYTESTRING password, + int* out_kind, + unsigned int* out_user_permissions, + unsigned int* out_effective_permissions, + int* out_security_handler_revision); + // Experimental EmbedPDF Extension API. // Attempts to unlock owner permissions on an already-opened encrypted document. // @@ -845,6 +881,20 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDF_UnlockOwnerPermissions(FPDF_DOCUMENT document, FPDF_BYTESTRING owner_password); +// Experimental EmbedPDF Extension API. +// Runtime policy override for EmbedPDF-managed sessions. +// +// document - handle to a document +// enabled - TRUE to make PDFium internals behave as owner-unlocked, FALSE to +// restore the normal user-permission state for this handle +// +// This does not prove an owner password and does not authorize the caller. +// Callers must enforce JWT/PDF permission policy outside PDFium. This exists so +// high-level PDFium subsystems that consult permissions (notably forms) do not +// silently block operations after app-level authorization has already happened. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDF_SetRuntimeOwnerPermissions(FPDF_DOCUMENT document, FPDF_BOOL enabled); + // Experimental EmbedPDF Extension API. // Checks if a document is encrypted. // From 25b803a41efd6bd125a8de41a0820ed5c9d8bcb1 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Wed, 27 May 2026 23:04:45 +0300 Subject: [PATCH 28/28] Add EMBD_Metadata API for annotation embed data Introduce support for an /EMBD_Metadata dictionary on annotations and provide a full API to read/write app-specific metadata. Adds helpers to access EMBD_Metadata keys (Rotation, UnrotatedRect, VerticalAlignment, CustomJSON) and integrates them into shape rotation, vertical alignment, stamp-fitting and rendering paths (GetShapeRotationInfo, EPDFAnnot_UpdateAppearanceToRect, EPDF_RenderAnnotBitmapUnrotated, etc.). Public API additions: EPDFAnnot_HasEmbedMetadata, EPDFAnnot_ClearEmbedMetadata, EPDFAnnot_ClearEmbedMetadataKey, EPDFAnnot_Set/GetEmbedMetadataString/Number/Boolean/Rect, EPDFAnnot_Set/GetEmbedMetadataJSON and EPDFDocument_ClearEmbedMetadata. Adds tests exercising the new metadata API and fixes minor formatting/whitespace issues. Deprecated/removed older EPDFRotate/EPDFUnrotatedRect/EPDF:VerticalAlignment helpers in favor of the unified EMBD_Metadata approach. --- core/fpdfdoc/cpdf_generateap.cpp | 40 ++- fpdfsdk/fpdf_annot.cpp | 439 ++++++++++++++++++++-------- fpdfsdk/fpdf_annot_embeddertest.cpp | 74 ++++- fpdfsdk/fpdf_view.cpp | 8 +- public/fpdf_annot.h | 381 +++++++++++++----------- public/fpdfview.h | 6 +- 6 files changed, 629 insertions(+), 319 deletions(-) diff --git a/core/fpdfdoc/cpdf_generateap.cpp b/core/fpdfdoc/cpdf_generateap.cpp index b5c03b2990..e399eb0635 100644 --- a/core/fpdfdoc/cpdf_generateap.cpp +++ b/core/fpdfdoc/cpdf_generateap.cpp @@ -436,8 +436,36 @@ AnnotationDimensionsAndColor GetAnnotationDimensionsAndColor( }; } +constexpr char kEmbedMetadataKey[] = "EMBD_Metadata"; +constexpr char kEmbedMetadataRotationKey[] = "Rotation"; +constexpr char kEmbedMetadataUnrotatedRectKey[] = "UnrotatedRect"; +constexpr char kEmbedMetadataVerticalAlignmentKey[] = "VerticalAlignment"; + +RetainPtr GetEmbedMetadataDict( + const CPDF_Dictionary* annot_dict) { + return annot_dict ? annot_dict->GetDictFor(kEmbedMetadataKey) : nullptr; +} + +float GetEmbedMetadataFloatFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetFloatFor(key) : 0.0f; +} + +CFX_FloatRect GetEmbedMetadataRectFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetRectFor(key) : CFX_FloatRect(); +} + +int GetEmbedMetadataIntegerFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetIntegerFor(key) : 0; +} + // Rotation info for shape annotations (Square, Circle) using EmbedPDF's -// custom /EPDFRotate and /EPDFUnrotatedRect entries. +// /EMBD_Metadata rotation fields. struct ShapeRotationInfo { CFX_FloatRect bbox; // BBox for the AP stream (unrotated rect in page coords) CFX_Matrix matrix; // Transforms from local BBox space to page/AABB space @@ -450,14 +478,16 @@ ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict) { info.matrix = CFX_Matrix(); info.bbox = annot_dict->GetRectFor(pdfium::annotation::kRect); - float rotate_deg = annot_dict->GetFloatFor("EPDFRotate"); + float rotate_deg = + GetEmbedMetadataFloatFor(annot_dict, kEmbedMetadataRotationKey); // Normalize to [0, 360) rotate_deg = fmod(fmod(rotate_deg, 360.0f) + 360.0f, 360.0f); if (rotate_deg < 0.01f || rotate_deg > 359.99f) { return info; // No rotation } - CFX_FloatRect unrotated = annot_dict->GetRectFor("EPDFUnrotatedRect"); + CFX_FloatRect unrotated = + GetEmbedMetadataRectFor(annot_dict, kEmbedMetadataUnrotatedRectKey); if (unrotated.IsEmpty()) { return info; // No unrotated rect stored -> no rotation in AP } @@ -789,8 +819,8 @@ RetainPtr GetDashArray(const CPDF_Dictionary* dict) { inline CPDF_Annot::VerticalAlignment GetVerticalAlign( const CPDF_Dictionary* annot_dict) { - const int v = - annot_dict ? annot_dict->GetIntegerFor("EPDF:VerticalAlignment") : 0; + const int v = GetEmbedMetadataIntegerFor(annot_dict, + kEmbedMetadataVerticalAlignmentKey); if (v < static_cast(CPDF_Annot::VerticalAlignment::kTop) || v > static_cast(CPDF_Annot::VerticalAlignment::kBottom)) { return CPDF_Annot::VerticalAlignment::kTop; // fallback diff --git a/fpdfsdk/fpdf_annot.cpp b/fpdfsdk/fpdf_annot.cpp index 2d8d1c2534..8659c21a9e 100644 --- a/fpdfsdk/fpdf_annot.cpp +++ b/fpdfsdk/fpdf_annot.cpp @@ -694,6 +694,52 @@ RetainPtr GetMutableAnnotDictFromFPDFAnnotation( return context ? context->GetMutableAnnotDict() : nullptr; } +constexpr char kEmbedMetadataKey[] = "EMBD_Metadata"; +constexpr char kEmbedMetadataCustomJSONKey[] = "CustomJSON"; + +RetainPtr GetEmbedMetadataDict( + const CPDF_Dictionary* annot_dict) { + return annot_dict ? annot_dict->GetDictFor(kEmbedMetadataKey) : nullptr; +} + +RetainPtr GetOrCreateEmbedMetadataDict( + RetainPtr annot_dict) { + if (!annot_dict) { + return nullptr; + } + + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (!metadata) { + metadata = annot_dict->SetNewFor(kEmbedMetadataKey); + } + return metadata; +} + +bool EmbedMetadataIsEmpty(const CPDF_Dictionary* metadata) { + return !metadata || metadata->size() == 0; +} + +void RemoveEmbedMetadataIfEmpty(RetainPtr annot_dict) { + RetainPtr metadata = + GetEmbedMetadataDict(annot_dict.Get()); + if (EmbedMetadataIsEmpty(metadata.Get())) { + annot_dict->RemoveFor(kEmbedMetadataKey); + } +} + +float GetEmbedMetadataFloatFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetFloatFor(key) : 0.0f; +} + +CFX_FloatRect GetEmbedMetadataRectFor(const CPDF_Dictionary* annot_dict, + ByteStringView key) { + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + return metadata ? metadata->GetRectFor(key) : CFX_FloatRect(); +} + static uint32_t EnsureIndirect(CPDF_Document* doc, RetainPtr dict) { uint32_t objnum = dict->GetObjNum(); @@ -1847,6 +1893,267 @@ EPDFAnnot_SetNumberValue(FPDF_ANNOTATION annot, return true; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_HasEmbedMetadata(FPDF_ANNOTATION annot) { + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); + return !!GetEmbedMetadataDict(annot_dict); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadata(FPDF_ANNOTATION annot) { + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return false; + } + + annot_dict->RemoveFor(kEmbedMetadataKey); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadataKey(FPDF_ANNOTATION annot, FPDF_BYTESTRING key) { + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict || !key) { + return false; + } + + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (!metadata) { + return true; + } + + metadata->RemoveFor(key); + RemoveEmbedMetadataIfEmpty(annot_dict); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor( + key, UNSAFE_BUFFERS(WideStringFromFPDFWideString(value).AsStringView())); + return true; +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen) { + if (!key) { + return 0; + } + + const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return 0; + } + + RetainPtr metadata = GetEmbedMetadataDict(annot_dict); + // SAFETY: required from caller. + return Utf16EncodeMaybeCopyAndReturnLength( + metadata ? metadata->GetUnicodeTextFor(key) : WideString(), + UNSAFE_BUFFERS(SpanFromFPDFApiArgs(buffer, buflen))); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor(key, value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float* value) { + if (!key || !value) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kNumber) { + return false; + } + + *value = object->GetNumber(); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL value) { + if (!key) { + return false; + } + + RetainPtr metadata = GetOrCreateEmbedMetadataDict( + GetMutableAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + metadata->SetNewFor(key, !!value); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL* value) { + if (!key || !value) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kBoolean) { + return false; + } + + *value = object->GetInteger() != 0; + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + const FS_RECTF* rect) { + if (!key) { + return false; + } + + RetainPtr annot_dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!annot_dict) { + return false; + } + + if (!rect) { + RetainPtr metadata = + annot_dict->GetMutableDictFor(kEmbedMetadataKey); + if (metadata) { + metadata->RemoveFor(key); + RemoveEmbedMetadataIfEmpty(annot_dict); + } + return true; + } + + RetainPtr metadata = + GetOrCreateEmbedMetadataDict(annot_dict); + if (!metadata) { + return false; + } + + metadata->SetRectFor(key, CFXFloatRectFromFSRectF(*rect)); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FS_RECTF* rect) { + if (!key || !rect) { + return false; + } + + RetainPtr metadata = + GetEmbedMetadataDict(GetAnnotDictFromFPDFAnnotation(annot)); + if (!metadata) { + return false; + } + + RetainPtr object = metadata->GetObjectFor(key); + if (!object || object->GetType() != CPDF_Object::Type::kArray) { + return false; + } + + *rect = FSRectFFromCFXFloatRect(metadata->GetRectFor(key)); + return true; +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataJSON(FPDF_ANNOTATION annot, FPDF_WIDESTRING json) { + return EPDFAnnot_SetEmbedMetadataString(annot, kEmbedMetadataCustomJSONKey, + json); +} + +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataJSON(FPDF_ANNOTATION annot, + FPDF_WCHAR* buffer, + unsigned long buflen) { + return EPDFAnnot_GetEmbedMetadataString(annot, kEmbedMetadataCustomJSONKey, + buffer, buflen); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDocument_ClearEmbedMetadata(FPDF_DOCUMENT document) { + CPDF_Document* pdf = CPDFDocumentFromFPDFDocument(document); + if (!pdf) { + return false; + } + + for (int page_index = 0; page_index < pdf->GetPageCount(); ++page_index) { + RetainPtr page_dict = + pdf->GetMutablePageDictionary(page_index); + if (!page_dict) { + continue; + } + + RetainPtr annots = page_dict->GetMutableArrayFor("Annots"); + if (!annots) { + continue; + } + + for (size_t annot_index = 0; annot_index < annots->size(); ++annot_index) { + RetainPtr annot_dict = + annots->GetMutableDictAt(annot_index); + if (annot_dict) { + annot_dict->RemoveFor(kEmbedMetadataKey); + } + } + } + + return true; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetAP(FPDF_ANNOTATION annot, FPDF_ANNOT_APPEARANCEMODE appearanceMode, @@ -3469,59 +3776,6 @@ EPDFAnnot_GetTextAlignment(FPDF_ANNOTATION annot) { return kDefaultAlignment; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, - FPDF_VERTICAL_ALIGNMENT alignment) { - RetainPtr annot_dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return false; - } - - // This property is only valid for FreeText annotations. - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { - return false; - } - - // Validate the enum range to ensure a valid value is passed. - if (alignment < FPDF_VERTICAL_ALIGNMENT_TOP || - alignment > FPDF_VERTICAL_ALIGNMENT_BOTTOM) { - return false; - } - - // Set the /EPDF:VerticalAlignment key in the annotation dictionary to the - // integer value of the enum. - annot_dict->SetNewFor("EPDF:VerticalAlignment", - static_cast(alignment)); - - return true; -} - -FPDF_EXPORT FPDF_VERTICAL_ALIGNMENT FPDF_CALLCONV -EPDFAnnot_GetVerticalAlignment(FPDF_ANNOTATION annot) { - const CPDF_Dictionary* annot_dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!annot_dict) { - return FPDF_VERTICAL_ALIGNMENT_TOP; - } - - // This property is only valid for FreeText annotations. - if (FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_FREETEXT) { - return FPDF_VERTICAL_ALIGNMENT_TOP; - } - - // GetIntegerFor() conveniently returns 0 if the key doesn't exist, - // which matches the PDF specification's default. - int alignment_value = annot_dict->GetIntegerFor("EPDF:VerticalAlignment"); - - // Validate the value is within the known enum range before casting. - if (alignment_value >= FPDF_VERTICAL_ALIGNMENT_TOP && - alignment_value <= FPDF_VERTICAL_ALIGNMENT_BOTTOM) { - return static_cast(alignment_value); - } - - return FPDF_VERTICAL_ALIGNMENT_TOP; -} - FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm) { if (!page || !nm || !*nm) { @@ -3708,9 +3962,8 @@ 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), - index); + 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()); @@ -3833,12 +4086,12 @@ EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit) { return false; } - // 1) Check for EPDFRotate + EPDFUnrotatedRect first. - float rotate_deg = ad->GetFloatFor("EPDFRotate"); + // 1) Check for /EMBD_Metadata rotation + unrotated rect first. + float rotate_deg = GetEmbedMetadataFloatFor(ad.Get(), "Rotation"); rotate_deg = fmod(fmod(rotate_deg, 360.0f) + 360.0f, 360.0f); bool has_rotation = (rotate_deg > 0.01f && rotate_deg < 359.99f); - CFX_FloatRect unrotated = ad->GetRectFor("EPDFUnrotatedRect"); + CFX_FloatRect unrotated = GetEmbedMetadataRectFor(ad.Get(), "UnrotatedRect"); bool use_rotation = has_rotation && !unrotated.IsEmpty(); // Use unrotated rect for image fitting when rotated, otherwise /Rect. @@ -4569,74 +4822,6 @@ EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, return exported_doc; } -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation) { - RetainPtr dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) { - return false; - } - - if (rotation == 0.0f) { - dict->RemoveFor("EPDFRotate"); - } else { - dict->SetNewFor("EPDFRotate", rotation); - } - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetExtendedRotation(FPDF_ANNOTATION annot, float* rotation) { - if (!rotation) { - return false; - } - - const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) { - return false; - } - - *rotation = dict->GetFloatFor("EPDFRotate"); - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetUnrotatedRect(FPDF_ANNOTATION annot, const FS_RECTF* rect) { - RetainPtr dict = - GetMutableAnnotDictFromFPDFAnnotation(annot); - if (!dict) { - return false; - } - - if (!rect) { - dict->RemoveFor("EPDFUnrotatedRect"); - return true; - } - - CFX_FloatRect float_rect = CFXFloatRectFromFSRectF(*rect); - if (float_rect.IsEmpty()) { - dict->RemoveFor("EPDFUnrotatedRect"); - } else { - dict->SetRectFor("EPDFUnrotatedRect", float_rect); - } - return true; -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect) { - if (!rect) { - return false; - } - - const CPDF_Dictionary* dict = GetAnnotDictFromFPDFAnnotation(annot); - if (!dict) { - return false; - } - - *rect = FSRectFFromCFXFloatRect(dict->GetRectFor("EPDFUnrotatedRect")); - return true; -} - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRect(FPDF_ANNOTATION annot, FS_RECTF* rect) { if (!FPDFAnnot_GetRect(annot, rect)) { diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index 19ad4ec52d..47449aed4e 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -60,8 +60,7 @@ std::wstring ExtractPageText(FPDF_PAGE page) { 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); + EXPECT_GT(FPDFText_GetText(text_page.get(), 0, char_count, buffer.data()), 0); return GetPlatformWString(buffer.data()); } @@ -83,8 +82,9 @@ RedactionReport ApplyRedactionWithReport(FPDF_PAGE page, 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) + for (uint32_t i = 0; i < report.written_count; ++i) { report.object_numbers.push_back(removed[i].object_number); + } return report; } @@ -98,8 +98,9 @@ RedactionReport ApplyPageRedactionsWithReport(FPDF_PAGE page) { 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) + for (uint32_t i = 0; i < report.written_count; ++i) { report.object_numbers.push_back(removed[i].object_number); + } return report; } @@ -1608,6 +1609,71 @@ TEST_F(FPDFAnnotEmbedderTest, GetNumberValue) { } } +TEST_F(FPDFAnnotEmbedderTest, EmbedMetadata) { + ASSERT_TRUE(OpenDocument("text_form_multiple.pdf")); + ScopedPage page = LoadScopedPage(0); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation annot(FPDFPage_GetAnnot(page.get(), 0)); + ASSERT_TRUE(annot); + + EXPECT_FALSE(EPDFAnnot_HasEmbedMetadata(annot.get())); + + ScopedFPDFWideString user_id = GetFPDFWideString(L"44"); + EXPECT_TRUE( + EPDFAnnot_SetEmbedMetadataString(annot.get(), "UserID", user_id.get())); + + unsigned long length_bytes = + EPDFAnnot_GetEmbedMetadataString(annot.get(), "UserID", nullptr, 0); + ASSERT_EQ(6u, length_bytes); + std::vector string_buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, + EPDFAnnot_GetEmbedMetadataString( + annot.get(), "UserID", string_buffer.data(), length_bytes)); + EXPECT_EQ(L"44", GetPlatformWString(string_buffer.data())); + + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataNumber(annot.get(), "Rotation", 12.5f)); + float number_value = 0.0f; + EXPECT_TRUE( + EPDFAnnot_GetEmbedMetadataNumber(annot.get(), "Rotation", &number_value)); + EXPECT_FLOAT_EQ(12.5f, number_value); + + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataBoolean(annot.get(), "Archived", true)); + FPDF_BOOL boolean_value = false; + EXPECT_TRUE(EPDFAnnot_GetEmbedMetadataBoolean(annot.get(), "Archived", + &boolean_value)); + EXPECT_TRUE(boolean_value); + + const FS_RECTF rect{1.0f, 2.0f, 3.0f, 4.0f}; + EXPECT_TRUE( + EPDFAnnot_SetEmbedMetadataRect(annot.get(), "UnrotatedRect", &rect)); + FS_RECTF rect_value; + EXPECT_TRUE(EPDFAnnot_GetEmbedMetadataRect(annot.get(), "UnrotatedRect", + &rect_value)); + EXPECT_FLOAT_EQ(rect.left, rect_value.left); + EXPECT_FLOAT_EQ(rect.bottom, rect_value.bottom); + EXPECT_FLOAT_EQ(rect.right, rect_value.right); + EXPECT_FLOAT_EQ(rect.top, rect_value.top); + + ScopedFPDFWideString json = GetFPDFWideString(L"{\"source\":\"test\"}"); + EXPECT_TRUE(EPDFAnnot_SetEmbedMetadataJSON(annot.get(), json.get())); + length_bytes = EPDFAnnot_GetEmbedMetadataJSON(annot.get(), nullptr, 0); + ASSERT_EQ(36u, length_bytes); + string_buffer = GetFPDFWideStringBuffer(length_bytes); + EXPECT_EQ(length_bytes, EPDFAnnot_GetEmbedMetadataJSON( + annot.get(), string_buffer.data(), length_bytes)); + EXPECT_EQ(L"{\"source\":\"test\"}", GetPlatformWString(string_buffer.data())); + + EXPECT_TRUE(EPDFAnnot_HasEmbedMetadata(annot.get())); + EXPECT_TRUE(EPDFAnnot_ClearEmbedMetadataKey(annot.get(), "UserID")); + EXPECT_TRUE(EPDFAnnot_HasEmbedMetadata(annot.get())); + EXPECT_EQ( + 2u, EPDFAnnot_GetEmbedMetadataString(annot.get(), "UserID", nullptr, 0)); + + EXPECT_TRUE(EPDFAnnot_ClearEmbedMetadata(annot.get())); + EXPECT_FALSE(EPDFAnnot_HasEmbedMetadata(annot.get())); +} + TEST_F(FPDFAnnotEmbedderTest, GetSetAP) { // Open a file with four annotations and load its first page. ASSERT_TRUE(OpenDocument("annotation_stamp_with_ap.pdf")); diff --git a/fpdfsdk/fpdf_view.cpp b/fpdfsdk/fpdf_view.cpp index 70a128e029..536e415a4a 100644 --- a/fpdfsdk/fpdf_view.cpp +++ b/fpdfsdk/fpdf_view.cpp @@ -1264,10 +1264,12 @@ EPDF_RenderAnnotBitmapUnrotated(FPDF_BITMAP bitmap, // 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. + // Use /EMBD_Metadata /UnrotatedRect as the target rect for MatchRect. + // Falls back to /Rect if EmbedPDF metadata is not set. + RetainPtr metadata = + pAnnot->GetAnnotDict()->GetDictFor("EMBD_Metadata"); CFX_FloatRect target = - pAnnot->GetAnnotDict()->GetRectFor("EPDFUnrotatedRect"); + metadata ? metadata->GetRectFor("UnrotatedRect") : CFX_FloatRect(); if (target.IsEmpty()) { target = pAnnot->GetRect(); } diff --git a/public/fpdf_annot.h b/public/fpdf_annot.h index a767537cfe..a9d7c84db4 100644 --- a/public/fpdf_annot.h +++ b/public/fpdf_annot.h @@ -103,13 +103,13 @@ typedef enum FPDFANNOT_COLORTYPE { } FPDFANNOT_COLORTYPE; typedef enum FPDF_ANNOT_BORDER_STYLE { - FPDF_ANNOT_BS_UNKNOWN = 0, - FPDF_ANNOT_BS_SOLID, - FPDF_ANNOT_BS_DASHED, - FPDF_ANNOT_BS_BEVELED, - FPDF_ANNOT_BS_INSET, - FPDF_ANNOT_BS_UNDERLINE, - FPDF_ANNOT_BS_CLOUDY + FPDF_ANNOT_BS_UNKNOWN = 0, + FPDF_ANNOT_BS_SOLID, + FPDF_ANNOT_BS_DASHED, + FPDF_ANNOT_BS_BEVELED, + FPDF_ANNOT_BS_INSET, + FPDF_ANNOT_BS_UNDERLINE, + FPDF_ANNOT_BS_CLOUDY } FPDF_ANNOT_BORDER_STYLE; typedef enum FPDF_BLENDMODE { @@ -231,7 +231,7 @@ typedef enum FPDF_ANNOT_NAME { typedef enum EPDF_STAMP_FIT { EPDF_STAMP_FIT_CONTAIN = 0, // preserve aspect, fully visible - EPDF_STAMP_FIT_COVER = 1, // preserve aspect, fill box, might crop + EPDF_STAMP_FIT_COVER = 1, // preserve aspect, fill box, might crop EPDF_STAMP_FIT_STRETCH = 2 // ignore aspect, fill box } EPDF_STAMP_FIT; @@ -757,6 +757,97 @@ EPDFAnnot_SetNumberValue(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, float value); +// Experimental EmbedPDF Extension API. +// Returns true if |annot| has an /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_HasEmbedMetadata(FPDF_ANNOTATION annot); + +// Experimental EmbedPDF Extension API. +// Removes the /EMBD_Metadata dictionary from |annot|. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadata(FPDF_ANNOTATION annot); + +// Experimental EmbedPDF Extension API. +// Removes |key| from |annot|'s /EMBD_Metadata dictionary. If the metadata +// dictionary becomes empty, /EMBD_Metadata is removed too. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearEmbedMetadataKey(FPDF_ANNOTATION annot, FPDF_BYTESTRING key); + +// Experimental EmbedPDF Extension API. +// Set a UTF-16LE string in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WIDESTRING value); + +// Experimental EmbedPDF Extension API. +// Get a UTF-16LE string from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataString(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Set a number in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float value); + +// Experimental EmbedPDF Extension API. +// Get a number from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataNumber(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + float* value); + +// Experimental EmbedPDF Extension API. +// Set a boolean in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL value); + +// Experimental EmbedPDF Extension API. +// Get a boolean from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataBoolean(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_BOOL* value); + +// Experimental EmbedPDF Extension API. +// Set a rect in |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + const FS_RECTF* rect); + +// Experimental EmbedPDF Extension API. +// Get a rect from |annot|'s /EMBD_Metadata dictionary. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataRect(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FS_RECTF* rect); + +// Experimental EmbedPDF Extension API. +// Set the arbitrary app-specific JSON bucket in /EMBD_Metadata /CustomJSON. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetEmbedMetadataJSON(FPDF_ANNOTATION annot, FPDF_WIDESTRING json); + +// Experimental EmbedPDF Extension API. +// Get the arbitrary app-specific JSON bucket from /EMBD_Metadata /CustomJSON. +FPDF_EXPORT unsigned long FPDF_CALLCONV +EPDFAnnot_GetEmbedMetadataJSON(FPDF_ANNOTATION annot, + FPDF_WCHAR* buffer, + unsigned long buflen); + +// Experimental EmbedPDF Extension API. +// Removes /EMBD_Metadata from every annotation in |document|. This does not +// remove standard annotation fields or appearance streams. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFDocument_ClearEmbedMetadata(FPDF_DOCUMENT document); + // Experimental API. // Set the AP (appearance string) in |annot|'s dictionary for a given // |appearanceMode|. @@ -837,8 +928,7 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFAnnot_SetFlags(FPDF_ANNOTATION annot, // // Returns the annotation flags specific to interactive forms. FPDF_EXPORT int FPDF_CALLCONV -FPDFAnnot_GetFormFieldFlags(FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot); +FPDFAnnot_GetFormFieldFlags(FPDF_FORMHANDLE handle, FPDF_ANNOTATION annot); // Experimental API. // Sets the form field flags for an interactive form annotation. @@ -1195,7 +1285,6 @@ FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot); FPDF_EXPORT FPDF_ATTACHMENT FPDF_CALLCONV FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name); - // Experimental EmbedPDF Extension API. // Get the color of an annotation. If no color is specified, default to yellow // for highlight annotation, black for all else. @@ -1205,12 +1294,11 @@ FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, FPDF_WIDESTRING name); // R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B); // Experimental EmbedPDF Extension API. // Set the color of an annotation. @@ -1220,12 +1308,11 @@ EPDFAnnot_GetColor(FPDF_ANNOTATION annot, // R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetColor(FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetColor(FPDF_ANNOTATION annot, + FPDFANNOT_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B); // Experimental EmbedPDF Extension API. // Set the opacity of an annotaion. @@ -1246,8 +1333,7 @@ EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, // // Returns true if succesful. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, - unsigned int* alpha /* 0-255 */); +EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, unsigned int* alpha /* 0-255 */); // Experimental EmbedPDF Extension API. // Clear the color of an annotation. @@ -1256,7 +1342,8 @@ EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, // type - type of the color to be cleared. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); // Experimental EmbedPDF Extension API. // Get the border style and width of an annotation. This function handles both @@ -1276,7 +1363,10 @@ EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width); // width - the border width to be set. // // Returns true if successful. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetBorderStyle(FPDF_ANNOTATION annot, FPDF_ANNOT_BORDER_STYLE style, float width); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetBorderStyle(FPDF_ANNOTATION annot, + FPDF_ANNOT_BORDER_STYLE style, + float width); // Experimental EmbedPDF Extension API. // Get the intensity of a cloudy border effect. @@ -1336,10 +1426,10 @@ EPDFAnnot_GetRectangleDifferences(FPDF_ANNOTATION annot, // Returns true on success, false on failure. FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRectangleDifferences(FPDF_ANNOTATION annot, - float left, - float bottom, - float right, - float top); + float left, + float bottom, + float right, + float top); // Experimental EmbedPDF Extension API. // Remove the rectangle differences (/RD) entry from a supported annotation. @@ -1384,10 +1474,10 @@ EPDFAnnot_GetBorderDashPattern(FPDF_ANNOTATION annot, // EPDFAnnot_GetBorderDashPatternCount(). // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, - const float* dash_array, - unsigned long count); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetBorderDashPattern(FPDF_ANNOTATION annot, + const float* dash_array, + unsigned long count); // Experimental EmbedPDF Extension API. // Generates or regenerates the appearance stream for a given annotation @@ -1426,20 +1516,20 @@ EPDFAnnot_GetBlendMode(FPDF_ANNOTATION annot); // ASCII byte string *without* the leading slash. If the caller includes a // leading '/', it is stripped. Returns false on invalid input or failure. // Succeeds regardless of subtype (permissive; /IT is just metadata). -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetIntent(FPDF_ANNOTATION annot, FPDF_BYTESTRING intent); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetIntent(FPDF_ANNOTATION annot, + FPDF_BYTESTRING intent); // Retrieves the Intent (/IT) name of an annotation as UTF-16 (without a // leading slash). Returns the number of 16-bit code units required (excluding // terminating NUL). If `buffer` is non-null and `buflen` large enough, copies -// the UTF-16 data and NUL-terminates it (same pattern as FPDFAnnot_GetStringValue). -// Returns 0 if annotation invalid, no /IT entry, or empty. +// the UTF-16 data and NUL-terminates it (same pattern as +// FPDFAnnot_GetStringValue). Returns 0 if annotation invalid, no /IT entry, or +// empty. FPDF_EXPORT unsigned long FPDF_CALLCONV EPDFAnnot_GetIntent(FPDF_ANNOTATION annot, FPDF_WCHAR* buffer, unsigned long buflen); - // Get the rich (formatted) text stored in the annotation’s /RC entry. // Returns the number of 16‑bit characters required (including the // terminating NUL). Call once with `buffer == nullptr` to get the size. @@ -1451,8 +1541,8 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, // Experimental EmbedPDF Extension API. // Set the line endings of a Line, Polyline, or FreeText annotation. // For Line/Polyline: writes /LE as a 2-element array [start_style, end_style]. -// For FreeText: writes /LE as a single name using end_style (Acrobat convention); -// start_style is ignored. +// For FreeText: writes /LE as a single name using end_style (Acrobat +// convention); start_style is ignored. // // annot - handle to an annotation. // start_style - the start line ending style (ignored for FreeText). @@ -1460,8 +1550,8 @@ EPDFAnnot_GetRichContent(FPDF_ANNOTATION annot, // FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, - FPDF_ANNOT_LINE_END start_style, - FPDF_ANNOT_LINE_END end_style); + FPDF_ANNOT_LINE_END start_style, + FPDF_ANNOT_LINE_END end_style); // Experimental EmbedPDF Extension API. // Get the line endings of a Line, Polyline, or FreeText annotation. @@ -1474,8 +1564,8 @@ EPDFAnnot_SetLineEndings(FPDF_ANNOTATION annot, // FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, - FPDF_ANNOT_LINE_END* start_style, - FPDF_ANNOT_LINE_END* end_style); + FPDF_ANNOT_LINE_END* start_style, + FPDF_ANNOT_LINE_END* end_style); // Experimental EmbedPDF Extension API. // Replace every vertex in the /Vertices array with the |points| supplied. @@ -1486,10 +1576,10 @@ EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, // count - the number of vertices to be set. // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, - const FS_POINTF* points, - unsigned long count); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetVertices(FPDF_ANNOTATION annot, + const FS_POINTF* points, + unsigned long count); // Experimental EmbedPDF Extension API. // Set (or create) the two end‑points of a **Line** annotation @@ -1500,12 +1590,11 @@ EPDFAnnot_GetLineEndings(FPDF_ANNOTATION annot, // end - pointer to an `FS_POINTF` holding the new end‑point. // // Returns true on success. - FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV - EPDFAnnot_SetLine(FPDF_ANNOTATION annot, - const FS_POINTF* start, - const FS_POINTF* end); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetLine(FPDF_ANNOTATION annot, + const FS_POINTF* start, + const FS_POINTF* end); -// Experimental EmbedPDF Extension API. +// Experimental EmbedPDF Extension API. // Set the default appearance of a FreeText annotation. // // annot - handle to an annotation. @@ -1546,8 +1635,8 @@ EPDFAnnot_GetDefaultAppearance(FPDF_ANNOTATION annot, // alignment - the text alignment to be set. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, FPDF_TEXT_ALIGNMENT alignment); // Experimental EmbedPDF Extension API. @@ -1556,37 +1645,16 @@ EPDFAnnot_SetTextAlignment(FPDF_ANNOTATION annot, // annot - handle to an annotation. // // Returns the text alignment. -FPDF_EXPORT FPDF_TEXT_ALIGNMENT FPDF_CALLCONV +FPDF_EXPORT FPDF_TEXT_ALIGNMENT FPDF_CALLCONV EPDFAnnot_GetTextAlignment(FPDF_ANNOTATION annot); -// Experimental EmbedPDF Extension API. -// Set the vertical alignment of a FreeText annotation. -// -// annot - handle to an annotation. -// alignment - the vertical alignment to be set. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetVerticalAlignment(FPDF_ANNOTATION annot, - FPDF_VERTICAL_ALIGNMENT alignment); - -// Experimental EmbedPDF Extension API. -// Get the vertical alignment of a FreeText annotation. -// -// annot - handle to an annotation. -// -// Returns the vertical alignment. -FPDF_EXPORT FPDF_VERTICAL_ALIGNMENT FPDF_CALLCONV -EPDFAnnot_GetVerticalAlignment(FPDF_ANNOTATION annot); - - // Get the annotation by name. // // page - handle to a page. // nm - the name of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // Remove the annotation by name. @@ -1595,7 +1663,7 @@ EPDFPage_GetAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // nm - the name of the annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // Set the linked annotation. @@ -1605,14 +1673,17 @@ EPDFPage_RemoveAnnotByName(FPDF_PAGE page, FPDF_WIDESTRING nm); // linked_annot - the linked annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, FPDF_ANNOTATION linked_annot); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, + FPDF_BYTESTRING key, + FPDF_ANNOTATION linked_annot); // Experimental EmbedPDF Extension API. // Set the action of a Link annotation. // // annot - handle to a link annotation. -// action - handle to an action dictionary (e.g. from EPDFAction_CreateGoTo()). +// action - handle to an action dictionary (e.g. from +// EPDFAction_CreateGoTo()). // // Returns true on success. // @@ -1620,8 +1691,8 @@ EPDFAnnot_SetLinkedAnnot(FPDF_ANNOTATION annot, FPDF_BYTESTRING key, FPDF_ANNOTA // * Only valid for FPDF_ANNOT_LINK annotations. // * The action must be an indirect object. // * Any existing /A entry will be replaced. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetAction(FPDF_ANNOTATION annot, + FPDF_ACTION action); // Experimental EmbedPDF Extension API. // Get the annotation count. @@ -1630,8 +1701,8 @@ EPDFAnnot_SetAction(FPDF_ANNOTATION annot, FPDF_ACTION action); // page_index - the index of the page. // // Returns the annotation count. -FPDF_EXPORT int FPDF_CALLCONV -EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, int page_index); +FPDF_EXPORT int FPDF_CALLCONV EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, + int page_index); // Experimental EmbedPDF Extension API. // Get the annotation by index. @@ -1641,7 +1712,7 @@ EPDFPage_GetAnnotCountRaw(FPDF_DOCUMENT doc, int page_index); // index - the index of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // Experimental EmbedPDF Extension API. @@ -1652,8 +1723,9 @@ EPDFPage_GetAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // index - the index of the annotation. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, + int page_index, + int index); // Experimental EmbedPDF Extension API. // Set the /Name entry of an annotation (icon name for text/file/sound, @@ -1663,8 +1735,8 @@ EPDFPage_RemoveAnnotRaw(FPDF_DOCUMENT doc, int page_index, int index); // name - the name to be set. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetName(FPDF_ANNOTATION annot, FPDF_ANNOT_NAME name); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetName(FPDF_ANNOTATION annot, + FPDF_ANNOT_NAME name); // Experimental EmbedPDF Extension API. // Get the /Name entry of an annotation. @@ -1676,8 +1748,9 @@ FPDF_EXPORT FPDF_ANNOT_NAME FPDF_CALLCONV EPDFAnnot_GetName(FPDF_ANNOTATION annot); // Experimental EmbedPDF Extension API. -// Resize the normal appearance (/AP/N) of a Stamp to match the annotation's /Rect -// using the specified fit policy. Updates the AP /BBox and the image's CTM. +// Resize the normal appearance (/AP/N) of a Stamp to match the annotation's +// /Rect using the specified fit policy. Updates the AP /BBox and the image's +// CTM. // // annot - handle to a Stamp annotation. // fit - one of EPDF_STAMP_FIT_*. @@ -1687,24 +1760,26 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_UpdateAppearanceToRect(FPDF_ANNOTATION annot, EPDF_STAMP_FIT fit); // Experimental EmbedPDF Extension API. -// Create an annotation. (the difference from FPDFPage_CreateAnnot is that it creates an indirect object) +// Create an annotation. (the difference from FPDFPage_CreateAnnot is that it +// creates an indirect object) // // page - handle to a page. // subtype - the subtype of the annotation. // // Returns the annotation. -FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV +FPDF_EXPORT FPDF_ANNOTATION FPDF_CALLCONV EPDFPage_CreateAnnot(FPDF_PAGE page, FPDF_ANNOTATION_SUBTYPE subtype); // Experimental EmbedPDF Extension API. // Set the rotation of an annotation in degrees. // // annot - handle to an annotation. -// rotation - the rotation in degrees (any value, e.g. 0, 45.5, 90, 180, etc.). +// rotation - the rotation in degrees (any value, e.g. 0, 45.5, 90, 180, +// etc.). // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, + float rotation); // Experimental EmbedPDF Extension API. // Get the rotation of an annotation in degrees. @@ -1713,8 +1788,8 @@ EPDFAnnot_SetRotate(FPDF_ANNOTATION annot, float rotation); // rotation - receives the rotation value in degrees. // // Returns true on success, false if annot is invalid or rotation is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, float* rotation); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRotate(FPDF_ANNOTATION annot, + float* rotation); // Experimental EmbedPDF Extension API. // Get the reply type (RT) of an annotation. This specifies how an annotation @@ -1798,8 +1873,8 @@ EPDFAnnot_GetOverlayTextRepeat(FPDF_ANNOTATION annot); // annot - handle to an annotation with an appearance stream // // Returns TRUE on success, FALSE if no appearance stream or error. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_Flatten(FPDF_PAGE page, FPDF_ANNOTATION annot); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_Flatten(FPDF_PAGE page, + FPDF_ANNOTATION annot); // Experimental EmbedPDF Extension API. // Set an annotation's normal appearance (AP/N) from a page of another document. @@ -1845,53 +1920,6 @@ FPDF_EXPORT FPDF_DOCUMENT FPDF_CALLCONV EPDFAnnot_ExportMultipleAppearancesAsDocument(FPDF_ANNOTATION* annots, int annot_count); -// Experimental EmbedPDF Extension API. -// Set the EmbedPDF extended rotation on an annotation. This stores a custom -// /EPDFRotate entry (not the standard /Rotate) for non-widget annotations. -// A value of 0 removes the key to keep the PDF clean. -// -// annot - handle to an annotation. -// rotation - the rotation in degrees. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetExtendedRotation(FPDF_ANNOTATION annot, float rotation); - -// Experimental EmbedPDF Extension API. -// Get the EmbedPDF extended rotation from an annotation. -// Reads the custom /EPDFRotate entry. -// -// annot - handle to an annotation. -// rotation - receives the rotation value in degrees. 0 if not set. -// -// Returns true on success, false if annot is invalid or rotation is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetExtendedRotation(FPDF_ANNOTATION annot, float* rotation); - -// Experimental EmbedPDF Extension API. -// Set the EmbedPDF unrotated rect on an annotation. This stores a custom -// /EPDFUnrotatedRect array representing the annotation's rect before rotation -// was applied. Follows the same FS_RECTF convention as FPDFAnnot_SetRect. -// -// annot - handle to an annotation. -// rect - pointer to the unrotated rect. Pass NULL to remove. -// -// Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetUnrotatedRect(FPDF_ANNOTATION annot, const FS_RECTF* rect); - -// Experimental EmbedPDF Extension API. -// Get the EmbedPDF unrotated rect from an annotation. -// Reads the custom /EPDFUnrotatedRect array. -// Follows the same FS_RECTF convention as FPDFAnnot_GetRect. -// -// annot - handle to an annotation. -// rect - receives the unrotated rect. All zeros if not set. -// -// Returns true on success, false if annot is invalid or rect is NULL. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect); - // Experimental EmbedPDF Extension API. // Get the annotation rectangle with normalization applied. // Wraps FPDFAnnot_GetRect and ensures the returned rect is normalized @@ -1901,8 +1929,8 @@ EPDFAnnot_GetUnrotatedRect(FPDF_ANNOTATION annot, FS_RECTF* rect); // rect - receives the normalized rectangle; must not be NULL. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetRect(FPDF_ANNOTATION annot, FS_RECTF* rect); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetRect(FPDF_ANNOTATION annot, + FS_RECTF* rect); // Experimental EmbedPDF Extension API. // Set the Matrix on an annotation's appearance stream for the given mode. @@ -1985,12 +2013,11 @@ typedef enum { // R,G,B - color components in 0..255. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int R, - unsigned int G, - unsigned int B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int R, + unsigned int G, + unsigned int B); // Experimental EmbedPDF Extension API. // Get a color from the widget annotation's /MK dictionary. @@ -2000,12 +2027,11 @@ EPDFAnnot_SetMKColor(FPDF_ANNOTATION annot, // R,G,B - pointers to receive color components in 0..255. // // Returns true on success, false if no MK color is set or annot is invalid. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, - EPDF_MK_COLORTYPE type, - unsigned int* R, - unsigned int* G, - unsigned int* B); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_GetMKColor(FPDF_ANNOTATION annot, + EPDF_MK_COLORTYPE type, + unsigned int* R, + unsigned int* G, + unsigned int* B); // Experimental EmbedPDF Extension API. // Remove a color from the widget annotation's /MK dictionary. @@ -2025,8 +2051,9 @@ EPDFAnnot_ClearMKColor(FPDF_ANNOTATION annot, EPDF_MK_COLORTYPE type); // model so that subsequent FPDFAnnot_SetFormFieldFlags etc. calls work. // // page - handle to the page. -// handle - handle to the form fill module (FPDFDOC_InitFormFillEnvironment). -// field_type - one of FPDF_FORMFIELD_TEXTFIELD, FPDF_FORMFIELD_CHECKBOX, +// handle - handle to the form fill module +// (FPDFDOC_InitFormFillEnvironment). field_type - one of +// FPDF_FORMFIELD_TEXTFIELD, FPDF_FORMFIELD_CHECKBOX, // FPDF_FORMFIELD_RADIOBUTTON, FPDF_FORMFIELD_COMBOBOX, // FPDF_FORMFIELD_LISTBOX, FPDF_FORMFIELD_PUSHBUTTON. // field_name - the partial field name (/T). May be NULL for unnamed fields. @@ -2084,9 +2111,10 @@ EPDFAnnot_GetFormFieldObjectNumber(FPDF_FORMHANDLE handle, // Re-parent the source widget field into the target widget field so both // widgets share the same logical AcroForm field. // -// handle - handle to the form fill module (FPDFDOC_InitFormFillEnvironment). -// source_annot - handle to the widget annotation whose field should be merged. -// target_annot - handle to the widget annotation whose field should be reused. +// handle - handle to the form fill module +// (FPDFDOC_InitFormFillEnvironment). source_annot - handle to the widget +// annotation whose field should be merged. target_annot - handle to the +// widget annotation whose field should be reused. // // Returns true on success, false if the annotations are not compatible form // fields or the share operation could not be completed. @@ -2233,8 +2261,8 @@ EPDFPage_GetAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num); // index - the index of the annotation in the /Annots array. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_RemoveAnnot(FPDF_PAGE page, int index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_RemoveAnnot(FPDF_PAGE page, + int index); // Experimental EmbedPDF Extension API. // Remove an annotation on a page by its PDF indirect object number. @@ -2269,11 +2297,10 @@ EPDFPage_RemoveAnnotByObjectNumber(FPDF_PAGE page, unsigned int obj_num); // (objectNumber, /NM) is preserved across the move. // // Returns true on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFPage_MoveAnnots(FPDF_PAGE page, - const int* from_indices, - int from_indices_len, - int to_index); +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFPage_MoveAnnots(FPDF_PAGE page, + const int* from_indices, + int from_indices_len, + int to_index); #ifdef __cplusplus } // extern "C" diff --git a/public/fpdfview.h b/public/fpdfview.h index 4b79a39617..7accf476c1 100644 --- a/public/fpdfview.h +++ b/public/fpdfview.h @@ -1750,9 +1750,9 @@ EPDF_RenderAnnotBitmap(FPDF_BITMAP bitmap, // Experimental EmbedPDF Extension API. // Renders the annotation's AP form content WITHOUT the AP stream's rotation -// Matrix applied, using /EPDFUnrotatedRect (falling back to /Rect) for the -// MatchRect mapping. This produces an unrotated bitmap suitable for UI layers -// that apply CSS rotation separately. +// Matrix applied, using /EMBD_Metadata /UnrotatedRect (falling back to /Rect) +// for the MatchRect mapping. This produces an unrotated bitmap suitable for UI +// layers that apply CSS rotation separately. // // Same parameters as EPDF_RenderAnnotBitmap. //