nf-key-extractor 0.1.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/LICENSE +21 -0
- package/README.md +427 -0
- package/benchmarks/README.md +53 -0
- package/dist/cli.cjs +1665 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1644 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1546 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +1511 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1546 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// src/errors.ts
|
|
39
|
+
function messageFromUnknown(error) {
|
|
40
|
+
return error instanceof Error ? error.message : "Unknown processing error.";
|
|
41
|
+
}
|
|
42
|
+
var ExtractionFailure;
|
|
43
|
+
var init_errors = __esm({
|
|
44
|
+
"src/errors.ts"() {
|
|
45
|
+
"use strict";
|
|
46
|
+
ExtractionFailure = class extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code, message, options) {
|
|
49
|
+
super(message, options);
|
|
50
|
+
this.name = "ExtractionFailure";
|
|
51
|
+
this.code = code;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// src/pdf/render-page.ts
|
|
58
|
+
var render_page_exports = {};
|
|
59
|
+
__export(render_page_exports, {
|
|
60
|
+
renderPage: () => renderPage
|
|
61
|
+
});
|
|
62
|
+
async function renderPage(page, recipe, maxPixels) {
|
|
63
|
+
const { createCanvas } = await import("@napi-rs/canvas");
|
|
64
|
+
const rotation = ((page.rotate + recipe.rotation) % 360 + 360) % 360;
|
|
65
|
+
let scale = recipe.scale;
|
|
66
|
+
let viewport = page.getViewport({ scale, rotation });
|
|
67
|
+
let width = 0;
|
|
68
|
+
let height = 0;
|
|
69
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
70
|
+
if (!Number.isFinite(viewport.width) || !Number.isFinite(viewport.height) || viewport.width <= 0 || viewport.height <= 0) {
|
|
71
|
+
throw new ExtractionFailure("INVALID_PDF", "A PDF page has invalid dimensions.");
|
|
72
|
+
}
|
|
73
|
+
width = Math.max(1, Math.ceil(viewport.width));
|
|
74
|
+
height = Math.max(1, Math.ceil(viewport.height));
|
|
75
|
+
const allocatedPixels = width * height;
|
|
76
|
+
if (width <= MAX_CANVAS_DIMENSION && height <= MAX_CANVAS_DIMENSION && allocatedPixels <= maxPixels) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const hasSubpixelAxis = viewport.width < 1 || viewport.height < 1;
|
|
80
|
+
const pixelFactor = hasSubpixelAxis ? maxPixels / allocatedPixels : Math.sqrt(maxPixels / allocatedPixels);
|
|
81
|
+
const reduction = Math.min(pixelFactor, MAX_CANVAS_DIMENSION / width, MAX_CANVAS_DIMENSION / height) * 0.999;
|
|
82
|
+
if (!Number.isFinite(reduction) || reduction <= 0 || reduction >= 1) {
|
|
83
|
+
throw new ExtractionFailure("INVALID_PDF", "A PDF page exceeds the supported render dimensions.");
|
|
84
|
+
}
|
|
85
|
+
scale *= reduction;
|
|
86
|
+
viewport = page.getViewport({ scale, rotation });
|
|
87
|
+
}
|
|
88
|
+
if (width > MAX_CANVAS_DIMENSION || height > MAX_CANVAS_DIMENSION || width * height > maxPixels) {
|
|
89
|
+
throw new ExtractionFailure("INVALID_PDF", "A PDF page exceeds the supported render dimensions.");
|
|
90
|
+
}
|
|
91
|
+
const canvas = createCanvas(width, height);
|
|
92
|
+
const context = canvas.getContext("2d");
|
|
93
|
+
context.fillStyle = "#ffffff";
|
|
94
|
+
context.fillRect(0, 0, width, height);
|
|
95
|
+
await page.render({
|
|
96
|
+
canvas,
|
|
97
|
+
canvasContext: context,
|
|
98
|
+
viewport,
|
|
99
|
+
intent: "display",
|
|
100
|
+
background: "#ffffff"
|
|
101
|
+
}).promise;
|
|
102
|
+
let pixels = null;
|
|
103
|
+
return {
|
|
104
|
+
width,
|
|
105
|
+
height,
|
|
106
|
+
appliedScale: scale,
|
|
107
|
+
rotation,
|
|
108
|
+
getPixels() {
|
|
109
|
+
pixels ??= context.getImageData(0, 0, width, height).data;
|
|
110
|
+
return pixels;
|
|
111
|
+
},
|
|
112
|
+
toPng() {
|
|
113
|
+
return canvas.toBuffer("image/png");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
var MAX_CANVAS_DIMENSION;
|
|
118
|
+
var init_render_page = __esm({
|
|
119
|
+
"src/pdf/render-page.ts"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
init_errors();
|
|
122
|
+
MAX_CANVAS_DIMENSION = 32767;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/recognition/barcode-reader.ts
|
|
127
|
+
var barcode_reader_exports = {};
|
|
128
|
+
__export(barcode_reader_exports, {
|
|
129
|
+
readBarcodes: () => readBarcodes
|
|
130
|
+
});
|
|
131
|
+
function scanRegions(source, width, height) {
|
|
132
|
+
const regions = [source];
|
|
133
|
+
const lowerRegionTop = Math.floor(height * 0.45);
|
|
134
|
+
const rightRegionLeft = Math.floor(width * 0.45);
|
|
135
|
+
const crops = [
|
|
136
|
+
[0, 0, width, Math.min(height, Math.ceil(height * 0.55))],
|
|
137
|
+
[0, lowerRegionTop, width, height - lowerRegionTop],
|
|
138
|
+
[0, 0, Math.min(width, Math.ceil(width * 0.55)), height],
|
|
139
|
+
[rightRegionLeft, 0, width - rightRegionLeft, height]
|
|
140
|
+
];
|
|
141
|
+
for (const [left, top, cropWidth, cropHeight] of crops) {
|
|
142
|
+
if (cropWidth >= 32 && cropHeight >= 32) {
|
|
143
|
+
regions.push(source.crop(left, top, cropWidth, cropHeight));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return regions;
|
|
147
|
+
}
|
|
148
|
+
function decodeBitmap(reader, bitmap, hints, source) {
|
|
149
|
+
try {
|
|
150
|
+
const result = reader.decode(bitmap, hints);
|
|
151
|
+
return {
|
|
152
|
+
text: result.getText(),
|
|
153
|
+
source
|
|
154
|
+
};
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
} finally {
|
|
158
|
+
reader.reset();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function readBarcodes(rendered) {
|
|
162
|
+
const { BarcodeFormat, BinaryBitmap, Code128Reader, DecodeHintType, HybridBinarizer, InvertedLuminanceSource, QRCodeReader, RGBLuminanceSource } = await import("@zxing/library");
|
|
163
|
+
const hints = /* @__PURE__ */ new Map();
|
|
164
|
+
hints.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.CODE_128, BarcodeFormat.QR_CODE]);
|
|
165
|
+
hints.set(DecodeHintType.TRY_HARDER, true);
|
|
166
|
+
const pixels = rendered.getPixels();
|
|
167
|
+
const luminance = new Uint8ClampedArray(rendered.width * rendered.height);
|
|
168
|
+
for (let pixel = 0, rgba = 0; pixel < luminance.length; pixel += 1, rgba += 4) {
|
|
169
|
+
const red = pixels[rgba] ?? 255;
|
|
170
|
+
const green = pixels[rgba + 1] ?? 255;
|
|
171
|
+
const blue = pixels[rgba + 2] ?? 255;
|
|
172
|
+
luminance[pixel] = (red + green * 2 + blue) / 4;
|
|
173
|
+
}
|
|
174
|
+
const source = new RGBLuminanceSource(luminance, rendered.width, rendered.height);
|
|
175
|
+
const attempts = [
|
|
176
|
+
{ reader: new Code128Reader(), source: "code128" },
|
|
177
|
+
{ reader: new QRCodeReader(), source: "qr-code" }
|
|
178
|
+
];
|
|
179
|
+
const unique = /* @__PURE__ */ new Map();
|
|
180
|
+
const regions = scanRegions(source, rendered.width, rendered.height);
|
|
181
|
+
for (const [regionIndex, region] of regions.entries()) {
|
|
182
|
+
const candidateSources = [region];
|
|
183
|
+
if (regionIndex === 0) {
|
|
184
|
+
candidateSources.push(new InvertedLuminanceSource(region));
|
|
185
|
+
}
|
|
186
|
+
for (const attempt of attempts) {
|
|
187
|
+
for (const candidateSource of candidateSources) {
|
|
188
|
+
const result = decodeBitmap(attempt.reader, new BinaryBitmap(new HybridBinarizer(candidateSource)), hints, attempt.source);
|
|
189
|
+
if (result !== null) {
|
|
190
|
+
unique.set(`${result.source}:${result.text}`, result);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...unique.values()];
|
|
196
|
+
}
|
|
197
|
+
var init_barcode_reader = __esm({
|
|
198
|
+
"src/recognition/barcode-reader.ts"() {
|
|
199
|
+
"use strict";
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// src/recognition/ocr-reader.ts
|
|
204
|
+
var ocr_reader_exports = {};
|
|
205
|
+
__export(ocr_reader_exports, {
|
|
206
|
+
createOcrSession: () => createOcrSession
|
|
207
|
+
});
|
|
208
|
+
async function createOcrSession() {
|
|
209
|
+
const [{ createWorker, OEM, PSM }, languageData] = await Promise.all([import("tesseract.js"), import("@tesseract.js-data/eng")]);
|
|
210
|
+
const worker = await createWorker("eng", OEM.LSTM_ONLY, {
|
|
211
|
+
langPath: languageData.default.langPath,
|
|
212
|
+
cacheMethod: "none",
|
|
213
|
+
gzip: true
|
|
214
|
+
});
|
|
215
|
+
try {
|
|
216
|
+
await worker.setParameters({
|
|
217
|
+
tessedit_char_whitelist: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
|
218
|
+
tessedit_pageseg_mode: PSM.SPARSE_TEXT,
|
|
219
|
+
preserve_interword_spaces: "1"
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
await worker.terminate().catch(() => void 0);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
async recognize(image) {
|
|
227
|
+
const result = await worker.recognize(image);
|
|
228
|
+
return {
|
|
229
|
+
text: result.data.text,
|
|
230
|
+
confidence: result.data.confidence
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
async terminate() {
|
|
234
|
+
await worker.terminate();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var init_ocr_reader = __esm({
|
|
239
|
+
"src/recognition/ocr-reader.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// src/index.ts
|
|
245
|
+
var index_exports = {};
|
|
246
|
+
__export(index_exports, {
|
|
247
|
+
ACCESS_KEY_ISSUE_CODES: () => ACCESS_KEY_ISSUE_CODES,
|
|
248
|
+
calculateAccessKeyCheckDigit: () => calculateAccessKeyCheckDigit,
|
|
249
|
+
extractNFeAccessKeys: () => extractNFeAccessKeys,
|
|
250
|
+
parseAccessKey: () => parseAccessKey,
|
|
251
|
+
validateAccessKey: () => validateAccessKey,
|
|
252
|
+
validateIssuerIdentifier: () => validateIssuerIdentifier
|
|
253
|
+
});
|
|
254
|
+
module.exports = __toCommonJS(index_exports);
|
|
255
|
+
|
|
256
|
+
// src/extractor.ts
|
|
257
|
+
var import_node_perf_hooks2 = require("perf_hooks");
|
|
258
|
+
|
|
259
|
+
// src/validation/access-key.ts
|
|
260
|
+
var NUMERIC_ACCESS_KEY_PATTERN = /^[0-9]{44}$/;
|
|
261
|
+
var CURRENT_ACCESS_KEY_PATTERN = /^[0-9]{6}[A-Z0-9]{12}[0-9]{26}$/;
|
|
262
|
+
var ACCESS_KEY_BODY_PATTERN = /^[0-9]{6}[A-Z0-9]{12}[0-9]{25}$/;
|
|
263
|
+
var ISSUER_IDENTIFIER_PATTERN = /^[A-Z0-9]{12}[0-9]{2}$/;
|
|
264
|
+
var ZERO_PADDED_CPF_PATTERN = /^000[0-9]{11}$/;
|
|
265
|
+
var VALID_STATE_CODES = /* @__PURE__ */ new Set(["11", "12", "13", "14", "15", "16", "17", "21", "22", "23", "24", "25", "26", "27", "28", "29", "31", "32", "33", "35", "41", "42", "43", "50", "51", "52", "53"]);
|
|
266
|
+
var VALID_MODELS = /* @__PURE__ */ new Set(["55", "65"]);
|
|
267
|
+
var ACCESS_KEY_ISSUE_CODES = {
|
|
268
|
+
INVALID_FORMAT: "INVALID_FORMAT",
|
|
269
|
+
INVALID_CHECK_DIGIT: "INVALID_CHECK_DIGIT",
|
|
270
|
+
INVALID_MODEL: "INVALID_MODEL",
|
|
271
|
+
INVALID_STATE_CODE: "INVALID_STATE_CODE",
|
|
272
|
+
INVALID_MONTH: "INVALID_MONTH",
|
|
273
|
+
INVALID_ISSUER_IDENTIFIER: "INVALID_ISSUER_IDENTIFIER",
|
|
274
|
+
INVALID_INVOICE_NUMBER: "INVALID_INVOICE_NUMBER"
|
|
275
|
+
};
|
|
276
|
+
function normalizeAccessKey(value) {
|
|
277
|
+
return typeof value === "string" ? value.toUpperCase() : "";
|
|
278
|
+
}
|
|
279
|
+
function getAccessKeyFormat(value) {
|
|
280
|
+
if (NUMERIC_ACCESS_KEY_PATTERN.test(value)) {
|
|
281
|
+
return "numeric";
|
|
282
|
+
}
|
|
283
|
+
if (CURRENT_ACCESS_KEY_PATTERN.test(value)) {
|
|
284
|
+
return "alphanumeric";
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
function getDocumentType(model) {
|
|
289
|
+
if (model === "55") {
|
|
290
|
+
return "NFe";
|
|
291
|
+
}
|
|
292
|
+
if (model === "65") {
|
|
293
|
+
return "NFCe";
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function calculateAsciiModulo11Digit(body) {
|
|
298
|
+
let sum = 0;
|
|
299
|
+
let weight = 2;
|
|
300
|
+
for (let index = body.length - 1; index >= 0; index -= 1) {
|
|
301
|
+
sum += (body.charCodeAt(index) - 48) * weight;
|
|
302
|
+
weight = weight === 9 ? 2 : weight + 1;
|
|
303
|
+
}
|
|
304
|
+
const remainder = sum % 11;
|
|
305
|
+
return remainder === 0 || remainder === 1 ? 0 : 11 - remainder;
|
|
306
|
+
}
|
|
307
|
+
function hasRepeatedCharacters(value) {
|
|
308
|
+
return value.length > 0 && new Set(value).size === 1;
|
|
309
|
+
}
|
|
310
|
+
function validateCnpj(value) {
|
|
311
|
+
const base = value.slice(0, 12);
|
|
312
|
+
if (hasRepeatedCharacters(base)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const firstCheckDigit = calculateAsciiModulo11Digit(base);
|
|
316
|
+
const secondCheckDigit = calculateAsciiModulo11Digit(`${base}${firstCheckDigit}`);
|
|
317
|
+
return value.slice(12) === `${firstCheckDigit}${secondCheckDigit}`;
|
|
318
|
+
}
|
|
319
|
+
function calculateCpfCheckDigit(body) {
|
|
320
|
+
let sum = 0;
|
|
321
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
322
|
+
sum += (body.charCodeAt(index) - 48) * (body.length + 1 - index);
|
|
323
|
+
}
|
|
324
|
+
const remainder = sum % 11;
|
|
325
|
+
return remainder === 0 || remainder === 1 ? 0 : 11 - remainder;
|
|
326
|
+
}
|
|
327
|
+
function validateZeroPaddedCpf(value) {
|
|
328
|
+
if (!ZERO_PADDED_CPF_PATTERN.test(value)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const cpf = value.slice(3);
|
|
332
|
+
const base = cpf.slice(0, 9);
|
|
333
|
+
if (hasRepeatedCharacters(cpf) || hasRepeatedCharacters(base)) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const firstCheckDigit = calculateCpfCheckDigit(base);
|
|
337
|
+
const secondCheckDigit = calculateCpfCheckDigit(`${base}${firstCheckDigit}`);
|
|
338
|
+
return cpf.slice(9) === `${firstCheckDigit}${secondCheckDigit}`;
|
|
339
|
+
}
|
|
340
|
+
function validateIssuerIdentifier(value) {
|
|
341
|
+
const normalizedValue = normalizeAccessKey(value);
|
|
342
|
+
if (!ISSUER_IDENTIFIER_PATTERN.test(normalizedValue)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
return validateCnpj(normalizedValue) || validateZeroPaddedCpf(normalizedValue);
|
|
346
|
+
}
|
|
347
|
+
function calculateAccessKeyCheckDigit(body) {
|
|
348
|
+
const normalizedBody = normalizeAccessKey(body);
|
|
349
|
+
if (!ACCESS_KEY_BODY_PATTERN.test(normalizedBody)) {
|
|
350
|
+
throw new TypeError("Access key body must have 43 characters and match the supported access key format.");
|
|
351
|
+
}
|
|
352
|
+
return calculateAsciiModulo11Digit(normalizedBody);
|
|
353
|
+
}
|
|
354
|
+
function parseAccessKey(value) {
|
|
355
|
+
const normalizedValue = normalizeAccessKey(value);
|
|
356
|
+
const format = getAccessKeyFormat(normalizedValue);
|
|
357
|
+
if (format === null) {
|
|
358
|
+
throw new TypeError("Access key does not match a supported 44-character format.");
|
|
359
|
+
}
|
|
360
|
+
const model = normalizedValue.slice(20, 22);
|
|
361
|
+
const yearMonth = normalizedValue.slice(2, 6);
|
|
362
|
+
return {
|
|
363
|
+
accessKey: normalizedValue,
|
|
364
|
+
format,
|
|
365
|
+
stateCode: normalizedValue.slice(0, 2),
|
|
366
|
+
yearMonth,
|
|
367
|
+
year: yearMonth.slice(0, 2),
|
|
368
|
+
month: yearMonth.slice(2, 4),
|
|
369
|
+
issuerId: normalizedValue.slice(6, 20),
|
|
370
|
+
model,
|
|
371
|
+
documentType: getDocumentType(model),
|
|
372
|
+
series: normalizedValue.slice(22, 25),
|
|
373
|
+
invoiceNumber: normalizedValue.slice(25, 34),
|
|
374
|
+
emissionType: normalizedValue.slice(34, 35),
|
|
375
|
+
numericCode: normalizedValue.slice(35, 43),
|
|
376
|
+
checkDigit: Number(normalizedValue[43])
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function validateAccessKey(value) {
|
|
380
|
+
const normalizedValue = normalizeAccessKey(value);
|
|
381
|
+
const format = getAccessKeyFormat(normalizedValue);
|
|
382
|
+
if (format === null) {
|
|
383
|
+
return {
|
|
384
|
+
isValid: false,
|
|
385
|
+
normalizedValue,
|
|
386
|
+
format: null,
|
|
387
|
+
components: null,
|
|
388
|
+
expectedCheckDigit: null,
|
|
389
|
+
issues: [
|
|
390
|
+
{
|
|
391
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_FORMAT,
|
|
392
|
+
message: "Access key does not match a supported 44-character format."
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const components = parseAccessKey(normalizedValue);
|
|
398
|
+
const expectedCheckDigit = calculateAccessKeyCheckDigit(normalizedValue.slice(0, 43));
|
|
399
|
+
const issues = [];
|
|
400
|
+
if (!VALID_STATE_CODES.has(components.stateCode)) {
|
|
401
|
+
issues.push({
|
|
402
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_STATE_CODE,
|
|
403
|
+
message: `State code ${components.stateCode} is not an official IBGE UF code.`
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const month = Number(components.month);
|
|
407
|
+
if (month < 1 || month > 12) {
|
|
408
|
+
issues.push({
|
|
409
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_MONTH,
|
|
410
|
+
message: `Month ${components.month} must be between 01 and 12.`
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (!VALID_MODELS.has(components.model)) {
|
|
414
|
+
issues.push({
|
|
415
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_MODEL,
|
|
416
|
+
message: `Model ${components.model} is not supported; expected 55 or 65.`
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (components.invoiceNumber === "000000000") {
|
|
420
|
+
issues.push({
|
|
421
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_INVOICE_NUMBER,
|
|
422
|
+
message: "Invoice number must be between 000000001 and 999999999."
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (!validateIssuerIdentifier(components.issuerId)) {
|
|
426
|
+
issues.push({
|
|
427
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_ISSUER_IDENTIFIER,
|
|
428
|
+
message: `Issuer identifier ${components.issuerId} is not a valid CNPJ or zero-padded CPF.`
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
if (components.checkDigit !== expectedCheckDigit) {
|
|
432
|
+
issues.push({
|
|
433
|
+
code: ACCESS_KEY_ISSUE_CODES.INVALID_CHECK_DIGIT,
|
|
434
|
+
message: `Check digit ${components.checkDigit} does not match expected digit ${expectedCheckDigit}.`
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
isValid: issues.length === 0,
|
|
439
|
+
normalizedValue,
|
|
440
|
+
format,
|
|
441
|
+
components,
|
|
442
|
+
expectedCheckDigit,
|
|
443
|
+
issues
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/candidates/from-text.ts
|
|
448
|
+
var ACCESS_KEY_PATTERN = /[0-9]{6}[A-Z0-9]{12}[0-9]{26}/g;
|
|
449
|
+
var SEPARATOR_PATTERN = /[\s.\-_/]/;
|
|
450
|
+
var ALPHANUMERIC_PATTERN = /[A-Z0-9]/;
|
|
451
|
+
var LABEL_PATTERN = /CHAVE\s+DE\s+ACESSO|ACCESS\s+KEY/;
|
|
452
|
+
function normalizedText(value) {
|
|
453
|
+
return value.normalize("NFKC").replace(/\u00a0/g, " ").toUpperCase();
|
|
454
|
+
}
|
|
455
|
+
function isNearLabel(text, start) {
|
|
456
|
+
const context = text.slice(Math.max(0, start - 120), start + 12);
|
|
457
|
+
return LABEL_PATTERN.test(context);
|
|
458
|
+
}
|
|
459
|
+
function addIfValid(output, seen, accessKey, page, source, pass, nearLabel) {
|
|
460
|
+
const validation = validateAccessKey(accessKey);
|
|
461
|
+
const signature = `${accessKey}:${source}:${pass}:${nearLabel}`;
|
|
462
|
+
if (!validation.isValid || seen.has(signature)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
seen.add(signature);
|
|
466
|
+
output.push({ accessKey, page, source, pass, nearLabel });
|
|
467
|
+
}
|
|
468
|
+
function findCandidatesInText(text, page) {
|
|
469
|
+
const normalized = normalizedText(text);
|
|
470
|
+
const output = [];
|
|
471
|
+
const seen = /* @__PURE__ */ new Set();
|
|
472
|
+
for (const match of normalized.matchAll(ACCESS_KEY_PATTERN)) {
|
|
473
|
+
const accessKey = match[0];
|
|
474
|
+
const start = match.index;
|
|
475
|
+
const before = start > 0 ? normalized[start - 1] : void 0;
|
|
476
|
+
const after = normalized[start + accessKey.length];
|
|
477
|
+
if (before !== void 0 && ALPHANUMERIC_PATTERN.test(before) || after !== void 0 && ALPHANUMERIC_PATTERN.test(after)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
addIfValid(output, seen, accessKey, page, "pdf-text", 0, isNearLabel(normalized, start));
|
|
481
|
+
}
|
|
482
|
+
for (let start = 0; start < normalized.length; start += 1) {
|
|
483
|
+
const first = normalized[start];
|
|
484
|
+
if (first === void 0 || !/[0-9]/.test(first)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
let accessKey = "";
|
|
488
|
+
let cursor = start;
|
|
489
|
+
let hadSeparator = false;
|
|
490
|
+
while (cursor < normalized.length && cursor - start <= 100) {
|
|
491
|
+
const character = normalized[cursor];
|
|
492
|
+
if (character === void 0) {
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
if (ALPHANUMERIC_PATTERN.test(character)) {
|
|
496
|
+
accessKey += character;
|
|
497
|
+
} else if (SEPARATOR_PATTERN.test(character)) {
|
|
498
|
+
hadSeparator = true;
|
|
499
|
+
} else {
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
cursor += 1;
|
|
503
|
+
if (accessKey.length === 44) {
|
|
504
|
+
const nearLabel = isNearLabel(normalized, start);
|
|
505
|
+
const rawSpan = normalized.slice(start, cursor);
|
|
506
|
+
let lookahead = cursor;
|
|
507
|
+
while (SEPARATOR_PATTERN.test(normalized[lookahead] ?? "")) {
|
|
508
|
+
lookahead += 1;
|
|
509
|
+
}
|
|
510
|
+
const likelyLongerNumericCluster = /[0-9]/.test(normalized[lookahead] ?? "") && !nearLabel;
|
|
511
|
+
const crossesLinesWithoutContext = rawSpan.includes("\n") && !nearLabel;
|
|
512
|
+
if (hadSeparator && !likelyLongerNumericCluster && !crossesLinesWithoutContext) {
|
|
513
|
+
addIfValid(output, seen, accessKey, page, "pdf-text-reconstructed", 0, nearLabel);
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
if (accessKey.length > 44) {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return output;
|
|
523
|
+
}
|
|
524
|
+
function findCandidatesInDecodedValue(value, page, source, pass) {
|
|
525
|
+
let decoded = normalizedText(value);
|
|
526
|
+
try {
|
|
527
|
+
decoded = decodeURIComponent(decoded);
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
const output = [];
|
|
531
|
+
const seen = /* @__PURE__ */ new Set();
|
|
532
|
+
for (const match of decoded.matchAll(ACCESS_KEY_PATTERN)) {
|
|
533
|
+
addIfValid(output, seen, match[0], page, source, pass, true);
|
|
534
|
+
}
|
|
535
|
+
return output;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/candidates/from-ocr.ts
|
|
539
|
+
var OCR_CLUSTER = /[A-Z0-9](?:[\s.\-_/]*[A-Z0-9]){43}/g;
|
|
540
|
+
var OCR_TO_DIGIT = {
|
|
541
|
+
O: "0",
|
|
542
|
+
Q: "0",
|
|
543
|
+
D: "0",
|
|
544
|
+
I: "1",
|
|
545
|
+
L: "1",
|
|
546
|
+
Z: "2",
|
|
547
|
+
S: "5",
|
|
548
|
+
G: "6",
|
|
549
|
+
B: "8"
|
|
550
|
+
};
|
|
551
|
+
var DIGIT_TO_OCR_LETTERS = {
|
|
552
|
+
"0": ["O", "Q", "D"],
|
|
553
|
+
"1": ["I", "L"],
|
|
554
|
+
"2": ["Z"],
|
|
555
|
+
"5": ["S"],
|
|
556
|
+
"6": ["G"],
|
|
557
|
+
"8": ["B"]
|
|
558
|
+
};
|
|
559
|
+
function correctedAlternatives(raw) {
|
|
560
|
+
const characters = raw.match(/[A-Z0-9]/g);
|
|
561
|
+
if (characters === null || characters.length !== 44) {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
let corrections = 0;
|
|
565
|
+
const issuerAlternatives = [];
|
|
566
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
567
|
+
const character = characters[index];
|
|
568
|
+
if (character === void 0) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const isAlphanumericIssuerPosition = index >= 6 && index < 18;
|
|
572
|
+
if (/[0-9]/.test(character)) {
|
|
573
|
+
if (isAlphanumericIssuerPosition) {
|
|
574
|
+
for (const letter of DIGIT_TO_OCR_LETTERS[character] ?? []) {
|
|
575
|
+
issuerAlternatives.push({ index, replacement: letter });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (isAlphanumericIssuerPosition) {
|
|
581
|
+
const digit = OCR_TO_DIGIT[character];
|
|
582
|
+
if (digit !== void 0) {
|
|
583
|
+
issuerAlternatives.push({ index, replacement: digit });
|
|
584
|
+
}
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const corrected = OCR_TO_DIGIT[character];
|
|
588
|
+
if (corrected === void 0) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
characters[index] = corrected;
|
|
592
|
+
corrections += 1;
|
|
593
|
+
}
|
|
594
|
+
if (corrections > 1) {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
const uncorrectedValue = characters.join("");
|
|
598
|
+
const alternatives = [{ value: uncorrectedValue, corrections }];
|
|
599
|
+
if (corrections === 0 && !validateAccessKey(uncorrectedValue).isValid) {
|
|
600
|
+
for (const { index, replacement } of issuerAlternatives) {
|
|
601
|
+
const corrected = [...characters];
|
|
602
|
+
corrected[index] = replacement;
|
|
603
|
+
alternatives.push({ value: corrected.join(""), corrections: 1 });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return alternatives;
|
|
607
|
+
}
|
|
608
|
+
function findCandidatesInOcrText(text, page, pass, confidence) {
|
|
609
|
+
const normalized = text.normalize("NFKC").toUpperCase();
|
|
610
|
+
const exact = findCandidatesInText(normalized, page).map((candidate) => ({
|
|
611
|
+
...candidate,
|
|
612
|
+
source: "ocr",
|
|
613
|
+
pass,
|
|
614
|
+
ocrConfidence: confidence
|
|
615
|
+
}));
|
|
616
|
+
const output = [...exact];
|
|
617
|
+
const seen = new Set(exact.map((candidate) => candidate.accessKey));
|
|
618
|
+
for (const match of normalized.matchAll(OCR_CLUSTER)) {
|
|
619
|
+
for (const corrected of correctedAlternatives(match[0])) {
|
|
620
|
+
if (corrected.corrections === 0 || seen.has(corrected.value) || !validateAccessKey(corrected.value).isValid) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
seen.add(corrected.value);
|
|
624
|
+
output.push({
|
|
625
|
+
accessKey: corrected.value,
|
|
626
|
+
page,
|
|
627
|
+
source: "ocr",
|
|
628
|
+
pass,
|
|
629
|
+
nearLabel: false,
|
|
630
|
+
ocrConfidence: confidence,
|
|
631
|
+
corrections: corrected.corrections
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return output;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/deadline.ts
|
|
639
|
+
var import_node_perf_hooks = require("perf_hooks");
|
|
640
|
+
init_errors();
|
|
641
|
+
var WorkGuard = class {
|
|
642
|
+
#startedAt;
|
|
643
|
+
#options;
|
|
644
|
+
#controller = new AbortController();
|
|
645
|
+
#abortListener;
|
|
646
|
+
#timeout;
|
|
647
|
+
#stopReason = null;
|
|
648
|
+
constructor(options, startedAt = import_node_perf_hooks.performance.now()) {
|
|
649
|
+
this.#options = options;
|
|
650
|
+
this.#startedAt = startedAt;
|
|
651
|
+
if (options.signal?.aborted === true) {
|
|
652
|
+
this.#stop("aborted");
|
|
653
|
+
} else if (options.signal !== void 0) {
|
|
654
|
+
this.#abortListener = () => {
|
|
655
|
+
this.#stop("aborted");
|
|
656
|
+
};
|
|
657
|
+
options.signal.addEventListener("abort", this.#abortListener, { once: true });
|
|
658
|
+
}
|
|
659
|
+
if (options.timeoutMs > 0 && this.#stopReason === null) {
|
|
660
|
+
const elapsed = import_node_perf_hooks.performance.now() - this.#startedAt;
|
|
661
|
+
this.#timeout = setTimeout(
|
|
662
|
+
() => {
|
|
663
|
+
this.#stop("timeout");
|
|
664
|
+
},
|
|
665
|
+
Math.max(0, options.timeoutMs - elapsed)
|
|
666
|
+
);
|
|
667
|
+
this.#timeout.unref();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
get signal() {
|
|
671
|
+
return this.#controller.signal;
|
|
672
|
+
}
|
|
673
|
+
check() {
|
|
674
|
+
if (this.#options.signal?.aborted === true || this.#stopReason === "aborted") {
|
|
675
|
+
this.#stop("aborted");
|
|
676
|
+
throw new ExtractionFailure("ABORTED", "Extraction was aborted.");
|
|
677
|
+
}
|
|
678
|
+
if (this.#stopReason === "timeout" || this.#options.timeoutMs > 0 && import_node_perf_hooks.performance.now() - this.#startedAt >= this.#options.timeoutMs) {
|
|
679
|
+
this.#stop("timeout");
|
|
680
|
+
throw new ExtractionFailure("TIMEOUT", "Extraction exceeded its configured deadline.");
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
dispose() {
|
|
684
|
+
if (this.#timeout !== void 0) {
|
|
685
|
+
clearTimeout(this.#timeout);
|
|
686
|
+
}
|
|
687
|
+
if (this.#abortListener !== void 0 && this.#options.signal !== void 0) {
|
|
688
|
+
this.#options.signal.removeEventListener("abort", this.#abortListener);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
#stop(reason) {
|
|
692
|
+
if (this.#stopReason !== null) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.#stopReason = reason;
|
|
696
|
+
this.#controller.abort();
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/extractor.ts
|
|
701
|
+
init_errors();
|
|
702
|
+
|
|
703
|
+
// src/options.ts
|
|
704
|
+
var import_node_http = require("http");
|
|
705
|
+
var MEBIBYTE = 1024 * 1024;
|
|
706
|
+
var BLOCKED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["accept-encoding", "connection", "content-length", "expect", "host", "if-range", "keep-alive", "proxy-connection", "range", "te", "trailer", "transfer-encoding", "upgrade"]);
|
|
707
|
+
var PROFILE_DEFAULTS = {
|
|
708
|
+
fast: {
|
|
709
|
+
passes: 1,
|
|
710
|
+
ocr: "never",
|
|
711
|
+
maxPages: 10,
|
|
712
|
+
maxPixelsPerPage: 8e6,
|
|
713
|
+
maxSourceImagePixels: 4e7,
|
|
714
|
+
timeoutMs: 3e4
|
|
715
|
+
},
|
|
716
|
+
balanced: {
|
|
717
|
+
passes: 2,
|
|
718
|
+
ocr: "fallback",
|
|
719
|
+
maxPages: 30,
|
|
720
|
+
maxPixelsPerPage: 12e6,
|
|
721
|
+
maxSourceImagePixels: 6e7,
|
|
722
|
+
timeoutMs: 12e4
|
|
723
|
+
},
|
|
724
|
+
accurate: {
|
|
725
|
+
passes: 3,
|
|
726
|
+
ocr: "fallback",
|
|
727
|
+
maxPages: 50,
|
|
728
|
+
maxPixelsPerPage: 2e7,
|
|
729
|
+
maxSourceImagePixels: 1e8,
|
|
730
|
+
timeoutMs: 3e5
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
var InvalidOptionsError = class extends Error {
|
|
734
|
+
constructor(message) {
|
|
735
|
+
super(message);
|
|
736
|
+
this.name = "InvalidOptionsError";
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
function integerInRange(name, value, fallback, minimum, maximum) {
|
|
740
|
+
const resolved = value ?? fallback;
|
|
741
|
+
if (!Number.isInteger(resolved) || resolved < minimum || resolved > maximum) {
|
|
742
|
+
throw new InvalidOptionsError(`${name} must be an integer between ${minimum} and ${maximum}.`);
|
|
743
|
+
}
|
|
744
|
+
return resolved;
|
|
745
|
+
}
|
|
746
|
+
function normalizeRequestHeaders(requestHeaders) {
|
|
747
|
+
if (requestHeaders === void 0) {
|
|
748
|
+
return void 0;
|
|
749
|
+
}
|
|
750
|
+
if (typeof requestHeaders !== "object" || requestHeaders === null || Array.isArray(requestHeaders)) {
|
|
751
|
+
throw new InvalidOptionsError("requestHeaders must be a record of strings.");
|
|
752
|
+
}
|
|
753
|
+
let prototype;
|
|
754
|
+
try {
|
|
755
|
+
prototype = Object.getPrototypeOf(requestHeaders);
|
|
756
|
+
} catch {
|
|
757
|
+
throw new InvalidOptionsError("requestHeaders could not be read.");
|
|
758
|
+
}
|
|
759
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
760
|
+
throw new InvalidOptionsError("requestHeaders must be a record of strings.");
|
|
761
|
+
}
|
|
762
|
+
const normalized = /* @__PURE__ */ Object.create(null);
|
|
763
|
+
let entries;
|
|
764
|
+
try {
|
|
765
|
+
entries = Object.entries(requestHeaders);
|
|
766
|
+
} catch {
|
|
767
|
+
throw new InvalidOptionsError("requestHeaders could not be read.");
|
|
768
|
+
}
|
|
769
|
+
for (const [name, value] of entries) {
|
|
770
|
+
const normalizedName = name.toLowerCase();
|
|
771
|
+
if (typeof value !== "string") {
|
|
772
|
+
throw new InvalidOptionsError("Every requestHeaders value must be a string.");
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
(0, import_node_http.validateHeaderName)(name);
|
|
776
|
+
(0, import_node_http.validateHeaderValue)(name, value);
|
|
777
|
+
} catch {
|
|
778
|
+
throw new InvalidOptionsError("requestHeaders contains an invalid header.");
|
|
779
|
+
}
|
|
780
|
+
if (BLOCKED_REQUEST_HEADERS.has(normalizedName)) {
|
|
781
|
+
throw new InvalidOptionsError("requestHeaders contains a header that cannot be overridden.");
|
|
782
|
+
}
|
|
783
|
+
if (Object.hasOwn(normalized, normalizedName)) {
|
|
784
|
+
throw new InvalidOptionsError("requestHeaders contains duplicate case-insensitive names.");
|
|
785
|
+
}
|
|
786
|
+
normalized[normalizedName] = value;
|
|
787
|
+
}
|
|
788
|
+
return Object.freeze(normalized);
|
|
789
|
+
}
|
|
790
|
+
function resolveOptions(options = {}) {
|
|
791
|
+
const performance3 = options.performance ?? "balanced";
|
|
792
|
+
if (!Object.hasOwn(PROFILE_DEFAULTS, performance3)) {
|
|
793
|
+
throw new InvalidOptionsError("performance must be fast, balanced, or accurate.");
|
|
794
|
+
}
|
|
795
|
+
const profile = PROFILE_DEFAULTS[performance3];
|
|
796
|
+
const ocr = options.ocr ?? profile.ocr;
|
|
797
|
+
if (!["never", "fallback", "always"].includes(ocr)) {
|
|
798
|
+
throw new InvalidOptionsError("ocr must be never, fallback, or always.");
|
|
799
|
+
}
|
|
800
|
+
const requestHeaders = normalizeRequestHeaders(options.requestHeaders);
|
|
801
|
+
return {
|
|
802
|
+
performance: performance3,
|
|
803
|
+
passes: integerInRange("passes", options.passes, profile.passes, 1, 5),
|
|
804
|
+
ocr,
|
|
805
|
+
maxPages: integerInRange("maxPages", options.maxPages, profile.maxPages, 1, 1e4),
|
|
806
|
+
maxFileSizeBytes: integerInRange("maxFileSizeBytes", options.maxFileSizeBytes, 30 * MEBIBYTE, 1, 1024 * MEBIBYTE),
|
|
807
|
+
maxPixelsPerPage: integerInRange("maxPixelsPerPage", options.maxPixelsPerPage, profile.maxPixelsPerPage, 25e4, 1e8),
|
|
808
|
+
maxSourceImagePixels: integerInRange("maxSourceImagePixels", options.maxSourceImagePixels, profile.maxSourceImagePixels, 25e4, 2e8),
|
|
809
|
+
timeoutMs: integerInRange("timeoutMs", options.timeoutMs, profile.timeoutMs, 0, 36e5),
|
|
810
|
+
stopAfterFirst: options.stopAfterFirst ?? false,
|
|
811
|
+
...requestHeaders === void 0 ? {} : { requestHeaders },
|
|
812
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
var RECIPES = {
|
|
816
|
+
fast: [
|
|
817
|
+
{ scale: 1.25, rotation: 0 },
|
|
818
|
+
{ scale: 1.6, rotation: 0 },
|
|
819
|
+
{ scale: 2, rotation: 0 },
|
|
820
|
+
{ scale: 1.6, rotation: 90 },
|
|
821
|
+
{ scale: 1.6, rotation: 270 }
|
|
822
|
+
],
|
|
823
|
+
balanced: [
|
|
824
|
+
{ scale: 1.5, rotation: 0 },
|
|
825
|
+
{ scale: 2, rotation: 0 },
|
|
826
|
+
{ scale: 2.5, rotation: 0 },
|
|
827
|
+
{ scale: 2, rotation: 90 },
|
|
828
|
+
{ scale: 2, rotation: 270 }
|
|
829
|
+
],
|
|
830
|
+
accurate: [
|
|
831
|
+
{ scale: 1.75, rotation: 0 },
|
|
832
|
+
{ scale: 2.25, rotation: 0 },
|
|
833
|
+
{ scale: 3, rotation: 0 },
|
|
834
|
+
{ scale: 2.25, rotation: 90 },
|
|
835
|
+
{ scale: 2.25, rotation: 270 }
|
|
836
|
+
]
|
|
837
|
+
};
|
|
838
|
+
function getRenderRecipes(options) {
|
|
839
|
+
return RECIPES[options.performance].slice(0, options.passes);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/pdf/extract-text.ts
|
|
843
|
+
init_errors();
|
|
844
|
+
var MAX_TEXT_ITEMS_PER_PAGE = 5e4;
|
|
845
|
+
var MAX_TEXT_CHARACTERS_PER_PAGE = 25e4;
|
|
846
|
+
function isTextItem(value) {
|
|
847
|
+
return typeof value === "object" && value !== null && "str" in value && typeof value.str === "string";
|
|
848
|
+
}
|
|
849
|
+
function positionedItem(item) {
|
|
850
|
+
const transform = item.transform ?? [];
|
|
851
|
+
return {
|
|
852
|
+
str: item.str,
|
|
853
|
+
x: transform[4] ?? 0,
|
|
854
|
+
y: transform[5] ?? 0,
|
|
855
|
+
width: item.width ?? 0,
|
|
856
|
+
height: Math.abs(item.height ?? transform[3] ?? 10),
|
|
857
|
+
hasEOL: item.hasEOL ?? false
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function reconstructLines(items) {
|
|
861
|
+
const sorted = [...items].sort((left, right) => right.y - left.y || left.x - right.x);
|
|
862
|
+
const lines = [];
|
|
863
|
+
for (const item of sorted) {
|
|
864
|
+
const line = lines.at(-1);
|
|
865
|
+
if (line === void 0 || Math.abs(line.y - item.y) > Math.max(2, item.height * 0.45)) {
|
|
866
|
+
lines.push({ y: item.y, height: item.height, items: [item] });
|
|
867
|
+
} else {
|
|
868
|
+
line.items.push(item);
|
|
869
|
+
line.height = Math.max(line.height, item.height);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return lines.sort((left, right) => right.y - left.y).map((line) => {
|
|
873
|
+
const lineItems = line.items.sort((left, right) => left.x - right.x);
|
|
874
|
+
let text = "";
|
|
875
|
+
let rightEdge = null;
|
|
876
|
+
for (const item of lineItems) {
|
|
877
|
+
if (rightEdge !== null && item.x - rightEdge > Math.max(0.75, line.height * 0.08)) {
|
|
878
|
+
text += " ";
|
|
879
|
+
}
|
|
880
|
+
text += item.str;
|
|
881
|
+
rightEdge = item.x + item.width;
|
|
882
|
+
}
|
|
883
|
+
return text.trim();
|
|
884
|
+
}).filter((line) => line.length > 0);
|
|
885
|
+
}
|
|
886
|
+
async function extractPageText(page) {
|
|
887
|
+
const content = await page.getTextContent();
|
|
888
|
+
if (content.items.length > MAX_TEXT_ITEMS_PER_PAGE) {
|
|
889
|
+
throw new ExtractionFailure("RESOURCE_LIMIT", "A PDF page contains too many text items to process safely.");
|
|
890
|
+
}
|
|
891
|
+
const items = [];
|
|
892
|
+
let characterCount = 0;
|
|
893
|
+
for (const value of content.items) {
|
|
894
|
+
if (!isTextItem(value)) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
characterCount += value.str.length;
|
|
898
|
+
if (characterCount > MAX_TEXT_CHARACTERS_PER_PAGE) {
|
|
899
|
+
throw new ExtractionFailure("RESOURCE_LIMIT", "A PDF page contains too much text to process safely.");
|
|
900
|
+
}
|
|
901
|
+
items.push(positionedItem(value));
|
|
902
|
+
}
|
|
903
|
+
const orderedText = items.map((item) => `${item.str}${item.hasEOL ? "\n" : " "}`).join("").trim();
|
|
904
|
+
const visualLines = reconstructLines(items);
|
|
905
|
+
return {
|
|
906
|
+
orderedText,
|
|
907
|
+
visualLines,
|
|
908
|
+
hasText: items.some((item) => item.str.trim().length > 0)
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/pdf/load-input.ts
|
|
913
|
+
var import_promises = require("fs/promises");
|
|
914
|
+
init_errors();
|
|
915
|
+
var MAX_REDIRECTS = 5;
|
|
916
|
+
var INITIAL_DOWNLOAD_BUFFER_BYTES = 64 * 1024;
|
|
917
|
+
var MAX_INITIAL_CONTENT_LENGTH_ALLOCATION = 1024 * 1024;
|
|
918
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
919
|
+
var CREDENTIAL_HEADERS = ["authorization", "cookie", "proxy-authorization"];
|
|
920
|
+
var WINDOWS_DRIVE_PATH = /^[a-z]:/i;
|
|
921
|
+
var HTTP_URL = /^https?:\/\//i;
|
|
922
|
+
var URI_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
923
|
+
function hasPdfSignature(data) {
|
|
924
|
+
const prefix = Buffer.from(data.buffer, data.byteOffset, Math.min(data.byteLength, 1024));
|
|
925
|
+
return prefix.indexOf("%PDF-") >= 0;
|
|
926
|
+
}
|
|
927
|
+
function validatePdfBytes(data, maxFileSizeBytes) {
|
|
928
|
+
if (data.byteLength === 0) {
|
|
929
|
+
throw new ExtractionFailure("INVALID_INPUT", "The PDF input is empty.");
|
|
930
|
+
}
|
|
931
|
+
if (data.byteLength > maxFileSizeBytes) {
|
|
932
|
+
throw new ExtractionFailure("FILE_TOO_LARGE", `The PDF exceeds the configured ${maxFileSizeBytes}-byte limit.`);
|
|
933
|
+
}
|
|
934
|
+
if (!hasPdfSignature(data)) {
|
|
935
|
+
throw new ExtractionFailure("INVALID_PDF", "The input does not contain a PDF signature.");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function checkedBytes(data, maxFileSizeBytes) {
|
|
939
|
+
validatePdfBytes(data, maxFileSizeBytes);
|
|
940
|
+
const bytes = new Uint8Array(data.byteLength);
|
|
941
|
+
bytes.set(data);
|
|
942
|
+
return { data: bytes, size: data.byteLength };
|
|
943
|
+
}
|
|
944
|
+
function checkedRemoteUrl(url) {
|
|
945
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
946
|
+
throw new ExtractionFailure("INVALID_INPUT", "Only HTTP and HTTPS remote URLs are accepted.");
|
|
947
|
+
}
|
|
948
|
+
if (url.username !== "" || url.password !== "") {
|
|
949
|
+
throw new ExtractionFailure("INVALID_INPUT", "URL credentials are not accepted; use requestHeaders instead.");
|
|
950
|
+
}
|
|
951
|
+
return url;
|
|
952
|
+
}
|
|
953
|
+
function remoteUrlFromInput(input) {
|
|
954
|
+
const value = input.trim();
|
|
955
|
+
if (value === "") {
|
|
956
|
+
throw new ExtractionFailure("INVALID_INPUT", "A non-empty PDF path or HTTP(S) URL is required.");
|
|
957
|
+
}
|
|
958
|
+
if (WINDOWS_DRIVE_PATH.test(value)) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
if (!HTTP_URL.test(value)) {
|
|
962
|
+
if (URI_SCHEME.test(value)) {
|
|
963
|
+
throw new ExtractionFailure("INVALID_INPUT", "Only complete HTTP and HTTPS remote URLs are accepted.");
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
return checkedRemoteUrl(new URL(value));
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (error instanceof ExtractionFailure) {
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
throw new ExtractionFailure("INVALID_INPUT", "The remote URL is invalid.", {
|
|
974
|
+
cause: error
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function remoteHeaders(requestHeaders) {
|
|
979
|
+
try {
|
|
980
|
+
const headers = new Headers(requestHeaders === void 0 ? void 0 : Object.entries(requestHeaders));
|
|
981
|
+
headers.set("accept-encoding", "identity");
|
|
982
|
+
return headers;
|
|
983
|
+
} catch (error) {
|
|
984
|
+
throw new ExtractionFailure("INVALID_OPTIONS", "requestHeaders contains an invalid header.", { cause: error });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function stripRedirectCredentials(headers) {
|
|
988
|
+
for (const name of CREDENTIAL_HEADERS) {
|
|
989
|
+
headers.delete(name);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function cancelResponse(response) {
|
|
993
|
+
void response.body?.cancel().catch(() => void 0);
|
|
994
|
+
}
|
|
995
|
+
function cancelReader(reader) {
|
|
996
|
+
void reader.cancel().catch(() => void 0);
|
|
997
|
+
}
|
|
998
|
+
function advertisedSize(response) {
|
|
999
|
+
const value = response.headers.get("content-length")?.trim();
|
|
1000
|
+
if (value === void 0 || !/^\d+$/.test(value)) {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
return BigInt(value);
|
|
1005
|
+
} catch {
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function readRemoteBody(response, maxFileSizeBytes, signal) {
|
|
1010
|
+
const size = advertisedSize(response);
|
|
1011
|
+
if (size !== null && size > BigInt(maxFileSizeBytes)) {
|
|
1012
|
+
cancelResponse(response);
|
|
1013
|
+
throw new ExtractionFailure("FILE_TOO_LARGE", `The PDF exceeds the configured ${maxFileSizeBytes}-byte limit.`);
|
|
1014
|
+
}
|
|
1015
|
+
if (response.body === null) {
|
|
1016
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF response did not contain a body.");
|
|
1017
|
+
}
|
|
1018
|
+
const advertisedInitialSize = size === null ? INITIAL_DOWNLOAD_BUFFER_BYTES : Math.min(Number(size), MAX_INITIAL_CONTENT_LENGTH_ALLOCATION);
|
|
1019
|
+
let data;
|
|
1020
|
+
try {
|
|
1021
|
+
data = new Uint8Array(Math.min(maxFileSizeBytes, advertisedInitialSize));
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
await cancelResponse(response);
|
|
1024
|
+
throw new ExtractionFailure("RESOURCE_LIMIT", "The remote PDF could not be buffered within available memory.", { cause: error });
|
|
1025
|
+
}
|
|
1026
|
+
let reader;
|
|
1027
|
+
try {
|
|
1028
|
+
reader = response.body.getReader();
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF response could not be read.", { cause: error });
|
|
1031
|
+
}
|
|
1032
|
+
let total = 0;
|
|
1033
|
+
try {
|
|
1034
|
+
while (true) {
|
|
1035
|
+
const chunk = await reader.read();
|
|
1036
|
+
if (chunk.done) {
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
if (chunk.value === void 0) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const requiredSize = total + chunk.value.byteLength;
|
|
1043
|
+
if (requiredSize > maxFileSizeBytes) {
|
|
1044
|
+
cancelReader(reader);
|
|
1045
|
+
throw new ExtractionFailure("FILE_TOO_LARGE", `The PDF exceeds the configured ${maxFileSizeBytes}-byte limit.`);
|
|
1046
|
+
}
|
|
1047
|
+
if (requiredSize > data.byteLength) {
|
|
1048
|
+
const doubledSize = data.byteLength === 0 ? INITIAL_DOWNLOAD_BUFFER_BYTES : data.byteLength * 2;
|
|
1049
|
+
const expanded = new Uint8Array(Math.min(maxFileSizeBytes, Math.max(requiredSize, doubledSize)));
|
|
1050
|
+
expanded.set(data.subarray(0, total));
|
|
1051
|
+
data = expanded;
|
|
1052
|
+
}
|
|
1053
|
+
data.set(chunk.value, total);
|
|
1054
|
+
total = requiredSize;
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error instanceof ExtractionFailure || signal?.aborted === true) {
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
if (error instanceof RangeError) {
|
|
1061
|
+
cancelReader(reader);
|
|
1062
|
+
throw new ExtractionFailure("RESOURCE_LIMIT", "The remote PDF could not be buffered within available memory.", { cause: error });
|
|
1063
|
+
}
|
|
1064
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF response could not be read.", { cause: error });
|
|
1065
|
+
} finally {
|
|
1066
|
+
try {
|
|
1067
|
+
reader.releaseLock();
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
const bytes = data.subarray(0, total);
|
|
1072
|
+
validatePdfBytes(bytes, maxFileSizeBytes);
|
|
1073
|
+
return { data: bytes, size: total };
|
|
1074
|
+
}
|
|
1075
|
+
async function downloadPdf(initialUrl, maxFileSizeBytes, controls) {
|
|
1076
|
+
let currentUrl = initialUrl;
|
|
1077
|
+
const headers = remoteHeaders(controls.requestHeaders);
|
|
1078
|
+
let redirects = 0;
|
|
1079
|
+
while (true) {
|
|
1080
|
+
let response;
|
|
1081
|
+
try {
|
|
1082
|
+
response = await fetch(currentUrl, {
|
|
1083
|
+
method: "GET",
|
|
1084
|
+
headers,
|
|
1085
|
+
redirect: "manual",
|
|
1086
|
+
...controls.signal === void 0 ? {} : { signal: controls.signal }
|
|
1087
|
+
});
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
if (controls.signal?.aborted === true) {
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF could not be downloaded.", { cause: error });
|
|
1093
|
+
}
|
|
1094
|
+
if (REDIRECT_STATUSES.has(response.status)) {
|
|
1095
|
+
const location = response.headers.get("location");
|
|
1096
|
+
cancelResponse(response);
|
|
1097
|
+
if (redirects >= MAX_REDIRECTS) {
|
|
1098
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", `The remote PDF exceeded the ${MAX_REDIRECTS}-redirect limit.`);
|
|
1099
|
+
}
|
|
1100
|
+
if (location === null) {
|
|
1101
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF returned a redirect without a destination.");
|
|
1102
|
+
}
|
|
1103
|
+
let nextUrl;
|
|
1104
|
+
try {
|
|
1105
|
+
nextUrl = checkedRemoteUrl(new URL(location, currentUrl));
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", "The remote PDF returned an invalid redirect destination.", { cause: error });
|
|
1108
|
+
}
|
|
1109
|
+
if (nextUrl.origin !== currentUrl.origin || currentUrl.protocol === "https:" && nextUrl.protocol === "http:") {
|
|
1110
|
+
stripRedirectCredentials(headers);
|
|
1111
|
+
}
|
|
1112
|
+
currentUrl = nextUrl;
|
|
1113
|
+
redirects += 1;
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
if (!response.ok) {
|
|
1117
|
+
const status = response.status;
|
|
1118
|
+
cancelResponse(response);
|
|
1119
|
+
throw new ExtractionFailure("DOWNLOAD_ERROR", `The remote PDF request returned HTTP status ${status}.`);
|
|
1120
|
+
}
|
|
1121
|
+
return readRemoteBody(response, maxFileSizeBytes, controls.signal);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
async function loadPdfInput(input, maxFileSizeBytes, controls = {}) {
|
|
1125
|
+
if (typeof input === "string") {
|
|
1126
|
+
const remoteUrl = remoteUrlFromInput(input);
|
|
1127
|
+
if (remoteUrl !== null) {
|
|
1128
|
+
return downloadPdf(remoteUrl, maxFileSizeBytes, controls);
|
|
1129
|
+
}
|
|
1130
|
+
if (controls.requestHeaders !== void 0) {
|
|
1131
|
+
throw new ExtractionFailure("INVALID_OPTIONS", "requestHeaders can only be used with an HTTP(S) URL input.");
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
const file = await (0, import_promises.stat)(input);
|
|
1135
|
+
if (!file.isFile()) {
|
|
1136
|
+
throw new ExtractionFailure("INVALID_INPUT", "The supplied path is not a file.");
|
|
1137
|
+
}
|
|
1138
|
+
if (file.size > maxFileSizeBytes) {
|
|
1139
|
+
throw new ExtractionFailure("FILE_TOO_LARGE", `The PDF exceeds the configured ${maxFileSizeBytes}-byte limit.`);
|
|
1140
|
+
}
|
|
1141
|
+
return checkedBytes(await (0, import_promises.readFile)(input), maxFileSizeBytes);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
if (error instanceof ExtractionFailure) {
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
1147
|
+
if (code === "ENOENT") {
|
|
1148
|
+
throw new ExtractionFailure("FILE_NOT_FOUND", "The PDF file was not found.", {
|
|
1149
|
+
cause: error
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
throw new ExtractionFailure("INVALID_INPUT", "The PDF file could not be read.", {
|
|
1153
|
+
cause: error
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (controls.requestHeaders !== void 0) {
|
|
1158
|
+
throw new ExtractionFailure("INVALID_OPTIONS", "requestHeaders can only be used with an HTTP(S) URL input.");
|
|
1159
|
+
}
|
|
1160
|
+
if (input instanceof Uint8Array) {
|
|
1161
|
+
return checkedBytes(input, maxFileSizeBytes);
|
|
1162
|
+
}
|
|
1163
|
+
if (input instanceof ArrayBuffer) {
|
|
1164
|
+
return checkedBytes(new Uint8Array(input), maxFileSizeBytes);
|
|
1165
|
+
}
|
|
1166
|
+
throw new ExtractionFailure("INVALID_INPUT", "Input must be a local path, HTTP(S) URL, ArrayBuffer, Buffer, or Uint8Array.");
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/pdf/open-document.ts
|
|
1170
|
+
init_errors();
|
|
1171
|
+
async function installCanvasGlobals() {
|
|
1172
|
+
const canvas = await import("@napi-rs/canvas");
|
|
1173
|
+
const target = globalThis;
|
|
1174
|
+
target["DOMMatrix"] ??= canvas.DOMMatrix;
|
|
1175
|
+
target["ImageData"] ??= canvas.ImageData;
|
|
1176
|
+
target["Path2D"] ??= canvas.Path2D;
|
|
1177
|
+
}
|
|
1178
|
+
async function openPdfDocument(data, maxSourceImagePixels, maxCanvasPixels) {
|
|
1179
|
+
await installCanvasGlobals();
|
|
1180
|
+
const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1181
|
+
const loadingTask = pdfjs.getDocument({
|
|
1182
|
+
data,
|
|
1183
|
+
isEvalSupported: false,
|
|
1184
|
+
maxImageSize: maxSourceImagePixels,
|
|
1185
|
+
canvasMaxAreaInBytes: maxCanvasPixels * 4,
|
|
1186
|
+
useSystemFonts: true,
|
|
1187
|
+
stopAtErrors: false,
|
|
1188
|
+
verbosity: 0
|
|
1189
|
+
});
|
|
1190
|
+
try {
|
|
1191
|
+
const document = await loadingTask.promise;
|
|
1192
|
+
return {
|
|
1193
|
+
document,
|
|
1194
|
+
async close() {
|
|
1195
|
+
await loadingTask.destroy();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
await loadingTask.destroy().catch(() => void 0);
|
|
1200
|
+
const name = error instanceof Error ? error.name : "";
|
|
1201
|
+
const message = messageFromUnknown(error);
|
|
1202
|
+
if (name === "PasswordException" || /password/i.test(message)) {
|
|
1203
|
+
throw new ExtractionFailure("PASSWORD_REQUIRED", "The PDF is encrypted and requires a password.", { cause: error });
|
|
1204
|
+
}
|
|
1205
|
+
throw new ExtractionFailure("INVALID_PDF", "The PDF could not be parsed.", {
|
|
1206
|
+
cause: error
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// src/scoring/merge-results.ts
|
|
1212
|
+
var SOURCE_BASE_SCORE = {
|
|
1213
|
+
"qr-code": 0.995,
|
|
1214
|
+
code128: 0.995,
|
|
1215
|
+
"pdf-text": 0.985,
|
|
1216
|
+
"pdf-text-reconstructed": 0.97,
|
|
1217
|
+
ocr: 0.82
|
|
1218
|
+
};
|
|
1219
|
+
var SOURCE_ORDER = ["qr-code", "code128", "pdf-text", "pdf-text-reconstructed", "ocr"];
|
|
1220
|
+
function evidenceScore(evidence) {
|
|
1221
|
+
let score = SOURCE_BASE_SCORE[evidence.source];
|
|
1222
|
+
if (evidence.source === "ocr") {
|
|
1223
|
+
const ocrConfidence = Math.max(0, Math.min(100, evidence.ocrConfidence ?? 0));
|
|
1224
|
+
score = Math.max(0.75, 0.72 + ocrConfidence / 100 * 0.2);
|
|
1225
|
+
if (evidence.nearLabel) {
|
|
1226
|
+
score += 0.04;
|
|
1227
|
+
}
|
|
1228
|
+
score -= (evidence.corrections ?? 0) * 0.1;
|
|
1229
|
+
}
|
|
1230
|
+
if (evidence.nearLabel && evidence.source.startsWith("pdf-text")) {
|
|
1231
|
+
score += 5e-3;
|
|
1232
|
+
}
|
|
1233
|
+
return Math.max(0, Math.min(0.995, score));
|
|
1234
|
+
}
|
|
1235
|
+
function independentFamilies(sources) {
|
|
1236
|
+
let families = 0;
|
|
1237
|
+
if ([...sources].some((source) => source.startsWith("pdf-text"))) {
|
|
1238
|
+
families += 1;
|
|
1239
|
+
}
|
|
1240
|
+
if (sources.has("code128") || sources.has("qr-code")) {
|
|
1241
|
+
families += 1;
|
|
1242
|
+
}
|
|
1243
|
+
if (sources.has("ocr")) {
|
|
1244
|
+
families += 1;
|
|
1245
|
+
}
|
|
1246
|
+
return families;
|
|
1247
|
+
}
|
|
1248
|
+
function mergeEvidence(evidence) {
|
|
1249
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const item of evidence) {
|
|
1251
|
+
const current = grouped.get(item.accessKey) ?? [];
|
|
1252
|
+
current.push(item);
|
|
1253
|
+
grouped.set(item.accessKey, current);
|
|
1254
|
+
}
|
|
1255
|
+
const results = [];
|
|
1256
|
+
for (const [accessKey, items] of grouped) {
|
|
1257
|
+
const validation = validateAccessKey(accessKey);
|
|
1258
|
+
const components = validation.components;
|
|
1259
|
+
if (!validation.isValid || components?.documentType === null || components === null) {
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const sources = new Set(items.map((item) => item.source));
|
|
1263
|
+
const pages = [...new Set(items.map((item) => item.page))].sort((a, b) => a - b);
|
|
1264
|
+
let precisionScore = Math.max(...items.map(evidenceScore));
|
|
1265
|
+
if (independentFamilies(sources) > 1) {
|
|
1266
|
+
precisionScore = Math.min(0.999, precisionScore + 4e-3);
|
|
1267
|
+
}
|
|
1268
|
+
results.push({
|
|
1269
|
+
accessKey,
|
|
1270
|
+
documentType: components.documentType,
|
|
1271
|
+
model: components.model,
|
|
1272
|
+
format: components.format,
|
|
1273
|
+
isValid: true,
|
|
1274
|
+
precisionScore: Number(precisionScore.toFixed(3)),
|
|
1275
|
+
pages,
|
|
1276
|
+
sources: SOURCE_ORDER.filter((source) => sources.has(source)),
|
|
1277
|
+
occurrences: items.length,
|
|
1278
|
+
components
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return results.sort((left, right) => {
|
|
1282
|
+
if (right.precisionScore !== left.precisionScore) {
|
|
1283
|
+
return right.precisionScore - left.precisionScore;
|
|
1284
|
+
}
|
|
1285
|
+
return left.pages[0] - right.pages[0];
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/extractor.ts
|
|
1290
|
+
function emptyState() {
|
|
1291
|
+
return {
|
|
1292
|
+
evidence: [],
|
|
1293
|
+
warnings: [],
|
|
1294
|
+
pagesTotal: 0,
|
|
1295
|
+
pagesProcessed: 0,
|
|
1296
|
+
renderedPages: /* @__PURE__ */ new Set(),
|
|
1297
|
+
ocrPages: /* @__PURE__ */ new Set(),
|
|
1298
|
+
passesUsed: 0,
|
|
1299
|
+
fileSizeBytes: 0,
|
|
1300
|
+
complete: true
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function textEvidence(pageText, page) {
|
|
1304
|
+
const candidates = [...findCandidatesInText(pageText.orderedText, page), ...findCandidatesInText(pageText.visualLines.join("\n"), page)];
|
|
1305
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1306
|
+
for (const candidate of candidates) {
|
|
1307
|
+
const signature = `${candidate.accessKey}:${candidate.source}`;
|
|
1308
|
+
const current = unique.get(signature);
|
|
1309
|
+
if (current === void 0 || !current.nearLabel && candidate.nearLabel) {
|
|
1310
|
+
unique.set(signature, candidate);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return [...unique.values()];
|
|
1314
|
+
}
|
|
1315
|
+
function hasPageEvidence(evidence, page) {
|
|
1316
|
+
return evidence.some((candidate) => candidate.page === page);
|
|
1317
|
+
}
|
|
1318
|
+
function metadata(options, state, startedAt) {
|
|
1319
|
+
return {
|
|
1320
|
+
performance: options.performance,
|
|
1321
|
+
ocrMode: options.ocr,
|
|
1322
|
+
passesRequested: options.passes,
|
|
1323
|
+
passesUsed: state.passesUsed,
|
|
1324
|
+
pagesTotal: state.pagesTotal,
|
|
1325
|
+
pagesProcessed: state.pagesProcessed,
|
|
1326
|
+
pagesRendered: state.renderedPages.size,
|
|
1327
|
+
ocrPages: state.ocrPages.size,
|
|
1328
|
+
fileSizeBytes: state.fileSizeBytes,
|
|
1329
|
+
maxPixelsPerPage: options.maxPixelsPerPage,
|
|
1330
|
+
maxSourceImagePixels: options.maxSourceImagePixels,
|
|
1331
|
+
durationMs: Number((import_node_perf_hooks2.performance.now() - startedAt).toFixed(2)),
|
|
1332
|
+
complete: state.complete,
|
|
1333
|
+
confidenceVersion: "1.0.0"
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function resultFromState(options, state, startedAt, error = null) {
|
|
1337
|
+
const results = mergeEvidence(state.evidence);
|
|
1338
|
+
let status;
|
|
1339
|
+
if (error !== null) {
|
|
1340
|
+
status = results.length > 0 ? "partial" : "error";
|
|
1341
|
+
} else if (!state.complete) {
|
|
1342
|
+
status = "partial";
|
|
1343
|
+
} else {
|
|
1344
|
+
status = results.length > 0 ? "success" : "not_found";
|
|
1345
|
+
}
|
|
1346
|
+
const precisionScore = results.length === 0 ? 0 : Math.min(...results.map((candidate) => candidate.precisionScore));
|
|
1347
|
+
return {
|
|
1348
|
+
status,
|
|
1349
|
+
success: results.length > 0,
|
|
1350
|
+
precisionScore: Number(precisionScore.toFixed(3)),
|
|
1351
|
+
bestMatch: results[0] ?? null,
|
|
1352
|
+
results,
|
|
1353
|
+
metadata: metadata(options, state, startedAt),
|
|
1354
|
+
warnings: state.warnings,
|
|
1355
|
+
error
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
function invalidOptionsResult(error, startedAt) {
|
|
1359
|
+
const options = resolveOptions();
|
|
1360
|
+
const state = emptyState();
|
|
1361
|
+
state.complete = false;
|
|
1362
|
+
return resultFromState(options, state, startedAt, {
|
|
1363
|
+
code: "INVALID_OPTIONS",
|
|
1364
|
+
message: error.message
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
async function withPage(handle, pageNumber, work) {
|
|
1368
|
+
const page = await handle.document.getPage(pageNumber);
|
|
1369
|
+
try {
|
|
1370
|
+
return await work(page);
|
|
1371
|
+
} finally {
|
|
1372
|
+
page.cleanup();
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function ocrRecipes(recipes, options) {
|
|
1376
|
+
const unrotated = recipes.filter((recipe) => recipe.rotation === 0).sort((left, right) => right.scale - left.scale)[0];
|
|
1377
|
+
const selected = unrotated === void 0 ? [] : [unrotated];
|
|
1378
|
+
if (options.performance === "accurate") {
|
|
1379
|
+
selected.push(...recipes.filter((recipe) => recipe.rotation !== 0));
|
|
1380
|
+
}
|
|
1381
|
+
return selected;
|
|
1382
|
+
}
|
|
1383
|
+
async function collectTextEvidence(handle, state, pageLimit, options, guard) {
|
|
1384
|
+
const processedPages = [];
|
|
1385
|
+
for (let pageNumber = 1; pageNumber <= pageLimit; pageNumber += 1) {
|
|
1386
|
+
guard.check();
|
|
1387
|
+
const candidates = await withPage(handle, pageNumber, async (page) => textEvidence(await extractPageText(page), pageNumber));
|
|
1388
|
+
guard.check();
|
|
1389
|
+
state.evidence.push(...candidates);
|
|
1390
|
+
state.pagesProcessed += 1;
|
|
1391
|
+
processedPages.push(pageNumber);
|
|
1392
|
+
if (options.stopAfterFirst && candidates.length > 0) {
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return processedPages;
|
|
1397
|
+
}
|
|
1398
|
+
async function collectBarcodeEvidence(handle, state, pages, recipes, options, guard) {
|
|
1399
|
+
const exhaustive = options.ocr === "always" || options.performance === "accurate";
|
|
1400
|
+
let pending = exhaustive ? [...pages] : pages.filter((page) => !hasPageEvidence(state.evidence, page));
|
|
1401
|
+
if (pending.length === 0) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const [{ renderPage: renderPage2 }, { readBarcodes: readBarcodes2 }] = await Promise.all([Promise.resolve().then(() => (init_render_page(), render_page_exports)), Promise.resolve().then(() => (init_barcode_reader(), barcode_reader_exports))]);
|
|
1405
|
+
for (let passIndex = 0; passIndex < recipes.length && pending.length > 0; passIndex += 1) {
|
|
1406
|
+
const recipe = recipes[passIndex];
|
|
1407
|
+
if (recipe === void 0) {
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
state.passesUsed = Math.max(state.passesUsed, passIndex + 1);
|
|
1411
|
+
const unresolved = [];
|
|
1412
|
+
for (const pageNumber of pending) {
|
|
1413
|
+
guard.check();
|
|
1414
|
+
const candidates = await withPage(handle, pageNumber, async (page) => {
|
|
1415
|
+
const rendered = await renderPage2(page, recipe, options.maxPixelsPerPage);
|
|
1416
|
+
state.renderedPages.add(pageNumber);
|
|
1417
|
+
return (await readBarcodes2(rendered)).flatMap((barcode) => findCandidatesInDecodedValue(barcode.text, pageNumber, barcode.source, passIndex + 1));
|
|
1418
|
+
});
|
|
1419
|
+
guard.check();
|
|
1420
|
+
state.evidence.push(...candidates);
|
|
1421
|
+
if (candidates.length === 0 || exhaustive) {
|
|
1422
|
+
unresolved.push(pageNumber);
|
|
1423
|
+
}
|
|
1424
|
+
if (options.stopAfterFirst && candidates.length > 0) {
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
pending = unresolved;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
async function collectOcrEvidence(handle, state, pages, recipes, options, guard) {
|
|
1432
|
+
if (options.ocr === "never") {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
const pending = options.ocr === "always" ? pages : pages.filter((page) => !hasPageEvidence(state.evidence, page));
|
|
1436
|
+
if (pending.length === 0) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
const selectedRecipes = ocrRecipes(recipes, options);
|
|
1440
|
+
if (selectedRecipes.length === 0) {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
const [{ renderPage: renderPage2 }, { createOcrSession: createOcrSession2 }] = await Promise.all([Promise.resolve().then(() => (init_render_page(), render_page_exports)), Promise.resolve().then(() => (init_ocr_reader(), ocr_reader_exports))]);
|
|
1444
|
+
const session = await createOcrSession2();
|
|
1445
|
+
try {
|
|
1446
|
+
for (const pageNumber of pending) {
|
|
1447
|
+
for (const recipe of selectedRecipes) {
|
|
1448
|
+
guard.check();
|
|
1449
|
+
const candidates = await withPage(handle, pageNumber, async (page) => {
|
|
1450
|
+
const rendered = await renderPage2(page, recipe, options.maxPixelsPerPage);
|
|
1451
|
+
state.renderedPages.add(pageNumber);
|
|
1452
|
+
state.ocrPages.add(pageNumber);
|
|
1453
|
+
const recognized = await session.recognize(rendered.toPng());
|
|
1454
|
+
return findCandidatesInOcrText(recognized.text, pageNumber, recipes.indexOf(recipe) + 1, recognized.confidence);
|
|
1455
|
+
});
|
|
1456
|
+
guard.check();
|
|
1457
|
+
state.evidence.push(...candidates);
|
|
1458
|
+
if (options.stopAfterFirst && candidates.length > 0) {
|
|
1459
|
+
return session;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return session;
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
await session.terminate().catch(() => void 0);
|
|
1466
|
+
throw error;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function extractNFeAccessKeys(input, optionsInput = {}) {
|
|
1470
|
+
const startedAt = import_node_perf_hooks2.performance.now();
|
|
1471
|
+
let options;
|
|
1472
|
+
try {
|
|
1473
|
+
options = resolveOptions(optionsInput);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
if (error instanceof InvalidOptionsError) {
|
|
1476
|
+
return invalidOptionsResult(error, startedAt);
|
|
1477
|
+
}
|
|
1478
|
+
return invalidOptionsResult(new InvalidOptionsError("Extraction options are invalid."), startedAt);
|
|
1479
|
+
}
|
|
1480
|
+
const state = emptyState();
|
|
1481
|
+
const guard = new WorkGuard(options, startedAt);
|
|
1482
|
+
let handle = null;
|
|
1483
|
+
let ocrSession = null;
|
|
1484
|
+
try {
|
|
1485
|
+
guard.check();
|
|
1486
|
+
const loaded = await loadPdfInput(input, options.maxFileSizeBytes, {
|
|
1487
|
+
...options.requestHeaders === void 0 ? {} : { requestHeaders: options.requestHeaders },
|
|
1488
|
+
signal: guard.signal
|
|
1489
|
+
});
|
|
1490
|
+
guard.check();
|
|
1491
|
+
state.fileSizeBytes = loaded.size;
|
|
1492
|
+
handle = await openPdfDocument(loaded.data, options.maxSourceImagePixels, options.maxPixelsPerPage);
|
|
1493
|
+
guard.check();
|
|
1494
|
+
state.pagesTotal = handle.document.numPages;
|
|
1495
|
+
const pageLimit = Math.min(state.pagesTotal, options.maxPages);
|
|
1496
|
+
if (pageLimit < state.pagesTotal) {
|
|
1497
|
+
state.complete = false;
|
|
1498
|
+
state.warnings.push(`Only the first ${pageLimit} of ${state.pagesTotal} pages were processed because of maxPages.`);
|
|
1499
|
+
}
|
|
1500
|
+
const processedPages = await collectTextEvidence(handle, state, pageLimit, options, guard);
|
|
1501
|
+
if (!(options.stopAfterFirst && state.evidence.length > 0)) {
|
|
1502
|
+
const recipes = getRenderRecipes(options);
|
|
1503
|
+
await collectBarcodeEvidence(handle, state, processedPages, recipes, options, guard);
|
|
1504
|
+
if (!(options.stopAfterFirst && state.evidence.length > 0)) {
|
|
1505
|
+
ocrSession = await collectOcrEvidence(handle, state, processedPages, recipes, options, guard);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
guard.check();
|
|
1509
|
+
if (options.stopAfterFirst && state.evidence.length > 0) {
|
|
1510
|
+
state.complete = true;
|
|
1511
|
+
}
|
|
1512
|
+
return resultFromState(options, state, startedAt);
|
|
1513
|
+
} catch (error) {
|
|
1514
|
+
state.complete = false;
|
|
1515
|
+
let resolvedError = error;
|
|
1516
|
+
try {
|
|
1517
|
+
guard.check();
|
|
1518
|
+
} catch (guardError) {
|
|
1519
|
+
resolvedError = guardError;
|
|
1520
|
+
}
|
|
1521
|
+
const failure = resolvedError instanceof ExtractionFailure ? resolvedError : new ExtractionFailure("PROCESSING_ERROR", "The PDF could not be processed.", {
|
|
1522
|
+
cause: resolvedError
|
|
1523
|
+
});
|
|
1524
|
+
if (!(resolvedError instanceof ExtractionFailure)) {
|
|
1525
|
+
state.warnings.push(`Internal detail: ${messageFromUnknown(resolvedError)}`);
|
|
1526
|
+
}
|
|
1527
|
+
return resultFromState(options, state, startedAt, {
|
|
1528
|
+
code: failure.code,
|
|
1529
|
+
message: failure.message
|
|
1530
|
+
});
|
|
1531
|
+
} finally {
|
|
1532
|
+
guard.dispose();
|
|
1533
|
+
await ocrSession?.terminate().catch(() => void 0);
|
|
1534
|
+
await handle?.close().catch(() => void 0);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1538
|
+
0 && (module.exports = {
|
|
1539
|
+
ACCESS_KEY_ISSUE_CODES,
|
|
1540
|
+
calculateAccessKeyCheckDigit,
|
|
1541
|
+
extractNFeAccessKeys,
|
|
1542
|
+
parseAccessKey,
|
|
1543
|
+
validateAccessKey,
|
|
1544
|
+
validateIssuerIdentifier
|
|
1545
|
+
});
|
|
1546
|
+
//# sourceMappingURL=index.cjs.map
|