@uurtech/jdf-cli 0.2.3 → 0.2.4
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 +483 -97
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -656,6 +656,14 @@ function textExtent(r, k, stretched) {
|
|
|
656
656
|
if (stretched) return Math.max(em * 0.5, Math.min(r.width, est));
|
|
657
657
|
return Math.max(em * 0.5, r.width > est * 1.4 ? est : r.width);
|
|
658
658
|
}
|
|
659
|
+
var numeric = (s) => {
|
|
660
|
+
const t = s.trim();
|
|
661
|
+
if (!t) return false;
|
|
662
|
+
if (/^[—–\-]+$/.test(t)) return true;
|
|
663
|
+
const core = t.replace(/[\s$€£¥(),%]/g, "").replace(/^[+\-−–]/, "");
|
|
664
|
+
return /^\d+(\.\d+)?(ms|s|k|m|b|M|K|B|x|×)?$/.test(core);
|
|
665
|
+
};
|
|
666
|
+
var isCurrencySymbol = (s) => /^[$€£¥]$/.test(s.trim());
|
|
659
667
|
function groupRows(runs, skip) {
|
|
660
668
|
const k = calibrateGlyphWidth(runs);
|
|
661
669
|
const stretched = hasStretchedSpaces(runs, k);
|
|
@@ -677,180 +685,494 @@ function groupRows(runs, skip) {
|
|
|
677
685
|
for (const row of rows) {
|
|
678
686
|
row.cells.sort((a, b) => a.x0 - b.x0);
|
|
679
687
|
const merged = [];
|
|
680
|
-
|
|
688
|
+
let pendingSym = null;
|
|
689
|
+
const pushCell = (c) => {
|
|
681
690
|
const last = merged[merged.length - 1];
|
|
682
691
|
const em = c.run.fontSize * PT_TO_MM;
|
|
683
|
-
|
|
692
|
+
const limit = last && numeric(last.run.text) && numeric(c.run.text) ? em * 0.35 : em * 1;
|
|
693
|
+
if (last && c.x0 - last.x1 <= limit) {
|
|
694
|
+
last.parts = [
|
|
695
|
+
...last.parts ?? [{ text: last.run.text, x0: last.x0, x1: last.x1, idx: [last.idx, ...last.extra ?? []] }],
|
|
696
|
+
{ text: c.run.text, x0: c.x0, x1: c.x1, idx: [c.idx, ...c.extra ?? []] }
|
|
697
|
+
];
|
|
684
698
|
last.x1 = Math.max(last.x1, c.x1);
|
|
685
699
|
last.run = { ...last.run, text: `${last.run.text.replace(/\s+$/, "")} ${c.run.text.replace(/^\s+/, "")}`, width: last.x1 - last.x0 };
|
|
686
|
-
last.extra = [...last.extra ?? [], c.idx];
|
|
700
|
+
last.extra = [...last.extra ?? [], c.idx, ...c.extra ?? []];
|
|
687
701
|
} else merged.push({ ...c });
|
|
702
|
+
};
|
|
703
|
+
for (const c of row.cells) {
|
|
704
|
+
if (isCurrencySymbol(c.run.text)) {
|
|
705
|
+
if (pendingSym) merged.push(pendingSym);
|
|
706
|
+
pendingSym = { ...c };
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (pendingSym) {
|
|
710
|
+
const em = c.run.fontSize * PT_TO_MM;
|
|
711
|
+
if (numeric(c.run.text) && c.x0 - pendingSym.x1 <= em * 8) {
|
|
712
|
+
const cell = { ...c, run: { ...c.run, text: `${pendingSym.run.text.trim()} ${c.run.text.trim()}` } };
|
|
713
|
+
cell.extra = [...c.extra ?? [], pendingSym.idx, ...pendingSym.extra ?? []];
|
|
714
|
+
pendingSym = null;
|
|
715
|
+
pushCell(cell);
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
merged.push(pendingSym);
|
|
719
|
+
pendingSym = null;
|
|
720
|
+
}
|
|
721
|
+
pushCell(c);
|
|
688
722
|
}
|
|
723
|
+
if (pendingSym) merged.push(pendingSym);
|
|
689
724
|
row.cells = merged;
|
|
690
725
|
}
|
|
691
726
|
return rows;
|
|
692
727
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const sorted = cells.slice().sort((a, b) => a.x0 - b.x0);
|
|
728
|
+
var overlaps = (c, b) => c.x0 < b.x1 - 0.2 && c.x1 > b.x0 + 0.2;
|
|
729
|
+
function clusterCells(cells) {
|
|
730
|
+
const sorted = cells.slice().sort((a, b) => a.c.x0 - b.c.x0);
|
|
696
731
|
const bands = [];
|
|
697
|
-
for (const
|
|
732
|
+
for (const m of sorted) {
|
|
698
733
|
const last = bands[bands.length - 1];
|
|
699
|
-
if (last && c.x0 <= last.x1 - 0.2) {
|
|
700
|
-
last.x1 = Math.max(last.x1, c.x1);
|
|
701
|
-
last.members.push(
|
|
702
|
-
} else bands.push({ x0: c.x0, x1: c.x1, members: [
|
|
734
|
+
if (last && m.c.x0 <= last.x1 - 0.2) {
|
|
735
|
+
last.x1 = Math.max(last.x1, m.c.x1);
|
|
736
|
+
last.members.push(m);
|
|
737
|
+
} else bands.push({ x0: m.c.x0, x1: m.c.x1, members: [m] });
|
|
703
738
|
}
|
|
739
|
+
return bands;
|
|
740
|
+
}
|
|
741
|
+
function hasConflict(bands) {
|
|
704
742
|
for (const b of bands) {
|
|
705
|
-
const
|
|
743
|
+
const seen = /* @__PURE__ */ new Set();
|
|
706
744
|
for (const m of b.members) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
745
|
+
if (seen.has(m.r)) return true;
|
|
746
|
+
seen.add(m.r);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
function columnBands(rows) {
|
|
752
|
+
const all = rows.flatMap((r) => r.cells.map((c) => ({ c, r })));
|
|
753
|
+
if (!all.length) return null;
|
|
754
|
+
const firstOf = new Set(rows.map((r) => r.cells[0]));
|
|
755
|
+
const others = all.filter((m) => !firstOf.has(m.c));
|
|
756
|
+
const othersMin = others.length ? Math.min(...others.map((m) => m.c.x0)) : Infinity;
|
|
757
|
+
const leftOf = (m) => m.c.x0 < othersMin - Math.max(1, m.c.run.fontSize * PT_TO_MM * 1.5);
|
|
758
|
+
const textLabels = all.filter((m) => firstOf.has(m.c) && leftOf(m) && !numeric(m.c.run.text));
|
|
759
|
+
const labelX = textLabels.length ? Math.min(...textLabels.map((m) => m.c.x0)) : Infinity;
|
|
760
|
+
const isLabel = (m) => firstOf.has(m.c) && leftOf(m) && (!numeric(m.c.run.text) || m.c.x0 <= labelX + m.c.run.fontSize * PT_TO_MM * 3);
|
|
761
|
+
const body = all.filter((m) => !isLabel(m));
|
|
762
|
+
const labels = all.filter(isLabel);
|
|
763
|
+
if (!body.length) return null;
|
|
764
|
+
const assign = /* @__PURE__ */ new Map();
|
|
765
|
+
const countOf = /* @__PURE__ */ new Map();
|
|
766
|
+
for (const m of body) countOf.set(m.r, (countOf.get(m.r) ?? 0) + 1);
|
|
767
|
+
const maxCount = Math.max(...countOf.values());
|
|
768
|
+
const anchorRows = new Set(rows.filter((r) => countOf.get(r) === maxCount));
|
|
769
|
+
let bands = clusterCells(body.filter((m) => anchorRows.has(m.r)));
|
|
770
|
+
if (hasConflict(bands)) {
|
|
771
|
+
const first = rows.find((r) => anchorRows.has(r));
|
|
772
|
+
bands = clusterCells(body.filter((m) => m.r === first));
|
|
773
|
+
if (hasConflict(bands)) return null;
|
|
774
|
+
}
|
|
775
|
+
for (const m of body) {
|
|
776
|
+
if (anchorRows.has(m.r)) continue;
|
|
777
|
+
let bi = -1, bestOv = 0;
|
|
778
|
+
bands.forEach((b, k) => {
|
|
779
|
+
if (b.members.some((o) => o.r === m.r)) return;
|
|
780
|
+
const ov = Math.min(m.c.x1, b.x1) - Math.max(m.c.x0, b.x0);
|
|
781
|
+
if (ov > bestOv) {
|
|
782
|
+
bestOv = ov;
|
|
783
|
+
bi = k;
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
if (bi < 0) {
|
|
787
|
+
const w = m.c.x1 - m.c.x0;
|
|
788
|
+
if (bands.some((b) => Math.min(m.c.x1, b.x1) - Math.max(m.c.x0, b.x0) > w * 0.5)) return null;
|
|
789
|
+
bands.push({ x0: m.c.x0, x1: m.c.x1, members: [m] });
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
bands[bi].members.push(m);
|
|
793
|
+
const touched = bands.filter((b) => overlaps(m.c, b)).length;
|
|
794
|
+
if (touched <= 1) {
|
|
795
|
+
bands[bi].x0 = Math.min(bands[bi].x0, m.c.x0);
|
|
796
|
+
bands[bi].x1 = Math.max(bands[bi].x1, m.c.x1);
|
|
710
797
|
}
|
|
711
798
|
}
|
|
712
|
-
|
|
799
|
+
bands.sort((a, b) => a.x0 - b.x0);
|
|
800
|
+
for (let k = 0; k + 1 < bands.length; ) {
|
|
801
|
+
const a = bands[k], b = bands[k + 1];
|
|
802
|
+
const shareRow = a.members.some((m) => b.members.some((o) => o.r === m.r));
|
|
803
|
+
const em = Math.min(...[...a.members, ...b.members].map((m) => m.c.run.fontSize)) * PT_TO_MM;
|
|
804
|
+
if (!shareRow && b.x0 - a.x1 <= em * 1.5) {
|
|
805
|
+
bands.splice(k, 2, { x0: a.x0, x1: Math.max(a.x1, b.x1), members: [...a.members, ...b.members] });
|
|
806
|
+
} else k++;
|
|
807
|
+
}
|
|
808
|
+
const out = [];
|
|
809
|
+
if (labels.length) {
|
|
810
|
+
const lx0 = Math.min(...labels.map((m) => m.c.x0));
|
|
811
|
+
const lx1 = Math.min(Math.max(...labels.map((m) => m.c.x1)), bands[0].x0 - 0.2);
|
|
812
|
+
out.push({ x0: lx0, x1: Math.max(lx1, lx0 + 1) });
|
|
813
|
+
for (const m of labels) assign.set(m.c, 0);
|
|
814
|
+
}
|
|
815
|
+
const off = out.length;
|
|
816
|
+
bands.forEach((b, k) => {
|
|
817
|
+
out.push({ x0: b.x0, x1: b.x1 });
|
|
818
|
+
for (const m of b.members) assign.set(m.c, k + off);
|
|
819
|
+
});
|
|
820
|
+
return { bands: out, assign };
|
|
713
821
|
}
|
|
714
|
-
var
|
|
822
|
+
var dbg = (...a) => {
|
|
823
|
+
if (typeof process !== "undefined" && process.env?.JDF_TABLE_DEBUG) console.error("[tables]", ...a);
|
|
824
|
+
};
|
|
715
825
|
function detectTables(runs, shapes, pageWidthMm, gutters = []) {
|
|
716
826
|
const out = [];
|
|
717
827
|
const used = /* @__PURE__ */ new Set();
|
|
718
828
|
const rows = groupRows(runs, () => false);
|
|
719
829
|
const pageW = pageWidthMm;
|
|
830
|
+
const emOf = (c) => c.run.fontSize * PT_TO_MM;
|
|
720
831
|
let i = 0;
|
|
721
832
|
while (i < rows.length) {
|
|
722
833
|
if (rows[i].cells.length < 2) {
|
|
723
834
|
i++;
|
|
724
835
|
continue;
|
|
725
836
|
}
|
|
837
|
+
dbg(`block from "${rows[i].cells.map((c) => c.run.text.slice(0, 14)).join(" | ")}"`);
|
|
726
838
|
let j = i;
|
|
727
839
|
let cur = columnBands([rows[i]]);
|
|
728
840
|
let best = null;
|
|
841
|
+
let loneStreak = 0;
|
|
729
842
|
while (j + 1 < rows.length && cur) {
|
|
730
843
|
const next = rows[j + 1];
|
|
731
844
|
const gap = next.y - (rows[j].y + rows[j].h);
|
|
732
845
|
const rowH = Math.max(rows[j].h, next.h);
|
|
733
|
-
|
|
846
|
+
const established = !!best && best.j - i >= 2;
|
|
847
|
+
if (gap > rowH * (established ? 3.4 : 2.2)) {
|
|
848
|
+
dbg(`break: gap ${gap.toFixed(1)} after "${rows[j].cells[0].run.text.slice(0, 30)}"`);
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
const bands2 = cur.bands;
|
|
734
852
|
if (next.cells.length === 1) {
|
|
735
853
|
const c = next.cells[0];
|
|
736
|
-
const
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
854
|
+
const inBands = bands2.filter((b) => overlaps(c, b));
|
|
855
|
+
const inBand = inBands.length ? bands2.indexOf(inBands[0]) : -1;
|
|
856
|
+
const spansSeveral = inBands.length > 1;
|
|
857
|
+
if (inBand > 0 && !spansSeveral) {
|
|
858
|
+
j++;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const isLabel = c.x1 <= bands2[bands2.length - 1].x0 - 0.5 && (c.x0 <= bands2[0].x0 + emOf(c) * 3 || c.x1 < bands2[0].x0 - 0.5);
|
|
862
|
+
if (isLabel && loneStreak < 4) {
|
|
863
|
+
loneStreak++;
|
|
864
|
+
j++;
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
dbg(`break: lone line "${c.run.text.slice(0, 40)}" inBand=${inBand} spans=${spansSeveral} label=${isLabel} streak=${loneStreak}`);
|
|
868
|
+
break;
|
|
741
869
|
}
|
|
870
|
+
loneStreak = 0;
|
|
742
871
|
const nb = columnBands(rows.slice(i, j + 2));
|
|
743
|
-
if (!nb || nb.length < 2)
|
|
744
|
-
|
|
872
|
+
if (!nb || nb.bands.length < 2) {
|
|
873
|
+
dbg(`break: no bands at "${next.cells.map((c) => c.run.text.slice(0, 15)).join(" | ")}"`);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
if (nb.bands.length > bands2.length && j - i >= 2) {
|
|
877
|
+
const fresh = nb.bands.filter((b) => !bands2.some((o) => overlaps(b, o)) && b.x0 > bands2[0].x0);
|
|
878
|
+
if (fresh.some((b) => b.x1 - b.x0 > 20)) {
|
|
879
|
+
dbg(`break: wide fresh band at "${next.cells.map((c) => c.run.text.slice(0, 15)).join(" | ")}"`);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
745
883
|
cur = nb;
|
|
746
884
|
j++;
|
|
747
|
-
if (cur.length >= 2) best = { j,
|
|
885
|
+
if (cur.bands.length >= 2) best = { j, res: cur };
|
|
748
886
|
}
|
|
749
|
-
const multiRows = best ? rows.slice(i, best.j + 1).filter((r) => r.cells.length >= 2).length : 0;
|
|
750
887
|
const blockRows = best ? rows.slice(i, best.j + 1) : [];
|
|
888
|
+
const multiRows = blockRows.filter((r) => r.cells.length >= 2).length;
|
|
751
889
|
const bbox = blockRows.length ? {
|
|
752
|
-
x0: Math.min(...best.bands.map((b) => b.x0)) - 5,
|
|
753
|
-
x1: Math.max(...best.bands.map((b) => b.x1)) + 5,
|
|
890
|
+
x0: Math.min(...best.res.bands.map((b) => b.x0)) - 5,
|
|
891
|
+
x1: Math.max(...best.res.bands.map((b) => b.x1)) + 5,
|
|
754
892
|
// Cell padding puts backgrounds/borders well above the first baseline and below the last.
|
|
755
893
|
y0: blockRows[0].y - blockRows[0].h * 2.5,
|
|
756
894
|
y1: blockRows[blockRows.length - 1].y + blockRows[blockRows.length - 1].h * 3
|
|
757
895
|
} : null;
|
|
758
896
|
const gridShapes = bbox ? shapes.map((s, k) => ({ s, k })).filter(({ s }) => s.x >= bbox.x0 - 1 && s.x + s.width <= bbox.x1 + 1 && s.y >= bbox.y0 - 1 && s.y + s.height <= bbox.y1 + 1 && (s.kind === "line" || s.kind === "rect")) : [];
|
|
759
|
-
const
|
|
760
|
-
|
|
897
|
+
const tableW = bbox ? bbox.x1 - bbox.x0 : 0;
|
|
898
|
+
const isRule = (s) => s.kind === "line" || s.kind === "rect" && (s.height < 0.6 || s.width < 0.6) && !!(s.fill || s.stroke);
|
|
899
|
+
const ruleYs = gridShapes.filter(({ s }) => isRule(s) && s.width >= s.height && s.width >= tableW * 0.45).map(({ s }) => s.y + s.height / 2).sort((a, b) => a - b);
|
|
900
|
+
const verticalRules = blockRows.length ? gridShapes.filter(({ s }) => isRule(s) && s.height > s.width && s.height >= blockRows[0].h * 1.5).length : 0;
|
|
901
|
+
const rowFill = (row) => {
|
|
902
|
+
const cy = row.y + row.h * 0.5;
|
|
903
|
+
const rects = gridShapes.filter(({ s }) => s.kind === "rect" && s.fill && s.fill.toLowerCase() !== "#ffffff" && s.height >= row.h * 0.6 && s.height < row.h * 4.5 && s.y <= cy && s.y + s.height >= cy);
|
|
904
|
+
const covered = rects.reduce((a, { s }) => a + s.width, 0);
|
|
905
|
+
if (!rects.length || covered < tableW * 0.5) return null;
|
|
906
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
907
|
+
for (const { s } of rects) counts2.set(s.fill, (counts2.get(s.fill) ?? 0) + s.width);
|
|
908
|
+
const fill = [...counts2.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
909
|
+
return { fill, rects };
|
|
910
|
+
};
|
|
911
|
+
const filledRows = blockRows.filter((r) => rowFill(r)).length;
|
|
912
|
+
const hasLattice = verticalRules >= 2 || ruleYs.length >= 3 || filledRows >= 2;
|
|
913
|
+
if (!best || multiRows < (hasLattice ? 2 : 3) || best.res.bands.length < 2) {
|
|
914
|
+
if (best) dbg(`reject: ${multiRows} multi rows from "${rows[i].cells[0].run.text.slice(0, 30)}"`);
|
|
761
915
|
i++;
|
|
762
916
|
continue;
|
|
763
917
|
}
|
|
764
918
|
if (gutters.length && !hasLattice) {
|
|
765
|
-
const b = best.bands;
|
|
766
|
-
const straddles =
|
|
919
|
+
const b = best.res.bands;
|
|
920
|
+
const straddles = gutters.some((g) => b.some((band) => band.x1 <= g.x1 + 1 && band.x1 - band.x0 > 30) && b.some((band) => band.x0 >= g.x0 - 1 && band.x1 - band.x0 > 30));
|
|
767
921
|
if (straddles) {
|
|
922
|
+
dbg("reject: straddles a column gutter");
|
|
768
923
|
i++;
|
|
769
924
|
continue;
|
|
770
925
|
}
|
|
771
926
|
}
|
|
772
|
-
const bands = best.
|
|
773
|
-
const
|
|
927
|
+
const { bands, assign } = best.res;
|
|
928
|
+
const ruleBetween = (a, b) => {
|
|
929
|
+
const y02 = a.y + a.h * 0.5, y1 = b.y + b.h * 0.5;
|
|
930
|
+
return ruleYs.some((y) => y > y02 && y < y1);
|
|
931
|
+
};
|
|
932
|
+
const denseRules = ruleYs.length >= Math.max(2, multiRows * 0.6);
|
|
933
|
+
for (const row of blockRows) {
|
|
934
|
+
for (const c of row.cells.slice()) {
|
|
935
|
+
const b = assign.get(c);
|
|
936
|
+
if (b == null || b + 1 >= bands.length) continue;
|
|
937
|
+
if (row.cells.some((o) => o !== c && assign.get(o) === b + 1)) continue;
|
|
938
|
+
const nextX0 = bands[b + 1].x0;
|
|
939
|
+
let cut;
|
|
940
|
+
if (!c.parts) {
|
|
941
|
+
const w = c.x1 - c.x0;
|
|
942
|
+
if (c.x1 <= nextX0 + w * 0.3 || !/\s/.test(c.run.text.trim())) continue;
|
|
943
|
+
const text = c.run.text.trim();
|
|
944
|
+
const at = Math.round((nextX0 - c.x0) / w * text.length);
|
|
945
|
+
let best2 = -1;
|
|
946
|
+
for (let q = 0; q < text.length; q++) if (text[q] === " " && (best2 < 0 || Math.abs(q - at) < Math.abs(best2 - at))) best2 = q;
|
|
947
|
+
if (best2 <= 0 || best2 >= text.length - 1 || Math.abs(best2 - at) > text.length * 0.25) continue;
|
|
948
|
+
const perChar = w / text.length;
|
|
949
|
+
c.parts = [
|
|
950
|
+
{ text: text.slice(0, best2), x0: c.x0, x1: c.x0 + best2 * perChar, idx: [c.idx, ...c.extra ?? []] },
|
|
951
|
+
{ text: text.slice(best2 + 1), x0: c.x0 + (best2 + 1) * perChar, x1: c.x1, idx: [c.idx] }
|
|
952
|
+
];
|
|
953
|
+
cut = 1;
|
|
954
|
+
} else {
|
|
955
|
+
cut = c.parts.findIndex((p) => p.x0 >= nextX0 - 0.5);
|
|
956
|
+
}
|
|
957
|
+
if (cut <= 0) continue;
|
|
958
|
+
const head = c.parts.slice(0, cut), tail = c.parts.slice(cut);
|
|
959
|
+
const tailCell = { run: { ...c.run, text: tail.map((p) => p.text.trim()).join(" ") }, idx: tail[0].idx[0], x0: tail[0].x0, x1: tail[tail.length - 1].x1, parts: tail };
|
|
960
|
+
tailCell.extra = tail.flatMap((p) => p.idx).filter((k) => k !== tailCell.idx);
|
|
961
|
+
c.run = { ...c.run, text: head.map((p) => p.text.trim()).join(" ") };
|
|
962
|
+
c.x1 = head[head.length - 1].x1;
|
|
963
|
+
c.idx = head[0].idx[0];
|
|
964
|
+
c.extra = head.flatMap((p) => p.idx).filter((k) => k !== c.idx);
|
|
965
|
+
c.parts = head.length > 1 ? head : void 0;
|
|
966
|
+
row.cells.push(tailCell);
|
|
967
|
+
assign.set(tailCell, b + 1);
|
|
968
|
+
}
|
|
969
|
+
row.cells.sort((a, b2) => a.x0 - b2.x0);
|
|
970
|
+
}
|
|
971
|
+
const clean = (t) => t.replace(/(?:\s*\.){3,}\s*$/, "").replace(/\s+/g, " ").trim();
|
|
972
|
+
const cellText = (row, b) => clean(row.cells.filter((c) => assign.get(c) === b).map((c) => c.run.text.trim()).join(" "));
|
|
973
|
+
const cellIdx = (c) => [c.idx, ...c.extra ?? []];
|
|
774
974
|
const grid = [];
|
|
975
|
+
const gridRows = [];
|
|
775
976
|
const lineIdx = [];
|
|
776
|
-
|
|
777
|
-
|
|
977
|
+
const pitches = [];
|
|
978
|
+
for (let r = 1; r < blockRows.length; r++) pitches.push(blockRows[r].y - blockRows[r - 1].y);
|
|
979
|
+
const medPitch = pitches.length ? pitches.slice().sort((a, b) => a - b)[Math.floor(pitches.length / 2)] : blockRows[0].h * 1.3;
|
|
980
|
+
let pendingLabel = null;
|
|
981
|
+
const flushPending = () => {
|
|
982
|
+
if (!pendingLabel) return;
|
|
983
|
+
const cells = bands.map(() => "");
|
|
984
|
+
cells[0] = pendingLabel.text;
|
|
985
|
+
grid.push(cells);
|
|
986
|
+
gridRows.push(pendingLabel.row);
|
|
987
|
+
lineIdx.push(...pendingLabel.idx);
|
|
988
|
+
pendingLabel = null;
|
|
989
|
+
};
|
|
990
|
+
for (let r = 0; r < blockRows.length; r++) {
|
|
991
|
+
const row = blockRows[r];
|
|
992
|
+
if (row.cells.length === 1 && grid.length + (pendingLabel ? 1 : 0) > 0) {
|
|
778
993
|
const c = row.cells[0];
|
|
779
|
-
const b = bands.findIndex((bb) => c
|
|
994
|
+
const b = assign.get(c) ?? bands.findIndex((bb) => overlaps(c, bb));
|
|
780
995
|
if (b > 0) {
|
|
996
|
+
flushPending();
|
|
781
997
|
grid[grid.length - 1][b] = (grid[grid.length - 1][b] + " " + c.run.text.trim()).trim();
|
|
782
|
-
lineIdx.push(
|
|
998
|
+
lineIdx.push(...cellIdx(c));
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const text = clean(c.run.text);
|
|
1002
|
+
const prev = gridRows[gridRows.length - 1];
|
|
1003
|
+
const nextRow = blockRows[r + 1];
|
|
1004
|
+
const nextLabel = nextRow && nextRow.cells.length >= 2 ? cellText(nextRow, 0) : "";
|
|
1005
|
+
const lower = /^[(\[a-zà-ÿ]/.test(text);
|
|
1006
|
+
const attachPrev = !pendingLabel && prev && grid.length && (denseRules ? !ruleBetween(prev, row) : lower);
|
|
1007
|
+
if (attachPrev) {
|
|
1008
|
+
grid[grid.length - 1][0] = (grid[grid.length - 1][0] + " " + text).trim();
|
|
1009
|
+
lineIdx.push(...cellIdx(c));
|
|
783
1010
|
continue;
|
|
784
1011
|
}
|
|
1012
|
+
const attachNext = nextLabel && (denseRules ? !ruleBetween(row, nextRow) : /^[a-zà-ÿ]/.test(nextLabel) || !/[.:]$/.test(text) && row.cells[0].x0 <= bands[0].x0 + emOf(c) * 0.5 && nextRow.y - row.y <= medPitch * 1.25);
|
|
1013
|
+
if (pendingLabel) {
|
|
1014
|
+
pendingLabel.text = `${pendingLabel.text} ${text}`;
|
|
1015
|
+
pendingLabel.idx.push(...cellIdx(c));
|
|
1016
|
+
} else pendingLabel = { text, idx: cellIdx(c), row };
|
|
1017
|
+
if (!attachNext) flushPending();
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
const prevRow = gridRows[gridRows.length - 1];
|
|
1021
|
+
if (prevRow && !pendingLabel && row.y - prevRow.y > medPitch * 1.7 && grid.length) {
|
|
1022
|
+
grid.push(bands.map(() => ""));
|
|
1023
|
+
gridRows.push(row);
|
|
785
1024
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1025
|
+
const cells = bands.map((_, b) => cellText(row, b));
|
|
1026
|
+
if (pendingLabel) {
|
|
1027
|
+
cells[0] = `${pendingLabel.text} ${cells[0]}`.trim();
|
|
1028
|
+
lineIdx.push(...pendingLabel.idx);
|
|
1029
|
+
pendingLabel = null;
|
|
790
1030
|
}
|
|
1031
|
+
grid.push(cells);
|
|
1032
|
+
gridRows.push(row);
|
|
1033
|
+
for (const c of row.cells) lineIdx.push(...cellIdx(c));
|
|
1034
|
+
}
|
|
1035
|
+
flushPending();
|
|
1036
|
+
for (let r = 1; r < grid.length; r++) {
|
|
1037
|
+
const cur2 = grid[r], prev = grid[r - 1];
|
|
1038
|
+
if (cur2[0] !== "" || !prev[0] || !prev.some(Boolean) || !cur2.some(Boolean)) continue;
|
|
1039
|
+
const last = bands.length - 1;
|
|
1040
|
+
if (prev[last] !== "" || cur2[last] === "") continue;
|
|
1041
|
+
if (prev.slice(1).some((v, k) => v && cur2[k + 1] && numeric(v) && numeric(cur2[k + 1]))) continue;
|
|
1042
|
+
for (let k = 0; k < bands.length; k++) prev[k] = [prev[k], cur2[k]].filter(Boolean).join(" ");
|
|
1043
|
+
grid.splice(r, 1);
|
|
1044
|
+
gridRows.splice(r, 1);
|
|
1045
|
+
r--;
|
|
1046
|
+
}
|
|
1047
|
+
while (grid.length && grid[grid.length - 1].every((c) => !c)) {
|
|
1048
|
+
grid.pop();
|
|
1049
|
+
gridRows.pop();
|
|
791
1050
|
}
|
|
792
1051
|
if (lineIdx.some((k) => used.has(k))) {
|
|
793
1052
|
i = best.j + 1;
|
|
794
1053
|
continue;
|
|
795
1054
|
}
|
|
1055
|
+
const isNumCol = bands.map((_, k) => {
|
|
1056
|
+
const vals = grid.map((r) => r[k]).filter(Boolean);
|
|
1057
|
+
const n = vals.filter(numeric).length;
|
|
1058
|
+
return n >= 2 && n / vals.length >= 0.6;
|
|
1059
|
+
});
|
|
1060
|
+
const numCols = isNumCol.filter(Boolean).length;
|
|
1061
|
+
const hasLoneLabel = blockRows.some((r) => r.cells.length === 1 && (assign.get(r.cells[0]) ?? -1) === 0);
|
|
1062
|
+
const structural = verticalRules >= 2 || filledRows >= 2 || ruleYs.length >= 3 && numCols > 0;
|
|
1063
|
+
const counts = new Set(blockRows.filter((r) => r.cells.length >= 2).map((r) => r.cells.length));
|
|
1064
|
+
if (!structural && numCols === 0 && (multiRows < 4 || hasLoneLabel || counts.size > 1 || bands.length === 2 && grid.length <= 3)) {
|
|
1065
|
+
dbg("reject text block", grid[0]);
|
|
1066
|
+
i++;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
796
1069
|
const first = blockRows[0];
|
|
797
|
-
const tableW = bbox.x1 - bbox.x0;
|
|
798
|
-
const rowFill = (row) => {
|
|
799
|
-
const cy = row.y + row.h * 0.5;
|
|
800
|
-
const rects = gridShapes.filter(({ s }) => s.kind === "rect" && s.fill && s.fill.toLowerCase() !== "#ffffff" && s.height >= row.h * 0.6 && s.height < row.h * 4.5 && s.y <= cy && s.y + s.height >= cy);
|
|
801
|
-
const covered = rects.reduce((a, { s }) => a + s.width, 0);
|
|
802
|
-
if (!rects.length || covered < tableW * 0.5) return null;
|
|
803
|
-
const counts = /* @__PURE__ */ new Map();
|
|
804
|
-
for (const { s } of rects) counts.set(s.fill, (counts.get(s.fill) ?? 0) + s.width);
|
|
805
|
-
const fill = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
806
|
-
return { fill, rects };
|
|
807
|
-
};
|
|
808
1070
|
const headerBg = rowFill(first);
|
|
809
1071
|
const firstBold = first.cells.every((c) => c.run.bold);
|
|
810
1072
|
const bodyFills = blockRows.slice(1).map(rowFill);
|
|
811
1073
|
const headerDistinct = !!headerBg && !bodyFills.every((f) => f?.fill === headerBg.fill);
|
|
812
|
-
|
|
1074
|
+
let headerRows = headerDistinct || firstBold && !blockRows.slice(1).every((r) => r.cells.every((c) => c.run.bold)) ? 1 : 0;
|
|
1075
|
+
const rowBold = (r) => r.cells.every((c) => c.run.bold);
|
|
1076
|
+
if (!blockRows.every(rowBold)) {
|
|
1077
|
+
let b = 0;
|
|
1078
|
+
while (b < Math.min(5, gridRows.length - 1) && gridRows[b] && rowBold(gridRows[b]) && grid[b].some(Boolean)) b++;
|
|
1079
|
+
headerRows = Math.max(headerRows, b);
|
|
1080
|
+
}
|
|
1081
|
+
if (denseRules && ruleYs.length) {
|
|
1082
|
+
const firstRule = ruleYs.find((y) => y > gridRows[0].y + gridRows[0].h * 0.5);
|
|
1083
|
+
if (firstRule != null) {
|
|
1084
|
+
const above = gridRows.filter((r) => r.y + r.h * 0.5 < firstRule).length;
|
|
1085
|
+
if (above >= 1 && above <= 5 && gridRows.length - above >= 2) headerRows = Math.max(headerRows, above);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (numCols > 0) {
|
|
1089
|
+
let h = 0;
|
|
1090
|
+
while (h < Math.min(5, grid.length - 2)) {
|
|
1091
|
+
const row = grid[h];
|
|
1092
|
+
const nonEmpty = row.filter(Boolean);
|
|
1093
|
+
if (!nonEmpty.length) break;
|
|
1094
|
+
const numericHere = row.some((v, k) => isNumCol[k] && v && numeric(v));
|
|
1095
|
+
if (numericHere) break;
|
|
1096
|
+
if (!row.some((v, k) => isNumCol[k] && v) && h >= headerRows) break;
|
|
1097
|
+
h++;
|
|
1098
|
+
}
|
|
1099
|
+
headerRows = Math.max(headerRows, h);
|
|
1100
|
+
}
|
|
1101
|
+
if (headerRows > 0 && grid.length - headerRows < 1) headerRows = 0;
|
|
813
1102
|
const columns = bands.map((b, k) => {
|
|
814
|
-
const vals = grid.slice(isHeader ? 1 : 0).map((r) => r[k]).filter(Boolean);
|
|
815
|
-
const numericShare = vals.length ? vals.filter(numeric).length / vals.length : 0;
|
|
816
1103
|
const col = { width: Math.round((b.x1 - b.x0) * 10) / 10 };
|
|
817
|
-
if (
|
|
1104
|
+
if (isNumCol[k]) col.align = "right";
|
|
818
1105
|
return col;
|
|
819
1106
|
});
|
|
820
1107
|
const x0 = Math.max(0, bands[0].x0 - 2.5);
|
|
821
1108
|
const x1 = Math.min(pageW, bands[bands.length - 1].x1 + 2.5);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const rowFills = (
|
|
1109
|
+
const edges = [x0];
|
|
1110
|
+
for (let k = 1; k < bands.length; k++) edges.push(Math.max(edges[k - 1] + 2, Math.min((bands[k - 1].x1 + bands[k].x0) / 2, x1 - 2 * (bands.length - k))));
|
|
1111
|
+
edges.push(Math.max(edges[edges.length - 1] + 2, x1));
|
|
1112
|
+
for (let k = 0; k < bands.length; k++) columns[k].width = Math.round((edges[k + 1] - edges[k]) * 10) / 10;
|
|
1113
|
+
const bodyRows = gridRows.slice(headerRows).filter((r, k, arr) => grid[headerRows + k]?.some(Boolean));
|
|
1114
|
+
const rowFills = bodyRows.map(rowFill).map((f) => f?.fill ?? null);
|
|
828
1115
|
const odd = rowFills.filter((_, k) => k % 2 === 1), even = rowFills.filter((_, k) => k % 2 === 0);
|
|
829
|
-
const
|
|
830
|
-
const
|
|
1116
|
+
const oddAlt = odd.length && odd[0] && odd.every((f) => f === odd[0]) && even.every((f) => f !== odd[0]) ? odd[0] : void 0;
|
|
1117
|
+
const evenAlt = !oddAlt && even.length > 1 && even[0] && even.every((f) => f === even[0]) && odd.every((f) => f !== even[0]) ? even[0] : void 0;
|
|
1118
|
+
const altColor = oddAlt || evenAlt;
|
|
1119
|
+
const borderShape = gridShapes.find(({ s }) => isRule(s));
|
|
831
1120
|
const borderColor = borderShape ? borderShape.s.stroke || borderShape.s.fill : void 0;
|
|
832
|
-
const
|
|
833
|
-
const
|
|
1121
|
+
const hasGrid = verticalRules >= 2;
|
|
1122
|
+
const sizeChars = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const row of blockRows) for (const c of row.cells) {
|
|
1124
|
+
const k = Math.round(c.run.fontSize * 2) / 2;
|
|
1125
|
+
sizeChars.set(k, (sizeChars.get(k) ?? 0) + c.run.text.length);
|
|
1126
|
+
}
|
|
1127
|
+
const fontSize = [...sizeChars.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? Math.round(first.cells[0].run.fontSize * 10) / 10;
|
|
1128
|
+
const lineMm = fontSize * PT_TO_MM * 1.2;
|
|
1129
|
+
const padV = Math.max(0.2, Math.min(2.5, (medPitch - lineMm) / 2));
|
|
1130
|
+
const padH = Math.max(0.5, Math.min(3, fontSize * PT_TO_MM * 0.35));
|
|
1131
|
+
const y0 = headerBg ? Math.min(...headerBg.rects.map(({ s }) => s.y)) : first.y - padV;
|
|
834
1132
|
const element = {
|
|
835
1133
|
type: "table",
|
|
836
1134
|
position: { x: Math.round(x0 * 100) / 100, y: Math.round(Math.max(0, y0) * 100) / 100 },
|
|
837
1135
|
width: Math.round((x1 - x0) * 100) / 100,
|
|
838
1136
|
columns,
|
|
839
|
-
rows:
|
|
840
|
-
|
|
1137
|
+
rows: (() => {
|
|
1138
|
+
const body = grid.slice(headerRows);
|
|
1139
|
+
while (body.length > 1 && body[0].every((c) => !c)) body.shift();
|
|
1140
|
+
return body;
|
|
1141
|
+
})(),
|
|
1142
|
+
style: { fontSize, lineHeight: 1.2, padding: `${Math.round(padV * 100) / 100}mm ${Math.round(padH * 100) / 100}mm` }
|
|
841
1143
|
};
|
|
842
|
-
if (
|
|
843
|
-
element.headers = grid[
|
|
1144
|
+
if (headerRows > 0) {
|
|
1145
|
+
element.headers = bands.map((_, k) => grid.slice(0, headerRows).map((r) => r[k]).filter(Boolean).join(" ").trim());
|
|
844
1146
|
const hs = { fontWeight: "bold" };
|
|
845
1147
|
if (headerBg) hs.backgroundColor = headerBg.fill;
|
|
846
|
-
|
|
847
|
-
|
|
1148
|
+
if (headerBg) {
|
|
1149
|
+
const tally = /* @__PURE__ */ new Map();
|
|
1150
|
+
for (const c of blockRows.slice(0, headerRows).flatMap((r) => r.cells)) tally.set(c.run.color, (tally.get(c.run.color) ?? 0) + c.run.text.length);
|
|
1151
|
+
const hc = [...tally.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
1152
|
+
if (hc && hc !== "#000000") hs.color = hc;
|
|
1153
|
+
}
|
|
848
1154
|
element.headerStyle = hs;
|
|
849
1155
|
}
|
|
850
|
-
if (
|
|
851
|
-
|
|
1156
|
+
if (oddAlt) element.alternatingRowColor = oddAlt;
|
|
1157
|
+
if (evenAlt) {
|
|
1158
|
+
element.rowStyle = { backgroundColor: evenAlt };
|
|
1159
|
+
element.alternateRowStyle = { backgroundColor: "#ffffff" };
|
|
1160
|
+
}
|
|
1161
|
+
element.borders = hasGrid && borderColor ? { outer: true, inner: true, color: borderColor, width: 1 } : false;
|
|
1162
|
+
const fillRects = /* @__PURE__ */ new Set();
|
|
1163
|
+
if (headerBg) for (const { k } of headerBg.rects) fillRects.add(k);
|
|
1164
|
+
if (headerRows > 0) {
|
|
1165
|
+
const hy0 = gridRows[0].y - gridRows[0].h * 0.5, hy1 = gridRows[headerRows - 1].y + gridRows[headerRows - 1].h * 1.2;
|
|
1166
|
+
for (const { s, k } of gridShapes) if (s.kind === "rect" && s.fill && s.fill.toLowerCase() !== "#ffffff" && !isRule(s) && s.y < hy1 && s.y + s.height > hy0 && s.y >= hy0 - gridRows[0].h) fillRects.add(k);
|
|
1167
|
+
}
|
|
1168
|
+
if (altColor) for (const row of blockRows) {
|
|
1169
|
+
const f = rowFill(row);
|
|
1170
|
+
if (f) for (const { k } of f.rects) fillRects.add(k);
|
|
1171
|
+
}
|
|
1172
|
+
const consumedShapes = gridShapes.filter(({ s, k }) => isRule(s) || fillRects.has(k)).map(({ k }) => k);
|
|
1173
|
+
dbg(`table ${grid.length} rows \xD7 ${bands.length} cols at y=${element.position.y}`, element.headers ?? grid[0]);
|
|
852
1174
|
for (const k of lineIdx) used.add(k);
|
|
853
|
-
out.push({ element, lineIdx, shapeIdx:
|
|
1175
|
+
out.push({ element, lineIdx, shapeIdx: consumedShapes });
|
|
854
1176
|
i = best.j + 1;
|
|
855
1177
|
}
|
|
856
1178
|
return out;
|
|
@@ -1040,6 +1362,47 @@ function rgbToHex(r, g, b) {
|
|
|
1040
1362
|
const h = (n) => clampByte(n).toString(16).padStart(2, "0");
|
|
1041
1363
|
return `#${h(r)}${h(g)}${h(b)}`;
|
|
1042
1364
|
}
|
|
1365
|
+
function mergeCollinearShapes(shapes) {
|
|
1366
|
+
const chain = (input, horizontal) => {
|
|
1367
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1368
|
+
input.forEach((s, k) => {
|
|
1369
|
+
if (s.kind !== "rect" || s.stroke || s.path || !s.fill) return;
|
|
1370
|
+
if (horizontal ? s.height > 1 : s.width > 1) return;
|
|
1371
|
+
const key = horizontal ? `${s.fill}|${s.opacity ?? 1}|${s.y.toFixed(1)}|${s.height.toFixed(1)}` : `${s.fill}|${s.opacity ?? 1}|${s.x.toFixed(1)}|${s.width.toFixed(1)}`;
|
|
1372
|
+
const g = groups.get(key);
|
|
1373
|
+
if (g) g.push({ s, k });
|
|
1374
|
+
else groups.set(key, [{ s, k }]);
|
|
1375
|
+
});
|
|
1376
|
+
const replaceAt = /* @__PURE__ */ new Map();
|
|
1377
|
+
const drop = /* @__PURE__ */ new Set();
|
|
1378
|
+
for (const g of groups.values()) {
|
|
1379
|
+
if (g.length < 2) continue;
|
|
1380
|
+
g.sort((a, b) => horizontal ? a.s.x - b.s.x : a.s.y - b.s.y);
|
|
1381
|
+
let run = [g[0]];
|
|
1382
|
+
const flush = () => {
|
|
1383
|
+
if (run.length >= 2) {
|
|
1384
|
+
const first = run[0].s;
|
|
1385
|
+
const end = Math.max(...run.map(({ s }) => horizontal ? s.x + s.width : s.y + s.height));
|
|
1386
|
+
replaceAt.set(run[0].k, horizontal ? { ...first, width: end - first.x } : { ...first, height: end - first.y });
|
|
1387
|
+
for (let q = 1; q < run.length; q++) drop.add(run[q].k);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
for (let q = 1; q < g.length; q++) {
|
|
1391
|
+
const prev = run[run.length - 1].s, cur = g[q].s;
|
|
1392
|
+
const gap = horizontal ? cur.x - (prev.x + prev.width) : cur.y - (prev.y + prev.height);
|
|
1393
|
+
if (gap <= 0.15) run.push(g[q]);
|
|
1394
|
+
else {
|
|
1395
|
+
flush();
|
|
1396
|
+
run = [g[q]];
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
flush();
|
|
1400
|
+
}
|
|
1401
|
+
if (!replaceAt.size) return input;
|
|
1402
|
+
return input.map((s, k) => replaceAt.get(k) ?? s).filter((_, k) => !drop.has(k));
|
|
1403
|
+
};
|
|
1404
|
+
return chain(chain(shapes, true), false);
|
|
1405
|
+
}
|
|
1043
1406
|
function averageStops(stops) {
|
|
1044
1407
|
if (!Array.isArray(stops) || stops.length === 0) return null;
|
|
1045
1408
|
let r = 0, g = 0, b = 0, n = 0;
|
|
@@ -1948,8 +2311,20 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1948
2311
|
const c = fontMap.get(name) || classifyFont(name || "");
|
|
1949
2312
|
return `${c.family}|${c.weight || ""}|${c.style || ""}`;
|
|
1950
2313
|
};
|
|
2314
|
+
const extent = (t) => {
|
|
2315
|
+
if (!/\s$/.test(t.text)) return t.width;
|
|
2316
|
+
const em = t.fontSize * PT_TO_MM3;
|
|
2317
|
+
const est = Math.max(1, t.text.trim().length) * em * kGlyph + em * 0.25;
|
|
2318
|
+
if (stretchedSpaces) return Math.min(t.width, est);
|
|
2319
|
+
return t.width > est * 1.4 ? est : t.width;
|
|
2320
|
+
};
|
|
2321
|
+
const spaceItems = runs.filter((r) => !r.text.trim() && r.text.length && r.width >= r.fontSize * PT_TO_MM3 * 0.5);
|
|
2322
|
+
const wideSpaces = spaceItems.filter((r) => r.width >= r.fontSize * PT_TO_MM3 * 2).length;
|
|
2323
|
+
const tabular = spaceItems.length >= 8 && wideSpaces / spaceItems.length >= 0.25;
|
|
2324
|
+
const gapEm = tabular ? 1 : 2;
|
|
1951
2325
|
for (const r of runs) {
|
|
1952
2326
|
if (!r.text.length) continue;
|
|
2327
|
+
if (!r.text.trim() && r.width > r.fontSize * PT_TO_MM3 * gapEm) continue;
|
|
1953
2328
|
const last = lines[lines.length - 1];
|
|
1954
2329
|
if (!last) {
|
|
1955
2330
|
lines.push({ ...r });
|
|
@@ -1958,15 +2333,9 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1958
2333
|
const sameLine = Math.abs(last.y - r.y) <= Y_TOL;
|
|
1959
2334
|
const sameStyle = Math.abs(last.fontSize - r.fontSize) < 0.4 && (last.fontName === r.fontName || fontKey(last.fontName) === fontKey(r.fontName)) && last.color === r.color && Math.abs(last.opacity - r.opacity) < 0.05;
|
|
1960
2335
|
const emMm = r.fontSize * PT_TO_MM3;
|
|
1961
|
-
const extent = (t) => {
|
|
1962
|
-
if (!/\s$/.test(t.text)) return t.width;
|
|
1963
|
-
const em = t.fontSize * PT_TO_MM3;
|
|
1964
|
-
const est = Math.max(1, t.text.trim().length) * em * kGlyph + em * 0.25;
|
|
1965
|
-
if (stretchedSpaces) return Math.min(t.width, est);
|
|
1966
|
-
return t.width > est * 1.4 ? est : t.width;
|
|
1967
|
-
};
|
|
1968
2336
|
const gapMm = r.x - (last.x + extent(last));
|
|
1969
|
-
const
|
|
2337
|
+
const symAfterNumber = /^[$€£¥]$/.test(r.text.trim()) && /[\d)]\s*$/.test(last.text);
|
|
2338
|
+
const mergeOk = sameLine && sameStyle && !symAfterNumber && gapMm >= -emMm * 0.5 && gapMm <= emMm * 0.45;
|
|
1970
2339
|
if (mergeOk) {
|
|
1971
2340
|
const lastEndsSpace = /\s$/.test(last.text);
|
|
1972
2341
|
const currStartsSpace = /^\s/.test(r.text);
|
|
@@ -1978,11 +2347,26 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1978
2347
|
lines.push({ ...r });
|
|
1979
2348
|
}
|
|
1980
2349
|
}
|
|
2350
|
+
{
|
|
2351
|
+
const keep = [];
|
|
2352
|
+
for (const l of lines) {
|
|
2353
|
+
const t = l.text.trim();
|
|
2354
|
+
const recent = keep.slice(-40);
|
|
2355
|
+
const dup = t.length ? recent.find((k) => Math.abs(k.y - l.y) <= 0.6 && Math.abs(k.x - l.x) <= 0.8 && Math.abs(k.fontSize - l.fontSize) < 0.6 && k.text.trim() === t) : void 0;
|
|
2356
|
+
if (dup) {
|
|
2357
|
+
dup.bold = true;
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
keep.push(l);
|
|
2361
|
+
}
|
|
2362
|
+
lines.length = 0;
|
|
2363
|
+
lines.push(...keep);
|
|
2364
|
+
}
|
|
1981
2365
|
const elements = [];
|
|
1982
2366
|
const lineMeta = /* @__PURE__ */ new WeakMap();
|
|
1983
2367
|
const tRuns = lines.map((l) => {
|
|
1984
2368
|
const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
|
|
1985
|
-
return { text: l.text, x: l.x, y: l.y, width: l
|
|
2369
|
+
return { text: l.text, x: l.x, y: l.y, width: extent(l), height: l.height, fontSize: l.fontSize, fontName: l.fontName, color: l.color, bold: cls.weight === "bold" || !!l.bold };
|
|
1986
2370
|
});
|
|
1987
2371
|
const sizeChars = /* @__PURE__ */ new Map();
|
|
1988
2372
|
for (const l of lines) {
|
|
@@ -1991,7 +2375,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1991
2375
|
}
|
|
1992
2376
|
const bodyFontSize = [...sizeChars.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 0;
|
|
1993
2377
|
const gutters = options.readingOrder === false ? [] : detectGutters(lines.map((l) => ({ text: l.text, x: l.x, y: l.y, width: l.width, fontSize: l.fontSize })), bodyFontSize, pageW * PT_TO_MM3);
|
|
1994
|
-
const
|
|
2378
|
+
const pageShapes = mergeCollinearShapes(ops.shapes);
|
|
2379
|
+
const detected = options.detectTables === false ? [] : detectTables(tRuns, pageShapes, pageW * PT_TO_MM3, gutters);
|
|
1995
2380
|
const consumedLines = /* @__PURE__ */ new Set();
|
|
1996
2381
|
const consumedShapes = /* @__PURE__ */ new Set();
|
|
1997
2382
|
const tableAtLine = /* @__PURE__ */ new Map();
|
|
@@ -2001,7 +2386,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2001
2386
|
tableAtLine.set(Math.min(...t.lineIdx), t.element);
|
|
2002
2387
|
}
|
|
2003
2388
|
const pageWmm = pageW * PT_TO_MM3, pageHmm = pageH * PT_TO_MM3;
|
|
2004
|
-
|
|
2389
|
+
pageShapes.forEach((sh, shapeIdx) => {
|
|
2005
2390
|
if (consumedShapes.has(shapeIdx)) return;
|
|
2006
2391
|
if (sh.width < 0.3 && sh.height < 0.3) return;
|
|
2007
2392
|
if (sh.x + sh.width <= 0 || sh.y + sh.height <= 0 || sh.x >= pageWmm || sh.y >= pageHmm) return;
|
|
@@ -2072,7 +2457,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2072
2457
|
while (nextOnRow.has(cur)) {
|
|
2073
2458
|
const j = nextOnRow.get(cur), lc = lines[cur], lj = lines[j];
|
|
2074
2459
|
const em = Math.min(lc.fontSize, lj.fontSize) * PT_TO_MM3;
|
|
2075
|
-
const gap = lj.x - (lc.x + lc
|
|
2460
|
+
const gap = lj.x - (lc.x + extent(lc));
|
|
2076
2461
|
if (gap < -em * 0.3 || gap > em * 0.6) break;
|
|
2077
2462
|
row.push(j);
|
|
2078
2463
|
seen.add(j);
|
|
@@ -2084,7 +2469,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2084
2469
|
}
|
|
2085
2470
|
const runStyle = (l) => {
|
|
2086
2471
|
const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
|
|
2087
|
-
return { cls, bold: cls.weight === "bold", italic: cls.style === "italic" };
|
|
2472
|
+
return { cls, bold: cls.weight === "bold" || !!l.bold, italic: cls.style === "italic" };
|
|
2088
2473
|
};
|
|
2089
2474
|
lines.forEach((l, lineIdx) => {
|
|
2090
2475
|
const tableEl = tableAtLine.get(lineIdx);
|
|
@@ -2095,7 +2480,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2095
2480
|
if (row.length > 1) {
|
|
2096
2481
|
const first = lines[row[0]], last = lines[row[row.length - 1]];
|
|
2097
2482
|
const base = runStyle(first);
|
|
2098
|
-
const rowEnd = last.x + last
|
|
2483
|
+
const rowEnd = last.x + extent(last);
|
|
2099
2484
|
const measuredW = Math.max((rowEnd - first.x) * 1.2 + first.fontSize * PT_TO_MM3 * 0.4, first.fontSize * PT_TO_MM3);
|
|
2100
2485
|
const nextIdx2 = nextOnRow.get(row[row.length - 1]);
|
|
2101
2486
|
const cap2 = nextIdx2 != null ? lines[nextIdx2].x - first.x - first.fontSize * PT_TO_MM3 * 0.3 : pageWmm - first.x;
|
|
@@ -2106,7 +2491,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2106
2491
|
let text2 = r.text;
|
|
2107
2492
|
if (k > 0) {
|
|
2108
2493
|
const prev2 = lines[row[k - 1]];
|
|
2109
|
-
const gap = r.x - (prev2.x + prev2
|
|
2494
|
+
const gap = r.x - (prev2.x + extent(prev2));
|
|
2110
2495
|
if (gap > r.fontSize * PT_TO_MM3 * 0.08 && !/\s$/.test(prev2.text) && !/^\s/.test(text2)) text2 = " " + text2;
|
|
2111
2496
|
}
|
|
2112
2497
|
const run = { text: text2 };
|
|
@@ -2138,12 +2523,12 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2138
2523
|
fontSize: Math.round(l.fontSize * 10) / 10,
|
|
2139
2524
|
fontFamily: cls.family
|
|
2140
2525
|
};
|
|
2141
|
-
if (cls.weight === "bold") style.fontWeight = "bold";
|
|
2526
|
+
if (cls.weight === "bold" || l.bold) style.fontWeight = "bold";
|
|
2142
2527
|
if (cls.style === "italic") style.fontStyle = "italic";
|
|
2143
2528
|
if (l.color !== "#000000") style.color = l.color;
|
|
2144
2529
|
if (l.opacity < 0.999) style.opacity = Math.round(l.opacity * 100) / 100;
|
|
2145
2530
|
const link = findLinkForRun2(l);
|
|
2146
|
-
const measured = Math.max(l
|
|
2531
|
+
const measured = Math.max(extent(l) * 1.2 + l.fontSize * PT_TO_MM3 * 0.4, l.fontSize * PT_TO_MM3);
|
|
2147
2532
|
const remaining = Math.max(measured, pageWmm - l.x);
|
|
2148
2533
|
const nextIdx = nextOnRow.get(lineIdx);
|
|
2149
2534
|
const cap = nextIdx != null ? Math.max(l.fontSize * PT_TO_MM3, lines[nextIdx].x - l.x - l.fontSize * PT_TO_MM3 * 0.3) : Infinity;
|
|
@@ -2155,7 +2540,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2155
2540
|
width: Math.max(2, Math.round(elWidth * 100) / 100),
|
|
2156
2541
|
style
|
|
2157
2542
|
};
|
|
2158
|
-
if (cls.weight === "bold" && l.text.trim().length <= 120 && !consumedLines.has(lineIdx)) {
|
|
2543
|
+
if ((cls.weight === "bold" || l.bold) && l.text.trim().length <= 120 && !consumedLines.has(lineIdx)) {
|
|
2159
2544
|
const ratio = bodyFontSize > 0 ? l.fontSize / bodyFontSize : 1;
|
|
2160
2545
|
if (l.fontSize >= 22 || ratio >= 1.8) text.heading = 1;
|
|
2161
2546
|
else if (l.fontSize >= 17 || ratio >= 1.35) text.heading = 2;
|
|
@@ -2173,7 +2558,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2173
2558
|
prev.width = Math.max(prev.width ?? 0, text.width ?? 0);
|
|
2174
2559
|
return;
|
|
2175
2560
|
}
|
|
2176
|
-
lineMeta.set(text, { w: Math.max(l.fontSize * PT_TO_MM3, l
|
|
2561
|
+
lineMeta.set(text, { w: Math.max(l.fontSize * PT_TO_MM3, extent(l)), size: l.fontSize, face: fontKey(l.fontName) });
|
|
2177
2562
|
elements.push(text);
|
|
2178
2563
|
});
|
|
2179
2564
|
if (options.readingOrder !== false) {
|
|
@@ -2781,10 +3166,11 @@ function serializeElement(el) {
|
|
|
2781
3166
|
(row) => row.map((c, i) => {
|
|
2782
3167
|
const h = headers[i];
|
|
2783
3168
|
const v = cell(c).trim();
|
|
3169
|
+
if (!v) return "";
|
|
2784
3170
|
return h ? `${h}: ${v}` : v;
|
|
2785
3171
|
}).filter((s) => s !== "").join(" | ")
|
|
2786
3172
|
);
|
|
2787
|
-
return lines.join("\n");
|
|
3173
|
+
return lines.filter((l) => l !== "").join("\n");
|
|
2788
3174
|
}
|
|
2789
3175
|
case "collapsible": {
|
|
2790
3176
|
const title = String(e.title ?? "").trim();
|