@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/dist/pdf/raster.d.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* canvas, after the render: the PNG never contains what was under a box.
|
|
7
7
|
* The caller (writer) replaces the page contents with this image and
|
|
8
8
|
* reports the page in `rasterizedPages`.
|
|
9
|
+
*
|
|
10
|
+
* Scan pages with an OCR text layer (T235) are rendered the same way at the
|
|
11
|
+
* scan's resolution, but each redacted value is covered in paper colour and
|
|
12
|
+
* its placeholder label is painted in its place, along the baseline and
|
|
13
|
+
* within the value's width (`rasterizeScanPage`).
|
|
9
14
|
*/
|
|
10
15
|
import type { CanvasFactory, OracleDocument } from './oracle.js';
|
|
11
16
|
import type { Rect } from './types.js';
|
|
@@ -43,3 +48,116 @@ export declare function padAndClamp(box: PixelBox, pad: number, widthPx: number,
|
|
|
43
48
|
* encode the canvas as PNG.
|
|
44
49
|
*/
|
|
45
50
|
export declare function rasterizePage(doc: OracleDocument, index: number, boxes: Rect[], options: RasterizeOptions): Promise<RasterizedPage>;
|
|
51
|
+
/**
|
|
52
|
+
* A value to paint over on a scan page, in user space: the baseline segment
|
|
53
|
+
* the value occupies and the label to show there.
|
|
54
|
+
*/
|
|
55
|
+
export interface PaintMark {
|
|
56
|
+
/** Baseline point where the value starts. */
|
|
57
|
+
x: number;
|
|
58
|
+
y: number;
|
|
59
|
+
/** Unit vector along the baseline and unit vector "up" (the text's em direction). */
|
|
60
|
+
ux: number;
|
|
61
|
+
uy: number;
|
|
62
|
+
vx: number;
|
|
63
|
+
vy: number;
|
|
64
|
+
/** Length of the value along the baseline, user units. */
|
|
65
|
+
length: number;
|
|
66
|
+
/**
|
|
67
|
+
* How far along the baseline the value's letters may still reach (an OCR show stretched over its
|
|
68
|
+
* word box): ink between `length` and here is covered even across a gap. Absent: `length`.
|
|
69
|
+
*/
|
|
70
|
+
reach?: number;
|
|
71
|
+
/**
|
|
72
|
+
* The value starts or ends inside a word of the OCR layer ("2026" of "2026/03/017"): on that side
|
|
73
|
+
* the cover grows only by IN_WORD_GROWTH, so the rest of the word stays readable.
|
|
74
|
+
*/
|
|
75
|
+
startsInWord?: boolean;
|
|
76
|
+
endsInWord?: boolean;
|
|
77
|
+
/** Font size of the value's text, user units. */
|
|
78
|
+
size: number;
|
|
79
|
+
/**
|
|
80
|
+
* Placeholder to show from the cover's start, with any punctuation glued to the value that the
|
|
81
|
+
* cover takes in ("[PHONE_1],"); on the rest of a value wrapped onto the next line only the
|
|
82
|
+
* punctuation glued before it, often '' (the cover alone).
|
|
83
|
+
*/
|
|
84
|
+
label: string;
|
|
85
|
+
/** On the rest of a wrapped value: the punctuation glued after it, painted up to the cover's end, where it was printed. */
|
|
86
|
+
labelEnd?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The text column along this mark's baseline (user units from its start): a label growing into
|
|
89
|
+
* blank paper beside a short value stays between the two. Absent: no room is taken.
|
|
90
|
+
*/
|
|
91
|
+
columnStart?: number;
|
|
92
|
+
columnEnd?: number;
|
|
93
|
+
}
|
|
94
|
+
/** A rendered scan page: raw pixels, 1 (gray) or 3 (RGB) bytes each, row-major. */
|
|
95
|
+
export interface ScanRaster {
|
|
96
|
+
pixels: Uint8Array;
|
|
97
|
+
channels: 1 | 3;
|
|
98
|
+
widthPx: number;
|
|
99
|
+
heightPx: number;
|
|
100
|
+
widthPt: number;
|
|
101
|
+
heightPt: number;
|
|
102
|
+
/** pdf.js viewport transform of the render: source user space to these pixels. */
|
|
103
|
+
transform: number[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Render page `index` at `options.dpi`, cover every mark in the paper colour
|
|
107
|
+
* around it and paint its label in the value's place: the baseline the OCR
|
|
108
|
+
* layer gives, the value's width (the label is squeezed, then shrunk, to fit;
|
|
109
|
+
* a shorter label leaves the rest of the width blank). Every cover is painted
|
|
110
|
+
* before any label, so a cover on the next line never clips a label above it.
|
|
111
|
+
* Returns the pixels, in gray when the page is gray (a scan usually is), so
|
|
112
|
+
* the writer can embed them losslessly at a size close to the original scan.
|
|
113
|
+
*/
|
|
114
|
+
export declare function rasterizeScanPage(doc: OracleDocument, index: number, marks: PaintMark[], options: RasterizeOptions): Promise<ScanRaster>;
|
|
115
|
+
/**
|
|
116
|
+
* Vertical extent of the value's ink (user units from the baseline): the block of inked rows
|
|
117
|
+
* across [start, end] that holds the letters' bodies, grown up and down until a blank row (the
|
|
118
|
+
* leading between lines), within INK_REACH_BELOW / INK_REACH_ABOVE. Null when no ink is there.
|
|
119
|
+
*/
|
|
120
|
+
export declare function inkRows(rgba: Uint8ClampedArray, width: number, height: number, at: (along: number, up: number) => [number, number], m: PaintMark, start: number, end: number, inkBelow: number): {
|
|
121
|
+
bottom: number;
|
|
122
|
+
top: number;
|
|
123
|
+
} | null;
|
|
124
|
+
/**
|
|
125
|
+
* Blank paper from `from` along a mark's baseline (user units; `dir` 1 after it, -1 before it), up
|
|
126
|
+
* to `want`: the run of columns with no ink from LABEL_ROOM_BELOW below the baseline to
|
|
127
|
+
* LABEL_ROOM_ABOVE above it (in `em`), ending LABEL_ROOM_GAP em before the next ink. The picture's
|
|
128
|
+
* edge ends it too. 0 without an ink threshold (no ink was measured, so blank cannot be told from print).
|
|
129
|
+
*/
|
|
130
|
+
export declare function blankAfter(rgba: Uint8ClampedArray, width: number, height: number, at: (along: number, up: number) => [number, number], from: number, want: number, em: number, inkBelow: number, dir?: 1 | -1): number;
|
|
131
|
+
/**
|
|
132
|
+
* Marks of ink standing apart above a value's letters, whose `top` is the letters' measured top
|
|
133
|
+
* and `em` their size (see DIACRITIC_*): each as its extent in the mark's frame (user units).
|
|
134
|
+
* The window above the letters is sampled pixel by pixel and split into 8-connected shapes; a
|
|
135
|
+
* shape that reaches the window's top or sides belongs to something else (the line above, the
|
|
136
|
+
* next word) and is left alone.
|
|
137
|
+
*/
|
|
138
|
+
export declare function diacriticsAbove(rgba: Uint8ClampedArray, width: number, height: number, at: (along: number, up: number) => [number, number], start: number, end: number, top: number, em: number, inkBelow: number): Array<{
|
|
139
|
+
start: number;
|
|
140
|
+
end: number;
|
|
141
|
+
top: number;
|
|
142
|
+
}>;
|
|
143
|
+
/**
|
|
144
|
+
* How far the ink of a value really reaches along its baseline (user units from the mark's
|
|
145
|
+
* start; `start` <= 0 and `end` >= length when the ink runs past the OCR layer's ends). Walks
|
|
146
|
+
* outwards pixel by pixel from inside the value and stops at the first blank gap of a word
|
|
147
|
+
* space, or after MAX_GROWTH em / MAX_GROWTH_SHARE of the value's length.
|
|
148
|
+
*/
|
|
149
|
+
export declare function inkExtent(rgba: Uint8ClampedArray, width: number, height: number, at: (along: number, up: number) => [number, number], m: PaintMark, inkBelow: number): {
|
|
150
|
+
start: number;
|
|
151
|
+
end: number;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The paper colour around a cover: per-channel median of the pixels on a ring
|
|
155
|
+
* three pixels outside the quad's bounding box (mostly background, whatever
|
|
156
|
+
* text the ring crosses is a minority). White when the ring is off canvas.
|
|
157
|
+
*/
|
|
158
|
+
export declare function paperColour(rgba: Uint8ClampedArray, width: number, height: number, quad: Array<[number, number]>): [number, number, number];
|
|
159
|
+
/** RGBA canvas pixels to gray (every pixel has R = G = B) or RGB bytes; alpha is dropped (pdf.js paints an opaque page). */
|
|
160
|
+
export declare function packPixels(rgba: Uint8ClampedArray): {
|
|
161
|
+
pixels: Uint8Array;
|
|
162
|
+
channels: 1 | 3;
|
|
163
|
+
};
|
package/dist/pdf/raster.js
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* canvas, after the render: the PNG never contains what was under a box.
|
|
7
7
|
* The caller (writer) replaces the page contents with this image and
|
|
8
8
|
* reports the page in `rasterizedPages`.
|
|
9
|
+
*
|
|
10
|
+
* Scan pages with an OCR text layer (T235) are rendered the same way at the
|
|
11
|
+
* scan's resolution, but each redacted value is covered in paper colour and
|
|
12
|
+
* its placeholder label is painted in its place, along the baseline and
|
|
13
|
+
* within the value's width (`rasterizeScanPage`).
|
|
9
14
|
*/
|
|
10
15
|
/**
|
|
11
16
|
* Map a user-space rectangle to canvas pixels through the pdf.js viewport
|
|
@@ -87,3 +92,523 @@ export async function rasterizePage(doc, index, boxes, options) {
|
|
|
87
92
|
heightPt: page.heightPt,
|
|
88
93
|
};
|
|
89
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Cover extent around a value, in em of its font size: along the baseline, and below and above
|
|
97
|
+
* it when the picture shows no ink to measure. The OCR layer's font size is only a hint (layers
|
|
98
|
+
* often stretch it to the line height), so the cover's height and the label's size come from
|
|
99
|
+
* the ink itself (see inkRows), within INK_REACH_BELOW / INK_REACH_ABOVE of the baseline; the
|
|
100
|
+
* cover always holds the core band COVER_CORE_BELOW..COVER_CORE_ABOVE.
|
|
101
|
+
*/
|
|
102
|
+
const COVER_PAD_ALONG = 0.12;
|
|
103
|
+
const COVER_BELOW = 0.32;
|
|
104
|
+
const COVER_ABOVE = 0.92;
|
|
105
|
+
const COVER_CORE_BELOW = 0.15;
|
|
106
|
+
const COVER_CORE_ABOVE = 0.6;
|
|
107
|
+
const INK_REACH_BELOW = 0.45;
|
|
108
|
+
const INK_REACH_ABOVE = 1.1;
|
|
109
|
+
/**
|
|
110
|
+
* OCR layers place words well but size letters from their own font, so a value's ink can run
|
|
111
|
+
* past the layer's ends, by a share of the value's length (a layer 15 % narrower than the print
|
|
112
|
+
* ends 2 em short of a 13 em e-mail address). A cover grows along the baseline to the end of the
|
|
113
|
+
* ink it touches: the search starts this far inside the value, stops at a blank gap of WORD_GAP
|
|
114
|
+
* (a word space is about 0.28 em, gaps between letters stay under about 0.12 em) and never grows
|
|
115
|
+
* by more than MAX_GROWTH em or MAX_GROWTH_SHARE of the value's length, whichever is more; ink is
|
|
116
|
+
* looked for in the band of the letters' bodies only, so the lines above and below do not count.
|
|
117
|
+
* In em of the value's font size unless said otherwise.
|
|
118
|
+
*/
|
|
119
|
+
const INK_SEARCH_INSIDE = 0.25;
|
|
120
|
+
const WORD_GAP = 0.16;
|
|
121
|
+
/**
|
|
122
|
+
* On a side where the value continues as the same word (its letters placed by real proportions),
|
|
123
|
+
* the cover ends in the blank valley between two letters nearest the estimate on the side away
|
|
124
|
+
* from the value, searched up to IN_WORD_SEARCH outwards and IN_WORD_INSIDE inwards: OCR letter
|
|
125
|
+
* boxes can sit half a letter off ("d'ALTAMIR" read with the "A" starting in its middle), and a
|
|
126
|
+
* cover that takes one letter too many keeps the value hidden where one letter too few shows it.
|
|
127
|
+
* Without a valley (touching letters) it ends IN_WORD_GROWTH past the estimate.
|
|
128
|
+
*/
|
|
129
|
+
const IN_WORD_SEARCH = 0.8;
|
|
130
|
+
const IN_WORD_INSIDE = 0.05;
|
|
131
|
+
const IN_WORD_GROWTH = 0.06;
|
|
132
|
+
const MAX_GROWTH = 1;
|
|
133
|
+
const MAX_GROWTH_SHARE = 0.35;
|
|
134
|
+
const INK_BAND_BELOW = 0.12;
|
|
135
|
+
const INK_BAND_ABOVE = 0.68;
|
|
136
|
+
/** A pixel is ink when its luminance is below this fraction of the paper's. */
|
|
137
|
+
const INK_LUMINANCE = 0.65;
|
|
138
|
+
/**
|
|
139
|
+
* Label size: the value's ink height (capitals and ascenders reach about 0.72 em) gives the
|
|
140
|
+
* label's em; without ink the OCR font size does, times LABEL_SIZE. The label is squeezed
|
|
141
|
+
* horizontally down to LABEL_MIN_SQUEEZE to fit the value's width, then shrunk.
|
|
142
|
+
*/
|
|
143
|
+
const INK_TOP_EM = 0.76;
|
|
144
|
+
/**
|
|
145
|
+
* Marks standing apart above the letters (the dot of "Ż", the accent of "ó" over a word with no
|
|
146
|
+
* ascender, the dot of an "i"): a shape of ink within DIACRITIC_GAP of the letters' top, no taller
|
|
147
|
+
* than DIACRITIC_HEIGHT and no wider than DIACRITIC_WIDTH, that does not run on past
|
|
148
|
+
* DIACRITIC_WINDOW above that top (the line above's descenders do) or out of the cover's sides,
|
|
149
|
+
* gets a cover of its own. In em of the letters' measured size (their top is INK_TOP_EM of it).
|
|
150
|
+
*/
|
|
151
|
+
const DIACRITIC_GAP = 0.2;
|
|
152
|
+
const DIACRITIC_HEIGHT = 0.3;
|
|
153
|
+
const DIACRITIC_WIDTH = 0.6;
|
|
154
|
+
const DIACRITIC_WINDOW = 0.5;
|
|
155
|
+
const LABEL_SIZE = 0.8;
|
|
156
|
+
const LABEL_MIN_SQUEEZE = 0.6;
|
|
157
|
+
/**
|
|
158
|
+
* A label longer than its value's width first takes the blank paper after the value (a line end,
|
|
159
|
+
* a wide gap), then before it (a right-aligned line), up to LABEL_ROOM_GAP em before the next ink
|
|
160
|
+
* and inside the text column (PaintMark.columnStart / columnEnd), so a short value's placeholder
|
|
161
|
+
* ("ul.", "557") stays readable; only then is it squeezed and shrunk. The paper must be blank from
|
|
162
|
+
* LABEL_ROOM_BELOW below the baseline to LABEL_ROOM_ABOVE above it (brackets reach that far).
|
|
163
|
+
*/
|
|
164
|
+
const LABEL_ROOM_GAP = 0.35;
|
|
165
|
+
const LABEL_ROOM_BELOW = 0.25;
|
|
166
|
+
const LABEL_ROOM_ABOVE = 0.85;
|
|
167
|
+
const LABEL_FONT = '"Liberation Sans", Arial, Helvetica, sans-serif';
|
|
168
|
+
/**
|
|
169
|
+
* Render page `index` at `options.dpi`, cover every mark in the paper colour
|
|
170
|
+
* around it and paint its label in the value's place: the baseline the OCR
|
|
171
|
+
* layer gives, the value's width (the label is squeezed, then shrunk, to fit;
|
|
172
|
+
* a shorter label leaves the rest of the width blank). Every cover is painted
|
|
173
|
+
* before any label, so a cover on the next line never clips a label above it.
|
|
174
|
+
* Returns the pixels, in gray when the page is gray (a scan usually is), so
|
|
175
|
+
* the writer can embed them losslessly at a size close to the original scan.
|
|
176
|
+
*/
|
|
177
|
+
export async function rasterizeScanPage(doc, index, marks, options) {
|
|
178
|
+
if (!(options.dpi > 0) || !Number.isFinite(options.dpi))
|
|
179
|
+
throw new RangeError(`invalid dpi ${options.dpi}`);
|
|
180
|
+
const page = await doc.render(index, options.dpi / 72, options.canvasFactory);
|
|
181
|
+
const ctx = page.canvas.context;
|
|
182
|
+
const before = page.canvas.getImageData();
|
|
183
|
+
const [a, b, c, d, e, f] = page.transform;
|
|
184
|
+
const toPx = (x, y) => [a * x + c * y + e, b * x + d * y + f];
|
|
185
|
+
// Measure every cover on the untouched pixels first.
|
|
186
|
+
const covers = marks.map((m) => {
|
|
187
|
+
const at = (along, up) => toPx(m.x + m.ux * along + m.vx * up, m.y + m.uy * along + m.vy * up);
|
|
188
|
+
const pad = COVER_PAD_ALONG * m.size;
|
|
189
|
+
const paper = paperColour(before, page.widthPx, page.heightPx, quadOf(at, -pad, m.length + pad, -COVER_BELOW * m.size, COVER_ABOVE * m.size));
|
|
190
|
+
const inkBelow = luminance(paper) * INK_LUMINANCE;
|
|
191
|
+
const along = inkExtent(before, page.widthPx, page.heightPx, at, m, inkBelow);
|
|
192
|
+
// Inside a word the cover stops in the valley between two letters: a pixel of margin, not the pad.
|
|
193
|
+
const onePxAlong = 1 / (Math.hypot(...sub(at(1, 0), at(0, 0))) || 1);
|
|
194
|
+
const start = (m.startsInWord ? along.start : Math.min(0, along.start)) - (m.startsInWord ? onePxAlong : pad);
|
|
195
|
+
const end = (m.endsInWord ? along.end : Math.max(m.length, along.end)) + (m.endsInWord ? onePxAlong : pad);
|
|
196
|
+
const rows = inkRows(before, page.widthPx, page.heightPx, at, m, start, end, inkBelow);
|
|
197
|
+
const onePx = 1 / (Math.hypot(...sub(at(0, 1), at(0, 0))) || 1);
|
|
198
|
+
const bottom = rows ? Math.min(-COVER_CORE_BELOW * m.size, rows.bottom - 1.5 * onePx) : -COVER_BELOW * m.size;
|
|
199
|
+
const top = rows ? Math.max(COVER_CORE_ABOVE * m.size, rows.top + 1.5 * onePx) : COVER_ABOVE * m.size;
|
|
200
|
+
const ink = rows ? inkColour(before, page.widthPx, page.heightPx, at, start, end, rows.bottom, rows.top, inkBelow) : null;
|
|
201
|
+
const marksAbove = rows && rows.top > 0 ? diacriticsAbove(before, page.widthPx, page.heightPx, at, start, end, rows.top, rows.top / INK_TOP_EM, inkBelow) : [];
|
|
202
|
+
const marks = marksAbove.map((d) => ({ start: d.start - 1.5 * onePxAlong, end: d.end + 1.5 * onePxAlong, bottom: top - onePx, top: d.top + 1.5 * onePx }));
|
|
203
|
+
return { mark: m, at, start, end, bottom, top, inkTop: rows ? rows.top : null, paper, ink, marks };
|
|
204
|
+
});
|
|
205
|
+
ctx.save();
|
|
206
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
207
|
+
ctx.globalAlpha = 1;
|
|
208
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
209
|
+
for (const cv of covers) {
|
|
210
|
+
const fill = `rgb(${cv.paper[0]}, ${cv.paper[1]}, ${cv.paper[2]})`;
|
|
211
|
+
ctx.fillStyle = fill;
|
|
212
|
+
ctx.strokeStyle = fill;
|
|
213
|
+
ctx.lineWidth = 2; // the stroke grows the cover by a pixel on every side (anti-aliased glyph edges)
|
|
214
|
+
for (const box of [cv, ...cv.marks]) {
|
|
215
|
+
const quad = quadOf(cv.at, box.start, box.end, box.bottom, box.top);
|
|
216
|
+
ctx.beginPath();
|
|
217
|
+
ctx.moveTo(quad[0][0], quad[0][1]);
|
|
218
|
+
for (let i = 1; i < 4; i++)
|
|
219
|
+
ctx.lineTo(quad[i][0], quad[i][1]);
|
|
220
|
+
ctx.closePath();
|
|
221
|
+
ctx.fill();
|
|
222
|
+
ctx.stroke();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// How far each label may reach past its cover into blank paper: half the gap to the next (and
|
|
226
|
+
// the previous) cover on the same line, so two neighbours never claim the same room.
|
|
227
|
+
const reach = covers.map((a) => {
|
|
228
|
+
let right = Infinity;
|
|
229
|
+
let left = Infinity;
|
|
230
|
+
const am = a.mark;
|
|
231
|
+
for (const b of covers) {
|
|
232
|
+
if (b === a)
|
|
233
|
+
continue;
|
|
234
|
+
const bm = b.mark;
|
|
235
|
+
const across = (bm.x - am.x) * am.vx + (bm.y - am.y) * am.vy;
|
|
236
|
+
if (Math.abs(across) > 0.5 * Math.max(am.size, bm.size) || am.ux * bm.ux + am.uy * bm.uy < 0.99)
|
|
237
|
+
continue;
|
|
238
|
+
const along = (t) => (bm.x + bm.ux * t - am.x) * am.ux + (bm.y + bm.uy * t - am.y) * am.uy;
|
|
239
|
+
const bs = along(b.start);
|
|
240
|
+
const be = along(b.end);
|
|
241
|
+
if (bs >= a.end)
|
|
242
|
+
right = Math.min(right, (bs - a.end) / 2);
|
|
243
|
+
else if (be <= a.start)
|
|
244
|
+
left = Math.min(left, (a.start - be) / 2);
|
|
245
|
+
}
|
|
246
|
+
return { right, left };
|
|
247
|
+
});
|
|
248
|
+
covers.forEach((cv, k) => {
|
|
249
|
+
const m = cv.mark;
|
|
250
|
+
// The label from the cover's start; the punctuation of a wrapped value's rest up to its end.
|
|
251
|
+
for (const [text, alignEnd] of [[m.label, false], [m.labelEnd ?? '', true]]) {
|
|
252
|
+
if (!text)
|
|
253
|
+
continue;
|
|
254
|
+
const pad = COVER_PAD_ALONG * m.size;
|
|
255
|
+
let from = alignEnd ? cv.end - pad : cv.start + pad;
|
|
256
|
+
const [ox, oy] = cv.at(from, 0);
|
|
257
|
+
const [ax, ay] = cv.at(from + 1, 0);
|
|
258
|
+
const [ux, uy] = cv.at(from, 1);
|
|
259
|
+
const pxPerUnit = Math.hypot(ax - ox, ay - oy);
|
|
260
|
+
const pxPerUnitUp = Math.hypot(ux - ox, uy - oy);
|
|
261
|
+
let available = (cv.end - cv.start - 2 * pad) * pxPerUnit;
|
|
262
|
+
const em = cv.inkTop !== null && cv.inkTop > 0 ? cv.inkTop / INK_TOP_EM : LABEL_SIZE * m.size;
|
|
263
|
+
let fontPx = Math.min(em, 1.2 * m.size) * pxPerUnitUp;
|
|
264
|
+
if (!(fontPx > 0) || !(available > 0))
|
|
265
|
+
continue;
|
|
266
|
+
ctx.font = `${fontPx}px ${LABEL_FONT}`;
|
|
267
|
+
const natural = ctx.measureText(text).width;
|
|
268
|
+
if (!alignEnd && natural > available) {
|
|
269
|
+
// Blank paper after the value first, then before it (a right-aligned line, a line start).
|
|
270
|
+
const inkBelow = cv.ink !== null ? luminance(cv.paper) * INK_LUMINANCE : 0;
|
|
271
|
+
// Never past the text column: no room without it.
|
|
272
|
+
const columnRight = Math.max(0, (m.columnEnd ?? -Infinity) - (cv.end - pad));
|
|
273
|
+
const columnLeft = Math.max(0, (cv.start + pad) - (m.columnStart ?? Infinity));
|
|
274
|
+
const after = blankAfter(before, page.widthPx, page.heightPx, cv.at, cv.end - pad, Math.min((natural - available) / pxPerUnit, reach[k].right + pad, columnRight), em, inkBelow);
|
|
275
|
+
available += after * pxPerUnit;
|
|
276
|
+
if (natural > available) {
|
|
277
|
+
const beforeRoom = blankAfter(before, page.widthPx, page.heightPx, cv.at, cv.start + pad, Math.min((natural - available) / pxPerUnit, reach[k].left + pad, columnLeft), em, inkBelow, -1);
|
|
278
|
+
available += beforeRoom * pxPerUnit;
|
|
279
|
+
from -= beforeRoom;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const [x0, y0] = cv.at(from, 0);
|
|
283
|
+
const [x1, y1] = cv.at(from + 1, 0);
|
|
284
|
+
let squeeze = natural > available ? available / natural : 1;
|
|
285
|
+
if (squeeze < LABEL_MIN_SQUEEZE) {
|
|
286
|
+
fontPx *= squeeze / LABEL_MIN_SQUEEZE;
|
|
287
|
+
squeeze = LABEL_MIN_SQUEEZE;
|
|
288
|
+
ctx.font = `${fontPx}px ${LABEL_FONT}`;
|
|
289
|
+
}
|
|
290
|
+
ctx.save();
|
|
291
|
+
ctx.translate(x0, y0);
|
|
292
|
+
ctx.rotate(Math.atan2(y1 - y0, x1 - x0));
|
|
293
|
+
ctx.scale(squeeze, 1);
|
|
294
|
+
ctx.fillStyle = cv.ink !== null ? `rgb(${cv.ink}, ${cv.ink}, ${cv.ink})` : luminance(cv.paper) > 0.45 ? '#000000' : '#ffffff';
|
|
295
|
+
ctx.textAlign = alignEnd ? 'right' : 'left';
|
|
296
|
+
ctx.textBaseline = 'alphabetic';
|
|
297
|
+
ctx.fillText(text, 0, 0);
|
|
298
|
+
ctx.restore();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
ctx.restore();
|
|
302
|
+
const rgba = page.canvas.getImageData();
|
|
303
|
+
const { pixels, channels } = packPixels(rgba);
|
|
304
|
+
return { pixels, channels, widthPx: page.widthPx, heightPx: page.heightPx, widthPt: page.widthPt, heightPt: page.heightPt, transform: page.transform };
|
|
305
|
+
}
|
|
306
|
+
function sub(p, q) {
|
|
307
|
+
return [p[0] - q[0], p[1] - q[1]];
|
|
308
|
+
}
|
|
309
|
+
/** Pixel corners of the box [lo, hi] along the baseline by [bottom, top] across it. */
|
|
310
|
+
function quadOf(at, lo, hi, bottom, top) {
|
|
311
|
+
return [at(lo, bottom), at(hi, bottom), at(hi, top), at(lo, top)];
|
|
312
|
+
}
|
|
313
|
+
/** Luminance (0..1) of the pixel under a baseline-frame point, or null off canvas. */
|
|
314
|
+
function lumAt(rgba, width, height, p) {
|
|
315
|
+
const px = Math.round(p[0]);
|
|
316
|
+
const py = Math.round(p[1]);
|
|
317
|
+
if (px < 0 || py < 0 || px >= width || py >= height)
|
|
318
|
+
return null;
|
|
319
|
+
const i = (py * width + px) * 4;
|
|
320
|
+
return (0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2]) / 255;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Vertical extent of the value's ink (user units from the baseline): the block of inked rows
|
|
324
|
+
* across [start, end] that holds the letters' bodies, grown up and down until a blank row (the
|
|
325
|
+
* leading between lines), within INK_REACH_BELOW / INK_REACH_ABOVE. Null when no ink is there.
|
|
326
|
+
*/
|
|
327
|
+
export function inkRows(rgba, width, height, at, m, start, end, inkBelow) {
|
|
328
|
+
const pxAlong = Math.hypot(...sub(at(1, 0), at(0, 0)));
|
|
329
|
+
const pxUp = Math.hypot(...sub(at(0, 1), at(0, 0)));
|
|
330
|
+
if (!(pxAlong > 0) || !(pxUp > 0))
|
|
331
|
+
return null;
|
|
332
|
+
const stepAlong = 1 / pxAlong;
|
|
333
|
+
const stepUp = 1 / pxUp;
|
|
334
|
+
const rowHasInk = (up) => {
|
|
335
|
+
for (let along = start; along <= end; along += stepAlong) {
|
|
336
|
+
const lum = lumAt(rgba, width, height, at(along, up));
|
|
337
|
+
if (lum !== null && lum < inkBelow)
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
};
|
|
342
|
+
// Seed in the letters' bodies (every letter has ink between the baseline and the x-height).
|
|
343
|
+
let seed = null;
|
|
344
|
+
for (let up = 0.1 * m.size; up <= 0.45 * m.size; up += stepUp) {
|
|
345
|
+
if (rowHasInk(up)) {
|
|
346
|
+
seed = up;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (seed === null)
|
|
351
|
+
return null;
|
|
352
|
+
let top = seed;
|
|
353
|
+
for (let up = seed; up <= INK_REACH_ABOVE * m.size && rowHasInk(up); up += stepUp)
|
|
354
|
+
top = up;
|
|
355
|
+
let bottom = seed;
|
|
356
|
+
for (let up = seed; up >= -INK_REACH_BELOW * m.size && rowHasInk(up); up -= stepUp)
|
|
357
|
+
bottom = up;
|
|
358
|
+
return { bottom, top };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Blank paper from `from` along a mark's baseline (user units; `dir` 1 after it, -1 before it), up
|
|
362
|
+
* to `want`: the run of columns with no ink from LABEL_ROOM_BELOW below the baseline to
|
|
363
|
+
* LABEL_ROOM_ABOVE above it (in `em`), ending LABEL_ROOM_GAP em before the next ink. The picture's
|
|
364
|
+
* edge ends it too. 0 without an ink threshold (no ink was measured, so blank cannot be told from print).
|
|
365
|
+
*/
|
|
366
|
+
export function blankAfter(rgba, width, height, at, from, want, em, inkBelow, dir = 1) {
|
|
367
|
+
if (!(want > 0) || !(em > 0) || !(inkBelow > 0))
|
|
368
|
+
return 0;
|
|
369
|
+
const pxAlong = Math.hypot(...sub(at(1, 0), at(0, 0)));
|
|
370
|
+
const pxUp = Math.hypot(...sub(at(0, 1), at(0, 0)));
|
|
371
|
+
if (!(pxAlong > 0) || !(pxUp > 0))
|
|
372
|
+
return 0;
|
|
373
|
+
const stepAlong = 1 / pxAlong;
|
|
374
|
+
const stepUp = 1 / pxUp;
|
|
375
|
+
const gap = LABEL_ROOM_GAP * em;
|
|
376
|
+
for (let d = 0; d <= want + gap; d += stepAlong) {
|
|
377
|
+
for (let up = -LABEL_ROOM_BELOW * em; up <= LABEL_ROOM_ABOVE * em; up += stepUp) {
|
|
378
|
+
const lum = lumAt(rgba, width, height, at(from + dir * d, up));
|
|
379
|
+
if (lum === null || lum < inkBelow)
|
|
380
|
+
return Math.max(0, Math.min(want, d - gap));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return want;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Marks of ink standing apart above a value's letters, whose `top` is the letters' measured top
|
|
387
|
+
* and `em` their size (see DIACRITIC_*): each as its extent in the mark's frame (user units).
|
|
388
|
+
* The window above the letters is sampled pixel by pixel and split into 8-connected shapes; a
|
|
389
|
+
* shape that reaches the window's top or sides belongs to something else (the line above, the
|
|
390
|
+
* next word) and is left alone.
|
|
391
|
+
*/
|
|
392
|
+
export function diacriticsAbove(rgba, width, height, at, start, end, top, em, inkBelow) {
|
|
393
|
+
const pxAlong = Math.hypot(...sub(at(1, 0), at(0, 0)));
|
|
394
|
+
const pxUp = Math.hypot(...sub(at(0, 1), at(0, 0)));
|
|
395
|
+
if (!(pxAlong > 0) || !(pxUp > 0) || !(em > 0) || !(end > start))
|
|
396
|
+
return [];
|
|
397
|
+
const stepAlong = 1 / pxAlong;
|
|
398
|
+
const stepUp = 1 / pxUp;
|
|
399
|
+
const cols = Math.min(4000, Math.ceil((end - start) / stepAlong) + 1);
|
|
400
|
+
const rows = Math.min(400, Math.ceil((DIACRITIC_WINDOW * em) / stepUp));
|
|
401
|
+
if (rows < 2 || cols < 2)
|
|
402
|
+
return [];
|
|
403
|
+
const ink = new Uint8Array(cols * rows);
|
|
404
|
+
for (let j = 0; j < rows; j++) {
|
|
405
|
+
for (let i = 0; i < cols; i++) {
|
|
406
|
+
const lum = lumAt(rgba, width, height, at(start + i * stepAlong, top + (j + 1) * stepUp));
|
|
407
|
+
if (lum !== null && lum < inkBelow)
|
|
408
|
+
ink[j * cols + i] = 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const seen = new Uint8Array(cols * rows);
|
|
412
|
+
const out = [];
|
|
413
|
+
const stack = [];
|
|
414
|
+
for (let p0 = 0; p0 < ink.length; p0++) {
|
|
415
|
+
if (!ink[p0] || seen[p0])
|
|
416
|
+
continue;
|
|
417
|
+
seen[p0] = 1;
|
|
418
|
+
stack.push(p0);
|
|
419
|
+
let i0 = cols, i1 = 0, j0 = rows, j1 = 0;
|
|
420
|
+
while (stack.length > 0) {
|
|
421
|
+
const p = stack.pop();
|
|
422
|
+
const i = p % cols;
|
|
423
|
+
const j = (p - i) / cols;
|
|
424
|
+
if (i < i0)
|
|
425
|
+
i0 = i;
|
|
426
|
+
if (i > i1)
|
|
427
|
+
i1 = i;
|
|
428
|
+
if (j < j0)
|
|
429
|
+
j0 = j;
|
|
430
|
+
if (j > j1)
|
|
431
|
+
j1 = j;
|
|
432
|
+
for (let dj = -1; dj <= 1; dj++) {
|
|
433
|
+
for (let di = -1; di <= 1; di++) {
|
|
434
|
+
const ni = i + di;
|
|
435
|
+
const nj = j + dj;
|
|
436
|
+
if (ni < 0 || nj < 0 || ni >= cols || nj >= rows)
|
|
437
|
+
continue;
|
|
438
|
+
const q = nj * cols + ni;
|
|
439
|
+
if (ink[q] && !seen[q]) {
|
|
440
|
+
seen[q] = 1;
|
|
441
|
+
stack.push(q);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (j1 === rows - 1 || i0 === 0 || i1 === cols - 1)
|
|
447
|
+
continue;
|
|
448
|
+
if ((j0 + 1) * stepUp > DIACRITIC_GAP * em)
|
|
449
|
+
continue;
|
|
450
|
+
if ((j1 - j0 + 1) * stepUp > DIACRITIC_HEIGHT * em || (i1 - i0 + 1) * stepAlong > DIACRITIC_WIDTH * em)
|
|
451
|
+
continue;
|
|
452
|
+
out.push({ start: start + i0 * stepAlong, end: start + (i1 + 1) * stepAlong, top: top + (j1 + 1) * stepUp });
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* How far the ink of a value really reaches along its baseline (user units from the mark's
|
|
458
|
+
* start; `start` <= 0 and `end` >= length when the ink runs past the OCR layer's ends). Walks
|
|
459
|
+
* outwards pixel by pixel from inside the value and stops at the first blank gap of a word
|
|
460
|
+
* space, or after MAX_GROWTH em / MAX_GROWTH_SHARE of the value's length.
|
|
461
|
+
*/
|
|
462
|
+
export function inkExtent(rgba, width, height, at, m, inkBelow) {
|
|
463
|
+
const [ox, oy] = at(0, 0);
|
|
464
|
+
const [ax, ay] = at(1, 0);
|
|
465
|
+
const pxPerUnit = Math.hypot(ax - ox, ay - oy);
|
|
466
|
+
if (!(pxPerUnit > 0))
|
|
467
|
+
return { start: 0, end: m.length };
|
|
468
|
+
const step = 1 / pxPerUnit; // one pixel along the baseline, in user units
|
|
469
|
+
const [bx, by] = at(0, INK_BAND_BELOW * -m.size);
|
|
470
|
+
const [tx, ty] = at(0, INK_BAND_ABOVE * m.size);
|
|
471
|
+
const bandPx = Math.max(1, Math.ceil(Math.hypot(tx - bx, ty - by)));
|
|
472
|
+
const isInk = (along) => {
|
|
473
|
+
for (let k = 0; k <= bandPx; k++) {
|
|
474
|
+
const up = -INK_BAND_BELOW * m.size + (k / bandPx) * (INK_BAND_ABOVE + INK_BAND_BELOW) * m.size;
|
|
475
|
+
const lum = lumAt(rgba, width, height, at(along, up));
|
|
476
|
+
if (lum !== null && lum < inkBelow)
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
return false;
|
|
480
|
+
};
|
|
481
|
+
const inside = Math.min(INK_SEARCH_INSIDE * m.size, m.length / 2);
|
|
482
|
+
const gap = WORD_GAP * m.size;
|
|
483
|
+
const growth = Math.max(MAX_GROWTH * m.size, MAX_GROWTH_SHARE * m.length);
|
|
484
|
+
// Ink up to `reach` belongs to the value whatever the gaps: the search for the end starts there.
|
|
485
|
+
let end = m.length;
|
|
486
|
+
if (m.reach !== undefined && m.reach > m.length) {
|
|
487
|
+
for (let along = m.length; along <= m.reach; along += step)
|
|
488
|
+
if (isInk(along))
|
|
489
|
+
end = along + step;
|
|
490
|
+
}
|
|
491
|
+
const reach = (from, dir, limit) => {
|
|
492
|
+
let last = from;
|
|
493
|
+
for (let along = from; dir > 0 ? along <= limit : along >= limit; along += dir * step) {
|
|
494
|
+
if (isInk(along))
|
|
495
|
+
last = along;
|
|
496
|
+
else if (Math.abs(along - last) >= gap)
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
return last + dir * step;
|
|
500
|
+
};
|
|
501
|
+
// The blank column run nearest `at` on the `outward` side (-1 before the value, 1 after it): the
|
|
502
|
+
// valley between two letters.
|
|
503
|
+
const valley = (at, outward) => {
|
|
504
|
+
const lo = outward < 0 ? at - IN_WORD_SEARCH * m.size : at - IN_WORD_INSIDE * m.size;
|
|
505
|
+
const hi = outward < 0 ? at + IN_WORD_INSIDE * m.size : at + IN_WORD_SEARCH * m.size;
|
|
506
|
+
let best = null;
|
|
507
|
+
let runStart = null;
|
|
508
|
+
for (let along = lo; along <= hi + step; along += step) {
|
|
509
|
+
if (!isInk(along) && along <= hi) {
|
|
510
|
+
if (runStart === null)
|
|
511
|
+
runStart = along;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (runStart !== null) {
|
|
515
|
+
const centre = (runStart + along - step) / 2;
|
|
516
|
+
if (best === null || Math.abs(centre - at) < Math.abs(best - at))
|
|
517
|
+
best = centre;
|
|
518
|
+
runStart = null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return best;
|
|
522
|
+
};
|
|
523
|
+
const startAt = m.startsInWord ? (valley(0, -1) ?? -IN_WORD_GROWTH * m.size) : Math.min(0, reach(inside, -1, -growth));
|
|
524
|
+
const endAt = m.endsInWord ? (valley(m.length, 1) ?? m.length + IN_WORD_GROWTH * m.size) : Math.max(end, reach(end - inside, 1, end + growth));
|
|
525
|
+
return { start: startAt, end: endAt };
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Gray level (0..255) of the ink in the box [start, end] x [bottom, top]: the median luminance of
|
|
529
|
+
* its ink pixels, without their hue (a label never repeats the colour a value was printed in).
|
|
530
|
+
* Null without ink.
|
|
531
|
+
*/
|
|
532
|
+
function inkColour(rgba, width, height, at, start, end, bottom, top, inkBelow) {
|
|
533
|
+
const stepAlong = 1 / (Math.hypot(...sub(at(1, 0), at(0, 0))) || 1);
|
|
534
|
+
const stepUp = 1 / (Math.hypot(...sub(at(0, 1), at(0, 0))) || 1);
|
|
535
|
+
const levels = [];
|
|
536
|
+
for (let up = bottom; up <= top; up += stepUp) {
|
|
537
|
+
for (let along = start; along <= end; along += stepAlong) {
|
|
538
|
+
const [x, y] = at(along, up);
|
|
539
|
+
const px = Math.round(x);
|
|
540
|
+
const py = Math.round(y);
|
|
541
|
+
if (px < 0 || py < 0 || px >= width || py >= height)
|
|
542
|
+
continue;
|
|
543
|
+
const i = (py * width + px) * 4;
|
|
544
|
+
const level = 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
545
|
+
if (level / 255 < inkBelow)
|
|
546
|
+
levels.push(level);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (levels.length === 0)
|
|
550
|
+
return null;
|
|
551
|
+
return Math.round(levels.sort((p, q) => p - q)[levels.length >> 1]);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The paper colour around a cover: per-channel median of the pixels on a ring
|
|
555
|
+
* three pixels outside the quad's bounding box (mostly background, whatever
|
|
556
|
+
* text the ring crosses is a minority). White when the ring is off canvas.
|
|
557
|
+
*/
|
|
558
|
+
export function paperColour(rgba, width, height, quad) {
|
|
559
|
+
const xs = quad.map((p) => p[0]);
|
|
560
|
+
const ys = quad.map((p) => p[1]);
|
|
561
|
+
const left = Math.floor(Math.min(...xs)) - 3;
|
|
562
|
+
const right = Math.ceil(Math.max(...xs)) + 3;
|
|
563
|
+
const top = Math.floor(Math.min(...ys)) - 3;
|
|
564
|
+
const bottom = Math.ceil(Math.max(...ys)) + 3;
|
|
565
|
+
const channels = [[], [], []];
|
|
566
|
+
const take = (x, y) => {
|
|
567
|
+
if (x < 0 || y < 0 || x >= width || y >= height)
|
|
568
|
+
return;
|
|
569
|
+
const i = (y * width + x) * 4;
|
|
570
|
+
channels[0].push(rgba[i]);
|
|
571
|
+
channels[1].push(rgba[i + 1]);
|
|
572
|
+
channels[2].push(rgba[i + 2]);
|
|
573
|
+
};
|
|
574
|
+
for (let x = left; x <= right; x++) {
|
|
575
|
+
take(x, top);
|
|
576
|
+
take(x, bottom);
|
|
577
|
+
}
|
|
578
|
+
for (let y = top + 1; y < bottom; y++) {
|
|
579
|
+
take(left, y);
|
|
580
|
+
take(right, y);
|
|
581
|
+
}
|
|
582
|
+
if (channels[0].length === 0)
|
|
583
|
+
return [255, 255, 255];
|
|
584
|
+
const median = (v) => v.sort((p, q) => p - q)[v.length >> 1];
|
|
585
|
+
return [median(channels[0]), median(channels[1]), median(channels[2])];
|
|
586
|
+
}
|
|
587
|
+
function luminance(rgb) {
|
|
588
|
+
return (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]) / 255;
|
|
589
|
+
}
|
|
590
|
+
/** RGBA canvas pixels to gray (every pixel has R = G = B) or RGB bytes; alpha is dropped (pdf.js paints an opaque page). */
|
|
591
|
+
export function packPixels(rgba) {
|
|
592
|
+
const n = rgba.length >> 2;
|
|
593
|
+
let gray = true;
|
|
594
|
+
for (let i = 0; i < n; i++) {
|
|
595
|
+
const r = rgba[i * 4];
|
|
596
|
+
if (r !== rgba[i * 4 + 1] || r !== rgba[i * 4 + 2]) {
|
|
597
|
+
gray = false;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (gray) {
|
|
602
|
+
const pixels = new Uint8Array(n);
|
|
603
|
+
for (let i = 0; i < n; i++)
|
|
604
|
+
pixels[i] = rgba[i * 4];
|
|
605
|
+
return { pixels, channels: 1 };
|
|
606
|
+
}
|
|
607
|
+
const pixels = new Uint8Array(n * 3);
|
|
608
|
+
for (let i = 0; i < n; i++) {
|
|
609
|
+
pixels[i * 3] = rgba[i * 4];
|
|
610
|
+
pixels[i * 3 + 1] = rgba[i * 4 + 1];
|
|
611
|
+
pixels[i * 3 + 2] = rgba[i * 4 + 2];
|
|
612
|
+
}
|
|
613
|
+
return { pixels, channels: 3 };
|
|
614
|
+
}
|
package/dist/pdf/read.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The extraction keeps the input bytes; the writer re-extracts from them so
|
|
7
7
|
* a stale extraction can never be applied to different bytes.
|
|
8
8
|
*/
|
|
9
|
-
import type { GlyphRun, PdfAssetPaths, PdfPageInfo, PdfReadOptions, RemovedPart } from './types.js';
|
|
9
|
+
import type { ExtraOcrPage, GlyphRun, PdfAssetPaths, PdfPageInfo, PdfReadOptions, RemovedPart } from './types.js';
|
|
10
10
|
import type { UnredactablePart } from '../dom/errors.js';
|
|
11
11
|
export interface PdfExtraction {
|
|
12
12
|
/** Flat text detection runs on. */
|
|
@@ -26,6 +26,8 @@ export interface PdfExtraction {
|
|
|
26
26
|
rasterOnlyPages: number[];
|
|
27
27
|
/** Input bytes (the writer's source of truth). */
|
|
28
28
|
bytes: Uint8Array;
|
|
29
|
+
/** Text DocCloak read with its own OCR where a scan's OCR layer had none (the writer re-creates its runs). */
|
|
30
|
+
extraOcr?: ExtraOcrPage[];
|
|
29
31
|
assets?: PdfAssetPaths;
|
|
30
32
|
password?: string;
|
|
31
33
|
}
|