@doccloak/core 0.12.1 → 0.14.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +386 -0
  2. package/NOTICE +5 -0
  3. package/README.md +148 -9
  4. package/dist/dom/errors.d.ts +1 -1
  5. package/dist/dom/ocr.d.ts +33 -7
  6. package/dist/dom/ocr.js +98 -4
  7. package/dist/dom/office.d.ts +22 -6
  8. package/dist/dom/office.js +45 -11
  9. package/dist/engine.js +4 -5
  10. package/dist/env.d.ts +3 -3
  11. package/dist/pdf/cmap.d.ts +83 -0
  12. package/dist/pdf/cmap.js +659 -0
  13. package/dist/pdf/encodings.d.ts +49 -0
  14. package/dist/pdf/encodings.js +181 -0
  15. package/dist/pdf/extract.d.ts +108 -0
  16. package/dist/pdf/extract.js +1164 -0
  17. package/dist/pdf/fallback-fonts.d.ts +146 -0
  18. package/dist/pdf/fallback-fonts.js +312 -0
  19. package/dist/pdf/fit.d.ts +35 -0
  20. package/dist/pdf/fit.js +60 -0
  21. package/dist/pdf/fonts.d.ts +82 -0
  22. package/dist/pdf/fonts.js +465 -0
  23. package/dist/pdf/glyphlist.d.ts +37 -0
  24. package/dist/pdf/glyphlist.js +485 -0
  25. package/dist/pdf/index.d.ts +21 -0
  26. package/dist/pdf/index.js +7 -0
  27. package/dist/pdf/lexer.d.ts +61 -0
  28. package/dist/pdf/lexer.js +554 -0
  29. package/dist/pdf/objects.d.ts +57 -0
  30. package/dist/pdf/objects.js +217 -0
  31. package/dist/pdf/oracle.d.ts +149 -0
  32. package/dist/pdf/oracle.js +384 -0
  33. package/dist/pdf/raster.d.ts +119 -0
  34. package/dist/pdf/raster.js +405 -0
  35. package/dist/pdf/read.d.ts +36 -0
  36. package/dist/pdf/read.js +65 -0
  37. package/dist/pdf/serializer.d.ts +21 -0
  38. package/dist/pdf/serializer.js +106 -0
  39. package/dist/pdf/standard-metrics.d.ts +27 -0
  40. package/dist/pdf/standard-metrics.js +72 -0
  41. package/dist/pdf/surgery.d.ts +131 -0
  42. package/dist/pdf/surgery.js +847 -0
  43. package/dist/pdf/textstate.d.ts +88 -0
  44. package/dist/pdf/textstate.js +293 -0
  45. package/dist/pdf/types.d.ts +206 -0
  46. package/dist/pdf/types.js +11 -0
  47. package/dist/pdf/verify.d.ts +33 -0
  48. package/dist/pdf/verify.js +446 -0
  49. package/dist/pdf/write.d.ts +62 -0
  50. package/dist/pdf/write.js +750 -0
  51. package/dist/pipeline.d.ts +15 -0
  52. package/dist/pipeline.js +123 -8
  53. package/dist/providers/bardsai.d.ts +1 -1
  54. package/dist/providers/bardsai.js +5 -1
  55. package/dist/providers/gliner-base.d.ts +1 -1
  56. package/dist/providers/gliner-base.js +5 -1
  57. package/dist/providers/gliner.d.ts +1 -1
  58. package/dist/providers/gliner.js +5 -1
  59. package/dist/regex/rules.data.js +13 -8
  60. package/dist/types.d.ts +17 -2
  61. package/dist/types.js +17 -0
  62. package/dist/worker-protocol.d.ts +11 -0
  63. package/dist/worker-protocol.js +41 -8
  64. package/fonts/liberation/LICENSE +102 -0
  65. package/fonts/liberation/LiberationMono-Bold.ttf +0 -0
  66. package/fonts/liberation/LiberationMono-BoldItalic.ttf +0 -0
  67. package/fonts/liberation/LiberationMono-Italic.ttf +0 -0
  68. package/fonts/liberation/LiberationMono-Regular.ttf +0 -0
  69. package/fonts/liberation/LiberationSans-Bold.ttf +0 -0
  70. package/fonts/liberation/LiberationSans-BoldItalic.ttf +0 -0
  71. package/fonts/liberation/LiberationSans-Italic.ttf +0 -0
  72. package/fonts/liberation/LiberationSans-Regular.ttf +0 -0
  73. package/fonts/liberation/LiberationSerif-Bold.ttf +0 -0
  74. package/fonts/liberation/LiberationSerif-BoldItalic.ttf +0 -0
  75. package/fonts/liberation/LiberationSerif-Italic.ttf +0 -0
  76. package/fonts/liberation/LiberationSerif-Regular.ttf +0 -0
  77. package/package.json +11 -2
  78. package/rules/pl.json +7 -3
  79. package/rules/universal.json +6 -5
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.
package/dist/dom/ocr.js CHANGED
@@ -174,17 +174,111 @@ export function buildTextFromBlocks(blocks) {
174
174
  }
