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_pagecontentgenerator.cpp b/core/fpdfapi/edit/cpdf_pagecontentgenerator.cpp index c422110843..83ada3d39b 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( @@ -357,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; @@ -510,6 +566,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 +978,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 +1068,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(); @@ -948,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); @@ -990,22 +1179,34 @@ 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 "; - ByteString text; - for (uint32_t charcode : pTextObj->GetCharCodes()) { - if (charcode != CPDF_Font::kInvalidCharCode) { - font->AppendChar(&text, charcode); + + 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"; + } 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/edit/cpdf_text_redactor.cpp b/core/fpdfapi/edit/cpdf_text_redactor.cpp new file mode 100644 index 0000000000..556468e5aa --- /dev/null +++ b/core/fpdfapi/edit/cpdf_text_redactor.cpp @@ -0,0 +1,648 @@ +// 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 +#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" +#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/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; +} + +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. +// +// 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) { + FX_RECT r_font_units = font->GetCharBBox(code); + const float fs = to->GetFontSize(); + + 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 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; + + // 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); + return parent_to_page.TransformRect(glyph_box); +} + +// 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)); + } else { + w_th = static_cast(font->GetCharWidthF(code)); + } + + const float fs = to->GetFontSize(); + + // 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; +} + +// 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 +} + +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; + + 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) { + const CPDF_TextObject::Item it = to->GetItemInfo(i); + + // Original file kerning separator inside TJ. + if (it.char_code_ == CPDF_Font::kInvalidCharCode) { + 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; + continue; + } + + // 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 (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); + } + } + + // Keep this glyph. + st.AppendKeptGlyph(it); + st.ResetBetweenRuns(); + any_kept = true; + } + + if (!any_kept) + return any_removed ? RedactOutcome::kRemovedAll : RedactOutcome::kUnchanged; + + // 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()); + + 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 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, + const CFX_Matrix& to_page, + bool recurse_forms, + bool fill_black) { + 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 (CPDF_ImageObject* io = po->AsImage()) { + if (RedactImageObject(page_for_cache, io, page_rects, to_page, fill_black)) { + changed = true; + } + 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(); + const CFX_Matrix next_to_page = to_page * placement; + 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); + form_gen.GenerateContent(); + changed = true; + } + } + } + } + + // Physically remove fully emptied text and path objects. + if (!to_remove.empty()) { + for (CPDF_PageObject* obj : to_remove) { + holder->RemovePageObject(obj); + } + changed = true; + } + + return changed; +} + +} // namespace + +bool RedactTextInRect(CPDF_Page* page, + const CFX_FloatRect& page_space_rect_in, + bool recurse_forms, + bool draw_black_boxes) { + if (!page) + return false; + + CFX_FloatRect r = page_space_rect_in; + r.Normalize(); + const CFX_Matrix identity; + + const CFX_FloatRect rects[] = {r}; + 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 draw_black_boxes) { + 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; + 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 new file mode 100644 index 0000000000..26084a0d2e --- /dev/null +++ b/core/fpdfapi/edit/cpdf_text_redactor.h @@ -0,0 +1,29 @@ +// 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 draw_black_boxes); + +bool RedactTextInRects(CPDF_Page* page, + pdfium::span page_space_rects, + 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_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_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 f80e1bf066..7d29570731 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.cpp +++ b/core/fpdfapi/page/cpdf_pageobjectholder.cpp @@ -120,6 +120,32 @@ 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); + 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..ba522f6227 100644 --- a/core/fpdfapi/page/cpdf_pageobjectholder.h +++ b/core/fpdfapi/page/cpdf_pageobjectholder.h @@ -136,6 +136,12 @@ 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); + // `stream` must be non-negative or `CPDF_PageObject::kNoContentStream`. CFX_Matrix GetCTMAtBeginningOfStream(int32_t stream); @@ -153,6 +159,8 @@ 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/core/fpdfapi/page/cpdf_textobject.cpp b/core/fpdfapi/page/cpdf_textobject.cpp index 624b21e8eb..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(); } @@ -355,3 +363,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..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); @@ -74,6 +76,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: diff --git a/fpdfsdk/fpdf_edittext.cpp b/fpdfsdk/fpdf_edittext.cpp index c08db236e0..3833675d00 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,33 @@ 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, 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, !!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 draw_black_boxes + ) { + 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, !!draw_black_boxes); +} \ No newline at end of file diff --git a/public/fpdf_edit.h b/public/fpdf_edit.h index c6b63f65f3..9a9dc0626b 100644 --- a/public/fpdf_edit.h +++ b/public/fpdf_edit.h @@ -1650,6 +1650,37 @@ 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, + FPDF_BOOL draw_black_boxes); + +// 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, + FPDF_BOOL draw_black_boxes); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus