@stll/anonymize-pdf 0.0.1-placeholder.0 → 2.4.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/ATTRIBUTION.md +28 -0
- package/LICENSE +17 -0
- package/README.md +110 -0
- package/dist/index.d.mts +231 -0
- package/dist/index.mjs +478 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +39 -3
package/ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Attribution
|
|
2
|
+
|
|
3
|
+
PDF structures are parsed by `lopdf`, distributed under the MIT License.
|
|
4
|
+
|
|
5
|
+
- Copyright (c) 2016 Junfeng Liu
|
|
6
|
+
- https://github.com/J-F-Liu/lopdf
|
|
7
|
+
|
|
8
|
+
## lopdf MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2016 Junfeng Liu
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 stella
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# `@stll/anonymize-pdf`
|
|
2
|
+
|
|
3
|
+
Bounded PDF inspection and destructive raster anonymization contracts.
|
|
4
|
+
|
|
5
|
+
Inspection results inventory
|
|
6
|
+
structures that can retain sensitive data, including forms, annotations,
|
|
7
|
+
attachments, metadata, JavaScript, XFA, optional content, signatures, and image
|
|
8
|
+
objects. A black rectangle drawn over a PDF page is not redaction because the
|
|
9
|
+
original text or image can remain underneath it.
|
|
10
|
+
|
|
11
|
+
Incremental revisions and non-whitespace bytes after the final `%%EOF` marker
|
|
12
|
+
are reported as retained document bytes and prevent full coverage. The current
|
|
13
|
+
object graph is inspected; superseded revision contents are not claimed as
|
|
14
|
+
parsed or safe.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
18
|
+
import { inspectPdf } from "@stll/anonymize-pdf";
|
|
19
|
+
|
|
20
|
+
const inspection = inspectPdf(await readFile("contract.pdf"));
|
|
21
|
+
console.log(inspection.risks);
|
|
22
|
+
console.log(inspection.coverage);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Without page observations from a renderer/OCR adapter, coverage is explicitly
|
|
26
|
+
`partial` with `page-content-not-observed`. The optional observation contract
|
|
27
|
+
uses UTF-16 text offsets and PDF points. Page dimensions must match the
|
|
28
|
+
effective CropBox/MediaBox intersection after page rotation and UserUnit scaling
|
|
29
|
+
(within 0.25 points for renderer rounding). A `complete` text layer must map
|
|
30
|
+
every observed UTF-16 code unit to glyph geometry. It is the seam for a later
|
|
31
|
+
PDFium-backed renderer and destructive raster workflow; callers must not
|
|
32
|
+
describe inspection alone as anonymization.
|
|
33
|
+
|
|
34
|
+
Glyph bounds use a normalized displayed-page coordinate space in PDF points:
|
|
35
|
+
the effective visible page's bottom-left is `(0, 0)`, and CropBox translation,
|
|
36
|
+
page rotation, and UserUnit scaling have already been applied.
|
|
37
|
+
|
|
38
|
+
Coverage is only `full` when every page was rendered, its text layer was
|
|
39
|
+
completely observed, and OCR completely covered the rendered page pixels. That
|
|
40
|
+
includes vector outlines, not only PDF image objects.
|
|
41
|
+
|
|
42
|
+
lopdf enforces the decompression ceiling per object or cross-reference stream
|
|
43
|
+
during parsing. The inspector additionally bounds aggregate loaded string,
|
|
44
|
+
name, dictionary-key, and stream payload after parsing. That aggregate check is
|
|
45
|
+
not a guarantee that the parser allocated no intermediate memory before the
|
|
46
|
+
check; callers must also enforce the input-byte limit and process isolation
|
|
47
|
+
appropriate for untrusted files.
|
|
48
|
+
|
|
49
|
+
## Destructive raster anonymization
|
|
50
|
+
|
|
51
|
+
`anonymizePdfRaster` accepts pages produced by an explicit renderer/OCR
|
|
52
|
+
provider and creates a brand-new image-only PDF. It requires a prepared stella
|
|
53
|
+
pipeline, runs native detection over each page's observed text, merges an
|
|
54
|
+
optional digest-bound `ExternalDetectionBatch`, and fails if any selected
|
|
55
|
+
non-whitespace UTF-16 span is not completely mapped to observed glyph geometry.
|
|
56
|
+
Provider metadata must identify one explicit OCR language pack; mixed or
|
|
57
|
+
implicit language selection is not part of the contract.
|
|
58
|
+
It requires one opaque, row-packed RGB8 buffer for every source page, SHA-256
|
|
59
|
+
binds the source and each page, requires complete rendering and OCR assertions,
|
|
60
|
+
destructively fills the mapped pixels, and verifies both the newly written PDF
|
|
61
|
+
graph and its decompressed image pixels. Encrypted inputs are rejected.
|
|
62
|
+
|
|
63
|
+
The output never reuses source PDF objects, streams, text layers, metadata,
|
|
64
|
+
forms, annotations, attachments, actions, signatures, optional content, or
|
|
65
|
+
incremental revisions. A solid rectangle in this output is destructive: the
|
|
66
|
+
original pixels are absent rather than hidden underneath it. The tradeoff is
|
|
67
|
+
intentional loss of searchability, accessibility, links, forms, signatures,
|
|
68
|
+
and other interactive behavior.
|
|
69
|
+
|
|
70
|
+
The package does not bundle renderer/OCR binaries or silently select a language.
|
|
71
|
+
Its optional Node adapter invokes locally installed Poppler `pdftoppm` and
|
|
72
|
+
Tesseract directly (without a shell), sequentially, in a private temporary
|
|
73
|
+
directory with bounded output and timeouts. It renders and removes one page at a
|
|
74
|
+
time so document-wide limits are enforced without first materializing every
|
|
75
|
+
page raster. Callers select exactly one installed Tesseract language pack.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import {
|
|
79
|
+
anonymizePdfRaster,
|
|
80
|
+
renderPdfWithPopplerTesseract,
|
|
81
|
+
} from "@stll/anonymize-pdf";
|
|
82
|
+
|
|
83
|
+
const observed = await renderPdfWithPopplerTesseract({
|
|
84
|
+
document,
|
|
85
|
+
ocrLanguage: "eng",
|
|
86
|
+
});
|
|
87
|
+
const result = anonymizePdfRaster({
|
|
88
|
+
document,
|
|
89
|
+
pipeline,
|
|
90
|
+
provider: observed.provider,
|
|
91
|
+
pages: observed.pages,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A provider's `complete` OCR assertion means every rendered page was submitted
|
|
96
|
+
to that OCR engine; it is not a claim that OCR or PII detection has perfect
|
|
97
|
+
recall. OCR word boxes are conservatively redacted as whole regions when a
|
|
98
|
+
selected span overlaps them. The returned certificate always says
|
|
99
|
+
`piiCleanGuaranteed: false`:
|
|
100
|
+
verification proves a fresh image-only structure and the requested destructive
|
|
101
|
+
pixel rewrite, not perfect OCR or detector recall. Browser rendering is not
|
|
102
|
+
part of this surface. Raster anonymization is a Node and Python
|
|
103
|
+
document-profile capability; PDF inspection remains available through the
|
|
104
|
+
byte-oriented core runtimes. The Poppler/Tesseract adapter is separately named
|
|
105
|
+
as the Node-only `document.pdf.observe-raster-local` capability.
|
|
106
|
+
|
|
107
|
+
`rewritePdfRasterFromDetections` is the advanced rewrite-only seam for trusted
|
|
108
|
+
adapters that already ran detection. Detection spans must be strictly ordered
|
|
109
|
+
and non-overlapping. Its name and certificate deliberately do not imply
|
|
110
|
+
detection or PII cleanliness.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { ExternalDetectionBatch, PreparedNativePipeline } from "@stll/anonymize";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
type PdfRect = {
|
|
4
|
+
left: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
right: number;
|
|
7
|
+
top: number;
|
|
8
|
+
};
|
|
9
|
+
type PdfGlyphObservation = {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
/** Bounds in normalized displayed-page PDF points, origin bottom-left. */
|
|
13
|
+
bounds: PdfRect;
|
|
14
|
+
source: "embedded-text" | "ocr";
|
|
15
|
+
};
|
|
16
|
+
type PdfPageObservation = {
|
|
17
|
+
pageIndex: number;
|
|
18
|
+
/** Effective visible width after page boxes, rotation, and UserUnit scaling. */
|
|
19
|
+
widthPoints: number;
|
|
20
|
+
/** Effective visible height after page boxes, rotation, and UserUnit scaling. */
|
|
21
|
+
heightPoints: number;
|
|
22
|
+
text: string;
|
|
23
|
+
glyphs: readonly PdfGlyphObservation[];
|
|
24
|
+
rendered: boolean;
|
|
25
|
+
textLayer: "absent" | "partial" | "complete";
|
|
26
|
+
ocr: "not-run" | "partial" | "complete";
|
|
27
|
+
imageCount: number;
|
|
28
|
+
};
|
|
29
|
+
type PdfRiskInventory = {
|
|
30
|
+
acroFormFieldCount: number;
|
|
31
|
+
annotationCount: number;
|
|
32
|
+
documentInfoEntryCount: number;
|
|
33
|
+
embeddedFileCount: number;
|
|
34
|
+
externalActionCount: number;
|
|
35
|
+
formXObjectCount: number;
|
|
36
|
+
imageObjectCount: number;
|
|
37
|
+
incrementalRevisionCount: number;
|
|
38
|
+
javascriptActionCount: number;
|
|
39
|
+
metadataStreamCount: number;
|
|
40
|
+
optionalContentGroupCount: number;
|
|
41
|
+
signatureCount: number;
|
|
42
|
+
trailingNonWhitespaceByteCount: number;
|
|
43
|
+
unsupportedActionCount: number;
|
|
44
|
+
xfaEntryCount: number;
|
|
45
|
+
};
|
|
46
|
+
type PdfInspectionGap = "encrypted-document" | "page-content-not-observed" | "page-not-rendered" | "partial-text-layer" | "retained-document-bytes" | "unobserved-visual-content";
|
|
47
|
+
type PdfPageInspection = {
|
|
48
|
+
pageIndex: number;
|
|
49
|
+
/** Effective visible width after page boxes, rotation, and UserUnit scaling. */
|
|
50
|
+
widthPoints: number;
|
|
51
|
+
/** Effective visible height after page boxes, rotation, and UserUnit scaling. */
|
|
52
|
+
heightPoints: number;
|
|
53
|
+
annotationCount: number;
|
|
54
|
+
observation: PdfPageObservation | null;
|
|
55
|
+
};
|
|
56
|
+
type PdfInspection = {
|
|
57
|
+
contractVersion: 1;
|
|
58
|
+
pdfVersion: string;
|
|
59
|
+
byteLength: number;
|
|
60
|
+
objectCount: number;
|
|
61
|
+
pageCount: number;
|
|
62
|
+
encrypted: boolean;
|
|
63
|
+
pages: readonly PdfPageInspection[];
|
|
64
|
+
risks: PdfRiskInventory;
|
|
65
|
+
coverage: {
|
|
66
|
+
status: "full" | "partial";
|
|
67
|
+
gaps: readonly PdfInspectionGap[];
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
type PdfRasterProvider = {
|
|
71
|
+
providerId: string;
|
|
72
|
+
rendererName: string;
|
|
73
|
+
rendererVersion: string;
|
|
74
|
+
ocrName: string;
|
|
75
|
+
ocrVersion: string;
|
|
76
|
+
/** One explicit provider language pack; never an implicit mixed-language set. */
|
|
77
|
+
ocrLanguage: string;
|
|
78
|
+
};
|
|
79
|
+
type PdfRasterPage = {
|
|
80
|
+
observation: PdfPageObservation;
|
|
81
|
+
widthPixels: number;
|
|
82
|
+
heightPixels: number;
|
|
83
|
+
/** Lowercase SHA-256 of this page's exact opaque, row-packed RGB8 bytes. */
|
|
84
|
+
pixelSha256: string;
|
|
85
|
+
/** Strictly ordered, non-overlapping UTF-16 spans. */
|
|
86
|
+
detections: readonly PdfRasterDetection[];
|
|
87
|
+
};
|
|
88
|
+
type PdfRasterDetection = {
|
|
89
|
+
start: number;
|
|
90
|
+
end: number;
|
|
91
|
+
};
|
|
92
|
+
type PdfRasterRewrite = {
|
|
93
|
+
contractVersion: 1;
|
|
94
|
+
/** Lowercase SHA-256 of the exact source PDF bytes supplied to the call. */
|
|
95
|
+
sourceSha256: string;
|
|
96
|
+
provider: PdfRasterProvider;
|
|
97
|
+
fillRgb: readonly [number, number, number];
|
|
98
|
+
pages: readonly PdfRasterPage[];
|
|
99
|
+
};
|
|
100
|
+
type PdfRasterRewriteCertificate = {
|
|
101
|
+
contractVersion: 1;
|
|
102
|
+
pageCount: number;
|
|
103
|
+
sourceSha256: string;
|
|
104
|
+
outputSha256: string;
|
|
105
|
+
provider: PdfRasterProvider;
|
|
106
|
+
detectionCount: number;
|
|
107
|
+
mappedRegionCount: number;
|
|
108
|
+
structurePixelRewriteVerified: true;
|
|
109
|
+
providerAssertedCoverage: "complete-rendering-and-ocr-observation";
|
|
110
|
+
piiCleanGuaranteed: false;
|
|
111
|
+
limitation: string;
|
|
112
|
+
};
|
|
113
|
+
type PdfRasterRewriteResult = {
|
|
114
|
+
document: Uint8Array;
|
|
115
|
+
certificate: PdfRasterRewriteCertificate;
|
|
116
|
+
};
|
|
117
|
+
declare const PDF_INSPECTION_ERROR_CODES: {
|
|
118
|
+
readonly documentLimitExceeded: "document-limit-exceeded";
|
|
119
|
+
readonly invalidDocument: "invalid-document";
|
|
120
|
+
readonly invalidObservation: "invalid-observation";
|
|
121
|
+
readonly observationLimitExceeded: "observation-limit-exceeded";
|
|
122
|
+
readonly providerFailed: "provider-failed";
|
|
123
|
+
};
|
|
124
|
+
type PdfInspectionErrorCode = (typeof PDF_INSPECTION_ERROR_CODES)[keyof typeof PDF_INSPECTION_ERROR_CODES];
|
|
125
|
+
declare const PDF_RASTER_ERROR_CODES: {
|
|
126
|
+
readonly detectionFailed: "detection-failed";
|
|
127
|
+
readonly invalidContract: "invalid-contract";
|
|
128
|
+
readonly limitExceeded: "limit-exceeded";
|
|
129
|
+
readonly sourceRejected: "source-rejected";
|
|
130
|
+
readonly verificationFailed: "verification-failed";
|
|
131
|
+
};
|
|
132
|
+
type PdfRasterErrorCode = (typeof PDF_RASTER_ERROR_CODES)[keyof typeof PDF_RASTER_ERROR_CODES];
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/local-provider.d.ts
|
|
135
|
+
declare const PDF_LOCAL_PROVIDER_ERROR_CODES: {
|
|
136
|
+
readonly cleanupFailed: "cleanup-failed";
|
|
137
|
+
readonly executableFailed: "executable-failed";
|
|
138
|
+
readonly invalidOptions: "invalid-options";
|
|
139
|
+
readonly invalidOutput: "invalid-output";
|
|
140
|
+
readonly limitExceeded: "limit-exceeded";
|
|
141
|
+
readonly sourceRejected: "source-rejected";
|
|
142
|
+
};
|
|
143
|
+
type PdfLocalProviderErrorCode = (typeof PDF_LOCAL_PROVIDER_ERROR_CODES)[keyof typeof PDF_LOCAL_PROVIDER_ERROR_CODES];
|
|
144
|
+
declare class PdfLocalProviderError extends Error {
|
|
145
|
+
readonly code: PdfLocalProviderErrorCode;
|
|
146
|
+
constructor(code: PdfLocalProviderErrorCode, message: string);
|
|
147
|
+
}
|
|
148
|
+
type RenderPdfWithPopplerTesseractOptions = {
|
|
149
|
+
document: Uint8Array;
|
|
150
|
+
/** One explicit installed Tesseract traineddata name, for example `eng`. */
|
|
151
|
+
ocrLanguage: string;
|
|
152
|
+
dpi?: number | undefined;
|
|
153
|
+
pdftoppmPath?: string | undefined;
|
|
154
|
+
tesseractPath?: string | undefined;
|
|
155
|
+
timeoutMs?: number | undefined;
|
|
156
|
+
};
|
|
157
|
+
type LocalPdfRasterPage = {
|
|
158
|
+
observation: PdfPageObservation;
|
|
159
|
+
widthPixels: number;
|
|
160
|
+
heightPixels: number;
|
|
161
|
+
pixels: Uint8Array;
|
|
162
|
+
};
|
|
163
|
+
type LocalPdfRasterObservation = {
|
|
164
|
+
provider: PdfRasterProvider;
|
|
165
|
+
pages: readonly LocalPdfRasterPage[];
|
|
166
|
+
};
|
|
167
|
+
declare const renderPdfWithPopplerTesseract: (options: RenderPdfWithPopplerTesseractOptions) => Promise<LocalPdfRasterObservation>;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/index.d.ts
|
|
170
|
+
declare const PDF_INSPECTION_CONTRACT_VERSION: 1;
|
|
171
|
+
declare const PDF_RASTER_CONTRACT_VERSION: 1;
|
|
172
|
+
declare const PDF_RASTER_MAX_PAGE_BYTES: number;
|
|
173
|
+
declare const PDF_RASTER_MAX_TOTAL_BYTES: number;
|
|
174
|
+
declare const PDF_RASTER_MAX_OUTPUT_BYTES: number;
|
|
175
|
+
declare const PDF_RASTER_REQUEST_JSON_MAX_BYTES: number;
|
|
176
|
+
declare const PDF_DOCUMENT_MAX_BYTES: number;
|
|
177
|
+
declare const PDF_STREAM_DECOMPRESSED_MAX_BYTES: number;
|
|
178
|
+
declare const PDF_LOADED_PAYLOAD_MAX_BYTES: number;
|
|
179
|
+
declare const PDF_MAX_OBJECTS = 200000;
|
|
180
|
+
declare const PDF_MAX_OBJECT_NODES = 1000000;
|
|
181
|
+
declare const PDF_MAX_OBJECT_DEPTH = 128;
|
|
182
|
+
declare const PDF_MAX_PAGES = 10000;
|
|
183
|
+
declare const PDF_MAX_GLYPHS = 5000000;
|
|
184
|
+
declare const PDF_MAX_PAGE_TEXT_UTF8_BYTES: number;
|
|
185
|
+
declare const PDF_MAX_OBSERVED_TEXT_UTF8_BYTES: number;
|
|
186
|
+
declare const PDF_OBSERVATIONS_JSON_MAX_BYTES: number;
|
|
187
|
+
/** Renderer dimensions may differ by this many points due to numeric rounding. */
|
|
188
|
+
declare const PDF_PAGE_DIMENSION_TOLERANCE_POINTS = 0.25;
|
|
189
|
+
declare class PdfInspectionError extends Error {
|
|
190
|
+
readonly code: PdfInspectionErrorCode;
|
|
191
|
+
constructor(code: PdfInspectionErrorCode, message: string);
|
|
192
|
+
}
|
|
193
|
+
declare class PdfRasterError extends Error {
|
|
194
|
+
readonly code: PdfRasterErrorCode;
|
|
195
|
+
constructor(code: PdfRasterErrorCode, message: string);
|
|
196
|
+
}
|
|
197
|
+
type InspectPdfOptions = {
|
|
198
|
+
pageObservations?: readonly PdfPageObservation[];
|
|
199
|
+
};
|
|
200
|
+
declare const inspectPdf: (document: Uint8Array, options?: InspectPdfOptions) => PdfInspection;
|
|
201
|
+
type RewritePdfRasterFromDetectionsOptions = {
|
|
202
|
+
document: Uint8Array;
|
|
203
|
+
request: PdfRasterRewrite;
|
|
204
|
+
/** One opaque, row-packed RGB8 buffer per request page, in page order. */
|
|
205
|
+
pagePixels: readonly Uint8Array[];
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Advanced rewrite-only surface. It verifies structure and destructive pixels but
|
|
209
|
+
* does not run detection or certify that the result is PII-free.
|
|
210
|
+
*/
|
|
211
|
+
declare const rewritePdfRasterFromDetections: ({ document, request, pagePixels }: RewritePdfRasterFromDetectionsOptions) => PdfRasterRewriteResult;
|
|
212
|
+
type PdfRasterObservedPageInput = {
|
|
213
|
+
observation: PdfPageObservation;
|
|
214
|
+
widthPixels: number;
|
|
215
|
+
heightPixels: number;
|
|
216
|
+
pixels: Uint8Array;
|
|
217
|
+
/** Optional digest-bound provider batch merged through stella caller semantics. */
|
|
218
|
+
externalDetectionBatch?: ExternalDetectionBatch | string | undefined;
|
|
219
|
+
};
|
|
220
|
+
type AnonymizePdfRasterOptions = {
|
|
221
|
+
document: Uint8Array;
|
|
222
|
+
pipeline: Pick<PreparedNativePipeline, "redactText" | "redactTextWithCallerDetections">;
|
|
223
|
+
provider: PdfRasterProvider;
|
|
224
|
+
pages: readonly PdfRasterObservedPageInput[];
|
|
225
|
+
fillRgb?: readonly [number, number, number] | undefined;
|
|
226
|
+
};
|
|
227
|
+
/** Run stella detection, merge optional external detections, then rewrite pixels. */
|
|
228
|
+
declare const anonymizePdfRaster: ({ document, pipeline, provider, pages, fillRgb }: AnonymizePdfRasterOptions) => PdfRasterRewriteResult;
|
|
229
|
+
//#endregion
|
|
230
|
+
export { AnonymizePdfRasterOptions, InspectPdfOptions, type LocalPdfRasterObservation, type LocalPdfRasterPage, PDF_DOCUMENT_MAX_BYTES, PDF_INSPECTION_CONTRACT_VERSION, PDF_INSPECTION_ERROR_CODES, PDF_LOADED_PAYLOAD_MAX_BYTES, PDF_LOCAL_PROVIDER_ERROR_CODES, PDF_MAX_GLYPHS, PDF_MAX_OBJECTS, PDF_MAX_OBJECT_DEPTH, PDF_MAX_OBJECT_NODES, PDF_MAX_OBSERVED_TEXT_UTF8_BYTES, PDF_MAX_PAGES, PDF_MAX_PAGE_TEXT_UTF8_BYTES, PDF_OBSERVATIONS_JSON_MAX_BYTES, PDF_PAGE_DIMENSION_TOLERANCE_POINTS, PDF_RASTER_CONTRACT_VERSION, PDF_RASTER_ERROR_CODES, PDF_RASTER_MAX_OUTPUT_BYTES, PDF_RASTER_MAX_PAGE_BYTES, PDF_RASTER_MAX_TOTAL_BYTES, PDF_RASTER_REQUEST_JSON_MAX_BYTES, PDF_STREAM_DECOMPRESSED_MAX_BYTES, type PdfGlyphObservation, type PdfInspection, PdfInspectionError, type PdfInspectionErrorCode, type PdfInspectionGap, PdfLocalProviderError, type PdfLocalProviderErrorCode, type PdfPageInspection, type PdfPageObservation, type PdfRasterDetection, PdfRasterError, type PdfRasterErrorCode, PdfRasterObservedPageInput, type PdfRasterPage, type PdfRasterProvider, type PdfRasterRewrite, type PdfRasterRewriteCertificate, type PdfRasterRewriteResult, type PdfRect, type PdfRiskInventory, type RenderPdfWithPopplerTesseractOptions, RewritePdfRasterFromDetectionsOptions, anonymizePdfRaster, inspectPdf, renderPdfWithPopplerTesseract, rewritePdfRasterFromDetections };
|
|
231
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { convert_external_detection_batch, loadNativeAnonymizeBinding } from "@stll/anonymize";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { lstat, mkdtemp, readFile, rm, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
|
+
//#region src/types.ts
|
|
8
|
+
const PDF_INSPECTION_ERROR_CODES = {
|
|
9
|
+
documentLimitExceeded: "document-limit-exceeded",
|
|
10
|
+
invalidDocument: "invalid-document",
|
|
11
|
+
invalidObservation: "invalid-observation",
|
|
12
|
+
observationLimitExceeded: "observation-limit-exceeded",
|
|
13
|
+
providerFailed: "provider-failed"
|
|
14
|
+
};
|
|
15
|
+
const PDF_RASTER_ERROR_CODES = {
|
|
16
|
+
detectionFailed: "detection-failed",
|
|
17
|
+
invalidContract: "invalid-contract",
|
|
18
|
+
limitExceeded: "limit-exceeded",
|
|
19
|
+
sourceRejected: "source-rejected",
|
|
20
|
+
verificationFailed: "verification-failed"
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/local-provider.ts
|
|
24
|
+
const LOCAL_PROVIDER_ID = "poppler-tesseract-local";
|
|
25
|
+
const PROCESS_OUTPUT_MAX_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
const PDF_DOCUMENT_MAX_BYTES$1 = 64 * 1024 * 1024;
|
|
27
|
+
const PDF_RASTER_MAX_PAGE_BYTES$1 = 128 * 1024 * 1024;
|
|
28
|
+
const PDF_RASTER_MAX_TOTAL_BYTES$1 = 512 * 1024 * 1024;
|
|
29
|
+
const PDF_MAX_PAGES$1 = 1e4;
|
|
30
|
+
const PDF_MAX_GLYPHS$1 = 5e6;
|
|
31
|
+
const PDF_MAX_OBSERVED_TEXT_UTF8_BYTES$1 = 64 * 1024 * 1024;
|
|
32
|
+
const DEFAULT_DPI = 300;
|
|
33
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
34
|
+
const MIN_DPI = 72;
|
|
35
|
+
const MAX_DPI = 600;
|
|
36
|
+
const MIN_TIMEOUT_MS = 100;
|
|
37
|
+
const MAX_TIMEOUT_MS = 3e5;
|
|
38
|
+
const LANGUAGE_PACK_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/u;
|
|
39
|
+
const TEMP_PREFIX = "stella-anonymize-pdf-";
|
|
40
|
+
const PDF_LOCAL_PROVIDER_ERROR_CODES = {
|
|
41
|
+
cleanupFailed: "cleanup-failed",
|
|
42
|
+
executableFailed: "executable-failed",
|
|
43
|
+
invalidOptions: "invalid-options",
|
|
44
|
+
invalidOutput: "invalid-output",
|
|
45
|
+
limitExceeded: "limit-exceeded",
|
|
46
|
+
sourceRejected: "source-rejected"
|
|
47
|
+
};
|
|
48
|
+
var PdfLocalProviderError = class extends Error {
|
|
49
|
+
code;
|
|
50
|
+
constructor(code, message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "PdfLocalProviderError";
|
|
53
|
+
this.code = code;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const providerError = (code, message) => new PdfLocalProviderError(code, `${code}: ${message}`);
|
|
57
|
+
const validateOptions = (options) => {
|
|
58
|
+
const dpi = options.dpi ?? DEFAULT_DPI;
|
|
59
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
60
|
+
const pdftoppmPath = options.pdftoppmPath ?? "pdftoppm";
|
|
61
|
+
const tesseractPath = options.tesseractPath ?? "tesseract";
|
|
62
|
+
if (!Number.isInteger(dpi) || dpi < MIN_DPI || dpi > MAX_DPI || !Number.isInteger(timeoutMs) || timeoutMs < MIN_TIMEOUT_MS || timeoutMs > MAX_TIMEOUT_MS || !LANGUAGE_PACK_PATTERN.test(options.ocrLanguage) || !pdftoppmPath || pdftoppmPath.includes("\0") || !tesseractPath || tesseractPath.includes("\0")) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOptions, "local PDF provider options are invalid");
|
|
63
|
+
return {
|
|
64
|
+
dpi,
|
|
65
|
+
ocrLanguage: options.ocrLanguage,
|
|
66
|
+
pdftoppmPath,
|
|
67
|
+
tesseractPath,
|
|
68
|
+
timeoutMs
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const runExecutable = (executable, args, timeoutMs, stage) => new Promise((resolve, reject) => {
|
|
72
|
+
execFile(executable, args, {
|
|
73
|
+
encoding: "buffer",
|
|
74
|
+
maxBuffer: PROCESS_OUTPUT_MAX_BYTES,
|
|
75
|
+
timeout: timeoutMs,
|
|
76
|
+
windowsHide: true
|
|
77
|
+
}, (error, stdout, stderr) => {
|
|
78
|
+
if (error !== null) {
|
|
79
|
+
reject(providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.executableFailed, `local PDF ${stage} failed`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
resolve({
|
|
83
|
+
stdout: Buffer.from(stdout),
|
|
84
|
+
stderr: Buffer.from(stderr)
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const decodeUtf8 = (value, context) => {
|
|
89
|
+
try {
|
|
90
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(value);
|
|
91
|
+
} catch {
|
|
92
|
+
throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, `local PDF ${context} is not valid UTF-8`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const versionFrom = (value, prefix, context) => {
|
|
96
|
+
const line = decodeUtf8(value, context).split(/\r?\n/u, 1)[0]?.trim();
|
|
97
|
+
const version = line?.startsWith(prefix) ? line.slice(prefix.length).trim() : "";
|
|
98
|
+
if (!version || version.length > 256 || /\p{Cc}/u.test(version)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, `local PDF ${context} is invalid`);
|
|
99
|
+
return version;
|
|
100
|
+
};
|
|
101
|
+
const inspectPageGeometry = (document) => {
|
|
102
|
+
if (document.byteLength > PDF_DOCUMENT_MAX_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "PDF source exceeds its byte limit");
|
|
103
|
+
const inspect = loadNativeAnonymizeBinding().inspectPdfJson;
|
|
104
|
+
if (inspect === void 0) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected, "native PDF inspection is unavailable");
|
|
105
|
+
try {
|
|
106
|
+
const inspection = JSON.parse(inspect(document));
|
|
107
|
+
if (inspection.encrypted === true || !Number.isSafeInteger(inspection.pageCount) || inspection.pageCount < 1 || inspection.pageCount > PDF_MAX_PAGES$1 || !Array.isArray(inspection.pages) || inspection.pages.length !== inspection.pageCount) throw new Error("source rejected");
|
|
108
|
+
return inspection.pages.map((page, pageIndex) => {
|
|
109
|
+
if (typeof page !== "object" || page === null) throw new Error("source rejected");
|
|
110
|
+
const { heightPoints, pageIndex: inspectedPageIndex, widthPoints } = page;
|
|
111
|
+
if (inspectedPageIndex !== pageIndex || typeof widthPoints !== "number" || !Number.isFinite(widthPoints) || widthPoints <= 0 || typeof heightPoints !== "number" || !Number.isFinite(heightPoints) || heightPoints <= 0) throw new Error("source rejected");
|
|
112
|
+
return {
|
|
113
|
+
heightPoints,
|
|
114
|
+
widthPoints
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected, "PDF source is unsupported by the local raster provider");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const assertProjectedRasterLimits = (pages, dpi) => {
|
|
122
|
+
let totalBytes = 0;
|
|
123
|
+
for (const { heightPoints, widthPoints } of pages) {
|
|
124
|
+
const widthPixels = Math.ceil(widthPoints * dpi / 72);
|
|
125
|
+
const heightPixels = Math.ceil(heightPoints * dpi / 72);
|
|
126
|
+
const pageBytes = widthPixels * heightPixels * 3;
|
|
127
|
+
totalBytes += pageBytes;
|
|
128
|
+
if (!Number.isSafeInteger(widthPixels) || !Number.isSafeInteger(heightPixels) || widthPixels <= 0 || heightPixels <= 0 || !Number.isSafeInteger(pageBytes) || pageBytes > PDF_RASTER_MAX_PAGE_BYTES$1 || !Number.isSafeInteger(totalBytes) || totalBytes > PDF_RASTER_MAX_TOTAL_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "PDF page geometry exceeds the selected DPI raster limits");
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const isWhitespace = (byte) => byte === 9 || byte === 10 || byte === 11 || byte === 12 || byte === 13 || byte === 32;
|
|
132
|
+
const ppmToken = (bytes, start) => {
|
|
133
|
+
let offset = start;
|
|
134
|
+
while (offset < bytes.length) {
|
|
135
|
+
if (isWhitespace(bytes[offset] ?? -1)) {
|
|
136
|
+
offset += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (bytes[offset] === 35) {
|
|
140
|
+
while (offset < bytes.length && bytes[offset] !== 10) offset += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
const tokenStart = offset;
|
|
146
|
+
while (offset < bytes.length && !isWhitespace(bytes[offset] ?? -1) && bytes[offset] !== 35) offset += 1;
|
|
147
|
+
if (tokenStart === offset) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler emitted an invalid PPM header");
|
|
148
|
+
const token = Buffer.from(bytes.subarray(tokenStart, offset)).toString("ascii");
|
|
149
|
+
if (!/^[\x21-\x7e]+$/u.test(token)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler emitted an invalid PPM header");
|
|
150
|
+
return [token, offset];
|
|
151
|
+
};
|
|
152
|
+
const parsePositiveInteger = (value) => {
|
|
153
|
+
if (!/^[1-9][0-9]*$/u.test(value)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler emitted invalid PPM dimensions");
|
|
154
|
+
const parsed = Number(value);
|
|
155
|
+
if (!Number.isSafeInteger(parsed)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Poppler PPM dimensions exceed their limit");
|
|
156
|
+
return parsed;
|
|
157
|
+
};
|
|
158
|
+
const parsePpm = (bytes) => {
|
|
159
|
+
const [magic, afterMagic] = ppmToken(bytes, 0);
|
|
160
|
+
const [widthValue, afterWidth] = ppmToken(bytes, afterMagic);
|
|
161
|
+
const [heightValue, afterHeight] = ppmToken(bytes, afterWidth);
|
|
162
|
+
const [maximumValue, afterMaximum] = ppmToken(bytes, afterHeight);
|
|
163
|
+
if (magic !== "P6" || maximumValue !== "255") throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler must emit binary RGB8 PPM pages");
|
|
164
|
+
const width = parsePositiveInteger(widthValue);
|
|
165
|
+
const height = parsePositiveInteger(heightValue);
|
|
166
|
+
const expected = width * height * 3;
|
|
167
|
+
if (!Number.isSafeInteger(expected) || expected > PDF_RASTER_MAX_PAGE_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Poppler page pixels exceed their byte limit");
|
|
168
|
+
let pixelOffset = afterMaximum;
|
|
169
|
+
if (!isWhitespace(bytes[pixelOffset] ?? -1)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler emitted an invalid PPM pixel separator");
|
|
170
|
+
const separator = bytes[pixelOffset];
|
|
171
|
+
pixelOffset += 1;
|
|
172
|
+
if (separator === 13 && bytes[pixelOffset] === 10) pixelOffset += 1;
|
|
173
|
+
if (bytes.length - pixelOffset !== expected) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler emitted a truncated or trailing PPM page");
|
|
174
|
+
return {
|
|
175
|
+
width,
|
|
176
|
+
height,
|
|
177
|
+
pixels: bytes.subarray(pixelOffset)
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
const boundedRenderedPage = async (path) => {
|
|
181
|
+
const metadata = await lstat(path);
|
|
182
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Poppler page output must be a regular file");
|
|
183
|
+
if (metadata.size > 135266304) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Poppler page output exceeds its byte limit");
|
|
184
|
+
return parsePpm(await readFile(path));
|
|
185
|
+
};
|
|
186
|
+
const integerTsvField = (value) => {
|
|
187
|
+
if (value === void 0 || !/^(?:0|[1-9][0-9]*)$/u.test(value)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Tesseract emitted invalid TSV geometry");
|
|
188
|
+
const parsed = Number(value);
|
|
189
|
+
if (!Number.isSafeInteger(parsed)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Tesseract TSV geometry exceeds its limit");
|
|
190
|
+
return parsed;
|
|
191
|
+
};
|
|
192
|
+
const parseTesseractTsv = (bytes, page, pageGeometry, pageIndex) => {
|
|
193
|
+
const lines = decodeUtf8(bytes, "Tesseract TSV").split(/\r?\n/u);
|
|
194
|
+
if ((lines.shift()?.split(" "))?.slice(0, 12).join(" ") !== "level page_num block_num par_num line_num word_num left top width height conf text") throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Tesseract emitted an unsupported TSV header");
|
|
195
|
+
const { heightPoints, widthPoints } = pageGeometry;
|
|
196
|
+
const glyphs = [];
|
|
197
|
+
let text = "";
|
|
198
|
+
let previousLine = "";
|
|
199
|
+
for (const line of lines) {
|
|
200
|
+
if (!line) continue;
|
|
201
|
+
const fields = line.split(" ");
|
|
202
|
+
if (fields[0] !== "5") continue;
|
|
203
|
+
const word = fields.slice(11).join(" ").trim();
|
|
204
|
+
if (!word) continue;
|
|
205
|
+
if (/\p{Cc}/u.test(word)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Tesseract emitted control characters in OCR text");
|
|
206
|
+
const lineKey = `${fields[2] ?? ""}/${fields[3] ?? ""}/${fields[4] ?? ""}`;
|
|
207
|
+
if (text) text += lineKey === previousLine ? " " : "\n";
|
|
208
|
+
previousLine = lineKey;
|
|
209
|
+
const start = text.length;
|
|
210
|
+
text += word;
|
|
211
|
+
const end = text.length;
|
|
212
|
+
const left = integerTsvField(fields[6]);
|
|
213
|
+
const top = integerTsvField(fields[7]);
|
|
214
|
+
const width = integerTsvField(fields[8]);
|
|
215
|
+
const height = integerTsvField(fields[9]);
|
|
216
|
+
const right = left + width;
|
|
217
|
+
const bottom = top + height;
|
|
218
|
+
if (width === 0 || height === 0 || !Number.isSafeInteger(right) || !Number.isSafeInteger(bottom) || right > page.width || bottom > page.height) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput, "Tesseract emitted out-of-page OCR geometry");
|
|
219
|
+
glyphs.push({
|
|
220
|
+
start,
|
|
221
|
+
end,
|
|
222
|
+
bounds: {
|
|
223
|
+
left: left * widthPoints / page.width,
|
|
224
|
+
bottom: heightPoints - bottom * heightPoints / page.height,
|
|
225
|
+
right: right * widthPoints / page.width,
|
|
226
|
+
top: heightPoints - top * heightPoints / page.height
|
|
227
|
+
},
|
|
228
|
+
source: "ocr"
|
|
229
|
+
});
|
|
230
|
+
if (glyphs.length > PDF_MAX_GLYPHS$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Tesseract OCR glyph count exceeds its limit");
|
|
231
|
+
if (Buffer.byteLength(text, "utf8") > PDF_MAX_OBSERVED_TEXT_UTF8_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Tesseract OCR text exceeds its byte limit");
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
pageIndex,
|
|
235
|
+
widthPoints,
|
|
236
|
+
heightPoints,
|
|
237
|
+
text,
|
|
238
|
+
glyphs,
|
|
239
|
+
rendered: true,
|
|
240
|
+
textLayer: "absent",
|
|
241
|
+
ocr: "complete",
|
|
242
|
+
imageCount: 0
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
const safeCleanup = async (directory) => {
|
|
246
|
+
const expectedPrefix = `${tmpdir()}${sep}${TEMP_PREFIX}`;
|
|
247
|
+
if (!directory.startsWith(expectedPrefix)) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed, "local PDF temporary directory could not be verified");
|
|
248
|
+
try {
|
|
249
|
+
const metadata = await lstat(directory);
|
|
250
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed, "local PDF temporary directory could not be verified");
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.code === "ENOENT") return;
|
|
253
|
+
if (error instanceof PdfLocalProviderError) throw error;
|
|
254
|
+
throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed, "local PDF temporary directory could not be inspected");
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
await rm(directory, {
|
|
258
|
+
force: true,
|
|
259
|
+
recursive: true
|
|
260
|
+
});
|
|
261
|
+
} catch {
|
|
262
|
+
throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed, "local PDF temporary files could not be removed");
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const renderPdfWithPopplerTesseract = async (options) => {
|
|
266
|
+
const resolved = validateOptions(options);
|
|
267
|
+
const pageGeometry = inspectPageGeometry(options.document);
|
|
268
|
+
assertProjectedRasterLimits(pageGeometry, resolved.dpi);
|
|
269
|
+
const pageCount = pageGeometry.length;
|
|
270
|
+
const popplerVersionResult = await runExecutable(resolved.pdftoppmPath, ["-v"], resolved.timeoutMs, "version probe");
|
|
271
|
+
const tesseractVersionResult = await runExecutable(resolved.tesseractPath, ["--version"], resolved.timeoutMs, "version probe");
|
|
272
|
+
const rendererVersion = versionFrom(popplerVersionResult.stderr.length > 0 ? popplerVersionResult.stderr : popplerVersionResult.stdout, "pdftoppm version ", "Poppler version");
|
|
273
|
+
const ocrVersion = versionFrom(tesseractVersionResult.stdout.length > 0 ? tesseractVersionResult.stdout : tesseractVersionResult.stderr, "tesseract ", "Tesseract version");
|
|
274
|
+
const directory = await mkdtemp(join(tmpdir(), TEMP_PREFIX));
|
|
275
|
+
try {
|
|
276
|
+
const sourcePath = join(directory, "source.pdf");
|
|
277
|
+
const outputPrefix = join(directory, "page");
|
|
278
|
+
await writeFile(sourcePath, options.document, {
|
|
279
|
+
flag: "wx",
|
|
280
|
+
mode: 384
|
|
281
|
+
});
|
|
282
|
+
const pages = [];
|
|
283
|
+
let totalPixels = 0;
|
|
284
|
+
let totalGlyphs = 0;
|
|
285
|
+
let totalObservedTextUtf8Bytes = 0;
|
|
286
|
+
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {
|
|
287
|
+
const inspectedGeometry = pageGeometry.at(pageNumber - 1);
|
|
288
|
+
if (inspectedGeometry === void 0) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected, "PDF page geometry became unavailable");
|
|
289
|
+
const pageOutputPrefix = `${outputPrefix}-${pageNumber}`;
|
|
290
|
+
const pagePath = `${pageOutputPrefix}.ppm`;
|
|
291
|
+
await runExecutable(resolved.pdftoppmPath, [
|
|
292
|
+
"-cropbox",
|
|
293
|
+
"-f",
|
|
294
|
+
String(pageNumber),
|
|
295
|
+
"-l",
|
|
296
|
+
String(pageNumber),
|
|
297
|
+
"-singlefile",
|
|
298
|
+
"-r",
|
|
299
|
+
String(resolved.dpi),
|
|
300
|
+
sourcePath,
|
|
301
|
+
pageOutputPrefix
|
|
302
|
+
], resolved.timeoutMs, "renderer");
|
|
303
|
+
try {
|
|
304
|
+
const page = await boundedRenderedPage(pagePath);
|
|
305
|
+
totalPixels += page.pixels.byteLength;
|
|
306
|
+
if (!Number.isSafeInteger(totalPixels) || totalPixels > PDF_RASTER_MAX_TOTAL_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Poppler page pixels exceed their aggregate limit");
|
|
307
|
+
const ocr = await runExecutable(resolved.tesseractPath, [
|
|
308
|
+
pagePath,
|
|
309
|
+
"stdout",
|
|
310
|
+
"-l",
|
|
311
|
+
resolved.ocrLanguage,
|
|
312
|
+
"tsv"
|
|
313
|
+
], resolved.timeoutMs, "OCR");
|
|
314
|
+
const observation = parseTesseractTsv(ocr.stdout, page, inspectedGeometry, pageNumber - 1);
|
|
315
|
+
totalGlyphs += observation.glyphs.length;
|
|
316
|
+
totalObservedTextUtf8Bytes += Buffer.byteLength(observation.text, "utf8");
|
|
317
|
+
if (!Number.isSafeInteger(totalGlyphs) || totalGlyphs > PDF_MAX_GLYPHS$1 || !Number.isSafeInteger(totalObservedTextUtf8Bytes) || totalObservedTextUtf8Bytes > PDF_MAX_OBSERVED_TEXT_UTF8_BYTES$1) throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded, "Tesseract OCR document output exceeds its aggregate limit");
|
|
318
|
+
pages.push({
|
|
319
|
+
observation,
|
|
320
|
+
widthPixels: page.width,
|
|
321
|
+
heightPixels: page.height,
|
|
322
|
+
pixels: page.pixels
|
|
323
|
+
});
|
|
324
|
+
} finally {
|
|
325
|
+
await unlink(pagePath).catch((error) => {
|
|
326
|
+
if (error.code === "ENOENT") return;
|
|
327
|
+
throw providerError(PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed, "local PDF page raster could not be removed");
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
provider: {
|
|
333
|
+
providerId: LOCAL_PROVIDER_ID,
|
|
334
|
+
rendererName: "Poppler pdftoppm",
|
|
335
|
+
rendererVersion,
|
|
336
|
+
ocrName: "Tesseract OCR",
|
|
337
|
+
ocrVersion,
|
|
338
|
+
ocrLanguage: resolved.ocrLanguage
|
|
339
|
+
},
|
|
340
|
+
pages
|
|
341
|
+
};
|
|
342
|
+
} finally {
|
|
343
|
+
await safeCleanup(directory);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/index.ts
|
|
348
|
+
const PDF_INSPECTION_CONTRACT_VERSION = 1;
|
|
349
|
+
const PDF_RASTER_CONTRACT_VERSION = 1;
|
|
350
|
+
const PDF_RASTER_MAX_PAGE_BYTES = 128 * 1024 * 1024;
|
|
351
|
+
const PDF_RASTER_MAX_TOTAL_BYTES = 512 * 1024 * 1024;
|
|
352
|
+
const PDF_RASTER_MAX_OUTPUT_BYTES = 512 * 1024 * 1024;
|
|
353
|
+
const PDF_RASTER_REQUEST_JSON_MAX_BYTES = 64 * 1024 * 1024;
|
|
354
|
+
const PDF_DOCUMENT_MAX_BYTES = 64 * 1024 * 1024;
|
|
355
|
+
const PDF_STREAM_DECOMPRESSED_MAX_BYTES = 32 * 1024 * 1024;
|
|
356
|
+
const PDF_LOADED_PAYLOAD_MAX_BYTES = 128 * 1024 * 1024;
|
|
357
|
+
const PDF_MAX_OBJECTS = 2e5;
|
|
358
|
+
const PDF_MAX_OBJECT_NODES = 1e6;
|
|
359
|
+
const PDF_MAX_OBJECT_DEPTH = 128;
|
|
360
|
+
const PDF_MAX_PAGES = 1e4;
|
|
361
|
+
const PDF_MAX_GLYPHS = 5e6;
|
|
362
|
+
const PDF_MAX_PAGE_TEXT_UTF8_BYTES = 16 * 1024 * 1024;
|
|
363
|
+
const PDF_MAX_OBSERVED_TEXT_UTF8_BYTES = 64 * 1024 * 1024;
|
|
364
|
+
const PDF_OBSERVATIONS_JSON_MAX_BYTES = 64 * 1024 * 1024;
|
|
365
|
+
/** Renderer dimensions may differ by this many points due to numeric rounding. */
|
|
366
|
+
const PDF_PAGE_DIMENSION_TOLERANCE_POINTS = .25;
|
|
367
|
+
var PdfInspectionError = class extends Error {
|
|
368
|
+
code;
|
|
369
|
+
constructor(code, message) {
|
|
370
|
+
super(message);
|
|
371
|
+
this.name = "PdfInspectionError";
|
|
372
|
+
this.code = code;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
var PdfRasterError = class extends Error {
|
|
376
|
+
code;
|
|
377
|
+
constructor(code, message) {
|
|
378
|
+
super(message);
|
|
379
|
+
this.name = "PdfRasterError";
|
|
380
|
+
this.code = code;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const errorCode = (message) => {
|
|
384
|
+
return Object.values(PDF_INSPECTION_ERROR_CODES).find((candidate) => message.startsWith(`${candidate}:`)) ?? PDF_INSPECTION_ERROR_CODES.invalidDocument;
|
|
385
|
+
};
|
|
386
|
+
const rasterErrorCode = (message) => {
|
|
387
|
+
return Object.values(PDF_RASTER_ERROR_CODES).find((candidate) => message.startsWith(`${candidate}:`)) ?? PDF_RASTER_ERROR_CODES.verificationFailed;
|
|
388
|
+
};
|
|
389
|
+
const inspectPdf = (document, options = {}) => {
|
|
390
|
+
const inspect = loadNativeAnonymizeBinding().inspectPdfJson;
|
|
391
|
+
if (inspect === void 0) throw new PdfInspectionError(PDF_INSPECTION_ERROR_CODES.invalidDocument, "Native anonymize binding does not expose PDF inspection");
|
|
392
|
+
try {
|
|
393
|
+
const observations = options.pageObservations;
|
|
394
|
+
return JSON.parse(inspect(document, observations === void 0 ? void 0 : JSON.stringify(observations)));
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const message = error instanceof Error ? error.message : "PDF inspection failed";
|
|
397
|
+
throw new PdfInspectionError(errorCode(message), message);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Advanced rewrite-only surface. It verifies structure and destructive pixels but
|
|
402
|
+
* does not run detection or certify that the result is PII-free.
|
|
403
|
+
*/
|
|
404
|
+
const rewritePdfRasterFromDetections = ({ document, request, pagePixels }) => {
|
|
405
|
+
const rewrite = loadNativeAnonymizeBinding().rewritePdfRasterFromDetectionsJson;
|
|
406
|
+
if (rewrite === void 0) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.verificationFailed, "Native anonymize binding does not expose PDF raster rewriting");
|
|
407
|
+
try {
|
|
408
|
+
const result = rewrite(document, JSON.stringify(request), pagePixels);
|
|
409
|
+
return {
|
|
410
|
+
document: result.document,
|
|
411
|
+
certificate: JSON.parse(result.certificateJson)
|
|
412
|
+
};
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const message = error instanceof Error ? error.message : "PDF raster rewrite failed";
|
|
415
|
+
throw new PdfRasterError(rasterErrorCode(message), message);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
419
|
+
/** Run stella detection, merge optional external detections, then rewrite pixels. */
|
|
420
|
+
const anonymizePdfRaster = ({ document, pipeline, provider, pages, fillRgb = [
|
|
421
|
+
0,
|
|
422
|
+
0,
|
|
423
|
+
0
|
|
424
|
+
] }) => {
|
|
425
|
+
if (document.byteLength > 67108864) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF source exceeds its byte limit");
|
|
426
|
+
if (pages.length > 1e4) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF raster page count exceeds its limit");
|
|
427
|
+
let totalPixelBytes = 0;
|
|
428
|
+
let totalObservedTextBytes = 0;
|
|
429
|
+
const textEncoder = new TextEncoder();
|
|
430
|
+
for (const page of pages) {
|
|
431
|
+
if (typeof page.observation?.text !== "string") throw new PdfRasterError(PDF_RASTER_ERROR_CODES.invalidContract, "invalid-contract: PDF raster observed text is invalid");
|
|
432
|
+
const observedTextBytes = textEncoder.encode(page.observation.text).byteLength;
|
|
433
|
+
if (observedTextBytes > 16777216) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF raster page text exceeds its byte limit");
|
|
434
|
+
totalObservedTextBytes += observedTextBytes;
|
|
435
|
+
if (totalObservedTextBytes > 67108864) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF raster observed text exceeds its aggregate byte limit");
|
|
436
|
+
const { heightPixels, widthPixels } = page;
|
|
437
|
+
if (!Number.isSafeInteger(widthPixels) || !Number.isSafeInteger(heightPixels) || widthPixels <= 0 || heightPixels <= 0) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.invalidContract, "invalid-contract: PDF raster pixel dimensions are invalid");
|
|
438
|
+
const expected = widthPixels * heightPixels * 3;
|
|
439
|
+
if (!Number.isSafeInteger(expected) || expected > 134217728 || page.pixels.byteLength !== expected) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF raster page pixels exceed limits or have an invalid RGB8 length");
|
|
440
|
+
totalPixelBytes += expected;
|
|
441
|
+
if (!Number.isSafeInteger(totalPixelBytes) || totalPixelBytes > 536870912) throw new PdfRasterError(PDF_RASTER_ERROR_CODES.limitExceeded, "limit-exceeded: PDF raster pixels exceed their aggregate limit");
|
|
442
|
+
}
|
|
443
|
+
const binding = loadNativeAnonymizeBinding();
|
|
444
|
+
let requestPages;
|
|
445
|
+
try {
|
|
446
|
+
requestPages = pages.map((page) => {
|
|
447
|
+
const external = page.externalDetectionBatch === void 0 ? void 0 : convert_external_detection_batch(new TextEncoder().encode(page.observation.text), page.externalDetectionBatch, { binding });
|
|
448
|
+
const result = external === void 0 ? pipeline.redactText(page.observation.text) : pipeline.redactTextWithCallerDetections(page.observation.text, { detections: external });
|
|
449
|
+
return {
|
|
450
|
+
observation: page.observation,
|
|
451
|
+
widthPixels: page.widthPixels,
|
|
452
|
+
heightPixels: page.heightPixels,
|
|
453
|
+
pixelSha256: sha256(page.pixels),
|
|
454
|
+
detections: result.resolvedEntities.map(({ start, end }) => ({
|
|
455
|
+
start,
|
|
456
|
+
end
|
|
457
|
+
}))
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
} catch {
|
|
461
|
+
throw new PdfRasterError(PDF_RASTER_ERROR_CODES.detectionFailed, "detection-failed: PDF raster detection failed");
|
|
462
|
+
}
|
|
463
|
+
return rewritePdfRasterFromDetections({
|
|
464
|
+
document,
|
|
465
|
+
request: {
|
|
466
|
+
contractVersion: 1,
|
|
467
|
+
sourceSha256: sha256(document),
|
|
468
|
+
provider,
|
|
469
|
+
fillRgb,
|
|
470
|
+
pages: requestPages
|
|
471
|
+
},
|
|
472
|
+
pagePixels: pages.map(({ pixels }) => pixels)
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
//#endregion
|
|
476
|
+
export { PDF_DOCUMENT_MAX_BYTES, PDF_INSPECTION_CONTRACT_VERSION, PDF_INSPECTION_ERROR_CODES, PDF_LOADED_PAYLOAD_MAX_BYTES, PDF_LOCAL_PROVIDER_ERROR_CODES, PDF_MAX_GLYPHS, PDF_MAX_OBJECTS, PDF_MAX_OBJECT_DEPTH, PDF_MAX_OBJECT_NODES, PDF_MAX_OBSERVED_TEXT_UTF8_BYTES, PDF_MAX_PAGES, PDF_MAX_PAGE_TEXT_UTF8_BYTES, PDF_OBSERVATIONS_JSON_MAX_BYTES, PDF_PAGE_DIMENSION_TOLERANCE_POINTS, PDF_RASTER_CONTRACT_VERSION, PDF_RASTER_ERROR_CODES, PDF_RASTER_MAX_OUTPUT_BYTES, PDF_RASTER_MAX_PAGE_BYTES, PDF_RASTER_MAX_TOTAL_BYTES, PDF_RASTER_REQUEST_JSON_MAX_BYTES, PDF_STREAM_DECOMPRESSED_MAX_BYTES, PdfInspectionError, PdfLocalProviderError, PdfRasterError, anonymizePdfRaster, inspectPdf, renderPdfWithPopplerTesseract, rewritePdfRasterFromDetections };
|
|
477
|
+
|
|
478
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["PDF_DOCUMENT_MAX_BYTES","PDF_RASTER_MAX_PAGE_BYTES","PDF_RASTER_MAX_TOTAL_BYTES","PDF_MAX_PAGES","PDF_MAX_GLYPHS","PDF_MAX_OBSERVED_TEXT_UTF8_BYTES","candidate"],"sources":["../src/types.ts","../src/local-provider.ts","../src/index.ts"],"sourcesContent":["export type PdfRect = {\n left: number;\n bottom: number;\n right: number;\n top: number;\n};\n\nexport type PdfGlyphObservation = {\n start: number;\n end: number;\n /** Bounds in normalized displayed-page PDF points, origin bottom-left. */\n bounds: PdfRect;\n source: \"embedded-text\" | \"ocr\";\n};\n\nexport type PdfPageObservation = {\n pageIndex: number;\n /** Effective visible width after page boxes, rotation, and UserUnit scaling. */\n widthPoints: number;\n /** Effective visible height after page boxes, rotation, and UserUnit scaling. */\n heightPoints: number;\n text: string;\n glyphs: readonly PdfGlyphObservation[];\n rendered: boolean;\n textLayer: \"absent\" | \"partial\" | \"complete\";\n ocr: \"not-run\" | \"partial\" | \"complete\";\n imageCount: number;\n};\n\nexport type PdfRiskInventory = {\n acroFormFieldCount: number;\n annotationCount: number;\n documentInfoEntryCount: number;\n embeddedFileCount: number;\n externalActionCount: number;\n formXObjectCount: number;\n imageObjectCount: number;\n incrementalRevisionCount: number;\n javascriptActionCount: number;\n metadataStreamCount: number;\n optionalContentGroupCount: number;\n signatureCount: number;\n trailingNonWhitespaceByteCount: number;\n unsupportedActionCount: number;\n xfaEntryCount: number;\n};\n\nexport type PdfInspectionGap =\n | \"encrypted-document\"\n | \"page-content-not-observed\"\n | \"page-not-rendered\"\n | \"partial-text-layer\"\n | \"retained-document-bytes\"\n | \"unobserved-visual-content\";\n\nexport type PdfPageInspection = {\n pageIndex: number;\n /** Effective visible width after page boxes, rotation, and UserUnit scaling. */\n widthPoints: number;\n /** Effective visible height after page boxes, rotation, and UserUnit scaling. */\n heightPoints: number;\n annotationCount: number;\n observation: PdfPageObservation | null;\n};\n\nexport type PdfInspection = {\n contractVersion: 1;\n pdfVersion: string;\n byteLength: number;\n objectCount: number;\n pageCount: number;\n encrypted: boolean;\n pages: readonly PdfPageInspection[];\n risks: PdfRiskInventory;\n coverage: {\n status: \"full\" | \"partial\";\n gaps: readonly PdfInspectionGap[];\n };\n};\n\nexport type PdfRasterProvider = {\n providerId: string;\n rendererName: string;\n rendererVersion: string;\n ocrName: string;\n ocrVersion: string;\n /** One explicit provider language pack; never an implicit mixed-language set. */\n ocrLanguage: string;\n};\n\nexport type PdfRasterPage = {\n observation: PdfPageObservation;\n widthPixels: number;\n heightPixels: number;\n /** Lowercase SHA-256 of this page's exact opaque, row-packed RGB8 bytes. */\n pixelSha256: string;\n /** Strictly ordered, non-overlapping UTF-16 spans. */\n detections: readonly PdfRasterDetection[];\n};\n\nexport type PdfRasterDetection = { start: number; end: number };\n\nexport type PdfRasterRewrite = {\n contractVersion: 1;\n /** Lowercase SHA-256 of the exact source PDF bytes supplied to the call. */\n sourceSha256: string;\n provider: PdfRasterProvider;\n fillRgb: readonly [number, number, number];\n pages: readonly PdfRasterPage[];\n};\n\nexport type PdfRasterRewriteCertificate = {\n contractVersion: 1;\n pageCount: number;\n sourceSha256: string;\n outputSha256: string;\n provider: PdfRasterProvider;\n detectionCount: number;\n mappedRegionCount: number;\n structurePixelRewriteVerified: true;\n providerAssertedCoverage: \"complete-rendering-and-ocr-observation\";\n piiCleanGuaranteed: false;\n limitation: string;\n};\n\nexport type PdfRasterRewriteResult = {\n document: Uint8Array;\n certificate: PdfRasterRewriteCertificate;\n};\n\nexport const PDF_INSPECTION_ERROR_CODES = {\n documentLimitExceeded: \"document-limit-exceeded\",\n invalidDocument: \"invalid-document\",\n invalidObservation: \"invalid-observation\",\n observationLimitExceeded: \"observation-limit-exceeded\",\n providerFailed: \"provider-failed\",\n} as const;\n\nexport type PdfInspectionErrorCode =\n (typeof PDF_INSPECTION_ERROR_CODES)[keyof typeof PDF_INSPECTION_ERROR_CODES];\n\nexport const PDF_RASTER_ERROR_CODES = {\n detectionFailed: \"detection-failed\",\n invalidContract: \"invalid-contract\",\n limitExceeded: \"limit-exceeded\",\n sourceRejected: \"source-rejected\",\n verificationFailed: \"verification-failed\",\n} as const;\n\nexport type PdfRasterErrorCode =\n (typeof PDF_RASTER_ERROR_CODES)[keyof typeof PDF_RASTER_ERROR_CODES];\n","import { execFile } from \"node:child_process\";\nimport {\n lstat,\n mkdtemp,\n readFile,\n rm,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join, sep } from \"node:path\";\n\nimport { loadNativeAnonymizeBinding } from \"@stll/anonymize\";\n\nimport type {\n PdfGlyphObservation,\n PdfPageObservation,\n PdfRasterProvider,\n} from \"./types\";\n\nconst LOCAL_PROVIDER_ID = \"poppler-tesseract-local\";\nconst PROCESS_OUTPUT_MAX_BYTES = 64 * 1024 * 1024;\nconst PDF_DOCUMENT_MAX_BYTES = 64 * 1024 * 1024;\nconst PDF_RASTER_MAX_PAGE_BYTES = 128 * 1024 * 1024;\nconst PDF_RASTER_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\nconst PDF_MAX_PAGES = 10_000;\nconst PDF_MAX_GLYPHS = 5_000_000;\nconst PDF_MAX_OBSERVED_TEXT_UTF8_BYTES = 64 * 1024 * 1024;\nconst DEFAULT_DPI = 300;\nconst DEFAULT_TIMEOUT_MS = 120_000;\nconst MIN_DPI = 72;\nconst MAX_DPI = 600;\nconst MIN_TIMEOUT_MS = 100;\nconst MAX_TIMEOUT_MS = 300_000;\nconst LANGUAGE_PACK_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/u;\nconst TEMP_PREFIX = \"stella-anonymize-pdf-\";\n\nexport const PDF_LOCAL_PROVIDER_ERROR_CODES = {\n cleanupFailed: \"cleanup-failed\",\n executableFailed: \"executable-failed\",\n invalidOptions: \"invalid-options\",\n invalidOutput: \"invalid-output\",\n limitExceeded: \"limit-exceeded\",\n sourceRejected: \"source-rejected\",\n} as const;\n\nexport type PdfLocalProviderErrorCode =\n (typeof PDF_LOCAL_PROVIDER_ERROR_CODES)[keyof typeof PDF_LOCAL_PROVIDER_ERROR_CODES];\n\nexport class PdfLocalProviderError extends Error {\n readonly code: PdfLocalProviderErrorCode;\n\n constructor(code: PdfLocalProviderErrorCode, message: string) {\n super(message);\n this.name = \"PdfLocalProviderError\";\n this.code = code;\n }\n}\n\nexport type RenderPdfWithPopplerTesseractOptions = {\n document: Uint8Array;\n /** One explicit installed Tesseract traineddata name, for example `eng`. */\n ocrLanguage: string;\n dpi?: number | undefined;\n pdftoppmPath?: string | undefined;\n tesseractPath?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\nexport type LocalPdfRasterPage = {\n observation: PdfPageObservation;\n widthPixels: number;\n heightPixels: number;\n pixels: Uint8Array;\n};\n\nexport type LocalPdfRasterObservation = {\n provider: PdfRasterProvider;\n pages: readonly LocalPdfRasterPage[];\n};\n\ntype ResolvedLocalProviderOptions = {\n dpi: number;\n ocrLanguage: string;\n pdftoppmPath: string;\n tesseractPath: string;\n timeoutMs: number;\n};\n\ntype ProcessResult = { stdout: Buffer; stderr: Buffer };\n\nconst providerError = (\n code: PdfLocalProviderErrorCode,\n message: string,\n): PdfLocalProviderError =>\n new PdfLocalProviderError(code, `${code}: ${message}`);\n\nconst validateOptions = (\n options: RenderPdfWithPopplerTesseractOptions,\n): ResolvedLocalProviderOptions => {\n const dpi = options.dpi ?? DEFAULT_DPI;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const pdftoppmPath = options.pdftoppmPath ?? \"pdftoppm\";\n const tesseractPath = options.tesseractPath ?? \"tesseract\";\n if (\n !Number.isInteger(dpi) ||\n dpi < MIN_DPI ||\n dpi > MAX_DPI ||\n !Number.isInteger(timeoutMs) ||\n timeoutMs < MIN_TIMEOUT_MS ||\n timeoutMs > MAX_TIMEOUT_MS ||\n !LANGUAGE_PACK_PATTERN.test(options.ocrLanguage) ||\n !pdftoppmPath ||\n pdftoppmPath.includes(\"\\0\") ||\n !tesseractPath ||\n tesseractPath.includes(\"\\0\")\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOptions,\n \"local PDF provider options are invalid\",\n );\n }\n return {\n dpi,\n ocrLanguage: options.ocrLanguage,\n pdftoppmPath,\n tesseractPath,\n timeoutMs,\n };\n};\n\nconst runExecutable = (\n executable: string,\n args: readonly string[],\n timeoutMs: number,\n stage: \"OCR\" | \"renderer\" | \"version probe\",\n): Promise<ProcessResult> =>\n new Promise((resolve, reject) => {\n execFile(\n executable,\n args,\n {\n encoding: \"buffer\",\n maxBuffer: PROCESS_OUTPUT_MAX_BYTES,\n timeout: timeoutMs,\n windowsHide: true,\n },\n (error, stdout, stderr) => {\n if (error !== null) {\n reject(\n providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.executableFailed,\n `local PDF ${stage} failed`,\n ),\n );\n return;\n }\n resolve({ stdout: Buffer.from(stdout), stderr: Buffer.from(stderr) });\n },\n );\n });\n\nconst decodeUtf8 = (value: Uint8Array, context: string): string => {\n try {\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(value);\n } catch {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n `local PDF ${context} is not valid UTF-8`,\n );\n }\n};\n\nconst versionFrom = (\n value: Uint8Array,\n prefix: string,\n context: string,\n): string => {\n const line = decodeUtf8(value, context).split(/\\r?\\n/u, 1)[0]?.trim();\n const version = line?.startsWith(prefix)\n ? line.slice(prefix.length).trim()\n : \"\";\n if (!version || version.length > 256 || /\\p{Cc}/u.test(version)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n `local PDF ${context} is invalid`,\n );\n }\n return version;\n};\n\ntype InspectedPageGeometry = {\n heightPoints: number;\n widthPoints: number;\n};\n\nconst inspectPageGeometry = (\n document: Uint8Array,\n): readonly InspectedPageGeometry[] => {\n if (document.byteLength > PDF_DOCUMENT_MAX_BYTES) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"PDF source exceeds its byte limit\",\n );\n }\n const inspect = loadNativeAnonymizeBinding().inspectPdfJson;\n if (inspect === undefined) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected,\n \"native PDF inspection is unavailable\",\n );\n }\n try {\n const inspection = JSON.parse(inspect(document)) as {\n encrypted?: unknown;\n pageCount?: unknown;\n pages?: unknown;\n };\n if (\n inspection.encrypted === true ||\n !Number.isSafeInteger(inspection.pageCount) ||\n (inspection.pageCount as number) < 1 ||\n (inspection.pageCount as number) > PDF_MAX_PAGES ||\n !Array.isArray(inspection.pages) ||\n inspection.pages.length !== inspection.pageCount\n ) {\n throw new Error(\"source rejected\");\n }\n return inspection.pages.map((page, pageIndex) => {\n if (typeof page !== \"object\" || page === null) {\n throw new Error(\"source rejected\");\n }\n const candidate = page as Record<string, unknown>;\n const {\n heightPoints,\n pageIndex: inspectedPageIndex,\n widthPoints,\n } = candidate;\n if (\n inspectedPageIndex !== pageIndex ||\n typeof widthPoints !== \"number\" ||\n !Number.isFinite(widthPoints) ||\n widthPoints <= 0 ||\n typeof heightPoints !== \"number\" ||\n !Number.isFinite(heightPoints) ||\n heightPoints <= 0\n ) {\n throw new Error(\"source rejected\");\n }\n return { heightPoints, widthPoints };\n });\n } catch {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected,\n \"PDF source is unsupported by the local raster provider\",\n );\n }\n};\n\nconst assertProjectedRasterLimits = (\n pages: readonly InspectedPageGeometry[],\n dpi: number,\n): void => {\n let totalBytes = 0;\n for (const { heightPoints, widthPoints } of pages) {\n const widthPixels = Math.ceil((widthPoints * dpi) / 72);\n const heightPixels = Math.ceil((heightPoints * dpi) / 72);\n const pageBytes = widthPixels * heightPixels * 3;\n totalBytes += pageBytes;\n if (\n !Number.isSafeInteger(widthPixels) ||\n !Number.isSafeInteger(heightPixels) ||\n widthPixels <= 0 ||\n heightPixels <= 0 ||\n !Number.isSafeInteger(pageBytes) ||\n pageBytes > PDF_RASTER_MAX_PAGE_BYTES ||\n !Number.isSafeInteger(totalBytes) ||\n totalBytes > PDF_RASTER_MAX_TOTAL_BYTES\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"PDF page geometry exceeds the selected DPI raster limits\",\n );\n }\n }\n};\n\ntype PpmPage = {\n width: number;\n height: number;\n pixels: Uint8Array;\n};\n\nconst isWhitespace = (byte: number): boolean =>\n byte === 0x09 ||\n byte === 0x0a ||\n byte === 0x0b ||\n byte === 0x0c ||\n byte === 0x0d ||\n byte === 0x20;\n\nconst ppmToken = (bytes: Uint8Array, start: number): [string, number] => {\n let offset = start;\n while (offset < bytes.length) {\n if (isWhitespace(bytes[offset] ?? -1)) {\n offset += 1;\n continue;\n }\n if (bytes[offset] === 0x23) {\n while (offset < bytes.length && bytes[offset] !== 0x0a) offset += 1;\n continue;\n }\n break;\n }\n const tokenStart = offset;\n while (\n offset < bytes.length &&\n !isWhitespace(bytes[offset] ?? -1) &&\n bytes[offset] !== 0x23\n ) {\n offset += 1;\n }\n if (tokenStart === offset) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler emitted an invalid PPM header\",\n );\n }\n const token = Buffer.from(bytes.subarray(tokenStart, offset)).toString(\n \"ascii\",\n );\n if (!/^[\\x21-\\x7e]+$/u.test(token)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler emitted an invalid PPM header\",\n );\n }\n return [token, offset];\n};\n\nconst parsePositiveInteger = (value: string): number => {\n if (!/^[1-9][0-9]*$/u.test(value)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler emitted invalid PPM dimensions\",\n );\n }\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Poppler PPM dimensions exceed their limit\",\n );\n }\n return parsed;\n};\n\nconst parsePpm = (bytes: Uint8Array): PpmPage => {\n const [magic, afterMagic] = ppmToken(bytes, 0);\n const [widthValue, afterWidth] = ppmToken(bytes, afterMagic);\n const [heightValue, afterHeight] = ppmToken(bytes, afterWidth);\n const [maximumValue, afterMaximum] = ppmToken(bytes, afterHeight);\n if (magic !== \"P6\" || maximumValue !== \"255\") {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler must emit binary RGB8 PPM pages\",\n );\n }\n const width = parsePositiveInteger(widthValue);\n const height = parsePositiveInteger(heightValue);\n const expected = width * height * 3;\n if (!Number.isSafeInteger(expected) || expected > PDF_RASTER_MAX_PAGE_BYTES) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Poppler page pixels exceed their byte limit\",\n );\n }\n let pixelOffset = afterMaximum;\n if (!isWhitespace(bytes[pixelOffset] ?? -1)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler emitted an invalid PPM pixel separator\",\n );\n }\n const separator = bytes[pixelOffset];\n pixelOffset += 1;\n if (separator === 0x0d && bytes[pixelOffset] === 0x0a) pixelOffset += 1;\n if (bytes.length - pixelOffset !== expected) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler emitted a truncated or trailing PPM page\",\n );\n }\n return { width, height, pixels: bytes.subarray(pixelOffset) };\n};\n\nconst boundedRenderedPage = async (path: string): Promise<PpmPage> => {\n const metadata = await lstat(path);\n if (!metadata.isFile() || metadata.isSymbolicLink()) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Poppler page output must be a regular file\",\n );\n }\n if (metadata.size > PDF_RASTER_MAX_PAGE_BYTES + 1024 * 1024) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Poppler page output exceeds its byte limit\",\n );\n }\n return parsePpm(await readFile(path));\n};\n\nconst integerTsvField = (value: string | undefined): number => {\n if (value === undefined || !/^(?:0|[1-9][0-9]*)$/u.test(value)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Tesseract emitted invalid TSV geometry\",\n );\n }\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Tesseract TSV geometry exceeds its limit\",\n );\n }\n return parsed;\n};\n\nconst parseTesseractTsv = (\n bytes: Uint8Array,\n page: PpmPage,\n pageGeometry: InspectedPageGeometry,\n pageIndex: number,\n): PdfPageObservation => {\n const lines = decodeUtf8(bytes, \"Tesseract TSV\").split(/\\r?\\n/u);\n const header = lines.shift()?.split(\"\\t\");\n if (\n header?.slice(0, 12).join(\"\\t\") !==\n \"level\\tpage_num\\tblock_num\\tpar_num\\tline_num\\tword_num\\tleft\\ttop\\twidth\\theight\\tconf\\ttext\"\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Tesseract emitted an unsupported TSV header\",\n );\n }\n const { heightPoints, widthPoints } = pageGeometry;\n const glyphs: PdfGlyphObservation[] = [];\n let text = \"\";\n let previousLine = \"\";\n for (const line of lines) {\n if (!line) continue;\n const fields = line.split(\"\\t\");\n if (fields[0] !== \"5\") continue;\n const word = fields.slice(11).join(\"\\t\").trim();\n if (!word) continue;\n if (/\\p{Cc}/u.test(word)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Tesseract emitted control characters in OCR text\",\n );\n }\n const lineKey = `${fields[2] ?? \"\"}/${fields[3] ?? \"\"}/${fields[4] ?? \"\"}`;\n if (text) text += lineKey === previousLine ? \" \" : \"\\n\";\n previousLine = lineKey;\n const start = text.length;\n text += word;\n const end = text.length;\n const left = integerTsvField(fields[6]);\n const top = integerTsvField(fields[7]);\n const width = integerTsvField(fields[8]);\n const height = integerTsvField(fields[9]);\n const right = left + width;\n const bottom = top + height;\n if (\n width === 0 ||\n height === 0 ||\n !Number.isSafeInteger(right) ||\n !Number.isSafeInteger(bottom) ||\n right > page.width ||\n bottom > page.height\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.invalidOutput,\n \"Tesseract emitted out-of-page OCR geometry\",\n );\n }\n glyphs.push({\n start,\n end,\n bounds: {\n left: (left * widthPoints) / page.width,\n bottom: heightPoints - (bottom * heightPoints) / page.height,\n right: (right * widthPoints) / page.width,\n top: heightPoints - (top * heightPoints) / page.height,\n },\n source: \"ocr\",\n });\n if (glyphs.length > PDF_MAX_GLYPHS) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Tesseract OCR glyph count exceeds its limit\",\n );\n }\n if (Buffer.byteLength(text, \"utf8\") > PDF_MAX_OBSERVED_TEXT_UTF8_BYTES) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Tesseract OCR text exceeds its byte limit\",\n );\n }\n }\n return {\n pageIndex,\n widthPoints,\n heightPoints,\n text,\n glyphs,\n rendered: true,\n textLayer: \"absent\",\n ocr: \"complete\",\n imageCount: 0,\n };\n};\n\nconst safeCleanup = async (directory: string): Promise<void> => {\n const expectedPrefix = `${tmpdir()}${sep}${TEMP_PREFIX}`;\n if (!directory.startsWith(expectedPrefix)) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed,\n \"local PDF temporary directory could not be verified\",\n );\n }\n try {\n const metadata = await lstat(directory);\n if (!metadata.isDirectory() || metadata.isSymbolicLink()) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed,\n \"local PDF temporary directory could not be verified\",\n );\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n if (error instanceof PdfLocalProviderError) throw error;\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed,\n \"local PDF temporary directory could not be inspected\",\n );\n }\n try {\n await rm(directory, { force: true, recursive: true });\n } catch {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed,\n \"local PDF temporary files could not be removed\",\n );\n }\n};\n\nexport const renderPdfWithPopplerTesseract = async (\n options: RenderPdfWithPopplerTesseractOptions,\n): Promise<LocalPdfRasterObservation> => {\n const resolved = validateOptions(options);\n const pageGeometry = inspectPageGeometry(options.document);\n assertProjectedRasterLimits(pageGeometry, resolved.dpi);\n const pageCount = pageGeometry.length;\n const popplerVersionResult = await runExecutable(\n resolved.pdftoppmPath,\n [\"-v\"],\n resolved.timeoutMs,\n \"version probe\",\n );\n const tesseractVersionResult = await runExecutable(\n resolved.tesseractPath,\n [\"--version\"],\n resolved.timeoutMs,\n \"version probe\",\n );\n const rendererVersion = versionFrom(\n popplerVersionResult.stderr.length > 0\n ? popplerVersionResult.stderr\n : popplerVersionResult.stdout,\n \"pdftoppm version \",\n \"Poppler version\",\n );\n const ocrVersion = versionFrom(\n tesseractVersionResult.stdout.length > 0\n ? tesseractVersionResult.stdout\n : tesseractVersionResult.stderr,\n \"tesseract \",\n \"Tesseract version\",\n );\n const directory = await mkdtemp(join(tmpdir(), TEMP_PREFIX));\n try {\n const sourcePath = join(directory, \"source.pdf\");\n const outputPrefix = join(directory, \"page\");\n await writeFile(sourcePath, options.document, { flag: \"wx\", mode: 0o600 });\n const pages: LocalPdfRasterPage[] = [];\n let totalPixels = 0;\n let totalGlyphs = 0;\n let totalObservedTextUtf8Bytes = 0;\n for (let pageNumber = 1; pageNumber <= pageCount; pageNumber += 1) {\n const inspectedGeometry = pageGeometry.at(pageNumber - 1);\n if (inspectedGeometry === undefined) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.sourceRejected,\n \"PDF page geometry became unavailable\",\n );\n }\n const pageOutputPrefix = `${outputPrefix}-${pageNumber}`;\n const pagePath = `${pageOutputPrefix}.ppm`;\n await runExecutable(\n resolved.pdftoppmPath,\n [\n \"-cropbox\",\n \"-f\",\n String(pageNumber),\n \"-l\",\n String(pageNumber),\n \"-singlefile\",\n \"-r\",\n String(resolved.dpi),\n sourcePath,\n pageOutputPrefix,\n ],\n resolved.timeoutMs,\n \"renderer\",\n );\n try {\n const page = await boundedRenderedPage(pagePath);\n totalPixels += page.pixels.byteLength;\n if (\n !Number.isSafeInteger(totalPixels) ||\n totalPixels > PDF_RASTER_MAX_TOTAL_BYTES\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Poppler page pixels exceed their aggregate limit\",\n );\n }\n const ocr = await runExecutable(\n resolved.tesseractPath,\n [pagePath, \"stdout\", \"-l\", resolved.ocrLanguage, \"tsv\"],\n resolved.timeoutMs,\n \"OCR\",\n );\n const observation = parseTesseractTsv(\n ocr.stdout,\n page,\n inspectedGeometry,\n pageNumber - 1,\n );\n totalGlyphs += observation.glyphs.length;\n totalObservedTextUtf8Bytes += Buffer.byteLength(\n observation.text,\n \"utf8\",\n );\n if (\n !Number.isSafeInteger(totalGlyphs) ||\n totalGlyphs > PDF_MAX_GLYPHS ||\n !Number.isSafeInteger(totalObservedTextUtf8Bytes) ||\n totalObservedTextUtf8Bytes > PDF_MAX_OBSERVED_TEXT_UTF8_BYTES\n ) {\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.limitExceeded,\n \"Tesseract OCR document output exceeds its aggregate limit\",\n );\n }\n pages.push({\n observation,\n widthPixels: page.width,\n heightPixels: page.height,\n pixels: page.pixels,\n });\n } finally {\n await unlink(pagePath).catch((error: NodeJS.ErrnoException) => {\n if (error.code === \"ENOENT\") return;\n throw providerError(\n PDF_LOCAL_PROVIDER_ERROR_CODES.cleanupFailed,\n \"local PDF page raster could not be removed\",\n );\n });\n }\n }\n return {\n provider: {\n providerId: LOCAL_PROVIDER_ID,\n rendererName: \"Poppler pdftoppm\",\n rendererVersion,\n ocrName: \"Tesseract OCR\",\n ocrVersion,\n ocrLanguage: resolved.ocrLanguage,\n },\n pages,\n };\n } finally {\n await safeCleanup(directory);\n }\n};\n","import { createHash } from \"node:crypto\";\n\nimport {\n convert_external_detection_batch,\n loadNativeAnonymizeBinding,\n type ExternalDetectionBatch,\n type PreparedNativePipeline,\n} from \"@stll/anonymize\";\n\nimport {\n PDF_INSPECTION_ERROR_CODES,\n PDF_RASTER_ERROR_CODES,\n type PdfInspection,\n type PdfInspectionErrorCode,\n type PdfPageObservation,\n type PdfRasterProvider,\n type PdfRasterErrorCode,\n type PdfRasterRewrite,\n type PdfRasterRewriteResult,\n} from \"./types\";\n\nexport const PDF_INSPECTION_CONTRACT_VERSION = 1 as const;\nexport const PDF_RASTER_CONTRACT_VERSION = 1 as const;\nexport const PDF_RASTER_MAX_PAGE_BYTES = 128 * 1024 * 1024;\nexport const PDF_RASTER_MAX_TOTAL_BYTES = 512 * 1024 * 1024;\nexport const PDF_RASTER_MAX_OUTPUT_BYTES = 512 * 1024 * 1024;\nexport const PDF_RASTER_REQUEST_JSON_MAX_BYTES = 64 * 1024 * 1024;\nexport const PDF_DOCUMENT_MAX_BYTES = 64 * 1024 * 1024;\nexport const PDF_STREAM_DECOMPRESSED_MAX_BYTES = 32 * 1024 * 1024;\nexport const PDF_LOADED_PAYLOAD_MAX_BYTES = 128 * 1024 * 1024;\nexport const PDF_MAX_OBJECTS = 200_000;\nexport const PDF_MAX_OBJECT_NODES = 1_000_000;\nexport const PDF_MAX_OBJECT_DEPTH = 128;\nexport const PDF_MAX_PAGES = 10_000;\nexport const PDF_MAX_GLYPHS = 5_000_000;\nexport const PDF_MAX_PAGE_TEXT_UTF8_BYTES = 16 * 1024 * 1024;\nexport const PDF_MAX_OBSERVED_TEXT_UTF8_BYTES = 64 * 1024 * 1024;\nexport const PDF_OBSERVATIONS_JSON_MAX_BYTES = 64 * 1024 * 1024;\n/** Renderer dimensions may differ by this many points due to numeric rounding. */\nexport const PDF_PAGE_DIMENSION_TOLERANCE_POINTS = 0.25;\n\nexport class PdfInspectionError extends Error {\n readonly code: PdfInspectionErrorCode;\n\n constructor(code: PdfInspectionErrorCode, message: string) {\n super(message);\n this.name = \"PdfInspectionError\";\n this.code = code;\n }\n}\n\nexport class PdfRasterError extends Error {\n readonly code: PdfRasterErrorCode;\n\n constructor(code: PdfRasterErrorCode, message: string) {\n super(message);\n this.name = \"PdfRasterError\";\n this.code = code;\n }\n}\n\nconst errorCode = (message: string): PdfInspectionErrorCode => {\n const code = Object.values(PDF_INSPECTION_ERROR_CODES).find((candidate) =>\n message.startsWith(`${candidate}:`),\n );\n return code ?? PDF_INSPECTION_ERROR_CODES.invalidDocument;\n};\n\nconst rasterErrorCode = (message: string): PdfRasterErrorCode => {\n const code = Object.values(PDF_RASTER_ERROR_CODES).find((candidate) =>\n message.startsWith(`${candidate}:`),\n );\n return code ?? PDF_RASTER_ERROR_CODES.verificationFailed;\n};\n\nexport type InspectPdfOptions = {\n pageObservations?: readonly PdfPageObservation[];\n};\n\nexport const inspectPdf = (\n document: Uint8Array,\n options: InspectPdfOptions = {},\n): PdfInspection => {\n const inspect = loadNativeAnonymizeBinding().inspectPdfJson;\n if (inspect === undefined) {\n throw new PdfInspectionError(\n PDF_INSPECTION_ERROR_CODES.invalidDocument,\n \"Native anonymize binding does not expose PDF inspection\",\n );\n }\n try {\n const observations = options.pageObservations;\n return JSON.parse(\n inspect(\n document,\n observations === undefined ? undefined : JSON.stringify(observations),\n ),\n ) as PdfInspection;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"PDF inspection failed\";\n throw new PdfInspectionError(errorCode(message), message);\n }\n};\n\nexport type RewritePdfRasterFromDetectionsOptions = {\n document: Uint8Array;\n request: PdfRasterRewrite;\n /** One opaque, row-packed RGB8 buffer per request page, in page order. */\n pagePixels: readonly Uint8Array[];\n};\n\n/**\n * Advanced rewrite-only surface. It verifies structure and destructive pixels but\n * does not run detection or certify that the result is PII-free.\n */\nexport const rewritePdfRasterFromDetections = ({\n document,\n request,\n pagePixels,\n}: RewritePdfRasterFromDetectionsOptions): PdfRasterRewriteResult => {\n const rewrite =\n loadNativeAnonymizeBinding().rewritePdfRasterFromDetectionsJson;\n if (rewrite === undefined) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.verificationFailed,\n \"Native anonymize binding does not expose PDF raster rewriting\",\n );\n }\n try {\n const result = rewrite(document, JSON.stringify(request), pagePixels);\n return {\n document: result.document,\n certificate: JSON.parse(result.certificateJson),\n } as PdfRasterRewriteResult;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"PDF raster rewrite failed\";\n throw new PdfRasterError(rasterErrorCode(message), message);\n }\n};\n\nexport type PdfRasterObservedPageInput = {\n observation: PdfPageObservation;\n widthPixels: number;\n heightPixels: number;\n pixels: Uint8Array;\n /** Optional digest-bound provider batch merged through stella caller semantics. */\n externalDetectionBatch?: ExternalDetectionBatch | string | undefined;\n};\n\nexport type AnonymizePdfRasterOptions = {\n document: Uint8Array;\n pipeline: Pick<\n PreparedNativePipeline,\n \"redactText\" | \"redactTextWithCallerDetections\"\n >;\n provider: PdfRasterProvider;\n pages: readonly PdfRasterObservedPageInput[];\n fillRgb?: readonly [number, number, number] | undefined;\n};\n\nconst sha256 = (value: Uint8Array): string =>\n createHash(\"sha256\").update(value).digest(\"hex\");\n\n/** Run stella detection, merge optional external detections, then rewrite pixels. */\nexport const anonymizePdfRaster = ({\n document,\n pipeline,\n provider,\n pages,\n fillRgb = [0, 0, 0],\n}: AnonymizePdfRasterOptions): PdfRasterRewriteResult => {\n if (document.byteLength > PDF_DOCUMENT_MAX_BYTES) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF source exceeds its byte limit\",\n );\n }\n if (pages.length > PDF_MAX_PAGES) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF raster page count exceeds its limit\",\n );\n }\n let totalPixelBytes = 0;\n let totalObservedTextBytes = 0;\n const textEncoder = new TextEncoder();\n for (const page of pages) {\n if (typeof page.observation?.text !== \"string\") {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.invalidContract,\n \"invalid-contract: PDF raster observed text is invalid\",\n );\n }\n const observedTextBytes = textEncoder.encode(\n page.observation.text,\n ).byteLength;\n if (observedTextBytes > PDF_MAX_PAGE_TEXT_UTF8_BYTES) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF raster page text exceeds its byte limit\",\n );\n }\n totalObservedTextBytes += observedTextBytes;\n if (totalObservedTextBytes > PDF_MAX_OBSERVED_TEXT_UTF8_BYTES) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF raster observed text exceeds its aggregate byte limit\",\n );\n }\n const { heightPixels, widthPixels } = page;\n if (\n !Number.isSafeInteger(widthPixels) ||\n !Number.isSafeInteger(heightPixels) ||\n widthPixels <= 0 ||\n heightPixels <= 0\n ) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.invalidContract,\n \"invalid-contract: PDF raster pixel dimensions are invalid\",\n );\n }\n const expected = widthPixels * heightPixels * 3;\n if (\n !Number.isSafeInteger(expected) ||\n expected > PDF_RASTER_MAX_PAGE_BYTES ||\n page.pixels.byteLength !== expected\n ) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF raster page pixels exceed limits or have an invalid RGB8 length\",\n );\n }\n totalPixelBytes += expected;\n if (\n !Number.isSafeInteger(totalPixelBytes) ||\n totalPixelBytes > PDF_RASTER_MAX_TOTAL_BYTES\n ) {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.limitExceeded,\n \"limit-exceeded: PDF raster pixels exceed their aggregate limit\",\n );\n }\n }\n const binding = loadNativeAnonymizeBinding();\n let requestPages: PdfRasterRewrite[\"pages\"];\n try {\n requestPages = pages.map((page) => {\n const external =\n page.externalDetectionBatch === undefined\n ? undefined\n : convert_external_detection_batch(\n new TextEncoder().encode(page.observation.text),\n page.externalDetectionBatch,\n { binding },\n );\n const result =\n external === undefined\n ? pipeline.redactText(page.observation.text)\n : pipeline.redactTextWithCallerDetections(page.observation.text, {\n detections: external,\n });\n return {\n observation: page.observation,\n widthPixels: page.widthPixels,\n heightPixels: page.heightPixels,\n pixelSha256: sha256(page.pixels),\n detections: result.resolvedEntities.map(({ start, end }) => ({\n start,\n end,\n })),\n };\n });\n } catch {\n throw new PdfRasterError(\n PDF_RASTER_ERROR_CODES.detectionFailed,\n \"detection-failed: PDF raster detection failed\",\n );\n }\n return rewritePdfRasterFromDetections({\n document,\n request: {\n contractVersion: PDF_RASTER_CONTRACT_VERSION,\n sourceSha256: sha256(document),\n provider,\n fillRgb,\n pages: requestPages,\n },\n pagePixels: pages.map(({ pixels }) => pixels),\n });\n};\n\nexport { PDF_INSPECTION_ERROR_CODES } from \"./types\";\nexport { PDF_RASTER_ERROR_CODES } from \"./types\";\nexport {\n PDF_LOCAL_PROVIDER_ERROR_CODES,\n PdfLocalProviderError,\n renderPdfWithPopplerTesseract,\n} from \"./local-provider\";\nexport type {\n LocalPdfRasterObservation,\n LocalPdfRasterPage,\n PdfLocalProviderErrorCode,\n RenderPdfWithPopplerTesseractOptions,\n} from \"./local-provider\";\nexport type {\n PdfGlyphObservation,\n PdfInspection,\n PdfInspectionErrorCode,\n PdfInspectionGap,\n PdfPageInspection,\n PdfPageObservation,\n PdfRect,\n PdfRiskInventory,\n PdfRasterDetection,\n PdfRasterErrorCode,\n PdfRasterPage,\n PdfRasterProvider,\n PdfRasterRewrite,\n PdfRasterRewriteCertificate,\n PdfRasterRewriteResult,\n} from \"./types\";\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/anonymize-pdf",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "Fail-closed PDF coverage and risk inspection contracts for stella anonymization",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"default": "./dist/index.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"ATTRIBUTION.md",
|
|
15
|
+
"dist",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/stella/anonymize.git"
|
|
25
|
+
},
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown",
|
|
29
|
+
"typecheck": "bun ../../scripts/tsc-native.ts --noEmit -p tsconfig.json && bun ../../scripts/tsc-native.ts --noEmit -p tsconfig.test.json",
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"format": "oxfmt ."
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@stll/anonymize": "2.4.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^26.1.1",
|
|
38
|
+
"bun-types": "^1.3.14",
|
|
39
|
+
"tsdown": "^0.22.7",
|
|
40
|
+
"typescript": "^6.0.3"
|
|
41
|
+
}
|
|
6
42
|
}
|