175
175
  return { text: text.trimEnd(), words };
176
176
  }
177
+ /** Share of the smaller box height two boxes must overlap to sit on one line. */
178
+ const SAME_LINE_OVERLAP = 0.5;
179
+ /**
180
+ * A gap at most this share of the line's typical word gap is not a space:
181
+ * OCR split one token there and the lost glyph went into a neighbour's box.
182
+ */
183
+ const TIGHT_GAP_SHARE = 0.3;
184
+ /** Typical word gap as a share of the line's box height, for lines too short to measure. */
185
+ const FALLBACK_GAP_SHARE = 0.4;
186
+ /** Lines with fewer measured gaps than this use the height-based fallback. */
187
+ const MIN_GAPS_FOR_MEDIAN = 3;
188
+ /**
189
+ * Boxes taller than this multiple of the line's median height are OCR noise
190
+ * (a stray mark or a punctuation box spanning lines); they never join, so a
191
+ * split-token fix does not paint a bar across the neighbouring lines.
192
+ */
193
+ const MAX_JOIN_HEIGHT_RATIO = 1.6;
194
+ /** Whether `right` continues `left` on the same OCR line (adjacent in the text, overlapping vertically). */
195
+ function continuesLine(left, right) {
196
+ if (right.start !== left.end + 1)
197
+ return false;
198
+ const a = left.bbox;
199
+ const b = right.bbox;
200
+ const minHeight = Math.min(a.y1 - a.y0, b.y1 - b.y0);
201
+ if (minHeight <= 0)
202
+ return false;
203
+ const overlap = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
204
+ return overlap >= minHeight * SAME_LINE_OVERLAP && b.x1 > a.x1;
205
+ }
206
+ function median(values) {
207
+ const sorted = [...values].sort((x, y) => x - y);
208
+ return sorted[Math.floor(sorted.length / 2)];
209
+ }
210
+ /**
211
+ * For each pair of consecutive OCR words, whether they are really one token
212
+ * that OCR split by dropping a character. Tesseract reads the dot in
213
+ * "t.zielinski@firma.pl" as a space and returns "t" + "zielinski@firma.pl",
214
+ * with the dot's pixels inside the second box; the e-mail rule then matches
215
+ * only the second word, and without this the "t" would stay visible. Such a
216
+ * split leaves a sliver between the boxes, far narrower than a real space: a
217
+ * pair on the same line is one token when its gap is at most
218
+ * TIGHT_GAP_SHARE of the line's typical word gap (boxes that touch or
219
+ * overlap included). Boxes much taller than the line (MAX_JOIN_HEIGHT_RATIO)
220
+ * are OCR noise and never join, so the fix cannot paint a bar across
221
+ * neighbouring lines.
222
+ *
223
+ * Not covered: a lost glyph that stays in a normal-width gap (seen only on
224
+ * low-resolution images, around 120 dpi). Telling it from a glyph edge that
225
+ * Tesseract cut off its own box needs pixels and misfired on real scans
226
+ * ("w terminie", "nr wpisu"), so gap width alone decides.
227
+ *
228
+ * Returns an array of length words.length - 1: entry i joins words i and i+1.
229
+ * Exported for tests.
230
+ */
231
+ export function findSplitTokens(words) {
232
+ const joined = new Array(Math.max(0, words.length - 1)).fill(false);
233
+ const height = (w) => w.bbox.y1 - w.bbox.y0;
234
+ let i = 0;
235
+ while (i < words.length - 1) {
236
+ if (!continuesLine(words[i], words[i + 1])) {
237
+ i++;
238
+ continue;
239
+ }
240
+ let end = i + 1; // the line runs from word i to word end
241
+ while (end < words.length - 1 && continuesLine(words[end], words[end + 1]))
242
+ end++;
243
+ const gaps = [];
244
+ for (let k = i; k < end; k++)
245
+ gaps.push(words[k + 1].bbox.x0 - words[k].bbox.x1);
246
+ const lineHeight = median(words.slice(i, end + 1).map(height));
247
+ const typicalGap = gaps.length >= MIN_GAPS_FOR_MEDIAN ? median(gaps) : lineHeight * FALLBACK_GAP_SHARE;
248
+ const tightGap = Math.max(1, typicalGap * TIGHT_GAP_SHARE);
249
+ const maxHeight = lineHeight * MAX_JOIN_HEIGHT_RATIO;
250
+ for (let k = i; k < end; k++) {
251
+ joined[k] = gaps[k - i] <= tightGap
252
+ && height(words[k]) <= maxHeight
253
+ && height(words[k + 1]) <= maxHeight;
254
+ }
255
+ i = end;
256
+ }
257
+ return joined;
258
+ }
177
259
  /**
178
260
  * Pick the bounding boxes of every OCR word that overlaps one of the given
179
261
  * character ranges. A word partially covered by a range is fully redacted
180
- * (over-redaction is safer than leaking half an identifier).
262
+ * (over-redaction is safer than leaking half an identifier). A selected
263
+ * word also pulls in the same-line neighbours OCR split off by dropping a
264
+ * character (findSplitTokens), so "t" + "zielinski@firma.pl" is redacted
265
+ * as one token.
181
266
  *
182
267
  * Exported for tests.
183
268
  */
