From 84c6071e5c03c1de24776b8564293c75a9a22f38 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Fri, 8 Aug 2025 23:25:40 +0300 Subject: [PATCH 1/8] Ability to redact text --- fpdfsdk/fpdf_edittext.cpp | 78 +++++++++++++++++++++++++++++++++++++++ public/fpdf_edit.h | 26 +++++++++++++ 2 files changed, 104 insertions(+) diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp index c08db236e0..033dd6d43e 100644 --- a/fpdfsdk/fpdf_edittext.cpp +++ b/fpdfsdk/fpdf_edittext.cpp @@ -85,6 +85,44 @@ namespace { constexpr uint32_t kMaxBfCharBfRangeEntries = 100; +static std::vector& MutCodes(CPDF_TextObject* tobj) { + return const_cast&>(tobj->GetCharCodes()); +} + +bool BlankOne(CPDF_TextObject* tobj, int i) { + auto& codes = MutCodes(tobj); + if (i < 0 || i >= static_cast(codes.size())) + return false; + + // Null charcode => nothing drawn; positions/spacing unchanged. + codes[i] = 0; + // We don’t touch Unicode here; charcode 0 won’t extract anyway. + return true; +} + +bool BlankIndicesInternal(CPDF_TextObject* tobj, + const int* indices, + int n) { + if (!tobj || !indices || n <= 0) + return false; + + std::vector idx(indices, indices + n); + std::sort(idx.begin(), idx.end()); + idx.erase(std::unique(idx.begin(), idx.end()), idx.end()); + + const int size = static_cast(tobj->GetCharCodes().size()); + bool any = false; + for (int i : idx) { + if (i >= 0 && i < size) + any |= BlankOne(tobj, i); + } + + if (any) + tobj->SetDirty(true); + + return any; +} + ByteString BaseFontNameForType(const CFX_Font* font, int font_type) { ByteString name = font_type == FPDF_FONT_TYPE1 ? font->GetPsName() : font->GetBaseFontName(); @@ -1103,3 +1141,43 @@ FPDFGlyphPath_GetGlyphPathSegment(FPDF_GLYPHPATH glyphpath, int index) { return FPDFPathSegmentFromFXPathPoint(&points[index]); } + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFTextObj_BlankRange(FPDF_PAGEOBJECT text_object, + int start_index, + int count) { + if (!text_object || count <= 0) + return false; + + CPDF_PageObject* obj = CPDFPageObjectFromFPDFPageObject(text_object); + if (!obj || obj->GetType() != CPDF_PageObject::Type::kText) + return false; + + CPDF_TextObject* tobj = obj->AsText(); + const int size = static_cast(tobj->GetCharCodes().size()); + if (start_index < 0 || start_index >= size) + return false; + + std::vector idx; + const int end = std::min(start_index + count, size); + idx.reserve(end - start_index); + for (int i = start_index; i < end; ++i) + idx.push_back(i); + + return BlankIndicesInternal(tobj, idx.data(), + static_cast(idx.size())); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFTextObj_BlankIndices(FPDF_PAGEOBJECT text_object, + const int* indices, + int num_indices) { + if (!text_object || !indices || num_indices <= 0) + return false; + + CPDF_PageObject* obj = CPDFPageObjectFromFPDFPageObject(text_object); + if (!obj || obj->GetType() != CPDF_PageObject::Type::kText) + return false; + + return BlankIndicesInternal(obj->AsText(), indices, num_indices); +} \ No newline at end of file diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h index c6b63f65f3..65825db5a0 100644 --- a/public/fpdf_edit.h +++ b/public/fpdf_edit.h @@ -1650,6 +1650,32 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFFormObj_RemoveObject(FPDF_PAGEOBJECT form_object, FPDF_PAGEOBJECT page_object); +// Experimental EmbedPDF Extension API. +// Blank (redact) a contiguous range of characters in a text object. +// +// text_object - handle to a text object. +// start_index - the index of the first character to blank. +// count - the number of characters to blank. +// +// Returns TRUE on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFTextObj_BlankRange(FPDF_PAGEOBJECT text_object, + int start_index, + int count); + +// Experimental EmbedPDF Extension API. +// Blank (redact) specific character indices in a text object. +// +// text_object - handle to a text object. +// indices - array of character indices to blank. +// num_indices - number of indices to blank. +// +// Returns TRUE on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFTextObj_BlankIndices(FPDF_PAGEOBJECT text_object, + const int* indices, + int num_indices); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus From 8e92fffb2348b48db10ee011bd29cb1b66084303 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sat, 9 Aug 2025 23:56:10 +0300 Subject: [PATCH 2/8] Support for TJ and also add support color space page regeneration --- .../edit/cpdf_pagecontentgenerator.cpp | 190 ++++++++++++++++-- core/fpdfapi/edit/cpdf_pagecontentgenerator.h | 8 + core/fpdfapi/page/cpdf_color.cpp | 19 ++ core/fpdfapi/page/cpdf_color.h | 5 + core/fpdfapi/page/cpdf_colorstate.cpp | 20 +- core/fpdfapi/page/cpdf_colorstate.h | 13 +- core/fpdfapi/page/cpdf_pageobjectholder.cpp | 13 ++ core/fpdfapi/page/cpdf_pageobjectholder.h | 4 + 8 files changed, 246 insertions(+), 26 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp index c422110843..fc2e60c932 100644 --- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp +++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp @@ -30,6 +30,9 @@ #include "core/fpdfapi/page/cpdf_path.h" #include "core/fpdfapi/page/cpdf_pathobject.h" #include "core/fpdfapi/page/cpdf_textobject.h" +#include "core/fpdfapi/page/cpdf_color.h" +#include "core/fpdfapi/page/cpdf_colorspace.h" +#include "core/fpdfapi/page/cpdf_iccprofile.h" #include "core/fpdfapi/parser/cpdf_array.h" #include "core/fpdfapi/parser/cpdf_dictionary.h" #include "core/fpdfapi/parser/cpdf_document.h" @@ -53,27 +56,56 @@ namespace { // - ColorSpace // - Pattern // - Shading -constexpr const char* kResourceKeys[] = {"ExtGState", "Font", "XObject"}; +constexpr const char* kResourceKeys[] = {"ExtGState", "Font", "XObject", "ColorSpace"}; // Key: The resource type. // Value: The resource names of a given type. using ResourcesMap = std::map>; -// Returns whether it wrote to `buf` or not. -bool WriteColorToStream(fxcrt::ostringstream& buf, const CPDF_Color* color) { - if (!color || (!color->IsColorSpaceRGB() && !color->IsColorSpaceGray())) { - return false; +bool TextObjectNeedsTJ(const CPDF_TextObject* obj) { + // We don’t have a public accessor for char_codes_, but we can reuse CountItems() + // and GetItemInfo(i). GetItemInfo() gives us char_code_ and (for vert writing) + // origin_. We only need to see if any item has the sentinel. + for (size_t i = 0, n = obj->CountItems(); i < n; ++i) { + auto it = obj->GetItemInfo(i); + if (it.char_code_ == CPDF_Font::kInvalidCharCode) + return true; } + return false; +} - std::optional> colors = color->GetRGB(); - if (!colors.has_value()) { - return false; +void WriteTextAsTJ(fxcrt::ostringstream& buf, + CPDF_TextObject* obj, + CPDF_Font* font) { + buf << "[ "; + ByteString hexChunk; + + for (size_t i = 0, n = obj->CountItems(); i < n; ++i) { + CPDF_TextObject::Item it = obj->GetItemInfo(i); + + if (it.char_code_ == CPDF_Font::kInvalidCharCode) { + if (!hexChunk.IsEmpty()) { + buf << PDF_HexEncodeString(hexChunk.AsStringView()) << " "; + hexChunk.clear(); + } + float thousandths = 0.0f; + if (obj->GetSeparatorAdjustment(i, &thousandths)) { + // TJ numbers are interpreted as “subtract this from the text position”, + // which matches how CalcPositionDataInternal() used the stored value: + // curpos -= (thousandths * fontSize)/1000. So we emit the value as-is. + WriteFloat(buf, thousandths); + buf << " "; + } + continue; + } + + font->AppendChar(&hexChunk, it.char_code_); } - WriteFloat(buf, colors.value().red) << " "; - WriteFloat(buf, colors.value().green) << " "; - WriteFloat(buf, colors.value().blue); - return true; + if (!hexChunk.IsEmpty()) { + buf << PDF_HexEncodeString(hexChunk.AsStringView()) << " "; + } + buf << "] TJ"; } // Balances the "q" operator ProcessGraphics() emitted. @@ -103,6 +135,11 @@ void RecordPageObjectResourceUsage(const CPDF_PageObject* page_object, CHECK(!name.IsEmpty()); seen_resources["ExtGState"].insert(name); } + const CPDF_ColorState& cs = page_object->color_state(); + if (!cs.GetFillColorSpaceResName().IsEmpty()) + seen_resources["ColorSpace"].insert(cs.GetFillColorSpaceResName()); + if (!cs.GetStrokeColorSpaceResName().IsEmpty()) + seen_resources["ColorSpace"].insert(cs.GetStrokeColorSpaceResName()); } CPDF_PageObjectHolder::RemovedResourceMap RemoveUnusedResources( @@ -510,6 +547,109 @@ void CPDF_PageContentGenerator::UpdateResourcesDict() { obj_holder_->all_removed_resources_map()); } +ByteString CPDF_PageContentGenerator::RealizeColorSpaceObject( + const CPDF_ColorSpace* cs) { + if (!cs) return ByteString(); + + const auto fam = cs->GetFamily(); + if (fam == CPDF_ColorSpace::Family::kDeviceGray || + fam == CPDF_ColorSpace::Family::kDeviceRGB || + fam == CPDF_ColorSpace::Family::kDeviceCMYK) { + return ByteString(); // device spaces don't need a resource + } + + if (fam == CPDF_ColorSpace::Family::kICCBased) { + RetainPtr profile = cs->GetIccProfile(); + if (!profile) return ByteString(); + + RetainPtr acc = profile->GetStreamAcc(); + if (!acc) + return ByteString(); + RetainPtr icc = acc->GetStream(); + if (!icc) + return ByteString(); + + // Stable cache key based on stream objnum + ByteString key = ByteString::Format("ICCB:%u", icc->GetObjNum()); + if (auto hit = obj_holder_->ColorSpaceMapSearch(key)) + return *hit; + + // IMPORTANT: make array indirect + RetainPtr arr = document_->NewIndirect(); + arr->AppendNew("ICCBased"); + arr->AppendNew(document_, icc->GetObjNum()); + + ByteString name = RealizeResource(arr.Get(), "ColorSpace"); + obj_holder_->ColorSpaceMapInsert(key, name); + return name; + } + + // (CalGray/CalRGB/Lab/Separation/DeviceN can be added later) + return ByteString(); +} + +bool CPDF_PageContentGenerator::EmitColor(fxcrt::ostringstream& buf, + const CPDF_Color* color, + bool is_stroke, + CPDF_PageObject* owner) { + if (!color) return false; + + if (color->IsColorSpaceGray()) { + auto comps = color->GetRawNonPatternComps(); + if (comps.size() == 1) { + WriteFloat(buf, comps[0]) << (is_stroke ? " G " : " g "); + // device space → clear any remembered resource name + if (is_stroke) owner->mutable_color_state().SetStrokeColorSpaceResName({}); + else owner->mutable_color_state().SetFillColorSpaceResName({}); + return true; + } + return false; + } + + if (color->IsColorSpaceRGB()) { + auto rgb = color->GetRGB(); + if (!rgb) return false; + WriteFloat(buf, rgb->red) << " "; + WriteFloat(buf, rgb->green) << " "; + WriteFloat(buf, rgb->blue) << (is_stroke ? " RG " : " rg "); + if (is_stroke) owner->mutable_color_state().SetStrokeColorSpaceResName({}); + else owner->mutable_color_state().SetFillColorSpaceResName({}); + return true; + } + + if (color->IsColorSpaceCMYK()) { + auto comps = color->GetRawNonPatternComps(); // expect 4 + if (comps.size() == 4) { + WriteFloat(buf, comps[0]) << " "; + WriteFloat(buf, comps[1]) << " "; + WriteFloat(buf, comps[2]) << " "; + WriteFloat(buf, comps[3]) << (is_stroke ? " K " : " k "); + if (is_stroke) owner->mutable_color_state().SetStrokeColorSpaceResName({}); + else owner->mutable_color_state().SetFillColorSpaceResName({}); + return true; + } + return false; + } + + // Non-device: realize resource + scn/SCN + const CPDF_ColorSpace* cs = color->GetColorSpace(); + ByteString cs_name = RealizeColorSpaceObject(cs); + if (cs_name.IsEmpty()) return false; + + if (is_stroke) owner->mutable_color_state().SetStrokeColorSpaceResName(cs_name); + else owner->mutable_color_state().SetFillColorSpaceResName(cs_name); + + buf << "/" << PDF_NameEncode(cs_name) << (is_stroke ? " CS " : " cs "); + + auto comps = color->GetRawNonPatternComps(); + for (size_t i = 0; i < comps.size(); ++i) { + if (i) buf << " "; + WriteFloat(buf, comps[i]); + } + buf << (is_stroke ? " SCN " : " scn "); + return true; +} + ByteString CPDF_PageContentGenerator::RealizeResource( const CPDF_Object* pResource, ByteStringView type) const { @@ -819,11 +959,11 @@ void CPDF_PageContentGenerator::ProcessPath(fxcrt::ostringstream* buf, void CPDF_PageContentGenerator::ProcessGraphics(fxcrt::ostringstream* buf, CPDF_PageObject* pPageObj) { *buf << "q "; - if (WriteColorToStream(*buf, pPageObj->color_state().GetFillColor())) { - *buf << " rg "; + if (const CPDF_Color* fill = pPageObj->color_state().GetFillColor()) { + EmitColor(*buf, fill, /*is_stroke=*/false, pPageObj); } - if (WriteColorToStream(*buf, pPageObj->color_state().GetStrokeColor())) { - *buf << " RG "; + if (const CPDF_Color* stroke = pPageObj->color_state().GetStrokeColor()) { + EmitColor(*buf, stroke, /*is_stroke=*/true, pPageObj); } float line_width = pPageObj->graph_state().GetLineWidth(); if (line_width != 1.0f) { @@ -909,7 +1049,7 @@ void CPDF_PageContentGenerator::ProcessGraphics(fxcrt::ostringstream* buf, void CPDF_PageContentGenerator::ProcessDefaultGraphics( fxcrt::ostringstream* buf) { - *buf << "0 0 0 RG 0 0 0 rg 1 w " + *buf << "1 w " << static_cast(CFX_GraphStateData::LineCap::kButt) << " J " << static_cast(CFX_GraphStateData::LineJoin::kMiter) << " j\n"; default_graphics_name_ = GetOrCreateDefaultGraphics(); @@ -1000,12 +1140,18 @@ void CPDF_PageContentGenerator::ProcessText(fxcrt::ostringstream* buf, *buf << "/" << PDF_NameEncode(dict_name) << " "; WriteFloat(*buf, pTextObj->GetFontSize()) << " Tf "; *buf << static_cast(pTextObj->GetTextRenderMode()) << " Tr "; - ByteString text; - for (uint32_t charcode : pTextObj->GetCharCodes()) { - if (charcode != CPDF_Font::kInvalidCharCode) { - font->AppendChar(&text, charcode); + + if (TextObjectNeedsTJ(pTextObj)) { + WriteTextAsTJ(*buf, pTextObj, font.Get()); + *buf << " ET"; + } else { + ByteString text; + for (uint32_t charcode : pTextObj->GetCharCodes()) { + if (charcode != CPDF_Font::kInvalidCharCode) + font->AppendChar(&text, charcode); } + *buf << PDF_HexEncodeString(text.AsStringView()) << " Tj ET"; } - *buf << PDF_HexEncodeString(text.AsStringView()) << " Tj ET"; + EndProcessGraphics(*buf); } diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.h b/core/fpdfapi/edit/cpdf_pagecontentgenerator.h index b37d06bb3a..acebd8c9b3 100644 --- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.h +++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.h @@ -26,6 +26,9 @@ class CPDF_PageObjectHolder; class CPDF_Path; class CPDF_PathObject; class CPDF_TextObject; +class CPDF_Color; +class CPDF_ColorSpace; +class CPDF_ColorState; class CPDF_PageContentGenerator { public: @@ -46,7 +49,12 @@ class CPDF_PageContentGenerator { void ProcessGraphics(fxcrt::ostringstream* buf, CPDF_PageObject* pPageObj); void ProcessDefaultGraphics(fxcrt::ostringstream* buf); void ProcessText(fxcrt::ostringstream* buf, CPDF_TextObject* pTextObj); + bool EmitColor(fxcrt::ostringstream& buf, + const CPDF_Color* color, + bool is_stroke, + CPDF_PageObject* owner); ByteString GetOrCreateDefaultGraphics() const; + ByteString RealizeColorSpaceObject(const CPDF_ColorSpace* cs); ByteString RealizeResource(const CPDF_Object* pResource, ByteStringView type) const; const CPDF_ContentMarks* ProcessContentMarks(fxcrt::ostringstream* buf, diff --git a/core/fpdfapi/page/cpdf_color.cpp b/core/fpdfapi/page/cpdf_color.cpp index 7709dfc2ad..86ea0e452a 100644 --- a/core/fpdfapi/page/cpdf_color.cpp +++ b/core/fpdfapi/page/cpdf_color.cpp @@ -100,6 +100,25 @@ bool CPDF_Color::IsColorSpaceGray() const { CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceGray); } +bool CPDF_Color::IsColorSpaceCMYK() const { + if (!cs_) + return false; + return cs_ == + CPDF_ColorSpace::GetStockCS(CPDF_ColorSpace::Family::kDeviceCMYK); +} + +pdfium::span CPDF_Color::GetRawNonPatternComps() const { + // Only non-pattern colors keep a plain float buffer in color_data_. + if (IsPatternInternal()) + return {}; + + if (std::holds_alternative>(color_data_)) { + const auto& buf = std::get>(color_data_); + return pdfium::span(buf.data(), buf.size()); + } + return {}; +} + std::optional CPDF_Color::GetColorRef() const { std::optional> maybe_rgb = GetRGB(); if (!maybe_rgb.has_value()) { diff --git a/core/fpdfapi/page/cpdf_color.h b/core/fpdfapi/page/cpdf_color.h index 69f0226882..383993dbcd 100644 --- a/core/fpdfapi/page/cpdf_color.h +++ b/core/fpdfapi/page/cpdf_color.h @@ -41,12 +41,17 @@ class CPDF_Color { uint32_t ComponentCount() const; bool IsColorSpaceRGB() const; bool IsColorSpaceGray() const; + bool IsColorSpaceCMYK() const; // Wrapper around GetRGB() that returns the RGB value as FX_COLORREF. The // GetRGB() return value is clamped to fit into FX_COLORREF, where the color // components are 8-bit fields within an unsigned integer. std::optional GetColorRef() const; std::optional> GetRGB() const; + pdfium::span GetRawNonPatternComps() const; + + const CPDF_ColorSpace* GetColorSpace() const { return cs_.Get(); } + // Should only be called if IsPattern() returns true. RetainPtr GetPattern() const; diff --git a/core/fpdfapi/page/cpdf_colorstate.cpp b/core/fpdfapi/page/cpdf_colorstate.cpp index c2f36e5883..3d1b078786 100644 --- a/core/fpdfapi/page/cpdf_colorstate.cpp +++ b/core/fpdfapi/page/cpdf_colorstate.cpp @@ -33,7 +33,7 @@ FX_COLORREF CPDF_ColorState::GetFillColorRef() const { } void CPDF_ColorState::SetFillColorRef(FX_COLORREF colorref) { - if (!ref_ || GetFillColorRef() != colorref) { + if (!ref_.GetObject() || GetFillColorRef() != colorref) { ref_.GetPrivateCopy()->fill_color_ref_ = colorref; } } @@ -43,7 +43,7 @@ FX_COLORREF CPDF_ColorState::GetStrokeColorRef() const { } void CPDF_ColorState::SetStrokeColorRef(FX_COLORREF colorref) { - if (!ref_ || GetStrokeColorRef() != colorref) { + if (!ref_.GetObject() || GetStrokeColorRef() != colorref) { ref_.GetPrivateCopy()->stroke_color_ref_ = colorref; } } @@ -166,3 +166,19 @@ RetainPtr CPDF_ColorState::ColorData::Clone() const { return pdfium::MakeRetain(*this); } + +const ByteString& CPDF_ColorState::GetFillColorSpaceResName() const { + return ref_.GetObject()->fill_colorspace_res_name_; +} + +const ByteString& CPDF_ColorState::GetStrokeColorSpaceResName() const { + return ref_.GetObject()->stroke_colorspace_res_name_; +} + +void CPDF_ColorState::SetFillColorSpaceResName(ByteString name) { + ref_.GetPrivateCopy()->fill_colorspace_res_name_ = std::move(name); +} + +void CPDF_ColorState::SetStrokeColorSpaceResName(ByteString name) { + ref_.GetPrivateCopy()->stroke_colorspace_res_name_ = std::move(name); +} \ No newline at end of file diff --git a/core/fpdfapi/page/cpdf_colorstate.h b/core/fpdfapi/page/cpdf_colorstate.h index 42b42a743e..76b0c18c98 100644 --- a/core/fpdfapi/page/cpdf_colorstate.h +++ b/core/fpdfapi/page/cpdf_colorstate.h @@ -11,6 +11,7 @@ #include #include "core/fpdfapi/page/cpdf_color.h" +#include "core/fxcrt/bytestring.h" #include "core/fxcrt/retain_ptr.h" #include "core/fxcrt/shared_copy_on_write.h" #include "core/fxcrt/span.h" @@ -51,10 +52,15 @@ class CPDF_ColorState { void SetStrokePattern(RetainPtr pattern, pdfium::span values); - bool HasRef() const { return !!ref_; } + const ByteString& GetFillColorSpaceResName() const; + const ByteString& GetStrokeColorSpaceResName() const; + void SetFillColorSpaceResName(ByteString name); + void SetStrokeColorSpaceResName(ByteString name); + + bool HasRef() const { return ref_.GetObject() != nullptr; } private: - class ColorData final : public Retainable { + class ColorData final : public fxcrt::Retainable { public: CONSTRUCT_VIA_MAKE_RETAIN; @@ -67,6 +73,9 @@ class CPDF_ColorState { CPDF_Color fill_color_; CPDF_Color stroke_color_; + ByteString fill_colorspace_res_name_; + ByteString stroke_colorspace_res_name_; + private: ColorData(); ColorData(const ColorData& src); diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.cpp b/core/fpdfapi/page/cpdf_pageobjectholder.cpp index f80e1bf066..53c614a1b4 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.cpp +++ b/core/fpdfapi/page/cpdf_pageobjectholder.cpp @@ -120,6 +120,19 @@ void CPDF_PageObjectHolder::FontsMapInsert(const FontData& fd, fonts_map_[fd] = str; } +std::optional CPDF_PageObjectHolder::ColorSpaceMapSearch( + const ByteString& key) { + auto it = colorspace_map_.find(key); + if (it == colorspace_map_.end()) + return std::nullopt; + return it->second; +} + +void CPDF_PageObjectHolder::ColorSpaceMapInsert(const ByteString& key, + const ByteString& name) { + colorspace_map_[key] = name; +} + CFX_Matrix CPDF_PageObjectHolder::GetCTMAtBeginningOfStream(int32_t stream) { CHECK(stream >= 0 || stream == CPDF_PageObject::kNoContentStream); diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.h b/core/fpdfapi/page/cpdf_pageobjectholder.h index 9bbd2b4579..3161288a55 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.h +++ b/core/fpdfapi/page/cpdf_pageobjectholder.h @@ -136,6 +136,9 @@ class CPDF_PageObjectHolder { std::optional FontsMapSearch(const FontData& fd); void FontsMapInsert(const FontData& fd, const ByteString& str); + std::optional ColorSpaceMapSearch(const ByteString& key); + void ColorSpaceMapInsert(const ByteString& key, const ByteString& name); + // `stream` must be non-negative or `CPDF_PageObject::kNoContentStream`. CFX_Matrix GetCTMAtBeginningOfStream(int32_t stream); @@ -153,6 +156,7 @@ class CPDF_PageObjectHolder { RetainPtr resources_; std::map graphics_map_; std::map fonts_map_; + std::map colorspace_map_; CFX_FloatRect bbox_; CPDF_Transparency transparency_; From 9ab1461d7983f322cade39ced90f81ee53b2345b Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 Aug 2025 00:43:18 +0300 Subject: [PATCH 3/8] Add GetSeperatorAdjustment --- core/fpdfapi/page/cpdf_textobject.cpp | 16 ++++++++++++++++ core/fpdfapi/page/cpdf_textobject.h | 2 ++ 2 files changed, 18 insertions(+) diff --git a/core/fpdfapi/page/cpdf_textobject.cpp b/core/fpdfapi/page/cpdf_textobject.cpp index 624b21e8eb..eeff1d7bf1 100644 --- a/core/fpdfapi/page/cpdf_textobject.cpp +++ b/core/fpdfapi/page/cpdf_textobject.cpp @@ -355,3 +355,19 @@ float CPDF_TextObject::CalcPositionDataInternal( return curpos; } + +bool CPDF_TextObject::GetSeparatorAdjustment(size_t index, + float* out_thousandths) const { + DCHECK(out_thousandths); + if (index >= char_codes_.size()) + return false; + if (char_codes_[index] != CPDF_Font::kInvalidCharCode) + return false; + if (index == 0) + return false; // there’s no preceding glyph + // By contract of SetSegments()/CalcPositionDataInternal(): + // - char_pos_[k] holds the original kerning value (thousandths) + // for the preceding real glyph when char_codes_[k+1] is Invalid. + *out_thousandths = char_pos_[index - 1]; + return true; +} diff --git a/core/fpdfapi/page/cpdf_textobject.h b/core/fpdfapi/page/cpdf_textobject.h index 2415b76066..6df0c0bffa 100644 --- a/core/fpdfapi/page/cpdf_textobject.h +++ b/core/fpdfapi/page/cpdf_textobject.h @@ -74,6 +74,8 @@ class CPDF_TextObject final : public CPDF_PageObject { void SetSegments(pdfium::span strings, pdfium::span kernings); + bool GetSeparatorAdjustment(size_t index, float* out_thousandths) const; + CFX_PointF CalcPositionData(float horz_scale); private: From 7611671efe8fafdf8ea85df77ed879bb50d46a9e Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 Aug 2025 01:06:46 +0300 Subject: [PATCH 4/8] Revert "Ability to redact text" This reverts commit 84c6071e5c03c1de24776b8564293c75a9a22f38. --- fpdfsdk/fpdf_edittext.cpp | 78 --------------------------------------- public/fpdf_edit.h | 26 ------------- 2 files changed, 104 deletions(-) diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp index 033dd6d43e..c08db236e0 100644 --- a/fpdfsdk/fpdf_edittext.cpp +++ b/fpdfsdk/fpdf_edittext.cpp @@ -85,44 +85,6 @@ namespace { constexpr uint32_t kMaxBfCharBfRangeEntries = 100; -static std::vector& MutCodes(CPDF_TextObject* tobj) { - return const_cast&>(tobj->GetCharCodes()); -} - -bool BlankOne(CPDF_TextObject* tobj, int i) { - auto& codes = MutCodes(tobj); - if (i < 0 || i >= static_cast(codes.size())) - return false; - - // Null charcode => nothing drawn; positions/spacing unchanged. - codes[i] = 0; - // We don’t touch Unicode here; charcode 0 won’t extract anyway. - return true; -} - -bool BlankIndicesInternal(CPDF_TextObject* tobj, - const int* indices, - int n) { - if (!tobj || !indices || n <= 0) - return false; - - std::vector idx(indices, indices + n); - std::sort(idx.begin(), idx.end()); - idx.erase(std::unique(idx.begin(), idx.end()), idx.end()); - - const int size = static_cast(tobj->GetCharCodes().size()); - bool any = false; - for (int i : idx) { - if (i >= 0 && i < size) - any |= BlankOne(tobj, i); - } - - if (any) - tobj->SetDirty(true); - - return any; -} - ByteString BaseFontNameForType(const CFX_Font* font, int font_type) { ByteString name = font_type == FPDF_FONT_TYPE1 ? font->GetPsName() : font->GetBaseFontName(); @@ -1141,43 +1103,3 @@ FPDFGlyphPath_GetGlyphPathSegment(FPDF_GLYPHPATH glyphpath, int index) { return FPDFPathSegmentFromFXPathPoint(&points[index]); } - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFTextObj_BlankRange(FPDF_PAGEOBJECT text_object, - int start_index, - int count) { - if (!text_object || count <= 0) - return false; - - CPDF_PageObject* obj = CPDFPageObjectFromFPDFPageObject(text_object); - if (!obj || obj->GetType() != CPDF_PageObject::Type::kText) - return false; - - CPDF_TextObject* tobj = obj->AsText(); - const int size = static_cast(tobj->GetCharCodes().size()); - if (start_index < 0 || start_index >= size) - return false; - - std::vector idx; - const int end = std::min(start_index + count, size); - idx.reserve(end - start_index); - for (int i = start_index; i < end; ++i) - idx.push_back(i); - - return BlankIndicesInternal(tobj, idx.data(), - static_cast(idx.size())); -} - -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFTextObj_BlankIndices(FPDF_PAGEOBJECT text_object, - const int* indices, - int num_indices) { - if (!text_object || !indices || num_indices <= 0) - return false; - - CPDF_PageObject* obj = CPDFPageObjectFromFPDFPageObject(text_object); - if (!obj || obj->GetType() != CPDF_PageObject::Type::kText) - return false; - - return BlankIndicesInternal(obj->AsText(), indices, num_indices); -} \ No newline at end of file diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h index 65825db5a0..c6b63f65f3 100644 --- a/public/fpdf_edit.h +++ b/public/fpdf_edit.h @@ -1650,32 +1650,6 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFFormObj_RemoveObject(FPDF_PAGEOBJECT form_object, FPDF_PAGEOBJECT page_object); -// Experimental EmbedPDF Extension API. -// Blank (redact) a contiguous range of characters in a text object. -// -// text_object - handle to a text object. -// start_index - the index of the first character to blank. -// count - the number of characters to blank. -// -// Returns TRUE on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFTextObj_BlankRange(FPDF_PAGEOBJECT text_object, - int start_index, - int count); - -// Experimental EmbedPDF Extension API. -// Blank (redact) specific character indices in a text object. -// -// text_object - handle to a text object. -// indices - array of character indices to blank. -// num_indices - number of indices to blank. -// -// Returns TRUE on success. -FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFTextObj_BlankIndices(FPDF_PAGEOBJECT text_object, - const int* indices, - int num_indices); - #ifdef __cplusplus } // extern "C" #endif // __cplusplus From 54c4469f6a3fc5562685e59028c7eb42238228e9 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 Aug 2025 02:53:51 +0300 Subject: [PATCH 5/8] Add text redaction --- core/fpdfapi/edit/BUILD.gn | 2 + core/fpdfapi/edit/cpdf_text_redactor.cpp | 271 +++++++++++++++++++++++ core/fpdfapi/edit/cpdf_text_redactor.h | 27 +++ core/fpdfapi/page/cpdf_textobject.cpp | 8 + core/fpdfapi/page/cpdf_textobject.h | 2 + fpdfsdk/fpdf_edittext.cpp | 38 ++++ public/fpdf_edit.h | 29 +++ 7 files changed, 377 insertions(+) create mode 100644 core/fpdfapi/edit/cpdf_text_redactor.cpp create mode 100644 core/fpdfapi/edit/cpdf_text_redactor.h diff --git a/core/fpdfapi/edit/BUILD.gn b/core/fpdfapi/edit/BUILD.gn index 38db4eb4b3..65ced169f2 100644 --- a/core/fpdfapi/edit/BUILD.gn +++ b/core/fpdfapi/edit/BUILD.gn @@ -21,6 +21,8 @@ source_set("edit") { "cpdf_pageorganizer.h", "cpdf_stringarchivestream.cpp", "cpdf_stringarchivestream.h", + "cpdf_text_redactor.cpp", + "cpdf_text_redactor.h", ] configs += [ "../../../:pdfium_strict_config", diff --git a/core/fpdfapi/edit/cpdf_text_redactor.cpp b/core/fpdfapi/edit/cpdf_text_redactor.cpp new file mode 100644 index 0000000000..0c744f52da --- /dev/null +++ b/core/fpdfapi/edit/cpdf_text_redactor.cpp @@ -0,0 +1,271 @@ +// Copyright 2025 +// Use of this source code is governed by a BSD-style license. + +#include "core/fpdfapi/edit/cpdf_text_redactor.h" + +#include +#include + +#include "core/fpdfapi/edit/cpdf_pagecontentgenerator.h" +#include "core/fpdfapi/font/cpdf_cidfont.h" +#include "core/fpdfapi/font/cpdf_font.h" +#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/page/cpdf_pageobjectholder.h" +#include "core/fpdfapi/page/cpdf_textobject.h" +#include "core/fxcrt/check.h" + +namespace { + +inline bool Intersects(const CFX_FloatRect& a, const CFX_FloatRect& b) { + return a.right > b.left && a.left < b.right && + a.top > b.bottom && a.bottom < b.top; +} + +inline bool IntersectsAny(const CFX_FloatRect& box, + pdfium::span rects) { + for (const auto& r : rects) { + if (Intersects(box, r)) + return true; + } + return false; +} + +// Compute a glyph's bbox in PAGE USER SPACE. +// +// Important: CPDF_TextObject::GetItemInfo() already adjusts `origin_` for +// vertical writing (includes the vertical origin shift), so we do NOT apply +// that offset again here. +CFX_FloatRect GlyphBBoxInPage(const CPDF_TextObject* to, + CPDF_Font* font, + uint32_t code, + const CPDF_TextObject::Item& it, + const CFX_Matrix& parent_to_page) { + // Glyph bbox in font units. + FX_RECT r_font_units = font->GetCharBBox(code); + + const float fs = to->GetFontSize(); + // Scale from 1/1000 em to user units. + CFX_FloatRect glyph_box( + r_font_units.left * fs / 1000.0f, r_font_units.bottom * fs / 1000.0f, + r_font_units.right * fs / 1000.0f, r_font_units.top * fs / 1000.0f); + + // Position within the text object’s local space. + glyph_box.left += it.origin_.x; + glyph_box.right += it.origin_.x; + glyph_box.bottom += it.origin_.y; + glyph_box.top += it.origin_.y; + + // Text matrix to page space (for this text object). + const CFX_Matrix tm = to->GetTextMatrix(); + glyph_box = tm.TransformRect(glyph_box); + + // Parent transform (e.g., Form placement) to page space. + return parent_to_page.TransformRect(glyph_box); +} + +// Advance in thousandths for a single code, matching CalcPositionDataInternal(). +// - Width portion is already thousandths. +// - char/word spaces are user-space, convert to thousandths via 1000/fs. +float AdvanceThousandths(const CPDF_TextObject* to, + CPDF_Font* font, + uint32_t code) { + float w_th = 0.0f; + if (const CPDF_CIDFont* cid = font->AsCIDFont(); cid && cid->IsVertWriting()) { + const uint16_t c = cid->CIDFromCharCode(code); + w_th = static_cast(cid->GetVertWidth(c)); + } else { + w_th = static_cast(font->GetCharWidthF(code)); + } + + const float fs = to->GetFontSize(); + // Word space applies to ASCII space in non-vertical, non-special cases. + if (code == ' ') { + const CPDF_CIDFont* cid = font->AsCIDFont(); + if (!cid || cid->GetCharSize(' ') == 1) + w_th += to->GetWordSpace() * 1000.0f / fs; + } + w_th += to->GetCharSpace() * 1000.0f / fs; + return w_th; +} + +enum class RedactOutcome { kUnchanged, kModified, kRemovedAll }; + +// Rewrites `to` so glyphs intersecting ANY rect in `page_rects` are dropped +// and spacing is preserved via TJ. Returns outcome. +RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, + pdfium::span page_rects, + const CFX_Matrix& parent_to_page) { + CPDF_Font* font = to->GetFont(); + if (!font) + return RedactOutcome::kUnchanged; + + ByteString run; // current kept hex run + std::vector strings; // segments for SetSegments() + std::vector kernings; // thousandths between segments + float pending_tj = 0.0f; // accumulated removal + original TJ + bool any_kept = false; + bool any_removed = false; + + const size_t n = to->CountItems(); + for (size_t i = 0; i < n; ++i) { + CPDF_TextObject::Item it = to->GetItemInfo(i); + + if (it.char_code_ == CPDF_Font::kInvalidCharCode) { + float original_adj = 0.0f; + if (to->GetSeparatorAdjustment(i, &original_adj)) { + // Merge original TJ into the pending pool. It will be emitted (with + // sign preserved) when we flush the next kept run. + pending_tj += original_adj; + } + continue; + } + + const CFX_FloatRect gbox = + GlyphBBoxInPage(to, font, it.char_code_, it, parent_to_page); + const bool hit = IntersectsAny(gbox, page_rects); + + if (hit) { + any_removed = true; + pending_tj -= AdvanceThousandths(to, font, it.char_code_); + continue; + } + + // Keep this glyph. + if (!run.IsEmpty() && pending_tj != 0.0f) { + strings.push_back(run); + kernings.push_back(pending_tj); + run.clear(); + pending_tj = 0.0f; + } else if (run.IsEmpty() && pending_tj != 0.0f) { + // We have removal/TJ before the first kept glyph: create an empty segment + // so we can attach the kerning in between segments. + strings.emplace_back(ByteString()); + kernings.push_back(pending_tj); + pending_tj = 0.0f; + } + + font->AppendChar(&run, it.char_code_); + any_kept = true; + } + + if (!run.IsEmpty()) + strings.push_back(run); + + if (!any_kept) + return any_removed ? RedactOutcome::kRemovedAll : RedactOutcome::kUnchanged; + + // `kernings.size()` must be exactly `strings.size() - 1`. + CHECK(kernings.size() + 1 == strings.size()); + + // Rebuild the text object. + to->SetSegments(pdfium::span(strings), pdfium::span(kernings)); + to->SetDirty(true); + CFX_Matrix tm = to->GetTextMatrix(); + to->SetTextMatrix(tm); + return any_removed ? RedactOutcome::kModified : RedactOutcome::kUnchanged; +} + +// Redact all text objects inside a holder (page or form). If `recurse_forms` is +// true, also descends into nested form XObjects using their placement matrices. +// +// `to_page` is the transform from holder-local space to PAGE USER SPACE. +// +// Returns true if anything changed in this holder. +bool RedactHolder(CPDF_PageObjectHolder* holder, + pdfium::span page_rects, + const CFX_Matrix& to_page, + bool recurse_forms) { + bool changed = false; + std::vector to_remove; + + for (auto it = holder->begin(); it != holder->end(); ++it) { + CPDF_PageObject* po = it->get(); + if (!po->IsActive()) + continue; + + if (CPDF_TextObject* to = po->AsText()) { + const RedactOutcome out = RedactTextObjectMulti(to, page_rects, to_page); + if (out == RedactOutcome::kRemovedAll) { + to_remove.push_back(po); + changed = true; + } else if (out == RedactOutcome::kModified) { + changed = true; + } + continue; + } + + if (recurse_forms) { + if (CPDF_FormObject* fo = po->AsForm()) { + CPDF_Form* form = fo->form(); + if (!form) + continue; + + // Placement matrix (object space -> parent space). + const CFX_Matrix placement = fo->form_matrix(); + const CFX_Matrix next_to_page = to_page * placement; + + // Recurse into the form’s own holder space. + const bool form_changed = + RedactHolder(form, page_rects, next_to_page, /*recurse_forms=*/true); + + if (form_changed) { + // Regenerate the form XObject stream immediately so changes are + // persisted and visible to the page; there's no public API to do + // this later from the embedder. + CPDF_PageContentGenerator form_gen(form); + form_gen.GenerateContent(); + changed = true; + } + } + } + } + + // Physically remove any fully-emptied text objects. + if (!to_remove.empty()) { + for (CPDF_PageObject* obj : to_remove) { + std::unique_ptr unused = holder->RemovePageObject(obj); + (void)unused; + } + changed = true; + } + + return changed; +} + +} // namespace + +bool RedactTextInRect(CPDF_Page* page, + const CFX_FloatRect& page_space_rect_in, + bool recurse_forms) { + if (!page) + return false; + + CFX_FloatRect r = page_space_rect_in; + r.Normalize(); + const CFX_Matrix identity; + + const CFX_FloatRect rects[] = {r}; + return RedactHolder(page, pdfium::span(rects), identity, recurse_forms); +} + +bool RedactTextInRects(CPDF_Page* page, + pdfium::span page_space_rects_in, + bool recurse_forms) { + if (!page || page_space_rects_in.empty()) + return false; + + // Normalize copies. + std::vector rects; + rects.reserve(page_space_rects_in.size()); + for (const auto& rr : page_space_rects_in) { + CFX_FloatRect r = rr; + r.Normalize(); + rects.push_back(r); + } + + const CFX_Matrix identity; + return RedactHolder(page, pdfium::span(rects), identity, recurse_forms); +} \ No newline at end of file diff --git a/core/fpdfapi/edit/cpdf_text_redactor.h b/core/fpdfapi/edit/cpdf_text_redactor.h new file mode 100644 index 0000000000..fe6ea607f0 --- /dev/null +++ b/core/fpdfapi/edit/cpdf_text_redactor.h @@ -0,0 +1,27 @@ +// Copyright 2025 +// Use of this source code is governed by a BSD-style license. + +#ifndef CORE_FPDFAPI_EDIT_CPDF_TEXT_REDACTOR_H_ +#define CORE_FPDFAPI_EDIT_CPDF_TEXT_REDACTOR_H_ + +#include "core/fxcrt/span.h" +#include "core/fxcrt/fx_coordinates.h" + +class CPDF_Page; + +// Redacts (removes) glyphs from text objects that intersect the given rect(s). +// Inputs are in PAGE USER SPACE (same space as highlights). +// If `recurse_forms` is true, contents of Form XObjects used on the page +// are also scanned and redacted. Edits inside a form regenerate that form’s +// content stream immediately. The page stream is NOT regenerated here. +// +// Returns true if anything changed. +bool RedactTextInRect(CPDF_Page* page, + const CFX_FloatRect& page_space_rect, + bool recurse_forms); + +bool RedactTextInRects(CPDF_Page* page, + pdfium::span page_space_rects, + bool recurse_forms); + +#endif // CORE_FPDFAPI_EDIT_CPDF_TEXT_REDACTOR_H_ \ No newline at end of file diff --git a/core/fpdfapi/page/cpdf_textobject.cpp b/core/fpdfapi/page/cpdf_textobject.cpp index eeff1d7bf1..2b305c8039 100644 --- a/core/fpdfapi/page/cpdf_textobject.cpp +++ b/core/fpdfapi/page/cpdf_textobject.cpp @@ -262,6 +262,14 @@ float CPDF_TextObject::GetFontSize() const { return text_state().GetFontSize(); } +float CPDF_TextObject::GetCharSpace() const { + return text_state().GetCharSpace(); +} + +float CPDF_TextObject::GetWordSpace() const { + return text_state().GetWordSpace(); +} + TextRenderingMode CPDF_TextObject::GetTextRenderMode() const { return text_state().GetTextMode(); } diff --git a/core/fpdfapi/page/cpdf_textobject.h b/core/fpdfapi/page/cpdf_textobject.h index 6df0c0bffa..fe26c52a31 100644 --- a/core/fpdfapi/page/cpdf_textobject.h +++ b/core/fpdfapi/page/cpdf_textobject.h @@ -58,6 +58,8 @@ class CPDF_TextObject final : public CPDF_PageObject { RetainPtr GetFont() const; float GetFontSize() const; + float GetCharSpace() const; + float GetWordSpace() const; TextRenderingMode GetTextRenderMode() const; void SetTextRenderMode(TextRenderingMode mode); diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp index c08db236e0..5c68ca8cee 100644 --- a/fpdfsdk/fpdf_edittext.cpp +++ b/fpdfsdk/fpdf_edittext.cpp @@ -12,6 +12,7 @@ #include "core/fpdfapi/font/cpdf_cidfont.h" #include "core/fpdfapi/font/cpdf_font.h" +#include "core/fpdfapi/edit/cpdf_text_redactor.h" #include "core/fpdfapi/page/cpdf_docpagedata.h" #include "core/fpdfapi/page/cpdf_textobject.h" #include "core/fpdfapi/page/cpdf_textstate.h" @@ -85,6 +86,15 @@ namespace { constexpr uint32_t kMaxBfCharBfRangeEntries = 100; +// Turn a quad into its axis-aligned bounding box in page space. +CFX_FloatRect BBoxOfQuad(const FS_QUADPOINTSF& q) { + const float l = std::min(std::min(q.x1, q.x2), std::min(q.x3, q.x4)); + const float r = std::max(std::max(q.x1, q.x2), std::max(q.x3, q.x4)); + const float b = std::min(std::min(q.y1, q.y2), std::min(q.y3, q.y4)); + const float t = std::max(std::max(q.y1, q.y2), std::max(q.y3, q.y4)); + return CFX_FloatRect(l, b, r, t); +} + ByteString BaseFontNameForType(const CFX_Font* font, int font_type) { ByteString name = font_type == FPDF_FONT_TYPE1 ? font->GetPsName() : font->GetBaseFontName(); @@ -1103,3 +1113,31 @@ FPDFGlyphPath_GetGlyphPathSegment(FPDF_GLYPHPATH glyphpath, int index) { return FPDFPathSegmentFromFXPathPoint(&points[index]); } + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFText_RedactInRect(FPDF_PAGE page, const FS_RECTF* rect, FPDF_BOOL recurse) { + if (!page || !rect) + return false; + + CPDF_Page* p = CPDFPageFromFPDFPage(page); + const CFX_FloatRect r = CFXFloatRectFromFSRectF(*rect); + return RedactTextInRect(p, r, !!recurse); +} + +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFText_RedactInQuads(FPDF_PAGE page, + const FS_QUADPOINTSF* quads, + size_t count, + FPDF_BOOL recurse) { + if (!page || (count && !quads)) + return false; + + CPDF_Page* p = CPDFPageFromFPDFPage(page); + + std::vector rects; + rects.reserve(count); + for (size_t i = 0; i < count; ++i) + rects.push_back(BBoxOfQuad(quads[i])); + + return RedactTextInRects(p, pdfium::span(rects), !!recurse); +} \ No newline at end of file diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h index c6b63f65f3..65ad15ba65 100644 --- a/public/fpdf_edit.h +++ b/public/fpdf_edit.h @@ -1650,6 +1650,35 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV FPDFFormObj_RemoveObject(FPDF_PAGEOBJECT form_object, FPDF_PAGEOBJECT page_object); +// Experimental API. +// +// Redact text in a given rectangle on a page. +// +// page - handle to a page. +// rect - handle to a rectangle. +// recurse_forms - whether to recurse into form objects. +// +// Returns TRUE on success. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFText_RedactInRect(FPDF_PAGE page, + const FS_RECTF* rect, + FPDF_BOOL recurse_forms); + +// Experimental API. +// +// Redact text in a given quads on a page. +// +// page - handle to a page. +// quads - handle to a quads. +// count - the number of quads. +// recurse_forms - whether to recurse into form objects. +// +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFText_RedactInQuads(FPDF_PAGE page, + const FS_QUADPOINTSF* quads, + size_t count, + FPDF_BOOL recurse_forms); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus From 83951847cdd1aecb4e84b0287f56ccfe4adc4afe Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Sun, 10 Aug 2025 23:55:03 +0300 Subject: [PATCH 6/8] Deal properly with the shifting the beginning of the sentence --- core/fpdfapi/edit/cpdf_text_redactor.cpp | 25 +++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_text_redactor.cpp b/core/fpdfapi/edit/cpdf_text_redactor.cpp index 0c744f52da..8f53d2003b 100644 --- a/core/fpdfapi/edit/cpdf_text_redactor.cpp +++ b/core/fpdfapi/edit/cpdf_text_redactor.cpp @@ -116,8 +116,7 @@ RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, if (it.char_code_ == CPDF_Font::kInvalidCharCode) { float original_adj = 0.0f; if (to->GetSeparatorAdjustment(i, &original_adj)) { - // Merge original TJ into the pending pool. It will be emitted (with - // sign preserved) when we flush the next kept run. + // Merge original TJ into the pending pool; sign preserved. pending_tj += original_adj; } continue; @@ -129,22 +128,30 @@ RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, if (hit) { any_removed = true; + // Remove glyph advance from the pen position (thousandths). pending_tj -= AdvanceThousandths(to, font, it.char_code_); continue; } // Keep this glyph. + if (run.IsEmpty() && pending_tj != 0.0f) { + // We have removal/TJ before the first kept glyph: shift the text matrix + // instead of emitting a leading TJ number (which can't move the run's origin). + CFX_Matrix tm = to->GetTextMatrix(); + const float fs = to->GetFontSize(); + const float du = -pending_tj * (fs / 1000.0f); // user-units along baseline + tm.e += du * tm.a; // shift along text X axis in user space + tm.f += du * tm.b; // (handles rotated text) + to->SetTextMatrix(tm); + pending_tj = 0.0f; + } + if (!run.IsEmpty() && pending_tj != 0.0f) { + // Between kept runs: flush run & attach the TJ adjustment. strings.push_back(run); kernings.push_back(pending_tj); run.clear(); pending_tj = 0.0f; - } else if (run.IsEmpty() && pending_tj != 0.0f) { - // We have removal/TJ before the first kept glyph: create an empty segment - // so we can attach the kerning in between segments. - strings.emplace_back(ByteString()); - kernings.push_back(pending_tj); - pending_tj = 0.0f; } font->AppendChar(&run, it.char_code_); @@ -164,7 +171,7 @@ RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, to->SetSegments(pdfium::span(strings), pdfium::span(kernings)); to->SetDirty(true); CFX_Matrix tm = to->GetTextMatrix(); - to->SetTextMatrix(tm); + to->SetTextMatrix(tm); return any_removed ? RedactOutcome::kModified : RedactOutcome::kUnchanged; } From 1e031f42c77208adbd791b4ad167812d111ead10 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 12 Aug 2025 16:39:31 +0300 Subject: [PATCH 7/8] Finish redaction --- .../edit/cpdf_pagecontentgenerator.cpp | 105 +++- core/fpdfapi/edit/cpdf_text_redactor.cpp | 515 +++++++++++++++--- core/fpdfapi/edit/cpdf_text_redactor.h | 8 +- core/fpdfapi/page/cpdf_image.cpp | 71 +++ core/fpdfapi/page/cpdf_image.h | 6 + core/fpdfapi/page/cpdf_pageobjectholder.cpp | 13 + core/fpdfapi/page/cpdf_pageobjectholder.h | 4 + fpdfsdk/fpdf_edittext.cpp | 10 +- public/fpdf_edit.h | 6 +- 9 files changed, 621 insertions(+), 117 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp index fc2e60c932..83ada3d39b 100644 --- a/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp +++ b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp @@ -394,6 +394,25 @@ CPDF_PageContentGenerator::GenerateModifiedStreams() { all_dirty_streams.insert(marked_dirty_streams.begin(), marked_dirty_streams.end()); + // --- embedpdf: if anything is dirty, regenerate *all* page content streams. + // Rationale: CTM / graphics-state handoff between streams means rewriting + // only a subset can leave the concatenated effect inconsistent. + if (!all_dirty_streams.empty()) { + int32_t last_index = -1; + if (RetainPtr contents = + obj_holder_->GetDict()->GetObjectFor(pdfium::page_object::kContents)) { + if (const CPDF_Array* arr = contents->AsArray()) { + last_index = static_cast(arr->size()) - 1; + } else if (contents->IsStream()) { + last_index = 0; + } + } + for (int32_t i = 0; i <= last_index; ++i) { + all_dirty_streams.insert(i); + } + } + // --- end embedpdf + // Start regenerating dirty streams. std::map streams; std::set empty_streams; @@ -1088,39 +1107,69 @@ void CPDF_PageContentGenerator::ProcessText(fxcrt::ostringstream* buf, ProcessGraphics(buf, pTextObj); *buf << "BT "; - const CFX_Matrix& matrix = pTextObj->GetTextMatrix(); - if (!matrix.IsIdentity()) { - WriteMatrix(*buf, matrix) << " Tm "; + // Separate translation (cm) from pure text matrix (Tm) + const CFX_Matrix& M = pTextObj->GetTextMatrix(); + if (M.e != 0 || M.f != 0) { + WriteMatrix(*buf, CFX_Matrix(1, 0, 0, 1, M.e, M.f)) << " cm "; } + CFX_Matrix TmNoTranslate(M.a, M.b, M.c, M.d, 0, 0); + if (!TmNoTranslate.IsIdentity()) { + WriteMatrix(*buf, TmNoTranslate) << " Tm "; + } else { + *buf << "1 0 0 1 0 0 Tm "; + } + + // Ensure we have a font. RetainPtr font(pTextObj->GetFont()); if (!font) { font = CPDF_Font::GetStockFont(document_, "Helvetica"); } - FontData data; - const CPDF_FontEncoding* pEncoding = nullptr; - if (font->IsType1Font()) { - data.type = "Type1"; - pEncoding = font->AsType1Font()->GetEncoding(); - } else if (font->IsTrueTypeFont()) { - data.type = "TrueType"; - pEncoding = font->AsTrueTypeFont()->GetEncoding(); - } else if (font->IsCIDFont()) { - data.type = "Type0"; - } else { - return; + // --- Object-number keyed font resource binding --- + // Get the font dictionary; if it's inline, make it indirect so it has a stable objnum. + RetainPtr pFontDict = font->GetFontDict(); + if (pFontDict && pFontDict->IsInline()) { + RetainPtr clone = pFontDict->Clone(); + document_->AddIndirectObject(clone); + pFontDict = std::move(clone); } - data.baseFont = font->GetBaseFontName(); + + // Some (very old/odd) fonts may not expose a dict; fall back safely. + uint32_t font_objnum = pFontDict ? pFontDict->GetObjNum() : 0; ByteString dict_name; - std::optional maybe_name = obj_holder_->FontsMapSearch(data); - if (maybe_name.has_value()) { - dict_name = std::move(maybe_name.value()); + if (font_objnum) { + if (auto hit = obj_holder_->FontsByObjnumSearch(font_objnum)) { + dict_name = *hit; + } else { + // Realize this exact font object into Resources/Font and cache by objnum. + dict_name = RealizeResource(pFontDict.Get(), "Font"); + obj_holder_->FontsByObjnumInsert(font_objnum, dict_name); + } } else { - RetainPtr pIndirectFont = font->GetFontDict(); - if (pIndirectFont->IsInline()) { - // In this case we assume it must be a standard font + // Last-resort path (should be rare): name by (type, base name) like before. + FontData data; + const CPDF_FontEncoding* pEncoding = nullptr; + if (font->IsType1Font()) { + data.type = "Type1"; + pEncoding = font->AsType1Font()->GetEncoding(); + } else if (font->IsTrueTypeFont()) { + data.type = "TrueType"; + pEncoding = font->AsTrueTypeFont()->GetEncoding(); + } else if (font->IsCIDFont()) { + data.type = "Type0"; + } else { + *buf << "ET"; // bail out cleanly + EndProcessGraphics(*buf); + return; + } + data.baseFont = font->GetBaseFontName(); + + if (auto hit = obj_holder_->FontsMapSearch(data)) { + dict_name = *hit; + } else { + // Build a minimal indirect font dict (same as your old code). auto font_dict = pdfium::MakeRetain(); font_dict->SetNewFor("Type", "Font"); font_dict->SetNewFor("Subtype", data.type); @@ -1130,17 +1179,23 @@ void CPDF_PageContentGenerator::ProcessText(fxcrt::ostringstream* buf, pEncoding->Realize(document_->GetByteStringPool())); } document_->AddIndirectObject(font_dict); - pIndirectFont = std::move(font_dict); + dict_name = RealizeResource(std::move(font_dict), "Font"); + obj_holder_->FontsMapInsert(data, dict_name); } - dict_name = RealizeResource(std::move(pIndirectFont), "Font"); - obj_holder_->FontsMapInsert(data, dict_name); } + pTextObj->SetResourceName(dict_name); *buf << "/" << PDF_NameEncode(dict_name) << " "; WriteFloat(*buf, pTextObj->GetFontSize()) << " Tf "; *buf << static_cast(pTextObj->GetTextRenderMode()) << " Tr "; + const float tc = pTextObj->GetCharSpace(); + const float tw = pTextObj->GetWordSpace(); + + if (tc != 0.0f) WriteFloat(*buf, tc) << " Tc "; + if (tw != 0.0f) WriteFloat(*buf, tw) << " Tw "; + if (TextObjectNeedsTJ(pTextObj)) { WriteTextAsTJ(*buf, pTextObj, font.Get()); *buf << " ET"; diff --git a/core/fpdfapi/edit/cpdf_text_redactor.cpp b/core/fpdfapi/edit/cpdf_text_redactor.cpp index 8f53d2003b..d5ef4933f8 100644 --- a/core/fpdfapi/edit/cpdf_text_redactor.cpp +++ b/core/fpdfapi/edit/cpdf_text_redactor.cpp @@ -3,10 +3,15 @@ #include "core/fpdfapi/edit/cpdf_text_redactor.h" +#include +#include #include #include +#include +#include "core/fpdfapi/edit/cpdf_contentstream_write_utils.h" #include "core/fpdfapi/edit/cpdf_pagecontentgenerator.h" +#include "core/fpdfapi/edit/cpdf_pagecontentmanager.h" #include "core/fpdfapi/font/cpdf_cidfont.h" #include "core/fpdfapi/font/cpdf_font.h" #include "core/fpdfapi/page/cpdf_form.h" @@ -15,13 +20,44 @@ #include "core/fpdfapi/page/cpdf_pageobject.h" #include "core/fpdfapi/page/cpdf_pageobjectholder.h" #include "core/fpdfapi/page/cpdf_textobject.h" +#include "core/fpdfapi/page/cpdf_pathobject.h" +#include "core/fpdfapi/page/cpdf_image.h" +#include "core/fpdfapi/page/cpdf_imageobject.h" +#include "core/fpdfapi/parser/cpdf_document.h" +#include "core/fpdfapi/parser/cpdf_name.h" +#include "core/fpdfapi/parser/cpdf_number.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_reference.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "core/fxge/dib/cfx_dibitmap.h" +#include "core/fxge/dib/fx_dib.h" #include "core/fxcrt/check.h" +#include "core/fxcrt/span.h" namespace { +static void AddBlackOverlayPaths(CPDF_Page* page, + pdfium::span rects_page_space) { + if (!page || rects_page_space.empty()) + return; + + for (const auto& r : rects_page_space) { + auto po = std::make_unique(); + po->set_stroke(false); + po->set_filltype(CFX_FillRenderOptions::FillType::kWinding); + po->path().AppendFloatRect(r); // left/bottom/right/top in PAGE USER SPACE + po->SetPathMatrix(CFX_Matrix()); // identity + po->CalcBoundingBox(); + po->SetDirty(true); + page->AppendPageObject(std::move(po)); // appended last => paints on top + } +} + +enum class RedactOutcome { kUnchanged, kModified, kRemovedAll }; + inline bool Intersects(const CFX_FloatRect& a, const CFX_FloatRect& b) { - return a.right > b.left && a.left < b.right && - a.top > b.bottom && a.bottom < b.top; + return a.right > b.left && a.left < b.right && a.top > b.bottom && + a.bottom < b.top; } inline bool IntersectsAny(const CFX_FloatRect& box, @@ -35,44 +71,39 @@ inline bool IntersectsAny(const CFX_FloatRect& box, // Compute a glyph's bbox in PAGE USER SPACE. // -// Important: CPDF_TextObject::GetItemInfo() already adjusts `origin_` for -// vertical writing (includes the vertical origin shift), so we do NOT apply -// that offset again here. +// Note: CPDF_TextObject::GetItemInfo() origin_ is already adjusted for vertical +// writing, so we do not apply any extra vertical origin shift here. CFX_FloatRect GlyphBBoxInPage(const CPDF_TextObject* to, CPDF_Font* font, uint32_t code, const CPDF_TextObject::Item& it, const CFX_Matrix& parent_to_page) { - // Glyph bbox in font units. FX_RECT r_font_units = font->GetCharBBox(code); - const float fs = to->GetFontSize(); - // Scale from 1/1000 em to user units. + CFX_FloatRect glyph_box( - r_font_units.left * fs / 1000.0f, r_font_units.bottom * fs / 1000.0f, - r_font_units.right * fs / 1000.0f, r_font_units.top * fs / 1000.0f); + r_font_units.left * fs / 1000.0f, r_font_units.bottom * fs / 1000.0f, + r_font_units.right * fs / 1000.0f, r_font_units.top * fs / 1000.0f); - // Position within the text object’s local space. - glyph_box.left += it.origin_.x; - glyph_box.right += it.origin_.x; + // Position inside the text object’s local space. + glyph_box.left += it.origin_.x; + glyph_box.right += it.origin_.x; glyph_box.bottom += it.origin_.y; - glyph_box.top += it.origin_.y; + glyph_box.top += it.origin_.y; - // Text matrix to page space (for this text object). + // Text matrix to page space (for this text object), then parent to page. const CFX_Matrix tm = to->GetTextMatrix(); glyph_box = tm.TransformRect(glyph_box); - - // Parent transform (e.g., Form placement) to page space. return parent_to_page.TransformRect(glyph_box); } -// Advance in thousandths for a single code, matching CalcPositionDataInternal(). -// - Width portion is already thousandths. -// - char/word spaces are user-space, convert to thousandths via 1000/fs. +// Advance in thousandths for a single code, matching how PDFium applies widths +// and char/word spacing during layout. float AdvanceThousandths(const CPDF_TextObject* to, CPDF_Font* font, uint32_t code) { float w_th = 0.0f; + if (const CPDF_CIDFont* cid = font->AsCIDFont(); cid && cid->IsVertWriting()) { const uint16_t c = cid->CIDFromCharCode(code); w_th = static_cast(cid->GetVertWidth(c)); @@ -81,20 +112,68 @@ float AdvanceThousandths(const CPDF_TextObject* to, } const float fs = to->GetFontSize(); - // Word space applies to ASCII space in non-vertical, non-special cases. + + // Apply word space only for ASCII space in typical (non-special) cases. if (code == ' ') { const CPDF_CIDFont* cid = font->AsCIDFont(); if (!cid || cid->GetCharSize(' ') == 1) w_th += to->GetWordSpace() * 1000.0f / fs; } + + // Always apply char space. w_th += to->GetCharSpace() * 1000.0f / fs; return w_th; } -enum class RedactOutcome { kUnchanged, kModified, kRemovedAll }; +// Round to nearest integer thousandth for stable TJ outputs. +inline int32_t RoundThousandths(float v) { + return v >= 0 ? static_cast(v + 0.5f) + : static_cast(v - 0.5f); +} + +// Small deadband to tame float fuzz when synthesizing TJ from origins. +constexpr float kTJDeadband = 0.25f; // thousandths + +// State for building a TJ array from kept glyph runs. +struct RedactionState { + CPDF_Font* font = nullptr; + + // Output buffers for SetSegments(): strings[i] followed by kernings[i] between + // strings[i] and strings[i+1]. + std::vector strings; + std::vector kernings; + + // Accumulates original file TJ numbers and removal advances between kept runs. + float kerning_accumulator = 0.0f; + bool has_explicit_kerning = false; + + // For synthesized kerning using origins when no explicit TJ exists. + CFX_PointF prev_glyph_origin{}; + uint32_t prev_glyph_code = 0; + + void ResetBetweenRuns() { + kerning_accumulator = 0.0f; + has_explicit_kerning = false; + } + + void AppendKeptGlyph(const CPDF_TextObject::Item& item) { + DCHECK(font); + DCHECK(!strings.empty()); + font->AppendChar(&strings.back(), item.char_code_); + prev_glyph_origin = item.origin_; + prev_glyph_code = item.char_code_; + } +}; + +// Push a kerning (integer thousandths) and open a new (initially empty) run. +void FlushSegment(RedactionState* s, float kerning_mth) { + const int32_t rounded = RoundThousandths(kerning_mth); + if (rounded == 0) + return; + s->kernings.push_back(static_cast(rounded)); + s->strings.push_back(ByteString()); // next glyphs will fill this +} -// Rewrites `to` so glyphs intersecting ANY rect in `page_rects` are dropped -// and spacing is preserved via TJ. Returns outcome. RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, pdfium::span page_rects, const CFX_Matrix& parent_to_page) { @@ -102,89 +181,340 @@ RedactOutcome RedactTextObjectMulti(CPDF_TextObject* to, if (!font) return RedactOutcome::kUnchanged; - ByteString run; // current kept hex run - std::vector strings; // segments for SetSegments() - std::vector kernings; // thousandths between segments - float pending_tj = 0.0f; // accumulated removal + original TJ + const CPDF_CIDFont* cid = font->AsCIDFont(); + const bool is_vert = cid && cid->IsVertWriting(); + const float fs = to->GetFontSize(); + bool any_kept = false; bool any_removed = false; + RedactionState st; + st.font = font; + st.strings.push_back(ByteString()); // start first run + const size_t n = to->CountItems(); for (size_t i = 0; i < n; ++i) { - CPDF_TextObject::Item it = to->GetItemInfo(i); + const CPDF_TextObject::Item it = to->GetItemInfo(i); + // Original file kerning separator inside TJ. if (it.char_code_ == CPDF_Font::kInvalidCharCode) { - float original_adj = 0.0f; - if (to->GetSeparatorAdjustment(i, &original_adj)) { - // Merge original TJ into the pending pool; sign preserved. - pending_tj += original_adj; + float adj = 0.0f; + if (to->GetSeparatorAdjustment(i, &adj)) { + st.kerning_accumulator += adj; // keep sign; PDF TJ semantics + st.has_explicit_kerning = true; } continue; } + // Decide keep/remove by intersection. const CFX_FloatRect gbox = GlyphBBoxInPage(to, font, it.char_code_, it, parent_to_page); const bool hit = IntersectsAny(gbox, page_rects); if (hit) { + // Merge the removed glyph's advance into the pending kerning pool. + st.kerning_accumulator -= AdvanceThousandths(to, font, it.char_code_); any_removed = true; - // Remove glyph advance from the pen position (thousandths). - pending_tj -= AdvanceThousandths(to, font, it.char_code_); continue; } - // Keep this glyph. - if (run.IsEmpty() && pending_tj != 0.0f) { - // We have removal/TJ before the first kept glyph: shift the text matrix - // instead of emitting a leading TJ number (which can't move the run's origin). - CFX_Matrix tm = to->GetTextMatrix(); - const float fs = to->GetFontSize(); - const float du = -pending_tj * (fs / 1000.0f); // user-units along baseline - tm.e += du * tm.a; // shift along text X axis in user space - tm.f += du * tm.b; // (handles rotated text) - to->SetTextMatrix(tm); - pending_tj = 0.0f; - } + // First kept glyph in the object. + if (!any_kept) { + float leading_offset_user = 0.0f; + + if (st.kerning_accumulator != 0.0f) { + // Remove pre-run spacing by shifting the text matrix (TJ cannot lead). + leading_offset_user = -st.kerning_accumulator * fs / 1000.0f; + st.kerning_accumulator = 0.0f; + st.has_explicit_kerning = false; + } else { + // If no pending spacing, align the run's origin to the first kept glyph. + leading_offset_user = is_vert ? it.origin_.y : it.origin_.x; + } - if (!run.IsEmpty() && pending_tj != 0.0f) { - // Between kept runs: flush run & attach the TJ adjustment. - strings.push_back(run); - kernings.push_back(pending_tj); - run.clear(); - pending_tj = 0.0f; + if (leading_offset_user != 0.0f) { + CFX_Matrix tm = to->GetTextMatrix(); + // Move along the text X axis in user space (handles rotation). + tm.e += leading_offset_user * tm.a; + tm.f += leading_offset_user * tm.b; + to->SetTextMatrix(tm); + } + } else { + // Between kept runs: emit an inter-run kerning. + if (st.has_explicit_kerning) { + float k = st.kerning_accumulator; + if (std::fabs(k) < kTJDeadband) + k = 0.0f; + FlushSegment(&st, k); + } else { + // Infer kerning from origins of consecutive kept glyphs. + const float delta_user = is_vert + ? (it.origin_.y - st.prev_glyph_origin.y) + : (it.origin_.x - st.prev_glyph_origin.x); + const float delta_mth = delta_user * 1000.0f / fs; + const float nominal_advance_mth = + AdvanceThousandths(to, font, st.prev_glyph_code); + float kerning_mth = nominal_advance_mth - delta_mth; + if (std::fabs(kerning_mth) < kTJDeadband) + kerning_mth = 0.0f; + FlushSegment(&st, kerning_mth); + } } - font->AppendChar(&run, it.char_code_); + // Keep this glyph. + st.AppendKeptGlyph(it); + st.ResetBetweenRuns(); any_kept = true; } - if (!run.IsEmpty()) - strings.push_back(run); - if (!any_kept) return any_removed ? RedactOutcome::kRemovedAll : RedactOutcome::kUnchanged; - // `kernings.size()` must be exactly `strings.size() - 1`. - CHECK(kernings.size() + 1 == strings.size()); + // If the last operation opened a new (empty) run by flushing a kerning, + // drop the dangling run and its paired kerning so we keep the invariant + // kernings.size() == strings.size() - 1. + if (!st.strings.empty() && st.strings.back().IsEmpty()) { + st.strings.pop_back(); + if (!st.kernings.empty()) + st.kernings.pop_back(); + } + + CHECK(st.kernings.size() + 1 == st.strings.size()); - // Rebuild the text object. - to->SetSegments(pdfium::span(strings), pdfium::span(kernings)); + to->SetSegments(pdfium::span(st.strings), pdfium::span(st.kernings)); to->SetDirty(true); + // Re-assert Tm to ensure downstream writers notice a change even when the + // numeric value is identical after float ops. CFX_Matrix tm = to->GetTextMatrix(); to->SetTextMatrix(tm); + return any_removed ? RedactOutcome::kModified : RedactOutcome::kUnchanged; } +// Map page-space rects into the image's sample grid (image-local). +static void PageRectsToImageGrid(const CFX_Matrix& image_to_page, + int img_w, int img_h, + pdfium::span page_rects, + std::vector* out_image_rects) { + out_image_rects->clear(); + if (img_w <= 0 || img_h <= 0 || page_rects.empty()) + return; + + // Step 1: page -> unit image space + const CFX_Matrix page_to_unit = image_to_page.GetInverse(); + + out_image_rects->reserve(page_rects.size()); + for (const auto& pr : page_rects) { + // Page -> unit + CFX_FloatRect ur = page_to_unit.TransformRect(pr); + ur.Normalize(); + + // Step 2: unit -> pixel + CFX_FloatRect ir(ur.left * img_w, + ur.bottom * img_h, + ur.right * img_w, + ur.top * img_h); + ir.Normalize(); + + // Clamp to pixel bounds + ir.left = std::clamp(ir.left, 0.0f, static_cast(img_w)); + ir.right = std::clamp(ir.right, 0.0f, static_cast(img_w)); + ir.bottom = std::clamp(ir.bottom, 0.0f, static_cast(img_h)); + ir.top = std::clamp(ir.top, 0.0f, static_cast(img_h)); + + if (ir.right > ir.left && ir.top > ir.bottom) + out_image_rects->push_back(ir); + } +} + +// Returns true if the image stream was overwritten. +// Returns true if the image stream was overwritten. +// Returns true if the image stream was overwritten. +static bool RedactImageObject(CPDF_Page* page, + CPDF_ImageObject* iobj, + pdfium::span page_rects, + const CFX_Matrix& parent_to_page, + bool fill_black) { + if (!iobj) return false; + CPDF_Image* image = iobj->GetImage(); + if (!image) return false; + + CPDF_Document* doc = page->GetDocument(); + const int W = image->GetPixelWidth(); + const int H = image->GetPixelHeight(); + if (W <= 0 || H <= 0) return false; + + const CFX_Matrix img_to_page = parent_to_page * iobj->matrix(); + + const CFX_FloatRect img_bbox_page = + img_to_page.TransformRect(CFX_FloatRect(0, 0, 1.0f, 1.0f)); + bool touches = false; + for (const auto& r : page_rects) { + if (img_bbox_page.right > r.left && img_bbox_page.left < r.right && + img_bbox_page.top > r.bottom && img_bbox_page.bottom < r.top) { + touches = true; + break; + } + } + if (!touches) return false; + + RetainPtr dib = image->LoadDIBBase(); + if (!dib) return false; + + const int bpp = dib->GetBPP(); + const bool has_alpha = dib->IsAlphaFormat(); + const bool is_gray8 = (bpp == 8) && !dib->IsMaskFormat(); + const bool is_rgb24 = (bpp == 24); + const bool is_bgra32 = (bpp == 32) && has_alpha; + const bool is_bgrx32 = (bpp == 32) && !has_alpha; + + if (!(is_gray8 || is_rgb24 || is_bgra32 || is_bgrx32)) { + return false; + } + + RetainPtr orig_smask_stream; + if (image->GetStream()) { + RetainPtr idict = image->GetStream()->GetDict(); + if (idict) { + RetainPtr smask_obj = idict->GetDirectObjectFor("SMask"); + if (smask_obj && smask_obj->AsStream()) { + orig_smask_stream = pdfium::WrapRetain(smask_obj->AsStream()); + } + } + } + + std::vector img_rects; + PageRectsToImageGrid(img_to_page, W, H, page_rects, &img_rects); + if (img_rects.empty()) return false; + + struct IRect { int x0, y0, x1, y1; }; + std::vector boxes; + boxes.reserve(img_rects.size()); + for (const auto& r : img_rects) { + IRect b; + b.x0 = std::max(0, std::min(W, static_cast(std::floor(r.left)))); + b.x1 = std::max(0, std::min(W, static_cast(std::ceil(r.right)))); + b.y0 = std::max(0, std::min(H, static_cast(std::floor(r.bottom)))); + b.y1 = std::max(0, std::min(H, static_cast(std::ceil(r.top)))); + if (b.x1 > b.x0 && b.y1 > b.y0) + boxes.push_back(b); + } + if (boxes.empty()) return false; + + const uint8_t fill_val = fill_black ? 0x00 : 0xFF; + DataVector out_rgb(static_cast(W) * H * 3); + DataVector out_a; // Alpha channel buffer. + + const bool process_alpha = is_bgra32 || !!orig_smask_stream; + + if (process_alpha) { + out_a.resize(static_cast(W) * H); + if (orig_smask_stream && !is_bgra32) { + auto acc = pdfium::MakeRetain(orig_smask_stream); + acc->LoadAllDataFiltered(); + pdfium::span span = acc->GetSpan(); + if (span.size() >= out_a.size()) { + memcpy(out_a.data(), span.data(), out_a.size()); + } + } + } + + size_t total_redacted_px = 0; + + for (int row_top = 0; row_top < H; ++row_top) { + const int y_img = H - 1 - row_top; + const pdfium::span sline = dib->GetScanline(row_top); + uint8_t* drow_rgb = out_rgb.data() + static_cast(row_top) * W * 3; + + for (int x = 0; x < W; ++x) { + const bool red = IntersectsAny({static_cast(x), static_cast(y_img), + static_cast(x + 1), static_cast(y_img + 1)}, + img_rects); // FIXED: Removed pdfium::make_span + + if (red) { + total_redacted_px++; + } + + if (red) { + drow_rgb[x * 3] = fill_val; + drow_rgb[x * 3 + 1] = fill_val; + drow_rgb[x * 3 + 2] = fill_val; + } else { + if (is_gray8) { + drow_rgb[x * 3] = drow_rgb[x * 3 + 1] = drow_rgb[x * 3 + 2] = sline[x]; + } else if (is_rgb24) { + drow_rgb[x * 3] = sline[x * 3 + 2]; + drow_rgb[x * 3 + 1] = sline[x * 3 + 1]; + drow_rgb[x * 3 + 2] = sline[x * 3]; + } else { // bgrx32 or bgra32 + drow_rgb[x * 3] = sline[x * 4 + 2]; + drow_rgb[x * 3 + 1] = sline[x * 4 + 1]; + drow_rgb[x * 3 + 2] = sline[x * 4]; + } + } + + if (is_bgra32) { + out_a[static_cast(row_top) * W + x] = sline[x * 4 + 3]; + } + } + } + + if (total_redacted_px == 0) return false; + + if (process_alpha) { + for (const auto& box : boxes) { + for (int y = box.y0; y < box.y1; ++y) { + int row_top = H - 1 - y; + uint8_t* row_ptr = out_a.data() + static_cast(row_top) * W; + std::fill(row_ptr + box.x0, row_ptr + box.x1, 255); + } + } + } + + RetainPtr ndict = doc->New(); + ndict->SetNewFor("Type", "XObject"); + ndict->SetNewFor("Subtype", "Image"); + ndict->SetNewFor("Width", W); + ndict->SetNewFor("Height", H); + ndict->SetNewFor("ColorSpace", "DeviceRGB"); + ndict->SetNewFor("BitsPerComponent", 8); + + if (process_alpha) { + RetainPtr smask_dict = doc->New(); + smask_dict->SetNewFor("Type", "XObject"); + smask_dict->SetNewFor("Subtype", "Image"); + smask_dict->SetNewFor("Width", W); + smask_dict->SetNewFor("Height", H); + smask_dict->SetNewFor("ColorSpace", "DeviceGray"); + smask_dict->SetNewFor("BitsPerComponent", 8); + + auto smask_stream = + pdfium::MakeRetain(std::move(out_a), std::move(smask_dict)); + const uint32_t smask_obj_num = doc->AddIndirectObject(smask_stream); + auto smask_ref = pdfium::MakeRetain(doc, smask_obj_num); + ndict->SetFor("SMask", std::move(smask_ref)); + } + + const bool ok = image->OverwriteStreamInPlace( + std::move(out_rgb), std::move(ndict), true); + if (ok) { + image->ResetCache(page); + page->ClearRenderContext(); + iobj->SetDirty(true); + } + return ok; +} + // Redact all text objects inside a holder (page or form). If `recurse_forms` is -// true, also descends into nested form XObjects using their placement matrices. +// true, also descends into nested Form XObjects via their placement matrices. // -// `to_page` is the transform from holder-local space to PAGE USER SPACE. -// -// Returns true if anything changed in this holder. -bool RedactHolder(CPDF_PageObjectHolder* holder, +// `to_page` transforms holder-local space to PAGE USER SPACE. +bool RedactHolder(CPDF_Page* page_for_cache, + CPDF_PageObjectHolder* holder, pdfium::span page_rects, const CFX_Matrix& to_page, - bool recurse_forms) { + bool recurse_forms, + bool fill_black) { bool changed = false; std::vector to_remove; @@ -204,24 +534,26 @@ bool RedactHolder(CPDF_PageObjectHolder* holder, continue; } + if (CPDF_ImageObject* io = po->AsImage()) { + if (RedactImageObject(page_for_cache, io, page_rects, to_page, fill_black)) { + changed = true; + } + continue; + } + if (recurse_forms) { if (CPDF_FormObject* fo = po->AsForm()) { CPDF_Form* form = fo->form(); if (!form) continue; - // Placement matrix (object space -> parent space). - const CFX_Matrix placement = fo->form_matrix(); + const CFX_Matrix placement = fo->form_matrix(); // object -> parent const CFX_Matrix next_to_page = to_page * placement; - // Recurse into the form’s own holder space. const bool form_changed = - RedactHolder(form, page_rects, next_to_page, /*recurse_forms=*/true); + RedactHolder(page_for_cache, form, page_rects, next_to_page, /*recurse_forms=*/true, fill_black); if (form_changed) { - // Regenerate the form XObject stream immediately so changes are - // persisted and visible to the page; there's no public API to do - // this later from the embedder. CPDF_PageContentGenerator form_gen(form); form_gen.GenerateContent(); changed = true; @@ -230,12 +562,10 @@ bool RedactHolder(CPDF_PageObjectHolder* holder, } } - // Physically remove any fully-emptied text objects. + // Physically remove fully emptied text objects. if (!to_remove.empty()) { - for (CPDF_PageObject* obj : to_remove) { - std::unique_ptr unused = holder->RemovePageObject(obj); - (void)unused; - } + for (CPDF_PageObject* obj : to_remove) + holder->RemovePageObject(obj); changed = true; } @@ -246,7 +576,8 @@ bool RedactHolder(CPDF_PageObjectHolder* holder, bool RedactTextInRect(CPDF_Page* page, const CFX_FloatRect& page_space_rect_in, - bool recurse_forms) { + bool recurse_forms, + bool draw_black_boxes) { if (!page) return false; @@ -255,12 +586,22 @@ bool RedactTextInRect(CPDF_Page* page, const CFX_Matrix identity; const CFX_FloatRect rects[] = {r}; - return RedactHolder(page, pdfium::span(rects), identity, recurse_forms); + const bool changed = + RedactHolder(page, page, pdfium::span(rects), identity, recurse_forms, + /*fill_black=*/draw_black_boxes); + + if (draw_black_boxes) { + AddBlackOverlayPaths(page, pdfium::span(rects)); // paint on top + } + + // Adding a stream is a change; reflect that. + return changed || draw_black_boxes; } bool RedactTextInRects(CPDF_Page* page, pdfium::span page_space_rects_in, - bool recurse_forms) { + bool recurse_forms, + bool draw_black_boxes) { if (!page || page_space_rects_in.empty()) return false; @@ -274,5 +615,13 @@ bool RedactTextInRects(CPDF_Page* page, } const CFX_Matrix identity; - return RedactHolder(page, pdfium::span(rects), identity, recurse_forms); + const bool changed = + RedactHolder(page, page, pdfium::span(rects), identity, recurse_forms, + /*fill_black=*/draw_black_boxes); + + if (draw_black_boxes) { + AddBlackOverlayPaths(page, pdfium::span(rects)); // paint on top + } + + return changed || draw_black_boxes; } \ No newline at end of file diff --git a/core/fpdfapi/edit/cpdf_text_redactor.h b/core/fpdfapi/edit/cpdf_text_redactor.h index fe6ea607f0..26084a0d2e 100644 --- a/core/fpdfapi/edit/cpdf_text_redactor.h +++ b/core/fpdfapi/edit/cpdf_text_redactor.h @@ -18,10 +18,12 @@ class CPDF_Page; // Returns true if anything changed. bool RedactTextInRect(CPDF_Page* page, const CFX_FloatRect& page_space_rect, - bool recurse_forms); - + bool recurse_forms, + bool draw_black_boxes); + bool RedactTextInRects(CPDF_Page* page, pdfium::span page_space_rects, - bool recurse_forms); + bool recurse_forms, + bool draw_black_boxes); #endif // CORE_FPDFAPI_EDIT_CPDF_TEXT_REDACTOR_H_ \ No newline at end of file diff --git a/core/fpdfapi/page/cpdf_image.cpp b/core/fpdfapi/page/cpdf_image.cpp index 7aacac192f..6214aca321 100644 --- a/core/fpdfapi/page/cpdf_image.cpp +++ b/core/fpdfapi/page/cpdf_image.cpp @@ -38,6 +38,76 @@ #include "core/fxge/dib/cfx_dibitmap.h" #include "core/fxge/dib/fx_dib.h" +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; + } + +} // 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()) + ConvertStreamToIndirectObject(); + + RetainPtr s_const = GetStream(); + if (!s_const) + return false; + + // Get a mutable stream by objnum. + RetainPtr s = + ToStream(document_->GetMutableIndirectObject(s_const->GetObjNum())); + if (!s) + return false; + + 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(); + } + return ok; +} + // static bool CPDF_Image::IsValidJpegComponent(int32_t comps) { return comps == 1 || comps == 3 || comps == 4; @@ -182,6 +252,7 @@ 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(); diff --git a/core/fpdfapi/page/cpdf_image.h b/core/fpdfapi/page/cpdf_image.h index 4327b8601e..5c1bf802c9 100644 --- a/core/fpdfapi/page/cpdf_image.h +++ b/core/fpdfapi/page/cpdf_image.h @@ -13,6 +13,7 @@ #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; @@ -23,6 +24,7 @@ class CPDF_Page; class CPDF_Stream; class PauseIndicatorIface; class IFX_SeekableReadStream; +class CPDF_Dictionary; class CPDF_Image final : public Retainable { public: @@ -55,6 +57,10 @@ class CPDF_Image final : public Retainable { void SetJpegImage(RetainPtr pFile); void SetJpegImageInline(RetainPtr pFile); + bool OverwriteStreamInPlace(DataVector new_data, + RetainPtr new_dict, + bool data_is_decoded); + void ResetCache(CPDF_Page* pPage); void WillBeDestroyed(); diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.cpp b/core/fpdfapi/page/cpdf_pageobjectholder.cpp index 53c614a1b4..7d29570731 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.cpp +++ b/core/fpdfapi/page/cpdf_pageobjectholder.cpp @@ -120,6 +120,19 @@ void CPDF_PageObjectHolder::FontsMapInsert(const FontData& fd, fonts_map_[fd] = str; } +std::optional CPDF_PageObjectHolder::FontsByObjnumSearch(uint32_t objnum) { + if (!objnum) + return std::nullopt; + auto it = fonts_by_objnum_.find(objnum); + return it == fonts_by_objnum_.end() ? std::nullopt : std::optional(it->second); +} + +void CPDF_PageObjectHolder::FontsByObjnumInsert(uint32_t objnum, const ByteString& name) { + if (!objnum) + return; + fonts_by_objnum_[objnum] = name; +} + std::optional CPDF_PageObjectHolder::ColorSpaceMapSearch( const ByteString& key) { auto it = colorspace_map_.find(key); diff --git a/core/fpdfapi/page/cpdf_pageobjectholder.h b/core/fpdfapi/page/cpdf_pageobjectholder.h index 3161288a55..ba522f6227 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.h +++ b/core/fpdfapi/page/cpdf_pageobjectholder.h @@ -136,6 +136,9 @@ class CPDF_PageObjectHolder { std::optional FontsMapSearch(const FontData& fd); void FontsMapInsert(const FontData& fd, const ByteString& str); + std::optional FontsByObjnumSearch(uint32_t objnum); + void FontsByObjnumInsert(uint32_t objnum, const ByteString& name); + std::optional ColorSpaceMapSearch(const ByteString& key); void ColorSpaceMapInsert(const ByteString& key, const ByteString& name); @@ -156,6 +159,7 @@ class CPDF_PageObjectHolder { RetainPtr resources_; std::map graphics_map_; std::map fonts_map_; + std::map fonts_by_objnum_; std::map colorspace_map_; CFX_FloatRect bbox_; CPDF_Transparency transparency_; diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp index 5c68ca8cee..3833675d00 100644 --- a/fpdfsdk/fpdf_edittext.cpp +++ b/fpdfsdk/fpdf_edittext.cpp @@ -1115,20 +1115,22 @@ FPDFGlyphPath_GetGlyphPathSegment(FPDF_GLYPHPATH glyphpath, int index) { } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV -EPDFText_RedactInRect(FPDF_PAGE page, const FS_RECTF* rect, FPDF_BOOL recurse) { +EPDFText_RedactInRect(FPDF_PAGE page, const FS_RECTF* rect, FPDF_BOOL recurse, FPDF_BOOL draw_black_boxes) { if (!page || !rect) return false; CPDF_Page* p = CPDFPageFromFPDFPage(page); const CFX_FloatRect r = CFXFloatRectFromFSRectF(*rect); - return RedactTextInRect(p, r, !!recurse); + return RedactTextInRect(p, r, !!recurse, !!draw_black_boxes); } FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFText_RedactInQuads(FPDF_PAGE page, const FS_QUADPOINTSF* quads, size_t count, - FPDF_BOOL recurse) { + FPDF_BOOL recurse, + FPDF_BOOL draw_black_boxes + ) { if (!page || (count && !quads)) return false; @@ -1139,5 +1141,5 @@ EPDFText_RedactInQuads(FPDF_PAGE page, for (size_t i = 0; i < count; ++i) rects.push_back(BBoxOfQuad(quads[i])); - return RedactTextInRects(p, pdfium::span(rects), !!recurse); + return RedactTextInRects(p, pdfium::span(rects), !!recurse, !!draw_black_boxes); } \ No newline at end of file diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h index 65ad15ba65..9a9dc0626b 100644 --- a/public/fpdf_edit.h +++ b/public/fpdf_edit.h @@ -1662,7 +1662,8 @@ FPDFFormObj_RemoveObject(FPDF_PAGEOBJECT form_object, FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFText_RedactInRect(FPDF_PAGE page, const FS_RECTF* rect, - FPDF_BOOL recurse_forms); + FPDF_BOOL recurse_forms, + FPDF_BOOL draw_black_boxes); // Experimental API. // @@ -1677,7 +1678,8 @@ FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFText_RedactInQuads(FPDF_PAGE page, const FS_QUADPOINTSF* quads, size_t count, - FPDF_BOOL recurse_forms); + FPDF_BOOL recurse_forms, + FPDF_BOOL draw_black_boxes); #ifdef __cplusplus } // extern "C" From 11b96eb90a5e37681d6a4a361cd7176e86648f50 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 12 Aug 2025 17:03:27 +0300 Subject: [PATCH 8/8] Remove object if it is completely covering the redacted area --- core/fpdfapi/edit/cpdf_text_redactor.cpp | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/core/fpdfapi/edit/cpdf_text_redactor.cpp b/core/fpdfapi/edit/cpdf_text_redactor.cpp index d5ef4933f8..556468e5aa 100644 --- a/core/fpdfapi/edit/cpdf_text_redactor.cpp +++ b/core/fpdfapi/edit/cpdf_text_redactor.cpp @@ -509,6 +509,7 @@ static bool RedactImageObject(CPDF_Page* page, // true, also descends into nested Form XObjects via their placement matrices. // // `to_page` transforms holder-local space to PAGE USER SPACE. +// Redact all page objects inside a holder (page or form). bool RedactHolder(CPDF_Page* page_for_cache, CPDF_PageObjectHolder* holder, pdfium::span page_rects, @@ -541,17 +542,36 @@ bool RedactHolder(CPDF_Page* page_for_cache, continue; } + if (CPDF_PathObject* path = po->AsPath()) { + // Get the path's bounding box and transform it to page coordinates. + CFX_Matrix total_transform = to_page * path->matrix(); + CFX_FloatRect path_bbox_page = total_transform.TransformRect(path->path().GetBoundingBox()); + path_bbox_page.Normalize(); + + // Check if the path's bounding box is completely inside any redaction rect. + for (const auto& redact_rect : page_rects) { + if (path_bbox_page.left >= redact_rect.left && + path_bbox_page.right <= redact_rect.right && + path_bbox_page.bottom >= redact_rect.bottom && + path_bbox_page.top <= redact_rect.top) { + + to_remove.push_back(path); + changed = true; + break; + } + } + continue; + } + if (recurse_forms) { if (CPDF_FormObject* fo = po->AsForm()) { CPDF_Form* form = fo->form(); if (!form) continue; - const CFX_Matrix placement = fo->form_matrix(); // object -> parent + const CFX_Matrix placement = fo->form_matrix(); const CFX_Matrix next_to_page = to_page * placement; - - const bool form_changed = - RedactHolder(page_for_cache, form, page_rects, next_to_page, /*recurse_forms=*/true, fill_black); + const bool form_changed = RedactHolder(page_for_cache, form, page_rects, next_to_page, true, fill_black); if (form_changed) { CPDF_PageContentGenerator form_gen(form); @@ -562,10 +582,11 @@ bool RedactHolder(CPDF_Page* page_for_cache, } } - // Physically remove fully emptied text objects. + // Physically remove fully emptied text and path objects. if (!to_remove.empty()) { - for (CPDF_PageObject* obj : to_remove) + for (CPDF_PageObject* obj : to_remove) { holder->RemovePageObject(obj); + } changed = true; }