@doccloak/core 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +333 -0
- package/NOTICE +5 -0
- package/README.md +131 -9
- package/dist/dom/errors.d.ts +1 -1
- package/dist/dom/ocr.d.ts +33 -7
- package/dist/dom/ocr.js +98 -4
- package/dist/dom/office.d.ts +20 -6
- package/dist/dom/office.js +44 -11
- package/dist/engine.js +4 -5
- package/dist/env.d.ts +3 -3
- package/dist/pdf/cmap.d.ts +83 -0
- package/dist/pdf/cmap.js +659 -0
- package/dist/pdf/encodings.d.ts +49 -0
- package/dist/pdf/encodings.js +181 -0
- package/dist/pdf/extract.d.ts +106 -0
- package/dist/pdf/extract.js +1064 -0
- package/dist/pdf/fallback-fonts.d.ts +146 -0
- package/dist/pdf/fallback-fonts.js +312 -0
- package/dist/pdf/fit.d.ts +35 -0
- package/dist/pdf/fit.js +60 -0
- package/dist/pdf/fonts.d.ts +82 -0
- package/dist/pdf/fonts.js +465 -0
- package/dist/pdf/glyphlist.d.ts +37 -0
- package/dist/pdf/glyphlist.js +485 -0
- package/dist/pdf/index.d.ts +21 -0
- package/dist/pdf/index.js +7 -0
- package/dist/pdf/lexer.d.ts +61 -0
- package/dist/pdf/lexer.js +554 -0
- package/dist/pdf/objects.d.ts +57 -0
- package/dist/pdf/objects.js +217 -0
- package/dist/pdf/oracle.d.ts +149 -0
- package/dist/pdf/oracle.js +384 -0
- package/dist/pdf/raster.d.ts +45 -0
- package/dist/pdf/raster.js +89 -0
- package/dist/pdf/read.d.ts +36 -0
- package/dist/pdf/read.js +65 -0
- package/dist/pdf/serializer.d.ts +21 -0
- package/dist/pdf/serializer.js +106 -0
- package/dist/pdf/standard-metrics.d.ts +27 -0
- package/dist/pdf/standard-metrics.js +72 -0
- package/dist/pdf/surgery.d.ts +131 -0
- package/dist/pdf/surgery.js +847 -0
- package/dist/pdf/textstate.d.ts +77 -0
- package/dist/pdf/textstate.js +280 -0
- package/dist/pdf/types.d.ts +191 -0
- package/dist/pdf/types.js +11 -0
- package/dist/pdf/verify.d.ts +33 -0
- package/dist/pdf/verify.js +446 -0
- package/dist/pdf/write.d.ts +52 -0
- package/dist/pdf/write.js +644 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.js +123 -8
- package/dist/providers/bardsai.d.ts +1 -1
- package/dist/providers/bardsai.js +5 -1
- package/dist/providers/gliner-base.d.ts +1 -1
- package/dist/providers/gliner-base.js +5 -1
- package/dist/providers/gliner.d.ts +1 -1
- package/dist/providers/gliner.js +5 -1
- package/dist/regex/rules.data.js +13 -8
- package/dist/types.d.ts +17 -2
- package/dist/types.js +17 -0
- package/dist/worker-protocol.d.ts +11 -0
- package/dist/worker-protocol.js +41 -8
- package/fonts/liberation/LICENSE +102 -0
- package/fonts/liberation/LiberationMono-Bold.ttf +0 -0
- package/fonts/liberation/LiberationMono-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Italic.ttf +0 -0
- package/fonts/liberation/LiberationMono-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSans-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSans-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSans-Regular.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Bold.ttf +0 -0
- package/fonts/liberation/LiberationSerif-BoldItalic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Italic.ttf +0 -0
- package/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
- package/package.json +11 -2
- package/rules/pl.json +7 -3
- package/rules/universal.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,339 @@ All notable changes to `@doccloak/core` are documented here. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.13.0] - 2026-09-25
|
|
8
|
+
|
|
9
|
+
PDF-to-PDF redaction that keeps the text layer (new `@doccloak/core/pdf`
|
|
10
|
+
entry point, see "Added" below), cancelable detection, and detection fixes
|
|
11
|
+
found while testing real PDFs: Polish landline phones, catch-all numbers
|
|
12
|
+
that swallowed a PESEL and a phone, regex spans without surrounding
|
|
13
|
+
whitespace, placeholder fitting, OCR tokens split by a lost character,
|
|
14
|
+
kerned punctuation after a redacted value, and overlapping detections that
|
|
15
|
+
dropped part of a value.
|
|
16
|
+
Host-facing additions are optional (`signal` on detect, `cancelDetect` in
|
|
17
|
+
the worker protocol, the PDF entry point); nothing a 0.12.x host uses was
|
|
18
|
+
removed. The new PDF dependencies are pinned exactly like the others:
|
|
19
|
+
`@cantoo/pdf-lib` 2.11.1, `@cantoo/fontkit` 2.0.12, `pdfjs-dist` 6.3.289.
|
|
20
|
+
|
|
21
|
+
### Found by the first CI corpus run on real LibreOffice PDFs (T233)
|
|
22
|
+
|
|
23
|
+
- **PDF writer: kerned punctuation after a redacted value.** LibreOffice
|
|
24
|
+
writes `[(... Müller) 40 (, wohnhaft)] TJ`: the comma is pulled 0.48 pt
|
|
25
|
+
into the value's span. When a narrower placeholder closed the slack, the
|
|
26
|
+
rest of the line moved left but the comma, starting just before the end
|
|
27
|
+
of the removed span, counted as "left of the gap" and kept its old x,
|
|
28
|
+
landing inside "wohnhaft" ("wohn,haft", "[PHO,NE_1]"). A kept glyph now
|
|
29
|
+
moves with the text after a gap when it starts past the gap's middle; a
|
|
30
|
+
TJ that really jumps back still draws its text before the gap.
|
|
31
|
+
- **Overlap resolution keeps what a losing entity covers past the winner.**
|
|
32
|
+
`resolveOverlaps` dropped the loser whole. In a PDF exported from a
|
|
33
|
+
spreadsheet a clipped cell is glued to the next one ("31-147
|
|
34
|
+
Krakók.wisniewska@firma.pl"): the model took "31-147 Krakók", the e-mail
|
|
35
|
+
rule the whole token, and ".wisniewska@firma.pl" stayed in clear. Now the
|
|
36
|
+
rest of a regex match is re-run through its rule and kept where it still
|
|
37
|
+
matches (the e-mail does; "before 2025" left over from a bogus
|
|
38
|
+
"00 before 2025" date does not), and the rest of a model span is kept
|
|
39
|
+
without its edge punctuation. Detection never covers less than before.
|
|
40
|
+
- Tests: the corpus matches needles in PDFs across the line breaks a PDF
|
|
41
|
+
export puts inside a value (a space turned newline, a long e-mail broken
|
|
42
|
+
in a narrow cell, "Pawlak-" / "Dudek"), so wrapped values are replaced
|
|
43
|
+
and asserted absent instead of being counted as present and left alone;
|
|
44
|
+
values that LibreOffice Calc clips at a cell border are listed under
|
|
45
|
+
`optionalFor.pdf` in two seeds. PDF outputs of tests that redact only
|
|
46
|
+
some values on purpose (the desktop fixtures, the office invoice) are
|
|
47
|
+
written as `pdf-*`, so the CI poppler step opens them without grepping
|
|
48
|
+
the corpus needles.
|
|
49
|
+
|
|
50
|
+
### OCR image redaction covers both halves of a token OCR split (T232)
|
|
51
|
+
|
|
52
|
+
- Tesseract can drop a character inside a token and return two words: in
|
|
53
|
+
the kit's scanned letter "t.zielinski@zielinski-kancelaria.pl" came back
|
|
54
|
+
as "t" + "zielinski@zielinski-kancelaria.pl." with the dot's pixels in the
|
|
55
|
+
second box. The e-mail rule matched the second word only and boxes are
|
|
56
|
+
drawn per OCR word, so the "t" stayed visible (with "jan.kowalski@..." it
|
|
57
|
+
would be a first name). `selectRedactionBoxes` now spreads a selection to
|
|
58
|
+
same-line neighbours that `findSplitTokens` (exported) marks as one token:
|
|
59
|
+
the gap between their boxes is at most 0.3 of the line's median word gap
|
|
60
|
+
(height-based fallback on short lines; touching boxes included), and
|
|
61
|
+
boxes taller than 1.6x the line's median height (OCR noise such as a
|
|
62
|
+
26 px "w" on an 11 px line) never join. On seven OCR runs (the scan plus
|
|
63
|
+
kit pages 01/02 at 1000/1700/2500 px, about 1 150 words) exactly one pair
|
|
64
|
+
joins: the split e-mail. An ink test for a lost glyph left in a
|
|
65
|
+
normal-width gap was tried and dropped: Tesseract often cuts a letter's
|
|
66
|
+
edge out of its own box, which reads the same ("w terminie", "nr wpisu").
|
|
67
|
+
`renderRedactedImage`'s signature is unchanged.
|
|
68
|
+
|
|
69
|
+
### A catch-all regex span yields to the specific detections it overlaps (T230)
|
|
70
|
+
|
|
71
|
+
- `universal:long_number` (confidence 0.5) admits whitespace, so a PESEL cell
|
|
72
|
+
followed by a phone cell without "+48" ("90050598761 512 987 654") was one
|
|
73
|
+
23-character span that overlap resolution preferred (same start, longer)
|
|
74
|
+
over PESEL 0.9 + PHONE 0.8: the row came out as a single [SSN] and the
|
|
75
|
+
phone was "not detected". `detectEntities` now runs `splitCatchAlls`
|
|
76
|
+
first: a regex entity at or below confidence 0.5 that overlaps specific
|
|
77
|
+
detections (confidence >= 0.8, ML or regex) is replaced by its leftovers,
|
|
78
|
+
which are re-run through the same rule (an unknown long number next to a
|
|
79
|
+
phone is still caught); the specific spans then win. Catch-alls nothing
|
|
80
|
+
specific overlaps are untouched, and so is every other overlap (the
|
|
81
|
+
leak-averse "earlier start, longer span" order is unchanged).
|
|
82
|
+
Tests: tests/regex/catch-all-split.test.ts. No corpus expectation changed.
|
|
83
|
+
|
|
84
|
+
### Polish landlines without +48; stricter postal-code-plus-city (T227)
|
|
85
|
+
|
|
86
|
+
- `regex:pl:phone` only knew the 3-3-3 mobile grouping, so Polish landlines
|
|
87
|
+
written without the country code ("22 555 01 02", "(22) 555-01-02",
|
|
88
|
+
"71 777 88 99") fell through to `universal:long_number` and came out as
|
|
89
|
+
[SSN_n] under region pl. The rule now also accepts the 2-3-2-2 landline
|
|
90
|
+
grouping with an optional bracketed area code, with or without +48, and
|
|
91
|
+
the "+" of a "+48" prefix is part of the match (the mobile branch used to
|
|
92
|
+
start at "48"). A PESEL, a NIP, a bank account and a date still do not
|
|
93
|
+
match; an 11-digit number starting with 48 is no longer taken for a phone.
|
|
94
|
+
- `regex:universal:postal_city` matched "05/2026 Maria" (date-like prefix)
|
|
95
|
+
and "111 222 przegląd" (lowercase word) and stole the start of a phone or
|
|
96
|
+
a person name. The code must now stand at the start of the line or after
|
|
97
|
+
whitespace (so nothing glued to a digit, "/", "." or "-" matches), a
|
|
98
|
+
4-2 / 4-4 hyphenated shape ("2025-03", "2024-2025") is skipped and the
|
|
99
|
+
city must start with an uppercase letter. "00-950 Warszawa", "75008 Paris"
|
|
100
|
+
and "1010 Wien" still match; a code glued to an opening bracket
|
|
101
|
+
("(75008 Paris)") no longer does through this rule (country packs still
|
|
102
|
+
cover it). The rule stays lookbehind-free (pattern dialect lint) by
|
|
103
|
+
consuming the leading whitespace, which `runRule` trims (T226).
|
|
104
|
+
- Web regression corpus `universal.json` case `u-03-generic-identifiers`:
|
|
105
|
+
"987654321 remains" was a baked-in postal_city false positive; the number
|
|
106
|
+
is now PHONE "987654321" via `regex:pl:phone` (0.8 beats
|
|
107
|
+
`universal:long_number` 0.5 and the other 9-digit rules on the same span).
|
|
108
|
+
|
|
109
|
+
### Regex matches never carry whitespace (T226)
|
|
110
|
+
|
|
111
|
+
- The IBAN rule ended its match on a word boundary after a class that admits
|
|
112
|
+
whitespace, so "PL61 1090 1014 0000 0712 1981 2874 w terminie" matched
|
|
113
|
+
with the trailing space and the redaction produced "[IBAN_1]w terminie"
|
|
114
|
+
(text, DOCX and PDF alike). The pattern now ends on a letter or digit, and
|
|
115
|
+
`runRule` trims whitespace at both ends of every regex match as a general
|
|
116
|
+
safety net (start/end adjusted; a validator sees the trimmed value).
|
|
117
|
+
|
|
118
|
+
### Placeholder fit: move before shrink (T225)
|
|
119
|
+
|
|
120
|
+
- The planner now moves the rest of the line before it shrinks a placeholder:
|
|
121
|
+
room up to the next column and up to the page's text edge is used at full
|
|
122
|
+
size first; only the remainder is shrunk (floor raised from 65 % to 75 %)
|
|
123
|
+
and condensed (80 %). Short words on left-aligned lines ("NIP", "Kraków")
|
|
124
|
+
no longer get half-size placeholders; lines that already reach the margin
|
|
125
|
+
behave as before. `fit.minSizeRatio` default is 0.75. The page's text edge
|
|
126
|
+
is a hard limit (content boxes are clipped by Chrome and Word), and of a
|
|
127
|
+
value wrapped over several lines the widest part now carries the
|
|
128
|
+
placeholder (before: always the first part).
|
|
129
|
+
|
|
130
|
+
### Cancelable detection (T222)
|
|
131
|
+
|
|
132
|
+
- `DetectionProvider.detect(text, onProgress?, signal?)`: the three built-in
|
|
133
|
+
providers check the signal between inference chunks and reject with the new
|
|
134
|
+
`DetectionAbortedError` (also thrown by `engine.detect` for a signal aborted
|
|
135
|
+
before or after the ML pass; it used to be a plain `Error('Detection aborted')`).
|
|
136
|
+
- Worker protocol: new `cancelDetect { requestId }` request; the cancelled
|
|
137
|
+
call answers `detectError { aborted: true }`. `connectEngine().detect` with
|
|
138
|
+
an aborted signal now sends `cancelDetect` and rejects with
|
|
139
|
+
`DetectionAbortedError` once the host has acknowledged, so a rejection
|
|
140
|
+
means the worker is idle again (before, it rejected at once and the host
|
|
141
|
+
kept computing).
|
|
142
|
+
|
|
143
|
+
PDF-to-PDF redaction that keeps the text layer (T206-T218, founder decision
|
|
144
|
+
2026-09-24 reversing the 2026-08-30 desktop-only split).
|
|
145
|
+
|
|
146
|
+
### Added
|
|
147
|
+
|
|
148
|
+
- New entry point **`@doccloak/core/pdf`**: `readPdf(file, { assets })` and
|
|
149
|
+
`writeAnonymizedPdfWithReport(extraction, replacements, values, options)`.
|
|
150
|
+
The redacted glyphs are cut out of the content streams, the placeholder is
|
|
151
|
+
written back as real text in the document's own font when that font can
|
|
152
|
+
show it (codes proven present in the subset), else in a metrics-compatible
|
|
153
|
+
Liberation face (SIL OFL, shipped under `fonts/liberation`) embedded as a
|
|
154
|
+
subset. Placeholders are fitted to the redacted span: same size when it
|
|
155
|
+
fits, then shrunk to 65 %, then condensed to 80 % width, then the rest of
|
|
156
|
+
the line moves right by the overflow; a narrow placeholder closes the slack
|
|
157
|
+
so no hole is left. Every later show in the positioning scope is
|
|
158
|
+
re-anchored absolutely, so residual glyph positions cannot encode the
|
|
159
|
+
removed text (USENIX 2022 "Story Beyond the Eye").
|
|
160
|
+
- Fonts: simple Type1/TrueType/Type3 (Standard/WinAnsi/MacRoman/Differences,
|
|
161
|
+
ToUnicode), composite Type0 with Identity-H/V or embedded CMaps (Chrome,
|
|
162
|
+
Word, macOS output), standard-14 metrics for non-embedded fonts.
|
|
163
|
+
- Two independent extractors: our decoder and pdf.js. A page whose text
|
|
164
|
+
differs between them, or whose fonts cannot be read safely, is rasterized
|
|
165
|
+
(pixels blacked out) when it carries a redaction, and listed in
|
|
166
|
+
`rasterizedPages`. Text of such pages comes from pdf.js so detection still
|
|
167
|
+
sees it.
|
|
168
|
+
- Output is rebuilt as a fresh single-revision document: Info, XMP,
|
|
169
|
+
outlines, forms, annotations, attachments, JavaScript, named destinations,
|
|
170
|
+
structure tree, page labels and the incremental-update history of the
|
|
171
|
+
input never reach the output (reported in `removed`); ToUnicode maps of
|
|
172
|
+
edited fonts are trimmed to the codes still used; ActualText/Alt marked
|
|
173
|
+
content is stripped; dates normalised to 2000-01-01, producer `DocCloak`.
|
|
174
|
+
- Verification after every write: re-extraction with both extractors plus a
|
|
175
|
+
byte scan of every decoded stream and string (UTF-8, Latin-1, UTF-16 BE/LE)
|
|
176
|
+
and of the removed code sequences; any trace throws `PdfVerifyError` and
|
|
177
|
+
nothing is exported.
|
|
178
|
+
- `office.ts`: `officeFileKind` returns `'pdf'`; `analyzeOfficeFile` and
|
|
179
|
+
`redactOfficeFile` handle PDFs through a lazy import of the PDF module
|
|
180
|
+
(`options.pdf` carries assets, password, fit policy); results gain
|
|
181
|
+
`removed` and `rasterizedPages`.
|
|
182
|
+
- `UnredactablePart.kind` gains `'scanned-page'` and `'undecodable-text'`.
|
|
183
|
+
- Dependencies: `@cantoo/pdf-lib`, `@cantoo/fontkit`, `pdfjs-dist`. Hosts
|
|
184
|
+
serve the pdf.js worker, its CMaps and standard fonts, and the Liberation
|
|
185
|
+
TTFs same-origin (see README, "PDF").
|
|
186
|
+
|
|
187
|
+
### Second review (T219, same release)
|
|
188
|
+
|
|
189
|
+
- Layer zero searches the flat text in the verifier's normal form (whitespace,
|
|
190
|
+
soft hyphens and zero-width characters dropped, ligatures spelled out, case
|
|
191
|
+
folded), so a value wrapped over a line end ("Jan" / "Kowalski" on the next
|
|
192
|
+
line), spaced differently by the layout or set with a ligature is redacted
|
|
193
|
+
instead of blocking the export with `PdfVerifyError`. Segments of a value
|
|
194
|
+
on other lines close up over the removed glyphs.
|
|
195
|
+
- A fallback font needed inside a form XObject that has no `/Resources` of
|
|
196
|
+
its own goes into the page resources (where viewers resolve it); duplicated
|
|
197
|
+
forms with identical bytes all receive it.
|
|
198
|
+
- Lexer: inline image data ends the way pdf.js finds it (`EI` need not follow
|
|
199
|
+
whitespace, must be followed by ASCII; DCT data ends at its EOI marker,
|
|
200
|
+
ASCIIHex at `>`, ASCII85 at `~>`; `/L` honoured); an image without an end
|
|
201
|
+
makes the rest of the stream unsafe instead of a silent opaque operator;
|
|
202
|
+
arrays/dicts nested deeper than 64 are unsafe instead of a stack overflow;
|
|
203
|
+
`--5` reads as -5 (pdf.js, Acrobat); raw CR inside literal strings is kept.
|
|
204
|
+
- CMap parser tolerates what pdf.js tolerates: a missing `endcmap`, a bfrange
|
|
205
|
+
destination array shorter than the range, bounds of different byte lengths.
|
|
206
|
+
Rewritten ToUnicode maps keep the source codespace and skip empty
|
|
207
|
+
destinations. Glyph list gains the AGL combining-mark names.
|
|
208
|
+
- `readPdf` enforces the byte cap before pdf.js parses the file. Files that
|
|
209
|
+
carry only an owner password (print/copy restrictions) open without one.
|
|
210
|
+
- Fonts: a placeholder reuses a simple font only through its encoding (glyph
|
|
211
|
+
names), never through ToUnicode, and only for codes whose ToUnicode entry
|
|
212
|
+
agrees with the glyph, so the page shows what the text layer says; composite
|
|
213
|
+
placeholders take the code byte length from the encoding CMap (Identity-H:
|
|
214
|
+
2), not from the ToUnicode codespace; the lowest code wins ties in the
|
|
215
|
+
reverse map (space is 32, not 160); an empty ToUnicode destination falls
|
|
216
|
+
back to the encoding; TrueType fonts without `/Encoding` default the way
|
|
217
|
+
pdf.js does (Standard when Nonsymbolic, WinAnsi without the flag, MacRoman
|
|
218
|
+
for symbolic non-embedded); a symbolic embedded font our decoder cannot
|
|
219
|
+
read is rasterized on edit (pdf.js reads it), no longer refused; direct
|
|
220
|
+
font dictionaries no longer share a cache entry; right-to-left pages are
|
|
221
|
+
compared with pdf.js as a bag of characters (pdf.js hands them back in
|
|
222
|
+
visual order).
|
|
223
|
+
- Style matching: weight and slant words count only as whole words
|
|
224
|
+
("Kobold", "Digital", "Hospital" are not bold/italic), `/StemV` is no
|
|
225
|
+
longer read as a weight (Chrome writes 130+ for regular Georgia), Computer
|
|
226
|
+
Modern / Nimbus / Cascadia names map to their families.
|
|
227
|
+
- Verifier: the removed-code-sequence pass only checks the complete code
|
|
228
|
+
sequence of a whole value (a fragment of a value split over two shows
|
|
229
|
+
legitimately occurs elsewhere) and deduplicates sequences; layer zero and
|
|
230
|
+
the placement planner are linear in the number of replacements (a 200-page
|
|
231
|
+
file with 32 000 replacements writes in under 2 s instead of 90 s).
|
|
232
|
+
- Layout: text pushed right by an overflowing placeholder stops before the
|
|
233
|
+
next column (the placeholder gives up its size floors rather than overlap);
|
|
234
|
+
a continuation line that starts with the tail of a wrapped value closes up
|
|
235
|
+
as a whole, justified word gaps included.
|
|
236
|
+
- `fitPlaceholder` rejects non-finite input and clamps a policy that would
|
|
237
|
+
enlarge the placeholder.
|
|
238
|
+
|
|
239
|
+
### Third review (T220, same release)
|
|
240
|
+
|
|
241
|
+
- Text that lives in tiling pattern cells and ExtGState soft-mask groups is
|
|
242
|
+
walked like form XObjects: it reaches detection, is edited in place, and
|
|
243
|
+
never shares a line with page text (it was invisible to both extractors).
|
|
244
|
+
- Glyphs are kept in reading order along the baseline: a value shown back to
|
|
245
|
+
front, or words drawn from the right, reads as "Jan Kowalski" in the text a
|
|
246
|
+
detector sees; overlapping glyphs keep content order.
|
|
247
|
+
- The pdf.js cross-check no longer replaces our text when pdf.js read less
|
|
248
|
+
(a font it rejects, pattern text) or misread a font we decoded completely;
|
|
249
|
+
such pages keep our text and are rasterized on edit with our glyph
|
|
250
|
+
geometry for the black boxes (pdf.js item widths can be wrong). A page
|
|
251
|
+
whose only reading comes from pdf.js as glyph ids, controls, U+FFFD or
|
|
252
|
+
private-use characters is unredactable instead of "fine".
|
|
253
|
+
- Fonts: a placeholder never reuses a code the font advances by zero;
|
|
254
|
+
/Differences names without metrics (/.notdef, /gNN) advance like the base
|
|
255
|
+
encoding's glyph and blank glyphs are not "unreadable"; symbolic embedded
|
|
256
|
+
TrueType fonts without /Encoding decode through MacRoman (pdf.js); glyph
|
|
257
|
+
text is folded at decode time (fi -> fi, Kangxi radicals -> ideographs).
|
|
258
|
+
- Matching: layer zero, the verifier's text passes and its byte passes share
|
|
259
|
+
one search (whitespace, hyphens and ligatures ignored, case folded) that
|
|
260
|
+
requires token boundaries: "Nowak" is not "Nowakowski", "kid" is not /Kids;
|
|
261
|
+
a value hyphenated at a line end is still found. The verifier also scans
|
|
262
|
+
name objects and, for multi-word or numeric values, font programs.
|
|
263
|
+
- Layout: two placeholders on one line, or a value touching another in one
|
|
264
|
+
show, each get their placeholder and their shift; consecutive TJ
|
|
265
|
+
adjustments add up; the room before the next column accounts for shifts
|
|
266
|
+
already planned; a placeholder never goes below 4 pt (a warning reports an
|
|
267
|
+
unavoidable overlap); text on a rotated baseline crossing a line no longer
|
|
268
|
+
drags that line's cells.
|
|
269
|
+
- Output hygiene: XMP on any copied object, /PieceInfo, font descriptor
|
|
270
|
+
family/charset strings and layer names are removed from the output (and
|
|
271
|
+
the orphaned objects from the file); ToUnicode maps are rewritten for every
|
|
272
|
+
edited font, including fonts whose edited shows were deleted whole.
|
|
273
|
+
- Files with only an owner password open; Type3 fonts never take standard-14
|
|
274
|
+
metrics.
|
|
275
|
+
|
|
276
|
+
### Fourth review (T221, same release)
|
|
277
|
+
|
|
278
|
+
- Randomized invariants (`tests/pdf-fuzz.test.ts`, seeded generator of
|
|
279
|
+
layouts with out-of-order text, kerning, wrapped and touching values,
|
|
280
|
+
ligatures, diacritics, fallback faces; 2000-document campaigns) found and
|
|
281
|
+
fixed: narrow glyphs drawn back to front filed after their neighbour;
|
|
282
|
+
shifts inside a TJ applied by array order instead of position (a jump-back
|
|
283
|
+
adjustment moved the wrong text, even off the page); overlapping runs
|
|
284
|
+
losing their word separator; two values touching in one show
|
|
285
|
+
over-estimating the room before the next column; layer zero handling a
|
|
286
|
+
shorter value before a longer one that contains it (the surname stayed);
|
|
287
|
+
verifier byte passes counting a Latin-1 letter (ó, ü) or a kerning number
|
|
288
|
+
as a token boundary ("Nowak" refused because of "Nowaków"); the
|
|
289
|
+
code-sequence pass without boundaries (now single-byte codes at token
|
|
290
|
+
boundaries only).
|
|
291
|
+
- Second real-world corpus (44 files: Acrobat, iText, Aspose, Scribus,
|
|
292
|
+
TCPDF, wkhtmltopdf, pdfTeX/xdvipdfmx, LibreOffice/OpenOffice, ArcGIS,
|
|
293
|
+
PDF 2.0 samples): pdfTeX Type1 fonts without /Encoding are decoded through
|
|
294
|
+
the font program's cleartext encoding (LaTeX documents are edited in place
|
|
295
|
+
instead of rasterized); a rasterized vertical-writing page is covered whole
|
|
296
|
+
(the value stayed legible before); the verifier scans only string operands
|
|
297
|
+
of content streams (a house number equal to a font size no longer refuses
|
|
298
|
+
the export), recognises font programs by their magic bytes (TrueType table
|
|
299
|
+
tags in the fallback subset are not values), and ignores a value that only
|
|
300
|
+
survives inside its own placeholder ("We" in "[WE]"); raster boxes on
|
|
301
|
+
pdf.js-read pages cover the value plus one glyph each side instead of the
|
|
302
|
+
whole item; scanned pages with a hidden OCR layer are described as such;
|
|
303
|
+
consecutive unredactable pages collapse into one entry ("pages 2-22").
|
|
304
|
+
- Robustness (byte mutations, hostile structures): page-tree errors and
|
|
305
|
+
every pdf.js failure surface as typed errors; a page pdf.js cannot read
|
|
306
|
+
fails verification closed; rasterization failures are typed; replacement
|
|
307
|
+
ranges outside the text are refused instead of clamped to the whole
|
|
308
|
+
document; caps on decoded content per page (32 MB), form XObject draws per
|
|
309
|
+
page (20 000) and glyphs per document (1.5 million, runs weighted) turn
|
|
310
|
+
Flate bombs and exponential XObject graphs into `too-large` or a refused
|
|
311
|
+
page, and pdf.js is never asked about such a page; every pdf.js page
|
|
312
|
+
operation has a 60 s deadline (effective where pdf.js runs in a worker);
|
|
313
|
+
layer zero, the placement planner (baseline index) and the byte scan
|
|
314
|
+
(native string search) are linear again for thousands of replacements.
|
|
315
|
+
- Verifier: pages both extractors read identically are settled by our own
|
|
316
|
+
pass; pdf.js is consulted only for pages with issues (its spacing
|
|
317
|
+
heuristics reported "Jan" inside a kerned "Janusz"), and its items are read
|
|
318
|
+
in baseline order (words drawn from the right no longer spell a value); the
|
|
319
|
+
removed-code-sequence pass runs only when our extractor cannot re-read the
|
|
320
|
+
output.
|
|
321
|
+
- pdf.js never registers document fonts with the browser's FontFace API:
|
|
322
|
+
reading a 100-font file in Chromium dropped from 4.2 s to 0.8 s and the
|
|
323
|
+
export is no longer dominated by font round trips.
|
|
324
|
+
- Web: a damaged PDF gets its own message (the docx one told users to open
|
|
325
|
+
the file in Word); a failed upload while another file is loaded shows its
|
|
326
|
+
message.
|
|
327
|
+
|
|
328
|
+
### Tests
|
|
329
|
+
|
|
330
|
+
- 15 synthetic PDF fixtures (standard-14, Type0 Liberation, TJ kerning,
|
|
331
|
+
split shows, short names, form XObject, hidden text, ActualText,
|
|
332
|
+
annotations, AcroForm, outlines/Info/XMP, embedded file, incremental
|
|
333
|
+
update, object streams, rotated page with rise/Tz/Tw), the desktop's 10
|
|
334
|
+
standard-14 fixtures and a Chrome/Skia Type0 fixture; `assertNoTrace`
|
|
335
|
+
understands PDF (inflated streams, object streams, strings, UTF-16 BE).
|
|
336
|
+
- Fixture zips no longer carry implicit folder entries: JSZip stamped them
|
|
337
|
+
with the current time, so the "is deterministic" check failed when two
|
|
338
|
+
builds straddled a 2-second boundary (seen once under full-suite load).
|
|
339
|
+
|
|
7
340
|
## [0.12.1] - 2026-09-24
|
|
8
341
|
|
|
9
342
|
First run of the real-file corpus job (LibreOffice-generated docx, doc and
|
package/NOTICE
CHANGED
|
@@ -3,3 +3,8 @@ Copyright 2026 Witold Lojek
|
|
|
3
3
|
|
|
4
4
|
This product includes software developed as part of the DocCloak project
|
|
5
5
|
(https://github.com/WLojek).
|
|
6
|
+
|
|
7
|
+
This product bundles the Liberation Fonts (fonts/liberation/), Copyright 2012
|
|
8
|
+
Red Hat, Inc., licensed under the SIL Open Font License, Version 1.1
|
|
9
|
+
(see fonts/liberation/LICENSE). The fonts are embedded as subsets into
|
|
10
|
+
redacted PDF output; the Reserved Font Name "Liberation" is kept.
|
package/README.md
CHANGED
|
@@ -59,7 +59,10 @@ ONNX Runtime also needs its WASM binaries at runtime; point
|
|
|
59
59
|
## API overview
|
|
60
60
|
|
|
61
61
|
- **Engine** - `createEngine(env, initialSettings?, options?)` returns a
|
|
62
|
-
`DocCloakEngine`: `detect(text, signal?, onProgress?)
|
|
62
|
+
`DocCloakEngine`: `detect(text, signal?, onProgress?)` (an aborted
|
|
63
|
+
`signal` stops the model at the next inference chunk and rejects with
|
|
64
|
+
`DetectionAbortedError`; over the worker protocol the client sends
|
|
65
|
+
`cancelDetect` and rejects once the host has stopped), `preload()`,
|
|
63
66
|
`switchProvider(id)`, `getSettings()` / `updateSettings(patch)`,
|
|
64
67
|
`onDownloadProgress` / `onDetectionProgress`, `release()`. Settings persist
|
|
65
68
|
through `env.kv`. `options.autoLoad` (default `true`) lets `detect()`
|
|
@@ -165,6 +168,89 @@ streams: { data, objectPool, macros } }` without throwing, so a host can show
|
|
|
165
168
|
the same consent card for a .doc whose `ObjectPool` (embedded objects) or
|
|
166
169
|
`Macros` (VBA) storage will be copied unchanged.
|
|
167
170
|
|
|
171
|
+
## PDF: redaction that keeps the text layer
|
|
172
|
+
|
|
173
|
+
`@doccloak/core/pdf` redacts PDFs without rasterizing them. The glyphs of a
|
|
174
|
+
detected value are cut out of the content stream; the placeholder is written
|
|
175
|
+
back as real text, in the document's own font whenever that font can show it
|
|
176
|
+
(every character must map to a code whose glyph is proven present), else in
|
|
177
|
+
a metrics-compatible Liberation face (Sans/Serif/Mono, matched by the font's
|
|
178
|
+
style flags and name) embedded as a subset. Everything else on the page
|
|
179
|
+
stays live text.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { readPdf, writeAnonymizedPdfWithReport } from '@doccloak/core/pdf';
|
|
183
|
+
|
|
184
|
+
const assets = {
|
|
185
|
+
pdfjsWorkerUrl: '/pdf/pdf.worker.mjs', // node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs
|
|
186
|
+
cMapUrl: '/pdf/cmaps/', // node_modules/pdfjs-dist/cmaps/
|
|
187
|
+
standardFontDataUrl: '/pdf/standard_fonts/', // node_modules/pdfjs-dist/standard_fonts/
|
|
188
|
+
fontsUrl: '/pdf/fonts/', // node_modules/@doccloak/core/fonts/liberation/
|
|
189
|
+
};
|
|
190
|
+
const extraction = await readPdf(file, { assets }); // extraction.plainText -> detect
|
|
191
|
+
const entities = await engine.detect(extraction.plainText);
|
|
192
|
+
const replacements = entities.map((e) => ({ start: e.start, end: e.end, replacement: session.anonymize(e.value, e.type) }));
|
|
193
|
+
const { blob, warnings, removed, rasterizedPages } = await writeAnonymizedPdfWithReport(
|
|
194
|
+
extraction, replacements, session.getEntries(), { assets },
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Or go through `redactOfficeFile(file, { session, detect, pdf: { assets } })`,
|
|
199
|
+
which now accepts `.pdf` next to `.docx` and `.xlsx`.
|
|
200
|
+
|
|
201
|
+
**Placeholder fitting.** A placeholder rarely has the width of what it
|
|
202
|
+
replaces. The writer keeps the original size when the placeholder fits the
|
|
203
|
+
gap. Otherwise it first moves the rest of the line right, as far as the next
|
|
204
|
+
column and the page's text edge (the farthest line end on the page, a proxy
|
|
205
|
+
for the right margin) allow; only what still does not fit is shrunk down to
|
|
206
|
+
75 % of the size, then condensed down to 80 % of its width (`Tz`), so a
|
|
207
|
+
short word on a left-aligned line gets a full-size placeholder and a line
|
|
208
|
+
that already reaches the margin (right-aligned, justified, a table cell)
|
|
209
|
+
gets a smaller one (`fit` option: `minSizeRatio`, `minHorizontalScale`,
|
|
210
|
+
`allowOverflow`). The page's text edge is a hard limit (Chrome and Word clip
|
|
211
|
+
their content boxes, so text pushed past the margin would be cut); against
|
|
212
|
+
it, as against a column, the placeholder gives up its floors (never below
|
|
213
|
+
4 pt). Of a value wrapped over several lines, the widest part carries the
|
|
214
|
+
placeholder and the other parts are excised, so "ul." at a line end never
|
|
215
|
+
has to hold "[ADDRESS_1]". A placeholder narrower than the gap closes the slack, so
|
|
216
|
+
"[PERSON_1], PESEL [ID_1], ..." reads without holes. Text on other lines and
|
|
217
|
+
text separated by a tab-sized gap on the same line never moves. Every other
|
|
218
|
+
occurrence of a session value is redacted too (layer zero, as for docx),
|
|
219
|
+
matched with whitespace, soft hyphens and ligatures ignored: a name wrapped
|
|
220
|
+
over a line end gets its placeholder on the first line, and the next line
|
|
221
|
+
closes up over the removed part.
|
|
222
|
+
|
|
223
|
+
**Safety.** Every show after an edited one in the same positioning scope is
|
|
224
|
+
re-anchored with an absolute text matrix computed from the original
|
|
225
|
+
geometry, so the surviving glyphs cannot encode what was removed. Text in
|
|
226
|
+
form XObjects, tiling pattern cells and soft-mask groups is walked too, and
|
|
227
|
+
glyphs are read in baseline order whatever the content order. Two
|
|
228
|
+
independent extractors read every page (our decoder and pdf.js): a page
|
|
229
|
+
whose text differs, or whose fonts cannot be read safely (CJK predefined
|
|
230
|
+
CMaps, unreadable ToUnicode, vertical writing, unparsable content), is rasterized
|
|
231
|
+
when it carries a redaction and blacked out in the pixels; such pages are
|
|
232
|
+
listed in `rasterizedPages`. After writing, the output is re-read by both
|
|
233
|
+
extractors and byte-scanned (every decoded stream and string, UTF-8, Latin-1,
|
|
234
|
+
UTF-16 BE/LE, plus the removed code sequences); any trace throws
|
|
235
|
+
`PdfVerifyError` and nothing is exported.
|
|
236
|
+
|
|
237
|
+
**What is removed.** The output is rebuilt as a fresh single-revision file:
|
|
238
|
+
document properties (`/Info`), XMP, bookmarks, interactive forms,
|
|
239
|
+
annotations (comments, links, widgets), attachments, JavaScript, named
|
|
240
|
+
destinations, page labels and the tagged-PDF structure tree are dropped and
|
|
241
|
+
listed in `removed`; the input's incremental-update history cannot survive.
|
|
242
|
+
ToUnicode maps of edited fonts are trimmed to the codes still used,
|
|
243
|
+
`ActualText`/`Alt` marked-content properties are stripped, dates are set to
|
|
244
|
+
2000-01-01 and the producer to `DocCloak`. Optional-content (layer)
|
|
245
|
+
configuration is dropped as well, so content of hidden layers becomes
|
|
246
|
+
visible (it is redacted like everything else).
|
|
247
|
+
|
|
248
|
+
**Refusals.** Encrypted files (`'encrypted'`, pass `password`), files over
|
|
249
|
+
50 MB or 500 pages (`'too-large'`), non-PDF bytes (`'invalid-package'`).
|
|
250
|
+
Scanned pages (image without a text layer) and text no decoder can read are
|
|
251
|
+
reported in `unredactable` (`'scanned-page'`, `'undecodable-text'`) and gate
|
|
252
|
+
the export behind `allowUnredactable`, like Office packages.
|
|
253
|
+
|
|
168
254
|
## What is removed or normalised in Office packages
|
|
169
255
|
|
|
170
256
|
Independently of what was detected, every exported .docx / .xlsx has:
|
|
@@ -200,6 +286,41 @@ Independently of what was detected, every exported .docx / .xlsx has:
|
|
|
200
286
|
|
|
201
287
|
## Known limits
|
|
202
288
|
|
|
289
|
+
**PDF**
|
|
290
|
+
|
|
291
|
+
- The width of a redacted region stays observable: the placeholder (or the
|
|
292
|
+
closed slack) tells how wide the original text was. This is inherent to
|
|
293
|
+
every layout-preserving redaction; only rasterizing or re-typesetting the
|
|
294
|
+
paragraph would hide it.
|
|
295
|
+
- Subset fonts keep their glyph repertoire (which characters occurred
|
|
296
|
+
somewhere in the document), not the removed text.
|
|
297
|
+
- Pages with predefined CJK CMaps, unreadable ToUnicode maps, vertical
|
|
298
|
+
writing or text our decoder cannot confirm against pdf.js are rasterized
|
|
299
|
+
when they carry a redaction (their text layer is gone on that page).
|
|
300
|
+
Type3 fonts are edited like other simple fonts (their text comes from the
|
|
301
|
+
encoding names); a placeholder is never drawn with a Type3 font unless the
|
|
302
|
+
font itself showed every character of it. Digital signatures are
|
|
303
|
+
invalidated by any change.
|
|
304
|
+
- Images are copied unchanged; text inside pictures is not OCR-scanned.
|
|
305
|
+
- Forms, annotations, bookmarks and attachments are dropped, not redacted.
|
|
306
|
+
- Right-to-left text: pdf.js reports it in visual order, so the cross-check
|
|
307
|
+
compares the characters of such pages as a set; detection runs on our
|
|
308
|
+
logical-order text.
|
|
309
|
+
- Placeholders are set in the document's font or a Liberation face: a
|
|
310
|
+
placeholder character neither can show (CJK, emoji) is written as `?`
|
|
311
|
+
and reported in `warnings`.
|
|
312
|
+
- A page whose text only pdf.js can read, and only as glyph ids, controls or
|
|
313
|
+
replacement characters (fonts without a usable encoding or ToUnicode), is
|
|
314
|
+
reported as unredactable: no detector could find a value in it.
|
|
315
|
+
- Limits: 50 MB, 500 pages, 1.5 million glyphs (`maxBytes`, `maxPages`,
|
|
316
|
+
`maxGlyphs`), 32 MB of decoded content and 20 000 form draws per page.
|
|
317
|
+
Memory: the glyph model of the whole file is held during write and verify
|
|
318
|
+
(a 200-page text-heavy file peaks around 0.8 GB of heap in Node).
|
|
319
|
+
- A page whose content would make a viewer work forever (an exponential
|
|
320
|
+
chain of form XObjects, a decompression bomb) is never handed to pdf.js and
|
|
321
|
+
cannot be rasterized; pdf.js page operations time out after 60 s where it
|
|
322
|
+
runs in a worker (in Node's fake worker a hostile file can still block).
|
|
323
|
+
|
|
203
324
|
**Office packages (.docx, .xlsx)**
|
|
204
325
|
|
|
205
326
|
- Images inside documents (`media/*`) are copied unchanged; text in pictures
|
|
@@ -207,8 +328,9 @@ Independently of what was detected, every exported .docx / .xlsx has:
|
|
|
207
328
|
- Embedded objects (`embeddings/*`), VBA projects, `altChunk` HTML fragments,
|
|
208
329
|
external data connections, external links and printer settings cannot be
|
|
209
330
|
redacted. They are reported in `extraction.unredactable` and exported
|
|
210
|
-
verbatim only with `allowUnredactable: true` (see above). Removing them
|
|
211
|
-
planned for
|
|
331
|
+
verbatim only with `allowUnredactable: true` (see above). Removing them
|
|
332
|
+
(instead of copying them) is planned for a later release (T199), gated on
|
|
333
|
+
the real-file corpus (T196); 0.13.0 keeps the report-and-ask behaviour.
|
|
212
334
|
- Spreadsheets: sheet names with an entity are reported in `warnings`, not
|
|
213
335
|
renamed; numeric and date cells, formula literals, defined names and data
|
|
214
336
|
validation texts are not extracted yet (T176).
|
|
@@ -268,7 +390,7 @@ interface CoreEnv {
|
|
|
268
390
|
fetch: typeof fetch; // injectable for tests/proxies
|
|
269
391
|
wasm: { paths: string; numThreads?: number }; // onnxruntime-web asset location
|
|
270
392
|
buildTokenizer?(tokenizerJson: unknown, tokenizerConfig: unknown): unknown | Promise<unknown>;
|
|
271
|
-
/** @deprecated since 0.12.0,
|
|
393
|
+
/** @deprecated since 0.12.0, removal planned for 0.14.0; used only when buildTokenizer is absent */
|
|
272
394
|
loadTokenizer?(hfModelId: string): Promise<unknown>;
|
|
273
395
|
hardware?: HardwareHints; // deviceMemoryGB / isMobile, feeds provider default
|
|
274
396
|
persistStorage?: () => Promise<boolean>; // e.g. navigator.storage.persist()
|
|
@@ -302,11 +424,11 @@ With `buildTokenizer` the host never contacts the network for tokenizers, a
|
|
|
302
424
|
warm `modelCache` works offline, and the only URLs fetched on a cold start
|
|
303
425
|
are the three pinned `resolve/<commit>/...` files of the active provider.
|
|
304
426
|
|
|
305
|
-
`loadTokenizer(hfModelId)` (0.11 and earlier) is still honoured
|
|
306
|
-
`buildTokenizer` is absent, with a one-time deprecation warning. It
|
|
307
|
-
host library probe the mutable `resolve/main` branch and keep an
|
|
308
|
-
second cache, so migrate
|
|
309
|
-
|
|
427
|
+
`loadTokenizer(hfModelId)` (0.11 and earlier) is still honoured in 0.13.0
|
|
428
|
+
when `buildTokenizer` is absent, with a one-time deprecation warning. It
|
|
429
|
+
lets the host library probe the mutable `resolve/main` branch and keep an
|
|
430
|
+
unverified second cache, so migrate now: the removal is planned for 0.14.0,
|
|
431
|
+
where `buildTokenizer` becomes required.
|
|
310
432
|
|
|
311
433
|
### autoLoad and ModelNotLoadedError
|
|
312
434
|
|
package/dist/dom/errors.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare function isUnsupportedDocumentError(err: unknown): err is Unsuppo
|
|
|
29
29
|
export interface UnredactablePart {
|
|
30
30
|
/** Zip path of the part, e.g. 'word/embeddings/oleObject1.bin' */
|
|
31
31
|
part: string;
|
|
32
|
-
kind: 'embedded-object' | 'macros' | 'html-chunk' | 'external-data' | 'printer-settings' | 'unknown';
|
|
32
|
+
kind: 'embedded-object' | 'macros' | 'html-chunk' | 'external-data' | 'printer-settings' | 'unknown' | 'scanned-page' | 'undecodable-text';
|
|
33
33
|
/** Human-readable label for host UIs, e.g. 'embedded Excel sheet' */
|
|
34
34
|
label: string;
|
|
35
35
|
}
|
package/dist/dom/ocr.d.ts
CHANGED
|
@@ -85,22 +85,48 @@ interface BlockLike {
|
|
|
85
85
|
* Exported for tests.
|
|
86
86
|
*/
|
|
87
87
|
export declare function buildTextFromBlocks(blocks: BlockLike[]): OcrExtraction;
|
|
88
|
+
type PixelBox = {
|
|
89
|
+
x0: number;
|
|
90
|
+
y0: number;
|
|
91
|
+
x1: number;
|
|
92
|
+
y1: number;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* For each pair of consecutive OCR words, whether they are really one token
|
|
96
|
+
* that OCR split by dropping a character. Tesseract reads the dot in
|
|
97
|
+
* "t.zielinski@firma.pl" as a space and returns "t" + "zielinski@firma.pl",
|
|
98
|
+
* with the dot's pixels inside the second box; the e-mail rule then matches
|
|
99
|
+
* only the second word, and without this the "t" would stay visible. Such a
|
|
100
|
+
* split leaves a sliver between the boxes, far narrower than a real space: a
|
|
101
|
+
* pair on the same line is one token when its gap is at most
|
|
102
|
+
* TIGHT_GAP_SHARE of the line's typical word gap (boxes that touch or
|
|
103
|
+
* overlap included). Boxes much taller than the line (MAX_JOIN_HEIGHT_RATIO)
|
|
104
|
+
* are OCR noise and never join, so the fix cannot paint a bar across
|
|
105
|
+
* neighbouring lines.
|
|
106
|
+
*
|
|
107
|
+
* Not covered: a lost glyph that stays in a normal-width gap (seen only on
|
|
108
|
+
* low-resolution images, around 120 dpi). Telling it from a glyph edge that
|
|
109
|
+
* Tesseract cut off its own box needs pixels and misfired on real scans
|
|
110
|
+
* ("w terminie", "nr wpisu"), so gap width alone decides.
|
|
111
|
+
*
|
|
112
|
+
* Returns an array of length words.length - 1: entry i joins words i and i+1.
|
|
113
|
+
* Exported for tests.
|
|
114
|
+
*/
|
|
115
|
+
export declare function findSplitTokens(words: OcrWord[]): boolean[];
|
|
88
116
|
/**
|
|
89
117
|
* Pick the bounding boxes of every OCR word that overlaps one of the given
|
|
90
118
|
* character ranges. A word partially covered by a range is fully redacted
|
|
91
|
-
* (over-redaction is safer than leaking half an identifier).
|
|
119
|
+
* (over-redaction is safer than leaking half an identifier). A selected
|
|
120
|
+
* word also pulls in the same-line neighbours OCR split off by dropping a
|
|
121
|
+
* character (findSplitTokens), so "t" + "zielinski@firma.pl" is redacted
|
|
122
|
+
* as one token.
|
|
92
123
|
*
|
|
93
124
|
* Exported for tests.
|
|
94
125
|
*/
|
|
95
126
|
export declare function selectRedactionBoxes(words: OcrWord[], ranges: {
|
|
96
127
|
start: number;
|
|
97
128
|
end: number;
|
|
98
|
-
}[]):
|
|
99
|
-
x0: number;
|
|
100
|
-
y0: number;
|
|
101
|
-
x1: number;
|
|
102
|
-
y1: number;
|
|
103
|
-
}[];
|
|
129
|
+
}[]): PixelBox[];
|
|
104
130
|
/**
|
|
105
131
|
* Draw black boxes over the given character ranges and return the redacted
|
|
106
132
|
* image as a PNG blob.
|