formatferry 1.0.10 → 1.0.12
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 +853 -39
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -21342,6 +21342,551 @@ var DocxAdapter = class {
|
|
|
21342
21342
|
};
|
|
21343
21343
|
var docxAdapter = new DocxAdapter();
|
|
21344
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.35;
|
|
21365
|
+
var COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR = 0.5;
|
|
21366
|
+
var MIN_CELL_OVERLAP_SCORE = 0.35;
|
|
21367
|
+
var WIDE_WHITESPACE_MIN_RUN = 2;
|
|
21368
|
+
var SHORT_CELL_THRESHOLD_LATIN = 40;
|
|
21369
|
+
var SHORT_CELL_THRESHOLD_CJK = 20;
|
|
21370
|
+
var MIN_CONFIDENCE_EMIT = 0.55;
|
|
21371
|
+
var MIN_CONFIDENCE_HIGH = 0.7;
|
|
21372
|
+
var PROSE_FLOW_MAX_FRACTION = 0.3;
|
|
21373
|
+
function splitWideWhitespaceItems(items) {
|
|
21374
|
+
const out = [];
|
|
21375
|
+
for (const it of items) {
|
|
21376
|
+
const tokens = splitAtWideRuns(it.str, WIDE_WHITESPACE_MIN_RUN);
|
|
21377
|
+
if (tokens.length < 2 || it.str.length < 4 || it.width <= 0) {
|
|
21378
|
+
out.push(it);
|
|
21379
|
+
continue;
|
|
21380
|
+
}
|
|
21381
|
+
const perChar = it.width / it.str.length;
|
|
21382
|
+
for (const t of tokens) {
|
|
21383
|
+
const subX = it.x + t.start * perChar;
|
|
21384
|
+
const subRight = it.x + t.end * perChar;
|
|
21385
|
+
const subWidth = Math.max(subRight - subX, perChar);
|
|
21386
|
+
out.push({
|
|
21387
|
+
...it,
|
|
21388
|
+
str: t.str,
|
|
21389
|
+
x: subX,
|
|
21390
|
+
width: subWidth,
|
|
21391
|
+
right: subX + subWidth
|
|
21392
|
+
});
|
|
21393
|
+
}
|
|
21394
|
+
}
|
|
21395
|
+
return out;
|
|
21396
|
+
}
|
|
21397
|
+
function preFilterAndMerge(items) {
|
|
21398
|
+
const filtered = items.filter(
|
|
21399
|
+
(it) => it.str.trim().length > 0 && it.width > 0 && it.height > 0
|
|
21400
|
+
);
|
|
21401
|
+
if (filtered.length === 0) return [];
|
|
21402
|
+
const alive = splitWideWhitespaceItems(filtered);
|
|
21403
|
+
if (alive.length === 0) return [];
|
|
21404
|
+
const widths = alive.map((it) => estimateCharWidth(it)).filter((w) => w > 0);
|
|
21405
|
+
const medianCharWidth = median(widths) || 1;
|
|
21406
|
+
const mergeGap = medianCharWidth * SUPERSCRIPT_MERGE_GAP_FACTOR;
|
|
21407
|
+
const sorted = [...alive].sort((a, b) => b.y + b.height - (a.y + a.height) || a.x - b.x);
|
|
21408
|
+
const bands = [];
|
|
21409
|
+
for (const it of sorted) {
|
|
21410
|
+
const band = bands[bands.length - 1];
|
|
21411
|
+
if (band && band.some((bi) => verticalOverlap(bi, it) > 0 && bi.rotation === it.rotation)) {
|
|
21412
|
+
band.push(it);
|
|
21413
|
+
} else {
|
|
21414
|
+
bands.push([it]);
|
|
21415
|
+
}
|
|
21416
|
+
}
|
|
21417
|
+
const out = [];
|
|
21418
|
+
for (const band of bands) {
|
|
21419
|
+
band.sort((a, b) => a.x - b.x);
|
|
21420
|
+
for (const item of band) {
|
|
21421
|
+
const last = out[out.length - 1];
|
|
21422
|
+
if (last && last.rotation === item.rotation && verticalOverlap(last, item) > 0 && item.x >= last.right && item.x - last.right < mergeGap) {
|
|
21423
|
+
last.str = `${last.str}${item.str}`;
|
|
21424
|
+
last.width = item.right - last.x;
|
|
21425
|
+
last.right = item.right;
|
|
21426
|
+
last.height = Math.max(last.height, item.height);
|
|
21427
|
+
last.y = Math.min(last.y, item.y);
|
|
21428
|
+
continue;
|
|
21429
|
+
}
|
|
21430
|
+
out.push({ ...item });
|
|
21431
|
+
}
|
|
21432
|
+
}
|
|
21433
|
+
return out;
|
|
21434
|
+
}
|
|
21435
|
+
function clusterIntoRows(items) {
|
|
21436
|
+
if (items.length === 0) return [];
|
|
21437
|
+
const upright = items.filter((it) => it.rotation === 0);
|
|
21438
|
+
upright.sort((a, b) => b.y - a.y || a.x - b.x);
|
|
21439
|
+
const rows = [];
|
|
21440
|
+
for (const item of upright) {
|
|
21441
|
+
const row = rows[rows.length - 1];
|
|
21442
|
+
if (row && overlapFraction(item.y, item.y + item.height, row.yMin, row.yMax) >= VERTICAL_OVERLAP_THRESHOLD) {
|
|
21443
|
+
row.items.push(item);
|
|
21444
|
+
row.yMin = Math.min(row.yMin, item.y);
|
|
21445
|
+
row.yMax = Math.max(row.yMax, item.y + item.height);
|
|
21446
|
+
} else {
|
|
21447
|
+
rows.push({ yMin: item.y, yMax: item.y + item.height, items: [item] });
|
|
21448
|
+
}
|
|
21449
|
+
}
|
|
21450
|
+
for (const r of rows) r.items.sort((a, b) => a.x - b.x);
|
|
21451
|
+
rows.sort((a, b) => b.yMax - a.yMax);
|
|
21452
|
+
return rows;
|
|
21453
|
+
}
|
|
21454
|
+
function detectColumns(rows) {
|
|
21455
|
+
if (rows.length === 0) return [];
|
|
21456
|
+
const allItems = rows.flatMap((r) => r.items);
|
|
21457
|
+
if (allItems.length === 0) return [];
|
|
21458
|
+
const medianCharWidth = median(allItems.map((it) => estimateCharWidth(it)).filter((w) => w > 0)) || 1;
|
|
21459
|
+
const gapTol = medianCharWidth * COLUMN_GAP_MERGE_FACTOR;
|
|
21460
|
+
const hardBoundaries = voteColumnBoundaries(rows, medianCharWidth);
|
|
21461
|
+
const intervals = allItems.map((it) => ({ x0: it.x, x1: it.right })).sort((a, b) => a.x0 - b.x0);
|
|
21462
|
+
const merged = [];
|
|
21463
|
+
for (const iv of intervals) {
|
|
21464
|
+
const last = merged[merged.length - 1];
|
|
21465
|
+
const crossesHardBoundary = last !== void 0 && hardBoundaries.some((b) => b >= last.x1 && b <= iv.x0);
|
|
21466
|
+
if (last && iv.x0 - last.x1 <= gapTol && !crossesHardBoundary) {
|
|
21467
|
+
last.x1 = Math.max(last.x1, iv.x1);
|
|
21468
|
+
} else {
|
|
21469
|
+
merged.push({ ...iv });
|
|
21470
|
+
}
|
|
21471
|
+
}
|
|
21472
|
+
const anchors = [];
|
|
21473
|
+
for (const iv of merged) {
|
|
21474
|
+
const rowsWithHit = rows.filter(
|
|
21475
|
+
(r) => r.items.some((it) => rangeOverlap(it.x, it.right, iv.x0, iv.x1))
|
|
21476
|
+
);
|
|
21477
|
+
const support = rowsWithHit.length / rows.length;
|
|
21478
|
+
if (support < COLUMN_SUPPORT_THRESHOLD) continue;
|
|
21479
|
+
const alignment = chooseAlignment(rows, iv);
|
|
21480
|
+
anchors.push({ x0: iv.x0, x1: iv.x1, alignment, support });
|
|
21481
|
+
}
|
|
21482
|
+
return anchors;
|
|
21483
|
+
}
|
|
21484
|
+
function coalesceMultiLineCells(rows, columns) {
|
|
21485
|
+
if (rows.length < 2 || columns.length < 2) return rows;
|
|
21486
|
+
const gaps = rowGaps(rows);
|
|
21487
|
+
const baseline = gaps.length ? median(gaps) : 0;
|
|
21488
|
+
const tightFactor = 0.7;
|
|
21489
|
+
const out = [];
|
|
21490
|
+
for (let i = 0; i < rows.length; i++) {
|
|
21491
|
+
const cur = rows[i];
|
|
21492
|
+
const prev = out[out.length - 1];
|
|
21493
|
+
if (!prev) {
|
|
21494
|
+
out.push({ ...cur, items: [...cur.items] });
|
|
21495
|
+
continue;
|
|
21496
|
+
}
|
|
21497
|
+
const gap = prev.yMin - cur.yMax;
|
|
21498
|
+
const prevCols = columnsCovered(prev, columns);
|
|
21499
|
+
const curCols = columnsCovered(cur, columns);
|
|
21500
|
+
const curIsSubset = [...curCols].every((c) => prevCols.has(c));
|
|
21501
|
+
const covers = curCols.size >= 1 && curCols.size < columns.length;
|
|
21502
|
+
const isTight = baseline > 0 && gap < baseline * tightFactor;
|
|
21503
|
+
const lastColOnly = curCols.size === 1;
|
|
21504
|
+
const isIntraCell = isTight && curIsSubset && covers || lastColOnly && gap < baseline * 1.5 && curIsSubset;
|
|
21505
|
+
if (isIntraCell) {
|
|
21506
|
+
for (const it of cur.items) {
|
|
21507
|
+
prev.items.push(it);
|
|
21508
|
+
}
|
|
21509
|
+
prev.yMin = Math.min(prev.yMin, cur.yMin);
|
|
21510
|
+
prev.items.sort((a, b) => a.x - b.x);
|
|
21511
|
+
continue;
|
|
21512
|
+
}
|
|
21513
|
+
out.push({ ...cur, items: [...cur.items] });
|
|
21514
|
+
}
|
|
21515
|
+
return out;
|
|
21516
|
+
}
|
|
21517
|
+
function scoreCandidate(rows, columns) {
|
|
21518
|
+
if (rows.length < 2 || columns.length < 2) return 0;
|
|
21519
|
+
const totalCells = rows.length * columns.length;
|
|
21520
|
+
let filledCells = 0;
|
|
21521
|
+
let shortCells = 0;
|
|
21522
|
+
const intraGaps = [];
|
|
21523
|
+
const interGaps = [];
|
|
21524
|
+
for (const row of rows) {
|
|
21525
|
+
const cells = assignRowToColumns(row, columns);
|
|
21526
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21527
|
+
const cellText = cells[c]?.str ?? "";
|
|
21528
|
+
if (cellText.length > 0) {
|
|
21529
|
+
filledCells++;
|
|
21530
|
+
if (isShortCell(cellText)) shortCells++;
|
|
21531
|
+
}
|
|
21532
|
+
}
|
|
21533
|
+
const sortedItems = [...row.items].sort((a, b) => a.x - b.x);
|
|
21534
|
+
for (let i = 1; i < sortedItems.length; i++) {
|
|
21535
|
+
const gap = sortedItems[i].x - sortedItems[i - 1].right;
|
|
21536
|
+
if (gap < 0) continue;
|
|
21537
|
+
const sameCol = sameColumn(sortedItems[i - 1], sortedItems[i], columns);
|
|
21538
|
+
(sameCol ? intraGaps : interGaps).push(gap);
|
|
21539
|
+
}
|
|
21540
|
+
}
|
|
21541
|
+
const columnAlignmentRatio = filledCells / totalCells;
|
|
21542
|
+
const shortCellRatio = filledCells > 0 ? shortCells / filledCells : 0;
|
|
21543
|
+
const gaps = rowGaps(rows);
|
|
21544
|
+
const rowGapConsistency = gaps.length > 0 && mean(gaps) > 0 ? clamp01(1 - stdev(gaps) / Math.max(mean(gaps), 1e-6)) : 0;
|
|
21545
|
+
const bimodality = intraGaps.length > 0 && interGaps.length > 0 ? clamp01(mean(interGaps) / Math.max(mean(intraGaps), 1e-6) / 5) : interGaps.length > 0 ? 0.5 : 0;
|
|
21546
|
+
const rowCountBonus = rows.length >= 3 ? 1 : 0.5;
|
|
21547
|
+
const score = 0.25 * columnAlignmentRatio + 0.25 * shortCellRatio + 0.2 * bimodality + 0.15 * rowGapConsistency + 0.15 * rowCountBonus;
|
|
21548
|
+
return clamp01(score);
|
|
21549
|
+
}
|
|
21550
|
+
function findTableRegions(items, options = {}) {
|
|
21551
|
+
const cleaned = preFilterAndMerge(items);
|
|
21552
|
+
const rows = clusterIntoRows(cleaned);
|
|
21553
|
+
if (rows.length < 2) return [];
|
|
21554
|
+
const candidates = [];
|
|
21555
|
+
let i = 0;
|
|
21556
|
+
while (i < rows.length - 1) {
|
|
21557
|
+
let best = null;
|
|
21558
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
21559
|
+
const run = rows.slice(i, j + 1);
|
|
21560
|
+
const columns = detectColumns(run);
|
|
21561
|
+
if (columns.length < 2) {
|
|
21562
|
+
if (best) break;
|
|
21563
|
+
continue;
|
|
21564
|
+
}
|
|
21565
|
+
const coalesced = coalesceMultiLineCells(run, columns);
|
|
21566
|
+
if (coalesced.length < 2) {
|
|
21567
|
+
continue;
|
|
21568
|
+
}
|
|
21569
|
+
if (!passesProseGuard(coalesced)) {
|
|
21570
|
+
if (best) break;
|
|
21571
|
+
else continue;
|
|
21572
|
+
}
|
|
21573
|
+
if (!passesLabelFormGuard(coalesced, columns)) {
|
|
21574
|
+
if (best) break;
|
|
21575
|
+
else continue;
|
|
21576
|
+
}
|
|
21577
|
+
const confidence = scoreCandidate(coalesced, columns);
|
|
21578
|
+
if (confidence < MIN_CONFIDENCE_EMIT) {
|
|
21579
|
+
if (best) break;
|
|
21580
|
+
continue;
|
|
21581
|
+
}
|
|
21582
|
+
const bbox = computeBbox(run);
|
|
21583
|
+
const medianH = median(run.flatMap((r) => r.items.map((it) => it.height))) || 0;
|
|
21584
|
+
const mayContinue = options.pageBottomY !== void 0 && medianH > 0 && bbox.y0 - options.pageBottomY < medianH * 2;
|
|
21585
|
+
best = {
|
|
21586
|
+
rows: coalesced,
|
|
21587
|
+
columns,
|
|
21588
|
+
confidence,
|
|
21589
|
+
bbox,
|
|
21590
|
+
rowRange: { start: i, end: j },
|
|
21591
|
+
mayContinue
|
|
21592
|
+
};
|
|
21593
|
+
}
|
|
21594
|
+
if (best) {
|
|
21595
|
+
candidates.push(best);
|
|
21596
|
+
i = best.rowRange.end + 1;
|
|
21597
|
+
} else {
|
|
21598
|
+
i++;
|
|
21599
|
+
}
|
|
21600
|
+
}
|
|
21601
|
+
return candidates;
|
|
21602
|
+
}
|
|
21603
|
+
var defaultHtmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21604
|
+
function renderTableHtml(candidate, escape = defaultHtmlEscape) {
|
|
21605
|
+
const { rows, columns } = candidate;
|
|
21606
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
21607
|
+
const cellGrid = rows.map(
|
|
21608
|
+
(row) => assignRowToColumns(row, columns).map((c) => c ? c.str : "")
|
|
21609
|
+
);
|
|
21610
|
+
const headerRow = rows[0];
|
|
21611
|
+
const isHeader = looksLikeHeader(headerRow);
|
|
21612
|
+
const parts = [];
|
|
21613
|
+
parts.push("<table>");
|
|
21614
|
+
let bodyStart = 0;
|
|
21615
|
+
if (isHeader) {
|
|
21616
|
+
parts.push("<thead>");
|
|
21617
|
+
parts.push("<tr>");
|
|
21618
|
+
for (const text of cellGrid[0]) {
|
|
21619
|
+
parts.push(`<th>${escape(text)}</th>`);
|
|
21620
|
+
}
|
|
21621
|
+
parts.push("</tr>");
|
|
21622
|
+
parts.push("</thead>");
|
|
21623
|
+
bodyStart = 1;
|
|
21624
|
+
}
|
|
21625
|
+
parts.push("<tbody>");
|
|
21626
|
+
for (let r = bodyStart; r < cellGrid.length; r++) {
|
|
21627
|
+
parts.push("<tr>");
|
|
21628
|
+
for (const text of cellGrid[r]) {
|
|
21629
|
+
parts.push(`<td>${escape(text)}</td>`);
|
|
21630
|
+
}
|
|
21631
|
+
parts.push("</tr>");
|
|
21632
|
+
}
|
|
21633
|
+
parts.push("</tbody>");
|
|
21634
|
+
parts.push("</table>");
|
|
21635
|
+
return parts.join("");
|
|
21636
|
+
}
|
|
21637
|
+
var ConfidenceTier = {
|
|
21638
|
+
HIGH: MIN_CONFIDENCE_HIGH,
|
|
21639
|
+
EMIT: MIN_CONFIDENCE_EMIT
|
|
21640
|
+
};
|
|
21641
|
+
function estimateCharWidth(item) {
|
|
21642
|
+
const chars = item.str.length;
|
|
21643
|
+
return chars > 0 ? item.width / chars : 0;
|
|
21644
|
+
}
|
|
21645
|
+
function splitAtWideRuns(str, minRun) {
|
|
21646
|
+
const tokens = [];
|
|
21647
|
+
const isWs = (ch) => ch === " " || ch === " " || ch === "\xA0";
|
|
21648
|
+
let i = 0;
|
|
21649
|
+
while (i < str.length) {
|
|
21650
|
+
while (i < str.length && isWs(str[i])) i++;
|
|
21651
|
+
if (i >= str.length) break;
|
|
21652
|
+
const start = i;
|
|
21653
|
+
let end = i;
|
|
21654
|
+
while (i < str.length) {
|
|
21655
|
+
if (isWs(str[i])) {
|
|
21656
|
+
let j = i;
|
|
21657
|
+
while (j < str.length && isWs(str[j])) j++;
|
|
21658
|
+
if (j - i >= minRun) break;
|
|
21659
|
+
i = j;
|
|
21660
|
+
} else {
|
|
21661
|
+
i++;
|
|
21662
|
+
end = i;
|
|
21663
|
+
}
|
|
21664
|
+
}
|
|
21665
|
+
if (end > start) tokens.push({ str: str.slice(start, end), start, end });
|
|
21666
|
+
}
|
|
21667
|
+
return tokens;
|
|
21668
|
+
}
|
|
21669
|
+
function voteColumnBoundaries(rows, medianCharWidth) {
|
|
21670
|
+
if (rows.length < 2 || medianCharWidth <= 0) return [];
|
|
21671
|
+
const candidates = [];
|
|
21672
|
+
rows.forEach((row, rIdx) => {
|
|
21673
|
+
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
21674
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
21675
|
+
const gap = sorted[i].x - sorted[i - 1].right;
|
|
21676
|
+
if (gap > 0) {
|
|
21677
|
+
candidates.push({
|
|
21678
|
+
mid: (sorted[i - 1].right + sorted[i].x) / 2,
|
|
21679
|
+
rowIdx: rIdx
|
|
21680
|
+
});
|
|
21681
|
+
}
|
|
21682
|
+
}
|
|
21683
|
+
});
|
|
21684
|
+
if (candidates.length === 0) return [];
|
|
21685
|
+
candidates.sort((a, b) => a.mid - b.mid);
|
|
21686
|
+
const tolerance = medianCharWidth * COLUMN_BOUNDARY_VOTE_TOLERANCE_FACTOR;
|
|
21687
|
+
const clusters = [];
|
|
21688
|
+
for (const c of candidates) {
|
|
21689
|
+
const last = clusters[clusters.length - 1];
|
|
21690
|
+
if (last && c.mid - last.mids[last.mids.length - 1] <= tolerance) {
|
|
21691
|
+
last.mids.push(c.mid);
|
|
21692
|
+
last.rowSet.add(c.rowIdx);
|
|
21693
|
+
} else {
|
|
21694
|
+
clusters.push({ mids: [c.mid], rowSet: /* @__PURE__ */ new Set([c.rowIdx]) });
|
|
21695
|
+
}
|
|
21696
|
+
}
|
|
21697
|
+
const hard = [];
|
|
21698
|
+
for (const cl of clusters) {
|
|
21699
|
+
if (cl.rowSet.size / rows.length >= COLUMN_SUPPORT_THRESHOLD) {
|
|
21700
|
+
hard.push(median(cl.mids));
|
|
21701
|
+
}
|
|
21702
|
+
}
|
|
21703
|
+
hard.sort((a, b) => a - b);
|
|
21704
|
+
return hard;
|
|
21705
|
+
}
|
|
21706
|
+
function median(xs) {
|
|
21707
|
+
if (xs.length === 0) return 0;
|
|
21708
|
+
const sorted = [...xs].sort((a, b) => a - b);
|
|
21709
|
+
const mid = Math.floor(sorted.length / 2);
|
|
21710
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
21711
|
+
}
|
|
21712
|
+
function mean(xs) {
|
|
21713
|
+
if (xs.length === 0) return 0;
|
|
21714
|
+
return xs.reduce((a, b) => a + b, 0) / xs.length;
|
|
21715
|
+
}
|
|
21716
|
+
function stdev(xs) {
|
|
21717
|
+
if (xs.length < 2) return 0;
|
|
21718
|
+
const m = mean(xs);
|
|
21719
|
+
const variance = xs.reduce((acc, x) => acc + (x - m) ** 2, 0) / xs.length;
|
|
21720
|
+
return Math.sqrt(variance);
|
|
21721
|
+
}
|
|
21722
|
+
function clamp01(x) {
|
|
21723
|
+
if (x < 0) return 0;
|
|
21724
|
+
if (x > 1) return 1;
|
|
21725
|
+
return x;
|
|
21726
|
+
}
|
|
21727
|
+
function overlapFraction(a0, a1, b0, b1) {
|
|
21728
|
+
const overlap = Math.max(0, Math.min(a1, b1) - Math.max(a0, b0));
|
|
21729
|
+
const span = Math.max(a1 - a0, 1e-6);
|
|
21730
|
+
return overlap / span;
|
|
21731
|
+
}
|
|
21732
|
+
function rangeOverlap(a0, a1, b0, b1) {
|
|
21733
|
+
return Math.min(a1, b1) - Math.max(a0, b0) > 0;
|
|
21734
|
+
}
|
|
21735
|
+
function verticalOverlap(a, b) {
|
|
21736
|
+
return overlapFraction(a.y, a.y + a.height, b.y, b.y + b.height);
|
|
21737
|
+
}
|
|
21738
|
+
function chooseAlignment(rows, iv) {
|
|
21739
|
+
const lefts = [];
|
|
21740
|
+
const rights = [];
|
|
21741
|
+
const centers = [];
|
|
21742
|
+
for (const row of rows) {
|
|
21743
|
+
const candidates = row.items.filter(
|
|
21744
|
+
(it) => rangeOverlap(it.x, it.right, iv.x0, iv.x1)
|
|
21745
|
+
);
|
|
21746
|
+
if (candidates.length === 0) continue;
|
|
21747
|
+
const leftmost = candidates.reduce((a, b) => a.x < b.x ? a : b);
|
|
21748
|
+
const rightmost = candidates.reduce((a, b) => a.right > b.right ? a : b);
|
|
21749
|
+
lefts.push(leftmost.x);
|
|
21750
|
+
rights.push(rightmost.right);
|
|
21751
|
+
centers.push((leftmost.x + rightmost.right) / 2);
|
|
21752
|
+
}
|
|
21753
|
+
const sL = stdev(lefts);
|
|
21754
|
+
const sR = stdev(rights);
|
|
21755
|
+
const sC = stdev(centers);
|
|
21756
|
+
if (sL <= sR && sL <= sC) return "left";
|
|
21757
|
+
if (sR < sL && sR <= sC) return "right";
|
|
21758
|
+
return "center";
|
|
21759
|
+
}
|
|
21760
|
+
function rowGaps(rows) {
|
|
21761
|
+
const gaps = [];
|
|
21762
|
+
for (let i = 1; i < rows.length; i++) {
|
|
21763
|
+
const gap = rows[i - 1].yMin - rows[i].yMax;
|
|
21764
|
+
if (gap >= 0) gaps.push(gap);
|
|
21765
|
+
}
|
|
21766
|
+
return gaps;
|
|
21767
|
+
}
|
|
21768
|
+
function columnsCovered(row, columns) {
|
|
21769
|
+
const out = /* @__PURE__ */ new Set();
|
|
21770
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21771
|
+
const col = columns[c];
|
|
21772
|
+
if (row.items.some((it) => rangeOverlap(it.x, it.right, col.x0, col.x1))) {
|
|
21773
|
+
out.add(c);
|
|
21774
|
+
}
|
|
21775
|
+
}
|
|
21776
|
+
return out;
|
|
21777
|
+
}
|
|
21778
|
+
function assignRowToColumns(row, columns) {
|
|
21779
|
+
const cells = columns.map(() => null);
|
|
21780
|
+
const buckets = columns.map(() => []);
|
|
21781
|
+
for (const item of row.items) {
|
|
21782
|
+
let best = -1;
|
|
21783
|
+
let bestScore = 0;
|
|
21784
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21785
|
+
const col = columns[c];
|
|
21786
|
+
const ovStart = Math.max(item.x, col.x0);
|
|
21787
|
+
const ovEnd = Math.min(item.right, col.x1);
|
|
21788
|
+
const overlap = Math.max(0, ovEnd - ovStart);
|
|
21789
|
+
const colWidth = Math.max(col.x1 - col.x0, 1e-6);
|
|
21790
|
+
const itemWidth = Math.max(item.width, 1e-6);
|
|
21791
|
+
const score = overlap / Math.min(colWidth, itemWidth);
|
|
21792
|
+
if (score > bestScore) {
|
|
21793
|
+
bestScore = score;
|
|
21794
|
+
best = c;
|
|
21795
|
+
}
|
|
21796
|
+
}
|
|
21797
|
+
if (best >= 0 && bestScore > MIN_CELL_OVERLAP_SCORE) buckets[best].push(item);
|
|
21798
|
+
}
|
|
21799
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21800
|
+
const bucket = buckets[c].sort((a, b) => a.x - b.x);
|
|
21801
|
+
if (bucket.length > 0) {
|
|
21802
|
+
cells[c] = { str: bucket.map((it) => it.str).join(" ").trim() };
|
|
21803
|
+
}
|
|
21804
|
+
}
|
|
21805
|
+
return cells;
|
|
21806
|
+
}
|
|
21807
|
+
function sameColumn(a, b, columns) {
|
|
21808
|
+
const ca = findColumnIndex(a, columns);
|
|
21809
|
+
const cb = findColumnIndex(b, columns);
|
|
21810
|
+
return ca !== -1 && ca === cb;
|
|
21811
|
+
}
|
|
21812
|
+
function findColumnIndex(item, columns) {
|
|
21813
|
+
for (let c = 0; c < columns.length; c++) {
|
|
21814
|
+
if (rangeOverlap(item.x, item.right, columns[c].x0, columns[c].x1)) return c;
|
|
21815
|
+
}
|
|
21816
|
+
return -1;
|
|
21817
|
+
}
|
|
21818
|
+
function isShortCell(text) {
|
|
21819
|
+
const trimmed = text.trim();
|
|
21820
|
+
if (trimmed.length === 0) return true;
|
|
21821
|
+
if (NUMBER_LIKE_REGEX.test(trimmed) || DATE_LIKE_REGEX.test(trimmed)) return true;
|
|
21822
|
+
const hasCjk = CJK_REGEX.test(trimmed);
|
|
21823
|
+
const threshold = hasCjk ? SHORT_CELL_THRESHOLD_CJK : SHORT_CELL_THRESHOLD_LATIN;
|
|
21824
|
+
return trimmed.length <= threshold;
|
|
21825
|
+
}
|
|
21826
|
+
function passesProseGuard(rows) {
|
|
21827
|
+
if (rows.length < 2) return true;
|
|
21828
|
+
let proseTransitions = 0;
|
|
21829
|
+
for (let i = 0; i < rows.length - 1; i++) {
|
|
21830
|
+
const cur = rows[i].items.map((it) => it.str).join(" ").trim();
|
|
21831
|
+
const next = rows[i + 1].items.map((it) => it.str).join(" ").trim();
|
|
21832
|
+
const curEndsWithPunct = /[.!?]["')\]]?$/.test(cur);
|
|
21833
|
+
const nextStartsLower = /^[\p{Ll}]/u.test(next);
|
|
21834
|
+
const bothLong = cur.length > 60 && next.length > 60;
|
|
21835
|
+
if (curEndsWithPunct && nextStartsLower || bothLong) proseTransitions++;
|
|
21836
|
+
}
|
|
21837
|
+
const ratio = proseTransitions / Math.max(rows.length - 1, 1);
|
|
21838
|
+
return ratio < PROSE_FLOW_MAX_FRACTION;
|
|
21839
|
+
}
|
|
21840
|
+
function passesLabelFormGuard(rows, columns) {
|
|
21841
|
+
if (columns.length < 2) return true;
|
|
21842
|
+
let labelish = 0;
|
|
21843
|
+
let total = 0;
|
|
21844
|
+
for (const row of rows) {
|
|
21845
|
+
const assigned = assignRowToColumns(row, columns);
|
|
21846
|
+
const cell = assigned[0];
|
|
21847
|
+
if (!cell) continue;
|
|
21848
|
+
total++;
|
|
21849
|
+
const trimmed = cell.str.trimEnd();
|
|
21850
|
+
if (/:$/.test(trimmed)) labelish++;
|
|
21851
|
+
}
|
|
21852
|
+
if (total < 2) return true;
|
|
21853
|
+
const ratio = labelish / total;
|
|
21854
|
+
if (columns.length === 2 && ratio >= 0.6) return false;
|
|
21855
|
+
if (ratio >= 0.8) return false;
|
|
21856
|
+
return true;
|
|
21857
|
+
}
|
|
21858
|
+
function computeBbox(rows) {
|
|
21859
|
+
let x0 = Infinity;
|
|
21860
|
+
let x1 = -Infinity;
|
|
21861
|
+
let y0 = Infinity;
|
|
21862
|
+
let y1 = -Infinity;
|
|
21863
|
+
for (const r of rows) {
|
|
21864
|
+
if (r.yMin < y0) y0 = r.yMin;
|
|
21865
|
+
if (r.yMax > y1) y1 = r.yMax;
|
|
21866
|
+
for (const it of r.items) {
|
|
21867
|
+
if (it.x < x0) x0 = it.x;
|
|
21868
|
+
if (it.right > x1) x1 = it.right;
|
|
21869
|
+
}
|
|
21870
|
+
}
|
|
21871
|
+
return { x0, y0, x1, y1 };
|
|
21872
|
+
}
|
|
21873
|
+
function looksLikeHeader(row) {
|
|
21874
|
+
if (row.items.length === 0) return false;
|
|
21875
|
+
let boldish = 0;
|
|
21876
|
+
let allCaps = 0;
|
|
21877
|
+
let trailingColon = 0;
|
|
21878
|
+
for (const it of row.items) {
|
|
21879
|
+
if (it.fontName && /Bold|Black|Heavy|Semibold|Demibold/i.test(it.fontName)) boldish++;
|
|
21880
|
+
const trimmed = it.str.trim();
|
|
21881
|
+
if (trimmed.length >= 2 && trimmed === trimmed.toUpperCase() && /[A-Z]/.test(trimmed)) {
|
|
21882
|
+
allCaps++;
|
|
21883
|
+
}
|
|
21884
|
+
if (trimmed.endsWith(":")) trailingColon++;
|
|
21885
|
+
}
|
|
21886
|
+
const n = row.items.length;
|
|
21887
|
+
return boldish / n >= 0.5 || allCaps / n >= 0.5 || trailingColon / n >= 0.5;
|
|
21888
|
+
}
|
|
21889
|
+
|
|
21345
21890
|
// ../../src/shared/adapters/pdf-adapter.ts
|
|
21346
21891
|
var PdfParseError = class extends Error {
|
|
21347
21892
|
constructor(message, code, originalError) {
|
|
@@ -21353,9 +21898,269 @@ var PdfParseError = class extends Error {
|
|
|
21353
21898
|
};
|
|
21354
21899
|
var MIN_TEXT_CHARS_THRESHOLD = 10;
|
|
21355
21900
|
var MULTI_COLUMN_X_VARIANCE_THRESHOLD = 100;
|
|
21901
|
+
var DESCENDER_BUDGET_FACTOR = 0.2;
|
|
21902
|
+
var FORM_LABEL_GAP_FACTOR = 3;
|
|
21356
21903
|
function escapeHtml(str) {
|
|
21357
21904
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
21358
21905
|
}
|
|
21906
|
+
function detectRotation(transform) {
|
|
21907
|
+
const EPS = 1e-3;
|
|
21908
|
+
const [sx, shy, shx, sy] = transform;
|
|
21909
|
+
const skew = Math.max(Math.abs(shy), Math.abs(shx));
|
|
21910
|
+
if (skew < EPS) {
|
|
21911
|
+
return sx < 0 && sy < 0 ? 180 : 0;
|
|
21912
|
+
}
|
|
21913
|
+
if (shy > EPS && shx < -EPS) return 90;
|
|
21914
|
+
if (shy < -EPS && shx > EPS) return 270;
|
|
21915
|
+
return 0;
|
|
21916
|
+
}
|
|
21917
|
+
function textItemToPositioned(item) {
|
|
21918
|
+
if (!item.str) return null;
|
|
21919
|
+
if (!item.transform || item.transform.length < 6) return null;
|
|
21920
|
+
const rotation = detectRotation(item.transform);
|
|
21921
|
+
const tx = item.transform[4];
|
|
21922
|
+
const ty = item.transform[5];
|
|
21923
|
+
const rawHeight = item.height ?? Math.abs(item.transform[3]) ?? 10;
|
|
21924
|
+
const height = rawHeight > 0 ? rawHeight : 10;
|
|
21925
|
+
const width = item.width ?? item.str.length * (height * 0.5);
|
|
21926
|
+
const descent = height * DESCENDER_BUDGET_FACTOR;
|
|
21927
|
+
const y = ty - descent;
|
|
21928
|
+
const paddedHeight = height + descent;
|
|
21929
|
+
return {
|
|
21930
|
+
str: item.str,
|
|
21931
|
+
x: tx,
|
|
21932
|
+
y,
|
|
21933
|
+
width,
|
|
21934
|
+
height: paddedHeight,
|
|
21935
|
+
right: tx + width,
|
|
21936
|
+
rotation,
|
|
21937
|
+
fontName: item.fontName,
|
|
21938
|
+
source: "text"
|
|
21939
|
+
};
|
|
21940
|
+
}
|
|
21941
|
+
function widgetToPositionedItems(ann) {
|
|
21942
|
+
if (ann.subtype !== "Widget" || !ann.rect || ann.rect.length !== 4) return [];
|
|
21943
|
+
const [llx, lly, urx, ury] = ann.rect;
|
|
21944
|
+
const width = urx - llx;
|
|
21945
|
+
const height = ury - lly;
|
|
21946
|
+
if (width <= 0 || height <= 0) return [];
|
|
21947
|
+
const mk = (str) => ({
|
|
21948
|
+
str,
|
|
21949
|
+
x: llx,
|
|
21950
|
+
y: lly,
|
|
21951
|
+
width,
|
|
21952
|
+
height,
|
|
21953
|
+
right: urx,
|
|
21954
|
+
rotation: 0,
|
|
21955
|
+
source: "acroform"
|
|
21956
|
+
});
|
|
21957
|
+
if (ann.fieldType === "Tx") {
|
|
21958
|
+
const raw = ann.fieldValue;
|
|
21959
|
+
const value = Array.isArray(raw) ? raw.join(" ") : raw;
|
|
21960
|
+
const str = (value ?? "").toString().trim();
|
|
21961
|
+
if (str.length === 0) return [];
|
|
21962
|
+
return [mk(str)];
|
|
21963
|
+
}
|
|
21964
|
+
if (ann.fieldType === "Btn" && (ann.checkBox === true || ann.radioButton === true)) {
|
|
21965
|
+
const value = ann.fieldValue;
|
|
21966
|
+
const expected = ann.exportValue ?? ann.buttonValue ?? "Yes";
|
|
21967
|
+
const checked = value != null && value !== "Off" && (Array.isArray(value) ? value.includes(expected) : String(value) === expected);
|
|
21968
|
+
return [mk(checked ? "[X]" : "[ ]")];
|
|
21969
|
+
}
|
|
21970
|
+
return [];
|
|
21971
|
+
}
|
|
21972
|
+
function normalizeForDedup(s) {
|
|
21973
|
+
return s.normalize("NFKC").replace(/\u200B|\u200C|\u200D|\uFEFF/g, "").replace(/\s+/g, " ").trim();
|
|
21974
|
+
}
|
|
21975
|
+
function dedupAgainstWidgets(textItems, widgets) {
|
|
21976
|
+
if (widgets.length === 0) return textItems;
|
|
21977
|
+
const kept = [];
|
|
21978
|
+
for (const txt of textItems) {
|
|
21979
|
+
let drop = false;
|
|
21980
|
+
const txtArea = Math.max(txt.width * txt.height, 1e-6);
|
|
21981
|
+
const txtStr = normalizeForDedup(txt.str);
|
|
21982
|
+
for (const w of widgets) {
|
|
21983
|
+
const ox = Math.max(0, Math.min(txt.right, w.right) - Math.max(txt.x, w.x));
|
|
21984
|
+
const oy = Math.max(0, Math.min(txt.y + txt.height, w.y + w.height) - Math.max(txt.y, w.y));
|
|
21985
|
+
const overlap = ox * oy;
|
|
21986
|
+
if (overlap / txtArea <= 0.3) continue;
|
|
21987
|
+
const wStr = normalizeForDedup(w.str);
|
|
21988
|
+
if (wStr.length > 0 && wStr === txtStr) {
|
|
21989
|
+
drop = true;
|
|
21990
|
+
break;
|
|
21991
|
+
}
|
|
21992
|
+
}
|
|
21993
|
+
if (!drop) kept.push(txt);
|
|
21994
|
+
}
|
|
21995
|
+
return kept;
|
|
21996
|
+
}
|
|
21997
|
+
function itemsToLinearText(items) {
|
|
21998
|
+
if (items.length === 0) return "";
|
|
21999
|
+
const sorted = [...items].sort((a, b) => b.y + b.height - (a.y + a.height) || a.x - b.x);
|
|
22000
|
+
const rows = [];
|
|
22001
|
+
for (const it of sorted) {
|
|
22002
|
+
const last = rows[rows.length - 1];
|
|
22003
|
+
if (last && last.some(
|
|
22004
|
+
(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
|
|
22005
|
+
)) {
|
|
22006
|
+
last.push(it);
|
|
22007
|
+
} else {
|
|
22008
|
+
rows.push([it]);
|
|
22009
|
+
}
|
|
22010
|
+
}
|
|
22011
|
+
const widths = items.map((i) => i.str.length > 0 ? i.width / i.str.length : 0).filter((w) => w > 0);
|
|
22012
|
+
const medianCharWidth = widths.length ? [...widths].sort((a, b) => a - b)[Math.floor(widths.length / 2)] || 1 : 1;
|
|
22013
|
+
const labelGap = medianCharWidth * FORM_LABEL_GAP_FACTOR;
|
|
22014
|
+
const NBSP2 = "\xA0\xA0";
|
|
22015
|
+
const lines = [];
|
|
22016
|
+
for (const row of rows) {
|
|
22017
|
+
row.sort((a, b) => a.x - b.x);
|
|
22018
|
+
let out = "";
|
|
22019
|
+
for (let i = 0; i < row.length; i++) {
|
|
22020
|
+
const item = row[i];
|
|
22021
|
+
if (i > 0) {
|
|
22022
|
+
const prev = row[i - 1];
|
|
22023
|
+
const gap = item.x - prev.right;
|
|
22024
|
+
const prevStr = prev.str.trimEnd();
|
|
22025
|
+
const isLabel = /:$/.test(prevStr);
|
|
22026
|
+
if (isLabel && gap > labelGap) {
|
|
22027
|
+
out += NBSP2;
|
|
22028
|
+
} else {
|
|
22029
|
+
out += " ";
|
|
22030
|
+
}
|
|
22031
|
+
}
|
|
22032
|
+
out += item.str;
|
|
22033
|
+
}
|
|
22034
|
+
lines.push(out);
|
|
22035
|
+
}
|
|
22036
|
+
return lines.join("\n").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
22037
|
+
}
|
|
22038
|
+
function renderPage(items, pageNum, nextPageNum, enableTableDetection, translate, pageBottomY) {
|
|
22039
|
+
const warnings = [];
|
|
22040
|
+
const candidates = enableTableDetection ? findTableRegions(items, { pageBottomY }) : [];
|
|
22041
|
+
const htmlBlocks = [];
|
|
22042
|
+
const candsByY = [...candidates].sort((a, b) => b.bbox.y1 - a.bbox.y1);
|
|
22043
|
+
const itemOverlapsCandidate = (it, c) => {
|
|
22044
|
+
const top = it.y + it.height;
|
|
22045
|
+
const overlap = Math.min(c.bbox.y1, top) - Math.max(c.bbox.y0, it.y);
|
|
22046
|
+
return overlap > 0;
|
|
22047
|
+
};
|
|
22048
|
+
const uprightItems = items.filter((it) => it.rotation === 0);
|
|
22049
|
+
const aboveBuckets = candsByY.map(() => []);
|
|
22050
|
+
const trailing = [];
|
|
22051
|
+
for (const it of uprightItems) {
|
|
22052
|
+
const top = it.y + it.height;
|
|
22053
|
+
let placed = false;
|
|
22054
|
+
let claimed = false;
|
|
22055
|
+
for (let k = 0; k < candsByY.length; k++) {
|
|
22056
|
+
const c = candsByY[k];
|
|
22057
|
+
if (itemOverlapsCandidate(it, c)) {
|
|
22058
|
+
claimed = true;
|
|
22059
|
+
break;
|
|
22060
|
+
}
|
|
22061
|
+
if (top > c.bbox.y1) {
|
|
22062
|
+
aboveBuckets[k].push(it);
|
|
22063
|
+
placed = true;
|
|
22064
|
+
break;
|
|
22065
|
+
}
|
|
22066
|
+
}
|
|
22067
|
+
if (claimed) continue;
|
|
22068
|
+
if (!placed) trailing.push(it);
|
|
22069
|
+
}
|
|
22070
|
+
const sortTopDown = (arr) => [...arr].sort((a, b) => b.y + b.height - (a.y + a.height));
|
|
22071
|
+
const ordered = [];
|
|
22072
|
+
for (let k = 0; k < candsByY.length; k++) {
|
|
22073
|
+
const bucket = aboveBuckets[k];
|
|
22074
|
+
if (bucket.length > 0) ordered.push({ kind: "text", items: sortTopDown(bucket) });
|
|
22075
|
+
ordered.push({ kind: "table", cand: candsByY[k] });
|
|
22076
|
+
}
|
|
22077
|
+
if (trailing.length > 0) ordered.push({ kind: "text", items: sortTopDown(trailing) });
|
|
22078
|
+
const rotatedItems = items.filter((it) => it.rotation !== 0);
|
|
22079
|
+
if (rotatedItems.length > 0) {
|
|
22080
|
+
ordered.push({ kind: "text", items: rotatedItems });
|
|
22081
|
+
}
|
|
22082
|
+
const linearTextParts = [];
|
|
22083
|
+
for (const block of ordered) {
|
|
22084
|
+
if (block.kind === "table") {
|
|
22085
|
+
htmlBlocks.push(renderTableHtml(block.cand, escapeHtml));
|
|
22086
|
+
if (block.cand.confidence < ConfidenceTier.HIGH && block.cand.confidence >= ConfidenceTier.EMIT) {
|
|
22087
|
+
warnings.push(
|
|
22088
|
+
localize(translate, "pdf.warnings.tableMediumConfidence", {
|
|
22089
|
+
page: pageNum,
|
|
22090
|
+
confidence: Math.round(block.cand.confidence * 100)
|
|
22091
|
+
})
|
|
22092
|
+
);
|
|
22093
|
+
}
|
|
22094
|
+
if (block.cand.mayContinue && nextPageNum !== void 0) {
|
|
22095
|
+
warnings.push(
|
|
22096
|
+
localize(translate, "pdf.warnings.tableMayContinue", {
|
|
22097
|
+
page: pageNum,
|
|
22098
|
+
nextPage: nextPageNum
|
|
22099
|
+
})
|
|
22100
|
+
);
|
|
22101
|
+
}
|
|
22102
|
+
linearTextParts.push(tableToText(block.cand));
|
|
22103
|
+
} else {
|
|
22104
|
+
const text = itemsToLinearText(block.items);
|
|
22105
|
+
if (!text) continue;
|
|
22106
|
+
linearTextParts.push(text);
|
|
22107
|
+
const medH = (() => {
|
|
22108
|
+
const hs = block.items.map((i) => i.height).filter((h) => h > 0);
|
|
22109
|
+
if (hs.length === 0) return 10;
|
|
22110
|
+
return hs.sort((a, b) => a - b)[Math.floor(hs.length / 2)] || 10;
|
|
22111
|
+
})();
|
|
22112
|
+
const topY = block.items.reduce(
|
|
22113
|
+
(m, it) => Math.max(m, it.y + it.height),
|
|
22114
|
+
-Infinity
|
|
22115
|
+
);
|
|
22116
|
+
const topRowItems = block.items.filter(
|
|
22117
|
+
(it) => topY - (it.y + it.height) <= medH * 0.5
|
|
22118
|
+
);
|
|
22119
|
+
const topRowLooksTabular = topRowItems.length >= 4 && topRowItems.some((a) => topRowItems.some((b) => b.x - a.right > medH * 2));
|
|
22120
|
+
const paragraphs = text.split(/\n{2,}/);
|
|
22121
|
+
let paraIdx = 0;
|
|
22122
|
+
for (const para of paragraphs) {
|
|
22123
|
+
const trimmed = para.trim();
|
|
22124
|
+
paraIdx++;
|
|
22125
|
+
if (!trimmed) continue;
|
|
22126
|
+
const hasWideGap = /( {3,}|\u00A0\u00A0|\t)/.test(trimmed);
|
|
22127
|
+
const isFirstPara = paraIdx === 1;
|
|
22128
|
+
const looksTabular = hasWideGap || isFirstPara && topRowLooksTabular;
|
|
22129
|
+
const short = trimmed.length < 100 && !/[.,:;!?]$/.test(trimmed) && !/\n/.test(trimmed) && !looksTabular;
|
|
22130
|
+
if (short) {
|
|
22131
|
+
htmlBlocks.push(`<p><strong>${escapeHtml(trimmed)}</strong></p>`);
|
|
22132
|
+
} else {
|
|
22133
|
+
htmlBlocks.push(`<p>${escapeHtml(trimmed)}</p>`);
|
|
22134
|
+
}
|
|
22135
|
+
}
|
|
22136
|
+
}
|
|
22137
|
+
}
|
|
22138
|
+
return {
|
|
22139
|
+
htmlBlocks,
|
|
22140
|
+
textContent: linearTextParts.join("\n\n").trim(),
|
|
22141
|
+
tablesDetected: candidates.length,
|
|
22142
|
+
warnings,
|
|
22143
|
+
anyTableDetected: candidates.length > 0
|
|
22144
|
+
};
|
|
22145
|
+
}
|
|
22146
|
+
function tableToText(cand) {
|
|
22147
|
+
return cand.rows.map(
|
|
22148
|
+
(row) => [...row.items].sort((a, b) => a.x - b.x).map((it) => it.str).join(" | ")
|
|
22149
|
+
).join("\n");
|
|
22150
|
+
}
|
|
22151
|
+
function localize(translate, key, vars) {
|
|
22152
|
+
if (translate) return translate(key, vars);
|
|
22153
|
+
if (key === "pdf.warnings.tableMediumConfidence") {
|
|
22154
|
+
return `Table on page ${vars.page} reconstructed with medium confidence (${vars.confidence}%) \u2014 please verify.`;
|
|
22155
|
+
}
|
|
22156
|
+
if (key === "pdf.warnings.tableMayContinue") {
|
|
22157
|
+
return `Table on page ${vars.page} may continue onto page ${vars.nextPage} and was not joined across pages.`;
|
|
22158
|
+
}
|
|
22159
|
+
if (key === "pdf.warnings.multiColumnLayout") {
|
|
22160
|
+
return "Multi-column PDFs may not preserve layout accurately";
|
|
22161
|
+
}
|
|
22162
|
+
return key;
|
|
22163
|
+
}
|
|
21359
22164
|
var PdfAdapter = class {
|
|
21360
22165
|
pdfjs = null;
|
|
21361
22166
|
/**
|
|
@@ -21375,7 +22180,11 @@ var PdfAdapter = class {
|
|
|
21375
22180
|
* @returns Promise resolving to the parse result with HTML and metadata
|
|
21376
22181
|
* @throws PdfParseError if the file cannot be parsed
|
|
21377
22182
|
*/
|
|
21378
|
-
async parseToText(arrayBuffer,
|
|
22183
|
+
async parseToText(arrayBuffer, optionsOrProgress) {
|
|
22184
|
+
const opts = typeof optionsOrProgress === "function" ? { onProgress: optionsOrProgress } : optionsOrProgress ?? {};
|
|
22185
|
+
const onProgress = opts.onProgress;
|
|
22186
|
+
const translate = opts.translate;
|
|
22187
|
+
const tableDetectionEnabled = opts.enableTableDetection ?? getFeatureFlags().enablePdfTableDetection ?? true;
|
|
21379
22188
|
const pdfjs = await this.ensurePdfjsLoaded();
|
|
21380
22189
|
const warnings = [];
|
|
21381
22190
|
const pages = [];
|
|
@@ -21421,18 +22230,35 @@ var PdfAdapter = class {
|
|
|
21421
22230
|
onProgress?.(pageNum / numPages);
|
|
21422
22231
|
const page = await pdf.getPage(pageNum);
|
|
21423
22232
|
const viewport = page.getViewport({ scale: 1 });
|
|
21424
|
-
const textContent = await
|
|
21425
|
-
|
|
22233
|
+
const [textContent, annotations] = await Promise.all([
|
|
22234
|
+
page.getTextContent(),
|
|
22235
|
+
page.getAnnotations().catch((err) => {
|
|
22236
|
+
console.warn(`[PdfAdapter] getAnnotations failed for page ${pageNum}:`, err);
|
|
22237
|
+
return [];
|
|
22238
|
+
})
|
|
22239
|
+
]);
|
|
22240
|
+
const positionedText = [];
|
|
21426
22241
|
const xPositions = [];
|
|
21427
22242
|
for (const item of textContent.items) {
|
|
21428
|
-
if ("str" in item
|
|
21429
|
-
|
|
21430
|
-
|
|
21431
|
-
|
|
21432
|
-
|
|
21433
|
-
|
|
21434
|
-
|
|
21435
|
-
|
|
22243
|
+
if (!("str" in item) || typeof item.str !== "string") continue;
|
|
22244
|
+
const positioned = textItemToPositioned(item);
|
|
22245
|
+
if (!positioned) continue;
|
|
22246
|
+
positionedText.push(positioned);
|
|
22247
|
+
xPositions.push(positioned.x);
|
|
22248
|
+
}
|
|
22249
|
+
const widgetItems = annotations.flatMap(widgetToPositionedItems);
|
|
22250
|
+
const items = widgetItems.length > 0 ? [...dedupAgainstWidgets(positionedText, widgetItems), ...widgetItems] : positionedText;
|
|
22251
|
+
const pageBottomY = Array.isArray(page.view) && page.view.length >= 2 ? page.view[1] : 0;
|
|
22252
|
+
const rendered = renderPage(
|
|
22253
|
+
items,
|
|
22254
|
+
pageNum,
|
|
22255
|
+
pageNum < numPages ? pageNum + 1 : void 0,
|
|
22256
|
+
tableDetectionEnabled,
|
|
22257
|
+
translate,
|
|
22258
|
+
pageBottomY
|
|
22259
|
+
);
|
|
22260
|
+
for (const w of rendered.warnings) warnings.push(w);
|
|
22261
|
+
if (!hasMultiColumnWarning && !rendered.anyTableDetected && xPositions.length > 10) {
|
|
21436
22262
|
const uniqueXPositions = [...new Set(xPositions.map((x) => Math.round(x / 10) * 10))];
|
|
21437
22263
|
if (uniqueXPositions.length >= 3) {
|
|
21438
22264
|
const xRange = Math.max(...xPositions) - Math.min(...xPositions);
|
|
@@ -21446,32 +22272,13 @@ var PdfAdapter = class {
|
|
|
21446
22272
|
}
|
|
21447
22273
|
}
|
|
21448
22274
|
if (hasGap) {
|
|
21449
|
-
warnings.push("
|
|
22275
|
+
warnings.push(localize(translate, "pdf.warnings.multiColumnLayout", {}));
|
|
21450
22276
|
hasMultiColumnWarning = true;
|
|
21451
22277
|
}
|
|
21452
22278
|
}
|
|
21453
22279
|
}
|
|
21454
22280
|
}
|
|
21455
|
-
|
|
21456
|
-
let lastY = null;
|
|
21457
|
-
for (const item of textItems) {
|
|
21458
|
-
const text = item.str;
|
|
21459
|
-
if (!text) continue;
|
|
21460
|
-
if (item.transform && item.transform.length >= 6) {
|
|
21461
|
-
const currentY = item.transform[5];
|
|
21462
|
-
if (lastY !== null && Math.abs(currentY - lastY) > 5) {
|
|
21463
|
-
pageText += "\n";
|
|
21464
|
-
} else if (pageText && !pageText.endsWith(" ") && !pageText.endsWith("\n")) {
|
|
21465
|
-
pageText += " ";
|
|
21466
|
-
}
|
|
21467
|
-
lastY = currentY;
|
|
21468
|
-
} else if (pageText && !pageText.endsWith(" ") && !pageText.endsWith("\n")) {
|
|
21469
|
-
pageText += " ";
|
|
21470
|
-
}
|
|
21471
|
-
pageText += text;
|
|
21472
|
-
}
|
|
21473
|
-
pageText = pageText.replace(/\n{3,}/g, "\n\n").replace(/[ \t]+/g, " ").trim();
|
|
21474
|
-
totalTextLength += pageText.length;
|
|
22281
|
+
totalTextLength += rendered.textContent.length;
|
|
21475
22282
|
let hasImages = false;
|
|
21476
22283
|
try {
|
|
21477
22284
|
const operatorList = await page.getOperatorList();
|
|
@@ -21480,9 +22287,11 @@ var PdfAdapter = class {
|
|
|
21480
22287
|
}
|
|
21481
22288
|
pages.push({
|
|
21482
22289
|
pageNumber: pageNum,
|
|
21483
|
-
textContent:
|
|
22290
|
+
textContent: rendered.textContent,
|
|
21484
22291
|
hasImages,
|
|
21485
|
-
viewportWidth: viewport.width
|
|
22292
|
+
viewportWidth: viewport.width,
|
|
22293
|
+
htmlBlocks: rendered.htmlBlocks,
|
|
22294
|
+
tablesDetected: rendered.tablesDetected
|
|
21486
22295
|
});
|
|
21487
22296
|
}
|
|
21488
22297
|
const hasAnyImages = pages.some((p) => p.hasImages);
|
|
@@ -21553,6 +22362,9 @@ var PdfAdapter = class {
|
|
|
21553
22362
|
return pages.map((page, index) => {
|
|
21554
22363
|
const pageMarker = index > 0 ? `<div data-page-marker="${page.pageNumber}" class="pdf-page-break">--- Page ${page.pageNumber} ---</div>
|
|
21555
22364
|
` : "";
|
|
22365
|
+
if (page.htmlBlocks && page.htmlBlocks.length > 0) {
|
|
22366
|
+
return pageMarker + page.htmlBlocks.join("\n");
|
|
22367
|
+
}
|
|
21556
22368
|
const paragraphs = page.textContent.split("\n\n").filter((p) => p.trim()).map((p) => {
|
|
21557
22369
|
const trimmed = p.trim();
|
|
21558
22370
|
if (trimmed.length < 100 && !trimmed.match(/[.,:;!?]$/)) {
|
|
@@ -21574,7 +22386,9 @@ var PdfAdapter = class {
|
|
|
21574
22386
|
const loadingTask = pdfjs.getDocument({
|
|
21575
22387
|
data: arrayBuffer,
|
|
21576
22388
|
disableAutoFetch: true,
|
|
21577
|
-
disableStream: true
|
|
22389
|
+
disableStream: true,
|
|
22390
|
+
// Disable JavaScript execution in PDF (Req 3.13) — match parseToText.
|
|
22391
|
+
isEvalSupported: false
|
|
21578
22392
|
});
|
|
21579
22393
|
try {
|
|
21580
22394
|
const pdf = await loadingTask.promise;
|
|
@@ -22147,7 +22961,7 @@ function extractArticle(html, domProvider) {
|
|
|
22147
22961
|
}
|
|
22148
22962
|
|
|
22149
22963
|
// ../../src/shared/token-estimator.ts
|
|
22150
|
-
var
|
|
22964
|
+
var CJK_REGEX2 = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
|
|
22151
22965
|
var MAX_INPUT_LENGTH = 1e6;
|
|
22152
22966
|
function estimateTokens(text) {
|
|
22153
22967
|
if (!text || text.length === 0) {
|
|
@@ -22156,7 +22970,7 @@ function estimateTokens(text) {
|
|
|
22156
22970
|
if (text.length > MAX_INPUT_LENGTH) {
|
|
22157
22971
|
return Math.round(text.length / 4);
|
|
22158
22972
|
}
|
|
22159
|
-
const cjkMatches = text.match(
|
|
22973
|
+
const cjkMatches = text.match(CJK_REGEX2);
|
|
22160
22974
|
const cjkCount = cjkMatches ? cjkMatches.length : 0;
|
|
22161
22975
|
const otherCount = text.length - cjkCount;
|
|
22162
22976
|
const cjkTokens = cjkCount / 1.5;
|
|
@@ -23140,8 +23954,8 @@ import { readFileSync as readFileSync4 } from "fs";
|
|
|
23140
23954
|
import { resolve, dirname as dirname3 } from "path";
|
|
23141
23955
|
import { fileURLToPath } from "url";
|
|
23142
23956
|
var getVersion = () => {
|
|
23143
|
-
if ("1.0.
|
|
23144
|
-
return "1.0.
|
|
23957
|
+
if ("1.0.12") {
|
|
23958
|
+
return "1.0.12";
|
|
23145
23959
|
}
|
|
23146
23960
|
try {
|
|
23147
23961
|
const __dirname = dirname3(fileURLToPath(import.meta.url));
|