@semiont/content 0.5.24 → 0.5.25
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/README.md +55 -11
- package/dist/index.d.ts +328 -31
- package/dist/index.js +780 -61
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -202,111 +202,830 @@ function deriveStorageUri(name, format) {
|
|
|
202
202
|
return `file://${slug}${MEDIA_TYPES[format].extension}`;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// src/content-extractor.ts
|
|
206
|
+
import { decodeRepresentation } from "@semiont/core";
|
|
207
|
+
|
|
208
|
+
// src/pdf-extractor.ts
|
|
209
|
+
import { isObject as isObject4 } from "@semiont/core";
|
|
210
|
+
|
|
205
211
|
// src/extract-pdf-text-layer.ts
|
|
206
212
|
import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
213
|
+
import { isObject, isString, isNumber, isArray, anchorRuns, isTextRun } from "@semiont/core";
|
|
214
|
+
function toFormField(entry) {
|
|
215
|
+
if (!isObject(entry)) return null;
|
|
216
|
+
const { name, value, page, rect } = entry;
|
|
217
|
+
if (!isString(name) || !isString(value) || !value.trim()) return null;
|
|
218
|
+
if (!isNumber(page) || page < 0) return null;
|
|
219
|
+
if (!isArray(rect) || rect.length < 4 || !rect.every(isNumber)) return null;
|
|
220
|
+
const [x1, y1, x2, y2] = rect;
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
value: value.trim(),
|
|
224
|
+
page: page + 1,
|
|
225
|
+
// pdf.js reports 0-indexed; PdfTextItem is 1-indexed
|
|
226
|
+
x: Math.min(x1, x2),
|
|
227
|
+
y: Math.min(y1, y2),
|
|
228
|
+
width: Math.abs(x2 - x1),
|
|
229
|
+
height: Math.abs(y2 - y1)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function readFormFields(doc) {
|
|
233
|
+
const fieldObjects = await doc.getFieldObjects();
|
|
234
|
+
if (!fieldObjects) return [];
|
|
235
|
+
const byName = /* @__PURE__ */ new Map();
|
|
236
|
+
for (const entries of Object.values(fieldObjects)) {
|
|
237
|
+
if (!isArray(entries)) continue;
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
const field = toFormField(entry);
|
|
240
|
+
if (field && !byName.has(field.name)) byName.set(field.name, field);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return [...byName.values()];
|
|
244
|
+
}
|
|
207
245
|
async function extractPdfTextLayer(bytes) {
|
|
208
|
-
const
|
|
209
|
-
const
|
|
246
|
+
const data = new Uint8Array(bytes);
|
|
247
|
+
const loadingTask = pdfjs.getDocument({ data });
|
|
210
248
|
try {
|
|
249
|
+
const doc = await loadingTask.promise;
|
|
211
250
|
const pages = [];
|
|
212
251
|
const items = [];
|
|
213
252
|
let text = "";
|
|
214
|
-
let hasAnyTextItems = false;
|
|
215
253
|
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
|
216
254
|
const page = await doc.getPage(pageNum);
|
|
217
255
|
const viewport = page.getViewport({ scale: 1 });
|
|
218
256
|
const content = await page.getTextContent();
|
|
257
|
+
const pageTextStart = text.length;
|
|
258
|
+
const page1 = anchorRuns(content.items.filter(isTextRun), pageNum);
|
|
259
|
+
for (const item of page1.items) {
|
|
260
|
+
items.push({ ...item, start: item.start + pageTextStart, end: item.end + pageTextStart });
|
|
261
|
+
}
|
|
262
|
+
text += page1.text;
|
|
263
|
+
text += "\n";
|
|
219
264
|
pages.push({
|
|
220
265
|
pageNumber: pageNum,
|
|
221
266
|
widthPt: viewport.width,
|
|
222
|
-
heightPt: viewport.height
|
|
267
|
+
heightPt: viewport.height,
|
|
268
|
+
textStart: pageTextStart,
|
|
269
|
+
textEnd: text.length,
|
|
270
|
+
hasTextLayer: page1.items.length > 0
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (!pages.some((page) => page.hasTextLayer)) return null;
|
|
274
|
+
return { pages, text, items, fields: await readFormFields(doc) };
|
|
275
|
+
} finally {
|
|
276
|
+
await loadingTask.destroy();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/pdf-tables.ts
|
|
281
|
+
var MIN_ROWS = 3;
|
|
282
|
+
var MIN_COLUMNS = 2;
|
|
283
|
+
var ROW_TOLERANCE = 0.5;
|
|
284
|
+
var CELL_GAP = 0.8;
|
|
285
|
+
function median(values) {
|
|
286
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
287
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
288
|
+
}
|
|
289
|
+
function groupRows(items, tolerance) {
|
|
290
|
+
const rows = [];
|
|
291
|
+
for (const item of [...items].sort((a, b) => b.y - a.y)) {
|
|
292
|
+
const row = rows[rows.length - 1];
|
|
293
|
+
if (row && Math.abs(row[0].y - item.y) <= tolerance) row.push(item);
|
|
294
|
+
else rows.push([item]);
|
|
295
|
+
}
|
|
296
|
+
return rows;
|
|
297
|
+
}
|
|
298
|
+
function toCell(runs, text) {
|
|
299
|
+
const x = Math.min(...runs.map((r) => r.x));
|
|
300
|
+
const y = Math.min(...runs.map((r) => r.y));
|
|
301
|
+
const right = Math.max(...runs.map((r) => r.x + r.width));
|
|
302
|
+
const top = Math.max(...runs.map((r) => r.y + r.height));
|
|
303
|
+
return {
|
|
304
|
+
text: runs.map((r) => text.slice(r.start, r.end)).join(" ").trim(),
|
|
305
|
+
x,
|
|
306
|
+
y,
|
|
307
|
+
width: right - x,
|
|
308
|
+
height: top - y
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function toCells(row, gap, text) {
|
|
312
|
+
const cells = [];
|
|
313
|
+
let current = [];
|
|
314
|
+
for (const item of [...row].sort((a, b) => a.x - b.x)) {
|
|
315
|
+
const previous = current[current.length - 1];
|
|
316
|
+
if (previous && item.x - (previous.x + previous.width) > gap) {
|
|
317
|
+
cells.push(toCell(current, text));
|
|
318
|
+
current = [];
|
|
319
|
+
}
|
|
320
|
+
current.push(item);
|
|
321
|
+
}
|
|
322
|
+
if (current.length > 0) cells.push(toCell(current, text));
|
|
323
|
+
return cells;
|
|
324
|
+
}
|
|
325
|
+
function detectTable(items, text) {
|
|
326
|
+
if (items.length === 0) return null;
|
|
327
|
+
const unit = median(items.map((i) => i.height).filter((h) => h > 0)) || 12;
|
|
328
|
+
const rows = groupRows(items, unit * ROW_TOLERANCE).map((row) => toCells(row, unit * CELL_GAP, text));
|
|
329
|
+
if (rows.length < MIN_ROWS) return null;
|
|
330
|
+
const columnCount = rows[0].length;
|
|
331
|
+
if (columnCount < MIN_COLUMNS) return null;
|
|
332
|
+
if (!rows.every((row) => row.length === columnCount)) return null;
|
|
333
|
+
for (let column = 0; column < columnCount; column++) {
|
|
334
|
+
const lefts = rows.map((row) => row[column].x);
|
|
335
|
+
if (Math.max(...lefts) - Math.min(...lefts) > unit) return null;
|
|
336
|
+
}
|
|
337
|
+
if (rows.some((row) => row.some((cell) => cell.text.length === 0))) return null;
|
|
338
|
+
return rows;
|
|
339
|
+
}
|
|
340
|
+
function renderTable(rows, page, offset) {
|
|
341
|
+
let text = "";
|
|
342
|
+
const items = [];
|
|
343
|
+
rows.forEach((row, rowIndex) => {
|
|
344
|
+
text += "|";
|
|
345
|
+
for (const cell of row) {
|
|
346
|
+
text += " ";
|
|
347
|
+
const start = offset + text.length;
|
|
348
|
+
text += cell.text;
|
|
349
|
+
items.push({
|
|
350
|
+
start,
|
|
351
|
+
end: offset + text.length,
|
|
352
|
+
page,
|
|
353
|
+
x: cell.x,
|
|
354
|
+
y: cell.y,
|
|
355
|
+
width: cell.width,
|
|
356
|
+
height: cell.height
|
|
223
357
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
358
|
+
text += " |";
|
|
359
|
+
}
|
|
360
|
+
text += "\n";
|
|
361
|
+
if (rowIndex === 0) text += `|${" --- |".repeat(row.length)}
|
|
362
|
+
`;
|
|
363
|
+
});
|
|
364
|
+
return { text, items };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/pdf-page-images.ts
|
|
368
|
+
import * as pdfjs2 from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
369
|
+
import { isObject as isObject2, isNumber as isNumber2, isString as isString2, isArray as isArray2 } from "@semiont/core";
|
|
370
|
+
|
|
371
|
+
// src/png-encode.ts
|
|
372
|
+
import zlib from "zlib";
|
|
373
|
+
var CRC_TABLE = (() => {
|
|
374
|
+
const table = new Int32Array(256);
|
|
375
|
+
for (let n = 0; n < 256; n++) {
|
|
376
|
+
let c = n;
|
|
377
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
378
|
+
table[n] = c;
|
|
379
|
+
}
|
|
380
|
+
return table;
|
|
381
|
+
})();
|
|
382
|
+
function crc32(buf) {
|
|
383
|
+
let c = -1;
|
|
384
|
+
for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
|
|
385
|
+
return (c ^ -1) >>> 0;
|
|
386
|
+
}
|
|
387
|
+
function chunk(type, data) {
|
|
388
|
+
const length = Buffer.alloc(4);
|
|
389
|
+
length.writeUInt32BE(data.length);
|
|
390
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
391
|
+
const crc = Buffer.alloc(4);
|
|
392
|
+
crc.writeUInt32BE(crc32(body));
|
|
393
|
+
return Buffer.concat([length, body, crc]);
|
|
394
|
+
}
|
|
395
|
+
function encodePng(width, height, rgb) {
|
|
396
|
+
const stride = width * 3 + 1;
|
|
397
|
+
const raw = Buffer.alloc(stride * height);
|
|
398
|
+
for (let y = 0; y < height; y++) {
|
|
399
|
+
raw[y * stride] = 0;
|
|
400
|
+
Buffer.from(rgb.buffer, rgb.byteOffset + y * width * 3, width * 3).copy(raw, y * stride + 1);
|
|
401
|
+
}
|
|
402
|
+
const ihdr = Buffer.alloc(13);
|
|
403
|
+
ihdr.writeUInt32BE(width, 0);
|
|
404
|
+
ihdr.writeUInt32BE(height, 4);
|
|
405
|
+
ihdr[8] = 8;
|
|
406
|
+
ihdr[9] = 2;
|
|
407
|
+
return Buffer.concat([
|
|
408
|
+
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
409
|
+
chunk("IHDR", ihdr),
|
|
410
|
+
chunk("IDAT", zlib.deflateSync(raw, { level: 9 })),
|
|
411
|
+
chunk("IEND", Buffer.alloc(0))
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/pdf-page-images.ts
|
|
416
|
+
var GRAYSCALE_1BPP = 1;
|
|
417
|
+
var RGB_24BPP = 2;
|
|
418
|
+
var RGBA_32BPP = 3;
|
|
419
|
+
var IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
420
|
+
var MAX_IMAGE_PIXELS = 48e6;
|
|
421
|
+
function withinPixelBudget(width, height) {
|
|
422
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return false;
|
|
423
|
+
if (width <= 0 || height <= 0) return false;
|
|
424
|
+
return width * height <= MAX_IMAGE_PIXELS;
|
|
425
|
+
}
|
|
426
|
+
function findPlacedImages(fnArray, argsArray) {
|
|
427
|
+
const placed = [];
|
|
428
|
+
const stack = [];
|
|
429
|
+
let ctm = [...IDENTITY];
|
|
430
|
+
for (let i = 0; i < fnArray.length; i++) {
|
|
431
|
+
const op = fnArray[i];
|
|
432
|
+
const args = argsArray[i];
|
|
433
|
+
if (op === pdfjs2.OPS.save) {
|
|
434
|
+
stack.push([...ctm]);
|
|
435
|
+
} else if (op === pdfjs2.OPS.restore) {
|
|
436
|
+
ctm = stack.pop() ?? [...IDENTITY];
|
|
437
|
+
} else if (op === pdfjs2.OPS.transform) {
|
|
438
|
+
if (isArray2(args) && args.length >= 6 && args.every(isNumber2)) {
|
|
439
|
+
ctm = pdfjs2.Util.transform(ctm, args);
|
|
440
|
+
}
|
|
441
|
+
} else if (op === pdfjs2.OPS.paintImageXObject) {
|
|
442
|
+
const ref = args?.[0];
|
|
443
|
+
const width = args?.[1];
|
|
444
|
+
const height = args?.[2];
|
|
445
|
+
if (isString2(ref) && isNumber2(width) && isNumber2(height)) {
|
|
446
|
+
placed.push({ ref, width, height, ctm: [...ctm] });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return placed;
|
|
451
|
+
}
|
|
452
|
+
function asBytes(data) {
|
|
453
|
+
if (data instanceof Uint8Array) return data;
|
|
454
|
+
if (data instanceof Uint8ClampedArray) return new Uint8Array(data.buffer, data.byteOffset, data.length);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function toRgb(image) {
|
|
458
|
+
if (!isObject2(image)) return null;
|
|
459
|
+
const { width, height, kind } = image;
|
|
460
|
+
const data = asBytes(image.data);
|
|
461
|
+
if (!isNumber2(width) || !isNumber2(height) || !data) return null;
|
|
462
|
+
if (width <= 0 || height <= 0) return null;
|
|
463
|
+
if (kind === RGB_24BPP) {
|
|
464
|
+
return data.length >= width * height * 3 ? { width, height, rgb: data } : null;
|
|
465
|
+
}
|
|
466
|
+
if (kind === RGBA_32BPP) {
|
|
467
|
+
if (data.length < width * height * 4) return null;
|
|
468
|
+
const rgb = new Uint8Array(width * height * 3);
|
|
469
|
+
for (let i = 0, o = 0; o < rgb.length; i += 4, o += 3) {
|
|
470
|
+
rgb[o] = data[i];
|
|
471
|
+
rgb[o + 1] = data[i + 1];
|
|
472
|
+
rgb[o + 2] = data[i + 2];
|
|
473
|
+
}
|
|
474
|
+
return { width, height, rgb };
|
|
475
|
+
}
|
|
476
|
+
if (kind === GRAYSCALE_1BPP) {
|
|
477
|
+
const rowBytes = Math.ceil(width / 8);
|
|
478
|
+
if (data.length < rowBytes * height) return null;
|
|
479
|
+
const rgb = new Uint8Array(width * height * 3);
|
|
480
|
+
for (let y = 0; y < height; y++) {
|
|
481
|
+
for (let x = 0; x < width; x++) {
|
|
482
|
+
const bit = data[y * rowBytes + (x >> 3)] & 128 >> (x & 7);
|
|
483
|
+
const value = bit ? 255 : 0;
|
|
484
|
+
const o = (y * width + x) * 3;
|
|
485
|
+
rgb[o] = value;
|
|
486
|
+
rgb[o + 1] = value;
|
|
487
|
+
rgb[o + 2] = value;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { width, height, rgb };
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
var IMAGE_RESOLVE_TIMEOUT_MS = 3e4;
|
|
495
|
+
function resolveImage(page, ref) {
|
|
496
|
+
const scope = ref.startsWith("g_") ? page.commonObjs : page.objs;
|
|
497
|
+
return new Promise((resolve) => {
|
|
498
|
+
const timer = setTimeout(() => resolve(null), IMAGE_RESOLVE_TIMEOUT_MS);
|
|
499
|
+
const settle = (value) => {
|
|
500
|
+
clearTimeout(timer);
|
|
501
|
+
resolve(value);
|
|
502
|
+
};
|
|
503
|
+
try {
|
|
504
|
+
scope.get(ref, settle);
|
|
505
|
+
} catch {
|
|
506
|
+
settle(null);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
async function extractPageImages(bytes, pageNumbers) {
|
|
511
|
+
const wanted = pageNumbers ? new Set(pageNumbers) : null;
|
|
512
|
+
const loadingTask = pdfjs2.getDocument({ data: new Uint8Array(bytes) });
|
|
513
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
514
|
+
try {
|
|
515
|
+
const doc = await loadingTask.promise;
|
|
516
|
+
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
|
|
517
|
+
if (wanted && !wanted.has(pageNum)) continue;
|
|
518
|
+
const page = await doc.getPage(pageNum);
|
|
519
|
+
const ops = await page.getOperatorList();
|
|
520
|
+
const images = [];
|
|
521
|
+
for (const placement of findPlacedImages(ops.fnArray, ops.argsArray)) {
|
|
522
|
+
if (!withinPixelBudget(placement.width, placement.height)) continue;
|
|
523
|
+
const rgb = toRgb(await resolveImage(page, placement.ref));
|
|
524
|
+
if (!rgb) continue;
|
|
525
|
+
images.push({
|
|
526
|
+
png: encodePng(rgb.width, rgb.height, rgb.rgb),
|
|
527
|
+
width: rgb.width,
|
|
528
|
+
height: rgb.height,
|
|
529
|
+
ctm: placement.ctm
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (images.length > 0) byPage.set(pageNum, images);
|
|
533
|
+
}
|
|
534
|
+
return byPage;
|
|
535
|
+
} finally {
|
|
536
|
+
await loadingTask.destroy();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/ocr.ts
|
|
541
|
+
import { createRequire } from "module";
|
|
542
|
+
import { createWorker } from "tesseract.js";
|
|
543
|
+
import { isObject as isObject3, isString as isString3 } from "@semiont/core";
|
|
544
|
+
var cachedLangPath;
|
|
545
|
+
function langPath() {
|
|
546
|
+
if (cachedLangPath) return cachedLangPath;
|
|
547
|
+
const data = createRequire(import.meta.url)("@tesseract.js-data/eng");
|
|
548
|
+
if (!isObject3(data) || !isString3(data.langPath)) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
"Vendored OCR language data is missing or malformed: @tesseract.js-data/eng did not export a langPath"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
cachedLangPath = data.langPath;
|
|
554
|
+
return cachedLangPath;
|
|
555
|
+
}
|
|
556
|
+
function assemblePage(blocks) {
|
|
557
|
+
let text = "";
|
|
558
|
+
const words = [];
|
|
559
|
+
for (const block of blocks ?? []) {
|
|
560
|
+
for (const paragraph of block.paragraphs ?? []) {
|
|
561
|
+
for (const line of paragraph.lines ?? []) {
|
|
562
|
+
let wroteWord = false;
|
|
563
|
+
for (const word of line.words ?? []) {
|
|
564
|
+
const value = word.text.trim();
|
|
565
|
+
if (!value) continue;
|
|
566
|
+
if (wroteWord) text += " ";
|
|
228
567
|
const start = text.length;
|
|
229
|
-
text +=
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
items.push({
|
|
568
|
+
text += value;
|
|
569
|
+
words.push({
|
|
570
|
+
text: value,
|
|
233
571
|
start,
|
|
234
|
-
end,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
572
|
+
end: text.length,
|
|
573
|
+
// Horizontal extent from the word, vertical from the
|
|
574
|
+
// line. OCR boxes hug their glyphs, so a descender
|
|
575
|
+
// ('page') sits lower than its neighbours — and
|
|
576
|
+
// `locate()` groups items into lines by comparing `y`
|
|
577
|
+
// within a couple of points, a threshold that holds
|
|
578
|
+
// because NATIVE runs take y from the shared baseline.
|
|
579
|
+
// Passing per-word descenders through would split one
|
|
580
|
+
// visual line into several rects and draw a highlight
|
|
581
|
+
// as stacked fragments. Nothing is lost: `locate()`
|
|
582
|
+
// bounds each line anyway, so per-word vertical extent
|
|
583
|
+
// never reaches an annotation.
|
|
584
|
+
bbox: {
|
|
585
|
+
x0: word.bbox.x0,
|
|
586
|
+
x1: word.bbox.x1,
|
|
587
|
+
y0: line.bbox.y0,
|
|
588
|
+
y1: line.bbox.y1
|
|
589
|
+
},
|
|
590
|
+
confidence: word.confidence
|
|
240
591
|
});
|
|
241
|
-
|
|
242
|
-
} else if (item.hasEOL) {
|
|
243
|
-
text += "\n";
|
|
592
|
+
wroteWord = true;
|
|
244
593
|
}
|
|
594
|
+
if (wroteWord) text += "\n";
|
|
245
595
|
}
|
|
246
596
|
text += "\n";
|
|
247
597
|
}
|
|
248
|
-
|
|
249
|
-
|
|
598
|
+
}
|
|
599
|
+
return { text: text.trimEnd(), words };
|
|
600
|
+
}
|
|
601
|
+
async function recognizeImages(images) {
|
|
602
|
+
if (images.length === 0) return [];
|
|
603
|
+
const worker = await createWorker("eng", void 0, {
|
|
604
|
+
langPath: langPath(),
|
|
605
|
+
cacheMethod: "none"
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
const results = [];
|
|
609
|
+
for (const image of images) {
|
|
610
|
+
const { data } = await worker.recognize(image, {}, { blocks: true, text: false });
|
|
611
|
+
results.push(assemblePage(data.blocks));
|
|
612
|
+
}
|
|
613
|
+
return results;
|
|
250
614
|
} finally {
|
|
251
|
-
await
|
|
615
|
+
await worker.terminate();
|
|
252
616
|
}
|
|
253
617
|
}
|
|
254
618
|
|
|
255
|
-
// src/
|
|
256
|
-
|
|
257
|
-
function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
619
|
+
// src/ocr-geometry.ts
|
|
620
|
+
import * as pdfjs3 from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
621
|
+
function toPagePoint(px, py, placement) {
|
|
622
|
+
const point = [px / placement.width, 1 - py / placement.height];
|
|
623
|
+
pdfjs3.Util.applyTransform(point, placement.ctm);
|
|
624
|
+
return point;
|
|
625
|
+
}
|
|
626
|
+
function mapWordsToItems(words, placement, page, textOffset) {
|
|
627
|
+
if (placement.width <= 0 || placement.height <= 0) return [];
|
|
628
|
+
return words.map((word) => {
|
|
629
|
+
const [ax, ay] = toPagePoint(word.bbox.x0, word.bbox.y0, placement);
|
|
630
|
+
const [bx, by] = toPagePoint(word.bbox.x1, word.bbox.y1, placement);
|
|
631
|
+
const x = Math.min(ax, bx);
|
|
632
|
+
const y = Math.min(ay, by);
|
|
633
|
+
return {
|
|
634
|
+
start: word.start + textOffset,
|
|
635
|
+
end: word.end + textOffset,
|
|
636
|
+
page,
|
|
637
|
+
x,
|
|
638
|
+
y,
|
|
639
|
+
width: Math.abs(bx - ax),
|
|
640
|
+
height: Math.abs(by - ay)
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/pdf-extractor.ts
|
|
646
|
+
var MAX_PDF_BYTES = 200 * 1024 * 1024;
|
|
647
|
+
function withinByteBudget(bytes) {
|
|
648
|
+
return Number.isFinite(bytes) && bytes >= 0 && bytes <= MAX_PDF_BYTES;
|
|
649
|
+
}
|
|
650
|
+
var LOW_CONFIDENCE = 60;
|
|
651
|
+
function summarize(confidences) {
|
|
652
|
+
if (confidences.length === 0) return void 0;
|
|
653
|
+
const total = confidences.reduce((sum, c) => sum + c, 0);
|
|
654
|
+
return {
|
|
655
|
+
mean: Math.round(total / confidences.length * 10) / 10,
|
|
656
|
+
lowConfidenceWords: confidences.filter((c) => c < LOW_CONFIDENCE).length,
|
|
657
|
+
totalWords: confidences.length
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
async function ocrPages(content, pageNumbers) {
|
|
661
|
+
const imagesByPage = await extractPageImages(content, pageNumbers);
|
|
662
|
+
if (imagesByPage.size === 0) return { text: "", items: [], confidences: [] };
|
|
663
|
+
const pages = [...imagesByPage.keys()].sort((a, b) => a - b);
|
|
664
|
+
const batch = pages.flatMap((page) => imagesByPage.get(page).map((image) => image.png));
|
|
665
|
+
const recognized = await recognizeImages(batch);
|
|
666
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
667
|
+
let cursor = 0;
|
|
668
|
+
for (const page of pages) {
|
|
669
|
+
const images = imagesByPage.get(page);
|
|
670
|
+
let text = "";
|
|
671
|
+
const items = [];
|
|
672
|
+
const confidences = [];
|
|
673
|
+
for (const image of images) {
|
|
674
|
+
const result = recognized[cursor++];
|
|
675
|
+
if (!result?.text.trim()) continue;
|
|
676
|
+
if (text) text += "\n";
|
|
677
|
+
items.push(...mapWordsToItems(result.words, image, page, text.length));
|
|
678
|
+
confidences.push(...result.words.map((word) => word.confidence));
|
|
679
|
+
text += result.text;
|
|
272
680
|
}
|
|
681
|
+
if (text) byPage.set(page, { text, items, confidences });
|
|
273
682
|
}
|
|
274
|
-
return
|
|
683
|
+
return joinPages(byPage, 0);
|
|
275
684
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
685
|
+
function joinPages(byPage, baseOffset) {
|
|
686
|
+
let text = "";
|
|
687
|
+
const items = [];
|
|
688
|
+
const confidences = [];
|
|
689
|
+
for (const [, page] of [...byPage.entries()].sort((a, b) => a[0] - b[0])) {
|
|
690
|
+
if (text) text += "\n\n";
|
|
691
|
+
const shift = baseOffset + text.length;
|
|
692
|
+
for (const item of page.items) {
|
|
693
|
+
items.push({ ...item, start: item.start + shift, end: item.end + shift });
|
|
694
|
+
}
|
|
695
|
+
confidences.push(...page.confidences);
|
|
696
|
+
text += page.text;
|
|
697
|
+
}
|
|
698
|
+
return { text, items, confidences };
|
|
699
|
+
}
|
|
700
|
+
function classifyPdfError(error) {
|
|
701
|
+
return isObject4(error) && error.name === "PasswordException" ? "encrypted" : "corrupt";
|
|
702
|
+
}
|
|
703
|
+
function foldFormFields(layer) {
|
|
704
|
+
let text = layer.text;
|
|
705
|
+
const items = [...layer.items];
|
|
706
|
+
for (const field of layer.fields) {
|
|
707
|
+
const start = text.length + `${field.name}: `.length;
|
|
708
|
+
text += `${field.name}: ${field.value}
|
|
709
|
+
`;
|
|
710
|
+
items.push({
|
|
711
|
+
start,
|
|
712
|
+
end: start + field.value.length,
|
|
713
|
+
page: field.page,
|
|
714
|
+
x: field.x,
|
|
715
|
+
y: field.y,
|
|
716
|
+
width: field.width,
|
|
717
|
+
height: field.height
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return { text, items, method: "form", pdfClass: "E" };
|
|
721
|
+
}
|
|
722
|
+
function shapeTables(layer) {
|
|
723
|
+
const pages = layer.pages.map((page) => {
|
|
724
|
+
const pageItems = layer.items.filter((item) => item.page === page.pageNumber);
|
|
725
|
+
return { page, pageItems, table: detectTable(pageItems, layer.text) };
|
|
726
|
+
});
|
|
727
|
+
if (!pages.some((p) => p.table)) return null;
|
|
728
|
+
let text = "";
|
|
729
|
+
const items = [];
|
|
730
|
+
for (const { page, pageItems, table } of pages) {
|
|
731
|
+
if (table) {
|
|
732
|
+
const rendered = renderTable(table, page.pageNumber, text.length);
|
|
733
|
+
text += rendered.text;
|
|
734
|
+
items.push(...rendered.items);
|
|
282
735
|
} else {
|
|
283
|
-
|
|
736
|
+
const shift = text.length - page.textStart;
|
|
737
|
+
text += layer.text.slice(page.textStart, page.textEnd);
|
|
738
|
+
for (const item of pageItems) {
|
|
739
|
+
items.push({ ...item, start: item.start + shift, end: item.end + shift });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { text, items, method: "table", pdfClass: "D" };
|
|
744
|
+
}
|
|
745
|
+
var pdfExtractor = {
|
|
746
|
+
// Every non-declined PDF extraction carries positioned runs — native text
|
|
747
|
+
// layers and OCR both anchor by page geometry.
|
|
748
|
+
yieldsGeometry: true,
|
|
749
|
+
async extract(content, _mediaType, cache) {
|
|
750
|
+
const hit = await cache?.store.read(cache.key);
|
|
751
|
+
if (hit) return hit;
|
|
752
|
+
const outcome = await extractPdf(content);
|
|
753
|
+
if (cache) {
|
|
754
|
+
if ("declined" in outcome) await cache.store.write(cache.key, outcome);
|
|
755
|
+
else if (outcome.items) await cache.store.write(cache.key, { ...outcome, items: outcome.items });
|
|
284
756
|
}
|
|
757
|
+
return outcome;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
async function extractPdf(content) {
|
|
761
|
+
if (!withinByteBudget(content.length)) return { declined: "too-large" };
|
|
762
|
+
let layer;
|
|
763
|
+
try {
|
|
764
|
+
layer = await extractPdfTextLayer(content);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return { declined: classifyPdfError(error) };
|
|
767
|
+
}
|
|
768
|
+
if (!layer) {
|
|
769
|
+
const ocr2 = await ocrPages(content);
|
|
770
|
+
if (!ocr2.text) return { declined: "no-text-layer" };
|
|
771
|
+
const confidence2 = summarize(ocr2.confidences);
|
|
772
|
+
return {
|
|
773
|
+
text: ocr2.text,
|
|
774
|
+
items: ocr2.items,
|
|
775
|
+
method: "ocr",
|
|
776
|
+
pdfClass: "B",
|
|
777
|
+
...confidence2 ? { ocrConfidence: confidence2 } : {}
|
|
778
|
+
};
|
|
285
779
|
}
|
|
286
|
-
|
|
780
|
+
const shaped = layer.fields.length > 0 ? foldFormFields(layer) : shapeTables(layer) ?? { text: layer.text, items: layer.items, method: "pdf-text-layer", pdfClass: "A" };
|
|
781
|
+
const unreadPages = layer.pages.filter((page) => !page.hasTextLayer).map((page) => page.pageNumber);
|
|
782
|
+
if (unreadPages.length === 0) return shaped;
|
|
783
|
+
const recovered = await ocrPages(content, unreadPages);
|
|
784
|
+
const readPages = new Set(recovered.items.map((item) => item.page));
|
|
785
|
+
const stillUnread = unreadPages.filter((page) => !readPages.has(page));
|
|
786
|
+
const hybridClass = shaped.pdfClass === "A" ? "C" : shaped.pdfClass;
|
|
787
|
+
if (!recovered.text) {
|
|
788
|
+
return { ...shaped, unreadPages: stillUnread, pdfClass: hybridClass };
|
|
789
|
+
}
|
|
790
|
+
const shift = shaped.text.length;
|
|
791
|
+
const ocr = {
|
|
792
|
+
text: recovered.text,
|
|
793
|
+
items: recovered.items.map((item) => ({ ...item, start: item.start + shift, end: item.end + shift })),
|
|
794
|
+
confidences: recovered.confidences
|
|
795
|
+
};
|
|
796
|
+
const confidence = summarize(ocr.confidences);
|
|
797
|
+
return {
|
|
798
|
+
...shaped,
|
|
799
|
+
text: `${shaped.text}${ocr.text}
|
|
800
|
+
`,
|
|
801
|
+
items: [...shaped.items ?? [], ...ocr.items],
|
|
802
|
+
method: "ocr",
|
|
803
|
+
pdfClass: hybridClass,
|
|
804
|
+
...confidence ? { ocrConfidence: confidence } : {},
|
|
805
|
+
...stillUnread.length > 0 ? { unreadPages: stillUnread } : {}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/content-extractor.ts
|
|
810
|
+
var passthroughExtractor = {
|
|
811
|
+
yieldsGeometry: false,
|
|
812
|
+
async extract(content, mediaType) {
|
|
813
|
+
return { text: decodeRepresentation(content, mediaType), method: "text-passthrough" };
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
var EXTRACTORS = {
|
|
817
|
+
"decode": passthroughExtractor,
|
|
818
|
+
"pdf-text-layer": pdfExtractor,
|
|
819
|
+
"none": null
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// src/anchored-text-store.ts
|
|
823
|
+
import fs2 from "fs";
|
|
824
|
+
import path2 from "path";
|
|
825
|
+
import { createRequire as createRequire2 } from "module";
|
|
826
|
+
import { getShardPath, isObject as isObject5, isString as isString4, isNumber as isNumber3, isArray as isArray3 } from "@semiont/core";
|
|
827
|
+
function buildStamp() {
|
|
828
|
+
const require2 = createRequire2(import.meta.url);
|
|
829
|
+
const version = (specifier) => {
|
|
830
|
+
try {
|
|
831
|
+
const pkg = require2(specifier);
|
|
832
|
+
return isObject5(pkg) && isString4(pkg.version) ? pkg.version : "unknown";
|
|
833
|
+
} catch {
|
|
834
|
+
return "unknown";
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
return `content-${version("../package.json")}+pdfjs-${version("pdfjs-dist/package.json")}+tesseract-${version("tesseract.js/package.json")}+eng-${version("@tesseract.js-data/eng/package.json")}`;
|
|
287
838
|
}
|
|
288
|
-
|
|
289
|
-
|
|
839
|
+
var STAMP = buildStamp();
|
|
840
|
+
function encodeLines(items) {
|
|
290
841
|
const lines = [];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
842
|
+
for (const item of items) {
|
|
843
|
+
const last = lines[lines.length - 1];
|
|
844
|
+
if (last && last.p === item.page && last.y === item.y && last.h === item.height) {
|
|
845
|
+
last.words.push([item.x, item.width, item.start, item.end]);
|
|
295
846
|
} else {
|
|
296
|
-
lines.push(
|
|
297
|
-
currentLine = [item];
|
|
847
|
+
lines.push({ p: item.page, y: item.y, h: item.height, words: [[item.x, item.width, item.start, item.end]] });
|
|
298
848
|
}
|
|
299
849
|
}
|
|
300
|
-
if (currentLine.length > 0) lines.push(currentLine);
|
|
301
850
|
return lines;
|
|
302
851
|
}
|
|
852
|
+
function decodeLines(lines) {
|
|
853
|
+
const items = [];
|
|
854
|
+
for (const line of lines) {
|
|
855
|
+
for (const [x, width, start, end] of line.words) {
|
|
856
|
+
items.push({ start, end, page: line.p, x, y: line.y, width, height: line.h });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return items;
|
|
860
|
+
}
|
|
861
|
+
function isCached(value) {
|
|
862
|
+
if (!isObject5(value) || value.v !== 2 || !isString4(value.stamp)) return false;
|
|
863
|
+
if (isString4(value.declined)) return true;
|
|
864
|
+
if (!isString4(value.text) || !isString4(value.method) || !isArray3(value.lines)) return false;
|
|
865
|
+
return value.lines.every((line) => isObject5(line) && isNumber3(line.p) && isNumber3(line.y) && isNumber3(line.h) && isArray3(line.words) && line.words.every((w) => isArray3(w) && w.length === 4 && w.every(isNumber3)));
|
|
866
|
+
}
|
|
867
|
+
var VALID_KEY = /^[A-Za-z0-9_-]+$/;
|
|
868
|
+
function createAnchoredTextStore(dir, logger) {
|
|
869
|
+
const fileFor = (key) => {
|
|
870
|
+
if (!VALID_KEY.test(key)) return null;
|
|
871
|
+
const [ab, cd] = getShardPath(key);
|
|
872
|
+
return path2.join(dir, ab, cd, `${key}.json`);
|
|
873
|
+
};
|
|
874
|
+
return {
|
|
875
|
+
async read(key) {
|
|
876
|
+
let hit = null;
|
|
877
|
+
try {
|
|
878
|
+
const file = fileFor(key);
|
|
879
|
+
if (file === null) throw new Error("invalid key");
|
|
880
|
+
const parsed = JSON.parse(await fs2.promises.readFile(file, "utf8"));
|
|
881
|
+
if (isCached(parsed) && parsed.stamp === STAMP) hit = parsed;
|
|
882
|
+
} catch {
|
|
883
|
+
hit = null;
|
|
884
|
+
}
|
|
885
|
+
logger?.debug("Anchored-text cache", {
|
|
886
|
+
outcome: hit ? "hit" : "miss",
|
|
887
|
+
key,
|
|
888
|
+
...hit ? "declined" in hit ? { declined: hit.declined } : { lines: hit.lines.length } : {}
|
|
889
|
+
});
|
|
890
|
+
if (!hit) return null;
|
|
891
|
+
if ("declined" in hit) return { declined: hit.declined };
|
|
892
|
+
const { v: _v, stamp: _stamp, lines, text, ...provenance } = hit;
|
|
893
|
+
return { text, items: decodeLines(lines), ...provenance };
|
|
894
|
+
},
|
|
895
|
+
async write(key, outcome) {
|
|
896
|
+
const target = fileFor(key);
|
|
897
|
+
if (target === null) {
|
|
898
|
+
logger?.debug("Anchored-text cache: refusing invalid key", { key });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const entry = "declined" in outcome ? { v: 2, stamp: STAMP, declined: outcome.declined } : (() => {
|
|
902
|
+
const { text, items, ...provenance } = outcome;
|
|
903
|
+
return { v: 2, stamp: STAMP, text, lines: encodeLines(items), ...provenance };
|
|
904
|
+
})();
|
|
905
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
906
|
+
try {
|
|
907
|
+
await fs2.promises.mkdir(path2.dirname(target), { recursive: true });
|
|
908
|
+
await fs2.promises.writeFile(temp, JSON.stringify(entry), "utf8");
|
|
909
|
+
await fs2.promises.rename(temp, target);
|
|
910
|
+
} catch {
|
|
911
|
+
await fs2.promises.rm(temp, { force: true }).catch(() => {
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
async list() {
|
|
916
|
+
const prefix = JSON.stringify({ v: 2, stamp: STAMP }).slice(0, -1) + ",";
|
|
917
|
+
let rootNames;
|
|
918
|
+
try {
|
|
919
|
+
rootNames = await fs2.promises.readdir(dir);
|
|
920
|
+
} catch {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
let swept = 0;
|
|
924
|
+
for (const name of rootNames) {
|
|
925
|
+
if (!name.endsWith(".json")) continue;
|
|
926
|
+
await fs2.promises.rm(path2.join(dir, name), { force: true }).then(() => {
|
|
927
|
+
swept += 1;
|
|
928
|
+
}, () => {
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (swept > 0) logger?.info("Anchored-text cache: swept pre-P1 flat entries", { swept });
|
|
932
|
+
const keys = [];
|
|
933
|
+
let sweptInterim = 0;
|
|
934
|
+
for (const ab of rootNames) {
|
|
935
|
+
if (!/^[0-9a-f]{2}$/.test(ab)) continue;
|
|
936
|
+
let cdNames;
|
|
937
|
+
try {
|
|
938
|
+
cdNames = await fs2.promises.readdir(path2.join(dir, ab));
|
|
939
|
+
} catch {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
for (const cd of cdNames) {
|
|
943
|
+
if (!/^[0-9a-f]{2}$/.test(cd)) continue;
|
|
944
|
+
let names;
|
|
945
|
+
try {
|
|
946
|
+
names = await fs2.promises.readdir(path2.join(dir, ab, cd));
|
|
947
|
+
} catch {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
for (const name of names) {
|
|
951
|
+
if (!name.endsWith(".json")) continue;
|
|
952
|
+
const base = name.slice(0, -".json".length);
|
|
953
|
+
if (/^[0-9a-f]{32}$/.test(base)) {
|
|
954
|
+
await fs2.promises.rm(path2.join(dir, ab, cd, name), { force: true }).then(() => {
|
|
955
|
+
sweptInterim += 1;
|
|
956
|
+
}, () => {
|
|
957
|
+
});
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
let handle = null;
|
|
961
|
+
try {
|
|
962
|
+
handle = await fs2.promises.open(path2.join(dir, ab, cd, name), "r");
|
|
963
|
+
const buf = Buffer.alloc(prefix.length);
|
|
964
|
+
const { bytesRead } = await handle.read(buf, 0, prefix.length, 0);
|
|
965
|
+
if (bytesRead === prefix.length && buf.toString("utf8") === prefix) {
|
|
966
|
+
keys.push(base);
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
} finally {
|
|
970
|
+
await handle?.close().catch(() => {
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (sweptInterim > 0) logger?.info("Anchored-text cache: swept interim resource-id entries", { swept: sweptInterim });
|
|
977
|
+
return keys;
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/anchored-text-store-adapter.ts
|
|
983
|
+
function anchoredTextStoreOverTransport(content, logger) {
|
|
984
|
+
return {
|
|
985
|
+
async read(key) {
|
|
986
|
+
try {
|
|
987
|
+
return await content.getAnchoredTextByChecksum(key);
|
|
988
|
+
} catch (error) {
|
|
989
|
+
logger?.debug("Anchored-text cache: transport read failed \u2014 treating as miss", {
|
|
990
|
+
key,
|
|
991
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
992
|
+
});
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
async write(key, outcome) {
|
|
997
|
+
try {
|
|
998
|
+
await content.putAnchoredText(key, outcome);
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
logger?.debug("Anchored-text cache: transport write failed \u2014 entry not stored", {
|
|
1001
|
+
key,
|
|
1002
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
async list() {
|
|
1007
|
+
try {
|
|
1008
|
+
return await content.listAnchoredTextKeys();
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
logger?.debug("Anchored-text cache: transport list failed \u2014 treating as empty", {
|
|
1011
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
1012
|
+
});
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
303
1018
|
export {
|
|
304
1019
|
ChecksumMismatchError,
|
|
1020
|
+
EXTRACTORS,
|
|
1021
|
+
MAX_PDF_BYTES,
|
|
305
1022
|
WorkingTreeStore,
|
|
1023
|
+
anchoredTextStoreOverTransport,
|
|
306
1024
|
calculateChecksum,
|
|
1025
|
+
createAnchoredTextStore,
|
|
307
1026
|
deriveStorageUri,
|
|
308
1027
|
extractPdfTextLayer,
|
|
309
|
-
|
|
310
|
-
|
|
1028
|
+
verifyChecksum,
|
|
1029
|
+
withinByteBudget
|
|
311
1030
|
};
|
|
312
1031
|
//# sourceMappingURL=index.js.map
|