184
269
  export function selectRedactionBoxes(words, ranges) {
185
- return words
186
- .filter((w) => ranges.some((r) => w.start < r.end && w.end > r.start))
187
- .map((w) => w.bbox);
270
+ const selected = words.map((w) => ranges.some((r) => w.start < r.end && w.end > r.start));
271
+ if (selected.some(Boolean)) {
272
+ const joined = findSplitTokens(words);
273
+ // Spread each selection across its split pieces, left to right then back.
274
+ for (let i = 0; i < joined.length; i++)
275
+ if (joined[i] && selected[i])
276
+ selected[i + 1] = true;
277
+ for (let i = joined.length - 1; i >= 0; i--)
278
+ if (joined[i] && selected[i + 1])
279
+ selected[i] = true;
280
+ }
281
+ return words.filter((_, i) => selected[i]).map((w) => w.bbox);
188
282
  }
189
283
  /** Encode a canvas as a PNG blob, whichever canvas flavor it is. */
190
284
  function canvasToPngBlob(canvas) {
@@ -1,24 +1,26 @@
1
1
  /**
2
2
  * @doccloak/core/dom - detection-driven office file redaction (T110).
3
3
  *
4
- * Ties the format modules (docx.ts, xlsx.ts) to the detection pipeline and an
4
+ * Ties the format modules (docx.ts, xlsx.ts, and the PDF module behind
5
+ * '@doccloak/core/pdf', T215) to the detection pipeline and an
5
6
  * AnonymizationSession, so a host can hand over a File plus its live session
6
7
  * and get back a redacted copy whose placeholders are consistent with every
7
8
  * prompt redacted in the same session: an AI reply that mentions the file's
8
9
  * placeholders restores through the same session map.
9
10
  *
10
- * PDF is intentionally NOT handled here: layout-preserving PDF redaction is
11
- * out of scope for this module.
11
+ * The PDF module is imported lazily so hosts that never see a PDF do not
12
+ * load pdf-lib, pdf.js and fontkit.
12
13
  */
13
14
  import type { DetectedEntity } from '../types.js';
14
15
  import type { AnonymizationSession } from '../session.js';
15
16
  import type { UnredactablePart } from './errors.js';
17
+ import type { PdfReadOptions, PdfWriteOptions, RemovedPart } from '../pdf/types.js';
16
18
  /** File kinds the office redaction flow can regenerate. */
17
- export type OfficeFileKind = 'docx' | 'xlsx';
19
+ export type OfficeFileKind = 'docx' | 'xlsx' | 'pdf';
18
20
  /**
19
21
  * Classify a filename into a supported office kind, or null when the format
20
22
  * cannot be regenerated by this module (.doc has its own binary writer in
21
- * src/doc.ts; PDF output is permanently out of scope for browser hosts).
23
+ * src/doc.ts).
22
24
  */
23
25
  export declare function officeFileKind(filename: string): OfficeFileKind | null;
24
26
  /** A detect function, typically DocCloakEngine.detect or detectEntities. */
@@ -54,6 +56,12 @@ export interface OfficeAnalysis {
54
56
  unredactable: UnredactablePart[];
55
57
  /** Reader notes: parts skipped by the extractor (malformed XML). */
56
58
  warnings: string[];
59
+ /** PDF only: parts the writer will drop (annotations, forms, bookmarks, ...). */
60
+ removed?: RemovedPart[];
61
+ }
62
+ export interface AnalyzeOfficeFileOptions {
63
+ /** PDF reader options (asset URLs for pdf.js and the fallback fonts, password, caps). */
64
+ pdf?: PdfReadOptions;
57
65
  }
58
66
  /**
59
67
  * Extract a file's text and run detection WITHOUT touching the session. Hosts
@@ -62,7 +70,7 @@ export interface OfficeAnalysis {
62
70
  * Extraction is deterministic, so entities returned here can be passed to
63
71
  * redactOfficeFile later and land on identical offsets.
64
72
  */
65
- export declare function analyzeOfficeFile(file: File, detect: DetectFn): Promise<OfficeAnalysis>;
73
+ export declare function analyzeOfficeFile(file: File, detect: DetectFn, options?: AnalyzeOfficeFileOptions): Promise<OfficeAnalysis>;
66
74
  export interface RedactOfficeFileOptions {
67
75
  /** The live session; file placeholders join the same map as prompt ones. */
68
76
  session: AnonymizationSession;
@@ -92,6 +100,8 @@ export interface RedactOfficeFileOptions {
92
100
  * analyzeOfficeFile.
93
101
  */
94
102
  allowUnredactable?: boolean;
103
+ /** PDF only: reader options (assets, password) and writer options (fit policy, raster DPI). */
104
+ pdf?: PdfReadOptions & PdfWriteOptions;
95
105
  }
96
106
  export interface OfficeRedactionResult {
97
107
  kind: OfficeFileKind;
@@ -113,6 +123,12 @@ export interface OfficeRedactionResult {
113
123
  warnings: string[];
114
124
  /** Parts copied verbatim because allowUnredactable was passed (T177). */
115
125
  unredactable: UnredactablePart[];
126
+ /** PDF only: parts dropped from the output. */
127
+ removed?: RemovedPart[];
128
+ /** PDF only: 0-based pages that were rasterized because they could not be edited safely. */
129
+ rasterizedPages?: number[];
130
+ /** PDF only: 0-based scan pages whose OCR-found values were painted over in the picture. */
131
+ scanPages?: number[];
116
132
  }
117
133
  /** "report.docx" -> "report.redacted.docx"; extensionless names get a suffix. */
118
134
  export declare function redactedFileName(filename: string): string;
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * @doccloak/core/dom - detection-driven office file redaction (T110).
3
3
  *
4
- * Ties the format modules (docx.ts, xlsx.ts) to the detection pipeline and an
4
+ * Ties the format modules (docx.ts, xlsx.ts, and the PDF module behind
5
+ * '@doccloak/core/pdf', T215) to the detection pipeline and an
5
6
  * AnonymizationSession, so a host can hand over a File plus its live session
6
7
  * and get back a redacted copy whose placeholders are consistent with every
7
8
  * prompt redacted in the same session: an AI reply that mentions the file's
8
9
  * placeholders restores through the same session map.
9
10
  *
10
- * PDF is intentionally NOT handled here: layout-preserving PDF redaction is
11
- * out of scope for this module.
11
+ * The PDF module is imported lazily so hosts that never see a PDF do not
12
+ * load pdf-lib, pdf.js and fontkit.
12
13
  */
13
14
  import { getFileExtension, readDocx, writeAnonymizedDocxWithReport } from "./docx.js";
14
15
  import { readXlsx, writeAnonymizedXlsxWithReport } from "./xlsx.js";
@@ -16,7 +17,7 @@ import { layerZeroValueReplacements } from "./opc.js";
16
17
  /**
17
18
  * Classify a filename into a supported office kind, or null when the format
18
19
  * cannot be regenerated by this module (.doc has its own binary writer in
19
- * src/doc.ts; PDF output is permanently out of scope for browser hosts).
20
+ * src/doc.ts).
20
21
  */
21
22
  export function officeFileKind(filename) {
22
23
  const ext = getFileExtension(filename);
@@ -24,8 +25,26 @@ export function officeFileKind(filename) {
24
25
  return 'docx';
25
26
  if (ext === 'xlsx')
26
27
  return 'xlsx';
28
+ if (ext === 'pdf')
29
+ return 'pdf';
27
30
  return null;
28
31
  }
32
+ /** Lazy loader of the PDF module (keeps pdf-lib/pdf.js/fontkit out of hosts that never see a PDF). */
33
+ async function pdfModule() {
34
+ return import("../pdf/index.js");
35
+ }
36
+ async function readByKind(kind, file, pdf) {
37
+ if (kind === 'docx')
38
+ return readDocx(file);
39
+ if (kind === 'xlsx')
40
+ return readXlsx(file);
41
+ // A host that has not wired the PDF assets (pdf.js worker, fallback fonts) cannot redact PDFs; refuse
42
+ // the way an unsupported format is refused instead of failing deep inside pdf.js.
43
+ if (!pdf)
44
+ throw new Error(`Unsupported office file: ${file.name} (this host has no PDF support configured)`);
45
+ const { readPdf } = await pdfModule();
46
+ return readPdf(file, pdf);
47
+ }
29
48
  /**
30
49
  * Raised by redactOfficeFile (T179, finding M4) when supplied entities do
31
50
  * not match the file's freshly extracted text: the analysis is stale or came
@@ -58,11 +77,11 @@ export function isStaleAnalysisError(err) {
58
77
  * Extraction is deterministic, so entities returned here can be passed to
59
78
  * redactOfficeFile later and land on identical offsets.
60
79
  */
61
- export async function analyzeOfficeFile(file, detect) {
80
+ export async function analyzeOfficeFile(file, detect, options = {}) {
62
81
  const kind = officeFileKind(file.name);
63
82
  if (!kind)
64
83
  throw new Error(`Unsupported office file: ${file.name}`);
65
- const extraction = kind === 'docx' ? await readDocx(file) : await readXlsx(file);
84
+ const extraction = await readByKind(kind, file, options.pdf);
66
85
  const entities = await detect(extraction.plainText);
67
86
  return {
68
87
  kind,
@@ -70,6 +89,7 @@ export async function analyzeOfficeFile(file, detect) {
70
89
  entities,
71
90
  unredactable: extraction.unredactable,
72
91
  warnings: extraction.warnings,
92
+ removed: kind === 'pdf' ? extraction.removed : undefined,
73
93
  };
74
94
  }
75
95
  /** "report.docx" -> "report.redacted.docx"; extensionless names get a suffix. */
@@ -102,7 +122,7 @@ export async function redactOfficeFile(file, options) {
102
122
  const kind = officeFileKind(file.name);
103
123
  if (!kind)
104
124
  throw new Error(`Unsupported office file: ${file.name}`);
105
- const extraction = kind === 'docx' ? await readDocx(file) : await readXlsx(file);
125
+ const extraction = await readByKind(kind, file, options.pdf);
106
126
  const plainText = extraction.plainText;
107
127
  let entities = options.entities;
108
128
  if (!entities) {
@@ -148,14 +168,25 @@ export async function redactOfficeFile(file, options) {
148
168
  for (const repl of [...replacements].sort((a, b) => b.start - a.start)) {
149
169
  redactedText = redactedText.slice(0, repl.start) + repl.replacement + redactedText.slice(repl.end);
150
170
  }
151
- const written = kind === 'docx'
152
- ? await writeAnonymizedDocxWithReport(extraction, replacements, valueReplacements, {
171
+ let written;
172
+ if (kind === 'docx') {
173
+ written = await writeAnonymizedDocxWithReport(extraction, replacements, valueReplacements, {
153
174
  acceptTrackedChanges: options.acceptTrackedChanges ?? true,
154
175
  allowUnredactable: options.allowUnredactable ?? false,
155
- })
156
- : await writeAnonymizedXlsxWithReport(extraction, replacements, valueReplacements, {
176
+ });
177
+ }
178
+ else if (kind === 'xlsx') {
179
+ written = await writeAnonymizedXlsxWithReport(extraction, replacements, valueReplacements, {
180
+ allowUnredactable: options.allowUnredactable ?? false,
181
+ });
182
+ }
183
+ else {
184
+ const { writeAnonymizedPdfWithReport } = await pdfModule();
185
+ written = await writeAnonymizedPdfWithReport(extraction, replacements, valueReplacements, {
186
+ ...(options.pdf ?? {}),
157
187
  allowUnredactable: options.allowUnredactable ?? false,
158
188
  });
189
+ }
159
190
  return {
160
191
  kind,
161
192
  blob: written.blob,
@@ -165,5 +196,8 @@ export async function redactOfficeFile(file, options) {
165
196
  redactedText,
166
197
  warnings: written.warnings,
167
198
  unredactable: extraction.unredactable,
199
+ removed: written.removed,
200
+ rasterizedPages: written.rasterizedPages,
201
+ scanPages: written.scanPages,
168
202
  };
169
203
  }
package/dist/engine.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * PortLike transport (Web Worker postMessage, MessageChannel, extension
14
14
  * ports).
15
15
  */
16
+ import { throwIfAborted } from "./types.js";
16
17
  import { detectWithRegex, detectEntities } from "./pipeline.js";
17
18
  import { GlinerProvider } from "./providers/gliner.js";
18
19
  import { GlinerBaseProvider } from "./providers/gliner-base.js";
@@ -264,8 +265,7 @@ export function createEngine(env, initial, options) {
264
265
  }
265
266
  async function detect(text, signal, onProgress) {
266
267
  await ready;
267
- if (signal?.aborted)
268
- throw new Error('Detection aborted');
268
+ throwIfAborted(signal);
269
269
  if (!text.trim())
270
270
  return [];
271
271
  const p = ensureProvider();
@@ -282,9 +282,8 @@ export function createEngine(env, initial, options) {
282
282
  onProgress?.(progress);
283
283
  for (const cb of detectionListeners)
284
284
  cb(progress);
285
- });
286
- if (signal?.aborted)
287
- throw new Error('Detection aborted');
285
+ }, signal);
286
+ throwIfAborted(signal);
288
287
  const regexResults = settings.regexEnabled ? detectWithRegex(text, settings.regexRegion) : [];
289
288
  return detectEntities(text, mlResults, regexResults);
290
289
  }
package/dist/env.d.ts CHANGED
@@ -41,12 +41,12 @@ export interface CoreEnv {
41
41
  * `new PreTrainedTokenizer(tokenizerJson, tokenizerConfig)` (or the class
42
42
  * named by `tokenizerConfig.tokenizer_class`). May return a promise.
43
43
  *
44
- * Optional in 0.12.0 only so hosts still on loadTokenizer keep compiling;
45
- * required from 0.13.0.
44
+ * Optional so hosts still on loadTokenizer keep compiling; required once
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, removed in 0.13.0. Used only when
49
+ * @deprecated since 0.12.0, removal planned for 0.14.0. 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,83 @@
1
+ /**
2
+ * @doccloak/core/pdf - CMap parser, decoder and ToUnicode serializer (T208).
3
+ *
4
+ * Handles the CMap syntax of ISO 32000-1 section 9.7.5 (embedded CMap
5
+ * streams) and 9.10.3 (ToUnicode CMaps): codespace ranges, cidrange /
6
+ * cidchar, bfchar / bfrange (both the incrementing and the array form,
7
+ * multi-code-unit UTF-16BE destinations with surrogates), /WMode, /CMapName.
8
+ * `usecmap` and predefined CMaps other than Identity-H/V are refused: the
9
+ * caller reports the font as unredactable rather than guessing.
10
+ *
11
+ * The parser is strict: malformed input throws CMapParseError, it never
12
+ * returns a partial map silently. A missing codespace section is the one
13
+ * tolerated defect (common in real ToUnicode CMaps): the ranges are then
14
+ * derived from the byte lengths of the codes seen.
15
+ */
16
+ export interface CodespaceRange {
17
+ /** Code length in bytes (1..4). */
18
+ numBytes: number;
19
+ /** Inclusive bounds as big-endian integers of `numBytes` bytes. */
20
+ low: number;
21
+ high: number;
22
+ }
23
+ export interface CMap {
24
+ codespace: CodespaceRange[];
25
+ /** code -> CID for CID CMaps. null means identity (Identity-H/V or a CMap without cid entries). */
26
+ cidMap: Map<number, number> | null;
27
+ /** code -> unicode string for ToUnicode CMaps. null when the CMap has no bf entries. */
28
+ unicodeMap: Map<number, string> | null;
29
+ /** True for vertical writing mode (/WMode 1, Identity-V). */
30
+ vertical: boolean;
31
+ /** /CMapName when present. */
32
+ name?: string;
33
+ /** Code lengths (in bytes, 1..4) seen in codespace ranges and mapping entries. */
34
+ codeLengths: Set<number>;
35
+ }
36
+ export declare class CMapParseError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ /** Largest number of entries one range (cidrange / bfrange) may expand to. Guards against memory bombs. */
40
+ export declare const MAX_RANGE_ENTRIES = 65536;
41
+ /** Parse an embedded CMap / ToUnicode stream. Throws CMapParseError on malformed input or `usecmap`. */
42
+ export declare function parseCMap(bytes: Uint8Array): CMap;
43
+ /** The 2-byte Identity CMap: every code is its own CID. */
44
+ export declare function identityCMap(vertical: boolean): CMap;
45
+ /** Only Identity-H / Identity-V are built in; any other predefined name is refused by the caller. */
46
+ export declare function predefinedCMap(name: string): CMap | undefined;
47
+ export interface DecodedCode {
48
+ /** Big-endian value of the code bytes. */
49
+ code: number;
50
+ /** Number of bytes consumed (1..4). */
51
+ length: number;
52
+ /** Byte offset of the code inside the string operand. */
53
+ offset: number;
54
+ }
55
+ /**
56
+ * Split a string operand into character codes (ISO 32000-1 9.7.6.3). For each
57
+ * position the shortest byte length whose codespace range contains the value
58
+ * wins; with no full match, the shortest range that matches on the first byte
59
+ * decides the length; otherwise 1 byte. Unmapped codes are still returned, the
60
+ * caller treats them as notdef. A code cut short by the end of the string takes
61
+ * the remaining bytes.
62
+ */
63
+ export declare function splitCodes(cmap: CMap, bytes: Uint8Array): DecodedCode[];
64
+ /** CID of a code: identity when the CMap has no cid entries, 0 (notdef) when the code is unmapped. */
65
+ export declare function cidOf(cmap: CMap, code: number): number;
66
+ /** Unicode text of a code from a ToUnicode CMap; undefined when unmapped (or when the CMap has no bf entries). */
67
+ export declare function unicodeOf(cmap: CMap, code: number): string | undefined;
68
+ export interface ToUnicodeEntry {
69
+ code: number;
70
+ /** Code length in bytes (1..4). */
71
+ length: number;
72
+ unicode: string;
73
+ }
74
+ /**
75
+ * Write a ToUnicode CMap holding exactly the given codes. One full-width
76
+ * codespace range per code length present, bfchar entries in groups of at most
77
+ * 100, UTF-16BE destinations. Output is deterministic (sorted by length, then
78
+ * code; later duplicates win) and reads back with parseCMap to the same map.
79
+ */
80
+ export declare function serializeToUnicode(entries: ToUnicodeEntry[], options?: {
81
+ name?: string;
82
+ codespace?: readonly CodespaceRange[];
83
+ }): Uint8Array;