@doccloak/core 0.13.0 → 0.15.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 +230 -0
- package/README.md +49 -7
- package/dist/dom/ocr.d.ts +41 -0
- package/dist/dom/ocr.js +78 -0
- package/dist/dom/office.d.ts +2 -0
- package/dist/dom/office.js +1 -0
- package/dist/env.d.ts +2 -2
- package/dist/pdf/coverage.d.ts +41 -0
- package/dist/pdf/coverage.js +193 -0
- package/dist/pdf/extract.d.ts +9 -1
- package/dist/pdf/extract.js +193 -12
- package/dist/pdf/raster.d.ts +118 -0
- package/dist/pdf/raster.js +525 -0
- package/dist/pdf/read.d.ts +3 -1
- package/dist/pdf/read.js +209 -7
- package/dist/pdf/standard-metrics.d.ts +6 -0
- package/dist/pdf/standard-metrics.js +24 -1
- package/dist/pdf/textstate.d.ts +15 -4
- package/dist/pdf/textstate.js +18 -5
- package/dist/pdf/types.d.ts +118 -4
- package/dist/pdf/verify.js +38 -5
- package/dist/pdf/write.d.ts +18 -1
- package/dist/pdf/write.js +382 -10
- package/dist/pipeline.d.ts +2 -1
- package/dist/pipeline.js +16 -2
- package/dist/regex/rules.data.js +5 -3
- package/package.json +1 -1
- package/rules/pl.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,236 @@ 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.15.0] - 2026-10-23
|
|
8
|
+
|
|
9
|
+
0.14.1 was tagged but never published to npm, so this is the first release
|
|
10
|
+
on npm after 0.14.0 and it carries the 0.14.1 changes too (listed under
|
|
11
|
+
0.14.1 below). New `readPdf` options (`ocrImagePages`, `onOcrProgress`),
|
|
12
|
+
hence the minor version; nothing a 0.14.0 host uses was removed.
|
|
13
|
+
|
|
14
|
+
Each PDF page is handled by what it holds (T239). The founder's mixed scan
|
|
15
|
+
had a picture page without any text layer between two OCR scans: its
|
|
16
|
+
personal data came out untouched.
|
|
17
|
+
|
|
18
|
+
- **Pages without a text layer are read with the host's OCR.** With a
|
|
19
|
+
`recognizeText`, `readPdf` reads a picture page that has no text at all on
|
|
20
|
+
the whole page (Tesseract's automatic segmentation only). A page with fewer
|
|
21
|
+
than three words read with 30 % confidence or more (a photo, a stamp,
|
|
22
|
+
noise) stays as it is, still listed as a scanned page. Otherwise every word
|
|
23
|
+
read is kept: Tesseract is least sure of elided words ("d'avril 2009",
|
|
24
|
+
"rachat d'actions"), and a word left out could be neither found nor
|
|
25
|
+
covered (seen on the issue file). A page read this way becomes a scan
|
|
26
|
+
with an OCR layer (`ocrText`, `PdfPageInfo.ocrWholePage` = the words
|
|
27
|
+
read): its values are detected, painted over in the picture and the page
|
|
28
|
+
keeps an invisible searchable layer. `ocrImagePages: false` turns this
|
|
29
|
+
off; without a host OCR nothing changes.
|
|
30
|
+
- **Text inside images on text pages.** The coverage check now also runs on
|
|
31
|
+
text pages with images, with every glyph of the page's own layer as
|
|
32
|
+
covered, so only text the images show is found and read. A text page with
|
|
33
|
+
a value inside an image is painted (rendered at 300 dpi) like a scan; a
|
|
34
|
+
text page whose images hold no value stays vector text, edited in place.
|
|
35
|
+
Its note says "an image on the page shows text its text layer does not
|
|
36
|
+
have", and the generic "contains images; text inside images is not
|
|
37
|
+
scanned" is no longer given for the pages a host OCR checked.
|
|
38
|
+
- **Values glued to an elision whose apostrophe OCR dropped.**
|
|
39
|
+
`propagateEntities` also finds a capitalised value right after a
|
|
40
|
+
lowercase "d", "l", "qu" and the other French and Italian elisions that
|
|
41
|
+
start a word ("dALTAMIR GERANCE", "lAltamir"); "McAltamir", "daltamir" and
|
|
42
|
+
"mALTAMIRE" are still left alone.
|
|
43
|
+
- **A value inside an OCR word is covered from its first letter.** Where a
|
|
44
|
+
value starts or ends inside a word ("ALTAMIR" of "d'ALTAMIR"), the cover
|
|
45
|
+
ended in the valley between two letters nearest the OCR's estimate,
|
|
46
|
+
either way. Tesseract placed that "A" from its middle, the nearest valley
|
|
47
|
+
was the one after it, and the "A" stayed visible ("d'A[PERSON_13]"). The
|
|
48
|
+
valley is now looked for on the side away from the value only (up to
|
|
49
|
+
0.8 em out, 0.05 em in): at worst the cover takes one letter of the rest
|
|
50
|
+
of the word, never leaves one of the value.
|
|
51
|
+
- **OCR progress.** `onOcrProgress(done, total)` reports the pages read so
|
|
52
|
+
far; the web app shows "page X of Y" while a scanned file loads.
|
|
53
|
+
- **One Tesseract worker per file.** `recognizeCanvasLines` keeps its worker
|
|
54
|
+
between pages (`releaseOcrWorker()` ends it) and takes `wholePage`; word
|
|
55
|
+
confidences are passed on in `RecognizedLine`.
|
|
56
|
+
- **The verifier no longer reads character codes in a font's ToUnicode map
|
|
57
|
+
as text.** The fallback font's map holds `<2019>` (the code of a closing
|
|
58
|
+
quote), so a detected year "2019" refused the export of the issue file. In
|
|
59
|
+
a CMap stream the hex strings are skipped; any other text in it is still
|
|
60
|
+
searched.
|
|
61
|
+
- Tests: each page kind (a picture read whole and painted, its values
|
|
62
|
+
covered when the OCR is unsure of their words, a picture with only noise,
|
|
63
|
+
a mixed file, a text page with an image with and without a value, no host
|
|
64
|
+
OCR); a value glued to an elision; a value whose first letter the OCR
|
|
65
|
+
places half a letter late; a value in a CMap's hex codes and in its
|
|
66
|
+
comment.
|
|
67
|
+
|
|
68
|
+
## [0.14.1] - 2026-10-22 (not published; shipped in 0.15.0)
|
|
69
|
+
|
|
70
|
+
Two fixes found by the acceptance run of web 0.15.0 on the preview (T237).
|
|
71
|
+
|
|
72
|
+
- **Long values on scans whose OCR letters are narrower than the print.** A
|
|
73
|
+
cover grew at most one em past the OCR layer's end. A layer 15 % narrower
|
|
74
|
+
than the print ends about 2 em short of a 13 em e-mail address, so its
|
|
75
|
+
last letters (".com") stayed visible. The cover may now grow by one em or
|
|
76
|
+
35 % of the value's length, whichever is more, still stopping at the first
|
|
77
|
+
word space. Measured on synthetic scans with two long values: clean down to
|
|
78
|
+
OCR letters at 75 % of the print's width (at 70 % a fragment remains).
|
|
79
|
+
- **The verifier no longer searches picture pixels for values.** It scanned
|
|
80
|
+
the decoded bytes of every stream not compressed with an image codec,
|
|
81
|
+
pixels of Flate pictures included, so a short value (the street number
|
|
82
|
+
"36") could match two neighbouring gray levels (51, 54) by chance and
|
|
83
|
+
refuse the export. Painted scan pages are such pictures, and so are Flate
|
|
84
|
+
logos and scans in any PDF. Image samples are skipped; the image
|
|
85
|
+
dictionary's strings are still searched.
|
|
86
|
+
- Tests: a long e-mail on the narrow-OCR scan; the verifier ignores a value's
|
|
87
|
+
bytes in an image's samples and finds them in the image dictionary and in
|
|
88
|
+
another stream.
|
|
89
|
+
|
|
90
|
+
Found by the next preview runs on real Tesseract scans (T238):
|
|
91
|
+
|
|
92
|
+
- **Values ending at punctuation glued to them in an OCR word.** Tesseract
|
|
93
|
+
gives every glyph of its invisible font the same width, so a value that
|
|
94
|
+
stops before glued punctuation ("+48 527 870 616," "12/4,") ended at a
|
|
95
|
+
guessed point inside its last letter, and the cover's growth stopped at the
|
|
96
|
+
0.23 em gap after a narrow "1": half of the last digit stayed visible.
|
|
97
|
+
Glued punctuation is now covered with the value, so the cover ends where
|
|
98
|
+
the OCR word does, and painted again around the placeholder
|
|
99
|
+
("[PHONE_1]," "([PHONE_1])."), so the picture reads like the redacted
|
|
100
|
+
text; on the line a value wraps onto, at the end of that line's cover,
|
|
101
|
+
where it was printed.
|
|
102
|
+
- **Diacritics standing apart above a value.** The cover's height follows
|
|
103
|
+
the letters' ink up to the first blank row, so the dot of "Ż" in
|
|
104
|
+
"Krzysztof Żółć" (and an accent over a word without ascenders) stayed in
|
|
105
|
+
the picture above the label. Small marks just above the letters that do
|
|
106
|
+
not run on into the line above get a cover of their own.
|
|
107
|
+
- **Short values' labels use the blank paper next to them.** A placeholder
|
|
108
|
+
never took more room than its value, so the label for "ul." or "Kraków,"
|
|
109
|
+
was squeezed and shrunk until it was hard to read. It now first takes the
|
|
110
|
+
blank paper after the value, then before it (a right-aligned line), up to
|
|
111
|
+
a word space before the next ink, half the gap to the next value on the
|
|
112
|
+
line and never outside the text column (where the page's OCR words start
|
|
113
|
+
and end, a margin note or a stamp left out), so a label never grows into
|
|
114
|
+
the margin. Values with no room on either side keep the squeezed label;
|
|
115
|
+
the text layer holds every placeholder in full.
|
|
116
|
+
- **Table rules the OCR read as "|" no longer hide a cell's word from
|
|
117
|
+
detection.** "|Kraków" (a vertical rule glued to the city in a scanned
|
|
118
|
+
table) was missed by the model while the same city without the rule was
|
|
119
|
+
found. On OCR text those characters read as spaces in the flat text, one
|
|
120
|
+
for one, so every offset still maps to its glyph (the layer keeps them).
|
|
121
|
+
- **Polish street addresses take in the flat number** (`regex:pl:street`):
|
|
122
|
+
"ul. Słoneczna 7 m. 12" and "al. Jerozolimskie 44 lok. 3B" match whole. On
|
|
123
|
+
a scan of a letter the model found "ul. Słoneczna 7" without "m. 12",
|
|
124
|
+
which stayed in the picture; the text PDF of the same letter got all of it.
|
|
125
|
+
- **Tesseract stretches each word with its trailing space.** Its shows hold
|
|
126
|
+
the word and a space, stretched together over the word's box, so a word's
|
|
127
|
+
last letter reaches past its own advance (the "7" of "31-147" stuck out).
|
|
128
|
+
When only whitespace follows a value in an OCR show, the end of the show
|
|
129
|
+
bounds where the value's ink may be (`PaintMark.reach`): ink up to there is
|
|
130
|
+
covered across gaps; in layers that are not stretched that space is blank
|
|
131
|
+
and nothing more is covered.
|
|
132
|
+
- **Text a scan's OCR layer does not have is reported.** A table or a stamp
|
|
133
|
+
the OCR could not read is not in the layer, so it cannot be found and stays
|
|
134
|
+
in the picture. `readPdf` now renders each page with OCR text at 100 dpi
|
|
135
|
+
and looks for rows of letter-sized ink that no OCR glyph covers
|
|
136
|
+
(`src/pdf/coverage.ts`); it sets `PdfPageInfo.ocrMissingLines` and adds a
|
|
137
|
+
warning. Rules, frames, signatures and pictures are larger than letters and
|
|
138
|
+
never count, nor do single marks or rows shorter than 2 em. Calibrated on
|
|
139
|
+
the scans of the acceptance run: the two tables Tesseract could not read
|
|
140
|
+
are reported, the court judgment (signature, stamp) and complete scans are
|
|
141
|
+
not. Options: `checkOcrCoverage` (default true), `canvasFactory` for Node
|
|
142
|
+
hosts; without a canvas the check is skipped.
|
|
143
|
+
- **Hosts can read that text with their own OCR.** `readPdf` takes an
|
|
144
|
+
optional `recognizeText(canvas, region)`: the page rendered at 300 dpi and
|
|
145
|
+
the region around the missing lines. The words it returns (lines with
|
|
146
|
+
baselines and letter boxes) that do not lie under the layer's own glyphs
|
|
147
|
+
join the page as hidden OCR runs after its text (`PdfPageInfo.ocrReadWords`,
|
|
148
|
+
`PdfExtraction.extraOcr`); the writer re-creates the same runs, so their
|
|
149
|
+
values are detected and painted like the layer's. `@doccloak/core/dom`
|
|
150
|
+
adds `recognizeCanvasLines`, which tries Tesseract's automatic,
|
|
151
|
+
single-column and single-block segmentation on the crop and keeps the
|
|
152
|
+
reading with the most letters (the same crop reads in one mode and gives
|
|
153
|
+
nothing in another). The tables Tesseract garbled on full pages read
|
|
154
|
+
cleanly this way. The region reaches 2.5 em above and below the missing
|
|
155
|
+
lines: a row whose only OCR text is junk ("[miesiąc | opowiezany ema Je |")
|
|
156
|
+
looks covered and is never reported missing itself. A word read there is
|
|
157
|
+
dropped only when the layer's glyphs under it spell about the same word
|
|
158
|
+
(60 % of the letters in order); under junk it is added.
|
|
159
|
+
- **Values inside a longer OCR word.** In a Tesseract word ("2026/03/017")
|
|
160
|
+
the letters of a value that is only part of it are placed by Helvetica
|
|
161
|
+
proportions over the printed word, and the cover ends in the blank gap
|
|
162
|
+
between two letters nearest that point, with a pixel of margin: "2026" of
|
|
163
|
+
"2026/03/017" leaves "/03/017" whole (before, the cover grew through "/0").
|
|
164
|
+
- **Painted scan pages stay searchable.** Their text, every redacted value
|
|
165
|
+
replaced by its placeholder, is written as an invisible layer (render mode
|
|
166
|
+
3, Liberation Sans) over the picture: each run on its own baseline,
|
|
167
|
+
stretched to its original width, so search and selection work as on the
|
|
168
|
+
input and sit over the printed words, on rotated pages too (the layer is
|
|
169
|
+
mapped through the same transform that placed the page in the picture).
|
|
170
|
+
A placeholder spans its whole value even when the OCR wrote the value as
|
|
171
|
+
several shows (Tesseract: one per word), and the punctuation after it
|
|
172
|
+
follows on the same line ("Telefon: [PHONE_1], e-mail").
|
|
173
|
+
The verifier reads the new layer like any text.
|
|
174
|
+
- The stretched-space reach applies only to shows whose glyphs all have the
|
|
175
|
+
same advance (Tesseract's invisible font): in a layer set in a real font
|
|
176
|
+
the trailing space is the gap before the next word, and the cover took in
|
|
177
|
+
the next word's first letter.
|
|
178
|
+
- Tests: glued punctuation and the Tesseract word reach (unit, fail without
|
|
179
|
+
the fixes), a Tesseract-style synthetic layer (one show per word with its
|
|
180
|
+
trailing space, equal letter widths, values red with black punctuation),
|
|
181
|
+
the missing-text finder (unit) and `readPdf` on a scan with a line the
|
|
182
|
+
layer lacks.
|
|
183
|
+
|
|
184
|
+
## [0.14.0] - 2026-10-22
|
|
185
|
+
|
|
186
|
+
Scanned PDF pages with an OCR text layer are redacted where a reader sees
|
|
187
|
+
them: in the picture. Additive for hosts (`PdfPageInfo.ocrText`,
|
|
188
|
+
`GlyphRun.ocr`, `PdfWriteResult.scanPages`, `scanPages` from
|
|
189
|
+
`redactOfficeFile`); one behaviour change: such pages are no longer reported
|
|
190
|
+
as unredactable `'scanned-page'` parts, because they are now redacted.
|
|
191
|
+
`CoreEnv.loadTokenizer` still works with its deprecation warning; its
|
|
192
|
+
removal, announced for this release, moves to a later one.
|
|
193
|
+
|
|
194
|
+
### Scanned pages with an OCR text layer are redacted in the picture (T235)
|
|
195
|
+
|
|
196
|
+
Reported in DocCloak.Core#1: a court judgment scanned with an OCR text layer
|
|
197
|
+
(Lutecia / 3-Heights: the text sits in a form XObject under an ExtGState
|
|
198
|
+
with `/ca 0`) was "redacted" in its invisible text layer only; the picture,
|
|
199
|
+
which is what a reader sees, still showed every name.
|
|
200
|
+
|
|
201
|
+
- **Transparent text is hidden text.** The state machine follows `gs` and
|
|
202
|
+
keeps the fill and stroke alpha (`ca` / `CA`); text painted at alpha below
|
|
203
|
+
0.02 counts as hidden, like render mode 3 and white text. Text painted
|
|
204
|
+
before an opaque image that covers it ("text under the page image") is
|
|
205
|
+
hidden too.
|
|
206
|
+
- **OCR text.** Hidden text over an image is marked `GlyphRun.ocr`, and its
|
|
207
|
+
page `PdfPageInfo.ocrText`. A scan page with OCR text is no longer an
|
|
208
|
+
unredactable part; a scan page without any text still is
|
|
209
|
+
(`'scanned-page'`, exported as is with `allowUnredactable`), and a file
|
|
210
|
+
without any text still reads as `empty`.
|
|
211
|
+
- **Painted placeholders.** A page whose OCR text holds a redacted value is
|
|
212
|
+
rendered at the scan's resolution (from the images' size and placement, at
|
|
213
|
+
least `rasterDpi`, at most 300 dpi, within a 40 Mpx budget). Each value is
|
|
214
|
+
covered in the paper colour (median of the pixels around it) and its
|
|
215
|
+
placeholder is painted on the value's baseline. The cover grows along the
|
|
216
|
+
line to where the value's ink ends (stopping at a word space, at most one
|
|
217
|
+
em), because OCR layers set letters in their own font and a value's last
|
|
218
|
+
letters can run past the layer's end; its height and the label's size come
|
|
219
|
+
from the value's ink, because OCR layers often stretch their font size to
|
|
220
|
+
the line height. The label fits the value's width: squeezed down to 60 %,
|
|
221
|
+
then shrunk. Pieces of a value on one line join under one label; a value
|
|
222
|
+
wrapped to the next line is covered there without a label. All covers are
|
|
223
|
+
painted before any label, so a cover never clips the line above.
|
|
224
|
+
- The page is embedded as a lossless Flate image (gray when the page is
|
|
225
|
+
gray), not a PNG: page 1 of the reported file stays the size of the scan.
|
|
226
|
+
It has no text layer. The writer lists it in the new
|
|
227
|
+
`PdfWriteResult.scanPages` (and `redactOfficeFile` in `scanPages`), with
|
|
228
|
+
a warning; `rasterizedPages` keeps meaning pages blacked out because
|
|
229
|
+
their text could not be edited safely.
|
|
230
|
+
- Text-layer PDFs are unchanged: the founder's kit files 01-08 read
|
|
231
|
+
identically (text, pages, parts, warnings) to 0.13.0.
|
|
232
|
+
- Tests: `pdf-scan.test.ts` (synthetic scans with values painted red: alpha,
|
|
233
|
+
render mode 3 and text-under-image layers, OCR letters narrower than the
|
|
234
|
+
scan, a mixed scan / picture-only / text file; transparent text without an
|
|
235
|
+
image; alpha in the state machine; cover growth; mark joining).
|
|
236
|
+
|
|
7
237
|
## [0.13.0] - 2026-09-25
|
|
8
238
|
|
|
9
239
|
PDF-to-PDF redaction that keeps the text layer (new `@doccloak/core/pdf`
|
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ const assets = {
|
|
|
190
190
|
const extraction = await readPdf(file, { assets }); // extraction.plainText -> detect
|
|
191
191
|
const entities = await engine.detect(extraction.plainText);
|
|
192
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(
|
|
193
|
+
const { blob, warnings, removed, rasterizedPages, scanPages } = await writeAnonymizedPdfWithReport(
|
|
194
194
|
extraction, replacements, session.getEntries(), { assets },
|
|
195
195
|
);
|
|
196
196
|
```
|
|
@@ -245,11 +245,52 @@ ToUnicode maps of edited fonts are trimmed to the codes still used,
|
|
|
245
245
|
configuration is dropped as well, so content of hidden layers becomes
|
|
246
246
|
visible (it is redacted like everything else).
|
|
247
247
|
|
|
248
|
+
**Scanned pages with an OCR text layer.** A scan with OCR shows a picture;
|
|
249
|
+
its text layer is invisible (render mode 3, fully transparent paint through
|
|
250
|
+
an ExtGState `/ca 0`, or text painted under the picture). Editing that layer
|
|
251
|
+
alone would leave every value visible in the picture, so the reader marks
|
|
252
|
+
such text (`GlyphRun.ocr`, `PdfPageInfo.ocrText`) and the writer renders a
|
|
253
|
+
page whose OCR text holds a redacted value at the scan's own resolution (at
|
|
254
|
+
least `rasterDpi`, at most 300 dpi) and paints the value over in the
|
|
255
|
+
picture: a cover in the paper colour, grown along the line to where the
|
|
256
|
+
value's ink really ends (OCR letters rarely have the scan's widths), and the
|
|
257
|
+
placeholder label in its place, on the value's baseline, sized from the
|
|
258
|
+
value's ink and fitted to the value's width, taking blank paper beside a
|
|
259
|
+
short value first (then squeezed, then shrunk). The
|
|
260
|
+
page is exported as a lossless image (gray for a gray scan) with an invisible
|
|
261
|
+
text layer holding its text, the placeholders in place of the values, so it
|
|
262
|
+
stays searchable, and listed in `scanPages`. Pages of the same file without any text are
|
|
263
|
+
not touched. Text the OCR layer does not have (a table or a stamp the OCR
|
|
264
|
+
could not read) cannot be found, so it stays in the picture: `readPdf`
|
|
265
|
+
renders each scan page with OCR text at 100 dpi, looks for rows of
|
|
266
|
+
letter-sized ink no OCR glyph covers and reports them in
|
|
267
|
+
`PdfPageInfo.ocrMissingLines` and `warnings` (`checkOcrCoverage: false` turns
|
|
268
|
+
the check off; Node hosts pass a `canvasFactory`, without one it is skipped).
|
|
269
|
+
A host with OCR passes `recognizeText(canvas, region)` (the web app hands in
|
|
270
|
+
Tesseract through `recognizeCanvasLines` from `@doccloak/core/dom`): the
|
|
271
|
+
words it reads there join the page as OCR text (`ocrReadWords`), and their
|
|
272
|
+
values are found and painted over like the rest.
|
|
273
|
+
|
|
274
|
+
**Each page by what it holds.** With a host OCR, a page that is only a
|
|
275
|
+
picture (no text layer at all) is read whole with Tesseract's automatic
|
|
276
|
+
segmentation. With fewer than three words read at 30 % confidence or more
|
|
277
|
+
the page stays as it is; otherwise every word read is kept, the unsure ones
|
|
278
|
+
too (`PdfPageInfo.ocrWholePage` gives the count).
|
|
279
|
+
A page read this way is handled as a scan with an OCR layer, so a file can
|
|
280
|
+
mix vector text pages, OCR scans and pictures, each redacted its own way. Text
|
|
281
|
+
pages with images get the same coverage check, with the page's own glyphs
|
|
282
|
+
counted as covered: text that only an image shows is read, and a text page
|
|
283
|
+
with a value in an image is exported painted, like a scan; without such a
|
|
284
|
+
value it stays vector text. `ocrImagePages: false` turns the whole-page
|
|
285
|
+
reading off, and `onOcrProgress(done, total)` reports the pages read.
|
|
286
|
+
|
|
248
287
|
**Refusals.** Encrypted files (`'encrypted'`, pass `password`), files over
|
|
249
288
|
50 MB or 500 pages (`'too-large'`), non-PDF bytes (`'invalid-package'`).
|
|
250
|
-
Scanned pages
|
|
251
|
-
reported in `unredactable` (`'scanned-page'`, `'undecodable-text'`) and gate
|
|
252
|
-
the export behind `allowUnredactable`, like Office packages
|
|
289
|
+
Scanned pages without a text layer (no OCR, and none the host's OCR could
|
|
290
|
+
read) and text no decoder can read are reported in `unredactable` (`'scanned-page'`, `'undecodable-text'`) and gate
|
|
291
|
+
the export behind `allowUnredactable`, like Office packages; with it they
|
|
292
|
+
are exported as they are. A file whose pages hold no text at all reads as
|
|
293
|
+
`empty`.
|
|
253
294
|
|
|
254
295
|
## What is removed or normalised in Office packages
|
|
255
296
|
|
|
@@ -301,7 +342,8 @@ Independently of what was detected, every exported .docx / .xlsx has:
|
|
|
301
342
|
encoding names); a placeholder is never drawn with a Type3 font unless the
|
|
302
343
|
font itself showed every character of it. Digital signatures are
|
|
303
344
|
invalidated by any change.
|
|
304
|
-
- Images are copied unchanged
|
|
345
|
+
- Images are copied unchanged. Without a host OCR, text inside pictures is
|
|
346
|
+
not OCR-scanned; with one, see "Each page by what it holds" above.
|
|
305
347
|
- Forms, annotations, bookmarks and attachments are dropped, not redacted.
|
|
306
348
|
- Right-to-left text: pdf.js reports it in visual order, so the cross-check
|
|
307
349
|
compares the characters of such pages as a set; detection runs on our
|
|
@@ -390,7 +432,7 @@ interface CoreEnv {
|
|
|
390
432
|
fetch: typeof fetch; // injectable for tests/proxies
|
|
391
433
|
wasm: { paths: string; numThreads?: number }; // onnxruntime-web asset location
|
|
392
434
|
buildTokenizer?(tokenizerJson: unknown, tokenizerConfig: unknown): unknown | Promise<unknown>;
|
|
393
|
-
/** @deprecated since 0.12.0, removal planned for
|
|
435
|
+
/** @deprecated since 0.12.0, removal planned for a later release; used only when buildTokenizer is absent */
|
|
394
436
|
loadTokenizer?(hfModelId: string): Promise<unknown>;
|
|
395
437
|
hardware?: HardwareHints; // deviceMemoryGB / isMobile, feeds provider default
|
|
396
438
|
persistStorage?: () => Promise<boolean>; // e.g. navigator.storage.persist()
|
|
@@ -427,7 +469,7 @@ are the three pinned `resolve/<commit>/...` files of the active provider.
|
|
|
427
469
|
`loadTokenizer(hfModelId)` (0.11 and earlier) is still honoured in 0.13.0
|
|
428
470
|
when `buildTokenizer` is absent, with a one-time deprecation warning. It
|
|
429
471
|
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
|
|
472
|
+
unverified second cache, so migrate now: the removal is planned for a later release,
|
|
431
473
|
where `buildTokenizer` becomes required.
|
|
432
474
|
|
|
433
475
|
### autoLoad and ModelNotLoadedError
|
package/dist/dom/ocr.d.ts
CHANGED
|
@@ -61,6 +61,47 @@ export declare function loadImageToCanvas(file: Blob): Promise<OcrCanvas>;
|
|
|
61
61
|
* TesseractAssetPaths). onProgress receives 0..1 during the recognition phase.
|
|
62
62
|
*/
|
|
63
63
|
export declare function recognizeCanvas(canvas: OcrCanvas, uiLanguage: string, assets: TesseractAssetPaths, onProgress?: (progress: number) => void): Promise<OcrExtraction>;
|
|
64
|
+
/** One line Tesseract read: its baseline and words, each word with its letters' boxes (pixels) and its confidence (0..100). */
|
|
65
|
+
export interface OcrLine {
|
|
66
|
+
baseline: {
|
|
67
|
+
x0: number;
|
|
68
|
+
y0: number;
|
|
69
|
+
x1: number;
|
|
70
|
+
y1: number;
|
|
71
|
+
};
|
|
72
|
+
words: Array<{
|
|
73
|
+
text: string;
|
|
74
|
+
bbox: {
|
|
75
|
+
x0: number;
|
|
76
|
+
y0: number;
|
|
77
|
+
x1: number;
|
|
78
|
+
y1: number;
|
|
79
|
+
};
|
|
80
|
+
confidence?: number;
|
|
81
|
+
symbols: Array<{
|
|
82
|
+
text: string;
|
|
83
|
+
bbox: {
|
|
84
|
+
x0: number;
|
|
85
|
+
y0: number;
|
|
86
|
+
x1: number;
|
|
87
|
+
y1: number;
|
|
88
|
+
};
|
|
89
|
+
}>;
|
|
90
|
+
}>;
|
|
91
|
+
}
|
|
92
|
+
/** Terminate the worker recognizeCanvasLines keeps between calls (after the last page of a document). */
|
|
93
|
+
export declare function releaseOcrWorker(): Promise<void>;
|
|
94
|
+
/**
|
|
95
|
+
* OCR a canvas holding part of a page and return its lines with baselines, words and letter
|
|
96
|
+
* boxes. Used on the parts of a scanned PDF page whose OCR layer has no text: cropped to such a
|
|
97
|
+
* part, a table that full-page segmentation turned into junk reads cleanly. Tesseract's page
|
|
98
|
+
* segmentation is picky about a crop (the same table reads in one mode and gives nothing in
|
|
99
|
+
* another), so the automatic, single-column and single-block modes are tried in turn and the
|
|
100
|
+
* reading with the most letters wins.
|
|
101
|
+
*/
|
|
102
|
+
export declare function recognizeCanvasLines(canvas: OcrCanvas, uiLanguage: string, assets: TesseractAssetPaths, dpi?: number, options?: {
|
|
103
|
+
wholePage?: boolean;
|
|
104
|
+
}): Promise<OcrLine[]>;
|
|
64
105
|
interface BlockLike {
|
|
65
106
|
paragraphs: {
|
|
66
107
|
lines: {
|
package/dist/dom/ocr.js
CHANGED
|
@@ -139,6 +139,84 @@ export async function recognizeCanvas(canvas, uiLanguage, assets, onProgress) {
|
|
|
139
139
|
await worker.terminate();
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* One Tesseract worker for the pages of a document: starting one loads the engine and the language
|
|
144
|
+
* data, which costs about a second, so the pages of a PDF are read by the same worker. The host
|
|
145
|
+
* releases it when the document is read (releaseOcrWorker).
|
|
146
|
+
*/
|
|
147
|
+
let shared = null;
|
|
148
|
+
function sharedWorker(langs, assets) {
|
|
149
|
+
const key = `${langs.join('+')}|${assets.workerPath}|${assets.corePath}|${assets.langPath}`;
|
|
150
|
+
if (shared && shared.key === key)
|
|
151
|
+
return shared.worker;
|
|
152
|
+
void releaseOcrWorker();
|
|
153
|
+
const worker = Tesseract.createWorker(langs, 1, { workerPath: assets.workerPath, corePath: assets.corePath, langPath: assets.langPath });
|
|
154
|
+
shared = { key, worker };
|
|
155
|
+
// A worker that failed to start is not kept: the next call tries again.
|
|
156
|
+
worker.catch(() => { if (shared?.worker === worker)
|
|
157
|
+
shared = null; });
|
|
158
|
+
return worker;
|
|
159
|
+
}
|
|
160
|
+
/** Terminate the worker recognizeCanvasLines keeps between calls (after the last page of a document). */
|
|
161
|
+
export async function releaseOcrWorker() {
|
|
162
|
+
const current = shared;
|
|
163
|
+
shared = null;
|
|
164
|
+
if (!current)
|
|
165
|
+
return;
|
|
166
|
+
try {
|
|
167
|
+
await (await current.worker).terminate();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* never started, or already gone */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* OCR a canvas holding part of a page and return its lines with baselines, words and letter
|
|
175
|
+
* boxes. Used on the parts of a scanned PDF page whose OCR layer has no text: cropped to such a
|
|
176
|
+
* part, a table that full-page segmentation turned into junk reads cleanly. Tesseract's page
|
|
177
|
+
* segmentation is picky about a crop (the same table reads in one mode and gives nothing in
|
|
178
|
+
* another), so the automatic, single-column and single-block modes are tried in turn and the
|
|
179
|
+
* reading with the most letters wins.
|
|
180
|
+
*/
|
|
181
|
+
export async function recognizeCanvasLines(canvas, uiLanguage, assets, dpi, options = {}) {
|
|
182
|
+
const worker = await sharedWorker(buildLangList(uiLanguage), assets);
|
|
183
|
+
let best = [];
|
|
184
|
+
let bestLetters = 0;
|
|
185
|
+
// A whole page is what automatic segmentation is made for; the other modes are for crops.
|
|
186
|
+
const modes = options.wholePage ? [Tesseract.PSM.AUTO] : [Tesseract.PSM.AUTO, Tesseract.PSM.SINGLE_COLUMN, Tesseract.PSM.SINGLE_BLOCK];
|
|
187
|
+
for (const mode of modes) {
|
|
188
|
+
// The resolution is known (a rendered page): Tesseract does not have to guess it (and log the guess).
|
|
189
|
+
await worker.setParameters({ tessedit_pageseg_mode: mode, ...(dpi ? { user_defined_dpi: String(Math.round(dpi)) } : {}) });
|
|
190
|
+
const { data } = await worker.recognize(canvas, {}, { blocks: true });
|
|
191
|
+
const lines = linesOf(data.blocks ?? []);
|
|
192
|
+
const letters = lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.text.length, 0), 0);
|
|
193
|
+
if (letters > bestLetters) {
|
|
194
|
+
best = lines;
|
|
195
|
+
bestLetters = letters;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return best;
|
|
199
|
+
}
|
|
200
|
+
function linesOf(blocks) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
for (const block of blocks) {
|
|
203
|
+
for (const paragraph of block.paragraphs ?? []) {
|
|
204
|
+
for (const line of paragraph.lines ?? []) {
|
|
205
|
+
const words = (line.words ?? [])
|
|
206
|
+
.filter((w) => (w.text ?? '').trim() !== '')
|
|
207
|
+
.map((w) => ({
|
|
208
|
+
text: (w.text ?? '').trim(),
|
|
209
|
+
bbox: w.bbox,
|
|
210
|
+
...(typeof w.confidence === 'number' ? { confidence: w.confidence } : {}),
|
|
211
|
+
symbols: (w.symbols ?? []).filter((x) => (x.text ?? '').trim() !== '').map((x) => ({ text: x.text ?? '', bbox: x.bbox })),
|
|
212
|
+
}));
|
|
213
|
+
if (words.length > 0)
|
|
214
|
+
lines.push({ baseline: line.baseline, words });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
142
220
|
/**
|
|
143
221
|
* Reconstruct plain text from Tesseract's block hierarchy while recording
|
|
144
222
|
* each word's character range. The reconstruction (not Tesseract's own
|
package/dist/dom/office.d.ts
CHANGED
|
@@ -127,6 +127,8 @@ export interface OfficeRedactionResult {
|
|
|
127
127
|
removed?: RemovedPart[];
|
|
128
128
|
/** PDF only: 0-based pages that were rasterized because they could not be edited safely. */
|
|
129
129
|
rasterizedPages?: number[];
|
|
130
|
+
/** PDF only: 0-based scan pages whose OCR-found values were painted over in the picture. */
|
|
131
|
+
scanPages?: number[];
|
|
130
132
|
}
|
|
131
133
|
/** "report.docx" -> "report.redacted.docx"; extensionless names get a suffix. */
|
|
132
134
|
export declare function redactedFileName(filename: string): string;
|
package/dist/dom/office.js
CHANGED
package/dist/env.d.ts
CHANGED
|
@@ -42,11 +42,11 @@ export interface CoreEnv {
|
|
|
42
42
|
* named by `tokenizerConfig.tokenizer_class`). May return a promise.
|
|
43
43
|
*
|
|
44
44
|
* Optional so hosts still on loadTokenizer keep compiling; required once
|
|
45
|
-
* loadTokenizer is removed (planned for
|
|
45
|
+
* loadTokenizer is removed (planned for a later release).
|
|
46
46
|
*/
|
|
47
47
|
buildTokenizer?(tokenizerJson: unknown, tokenizerConfig: unknown): unknown | Promise<unknown>;
|
|
48
48
|
/**
|
|
49
|
-
* @deprecated since 0.12.0, removal planned for
|
|
49
|
+
* @deprecated since 0.12.0, removal planned for a later release. Used only when
|
|
50
50
|
* buildTokenizer is absent. Loading by Hugging Face model id lets the
|
|
51
51
|
* host library probe mutable `resolve/main` and keep an unverified
|
|
52
52
|
* second cache (security report S1/S3); implement buildTokenizer.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @doccloak/core/pdf - text in a scan that its OCR layer does not have (T238).
|
|
3
|
+
*
|
|
4
|
+
* A scan page is redacted through its OCR text layer: what the layer does not
|
|
5
|
+
* hold (a table the OCR could not read, a stamp, a margin note) cannot be
|
|
6
|
+
* found, so it stays in the picture. This module finds such places so the
|
|
7
|
+
* reader can say so. It works on the rendered page: ink is grouped into
|
|
8
|
+
* letter-sized shapes, shapes that no OCR glyph box covers are grouped into
|
|
9
|
+
* lines, and a line of several such shapes is text the layer does not have.
|
|
10
|
+
* Rules, frames, signatures and pictures are larger than letters and never
|
|
11
|
+
* count; single marks (a page number, a tick) are too short to form a line.
|
|
12
|
+
*/
|
|
13
|
+
import type { GlyphRun } from './types.js';
|
|
14
|
+
/** Pixel box, top-left origin, inclusive start and exclusive end. */
|
|
15
|
+
export interface PxBox {
|
|
16
|
+
x0: number;
|
|
17
|
+
y0: number;
|
|
18
|
+
x1: number;
|
|
19
|
+
y1: number;
|
|
20
|
+
}
|
|
21
|
+
/** A line of text-like ink no OCR glyph covers. */
|
|
22
|
+
export interface MissingLine extends PxBox {
|
|
23
|
+
/** Letter-sized shapes on the line. */
|
|
24
|
+
shapes: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Lines of text-like ink in `rgba` (width x height, RGBA) that none of
|
|
28
|
+
* `ocrBoxes` covers. `emPx` is the page's typical OCR text size in pixels.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findTextWithoutOcr(rgba: Uint8ClampedArray, width: number, height: number, ocrBoxes: PxBox[], emPx: number): MissingLine[];
|
|
31
|
+
/**
|
|
32
|
+
* Pixel boxes of every OCR glyph on `page` (runs marked `ocr`; with `allText`, every glyph of the
|
|
33
|
+
* page's text layer), from 0.3 em below the baseline to 0.9 em above it, through the render
|
|
34
|
+
* transform [a b c d e f]; and the median glyph size in pixels (the page's text size, the scale
|
|
35
|
+
* every rule above is measured in).
|
|
36
|
+
*/
|
|
37
|
+
export declare function ocrGlyphBoxes(runs: GlyphRun[], page: number, transform: number[], allText?: boolean): {
|
|
38
|
+
boxes: PxBox[];
|
|
39
|
+
chars: string[];
|
|
40
|
+
emPx: number;
|
|
41
|
+
};
|