Class: Pdf::TextReplacer
- Inherits:
-
Object
- Object
- Pdf::TextReplacer
- Defined in:
- app/services/pdf/text_replacer.rb
Overview
Find-and-replace in a PDF's TEXT layer ("everywhere the spec says X, make
it Y"). Each occurrence of +search+ is located with PDF::Reader's
positioned text runs, redacted with a cover rectangle, and overlaid with
+replacement+ at the same position and font size via Pdf::Toolkit.stamp.
Honest limits (surfaced in +meta+ / errors, never hidden):
- Only matches that fit inside a SINGLE text run are replaced. A phrase
fragmented across runs (kerning splits, mixed styling) is reported in
the not-found error message — the operator handles those by hand. - The overlay uses Helvetica; the original font is NOT matched, and a
replacement longer than the search text overflows the vacated width.
Review staged output in the PDF studio before importing. - Rasterized text (inside images) is invisible here — that's the
image-translation pipeline's job, and encoding-corrupted text layers
(broken ToUnicode CMaps) extract as mojibake and won't match. - Redaction is VISUAL (standard overlay redaction): the original text
stays in the text layer underneath the cover rectangle, so text
extraction (docling, search indexing) will read BOTH the old and new
values until a true content-stream redaction exists. The rendered
document is correct; re-indexes of the revised PDF are not, yet.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- COVER_PAD_X =
Cover-rectangle padding (points) so glyph descenders/antialiasing at the
edges don't peek out from under the redaction. 1.0- DESCENT_RATIO =
0.25- LINE_HEIGHT_RATIO =
1.3
Class Method Summary collapse
Class Method Details
.replace(path, search:, replacement:, case_sensitive: false, background: 'FFFFFF') ⇒ Result
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'app/services/pdf/text_replacer.rb', line 46 def replace(path, search:, replacement:, case_sensitive: false, background: 'FFFFFF') Pdf::Loader.load! raise Pdf::Toolkit::Error, 'search must be present' if search.to_s.strip.empty? hits, found_in_page_text = collect_matches(path, search.to_s, case_sensitive) raise Pdf::Toolkit::Error, (search, found_in_page_text) if hits.empty? bytes = hits.group_by { |h| h[:page] }.reduce(File.binread(path)) do |data, (page, page_hits)| operations = page_hits.flat_map do |hit| [cover_op(hit, background), text_op(hit, replacement)] end with_temp_pdf(data) { |tmp| Pdf::Toolkit.stamp(tmp, operations:, pages: page).bytes } end Result.new(bytes:, meta: { replacements: hits.size, pages: hits.pluck(:page).uniq.sort }) end |