formatferry 1.0.9 → 1.0.11
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/dist/index.js +797 -64
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21124,27 +21124,26 @@ async function importXlsx() {
|
|
|
21124
21124
|
}
|
|
21125
21125
|
async function importPdfjs() {
|
|
21126
21126
|
if (typeof process !== "undefined") {
|
|
21127
|
-
const
|
|
21128
|
-
if (typeof
|
|
21129
|
-
let requireWorker;
|
|
21127
|
+
const p = process;
|
|
21128
|
+
if (typeof p.getBuiltinModule !== "function") {
|
|
21130
21129
|
try {
|
|
21131
|
-
const
|
|
21132
|
-
const
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
|
|
21137
|
-
|
|
21130
|
+
const mod2 = await import("node:module");
|
|
21131
|
+
const createRequire = mod2.createRequire;
|
|
21132
|
+
if (typeof createRequire === "function") {
|
|
21133
|
+
const req = createRequire(process.argv?.[1] || import.meta.url);
|
|
21134
|
+
p.getBuiltinModule = (name) => {
|
|
21135
|
+
try {
|
|
21136
|
+
return req(name);
|
|
21137
|
+
} catch {
|
|
21138
|
+
return void 0;
|
|
21139
|
+
}
|
|
21140
|
+
};
|
|
21141
|
+
} else {
|
|
21142
|
+
p.getBuiltinModule = () => void 0;
|
|
21138
21143
|
}
|
|
21139
21144
|
} catch {
|
|
21145
|
+
p.getBuiltinModule = () => void 0;
|
|
21140
21146
|
}
|
|
21141
|
-
proxyProcess.getBuiltinModule = (name) => {
|
|
21142
|
-
try {
|
|
21143
|
-
return requireWorker ? requireWorker(name) : void 0;
|
|
21144
|
-
} catch {
|
|
21145
|
-
return void 0;
|
|
21146
|
-
}
|
|
21147
|
-
};
|
|
21148
21147
|
}
|
|
21149
21148
|
}
|
|
21150
21149
|
const mod = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
@@ -21343,6 +21342,459 @@ var DocxAdapter = class {
|
|
|
21343
21342
|
};
|
|
21344
21343
|
var docxAdapter = new DocxAdapter();
|
|
21345
21344
|
|
|
21345
|
+
// ../../src/shared/feature-flags.ts
|
|
21346
|
+
var _flags = {
|
|
21347
|
+
enableDocx: true,
|
|
21348
|
+
enableXlsx: true,
|
|
21349
|
+
enablePdf: true,
|
|
21350
|
+
enableHtml: true,
|
|
21351
|
+
enablePdfTableDetection: true
|
|
21352
|
+
};
|
|
21353
|
+
function getFeatureFlags() {
|
|
21354
|
+
return { ..._flags };
|
|
21355
|
+
}
|
|
21356
|
+
|
|
21357
|
+
// ../../src/shared/adapters/pdf-table-detector.ts
|
|
21358
|
+
var CJK_REGEX = /[\u3400-\u9FFF\uF900-\uFAFF\u3040-\u30FF\uAC00-\uD7AF]/;
|
|
21359
|
+
var NUMBER_LIKE_REGEX = /^[\s\-+$€£¥]*\(?[\d.,/:%\- ]+\)?[\s%$€£¥]*$/;
|
|
21360
|
+
var DATE_LIKE_REGEX = /^\d{1,4}[-/.]\d{1,2}([-/.]\d{1,4})?$/;
|
|
21361
|
+
var SUPERSCRIPT_MERGE_GAP_FACTOR = 0.3;
|
|
21362
|
+
var VERTICAL_OVERLAP_THRESHOLD = 0.5;
|
|
21363
|
+
var COLUMN_SUPPORT_THRESHOLD = 0.6;
|
|
21364
|
+
var COLUMN_GAP_MERGE_FACTOR = 0.5;
|
|
21365
|
+
var SHORT_CELL_THRESHOLD_LATIN = 40;
|
|
21366
|
+
var SHORT_CELL_THRESHOLD_CJK = 20;
|
|
21367
|
+
var MIN_CONFIDENCE_EMIT = 0.55;
|
|
21368
|
+
var MIN_CONFIDENCE_HIGH = 0.7;
|
|
21369
|
+
var PROSE_FLOW_MAX_FRACTION = 0.3;
|
|
21370
|
+
function preFilterAndMerge(items) {
|
|
21371
|
+
const alive = items.filter(
|
|
21372
|
+
(it) => it.str.trim().length > 0 && it.width > 0 && it.height > 0
|
|
21373
|
+
);
|
|
21374
|
+
if (alive.length === 0) return [];
|
|
21375
|
+
const widths = alive.map((it) => estimateCharWidth(it)).filter((w) => w > 0);
|
|
21376
|
+
const medianCharWidth = median(widths) || 1;
|
|
21377
|
+
const mergeGap = medianCharWidth * SUPERSCRIPT_MERGE_GAP_FACTOR;
|
|
21378
|
+
const sorted = [...alive].sort((a, b) => b.y + b.height - (a.y + a.height) || a.x - b.x);
|
|
21379
|
+
const bands = [];
|
|
21380
|
+
for (const it of sorted) {
|
|
21381
|
+
const band = bands[bands.length - 1];
|
|
21382
|
+
if (band && band.some((bi) => verticalOverlap(bi, it) > 0 && bi.rotation === it.rotation)) {
|
|
21383
|
+
band.push(it);
|
|
21384
|
+
} else {
|
|
21385
|
+
bands.push([it]);
|
|
21386
|
+
}
|
|
21387
|
+
}
|
|
21388
|
+
const out = [];
|
|
21389
|
+
for (const band of bands) {
|
|
21390
|
+
band.sort((a, b) => a.x - b.x);
|
|
21391
|
+
for (const item of band) {
|
|
21392
|
+
const last = out[out.length - 1];
|
|
21393
|
+
if (last && last.rotation === item.rotation && verticalOverlap(last, item) > 0 && item.x >= last.right && item.x - last.right < mergeGap) {
|
|
21394
|
+
last.str = `${last.str}${item.str}`;
|
|
21395
|
+
last.width = item.right - last.x;
|
|
21396
|
+
last.right = item.right;
|
|
21397
|
+
last.height = Math.max(last.height, item.height);
|
|
21398
|
+
last.y = Math.min(last.y, item.y);
|
|
21399
|
+
continue;
|
|
21400
|
+
}
|
|
21401
|
+
out.push({ ...item });
|
|
21402
|
+
}
|
|
21403
|
+
}
|
|
21404
|
+
return out;
|
|
21405
|
+
}
|
|
21406
|
+
function clusterIntoRows(items) {
|
|
21407
|
+
if (items.length === 0) return [];
|
|
21408
|
+
const upright = items.filter((it) => it.rotation === 0);
|
|
21409
|
+
upright.sort((a, b) => b.y - a.y || a.x - b.x);
|
|
21410
|
+
const rows = [];
|
|
21411
|
+
for (const item of upright) {
|
|
21412
|
+
const row = rows[rows.length - 1];
|
|
21413
|
+
if (row && overlapFraction(item.y, item.y + item.height, row.yMin, row.yMax) >= VERTICAL_OVERLAP_THRESHOLD) {
|
|
21414
|
+
row.items.push(item);
|
|
21415
|
+
row.yMin = Math.min(row.yMin, item.y);
|
|
21416
|
+
row.yMax = Math.max(row.yMax, item.y + item.height);
|
|
21417
|
+
} else {
|
|
21418
|
+
rows.push({ yMin: item.y, yMax: item.y + item.height, items: [item] });
|
|
21419
|
+
}
|
|
21420
|
+
}
|
|
21421
|
+
for (const r of rows) r.items.sort((a, b) => a.x - b.x);
|
|
21422
|
+
rows.sort((a, b) => b.yMax - a.yMax);
|
|
21423
|
+
return rows;
|
|
21424
|
+
}
|
|
21425
|
+
function detectColumns(rows) {
|
|
21426
|
+
if (rows.length === 0) return [];
|
|
21427
|
+
const allItems = rows.flatMap((r) => r.items);
|
|
21428
|
+
if (allItems.length === 0) return [];
|
|
21429
|
+
const medianCharWidth = median(allItems.map((it) => estimateCharWidth(it)).filter((w) => w > 0)) || 1;
|
|
21430
|
+
const gapTol = medianCharWidth * COLUMN_GAP_MERGE_FACTOR;
|
|
21431
|
+
const intervals = allItems.map((it) => ({ x0: it.x, x1: it.right })).sort((a, b) => a.x0 - b.x0);
|
|
21432
|
+
const merged = [];
|
|
21433
|
+
for (const iv of intervals) {
|
|
21434
|
+
const last = merged[merged.length - 1];
|
|
21435
|
+
if (last && iv.x0 - last.x1 <= gapTol) {
|
|
21436
|
+
last.x1 = Math.max(last.x1, iv.x1);
|
|
21437
|
+
} else {
|
|
21438
|
+
merged.push({ ...iv });
|
|
21439
|
+
}
|
|
21440
|
+
}
|
|
21441
|
+
const anchors = [];
|
|
21442
|
+
for (const iv of merged) {
|
|
21443
|
+
const rowsWithHit = rows.filter(
|
|
21444
|
+
(r) => r.items.some((it) => rangeOverlap(it.x, it.right, iv.x0, iv.x1))
|
|
21445
|
+
);
|
|
21446
|
+
const support = rowsWithHit.length / rows.length;
|
|
21447
|
+
if (support < COLUMN_SUPPORT_THRESHOLD) continue;
|
|
21448
|
+
const alignment = chooseAlignment(rows, iv);
|
|
21449
|
+
anchors.push({ x0: iv.x0, x1: iv.x1, alignment, support });
|
|
21450
|
+
}
|
|
21451
|
+
return anchors;
|
|
21452
|
+
}
|
|
21453
|
+
function coalesceMultiLineCells(rows, columns) {
|
|
21454
|
+
if (rows.length < 2 || columns.length < 2) return rows;
|
|
21455
|
+
const gaps = rowGaps(rows);
|
|
21456
|
+
const baseline = gaps.length ? median(gaps) : 0;
|
|
21457
|
+
const tightFactor = 0.7;
|
|
21458
|
+
const out = [];
|
|
21459
|
+
for (let i = 0; i < rows.length; i++) {
|
|
21460
|
+
const cur = rows[i];
|
|
21461
|
+
const prev = out[out.length - 1];
|
|
21462
|
+
if (!prev) {
|
|
21463
|
+
out.push({ ...cur, items: [...cur.items] });
|
|
21464
|
+
continue;
|
|
21465
|
+
}
|
|
21466
|
+
const gap = prev.yMin - cur.yMax;
|
|
21467
|
+
const prevCols = columnsCovered(prev, columns);
|
|
21468
|
+
const curCols = columnsCovered(cur, columns);
|
|
21469
|
+
const curIsSubset = [...curCols].every((c) => prevCols.has(c));
|
|
21470
|
+
const covers = curCols.size >= 1 && curCols.size < columns.length;
|
|
21471
|
+
const isTight = baseline > 0 && gap < baseline * tightFactor;
|
|
21472
|
+
const lastColOnly = curCols.size === 1;
|
|
21473
|
+
const isIntraCell = isTight && curIsSubset && covers || lastColOnly && gap < baseline * 1.5 && curIsSubset;
|
|
21474
|
+
if (isIntraCell) {
|
|
21475
|
+
for (const it of cur.items) {
|
|
21476
|
+
prev.items.push(it);
|
|
21477
|
+
}
|
|
21478
|
+
prev.yMin = Math.min(prev.yMin, cur.yMin);
|
|
21479
|
+
prev.items.sort((a, b) => a.x - b.x);
|
|
21480
|
+
continue;
|
|
21481
|
+
}
|
|
21482
|
+
out.push({ ...cur, items: [...cur.items] });
|
|
21483
|
+
}
|
|
21484
|
+
return out;
|
|
21485
|
+
}
|
|
21486
|
+
function scoreCandidate(rows, columns) {
|
|
21487
|
+
if (rows.length < 2 || columns.length < 2) return 0;
|
|
21488
|
+
const totalCells = rows.length * columns.length;
|
|
21489
|
+
let filledCells = 0;
|
|
21490
|
+
let shortCells = 0;
|
|
21491
|
+
const intraGaps = [];
|
|
21492
|
+
const interGaps = [];
|
|
21493
|
+
for (const row of rows) {
|
|
21494
|
+
const cells = assignRowToColumns(row, columns);
|
|
21495
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21496
|
+
const cellText = cells[c]?.str ?? "";
|
|
21497
|
+
if (cellText.length > 0) {
|
|
21498
|
+
filledCells++;
|
|
21499
|
+
if (isShortCell(cellText)) shortCells++;
|
|
21500
|
+
}
|
|
21501
|
+
}
|
|
21502
|
+
const sortedItems = [...row.items].sort((a, b) => a.x - b.x);
|
|
21503
|
+
for (let i = 1; i < sortedItems.length; i++) {
|
|
21504
|
+
const gap = sortedItems[i].x - sortedItems[i - 1].right;
|
|
21505
|
+
if (gap < 0) continue;
|
|
21506
|
+
const sameCol = sameColumn(sortedItems[i - 1], sortedItems[i], columns);
|
|
21507
|
+
(sameCol ? intraGaps : interGaps).push(gap);
|
|
21508
|
+
}
|
|
21509
|
+
}
|
|
21510
|
+
const columnAlignmentRatio = filledCells / totalCells;
|
|
21511
|
+
const shortCellRatio = filledCells > 0 ? shortCells / filledCells : 0;
|
|
21512
|
+
const gaps = rowGaps(rows);
|
|
21513
|
+
const rowGapConsistency = gaps.length > 0 && mean(gaps) > 0 ? clamp01(1 - stdev(gaps) / Math.max(mean(gaps), 1e-6)) : 0;
|
|
21514
|
+
const bimodality = intraGaps.length > 0 && interGaps.length > 0 ? clamp01(mean(interGaps) / Math.max(mean(intraGaps), 1e-6) / 5) : interGaps.length > 0 ? 0.5 : 0;
|
|
21515
|
+
const rowCountBonus = rows.length >= 3 ? 1 : 0.5;
|
|
21516
|
+
const score = 0.25 * columnAlignmentRatio + 0.25 * shortCellRatio + 0.2 * bimodality + 0.15 * rowGapConsistency + 0.15 * rowCountBonus;
|
|
21517
|
+
return clamp01(score);
|
|
21518
|
+
}
|
|
21519
|
+
function findTableRegions(items, options = {}) {
|
|
21520
|
+
const cleaned = preFilterAndMerge(items);
|
|
21521
|
+
const rows = clusterIntoRows(cleaned);
|
|
21522
|
+
if (rows.length < 2) return [];
|
|
21523
|
+
const candidates = [];
|
|
21524
|
+
let i = 0;
|
|
21525
|
+
while (i < rows.length - 1) {
|
|
21526
|
+
let best = null;
|
|
21527
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
21528
|
+
const run = rows.slice(i, j + 1);
|
|
21529
|
+
const columns = detectColumns(run);
|
|
21530
|
+
if (columns.length < 2) {
|
|
21531
|
+
if (best) break;
|
|
21532
|
+
continue;
|
|
21533
|
+
}
|
|
21534
|
+
const coalesced = coalesceMultiLineCells(run, columns);
|
|
21535
|
+
if (coalesced.length < 2) {
|
|
21536
|
+
continue;
|
|
21537
|
+
}
|
|
21538
|
+
if (!passesProseGuard(coalesced)) {
|
|
21539
|
+
if (best) break;
|
|
21540
|
+
else continue;
|
|
21541
|
+
}
|
|
21542
|
+
if (!passesLabelFormGuard(coalesced, columns)) {
|
|
21543
|
+
if (best) break;
|
|
21544
|
+
else continue;
|
|
21545
|
+
}
|
|
21546
|
+
const confidence = scoreCandidate(coalesced, columns);
|
|
21547
|
+
if (confidence < MIN_CONFIDENCE_EMIT) {
|
|
21548
|
+
if (best) break;
|
|
21549
|
+
continue;
|
|
21550
|
+
}
|
|
21551
|
+
const bbox = computeBbox(run);
|
|
21552
|
+
const medianH = median(run.flatMap((r) => r.items.map((it) => it.height))) || 0;
|
|
21553
|
+
const mayContinue = options.pageBottomY !== void 0 && medianH > 0 && bbox.y0 - options.pageBottomY < medianH * 2;
|
|
21554
|
+
best = {
|
|
21555
|
+
rows: coalesced,
|
|
21556
|
+
columns,
|
|
21557
|
+
confidence,
|
|
21558
|
+
bbox,
|
|
21559
|
+
rowRange: { start: i, end: j },
|
|
21560
|
+
mayContinue
|
|
21561
|
+
};
|
|
21562
|
+
}
|
|
21563
|
+
if (best) {
|
|
21564
|
+
candidates.push(best);
|
|
21565
|
+
i = best.rowRange.end + 1;
|
|
21566
|
+
} else {
|
|
21567
|
+
i++;
|
|
21568
|
+
}
|
|
21569
|
+
}
|
|
21570
|
+
return candidates;
|
|
21571
|
+
}
|
|
21572
|
+
var defaultHtmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21573
|
+
function renderTableHtml(candidate, escape = defaultHtmlEscape) {
|
|
21574
|
+
const { rows, columns } = candidate;
|
|
21575
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
21576
|
+
const cellGrid = rows.map(
|
|
21577
|
+
(row) => assignRowToColumns(row, columns).map((c) => c ? c.str : "")
|
|
21578
|
+
);
|
|
21579
|
+
const headerRow = rows[0];
|
|
21580
|
+
const isHeader = looksLikeHeader(headerRow);
|
|
21581
|
+
const parts = [];
|
|
21582
|
+
parts.push("<table>");
|
|
21583
|
+
let bodyStart = 0;
|
|
21584
|
+
if (isHeader) {
|
|
21585
|
+
parts.push("<thead>");
|
|
21586
|
+
parts.push("<tr>");
|
|
21587
|
+
for (const text of cellGrid[0]) {
|
|
21588
|
+
parts.push(`<th>${escape(text)}</th>`);
|
|
21589
|
+
}
|
|
21590
|
+
parts.push("</tr>");
|
|
21591
|
+
parts.push("</thead>");
|
|
21592
|
+
bodyStart = 1;
|
|
21593
|
+
}
|
|
21594
|
+
parts.push("<tbody>");
|
|
21595
|
+
for (let r = bodyStart; r < cellGrid.length; r++) {
|
|
21596
|
+
parts.push("<tr>");
|
|
21597
|
+
for (const text of cellGrid[r]) {
|
|
21598
|
+
parts.push(`<td>${escape(text)}</td>`);
|
|
21599
|
+
}
|
|
21600
|
+
parts.push("</tr>");
|
|
21601
|
+
}
|
|
21602
|
+
parts.push("</tbody>");
|
|
21603
|
+
parts.push("</table>");
|
|
21604
|
+
return parts.join("");
|
|
21605
|
+
}
|
|
21606
|
+
var ConfidenceTier = {
|
|
21607
|
+
HIGH: MIN_CONFIDENCE_HIGH,
|
|
21608
|
+
EMIT: MIN_CONFIDENCE_EMIT
|
|
21609
|
+
};
|
|
21610
|
+
function estimateCharWidth(item) {
|
|
21611
|
+
const chars = item.str.length;
|
|
21612
|
+
return chars > 0 ? item.width / chars : 0;
|
|
21613
|
+
}
|
|
21614
|
+
function median(xs) {
|
|
21615
|
+
if (xs.length === 0) return 0;
|
|
21616
|
+
const sorted = [...xs].sort((a, b) => a - b);
|
|
21617
|
+
const mid = Math.floor(sorted.length / 2);
|
|
21618
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
21619
|
+
}
|
|
21620
|
+
function mean(xs) {
|
|
21621
|
+
if (xs.length === 0) return 0;
|
|
21622
|
+
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
21623
|
+
}
|
|
21624
|
+
function stdev(xs) {
|
|
21625
|
+
if (xs.length < 2) return 0;
|
|
21626
|
+
const m = mean(xs);
|
|
21627
|
+
const variance = xs.reduce((acc, x) => acc + (x - m) ** 2, 0) / xs.length;
|
|
21628
|
+
return Math.sqrt(variance);
|
|
21629
|
+
}
|
|
21630
|
+
function clamp01(x) {
|
|
21631
|
+
if (x < 0) return 0;
|
|
21632
|
+
if (x > 1) return 1;
|
|
21633
|
+
return x;
|
|
21634
|
+
}
|
|
21635
|
+
function overlapFraction(a0, a1, b0, b1) {
|
|
21636
|
+
const overlap = Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
|
|
21637
|
+
const span = Math.max(a1 - a0, 1e-6);
|
|
21638
|
+
return overlap / span;
|
|
21639
|
+
}
|
|
21640
|
+
function rangeOverlap(a0, a1, b0, b1) {
|
|
21641
|
+
return Math.min(a1, b1) - Math.max(a0, b0) > 0;
|
|
21642
|
+
}
|
|
21643
|
+
function verticalOverlap(a, b) {
|
|
21644
|
+
return overlapFraction(a.y, a.y + a.height, b.y, b.y + b.height);
|
|
21645
|
+
}
|
|
21646
|
+
function chooseAlignment(rows, iv) {
|
|
21647
|
+
const lefts = [];
|
|
21648
|
+
const rights = [];
|
|
21649
|
+
const centers = [];
|
|
21650
|
+
for (const row of rows) {
|
|
21651
|
+
const candidates = row.items.filter(
|
|
21652
|
+
(it) => rangeOverlap(it.x, it.right, iv.x0, iv.x1)
|
|
21653
|
+
);
|
|
21654
|
+
if (candidates.length === 0) continue;
|
|
21655
|
+
const leftmost = candidates.reduce((a, b) => a.x < b.x ? a : b);
|
|
21656
|
+
const rightmost = candidates.reduce((a, b) => a.right > b.right ? a : b);
|
|
21657
|
+
lefts.push(leftmost.x);
|
|
21658
|
+
rights.push(rightmost.right);
|
|
21659
|
+
centers.push((leftmost.x + rightmost.right) / 2);
|
|
21660
|
+
}
|
|
21661
|
+
const sL = stdev(lefts);
|
|
21662
|
+
const sR = stdev(rights);
|
|
21663
|
+
const sC = stdev(centers);
|
|
21664
|
+
if (sL <= sR && sL <= sC) return "left";
|
|
21665
|
+
if (sR < sL && sR <= sC) return "right";
|
|
21666
|
+
return "center";
|
|
21667
|
+
}
|
|
21668
|
+
function rowGaps(rows) {
|
|
21669
|
+
const gaps = [];
|
|
21670
|
+
for (let i = 1; i < rows.length; i++) {
|
|
21671
|
+
const gap = rows[i - 1].yMin - rows[i].yMax;
|
|
21672
|
+
if (gap >= 0) gaps.push(gap);
|
|
21673
|
+
}
|
|
21674
|
+
return gaps;
|
|
21675
|
+
}
|
|
21676
|
+
function columnsCovered(row, columns) {
|
|
21677
|
+
const out = /* @__PURE__ */ new Set();
|
|
21678
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21679
|
+
const col = columns[c];
|
|
21680
|
+
if (row.items.some((it) => rangeOverlap(it.x, it.right, col.x0, col.x1))) {
|
|
21681
|
+
out.add(c);
|
|
21682
|
+
}
|
|
21683
|
+
}
|
|
21684
|
+
return out;
|
|
21685
|
+
}
|
|
21686
|
+
function assignRowToColumns(row, columns) {
|
|
21687
|
+
const cells = columns.map(() => null);
|
|
21688
|
+
const buckets = columns.map(() => []);
|
|
21689
|
+
for (const item of row.items) {
|
|
21690
|
+
let best = -1;
|
|
21691
|
+
let bestScore = 0;
|
|
21692
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21693
|
+
const col = columns[c];
|
|
21694
|
+
const ovStart = Math.max(item.x, col.x0);
|
|
21695
|
+
const ovEnd = Math.min(item.right, col.x1);
|
|
21696
|
+
const overlap = Math.max(0, ovEnd - ovStart);
|
|
21697
|
+
const colWidth = Math.max(col.x1 - col.x0, 1e-6);
|
|
21698
|
+
const itemWidth = Math.max(item.width, 1e-6);
|
|
21699
|
+
const score = overlap / Math.min(colWidth, itemWidth);
|
|
21700
|
+
if (score > bestScore) {
|
|
21701
|
+
bestScore = score;
|
|
21702
|
+
best = c;
|
|
21703
|
+
}
|
|
21704
|
+
}
|
|
21705
|
+
if (best >= 0 && bestScore > 0.2) buckets[best].push(item);
|
|
21706
|
+
}
|
|
21707
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21708
|
+
const bucket = buckets[c].sort((a, b) => a.x - b.x);
|
|
21709
|
+
if (bucket.length > 0) {
|
|
21710
|
+
cells[c] = { str: bucket.map((it) => it.str).join(" ").trim() };
|
|
21711
|
+
}
|
|
21712
|
+
}
|
|
21713
|
+
return cells;
|
|
21714
|
+
}
|
|
21715
|
+
function sameColumn(a, b, columns) {
|
|
21716
|
+
const ca = findColumnIndex(a, columns);
|
|
21717
|
+
const cb = findColumnIndex(b, columns);
|
|
21718
|
+
return ca !== -1 && ca === cb;
|
|
21719
|
+
}
|
|
21720
|
+
function findColumnIndex(item, columns) {
|
|
21721
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21722
|
+
if (rangeOverlap(item.x, item.right, columns[c].x0, columns[c].x1)) return c;
|
|
21723
|
+
}
|
|
21724
|
+
return -1;
|
|
21725
|
+
}
|
|
21726
|
+
function isShortCell(text) {
|
|
21727
|
+
const trimmed = text.trim();
|
|
21728
|
+
if (trimmed.length === 0) return true;
|
|
21729
|
+
if (NUMBER_LIKE_REGEX.test(trimmed) || DATE_LIKE_REGEX.test(trimmed)) return true;
|
|
21730
|
+
const hasCjk = CJK_REGEX.test(trimmed);
|
|
21731
|
+
const threshold = hasCjk ? SHORT_CELL_THRESHOLD_CJK : SHORT_CELL_THRESHOLD_LATIN;
|
|
21732
|
+
return trimmed.length <= threshold;
|
|
21733
|
+
}
|
|
21734
|
+
function passesProseGuard(rows) {
|
|
21735
|
+
if (rows.length < 2) return true;
|
|
21736
|
+
let proseTransitions = 0;
|
|
21737
|
+
for (let i = 0; i < rows.length - 1; i++) {
|
|
21738
|
+
const cur = rows[i].items.map((it) => it.str).join(" ").trim();
|
|
21739
|
+
const next = rows[i + 1].items.map((it) => it.str).join(" ").trim();
|
|
21740
|
+
const curEndsWithPunct = /[.!?]["')\]]?$/.test(cur);
|
|
21741
|
+
const nextStartsLower = /^[\p{Ll}]/u.test(next);
|
|
21742
|
+
const bothLong = cur.length > 60 && next.length > 60;
|
|
21743
|
+
if (curEndsWithPunct && nextStartsLower || bothLong) proseTransitions++;
|
|
21744
|
+
}
|
|
21745
|
+
const ratio = proseTransitions / Math.max(rows.length - 1, 1);
|
|
21746
|
+
return ratio < PROSE_FLOW_MAX_FRACTION;
|
|
21747
|
+
}
|
|
21748
|
+
function passesLabelFormGuard(rows, columns) {
|
|
21749
|
+
if (columns.length < 2) return true;
|
|
21750
|
+
let labelish = 0;
|
|
21751
|
+
let total = 0;
|
|
21752
|
+
for (const row of rows) {
|
|
21753
|
+
const assigned = assignRowToColumns(row, columns);
|
|
21754
|
+
const cell = assigned[0];
|
|
21755
|
+
if (!cell) continue;
|
|
21756
|
+
total++;
|
|
21757
|
+
const trimmed = cell.str.trimEnd();
|
|
21758
|
+
if (/:$/.test(trimmed)) labelish++;
|
|
21759
|
+
}
|
|
21760
|
+
if (total < 2) return true;
|
|
21761
|
+
const ratio = labelish / total;
|
|
21762
|
+
if (columns.length === 2 && ratio >= 0.6) return false;
|
|
21763
|
+
if (ratio >= 0.8) return false;
|
|
21764
|
+
return true;
|
|
21765
|
+
}
|
|
21766
|
+
function computeBbox(rows) {
|
|
21767
|
+
let x0 = Infinity;
|
|
21768
|
+
let x1 = -Infinity;
|
|
21769
|
+
let y0 = Infinity;
|
|
21770
|
+
let y1 = -Infinity;
|
|
21771
|
+
for (const r of rows) {
|
|
21772
|
+
if (r.yMin < y0) y0 = r.yMin;
|
|
21773
|
+
if (r.yMax > y1) y1 = r.yMax;
|
|
21774
|
+
for (const it of r.items) {
|
|
21775
|
+
if (it.x < x0) x0 = it.x;
|
|
21776
|
+
if (it.right > x1) x1 = it.right;
|
|
21777
|
+
}
|
|
21778
|
+
}
|
|
21779
|
+
return { x0, y0, x1, y1 };
|
|
21780
|
+
}
|
|
21781
|
+
function looksLikeHeader(row) {
|
|
21782
|
+
if (row.items.length === 0) return false;
|
|
21783
|
+
let boldish = 0;
|
|
21784
|
+
let allCaps = 0;
|
|
21785
|
+
let trailingColon = 0;
|
|
21786
|
+
for (const it of row.items) {
|
|
21787
|
+
if (it.fontName && /Bold|Black|Heavy|Semibold|Demibold/i.test(it.fontName)) boldish++;
|
|
21788
|
+
const trimmed = it.str.trim();
|
|
21789
|
+
if (trimmed.length >= 2 && trimmed === trimmed.toUpperCase() && /[A-Z]/.test(trimmed)) {
|
|
21790
|
+
allCaps++;
|
|
21791
|
+
}
|
|
21792
|
+
if (trimmed.endsWith(":")) trailingColon++;
|
|
21793
|
+
}
|
|
21794
|
+
const n = row.items.length;
|
|
21795
|
+
return boldish / n >= 0.5 || allCaps / n >= 0.5 || trailingColon / n >= 0.5;
|
|
21796
|
+
}
|
|
21797
|
+
|
|
21346
21798
|
// ../../src/shared/adapters/pdf-adapter.ts
|
|
21347
21799
|
var PdfParseError = class extends Error {
|
|
21348
21800
|
constructor(message, code, originalError) {
|
|
@@ -21354,9 +21806,269 @@ var PdfParseError = class extends Error {
|
|
|
21354
21806
|
};
|
|
21355
21807
|
var MIN_TEXT_CHARS_THRESHOLD = 10;
|
|
21356
21808
|
var MULTI_COLUMN_X_VARIANCE_THRESHOLD = 100;
|
|
21809
|
+
var DESCENDER_BUDGET_FACTOR = 0.2;
|
|
21810
|
+
var FORM_LABEL_GAP_FACTOR = 3;
|
|
21357
21811
|
function escapeHtml(str) {
|
|
21358
21812
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21359
21813
|
}
|
|
21814
|
+
function detectRotation(transform) {
|
|
21815
|
+
const EPS = 1e-3;
|
|
21816
|
+
const [sx, shy, shx, sy] = transform;
|
|
21817
|
+
const skew = Math.max(Math.abs(shy), Math.abs(shx));
|
|
21818
|
+
if (skew < EPS) {
|
|
21819
|
+
return sx < 0 && sy < 0 ? 180 : 0;
|
|
21820
|
+
}
|
|
21821
|
+
if (shy > EPS && shx < -EPS) return 90;
|
|
21822
|
+
if (shy < -EPS && shx > EPS) return 270;
|
|
21823
|
+
return 0;
|
|
21824
|
+
}
|
|
21825
|
+
function textItemToPositioned(item) {
|
|
21826
|
+
if (!item.str) return null;
|
|
21827
|
+
if (!item.transform || item.transform.length < 6) return null;
|
|
21828
|
+
const rotation = detectRotation(item.transform);
|
|
21829
|
+
const tx = item.transform[4];
|
|
21830
|
+
const ty = item.transform[5];
|
|
21831
|
+
const rawHeight = item.height ?? Math.abs(item.transform[3]) ?? 10;
|
|
21832
|
+
const height = rawHeight > 0 ? rawHeight : 10;
|
|
21833
|
+
const width = item.width ?? item.str.length * (height * 0.5);
|
|
21834
|
+
const descent = height * DESCENDER_BUDGET_FACTOR;
|
|
21835
|
+
const y = ty - descent;
|
|
21836
|
+
const paddedHeight = height + descent;
|
|
21837
|
+
return {
|
|
21838
|
+
str: item.str,
|
|
21839
|
+
x: tx,
|
|
21840
|
+
y,
|
|
21841
|
+
width,
|
|
21842
|
+
height: paddedHeight,
|
|
21843
|
+
right: tx + width,
|
|
21844
|
+
rotation,
|
|
21845
|
+
fontName: item.fontName,
|
|
21846
|
+
source: "text"
|
|
21847
|
+
};
|
|
21848
|
+
}
|
|
21849
|
+
function widgetToPositionedItems(ann) {
|
|
21850
|
+
if (ann.subtype !== "Widget" || !ann.rect || ann.rect.length !== 4) return [];
|
|
21851
|
+
const [llx, lly, urx, ury] = ann.rect;
|
|
21852
|
+
const width = urx - llx;
|
|
21853
|
+
const height = ury - lly;
|
|
21854
|
+
if (width <= 0 || height <= 0) return [];
|
|
21855
|
+
const mk = (str) => ({
|
|
21856
|
+
str,
|
|
21857
|
+
x: llx,
|
|
21858
|
+
y: lly,
|
|
21859
|
+
width,
|
|
21860
|
+
height,
|
|
21861
|
+
right: urx,
|
|
21862
|
+
rotation: 0,
|
|
21863
|
+
source: "acroform"
|
|
21864
|
+
});
|
|
21865
|
+
if (ann.fieldType === "Tx") {
|
|
21866
|
+
const raw = ann.fieldValue;
|
|
21867
|
+
const value = Array.isArray(raw) ? raw.join(" ") : raw;
|
|
21868
|
+
const str = (value ?? "").toString().trim();
|
|
21869
|
+
if (str.length === 0) return [];
|
|
21870
|
+
return [mk(str)];
|
|
21871
|
+
}
|
|
21872
|
+
if (ann.fieldType === "Btn" && (ann.checkBox === true || ann.radioButton === true)) {
|
|
21873
|
+
const value = ann.fieldValue;
|
|
21874
|
+
const expected = ann.exportValue ?? ann.buttonValue ?? "Yes";
|
|
21875
|
+
const checked = value != null && value !== "Off" && (Array.isArray(value) ? value.includes(expected) : String(value) === expected);
|
|
21876
|
+
return [mk(checked ? "[X]" : "[ ]")];
|
|
21877
|
+
}
|
|
21878
|
+
return [];
|
|
21879
|
+
}
|
|
21880
|
+
function normalizeForDedup(s) {
|
|
21881
|
+
return s.normalize("NFKC").replace(/\u200B|\u200C|\u200D|\uFEFF/g, "").replace(/\s+/g, " ").trim();
|
|
21882
|
+
}
|
|
21883
|
+
function dedupAgainstWidgets(textItems, widgets) {
|
|
21884
|
+
if (widgets.length === 0) return textItems;
|
|
21885
|
+
const kept = [];
|
|
21886
|
+
for (const txt of textItems) {
|
|
21887
|
+
let drop = false;
|
|
21888
|
+
const txtArea = Math.max(txt.width * txt.height, 1e-6);
|
|
21889
|
+
const txtStr = normalizeForDedup(txt.str);
|
|
21890
|
+
for (const w of widgets) {
|
|
21891
|
+
const ox = Math.max(0, Math.min(txt.right, w.right) - Math.max(txt.x, w.x));
|
|
21892
|
+
const oy = Math.max(0, Math.min(txt.y + txt.height, w.y + w.height) - Math.max(txt.y, w.y));
|
|
21893
|
+
const overlap = ox * oy;
|
|
21894
|
+
if (overlap / txtArea <= 0.3) continue;
|
|
21895
|
+
const wStr = normalizeForDedup(w.str);
|
|
21896
|
+
if (wStr.length > 0 && wStr === txtStr) {
|
|
21897
|
+
drop = true;
|
|
21898
|
+
break;
|
|
21899
|
+
}
|
|
21900
|
+
}
|
|
21901
|
+
if (!drop) kept.push(txt);
|
|
21902
|
+
}
|
|
21903
|
+
return kept;
|
|
21904
|
+
}
|
|
21905
|
+
function itemsToLinearText(items) {
|
|
21906
|
+
if (items.length === 0) return "";
|
|
21907
|
+
const sorted = [...items].sort((a, b) => b.y + b.height - (a.y + a.height) || a.x - b.x);
|
|
21908
|
+
const rows = [];
|
|
21909
|
+
for (const it of sorted) {
|
|
21910
|
+
const last = rows[rows.length - 1];
|
|
21911
|
+
if (last && last.some(
|
|
21912
|
+
(li) => Math.max(0, Math.min(li.y + li.height, it.y + it.height) - Math.max(li.y, it.y)) / Math.max(Math.min(li.height, it.height), 1e-6) >= 0.5
|
|
21913
|
+
)) {
|
|
21914
|
+
last.push(it);
|
|
21915
|
+
} else {
|
|
21916
|
+
rows.push([it]);
|
|
21917
|
+
}
|
|
21918
|
+
}
|
|
21919
|
+
const widths = items.map((i) => i.str.length > 0 ? i.width / i.str.length : 0).filter((w) => w > 0);
|
|
21920
|
+
const medianCharWidth = widths.length ? [...widths].sort((a, b) => a - b)[Math.floor(widths.length / 2)] || 1 : 1;
|
|
21921
|
+
const labelGap = medianCharWidth * FORM_LABEL_GAP_FACTOR;
|
|
21922
|
+
const NBSP2 = "\xA0\xA0";
|
|
21923
|
+
const lines = [];
|
|
21924
|
+
for (const row of rows) {
|
|
21925
|
+
row.sort((a, b) => a.x - b.x);
|
|
21926
|
+
let out = "";
|
|
21927
|
+
for (let i = 0; i < row.length; i++) {
|
|
21928
|
+
const item = row[i];
|
|
21929
|
+
if (i > 0) {
|
|
21930
|
+
const prev = row[i - 1];
|
|
21931
|
+
const gap = item.x - prev.right;
|
|
21932
|
+
const prevStr = prev.str.trimEnd();
|
|
21933
|
+
const isLabel = /:$/.test(prevStr);
|
|
21934
|
+
if (isLabel && gap > labelGap) {
|
|
21935
|
+
out += NBSP2;
|
|
21936
|
+
} else {
|
|
21937
|
+
out += " ";
|
|
21938
|
+
}
|
|
21939
|
+
}
|
|
21940
|
+
out += item.str;
|
|
21941
|
+
}
|
|
21942
|
+
lines.push(out);
|
|
21943
|
+
}
|
|
21944
|
+
return lines.join("\n").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
21945
|
+
}
|
|
21946
|
+
function renderPage(items, pageNum, nextPageNum, enableTableDetection, translate, pageBottomY) {
|
|
21947
|
+
const warnings = [];
|
|
21948
|
+
const candidates = enableTableDetection ? findTableRegions(items, { pageBottomY }) : [];
|
|
21949
|
+
const htmlBlocks = [];
|
|
21950
|
+
const candsByY = [...candidates].sort((a, b) => b.bbox.y1 - a.bbox.y1);
|
|
21951
|
+
const itemOverlapsCandidate = (it, c) => {
|
|
21952
|
+
const top = it.y + it.height;
|
|
21953
|
+
const overlap = Math.min(c.bbox.y1, top) - Math.max(c.bbox.y0, it.y);
|
|
21954
|
+
return overlap > 0;
|
|
21955
|
+
};
|
|
21956
|
+
const uprightItems = items.filter((it) => it.rotation === 0);
|
|
21957
|
+
const aboveBuckets = candsByY.map(() => []);
|
|
21958
|
+
const trailing = [];
|
|
21959
|
+
for (const it of uprightItems) {
|
|
21960
|
+
const top = it.y + it.height;
|
|
21961
|
+
let placed = false;
|
|
21962
|
+
let claimed = false;
|
|
21963
|
+
for (let k = 0; k < candsByY.length; k++) {
|
|
21964
|
+
const c = candsByY[k];
|
|
21965
|
+
if (itemOverlapsCandidate(it, c)) {
|
|
21966
|
+
claimed = true;
|
|
21967
|
+
break;
|
|
21968
|
+
}
|
|
21969
|
+
if (top > c.bbox.y1) {
|
|
21970
|
+
aboveBuckets[k].push(it);
|
|
21971
|
+
placed = true;
|
|
21972
|
+
break;
|
|
21973
|
+
}
|
|
21974
|
+
}
|
|
21975
|
+
if (claimed) continue;
|
|
21976
|
+
if (!placed) trailing.push(it);
|
|
21977
|
+
}
|
|
21978
|
+
const sortTopDown = (arr) => [...arr].sort((a, b) => b.y + b.height - (a.y + a.height));
|
|
21979
|
+
const ordered = [];
|
|
21980
|
+
for (let k = 0; k < candsByY.length; k++) {
|
|
21981
|
+
const bucket = aboveBuckets[k];
|
|
21982
|
+
if (bucket.length > 0) ordered.push({ kind: "text", items: sortTopDown(bucket) });
|
|
21983
|
+
ordered.push({ kind: "table", cand: candsByY[k] });
|
|
21984
|
+
}
|
|
21985
|
+
if (trailing.length > 0) ordered.push({ kind: "text", items: sortTopDown(trailing) });
|
|
21986
|
+
const rotatedItems = items.filter((it) => it.rotation !== 0);
|
|
21987
|
+
if (rotatedItems.length > 0) {
|
|
21988
|
+
ordered.push({ kind: "text", items: rotatedItems });
|
|
21989
|
+
}
|
|
21990
|
+
const linearTextParts = [];
|
|
21991
|
+
for (const block of ordered) {
|
|
21992
|
+
if (block.kind === "table") {
|
|
21993
|
+
htmlBlocks.push(renderTableHtml(block.cand, escapeHtml));
|
|
21994
|
+
if (block.cand.confidence < ConfidenceTier.HIGH && block.cand.confidence >= ConfidenceTier.EMIT) {
|
|
21995
|
+
warnings.push(
|
|
21996
|
+
localize(translate, "pdf.warnings.tableMediumConfidence", {
|
|
21997
|
+
page: pageNum,
|
|
21998
|
+
confidence: Math.round(block.cand.confidence * 100)
|
|
21999
|
+
})
|
|
22000
|
+
);
|
|
22001
|
+
}
|
|
22002
|
+
if (block.cand.mayContinue && nextPageNum !== void 0) {
|
|
22003
|
+
warnings.push(
|
|
22004
|
+
localize(translate, "pdf.warnings.tableMayContinue", {
|
|
22005
|
+
page: pageNum,
|
|
22006
|
+
nextPage: nextPageNum
|
|
22007
|
+
})
|
|
22008
|
+
);
|
|
22009
|
+
}
|
|
22010
|
+
linearTextParts.push(tableToText(block.cand));
|
|
22011
|
+
} else {
|
|
22012
|
+
const text = itemsToLinearText(block.items);
|
|
22013
|
+
if (!text) continue;
|
|
22014
|
+
linearTextParts.push(text);
|
|
22015
|
+
const medH = (() => {
|
|
22016
|
+
const hs = block.items.map((i) => i.height).filter((h) => h > 0);
|
|
22017
|
+
if (hs.length === 0) return 10;
|
|
22018
|
+
return hs.sort((a, b) => a - b)[Math.floor(hs.length / 2)] || 10;
|
|
22019
|
+
})();
|
|
22020
|
+
const topY = block.items.reduce(
|
|
22021
|
+
(m, it) => Math.max(m, it.y + it.height),
|
|
22022
|
+
-Infinity
|
|
22023
|
+
);
|
|
22024
|
+
const topRowItems = block.items.filter(
|
|
22025
|
+
(it) => topY - (it.y + it.height) <= medH * 0.5
|
|
22026
|
+
);
|
|
22027
|
+
const topRowLooksTabular = topRowItems.length >= 4 && topRowItems.some((a) => topRowItems.some((b) => b.x - a.right > medH * 2));
|
|
22028
|
+
const paragraphs = text.split(/\n{2,}/);
|
|
22029
|
+
let paraIdx = 0;
|
|
22030
|
+
for (const para of paragraphs) {
|
|
22031
|
+
const trimmed = para.trim();
|
|
22032
|
+
paraIdx++;
|
|
22033
|
+
if (!trimmed) continue;
|
|
22034
|
+
const hasWideGap = /( {3,}|\u00A0\u00A0|\t)/.test(trimmed);
|
|
22035
|
+
const isFirstPara = paraIdx === 1;
|
|
22036
|
+
const looksTabular = hasWideGap || isFirstPara && topRowLooksTabular;
|
|
22037
|
+
const short = trimmed.length < 100 && !/[.,:;!?]$/.test(trimmed) && !/\n/.test(trimmed) && !looksTabular;
|
|
22038
|
+
if (short) {
|
|
22039
|
+
htmlBlocks.push(`<p><strong>${escapeHtml(trimmed)}</strong></p>`);
|
|
22040
|
+
} else {
|
|
22041
|
+
htmlBlocks.push(`<p>${escapeHtml(trimmed)}</p>`);
|
|
22042
|
+
}
|
|
22043
|
+
}
|
|
22044
|
+
}
|
|
22045
|
+
}
|
|
22046
|
+
return {
|
|
22047
|
+
htmlBlocks,
|
|
22048
|
+
textContent: linearTextParts.join("\n\n").trim(),
|
|
22049
|
+
tablesDetected: candidates.length,
|
|
22050
|
+
warnings,
|
|
22051
|
+
anyTableDetected: candidates.length > 0
|
|
22052
|
+
};
|
|
22053
|
+
}
|
|
22054
|
+
function tableToText(cand) {
|
|
22055
|
+
return cand.rows.map(
|
|
22056
|
+
(row) => [...row.items].sort((a, b) => a.x - b.x).map((it) => it.str).join(" | ")
|
|
22057
|
+
).join("\n");
|
|
22058
|
+
}
|
|
22059
|
+
function localize(translate, key, vars) {
|
|
22060
|
+
if (translate) return translate(key, vars);
|
|
22061
|
+
if (key === "pdf.warnings.tableMediumConfidence") {
|
|
22062
|
+
return `Table on page ${vars.page} reconstructed with medium confidence (${vars.confidence}%) \u2014 please verify.`;
|
|
22063
|
+
}
|
|
22064
|
+
if (key === "pdf.warnings.tableMayContinue") {
|
|
22065
|
+
return `Table on page ${vars.page} may continue onto page ${vars.nextPage} and was not joined across pages.`;
|
|
22066
|
+
}
|
|
22067
|
+
if (key === "pdf.warnings.multiColumnLayout") {
|
|
22068
|
+
return "Multi-column PDFs may not preserve layout accurately";
|
|
22069
|
+
}
|
|
22070
|
+
return key;
|
|
22071
|
+
}
|
|
21360
22072
|
var PdfAdapter = class {
|
|
21361
22073
|
pdfjs = null;
|
|
21362
22074
|
/**
|
|
@@ -21376,7 +22088,11 @@ var PdfAdapter = class {
|
|
|
21376
22088
|
* @returns Promise resolving to the parse result with HTML and metadata
|
|
21377
22089
|
* @throws PdfParseError if the file cannot be parsed
|
|
21378
22090
|
*/
|
|
21379
|
-
async parseToText(arrayBuffer,
|
|
22091
|
+
async parseToText(arrayBuffer, optionsOrProgress) {
|
|
22092
|
+
const opts = typeof optionsOrProgress === "function" ? { onProgress: optionsOrProgress } : optionsOrProgress ?? {};
|
|
22093
|
+
const onProgress = opts.onProgress;
|
|
22094
|
+
const translate = opts.translate;
|
|
22095
|
+
const tableDetectionEnabled = opts.enableTableDetection ?? getFeatureFlags().enablePdfTableDetection ?? true;
|
|
21380
22096
|
const pdfjs = await this.ensurePdfjsLoaded();
|
|
21381
22097
|
const warnings = [];
|
|
21382
22098
|
const pages = [];
|
|
@@ -21422,18 +22138,35 @@ var PdfAdapter = class {
|
|
|
21422
22138
|
onProgress?.(pageNum / numPages);
|
|
21423
22139
|
const page = await pdf.getPage(pageNum);
|
|
21424
22140
|
const viewport = page.getViewport({ scale: 1 });
|
|
21425
|
-
const textContent = await
|
|
21426
|
-
|
|
22141
|
+
const [textContent, annotations] = await Promise.all([
|
|
22142
|
+
page.getTextContent(),
|
|
22143
|
+
page.getAnnotations().catch((err) => {
|
|
22144
|
+
console.warn(`[PdfAdapter] getAnnotations failed for page ${pageNum}:`, err);
|
|
22145
|
+
return [];
|
|
22146
|
+
})
|
|
22147
|
+
]);
|
|
22148
|
+
const positionedText = [];
|
|
21427
22149
|
const xPositions = [];
|
|
21428
22150
|
for (const item of textContent.items) {
|
|
21429
|
-
if ("str" in item
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
|
|
21436
|
-
|
|
22151
|
+
if (!("str" in item) || typeof item.str !== "string") continue;
|
|
22152
|
+
const positioned = textItemToPositioned(item);
|
|
22153
|
+
if (!positioned) continue;
|
|
22154
|
+
positionedText.push(positioned);
|
|
22155
|
+
xPositions.push(positioned.x);
|
|
22156
|
+
}
|
|
22157
|
+
const widgetItems = annotations.flatMap(widgetToPositionedItems);
|
|
22158
|
+
const items = widgetItems.length > 0 ? [...dedupAgainstWidgets(positionedText, widgetItems), ...widgetItems] : positionedText;
|
|
22159
|
+
const pageBottomY = Array.isArray(page.view) && page.view.length >= 2 ? page.view[1] : 0;
|
|
22160
|
+
const rendered = renderPage(
|
|
22161
|
+
items,
|
|
22162
|
+
pageNum,
|
|
22163
|
+
pageNum < numPages ? pageNum + 1 : void 0,
|
|
22164
|
+
tableDetectionEnabled,
|
|
22165
|
+
translate,
|
|
22166
|
+
pageBottomY
|
|
22167
|
+
);
|
|
22168
|
+
for (const w of rendered.warnings) warnings.push(w);
|
|
22169
|
+
if (!hasMultiColumnWarning && !rendered.anyTableDetected && xPositions.length > 10) {
|
|
21437
22170
|
const uniqueXPositions = [...new Set(xPositions.map((x) => Math.round(x / 10) * 10))];
|
|
21438
22171
|
if (uniqueXPositions.length >= 3) {
|
|
21439
22172
|
const xRange = Math.max(...xPositions) - Math.min(...xPositions);
|
|
@@ -21447,32 +22180,13 @@ var PdfAdapter = class {
|
|
|
21447
22180
|
}
|
|
21448
22181
|
}
|
|
21449
22182
|
if (hasGap) {
|
|
21450
|
-
warnings.push("
|
|
22183
|
+
warnings.push(localize(translate, "pdf.warnings.multiColumnLayout", {}));
|
|
21451
22184
|
hasMultiColumnWarning = true;
|
|
21452
22185
|
}
|
|
21453
22186
|
}
|
|
21454
22187
|
}
|
|
21455
22188
|
}
|
|
21456
|
-
|
|
21457
|
-
let lastY = null;
|
|
21458
|
-
for (const item of textItems) {
|
|
21459
|
-
const text = item.str;
|
|
21460
|
-
if (!text) continue;
|
|
21461
|
-
if (item.transform && item.transform.length >= 6) {
|
|
21462
|
-
const currentY = item.transform[5];
|
|
21463
|
-
if (lastY !== null && Math.abs(currentY - lastY) > 5) {
|
|
21464
|
-
pageText += "\n";
|
|
21465
|
-
} else if (pageText && !pageText.endsWith(" ") && !pageText.endsWith("\n")) {
|
|
21466
|
-
pageText += " ";
|
|
21467
|
-
}
|
|
21468
|
-
lastY = currentY;
|
|
21469
|
-
} else if (pageText && !pageText.endsWith(" ") && !pageText.endsWith("\n")) {
|
|
21470
|
-
pageText += " ";
|
|
21471
|
-
}
|
|
21472
|
-
pageText += text;
|
|
21473
|
-
}
|
|
21474
|
-
pageText = pageText.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ").trim();
|
|
21475
|
-
totalTextLength += pageText.length;
|
|
22189
|
+
totalTextLength += rendered.textContent.length;
|
|
21476
22190
|
let hasImages = false;
|
|
21477
22191
|
try {
|
|
21478
22192
|
const operatorList = await page.getOperatorList();
|
|
@@ -21481,9 +22195,11 @@ var PdfAdapter = class {
|
|
|
21481
22195
|
}
|
|
21482
22196
|
pages.push({
|
|
21483
22197
|
pageNumber: pageNum,
|
|
21484
|
-
textContent:
|
|
22198
|
+
textContent: rendered.textContent,
|
|
21485
22199
|
hasImages,
|
|
21486
|
-
viewportWidth: viewport.width
|
|
22200
|
+
viewportWidth: viewport.width,
|
|
22201
|
+
htmlBlocks: rendered.htmlBlocks,
|
|
22202
|
+
tablesDetected: rendered.tablesDetected
|
|
21487
22203
|
});
|
|
21488
22204
|
}
|
|
21489
22205
|
const hasAnyImages = pages.some((p) => p.hasImages);
|
|
@@ -21554,6 +22270,9 @@ var PdfAdapter = class {
|
|
|
21554
22270
|
return pages.map((page, index) => {
|
|
21555
22271
|
const pageMarker = index > 0 ? `<div data-page-marker="${page.pageNumber}" class="pdf-page-break">--- Page ${page.pageNumber} ---</div>
|
|
21556
22272
|
` : "";
|
|
22273
|
+
if (page.htmlBlocks && page.htmlBlocks.length > 0) {
|
|
22274
|
+
return pageMarker + page.htmlBlocks.join("\n");
|
|
22275
|
+
}
|
|
21557
22276
|
const paragraphs = page.textContent.split("\n\n").filter((p) => p.trim()).map((p) => {
|
|
21558
22277
|
const trimmed = p.trim();
|
|
21559
22278
|
if (trimmed.length < 100 && !trimmed.match(/[.,:;!?]$/)) {
|
|
@@ -21575,7 +22294,9 @@ var PdfAdapter = class {
|
|
|
21575
22294
|
const loadingTask = pdfjs.getDocument({
|
|
21576
22295
|
data: arrayBuffer,
|
|
21577
22296
|
disableAutoFetch: true,
|
|
21578
|
-
disableStream: true
|
|
22297
|
+
disableStream: true,
|
|
22298
|
+
// Disable JavaScript execution in PDF (Req 3.13) — match parseToText.
|
|
22299
|
+
isEvalSupported: false
|
|
21579
22300
|
});
|
|
21580
22301
|
try {
|
|
21581
22302
|
const pdf = await loadingTask.promise;
|
|
@@ -21969,6 +22690,9 @@ function readConfig() {
|
|
|
21969
22690
|
return null;
|
|
21970
22691
|
}
|
|
21971
22692
|
}
|
|
22693
|
+
function getApiKey() {
|
|
22694
|
+
return process.env.FORMATFERRY_API_KEY || readConfig()?.apiKey;
|
|
22695
|
+
}
|
|
21972
22696
|
function writeConfig(config) {
|
|
21973
22697
|
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
21974
22698
|
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
@@ -22145,7 +22869,7 @@ function extractArticle(html, domProvider) {
|
|
|
22145
22869
|
}
|
|
22146
22870
|
|
|
22147
22871
|
// ../../src/shared/token-estimator.ts
|
|
22148
|
-
var
|
|
22872
|
+
var CJK_REGEX2 = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
|
|
22149
22873
|
var MAX_INPUT_LENGTH = 1e6;
|
|
22150
22874
|
function estimateTokens(text) {
|
|
22151
22875
|
if (!text || text.length === 0) {
|
|
@@ -22154,7 +22878,7 @@ function estimateTokens(text) {
|
|
|
22154
22878
|
if (text.length > MAX_INPUT_LENGTH) {
|
|
22155
22879
|
return Math.round(text.length / 4);
|
|
22156
22880
|
}
|
|
22157
|
-
const cjkMatches = text.match(
|
|
22881
|
+
const cjkMatches = text.match(CJK_REGEX2);
|
|
22158
22882
|
const cjkCount = cjkMatches ? cjkMatches.length : 0;
|
|
22159
22883
|
const otherCount = text.length - cjkCount;
|
|
22160
22884
|
const cjkTokens = cjkCount / 1.5;
|
|
@@ -22731,11 +23455,10 @@ async function convertCommand(options) {
|
|
|
22731
23455
|
process.stderr.write("Error: --url and --input are mutually exclusive. Use one or the other.\n");
|
|
22732
23456
|
process.exit(1);
|
|
22733
23457
|
}
|
|
22734
|
-
const
|
|
22735
|
-
const apiKey = config?.apiKey;
|
|
23458
|
+
const apiKey = getApiKey();
|
|
22736
23459
|
if (!apiKey) {
|
|
22737
23460
|
process.stderr.write(
|
|
22738
|
-
"Error: No SaaS API key configured. URL extraction requires a subscription.\nSet your API key with: formatferry auth --
|
|
23461
|
+
"Error: No SaaS API key configured. URL extraction requires a subscription.\nSet your API key with: formatferry auth --api-key <KEY>\nOr via the FORMATFERRY_API_KEY environment variable.\n"
|
|
22739
23462
|
);
|
|
22740
23463
|
process.exit(1);
|
|
22741
23464
|
}
|
|
@@ -23074,15 +23797,20 @@ async function handleLicenseKey(key) {
|
|
|
23074
23797
|
}
|
|
23075
23798
|
}
|
|
23076
23799
|
function handleStatus() {
|
|
23800
|
+
const envKey = process.env.FORMATFERRY_API_KEY;
|
|
23077
23801
|
const config = readConfig();
|
|
23078
|
-
if (
|
|
23802
|
+
if (envKey) {
|
|
23803
|
+
process.stdout.write(`Using API Key from FORMATFERRY_API_KEY environment variable.
|
|
23804
|
+
`);
|
|
23805
|
+
}
|
|
23806
|
+
if ((!config || !config.licenseKey && !config.apiKey) && !envKey) {
|
|
23079
23807
|
process.stdout.write(`Tier: free
|
|
23080
23808
|
`);
|
|
23081
23809
|
process.stdout.write(`No license key or API key configured.
|
|
23082
23810
|
`);
|
|
23083
23811
|
return;
|
|
23084
23812
|
}
|
|
23085
|
-
if (config
|
|
23813
|
+
if (config?.apiKey) {
|
|
23086
23814
|
const plan = config.plan ?? "free";
|
|
23087
23815
|
process.stdout.write(`Plan: ${formatPlanName(plan)}
|
|
23088
23816
|
`);
|
|
@@ -23104,9 +23832,14 @@ function handleStatus() {
|
|
|
23104
23832
|
}
|
|
23105
23833
|
return;
|
|
23106
23834
|
}
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
|
|
23835
|
+
if (!config && envKey) {
|
|
23836
|
+
process.stdout.write(`Plan: API key configured via environment variable
|
|
23837
|
+
`);
|
|
23838
|
+
return;
|
|
23839
|
+
}
|
|
23840
|
+
const tier = config?.tier ?? "free";
|
|
23841
|
+
const features = config?.features ?? [];
|
|
23842
|
+
const expiresAt = config?.expiresAt ?? null;
|
|
23110
23843
|
process.stdout.write(`Tier: ${tier}
|
|
23111
23844
|
`);
|
|
23112
23845
|
if (expiresAt) {
|
|
@@ -23129,8 +23862,8 @@ import { readFileSync as readFileSync4 } from "fs";
|
|
|
23129
23862
|
import { resolve, dirname as dirname3 } from "path";
|
|
23130
23863
|
import { fileURLToPath } from "url";
|
|
23131
23864
|
var getVersion = () => {
|
|
23132
|
-
if ("1.0.
|
|
23133
|
-
return "1.0.
|
|
23865
|
+
if ("1.0.11") {
|
|
23866
|
+
return "1.0.11";
|
|
23134
23867
|
}
|
|
23135
23868
|
try {
|
|
23136
23869
|
const __dirname = dirname3(fileURLToPath(import.meta.url));
|