@uurtech/jdf-cli 0.2.2 → 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 +728 -160
- package/dist/jdf-schema.json +1 -0
- 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,179 +685,658 @@ 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;
|
|
710
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);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
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++;
|
|
711
807
|
}
|
|
712
|
-
|
|
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
|
|
715
|
-
|
|
822
|
+
var dbg = (...a) => {
|
|
823
|
+
if (typeof process !== "undefined" && process.env?.JDF_TABLE_DEBUG) console.error("[tables]", ...a);
|
|
824
|
+
};
|
|
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
|
-
|
|
765
|
-
|
|
918
|
+
if (gutters.length && !hasLattice) {
|
|
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));
|
|
921
|
+
if (straddles) {
|
|
922
|
+
dbg("reject: straddles a column gutter");
|
|
923
|
+
i++;
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
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 ?? []];
|
|
766
974
|
const grid = [];
|
|
975
|
+
const gridRows = [];
|
|
767
976
|
const lineIdx = [];
|
|
768
|
-
|
|
769
|
-
|
|
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) {
|
|
770
993
|
const c = row.cells[0];
|
|
771
|
-
const b = bands.findIndex((bb) => c
|
|
994
|
+
const b = assign.get(c) ?? bands.findIndex((bb) => overlaps(c, bb));
|
|
772
995
|
if (b > 0) {
|
|
996
|
+
flushPending();
|
|
773
997
|
grid[grid.length - 1][b] = (grid[grid.length - 1][b] + " " + c.run.text.trim()).trim();
|
|
774
|
-
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));
|
|
775
1010
|
continue;
|
|
776
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);
|
|
777
1024
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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;
|
|
782
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();
|
|
783
1050
|
}
|
|
784
1051
|
if (lineIdx.some((k) => used.has(k))) {
|
|
785
1052
|
i = best.j + 1;
|
|
786
1053
|
continue;
|
|
787
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
|
+
}
|
|
788
1069
|
const first = blockRows[0];
|
|
789
|
-
const tableW = bbox.x1 - bbox.x0;
|
|
790
|
-
const rowFill = (row) => {
|
|
791
|
-
const cy = row.y + row.h * 0.5;
|
|
792
|
-
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);
|
|
793
|
-
const covered = rects.reduce((a, { s }) => a + s.width, 0);
|
|
794
|
-
if (!rects.length || covered < tableW * 0.5) return null;
|
|
795
|
-
const counts = /* @__PURE__ */ new Map();
|
|
796
|
-
for (const { s } of rects) counts.set(s.fill, (counts.get(s.fill) ?? 0) + s.width);
|
|
797
|
-
const fill = [...counts.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
798
|
-
return { fill, rects };
|
|
799
|
-
};
|
|
800
1070
|
const headerBg = rowFill(first);
|
|
801
1071
|
const firstBold = first.cells.every((c) => c.run.bold);
|
|
802
1072
|
const bodyFills = blockRows.slice(1).map(rowFill);
|
|
803
1073
|
const headerDistinct = !!headerBg && !bodyFills.every((f) => f?.fill === headerBg.fill);
|
|
804
|
-
|
|
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;
|
|
805
1102
|
const columns = bands.map((b, k) => {
|
|
806
|
-
const vals = grid.slice(isHeader ? 1 : 0).map((r) => r[k]).filter(Boolean);
|
|
807
|
-
const numericShare = vals.length ? vals.filter(numeric).length / vals.length : 0;
|
|
808
1103
|
const col = { width: Math.round((b.x1 - b.x0) * 10) / 10 };
|
|
809
|
-
if (
|
|
1104
|
+
if (isNumCol[k]) col.align = "right";
|
|
810
1105
|
return col;
|
|
811
1106
|
});
|
|
812
1107
|
const x0 = Math.max(0, bands[0].x0 - 2.5);
|
|
813
1108
|
const x1 = Math.min(pageW, bands[bands.length - 1].x1 + 2.5);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
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);
|
|
820
1115
|
const odd = rowFills.filter((_, k) => k % 2 === 1), even = rowFills.filter((_, k) => k % 2 === 0);
|
|
821
|
-
const
|
|
822
|
-
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));
|
|
823
1120
|
const borderColor = borderShape ? borderShape.s.stroke || borderShape.s.fill : void 0;
|
|
824
|
-
const
|
|
825
|
-
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;
|
|
826
1132
|
const element = {
|
|
827
1133
|
type: "table",
|
|
828
1134
|
position: { x: Math.round(x0 * 100) / 100, y: Math.round(Math.max(0, y0) * 100) / 100 },
|
|
829
1135
|
width: Math.round((x1 - x0) * 100) / 100,
|
|
830
1136
|
columns,
|
|
831
|
-
rows:
|
|
832
|
-
|
|
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` }
|
|
833
1143
|
};
|
|
834
|
-
if (
|
|
835
|
-
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());
|
|
836
1146
|
const hs = { fontWeight: "bold" };
|
|
837
1147
|
if (headerBg) hs.backgroundColor = headerBg.fill;
|
|
838
|
-
|
|
839
|
-
|
|
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
|
+
}
|
|
840
1154
|
element.headerStyle = hs;
|
|
841
1155
|
}
|
|
842
|
-
if (
|
|
843
|
-
|
|
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]);
|
|
844
1174
|
for (const k of lineIdx) used.add(k);
|
|
845
|
-
out.push({ element, lineIdx, shapeIdx:
|
|
1175
|
+
out.push({ element, lineIdx, shapeIdx: consumedShapes });
|
|
846
1176
|
i = best.j + 1;
|
|
847
1177
|
}
|
|
848
1178
|
return out;
|
|
849
1179
|
}
|
|
850
1180
|
|
|
851
|
-
// ../../packages/jdf-pdf-import/src/
|
|
1181
|
+
// ../../packages/jdf-pdf-import/src/columns.ts
|
|
1182
|
+
function detectGutters(lines, bodyFontSize, pageWmm) {
|
|
1183
|
+
const body = lines.filter((l) => l.text.trim().length > 0 && Math.abs(l.fontSize - bodyFontSize) <= 1.5 && l.width < pageWmm * 0.6 && l.width > 0);
|
|
1184
|
+
if (body.length < 12) return [];
|
|
1185
|
+
const minX = Math.min(...body.map((l) => l.x));
|
|
1186
|
+
const maxX = Math.max(...body.map((l) => l.x + l.width));
|
|
1187
|
+
const span = maxX - minX;
|
|
1188
|
+
if (span < pageWmm * 0.4) return [];
|
|
1189
|
+
const BIN = 0.5;
|
|
1190
|
+
const n = Math.ceil(span / BIN) + 1;
|
|
1191
|
+
const cov = new Array(n).fill(0);
|
|
1192
|
+
for (const l of body) {
|
|
1193
|
+
const a = Math.max(0, Math.floor((l.x - minX) / BIN)), b = Math.min(n, Math.ceil((l.x + l.width - minX) / BIN));
|
|
1194
|
+
for (let i2 = a; i2 < b; i2++) cov[i2]++;
|
|
1195
|
+
}
|
|
1196
|
+
const noise = Math.max(2, Math.round(body.length * 0.05));
|
|
1197
|
+
const gutters = [];
|
|
1198
|
+
let i = 0;
|
|
1199
|
+
while (i < n) {
|
|
1200
|
+
if (cov[i] > noise) {
|
|
1201
|
+
i++;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
let j = i;
|
|
1205
|
+
while (j < n && cov[j] <= noise) j++;
|
|
1206
|
+
const g0 = minX + i * BIN, g1 = minX + j * BIN;
|
|
1207
|
+
if (g1 - g0 >= 3 && g0 > minX + span * 0.15 && g1 < maxX - span * 0.15) gutters.push({ x0: g0, x1: g1 });
|
|
1208
|
+
i = j;
|
|
1209
|
+
}
|
|
1210
|
+
const solid = gutters.every((g) => body.filter((l) => l.x + l.width <= g.x0 + 0.5).length >= 4 && body.filter((l) => l.x >= g.x1 - 0.5).length >= 4);
|
|
1211
|
+
return solid ? gutters : [];
|
|
1212
|
+
}
|
|
1213
|
+
function orderByColumns(els, gutters, pageWmm) {
|
|
1214
|
+
if (!gutters.length || els.length < 2) return els;
|
|
1215
|
+
const bands = [];
|
|
1216
|
+
let left = 0;
|
|
1217
|
+
for (const g of gutters) {
|
|
1218
|
+
bands.push({ x0: left, x1: g.x0 });
|
|
1219
|
+
left = g.x1;
|
|
1220
|
+
}
|
|
1221
|
+
bands.push({ x0: left, x1: pageWmm });
|
|
1222
|
+
const colOf = (e) => {
|
|
1223
|
+
const x = e.position?.x ?? 0;
|
|
1224
|
+
const k = bands.findIndex((b) => x >= b.x0 && x < b.x1);
|
|
1225
|
+
if (k >= 0) return k;
|
|
1226
|
+
let best = 0, d = Infinity;
|
|
1227
|
+
bands.forEach((b, i) => {
|
|
1228
|
+
const dd = Math.min(Math.abs(x - b.x0), Math.abs(x - b.x1));
|
|
1229
|
+
if (dd < d) {
|
|
1230
|
+
d = dd;
|
|
1231
|
+
best = i;
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
return best;
|
|
1235
|
+
};
|
|
1236
|
+
const spans = (e) => {
|
|
1237
|
+
const c = colOf(e);
|
|
1238
|
+
if (c >= bands.length - 1) return false;
|
|
1239
|
+
const right = (e.position?.x ?? 0) + (e.width ?? 0);
|
|
1240
|
+
const next = bands[c + 1];
|
|
1241
|
+
return right > (next.x0 + next.x1) / 2;
|
|
1242
|
+
};
|
|
1243
|
+
const sorted = els.slice().sort((a, b) => (a.position?.y ?? 0) - (b.position?.y ?? 0) || (a.position?.x ?? 0) - (b.position?.x ?? 0));
|
|
1244
|
+
const out = [];
|
|
1245
|
+
let band = [];
|
|
1246
|
+
const flush = () => {
|
|
1247
|
+
for (let c = 0; c < bands.length; c++) for (const e of band) if (colOf(e) === c) out.push(e);
|
|
1248
|
+
band = [];
|
|
1249
|
+
};
|
|
1250
|
+
for (const e of sorted) {
|
|
1251
|
+
if (spans(e)) {
|
|
1252
|
+
flush();
|
|
1253
|
+
out.push(e);
|
|
1254
|
+
} else band.push(e);
|
|
1255
|
+
}
|
|
1256
|
+
flush();
|
|
1257
|
+
return out;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ../../packages/jdf-pdf-import/src/paragraphs.ts
|
|
852
1261
|
var PT_TO_MM2 = 0.352778;
|
|
1262
|
+
var LIST_START = /^\s*(?:[•◦▪●■\-–—*]\s|\(?\d{1,3}[.)]\s|\(?[a-zA-Z][.)]\s|[ivx]{1,4}[.)]\s)/;
|
|
1263
|
+
var TERMINAL = /[.!?:;]["'”’)\]]*$/;
|
|
1264
|
+
function textOf(e) {
|
|
1265
|
+
if (e.type === "text") return String(e.content ?? "");
|
|
1266
|
+
return (e.runs ?? []).map((r) => String(r.text ?? "")).join("");
|
|
1267
|
+
}
|
|
1268
|
+
function foldParagraphs(elements, meta, pageWmm) {
|
|
1269
|
+
const foldable = (e) => !!meta.get(e) && !!e.position && (e.type === "text" && !e.heading || e.type === "richtext");
|
|
1270
|
+
const out = [];
|
|
1271
|
+
let i = 0;
|
|
1272
|
+
while (i < elements.length) {
|
|
1273
|
+
const first = elements[i];
|
|
1274
|
+
if (!foldable(first)) {
|
|
1275
|
+
out.push(first);
|
|
1276
|
+
i++;
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const para = [first];
|
|
1280
|
+
let j = i + 1;
|
|
1281
|
+
while (j < elements.length) {
|
|
1282
|
+
const prev = para[para.length - 1], next = elements[j];
|
|
1283
|
+
if (!foldable(next) || !continues(para, prev, next, meta)) break;
|
|
1284
|
+
para.push(next);
|
|
1285
|
+
j++;
|
|
1286
|
+
}
|
|
1287
|
+
out.push(para.length > 1 ? merge(para, meta, pageWmm) : first);
|
|
1288
|
+
i = j;
|
|
1289
|
+
}
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
function continues(para, prev, next, meta) {
|
|
1293
|
+
const mp = meta.get(prev), mn = meta.get(next); meta.get(para[0]);
|
|
1294
|
+
if (mp.face !== mn.face || Math.abs(mp.size - mn.size) >= 0.5) return false;
|
|
1295
|
+
const lineH = mp.size * PT_TO_MM2;
|
|
1296
|
+
const pitch = next.position.y - prev.position.y;
|
|
1297
|
+
if (pitch < lineH * 0.9 || pitch > lineH * 1.75) return false;
|
|
1298
|
+
const dx = next.position.x - (para.length === 1 ? prev.position.x : para[0].position.x);
|
|
1299
|
+
if (para.length === 1 ? Math.abs(dx) > 6 : Math.abs(dx) > 1.2) return false;
|
|
1300
|
+
if (para.length >= 2 && Math.abs(next.position.x - para[1].position.x) > 1.2) return false;
|
|
1301
|
+
const nText = textOf(next), pText = textOf(prev).trimEnd();
|
|
1302
|
+
if (LIST_START.test(nText) || !nText.trim()) return false;
|
|
1303
|
+
const maxW = Math.max(mp.w, mn.w, ...para.map((e) => meta.get(e).w));
|
|
1304
|
+
const full = mp.w >= maxW * 0.85;
|
|
1305
|
+
const endsHyphen = /[-‐‑]$/.test(pText);
|
|
1306
|
+
const softEnd = !TERMINAL.test(pText);
|
|
1307
|
+
const startsLower = /^[a-z(\[]/.test(nText.trimStart());
|
|
1308
|
+
return full || endsHyphen || softEnd && (startsLower || mp.w >= maxW * 0.6);
|
|
1309
|
+
}
|
|
1310
|
+
function merge(para, meta, pageWmm) {
|
|
1311
|
+
const first = para[0], last = para[para.length - 1];
|
|
1312
|
+
const m0 = meta.get(first);
|
|
1313
|
+
const lineH = m0.size * PT_TO_MM2;
|
|
1314
|
+
const x = Math.min(...para.map((e) => e.position.x));
|
|
1315
|
+
const indent = first.position.x - x;
|
|
1316
|
+
const right = Math.max(...para.map((e) => e.position.x + meta.get(e).w));
|
|
1317
|
+
const pitch = (last.position.y - first.position.y) / (para.length - 1);
|
|
1318
|
+
const width = Math.min(pageWmm - x, (right - x) * 1.2 + lineH * 0.4);
|
|
1319
|
+
const joiner = () => "\n";
|
|
1320
|
+
const style = { ...first.style ?? {}, lineHeight: Math.round(pitch / lineH * 100) / 100, ...indent > 0.5 ? { textIndent: Math.round(indent * 100) / 100 } : {} };
|
|
1321
|
+
const allText = para.every((e) => e.type === "text" && !e.link);
|
|
1322
|
+
const base = { position: { x, y: first.position.y }, width: Math.round(width * 100) / 100, style };
|
|
1323
|
+
if (allText) {
|
|
1324
|
+
const content = para.map((e) => String(e.content ?? "").trim().replace(/[ \t]+/g, " ")).join(joiner());
|
|
1325
|
+
const { height: _hh2, ...firstRest } = first;
|
|
1326
|
+
return { ...firstRest, ...base, content };
|
|
1327
|
+
}
|
|
1328
|
+
const runs = [];
|
|
1329
|
+
for (const e of para) {
|
|
1330
|
+
const lineRuns = e.type === "text" ? [{ text: String(e.content ?? "").trim(), ...e.style?.fontWeight === "bold" ? { bold: true } : {}, ...e.style?.fontStyle === "italic" ? { italic: true } : {}, ...e.style?.color && e.style.color !== "#000000" ? { color: e.style.color } : {}, ...e.link ? { link: e.link } : {} }] : (e.runs ?? []).map((r) => ({ ...r }));
|
|
1331
|
+
if (runs.length) runs[runs.length - 1].text = `${String(runs[runs.length - 1].text).trimEnd()}${joiner()}`;
|
|
1332
|
+
runs.push(...lineRuns);
|
|
1333
|
+
}
|
|
1334
|
+
const { content: _c, heading: _h, link: _l, height: _hh, ...rest } = first;
|
|
1335
|
+
return { ...rest, ...base, type: "richtext", runs, style: { fontSize: style.fontSize, fontFamily: style.fontFamily, lineHeight: style.lineHeight, ...style.textIndent ? { textIndent: style.textIndent } : {}, ...style.opacity != null ? { opacity: style.opacity } : {} } };
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ../../packages/jdf-pdf-import/src/core.ts
|
|
1339
|
+
var PT_TO_MM3 = 0.352778;
|
|
853
1340
|
function classifyFont(name) {
|
|
854
1341
|
const n = (name || "").toLowerCase();
|
|
855
1342
|
const bold = /bold|black|heavy|semibold|demibold|extrabold/.test(n);
|
|
@@ -875,6 +1362,47 @@ function rgbToHex(r, g, b) {
|
|
|
875
1362
|
const h = (n) => clampByte(n).toString(16).padStart(2, "0");
|
|
876
1363
|
return `#${h(r)}${h(g)}${h(b)}`;
|
|
877
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
|
+
}
|
|
878
1406
|
function averageStops(stops) {
|
|
879
1407
|
if (!Array.isArray(stops) || stops.length === 0) return null;
|
|
880
1408
|
let r = 0, g = 0, b = 0, n = 0;
|
|
@@ -959,10 +1487,10 @@ async function walkOps(page, OPS, viewport) {
|
|
|
959
1487
|
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
|
960
1488
|
imagePositions.push({
|
|
961
1489
|
name,
|
|
962
|
-
x: minX *
|
|
963
|
-
y: minY *
|
|
964
|
-
w: (maxX - minX) *
|
|
965
|
-
h: (maxY - minY) *
|
|
1490
|
+
x: minX * PT_TO_MM3,
|
|
1491
|
+
y: minY * PT_TO_MM3,
|
|
1492
|
+
w: (maxX - minX) * PT_TO_MM3,
|
|
1493
|
+
h: (maxY - minY) * PT_TO_MM3,
|
|
966
1494
|
inline,
|
|
967
1495
|
maskFill
|
|
968
1496
|
});
|
|
@@ -978,13 +1506,13 @@ async function walkOps(page, OPS, viewport) {
|
|
|
978
1506
|
const br = toViewport(r.x + r.w, r.y);
|
|
979
1507
|
shapes.push({
|
|
980
1508
|
kind: "rect",
|
|
981
|
-
x: Math.min(tl.x, br.x) *
|
|
982
|
-
y: Math.min(tl.y, br.y) *
|
|
983
|
-
width: Math.abs(br.x - tl.x) *
|
|
984
|
-
height: Math.abs(br.y - tl.y) *
|
|
1509
|
+
x: Math.min(tl.x, br.x) * PT_TO_MM3,
|
|
1510
|
+
y: Math.min(tl.y, br.y) * PT_TO_MM3,
|
|
1511
|
+
width: Math.abs(br.x - tl.x) * PT_TO_MM3,
|
|
1512
|
+
height: Math.abs(br.y - tl.y) * PT_TO_MM3,
|
|
985
1513
|
fill: isFill ? gs.fill : void 0,
|
|
986
1514
|
stroke: isStroke ? gs.stroke : void 0,
|
|
987
|
-
strokeWidth: isStroke ? gs.lineWidth *
|
|
1515
|
+
strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
|
|
988
1516
|
opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
|
|
989
1517
|
});
|
|
990
1518
|
}
|
|
@@ -998,13 +1526,13 @@ async function walkOps(page, OPS, viewport) {
|
|
|
998
1526
|
const h = Math.abs(br.y - tl.y);
|
|
999
1527
|
shapes.push({
|
|
1000
1528
|
kind: "rect",
|
|
1001
|
-
x: x *
|
|
1002
|
-
y: y *
|
|
1003
|
-
width: w *
|
|
1004
|
-
height: h *
|
|
1529
|
+
x: x * PT_TO_MM3,
|
|
1530
|
+
y: y * PT_TO_MM3,
|
|
1531
|
+
width: w * PT_TO_MM3,
|
|
1532
|
+
height: h * PT_TO_MM3,
|
|
1005
1533
|
fill: isFill ? gs.fill : void 0,
|
|
1006
1534
|
stroke: isStroke ? gs.stroke : void 0,
|
|
1007
|
-
strokeWidth: isStroke ? gs.lineWidth *
|
|
1535
|
+
strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
|
|
1008
1536
|
opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
|
|
1009
1537
|
});
|
|
1010
1538
|
} else if (pathSegments.length === 2 && pathSegments[0].type === "M" && pathSegments[1].type === "L") {
|
|
@@ -1016,35 +1544,35 @@ async function walkOps(page, OPS, viewport) {
|
|
|
1016
1544
|
const minY = Math.min(va.y, vb.y);
|
|
1017
1545
|
const maxX = Math.max(va.x, vb.x);
|
|
1018
1546
|
const maxY = Math.max(va.y, vb.y);
|
|
1019
|
-
const x1Local = (va.x - minX) *
|
|
1020
|
-
const y1Local = (va.y - minY) *
|
|
1021
|
-
const x2Local = (vb.x - minX) *
|
|
1022
|
-
const y2Local = (vb.y - minY) *
|
|
1023
|
-
const wLocal = Math.max(0.05, (maxX - minX) *
|
|
1024
|
-
const hLocal = Math.max(0.05, (maxY - minY) *
|
|
1547
|
+
const x1Local = (va.x - minX) * PT_TO_MM3;
|
|
1548
|
+
const y1Local = (va.y - minY) * PT_TO_MM3;
|
|
1549
|
+
const x2Local = (vb.x - minX) * PT_TO_MM3;
|
|
1550
|
+
const y2Local = (vb.y - minY) * PT_TO_MM3;
|
|
1551
|
+
const wLocal = Math.max(0.05, (maxX - minX) * PT_TO_MM3);
|
|
1552
|
+
const hLocal = Math.max(0.05, (maxY - minY) * PT_TO_MM3);
|
|
1025
1553
|
const dx = Math.abs(va.x - vb.x);
|
|
1026
1554
|
const dy = Math.abs(va.y - vb.y);
|
|
1027
1555
|
const axisAligned = dx < 0.5 || dy < 0.5;
|
|
1028
1556
|
if (axisAligned) {
|
|
1029
1557
|
shapes.push({
|
|
1030
1558
|
kind: "line",
|
|
1031
|
-
x: minX *
|
|
1032
|
-
y: minY *
|
|
1559
|
+
x: minX * PT_TO_MM3,
|
|
1560
|
+
y: minY * PT_TO_MM3,
|
|
1033
1561
|
width: wLocal,
|
|
1034
1562
|
height: hLocal,
|
|
1035
1563
|
stroke: isStroke ? gs.stroke : void 0,
|
|
1036
|
-
strokeWidth: isStroke ? gs.lineWidth *
|
|
1564
|
+
strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
|
|
1037
1565
|
opacity: gs.strokeAlpha
|
|
1038
1566
|
});
|
|
1039
1567
|
} else {
|
|
1040
1568
|
shapes.push({
|
|
1041
1569
|
kind: "path",
|
|
1042
|
-
x: minX *
|
|
1043
|
-
y: minY *
|
|
1570
|
+
x: minX * PT_TO_MM3,
|
|
1571
|
+
y: minY * PT_TO_MM3,
|
|
1044
1572
|
width: wLocal,
|
|
1045
1573
|
height: hLocal,
|
|
1046
1574
|
stroke: isStroke ? gs.stroke : void 0,
|
|
1047
|
-
strokeWidth: isStroke ? gs.lineWidth *
|
|
1575
|
+
strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
|
|
1048
1576
|
opacity: gs.strokeAlpha,
|
|
1049
1577
|
path: `M ${x1Local.toFixed(2)} ${y1Local.toFixed(2)} L ${x2Local.toFixed(2)} ${y2Local.toFixed(2)}`
|
|
1050
1578
|
});
|
|
@@ -1077,20 +1605,20 @@ async function walkOps(page, OPS, viewport) {
|
|
|
1077
1605
|
if (seg.type === "Z") return "Z";
|
|
1078
1606
|
const p = [];
|
|
1079
1607
|
for (let i = 0; i < seg.pts.length; i += 2) {
|
|
1080
|
-
p.push(((seg.pts[i] - minX) *
|
|
1081
|
-
p.push(((seg.pts[i + 1] - minY) *
|
|
1608
|
+
p.push(((seg.pts[i] - minX) * PT_TO_MM3).toFixed(2));
|
|
1609
|
+
p.push(((seg.pts[i + 1] - minY) * PT_TO_MM3).toFixed(2));
|
|
1082
1610
|
}
|
|
1083
1611
|
return `${seg.type} ${p.join(" ")}`;
|
|
1084
1612
|
}).join(" ");
|
|
1085
1613
|
shapes.push({
|
|
1086
1614
|
kind: "path",
|
|
1087
|
-
x: minX *
|
|
1088
|
-
y: minY *
|
|
1089
|
-
width: bw *
|
|
1090
|
-
height: bh *
|
|
1615
|
+
x: minX * PT_TO_MM3,
|
|
1616
|
+
y: minY * PT_TO_MM3,
|
|
1617
|
+
width: bw * PT_TO_MM3,
|
|
1618
|
+
height: bh * PT_TO_MM3,
|
|
1091
1619
|
fill: isFill ? gs.fill : void 0,
|
|
1092
1620
|
stroke: isStroke ? gs.stroke : void 0,
|
|
1093
|
-
strokeWidth: isStroke ? gs.lineWidth *
|
|
1621
|
+
strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
|
|
1094
1622
|
opacity: isFill ? gs.fillAlpha : gs.strokeAlpha,
|
|
1095
1623
|
path: d
|
|
1096
1624
|
});
|
|
@@ -1431,10 +1959,10 @@ async function extractLinks(doc, page, viewport) {
|
|
|
1431
1959
|
const xMax = Math.max(c1.x, c2.x);
|
|
1432
1960
|
const yMax = Math.max(c1.y, c2.y);
|
|
1433
1961
|
const rectMm = {
|
|
1434
|
-
x: xMin *
|
|
1435
|
-
y: yMin *
|
|
1436
|
-
w: (xMax - xMin) *
|
|
1437
|
-
h: (yMax - yMin) *
|
|
1962
|
+
x: xMin * PT_TO_MM3,
|
|
1963
|
+
y: yMin * PT_TO_MM3,
|
|
1964
|
+
w: (xMax - xMin) * PT_TO_MM3,
|
|
1965
|
+
h: (yMax - yMin) * PT_TO_MM3
|
|
1438
1966
|
};
|
|
1439
1967
|
const url = a.url || a.unsafeUrl;
|
|
1440
1968
|
const destPage = url ? void 0 : await resolveDestPage(doc, a.dest);
|
|
@@ -1472,10 +2000,10 @@ async function extractFormWidgets(page, viewport) {
|
|
|
1472
2000
|
})).filter((o) => o.value !== "") : [];
|
|
1473
2001
|
out.push({
|
|
1474
2002
|
rectMm: {
|
|
1475
|
-
x: xMin *
|
|
1476
|
-
y: yMin *
|
|
1477
|
-
w: (xMax - xMin) *
|
|
1478
|
-
h: (yMax - yMin) *
|
|
2003
|
+
x: xMin * PT_TO_MM3,
|
|
2004
|
+
y: yMin * PT_TO_MM3,
|
|
2005
|
+
w: (xMax - xMin) * PT_TO_MM3,
|
|
2006
|
+
h: (yMax - yMin) * PT_TO_MM3
|
|
1479
2007
|
},
|
|
1480
2008
|
fieldType: a.fieldType || "",
|
|
1481
2009
|
fieldName: a.fieldName || `field-${out.length + 1}`,
|
|
@@ -1764,12 +2292,12 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1764
2292
|
const w = safeNum(it.width, 0);
|
|
1765
2293
|
runs.push({
|
|
1766
2294
|
text: it.str,
|
|
1767
|
-
x: safeNum(vx *
|
|
1768
|
-
y: safeNum(yTop *
|
|
2295
|
+
x: safeNum(vx * PT_TO_MM3, 0),
|
|
2296
|
+
y: safeNum(yTop * PT_TO_MM3, 0),
|
|
1769
2297
|
fontSize: safeNum(fontSize, 10),
|
|
1770
2298
|
fontName: it.fontName,
|
|
1771
|
-
width: safeNum(w *
|
|
1772
|
-
height: safeNum((it.height || fontSize) *
|
|
2299
|
+
width: safeNum(w * PT_TO_MM3, 0),
|
|
2300
|
+
height: safeNum((it.height || fontSize) * PT_TO_MM3, fontSize * PT_TO_MM3),
|
|
1773
2301
|
color: op?.fill || "#000000",
|
|
1774
2302
|
opacity: invisible ? 0 : safeNum(op?.alpha, 1)
|
|
1775
2303
|
});
|
|
@@ -1783,8 +2311,20 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1783
2311
|
const c = fontMap.get(name) || classifyFont(name || "");
|
|
1784
2312
|
return `${c.family}|${c.weight || ""}|${c.style || ""}`;
|
|
1785
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;
|
|
1786
2325
|
for (const r of runs) {
|
|
1787
2326
|
if (!r.text.length) continue;
|
|
2327
|
+
if (!r.text.trim() && r.width > r.fontSize * PT_TO_MM3 * gapEm) continue;
|
|
1788
2328
|
const last = lines[lines.length - 1];
|
|
1789
2329
|
if (!last) {
|
|
1790
2330
|
lines.push({ ...r });
|
|
@@ -1792,16 +2332,10 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1792
2332
|
}
|
|
1793
2333
|
const sameLine = Math.abs(last.y - r.y) <= Y_TOL;
|
|
1794
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;
|
|
1795
|
-
const emMm = r.fontSize *
|
|
1796
|
-
const extent = (t) => {
|
|
1797
|
-
if (!/\s$/.test(t.text)) return t.width;
|
|
1798
|
-
const em = t.fontSize * PT_TO_MM2;
|
|
1799
|
-
const est = Math.max(1, t.text.trim().length) * em * kGlyph + em * 0.25;
|
|
1800
|
-
if (stretchedSpaces) return Math.min(t.width, est);
|
|
1801
|
-
return t.width > est * 1.4 ? est : t.width;
|
|
1802
|
-
};
|
|
2335
|
+
const emMm = r.fontSize * PT_TO_MM3;
|
|
1803
2336
|
const gapMm = r.x - (last.x + extent(last));
|
|
1804
|
-
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;
|
|
1805
2339
|
if (mergeOk) {
|
|
1806
2340
|
const lastEndsSpace = /\s$/.test(last.text);
|
|
1807
2341
|
const currStartsSpace = /^\s/.test(r.text);
|
|
@@ -1813,12 +2347,36 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1813
2347
|
lines.push({ ...r });
|
|
1814
2348
|
}
|
|
1815
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
|
+
}
|
|
1816
2365
|
const elements = [];
|
|
2366
|
+
const lineMeta = /* @__PURE__ */ new WeakMap();
|
|
1817
2367
|
const tRuns = lines.map((l) => {
|
|
1818
2368
|
const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
|
|
1819
|
-
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 };
|
|
1820
2370
|
});
|
|
1821
|
-
const
|
|
2371
|
+
const sizeChars = /* @__PURE__ */ new Map();
|
|
2372
|
+
for (const l of lines) {
|
|
2373
|
+
const k = Math.round(l.fontSize * 2) / 2;
|
|
2374
|
+
sizeChars.set(k, (sizeChars.get(k) ?? 0) + l.text.length);
|
|
2375
|
+
}
|
|
2376
|
+
const bodyFontSize = [...sizeChars.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 0;
|
|
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);
|
|
2378
|
+
const pageShapes = mergeCollinearShapes(ops.shapes);
|
|
2379
|
+
const detected = options.detectTables === false ? [] : detectTables(tRuns, pageShapes, pageW * PT_TO_MM3, gutters);
|
|
1822
2380
|
const consumedLines = /* @__PURE__ */ new Set();
|
|
1823
2381
|
const consumedShapes = /* @__PURE__ */ new Set();
|
|
1824
2382
|
const tableAtLine = /* @__PURE__ */ new Map();
|
|
@@ -1827,8 +2385,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1827
2385
|
for (const k of t.shapeIdx) consumedShapes.add(k);
|
|
1828
2386
|
tableAtLine.set(Math.min(...t.lineIdx), t.element);
|
|
1829
2387
|
}
|
|
1830
|
-
const pageWmm = pageW *
|
|
1831
|
-
|
|
2388
|
+
const pageWmm = pageW * PT_TO_MM3, pageHmm = pageH * PT_TO_MM3;
|
|
2389
|
+
pageShapes.forEach((sh, shapeIdx) => {
|
|
1832
2390
|
if (consumedShapes.has(shapeIdx)) return;
|
|
1833
2391
|
if (sh.width < 0.3 && sh.height < 0.3) return;
|
|
1834
2392
|
if (sh.x + sh.width <= 0 || sh.y + sh.height <= 0 || sh.x >= pageWmm || sh.y >= pageHmm) return;
|
|
@@ -1871,12 +2429,6 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1871
2429
|
fit: "fill"
|
|
1872
2430
|
});
|
|
1873
2431
|
}
|
|
1874
|
-
const sizeChars = /* @__PURE__ */ new Map();
|
|
1875
|
-
for (const l of lines) {
|
|
1876
|
-
const k = Math.round(l.fontSize * 2) / 2;
|
|
1877
|
-
sizeChars.set(k, (sizeChars.get(k) ?? 0) + l.text.length);
|
|
1878
|
-
}
|
|
1879
|
-
const bodyFontSize = [...sizeChars.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 0;
|
|
1880
2432
|
const rowOf = /* @__PURE__ */ new Map();
|
|
1881
2433
|
const rowStartOf = /* @__PURE__ */ new Map();
|
|
1882
2434
|
const nextOnRow = /* @__PURE__ */ new Map();
|
|
@@ -1884,10 +2436,10 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1884
2436
|
const order = lines.map((_, i) => i).filter((i) => !consumedLines.has(i));
|
|
1885
2437
|
for (let a = 0; a < order.length; a++) {
|
|
1886
2438
|
const i = order[a], li = lines[i];
|
|
1887
|
-
const tolY = Math.max(0.6, li.fontSize * PT_TO_MM2 * 0.35);
|
|
1888
2439
|
let bestNext = -1, bestX = Infinity;
|
|
1889
2440
|
for (let b = 0; b < order.length; b++) {
|
|
1890
2441
|
const j = order[b], lj = lines[j];
|
|
2442
|
+
const tolY = Math.max(0.6, Math.max(li.fontSize, lj.fontSize) * PT_TO_MM3 * 0.5);
|
|
1891
2443
|
if (j === i || Math.abs(lj.y - li.y) > tolY || lj.x <= li.x) continue;
|
|
1892
2444
|
if (lj.x < bestX) {
|
|
1893
2445
|
bestX = lj.x;
|
|
@@ -1904,8 +2456,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1904
2456
|
let cur = i;
|
|
1905
2457
|
while (nextOnRow.has(cur)) {
|
|
1906
2458
|
const j = nextOnRow.get(cur), lc = lines[cur], lj = lines[j];
|
|
1907
|
-
const em = Math.min(lc.fontSize, lj.fontSize) *
|
|
1908
|
-
const gap = lj.x - (lc.x + lc
|
|
2459
|
+
const em = Math.min(lc.fontSize, lj.fontSize) * PT_TO_MM3;
|
|
2460
|
+
const gap = lj.x - (lc.x + extent(lc));
|
|
1909
2461
|
if (gap < -em * 0.3 || gap > em * 0.6) break;
|
|
1910
2462
|
row.push(j);
|
|
1911
2463
|
seen.add(j);
|
|
@@ -1917,7 +2469,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1917
2469
|
}
|
|
1918
2470
|
const runStyle = (l) => {
|
|
1919
2471
|
const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
|
|
1920
|
-
return { cls, bold: cls.weight === "bold", italic: cls.style === "italic" };
|
|
2472
|
+
return { cls, bold: cls.weight === "bold" || !!l.bold, italic: cls.style === "italic" };
|
|
1921
2473
|
};
|
|
1922
2474
|
lines.forEach((l, lineIdx) => {
|
|
1923
2475
|
const tableEl = tableAtLine.get(lineIdx);
|
|
@@ -1928,10 +2480,10 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1928
2480
|
if (row.length > 1) {
|
|
1929
2481
|
const first = lines[row[0]], last = lines[row[row.length - 1]];
|
|
1930
2482
|
const base = runStyle(first);
|
|
1931
|
-
const rowEnd = last.x + last
|
|
1932
|
-
const measuredW = Math.max((rowEnd - first.x) * 1.2 + first.fontSize *
|
|
2483
|
+
const rowEnd = last.x + extent(last);
|
|
2484
|
+
const measuredW = Math.max((rowEnd - first.x) * 1.2 + first.fontSize * PT_TO_MM3 * 0.4, first.fontSize * PT_TO_MM3);
|
|
1933
2485
|
const nextIdx2 = nextOnRow.get(row[row.length - 1]);
|
|
1934
|
-
const cap2 = nextIdx2 != null ? lines[nextIdx2].x - first.x - first.fontSize *
|
|
2486
|
+
const cap2 = nextIdx2 != null ? lines[nextIdx2].x - first.x - first.fontSize * PT_TO_MM3 * 0.3 : pageWmm - first.x;
|
|
1935
2487
|
const runs2 = [];
|
|
1936
2488
|
row.forEach((idx, k) => {
|
|
1937
2489
|
const r = lines[idx];
|
|
@@ -1939,8 +2491,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1939
2491
|
let text2 = r.text;
|
|
1940
2492
|
if (k > 0) {
|
|
1941
2493
|
const prev2 = lines[row[k - 1]];
|
|
1942
|
-
const gap = r.x - (prev2.x + prev2
|
|
1943
|
-
if (gap > r.fontSize *
|
|
2494
|
+
const gap = r.x - (prev2.x + extent(prev2));
|
|
2495
|
+
if (gap > r.fontSize * PT_TO_MM3 * 0.08 && !/\s$/.test(prev2.text) && !/^\s/.test(text2)) text2 = " " + text2;
|
|
1944
2496
|
}
|
|
1945
2497
|
const run = { text: text2 };
|
|
1946
2498
|
if (st.bold) run.bold = true;
|
|
@@ -1954,13 +2506,16 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1954
2506
|
});
|
|
1955
2507
|
const style2 = { fontSize: Math.round(first.fontSize * 10) / 10, fontFamily: base.cls.family };
|
|
1956
2508
|
if (first.opacity < 0.999) style2.opacity = Math.round(first.opacity * 100) / 100;
|
|
1957
|
-
|
|
2509
|
+
const rt = {
|
|
1958
2510
|
type: "richtext",
|
|
1959
2511
|
runs: runs2,
|
|
1960
2512
|
position: { x: Math.max(0, Math.round(first.x * 100) / 100), y: Math.max(0, Math.round(Math.min(...row.map((i) => lines[i].y)) * 100) / 100) },
|
|
1961
|
-
width: Math.max(2, Math.round(Math.max(first.fontSize *
|
|
2513
|
+
width: Math.max(2, Math.round(Math.max(first.fontSize * PT_TO_MM3, Math.min(measuredW, cap2)) * 100) / 100),
|
|
1962
2514
|
style: style2
|
|
1963
|
-
}
|
|
2515
|
+
};
|
|
2516
|
+
const dominant = row.map((i) => lines[i]).sort((a, b) => b.text.trim().length - a.text.trim().length)[0];
|
|
2517
|
+
lineMeta.set(rt, { w: Math.max(first.fontSize * PT_TO_MM3, rowEnd - first.x), size: dominant.fontSize, face: fontKey(dominant.fontName) });
|
|
2518
|
+
elements.push(rt);
|
|
1964
2519
|
return;
|
|
1965
2520
|
}
|
|
1966
2521
|
const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
|
|
@@ -1968,15 +2523,15 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1968
2523
|
fontSize: Math.round(l.fontSize * 10) / 10,
|
|
1969
2524
|
fontFamily: cls.family
|
|
1970
2525
|
};
|
|
1971
|
-
if (cls.weight === "bold") style.fontWeight = "bold";
|
|
2526
|
+
if (cls.weight === "bold" || l.bold) style.fontWeight = "bold";
|
|
1972
2527
|
if (cls.style === "italic") style.fontStyle = "italic";
|
|
1973
2528
|
if (l.color !== "#000000") style.color = l.color;
|
|
1974
2529
|
if (l.opacity < 0.999) style.opacity = Math.round(l.opacity * 100) / 100;
|
|
1975
2530
|
const link = findLinkForRun2(l);
|
|
1976
|
-
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);
|
|
1977
2532
|
const remaining = Math.max(measured, pageWmm - l.x);
|
|
1978
2533
|
const nextIdx = nextOnRow.get(lineIdx);
|
|
1979
|
-
const cap = nextIdx != null ? Math.max(l.fontSize *
|
|
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;
|
|
1980
2535
|
const elWidth = Math.min(measured, remaining, cap);
|
|
1981
2536
|
const text = {
|
|
1982
2537
|
type: "text",
|
|
@@ -1985,7 +2540,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1985
2540
|
width: Math.max(2, Math.round(elWidth * 100) / 100),
|
|
1986
2541
|
style
|
|
1987
2542
|
};
|
|
1988
|
-
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)) {
|
|
1989
2544
|
const ratio = bodyFontSize > 0 ? l.fontSize / bodyFontSize : 1;
|
|
1990
2545
|
if (l.fontSize >= 22 || ratio >= 1.8) text.heading = 1;
|
|
1991
2546
|
else if (l.fontSize >= 17 || ratio >= 1.35) text.heading = 2;
|
|
@@ -1997,14 +2552,26 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1997
2552
|
else if (link.destPage != null) text.link = { type: "internal", target: `#page-${link.destPage + 1}` };
|
|
1998
2553
|
}
|
|
1999
2554
|
const prev = elements[elements.length - 1];
|
|
2000
|
-
if (text.heading && prev && prev.type === "text" && prev.heading === text.heading && !link && !prev.link && Math.abs(prev.style?.fontSize - style.fontSize) < 0.5 && Math.abs(prev.position.x - text.position.x) < 1 && text.position.y - prev.position.y < l.fontSize *
|
|
2555
|
+
if (text.heading && prev && prev.type === "text" && prev.heading === text.heading && !link && !prev.link && Math.abs(prev.style?.fontSize - style.fontSize) < 0.5 && Math.abs(prev.position.x - text.position.x) < 1 && text.position.y - prev.position.y < l.fontSize * PT_TO_MM3 * 2.2 && text.position.y > prev.position.y) {
|
|
2001
2556
|
prev.content = `${prev.content} ${text.content}`.replace(/\s+/g, " ");
|
|
2002
2557
|
prev.tocEntry = prev.content;
|
|
2003
2558
|
prev.width = Math.max(prev.width ?? 0, text.width ?? 0);
|
|
2004
2559
|
return;
|
|
2005
2560
|
}
|
|
2561
|
+
lineMeta.set(text, { w: Math.max(l.fontSize * PT_TO_MM3, extent(l)), size: l.fontSize, face: fontKey(l.fontName) });
|
|
2006
2562
|
elements.push(text);
|
|
2007
2563
|
});
|
|
2564
|
+
if (options.readingOrder !== false) {
|
|
2565
|
+
if (gutters.length) {
|
|
2566
|
+
const isFlow = (e) => e.type === "text" || e.type === "richtext" || e.type === "table";
|
|
2567
|
+
const flow = elements.filter(isFlow), rest = elements.filter((e) => !isFlow(e));
|
|
2568
|
+
elements.splice(0, elements.length, ...rest, ...orderByColumns(flow, gutters, pageWmm));
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (options.foldParagraphs !== false) {
|
|
2572
|
+
const folded = foldParagraphs(elements, lineMeta, pageWmm);
|
|
2573
|
+
elements.splice(0, elements.length, ...folded);
|
|
2574
|
+
}
|
|
2008
2575
|
for (const w of formWidgets) {
|
|
2009
2576
|
if (w.pushButton) continue;
|
|
2010
2577
|
const baseEl = {
|
|
@@ -2039,7 +2606,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
2039
2606
|
}
|
|
2040
2607
|
pages.push({
|
|
2041
2608
|
id: `page-${pi}`,
|
|
2042
|
-
pageSize: { width: Math.round(pageW *
|
|
2609
|
+
pageSize: { width: Math.round(pageW * PT_TO_MM3 * 100) / 100, height: Math.round(pageH * PT_TO_MM3 * 100) / 100 },
|
|
2043
2610
|
margins: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
2044
2611
|
elements
|
|
2045
2612
|
});
|
|
@@ -2599,10 +3166,11 @@ function serializeElement(el) {
|
|
|
2599
3166
|
(row) => row.map((c, i) => {
|
|
2600
3167
|
const h = headers[i];
|
|
2601
3168
|
const v = cell(c).trim();
|
|
3169
|
+
if (!v) return "";
|
|
2602
3170
|
return h ? `${h}: ${v}` : v;
|
|
2603
3171
|
}).filter((s) => s !== "").join(" | ")
|
|
2604
3172
|
);
|
|
2605
|
-
return lines.join("\n");
|
|
3173
|
+
return lines.filter((l) => l !== "").join("\n");
|
|
2606
3174
|
}
|
|
2607
3175
|
case "collapsible": {
|
|
2608
3176
|
const title = String(e.title ?? "").trim();
|
package/dist/jdf-schema.json
CHANGED
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"backgroundColor": { "type": "string" },
|
|
104
104
|
"textAlign": { "type": "string", "enum": ["left","center","right","justify"] },
|
|
105
105
|
"lineHeight": { "type": "number" },
|
|
106
|
+
"textIndent": { "type": "number", "description": "First-line indent in the document unit." },
|
|
106
107
|
"letterSpacing": { "oneOf": [{ "type": "number" }, { "type": "string" }] },
|
|
107
108
|
"padding": { "oneOf": [{ "type": "number" }, { "type": "string" }, { "$ref": "#/definitions/Margins" }] },
|
|
108
109
|
"margin": { "oneOf": [{ "type": "number" }, { "type": "string" }, { "$ref": "#/definitions/Margins" }] },
|