@uurtech/jdf-cli 0.1.25 → 0.1.26
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 +449 -90
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -640,6 +640,38 @@ function rgbToHex(r, g, b) {
|
|
|
640
640
|
const h = (n) => clampByte(n).toString(16).padStart(2, "0");
|
|
641
641
|
return `#${h(r)}${h(g)}${h(b)}`;
|
|
642
642
|
}
|
|
643
|
+
function averageStops(stops) {
|
|
644
|
+
if (!Array.isArray(stops) || stops.length === 0) return null;
|
|
645
|
+
let r = 0, g = 0, b = 0, n = 0;
|
|
646
|
+
for (const st of stops) {
|
|
647
|
+
const css = Array.isArray(st) ? st[1] : null;
|
|
648
|
+
const m = typeof css === "string" && css.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
649
|
+
if (!m) continue;
|
|
650
|
+
r += parseInt(m[1], 16);
|
|
651
|
+
g += parseInt(m[2], 16);
|
|
652
|
+
b += parseInt(m[3], 16);
|
|
653
|
+
n++;
|
|
654
|
+
}
|
|
655
|
+
return n ? rgbToHex(r / n, g / n, b / n) : null;
|
|
656
|
+
}
|
|
657
|
+
function patternToColor(page, arg) {
|
|
658
|
+
if (!Array.isArray(arg)) return null;
|
|
659
|
+
if (arg[0] === "TilingPattern") {
|
|
660
|
+
const c = arg[1];
|
|
661
|
+
return Array.isArray(c) && c.length >= 3 ? rgbToHex(c[0], c[1], c[2]) : null;
|
|
662
|
+
}
|
|
663
|
+
if (arg[0] === "Shading") {
|
|
664
|
+
const id = arg[1];
|
|
665
|
+
try {
|
|
666
|
+
const store = typeof id === "string" && id.startsWith("g_") ? page.commonObjs : page.objs;
|
|
667
|
+
if (typeof store?.has === "function" && !store.has(id)) return null;
|
|
668
|
+
const ir = store.get(id);
|
|
669
|
+
if (Array.isArray(ir) && ir[0] === "RadialAxial") return averageStops(ir[3]);
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
643
675
|
function multiplyCtm(a, b) {
|
|
644
676
|
return [
|
|
645
677
|
a[0] * b[0] + a[1] * b[2],
|
|
@@ -658,7 +690,7 @@ async function walkOps(page, OPS, viewport) {
|
|
|
658
690
|
const [vx, vy] = viewport.convertToViewportPoint(x, y);
|
|
659
691
|
return { x: vx, y: vy };
|
|
660
692
|
};
|
|
661
|
-
const opList = await page.getOperatorList();
|
|
693
|
+
const opList = await page.getOperatorList({ annotationMode: annotationModeDisable });
|
|
662
694
|
const fnArr = opList.fnArray;
|
|
663
695
|
const argsArr = opList.argsArray;
|
|
664
696
|
const gs = {
|
|
@@ -668,15 +700,38 @@ async function walkOps(page, OPS, viewport) {
|
|
|
668
700
|
lineWidth: 1,
|
|
669
701
|
fillAlpha: 1,
|
|
670
702
|
strokeAlpha: 1,
|
|
671
|
-
textRenderingMode: 0
|
|
703
|
+
textRenderingMode: 0,
|
|
704
|
+
// Text state (PDF 9.3) — needed to know where each showText lands.
|
|
705
|
+
fontSize: 0,
|
|
706
|
+
charSpacing: 0,
|
|
707
|
+
wordSpacing: 0,
|
|
708
|
+
hscale: 1,
|
|
709
|
+
leading: 0,
|
|
710
|
+
rise: 0
|
|
672
711
|
};
|
|
712
|
+
const snapshot = () => ({ ...gs, ctm: [...gs.ctm] });
|
|
673
713
|
const stack = [];
|
|
674
|
-
const
|
|
675
|
-
const textOpacities = [];
|
|
676
|
-
const textRenderingModes = [];
|
|
714
|
+
const textOps = [];
|
|
677
715
|
const shapes = [];
|
|
678
716
|
const imagePositions = [];
|
|
679
|
-
let
|
|
717
|
+
let tm = [1, 0, 0, 1, 0, 0];
|
|
718
|
+
let tlm = [1, 0, 0, 1, 0, 0];
|
|
719
|
+
function pushImage(name, ctm, inline, maskFill) {
|
|
720
|
+
const corners = [tx(ctm, 0, 0), tx(ctm, 1, 0), tx(ctm, 1, 1), tx(ctm, 0, 1)];
|
|
721
|
+
const vpCorners = corners.map((p) => toViewport(p.x, p.y));
|
|
722
|
+
const xs = vpCorners.map((p) => p.x), ys = vpCorners.map((p) => p.y);
|
|
723
|
+
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
|
724
|
+
const minY = Math.min(...ys), maxY = Math.max(...ys);
|
|
725
|
+
imagePositions.push({
|
|
726
|
+
name,
|
|
727
|
+
x: minX * PT_TO_MM,
|
|
728
|
+
y: minY * PT_TO_MM,
|
|
729
|
+
w: (maxX - minX) * PT_TO_MM,
|
|
730
|
+
h: (maxY - minY) * PT_TO_MM,
|
|
731
|
+
inline,
|
|
732
|
+
maskFill
|
|
733
|
+
});
|
|
734
|
+
}
|
|
680
735
|
let pathSegments = [];
|
|
681
736
|
let pathRect = null;
|
|
682
737
|
let pathStart = null;
|
|
@@ -798,10 +853,51 @@ async function walkOps(page, OPS, viewport) {
|
|
|
798
853
|
const fn = fnArr[i];
|
|
799
854
|
const args = argsArr[i] || [];
|
|
800
855
|
if (fn === OPS.save) {
|
|
801
|
-
stack.push(
|
|
856
|
+
stack.push(snapshot());
|
|
802
857
|
} else if (fn === OPS.restore) {
|
|
803
858
|
const s = stack.pop();
|
|
804
859
|
if (s) Object.assign(gs, s);
|
|
860
|
+
} else if (fn === OPS.paintFormXObjectBegin) {
|
|
861
|
+
stack.push(snapshot());
|
|
862
|
+
const matrix = args[0];
|
|
863
|
+
if (Array.isArray(matrix) && matrix.length === 6) gs.ctm = multiplyCtm(matrix, gs.ctm);
|
|
864
|
+
} else if (fn === OPS.paintFormXObjectEnd) {
|
|
865
|
+
const s = stack.pop();
|
|
866
|
+
if (s) Object.assign(gs, s);
|
|
867
|
+
} else if (fn === OPS.beginText) {
|
|
868
|
+
tm = [1, 0, 0, 1, 0, 0];
|
|
869
|
+
tlm = [1, 0, 0, 1, 0, 0];
|
|
870
|
+
} else if (fn === OPS.setTextMatrix) {
|
|
871
|
+
tm = [...args];
|
|
872
|
+
tlm = [...tm];
|
|
873
|
+
} else if (fn === OPS.moveText) {
|
|
874
|
+
tlm = multiplyCtm([1, 0, 0, 1, args[0], args[1]], tlm);
|
|
875
|
+
tm = [...tlm];
|
|
876
|
+
} else if (fn === OPS.setLeadingMoveText) {
|
|
877
|
+
gs.leading = -args[1];
|
|
878
|
+
tlm = multiplyCtm([1, 0, 0, 1, args[0], args[1]], tlm);
|
|
879
|
+
tm = [...tlm];
|
|
880
|
+
} else if (fn === OPS.nextLine) {
|
|
881
|
+
tlm = multiplyCtm([1, 0, 0, 1, 0, -gs.leading], tlm);
|
|
882
|
+
tm = [...tlm];
|
|
883
|
+
} else if (fn === OPS.setLeading) {
|
|
884
|
+
gs.leading = args[0];
|
|
885
|
+
} else if (fn === OPS.setFont) {
|
|
886
|
+
gs.fontSize = typeof args[1] === "number" ? args[1] : gs.fontSize;
|
|
887
|
+
} else if (fn === OPS.setCharSpacing) {
|
|
888
|
+
gs.charSpacing = args[0];
|
|
889
|
+
} else if (fn === OPS.setWordSpacing) {
|
|
890
|
+
gs.wordSpacing = args[0];
|
|
891
|
+
} else if (fn === OPS.setHScale) {
|
|
892
|
+
gs.hscale = (args[0] ?? 100) / 100;
|
|
893
|
+
} else if (fn === OPS.setTextRise) {
|
|
894
|
+
gs.rise = args[0];
|
|
895
|
+
} else if (fn === OPS.setFillColorN) {
|
|
896
|
+
const c = patternToColor(page, args[0]);
|
|
897
|
+
if (c) gs.fill = c;
|
|
898
|
+
} else if (fn === OPS.setStrokeColorN) {
|
|
899
|
+
const c = patternToColor(page, args[0]);
|
|
900
|
+
if (c) gs.stroke = c;
|
|
805
901
|
} else if (fn === OPS.transform) {
|
|
806
902
|
gs.ctm = multiplyCtm(args, gs.ctm);
|
|
807
903
|
} else if (fn === OPS.setFillRGBColor) {
|
|
@@ -835,11 +931,23 @@ async function walkOps(page, OPS, viewport) {
|
|
|
835
931
|
else if (key === "CA") gs.strokeAlpha = val;
|
|
836
932
|
}
|
|
837
933
|
}
|
|
838
|
-
} else if (fn === OPS.showText
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
934
|
+
} else if (fn === OPS.showText) {
|
|
935
|
+
const trm = multiplyCtm(tm, gs.ctm);
|
|
936
|
+
const origin = tx(trm, 0, gs.rise);
|
|
937
|
+
const vp = toViewport(origin.x, origin.y);
|
|
938
|
+
const scale = Math.hypot(trm[2], trm[3]) || 1;
|
|
939
|
+
textOps.push({ x: vp.x, y: vp.y, fontSize: gs.fontSize * scale, fill: gs.fill, alpha: gs.fillAlpha, mode: gs.textRenderingMode });
|
|
940
|
+
let advance = 0;
|
|
941
|
+
const glyphs = Array.isArray(args[0]) ? args[0] : [];
|
|
942
|
+
for (const g of glyphs) {
|
|
943
|
+
if (typeof g === "number") {
|
|
944
|
+
advance += -g / 1e3 * gs.fontSize * gs.hscale;
|
|
945
|
+
} else if (g && typeof g === "object") {
|
|
946
|
+
const w = typeof g.width === "number" ? g.width : 0;
|
|
947
|
+
advance += (w / 1e3 * gs.fontSize + gs.charSpacing + (g.isSpace ? gs.wordSpacing : 0)) * gs.hscale;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
tm = multiplyCtm([1, 0, 0, 1, advance, 0], tm);
|
|
843
951
|
} else if (fn === OPS.rectangle) {
|
|
844
952
|
const [x, y, w, h] = args;
|
|
845
953
|
const p1 = tx(gs.ctm, x, y);
|
|
@@ -903,56 +1011,124 @@ async function walkOps(page, OPS, viewport) {
|
|
|
903
1011
|
pathRect = null;
|
|
904
1012
|
pathStart = null;
|
|
905
1013
|
pathLast = null;
|
|
906
|
-
} else if (fn === OPS.paintImageXObject
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
});
|
|
1014
|
+
} else if (fn === OPS.paintImageXObject) {
|
|
1015
|
+
pushImage(String(args[0]), gs.ctm);
|
|
1016
|
+
} else if (fn === OPS.paintImageXObjectRepeat) {
|
|
1017
|
+
const [name, scaleX, scaleY, positions] = args;
|
|
1018
|
+
if (Array.isArray(positions)) {
|
|
1019
|
+
for (let k = 0; k + 1 < positions.length; k += 2) {
|
|
1020
|
+
pushImage(String(name), multiplyCtm([scaleX, 0, 0, scaleY, positions[k], positions[k + 1]], gs.ctm));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} else if (fn === OPS.paintInlineImageXObject) {
|
|
1024
|
+
const img = args[0];
|
|
1025
|
+
if (img && typeof img === "object") pushImage(`inline-${imagePositions.length}`, gs.ctm, img);
|
|
1026
|
+
} else if (fn === OPS.paintImageMaskXObject) {
|
|
1027
|
+
const img = args[0];
|
|
1028
|
+
if (img && typeof img === "object") pushImage(`mask-${imagePositions.length}`, gs.ctm, img, gs.fill);
|
|
1029
|
+
} else if (fn === OPS.paintImageMaskXObjectRepeat) {
|
|
1030
|
+
const [img, scaleX, skewX, skewY, scaleY, positions] = args;
|
|
1031
|
+
if (img && typeof img === "object" && Array.isArray(positions)) {
|
|
1032
|
+
for (let k = 0; k + 1 < positions.length; k += 2) {
|
|
1033
|
+
pushImage(`mask-${imagePositions.length}`, multiplyCtm([scaleX, skewX, skewY, scaleY, positions[k], positions[k + 1]], gs.ctm), img, gs.fill);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} else if (fn === OPS.paintImageMaskXObjectGroup) {
|
|
1037
|
+
const group = args[0];
|
|
1038
|
+
if (Array.isArray(group)) {
|
|
1039
|
+
for (const img of group) {
|
|
1040
|
+
if (img && typeof img === "object" && Array.isArray(img.transform)) {
|
|
1041
|
+
pushImage(`mask-${imagePositions.length}`, multiplyCtm(img.transform, gs.ctm), img, gs.fill);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
921
1045
|
}
|
|
922
1046
|
}
|
|
923
|
-
return {
|
|
1047
|
+
return { textOps, shapes, imagePositions };
|
|
924
1048
|
}
|
|
925
1049
|
async function extractImages(page, positions, runtime, dataUrlCache) {
|
|
1050
|
+
const resolveObj = (id) => new Promise((resolve) => {
|
|
1051
|
+
let settled2 = false;
|
|
1052
|
+
const timer = setTimeout(() => done(null), 250);
|
|
1053
|
+
const done = (v) => {
|
|
1054
|
+
if (settled2) return;
|
|
1055
|
+
settled2 = true;
|
|
1056
|
+
clearTimeout(timer);
|
|
1057
|
+
resolve(v);
|
|
1058
|
+
};
|
|
1059
|
+
try {
|
|
1060
|
+
const store = id.startsWith("g_") ? page.commonObjs : page.objs;
|
|
1061
|
+
store.get(id, (img) => done(img));
|
|
1062
|
+
} catch {
|
|
1063
|
+
try {
|
|
1064
|
+
page.objs.get(id, (img) => done(img));
|
|
1065
|
+
} catch {
|
|
1066
|
+
done(null);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
const maskToRgba = (data, width, height, fill) => {
|
|
1071
|
+
const m = fill.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
1072
|
+
const r = m ? parseInt(m[1], 16) : 0, g = m ? parseInt(m[2], 16) : 0, b = m ? parseInt(m[3], 16) : 0;
|
|
1073
|
+
const out = new Uint8ClampedArray(width * height * 4);
|
|
1074
|
+
const rowBytes = width + 7 >> 3;
|
|
1075
|
+
for (let y = 0; y < height; y++) {
|
|
1076
|
+
for (let x = 0; x < width; x++) {
|
|
1077
|
+
const byte = data[y * rowBytes + (x >> 3)] ?? 255;
|
|
1078
|
+
const bit = byte >> 7 - (x & 7) & 1;
|
|
1079
|
+
const o = (y * width + x) * 4;
|
|
1080
|
+
out[o] = r;
|
|
1081
|
+
out[o + 1] = g;
|
|
1082
|
+
out[o + 2] = b;
|
|
1083
|
+
out[o + 3] = bit ? 0 : 255;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return out;
|
|
1087
|
+
};
|
|
1088
|
+
const encode = async (imgObj, maskFill) => {
|
|
1089
|
+
if (!imgObj || !imgObj.width || !imgObj.height) return null;
|
|
1090
|
+
let data = imgObj.data;
|
|
1091
|
+
if (typeof data === "string") data = (await resolveObj(data))?.data ?? null;
|
|
1092
|
+
if (maskFill) {
|
|
1093
|
+
if (!data) return null;
|
|
1094
|
+
return runtime.encodePng(imgObj.width, imgObj.height, 3, maskToRgba(data, imgObj.width, imgObj.height, maskFill));
|
|
1095
|
+
}
|
|
1096
|
+
if (data) {
|
|
1097
|
+
let kind = imgObj.kind || 0;
|
|
1098
|
+
if (!kind) {
|
|
1099
|
+
const px = imgObj.width * imgObj.height;
|
|
1100
|
+
if (data.length === px * 4) kind = 3;
|
|
1101
|
+
else if (data.length === px * 3) kind = 2;
|
|
1102
|
+
else if (data.length === (imgObj.width + 7 >> 3) * imgObj.height) kind = 1;
|
|
1103
|
+
}
|
|
1104
|
+
return runtime.encodePng(imgObj.width, imgObj.height, kind, data);
|
|
1105
|
+
}
|
|
1106
|
+
if (imgObj.bitmap) {
|
|
1107
|
+
try {
|
|
1108
|
+
const { canvas, context } = runtime.createCanvas(imgObj.width, imgObj.height);
|
|
1109
|
+
context.drawImage(imgObj.bitmap, 0, 0);
|
|
1110
|
+
if (typeof canvas.toDataURL === "function") return canvas.toDataURL("image/png");
|
|
1111
|
+
if (typeof canvas.toBuffer === "function") return `data:image/png;base64,${canvas.toBuffer("image/png").toString("base64")}`;
|
|
1112
|
+
} catch {
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
};
|
|
926
1117
|
const tasks = positions.map(async (pos) => {
|
|
1118
|
+
if (pos.inline) {
|
|
1119
|
+
const dataUrl2 = await encode(pos.inline, pos.maskFill);
|
|
1120
|
+
return dataUrl2 ? { pos, dataUrl: dataUrl2 } : null;
|
|
1121
|
+
}
|
|
927
1122
|
if (dataUrlCache.has(pos.name)) {
|
|
928
1123
|
return { pos, dataUrl: dataUrlCache.get(pos.name) };
|
|
929
1124
|
}
|
|
930
1125
|
let imgObj = null;
|
|
931
1126
|
try {
|
|
932
|
-
imgObj = await
|
|
933
|
-
let settled2 = false;
|
|
934
|
-
const timer = setTimeout(() => done(null), 250);
|
|
935
|
-
const done = (v) => {
|
|
936
|
-
if (settled2) return;
|
|
937
|
-
settled2 = true;
|
|
938
|
-
clearTimeout(timer);
|
|
939
|
-
resolve(v);
|
|
940
|
-
};
|
|
941
|
-
try {
|
|
942
|
-
page.objs.get(pos.name, (img) => done(img));
|
|
943
|
-
} catch {
|
|
944
|
-
try {
|
|
945
|
-
page.commonObjs.get(pos.name, (img) => done(img));
|
|
946
|
-
} catch {
|
|
947
|
-
done(null);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
});
|
|
1127
|
+
imgObj = await resolveObj(pos.name);
|
|
951
1128
|
} catch {
|
|
952
1129
|
imgObj = null;
|
|
953
1130
|
}
|
|
954
|
-
|
|
955
|
-
const dataUrl = runtime.encodePng(imgObj.width, imgObj.height, imgObj.kind || 0, imgObj.data);
|
|
1131
|
+
const dataUrl = await encode(imgObj);
|
|
956
1132
|
if (!dataUrl) return null;
|
|
957
1133
|
dataUrlCache.set(pos.name, dataUrl);
|
|
958
1134
|
return { pos, dataUrl };
|
|
@@ -960,7 +1136,20 @@ async function extractImages(page, positions, runtime, dataUrlCache) {
|
|
|
960
1136
|
const settled = await Promise.all(tasks);
|
|
961
1137
|
return settled.filter((x) => x !== null);
|
|
962
1138
|
}
|
|
963
|
-
async function
|
|
1139
|
+
async function resolveDestPage(doc, dest) {
|
|
1140
|
+
try {
|
|
1141
|
+
let d = dest;
|
|
1142
|
+
if (typeof d === "string") d = await doc.getDestination(d);
|
|
1143
|
+
if (Array.isArray(d) && d[0] != null) {
|
|
1144
|
+
if (typeof d[0] === "number") return d[0];
|
|
1145
|
+
const idx = await doc.getPageIndex(d[0]);
|
|
1146
|
+
if (typeof idx === "number") return idx;
|
|
1147
|
+
}
|
|
1148
|
+
} catch {
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
1152
|
+
async function extractLinks(doc, page, viewport) {
|
|
964
1153
|
const out = [];
|
|
965
1154
|
let annots = [];
|
|
966
1155
|
try {
|
|
@@ -989,7 +1178,9 @@ async function extractLinks(page, viewport) {
|
|
|
989
1178
|
h: (yMax - yMin) * PT_TO_MM
|
|
990
1179
|
};
|
|
991
1180
|
const url = a.url || a.unsafeUrl;
|
|
992
|
-
|
|
1181
|
+
const destPage = url ? void 0 : await resolveDestPage(doc, a.dest);
|
|
1182
|
+
if (!url && destPage == null) continue;
|
|
1183
|
+
out.push({ rectMm, url, destPage });
|
|
993
1184
|
}
|
|
994
1185
|
return out;
|
|
995
1186
|
}
|
|
@@ -1045,26 +1236,62 @@ async function extractFormWidgets(page, viewport) {
|
|
|
1045
1236
|
async function flattenOutline(doc, outline) {
|
|
1046
1237
|
if (!outline) return [];
|
|
1047
1238
|
const out = [];
|
|
1048
|
-
async function walk(items) {
|
|
1239
|
+
async function walk(items, depth) {
|
|
1049
1240
|
for (const item of items) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
dest = await doc.getDestination(dest);
|
|
1054
|
-
}
|
|
1055
|
-
if (Array.isArray(dest) && dest[0]) {
|
|
1056
|
-
const ref = dest[0];
|
|
1057
|
-
const idx = await doc.getPageIndex(ref);
|
|
1058
|
-
if (typeof idx === "number") out.push({ title: item.title, pageIndex: idx });
|
|
1059
|
-
}
|
|
1060
|
-
} catch {
|
|
1241
|
+
const idx = await resolveDestPage(doc, item.dest);
|
|
1242
|
+
if (idx != null && typeof item.title === "string" && item.title.trim()) {
|
|
1243
|
+
out.push({ title: item.title.trim(), pageIndex: idx, depth });
|
|
1061
1244
|
}
|
|
1062
|
-
if (item.items?.length) await walk(item.items);
|
|
1245
|
+
if (item.items?.length) await walk(item.items, depth + 1);
|
|
1063
1246
|
}
|
|
1064
1247
|
}
|
|
1065
|
-
await walk(outline);
|
|
1248
|
+
await walk(outline, 1);
|
|
1066
1249
|
return out;
|
|
1067
1250
|
}
|
|
1251
|
+
var normTitle = (s) => s.toLowerCase().replace(/[\s\u00a0]+/g, " ").replace(/[^\p{L}\p{N} ]/gu, "").trim();
|
|
1252
|
+
function applyOutline(pages, outline) {
|
|
1253
|
+
for (const entry of outline) {
|
|
1254
|
+
const page = pages[entry.pageIndex];
|
|
1255
|
+
if (!page) continue;
|
|
1256
|
+
const want = normTitle(entry.title);
|
|
1257
|
+
if (!want) continue;
|
|
1258
|
+
let best = null;
|
|
1259
|
+
let bestScore = 0;
|
|
1260
|
+
for (const el of page.elements) {
|
|
1261
|
+
if (el.type !== "text") continue;
|
|
1262
|
+
const have = normTitle(el.content || "");
|
|
1263
|
+
if (!have) continue;
|
|
1264
|
+
let score = 0;
|
|
1265
|
+
if (have === want) score = 3;
|
|
1266
|
+
else if (have.startsWith(want) || want.startsWith(have)) score = 2;
|
|
1267
|
+
else if (have.length >= 6 && want.includes(have)) score = 1;
|
|
1268
|
+
if (score > bestScore || score === bestScore && best && score > 0 && (el.position?.y ?? 0) < (best.position?.y ?? 0)) {
|
|
1269
|
+
best = el;
|
|
1270
|
+
bestScore = score;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (best && bestScore > 0) {
|
|
1274
|
+
const level = Math.min(6, Math.max(1, entry.depth));
|
|
1275
|
+
if (!best.heading) best.heading = level;
|
|
1276
|
+
best.tocEntry = entry.title;
|
|
1277
|
+
best.tocLevel = level;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function pdfDateToIso(v) {
|
|
1282
|
+
if (typeof v !== "string") return void 0;
|
|
1283
|
+
const m = v.match(/^D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?([Zz+-])?(\d{2})?'?(\d{2})?/);
|
|
1284
|
+
if (!m) {
|
|
1285
|
+
const t2 = Date.parse(v);
|
|
1286
|
+
return Number.isFinite(t2) ? new Date(t2).toISOString() : void 0;
|
|
1287
|
+
}
|
|
1288
|
+
const [, Y, Mo = "01", D = "01", h = "00", mi = "00", s = "00", sign, oh = "00", om = "00"] = m;
|
|
1289
|
+
const tz = !sign || sign === "Z" || sign === "z" ? "Z" : `${sign}${oh}:${om}`;
|
|
1290
|
+
const iso = `${Y}-${Mo}-${D}T${h}:${mi}:${s}${tz}`;
|
|
1291
|
+
const t = Date.parse(iso);
|
|
1292
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : void 0;
|
|
1293
|
+
}
|
|
1294
|
+
var annotationModeDisable = 0;
|
|
1068
1295
|
async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
1069
1296
|
const pdfjs = options.pdfjs || runtime.pdfjs;
|
|
1070
1297
|
if (!pdfjs) {
|
|
@@ -1085,7 +1312,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1085
1312
|
} else {
|
|
1086
1313
|
data = source;
|
|
1087
1314
|
}
|
|
1088
|
-
const
|
|
1315
|
+
const loadingTask = pdfjs.getDocument({
|
|
1089
1316
|
data,
|
|
1090
1317
|
// The runtime adapter declares whether it supports a real Web Worker.
|
|
1091
1318
|
// We don't sniff `typeof Worker` here because Node 22+ exposes a global
|
|
@@ -1094,15 +1321,55 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1094
1321
|
// newer Node. Browser entry leaves this unset (= false = real worker
|
|
1095
1322
|
// via GlobalWorkerOptions.workerSrc); node entry sets `true`.
|
|
1096
1323
|
disableWorker: runtime.disableWorker === true,
|
|
1097
|
-
isEvalSupported: false
|
|
1098
|
-
|
|
1324
|
+
isEvalSupported: false,
|
|
1325
|
+
// Keep going past malformed content streams instead of failing the page.
|
|
1326
|
+
stopAtErrors: false,
|
|
1327
|
+
// Force the classic pixel-array image path on every host so the reader
|
|
1328
|
+
// (WKWebView has OffscreenCanvas) and the CLI produce identical PNGs.
|
|
1329
|
+
isOffscreenCanvasSupported: false,
|
|
1330
|
+
password: options.password,
|
|
1331
|
+
// Standard-14 font metrics + CJK CMaps: without these PDF.js falls back
|
|
1332
|
+
// to guesses for non-embedded fonts and logs a warning per page.
|
|
1333
|
+
...runtime.standardFontDataUrl ? { standardFontDataUrl: runtime.standardFontDataUrl } : {},
|
|
1334
|
+
...runtime.cMapUrl ? { cMapUrl: runtime.cMapUrl, cMapPacked: true } : {}
|
|
1335
|
+
});
|
|
1336
|
+
let passwordTried = typeof options.password === "string";
|
|
1337
|
+
loadingTask.onPassword = (updatePassword, reason) => {
|
|
1338
|
+
const retry = reason === 2 || passwordTried;
|
|
1339
|
+
if (!options.onPassword) {
|
|
1340
|
+
updatePassword(new Error(retry ? "[@jdf/pdf-import] Wrong password for encrypted PDF" : "[@jdf/pdf-import] PDF is password-protected \u2014 pass `password`"));
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
passwordTried = true;
|
|
1344
|
+
options.onPassword(retry).then((pw) => {
|
|
1345
|
+
if (pw == null) updatePassword(new Error("[@jdf/pdf-import] Password entry cancelled"));
|
|
1346
|
+
else updatePassword(pw);
|
|
1347
|
+
}).catch((e) => updatePassword(e instanceof Error ? e : new Error(String(e))));
|
|
1348
|
+
};
|
|
1349
|
+
let doc;
|
|
1350
|
+
try {
|
|
1351
|
+
doc = await loadingTask.promise;
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
if (e?.name === "PasswordException") {
|
|
1354
|
+
const msg = /no password/i.test(e.message || "") ? "PDF is password-protected \u2014 pass a password (CLI: --password)" : e.message || "PDF is password-protected";
|
|
1355
|
+
throw new Error(`[@jdf/pdf-import] ${msg}`);
|
|
1356
|
+
}
|
|
1357
|
+
throw e;
|
|
1358
|
+
}
|
|
1099
1359
|
const pages = [];
|
|
1100
1360
|
const imageResources = {};
|
|
1101
1361
|
let imgCounter = 0;
|
|
1102
1362
|
const dataUrlCache = /* @__PURE__ */ new Map();
|
|
1103
1363
|
const resourceKeyByName = /* @__PURE__ */ new Map();
|
|
1104
|
-
const outline = await doc.getOutline().catch(() => null);
|
|
1105
|
-
|
|
1364
|
+
const outline = await flattenOutline(doc, await doc.getOutline().catch(() => null));
|
|
1365
|
+
let pdfInfo = null;
|
|
1366
|
+
let pdfMetadata = null;
|
|
1367
|
+
try {
|
|
1368
|
+
const md = await doc.getMetadata();
|
|
1369
|
+
pdfInfo = md?.info ?? null;
|
|
1370
|
+
pdfMetadata = md?.metadata ?? null;
|
|
1371
|
+
} catch {
|
|
1372
|
+
}
|
|
1106
1373
|
for (let pi = 1; pi <= doc.numPages; pi++) {
|
|
1107
1374
|
let findLinkForRun2 = function(r) {
|
|
1108
1375
|
const cx = r.x + r.width / 2;
|
|
@@ -1124,7 +1391,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1124
1391
|
} catch {
|
|
1125
1392
|
}
|
|
1126
1393
|
const ops = await walkOps(page, OPS, viewport);
|
|
1127
|
-
const links = await extractLinks(page, viewport);
|
|
1394
|
+
const links = await extractLinks(doc, page, viewport);
|
|
1128
1395
|
const formWidgets = await extractFormWidgets(page, viewport);
|
|
1129
1396
|
const textContent = await page.getTextContent({ disableCombineTextItems: false });
|
|
1130
1397
|
const items = textContent.items;
|
|
@@ -1167,9 +1434,53 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1167
1434
|
const n = typeof v === "number" ? v : Number(v);
|
|
1168
1435
|
return Number.isFinite(n) ? n : fallback;
|
|
1169
1436
|
};
|
|
1170
|
-
|
|
1437
|
+
const opBins = /* @__PURE__ */ new Map();
|
|
1438
|
+
for (const op of ops.textOps) {
|
|
1439
|
+
const key = `${Math.round(op.x)},${Math.round(op.y)}`;
|
|
1440
|
+
const arr = opBins.get(key);
|
|
1441
|
+
if (arr) arr.push(op);
|
|
1442
|
+
else opBins.set(key, [op]);
|
|
1443
|
+
}
|
|
1444
|
+
const findOp = (x, y, fontSize) => {
|
|
1445
|
+
let best = null;
|
|
1446
|
+
let bestD = Infinity;
|
|
1447
|
+
const bx = Math.round(x), by = Math.round(y);
|
|
1448
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
1449
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
1450
|
+
const arr = opBins.get(`${bx + dx},${by + dy}`);
|
|
1451
|
+
if (!arr) continue;
|
|
1452
|
+
for (const op of arr) {
|
|
1453
|
+
const d = Math.hypot(op.x - x, op.y - y);
|
|
1454
|
+
if (d < bestD) {
|
|
1455
|
+
bestD = d;
|
|
1456
|
+
best = op;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (best) return best;
|
|
1462
|
+
const tol = Math.max(2, fontSize * 0.6);
|
|
1463
|
+
for (const op of ops.textOps) {
|
|
1464
|
+
if (Math.abs(op.y - y) > tol) continue;
|
|
1465
|
+
const d = Math.abs(op.x - x) + Math.abs(op.y - y) * 4;
|
|
1466
|
+
if (d < bestD) {
|
|
1467
|
+
bestD = d;
|
|
1468
|
+
best = op;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (best) return best;
|
|
1472
|
+
for (const op of ops.textOps) {
|
|
1473
|
+
const d = Math.hypot(op.x - x, op.y - y);
|
|
1474
|
+
if (d < bestD) {
|
|
1475
|
+
bestD = d;
|
|
1476
|
+
best = op;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return bestD < 40 ? best : null;
|
|
1480
|
+
};
|
|
1481
|
+
const keepInvisible = options.invisibleText !== "drop";
|
|
1482
|
+
items.forEach((it) => {
|
|
1171
1483
|
if (!it.str || !it.str.length) return;
|
|
1172
|
-
if ((ops.textRenderingModes[idx] ?? 0) === 3) return;
|
|
1173
1484
|
const tr = it.transform;
|
|
1174
1485
|
const fontSize = safeNum(Math.hypot(safeNum(tr?.[2], 0), safeNum(tr?.[3], 0)), 0) || safeNum(it.height, 0) || 10;
|
|
1175
1486
|
const baseX = safeNum(tr?.[4], 0);
|
|
@@ -1177,6 +1488,11 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1177
1488
|
const conv = viewport.convertToViewportPoint(baseX, baseY);
|
|
1178
1489
|
const vx = safeNum(conv?.[0], 0);
|
|
1179
1490
|
const vy = safeNum(conv?.[1], 0);
|
|
1491
|
+
const op = findOp(vx, vy, fontSize);
|
|
1492
|
+
const mode = op?.mode ?? 0;
|
|
1493
|
+
if (mode === 7) return;
|
|
1494
|
+
const invisible = mode === 3;
|
|
1495
|
+
if (invisible && !keepInvisible) return;
|
|
1180
1496
|
const ascent = it.height ? safeNum(it.height, fontSize) * 0.78 : fontSize * 0.78;
|
|
1181
1497
|
const yTop = vy - ascent;
|
|
1182
1498
|
const w = safeNum(it.width, 0);
|
|
@@ -1188,8 +1504,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1188
1504
|
fontName: it.fontName,
|
|
1189
1505
|
width: safeNum(w * PT_TO_MM, 0),
|
|
1190
1506
|
height: safeNum((it.height || fontSize) * PT_TO_MM, fontSize * PT_TO_MM),
|
|
1191
|
-
color:
|
|
1192
|
-
opacity: safeNum(
|
|
1507
|
+
color: op?.fill || "#000000",
|
|
1508
|
+
opacity: invisible ? 0 : safeNum(op?.alpha, 1)
|
|
1193
1509
|
});
|
|
1194
1510
|
});
|
|
1195
1511
|
runs.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
@@ -1332,14 +1648,36 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1332
1648
|
elements
|
|
1333
1649
|
});
|
|
1334
1650
|
}
|
|
1651
|
+
applyOutline(pages, outline);
|
|
1652
|
+
const meta = {
|
|
1653
|
+
title,
|
|
1654
|
+
pageSize: pages[0]?.pageSize || "A4",
|
|
1655
|
+
unit: "mm",
|
|
1656
|
+
margins: { top: 0, right: 0, bottom: 0, left: 0 }
|
|
1657
|
+
};
|
|
1658
|
+
if (pdfInfo) {
|
|
1659
|
+
if (typeof pdfInfo.Author === "string" && pdfInfo.Author.trim()) meta.author = pdfInfo.Author.trim();
|
|
1660
|
+
const created = pdfDateToIso(pdfInfo.CreationDate);
|
|
1661
|
+
const modified = pdfDateToIso(pdfInfo.ModDate);
|
|
1662
|
+
if (created) meta.created = created;
|
|
1663
|
+
if (modified) meta.modified = modified;
|
|
1664
|
+
if (typeof pdfInfo.Keywords === "string") {
|
|
1665
|
+
const kws = pdfInfo.Keywords.split(/[,;]+/).map((k) => k.trim()).filter(Boolean);
|
|
1666
|
+
if (kws.length) meta.keywords = kws;
|
|
1667
|
+
}
|
|
1668
|
+
if (typeof pdfInfo.Language === "string" && pdfInfo.Language.trim()) meta.language = pdfInfo.Language.trim();
|
|
1669
|
+
}
|
|
1670
|
+
if (!meta.language && pdfMetadata && typeof pdfMetadata.get === "function") {
|
|
1671
|
+
try {
|
|
1672
|
+
const lang = pdfMetadata.get("dc:language");
|
|
1673
|
+
const first = Array.isArray(lang) ? lang[0] : lang;
|
|
1674
|
+
if (typeof first === "string" && first.trim()) meta.language = first.trim();
|
|
1675
|
+
} catch {
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1335
1678
|
const result = {
|
|
1336
1679
|
$jdf: "1.0.0",
|
|
1337
|
-
meta
|
|
1338
|
-
title,
|
|
1339
|
-
pageSize: pages[0]?.pageSize || "A4",
|
|
1340
|
-
unit: "mm",
|
|
1341
|
-
margins: { top: 0, right: 0, bottom: 0, left: 0 }
|
|
1342
|
-
},
|
|
1680
|
+
meta,
|
|
1343
1681
|
pages
|
|
1344
1682
|
};
|
|
1345
1683
|
if (Object.keys(imageResources).length > 0) {
|
|
@@ -1351,27 +1689,31 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
|
|
|
1351
1689
|
// ../../packages/jdf-pdf-import/src/node.ts
|
|
1352
1690
|
var pdfjsModule = null;
|
|
1353
1691
|
var pdfjsLoadPromise = null;
|
|
1692
|
+
var pdfjsAssetDirs = null;
|
|
1354
1693
|
async function loadNodePdfJs() {
|
|
1355
1694
|
if (pdfjsModule) return pdfjsModule;
|
|
1356
1695
|
if (pdfjsLoadPromise) return pdfjsLoadPromise;
|
|
1357
1696
|
pdfjsLoadPromise = (async () => {
|
|
1358
1697
|
const { createRequire } = await import('module');
|
|
1359
1698
|
const require_ = createRequire(import.meta.url);
|
|
1360
|
-
const
|
|
1361
|
-
console.
|
|
1362
|
-
if (typeof args[0] === "string" && args[0].includes("legacy")) return;
|
|
1363
|
-
|
|
1699
|
+
const origLog = console.log;
|
|
1700
|
+
console.log = (...args) => {
|
|
1701
|
+
if (typeof args[0] === "string" && args[0].includes("`legacy` build")) return;
|
|
1702
|
+
origLog.apply(console, args);
|
|
1364
1703
|
};
|
|
1365
1704
|
let lib;
|
|
1366
1705
|
try {
|
|
1367
1706
|
lib = await import('pdfjs-dist/build/pdf.mjs');
|
|
1368
1707
|
} finally {
|
|
1369
|
-
console.
|
|
1708
|
+
console.log = origLog;
|
|
1370
1709
|
}
|
|
1371
1710
|
const workerPath = require_.resolve("pdfjs-dist/build/pdf.worker.mjs");
|
|
1372
1711
|
if (lib.GlobalWorkerOptions) {
|
|
1373
1712
|
lib.GlobalWorkerOptions.workerSrc = workerPath;
|
|
1374
1713
|
}
|
|
1714
|
+
const { dirname, join } = await import('path');
|
|
1715
|
+
const pkgDir = dirname(dirname(workerPath));
|
|
1716
|
+
pdfjsAssetDirs = { standardFontDataUrl: join(pkgDir, "standard_fonts") + "/", cMapUrl: join(pkgDir, "cmaps") + "/" };
|
|
1375
1717
|
pdfjsModule = lib;
|
|
1376
1718
|
return lib;
|
|
1377
1719
|
})();
|
|
@@ -1382,6 +1724,10 @@ async function loadCanvas() {
|
|
|
1382
1724
|
if (canvasModule) return canvasModule;
|
|
1383
1725
|
try {
|
|
1384
1726
|
canvasModule = await import('@napi-rs/canvas');
|
|
1727
|
+
const g = globalThis;
|
|
1728
|
+
if (typeof g.DOMMatrix === "undefined" && canvasModule.DOMMatrix) g.DOMMatrix = canvasModule.DOMMatrix;
|
|
1729
|
+
if (typeof g.Path2D === "undefined" && canvasModule.Path2D) g.Path2D = canvasModule.Path2D;
|
|
1730
|
+
if (typeof g.ImageData === "undefined" && canvasModule.ImageData) g.ImageData = canvasModule.ImageData;
|
|
1385
1731
|
return canvasModule;
|
|
1386
1732
|
} catch (err) {
|
|
1387
1733
|
throw new Error(
|
|
@@ -1433,6 +1779,7 @@ async function importPdfToJdf2(source, title, options = {}) {
|
|
|
1433
1779
|
const runtime = {
|
|
1434
1780
|
pdfjs,
|
|
1435
1781
|
disableWorker: true,
|
|
1782
|
+
...pdfjsAssetDirs ?? {},
|
|
1436
1783
|
createCanvas(width, height) {
|
|
1437
1784
|
const canvas = canvasMod.createCanvas(width, height);
|
|
1438
1785
|
const context = canvas.getContext("2d");
|
|
@@ -1457,7 +1804,10 @@ async function importPdf(inputPath, outputPath, options = {}) {
|
|
|
1457
1804
|
console.log(`Importing: ${input}`);
|
|
1458
1805
|
const title = path2.basename(input, path2.extname(input));
|
|
1459
1806
|
const t0 = Date.now();
|
|
1460
|
-
const doc = await importPdfToJdf2(input, title
|
|
1807
|
+
const doc = await importPdfToJdf2(input, title, {
|
|
1808
|
+
password: options.password,
|
|
1809
|
+
invisibleText: options.dropInvisibleText ? "drop" : "keep"
|
|
1810
|
+
});
|
|
1461
1811
|
console.log(`Parsed in ${((Date.now() - t0) / 1e3).toFixed(1)}s \u2014 ${doc.pages.length} page(s)`);
|
|
1462
1812
|
let output;
|
|
1463
1813
|
if (outputPath) {
|
|
@@ -2048,7 +2398,7 @@ The CLI exists for these workflows:
|
|
|
2048
2398
|
|
|
2049
2399
|
Usage:
|
|
2050
2400
|
jdf validate <file.jdf>
|
|
2051
|
-
jdf convert <file.{pdf,json,md}> [-o output.{jdf,jdfx}] [--json]
|
|
2401
|
+
jdf convert <file.{pdf,json,md}> [-o output.{jdf,jdfx}] [--json] [--password PW] [--drop-invisible-text]
|
|
2052
2402
|
jdf chunk <file.{jdf,jdfx}> [--strategy section|element|fixed] [--format jsonl|json|inline] [--max-tokens N] [-o out]
|
|
2053
2403
|
jdf embed <file.{jdf,jdfx}> [--provider ollama|openai] [--model NAME] [--strategy \u2026] [--incremental] [-o out]
|
|
2054
2404
|
jdf --help
|
|
@@ -2063,6 +2413,11 @@ Flags:
|
|
|
2063
2413
|
-o, --output <path> Explicit output path
|
|
2064
2414
|
--json convert: force pure JSON .jdf output (inline base64
|
|
2065
2415
|
instead of a .jdfx zip bundle)
|
|
2416
|
+
--password <pw> convert(pdf): password for an encrypted PDF
|
|
2417
|
+
--drop-invisible-text
|
|
2418
|
+
convert(pdf): omit invisible (OCR-layer) text; by
|
|
2419
|
+
default it is kept with opacity 0 so RAG / search
|
|
2420
|
+
still see the words of a scanned PDF
|
|
2066
2421
|
--strategy <s> chunk/embed: section (default) | element | fixed
|
|
2067
2422
|
--format <f> chunk: jsonl (default) | json | inline
|
|
2068
2423
|
--max-tokens <n> chunk/embed: soft cap per chunk (default 512)
|
|
@@ -2084,7 +2439,7 @@ Examples:
|
|
|
2084
2439
|
jdf embed report.jdf # local embeddings via Ollama (auto-setup)
|
|
2085
2440
|
jdf embed report.jdf --provider openai --incremental
|
|
2086
2441
|
`;
|
|
2087
|
-
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["help", "h", "json", "verbose", "skip-validate", "incremental", "no-auto-start"]);
|
|
2442
|
+
var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["help", "h", "json", "verbose", "skip-validate", "incremental", "no-auto-start", "drop-invisible-text"]);
|
|
2088
2443
|
function parseArgs(argv) {
|
|
2089
2444
|
const positional = [];
|
|
2090
2445
|
const flags = {};
|
|
@@ -2164,7 +2519,11 @@ async function main() {
|
|
|
2164
2519
|
await importMarkdown(input, output);
|
|
2165
2520
|
process.exit(0);
|
|
2166
2521
|
} else if (lower.endsWith(".pdf")) {
|
|
2167
|
-
await importPdf(input, output, {
|
|
2522
|
+
await importPdf(input, output, {
|
|
2523
|
+
forceJson,
|
|
2524
|
+
password: typeof flags.password === "string" ? flags.password : void 0,
|
|
2525
|
+
dropInvisibleText: flags["drop-invisible-text"] === true
|
|
2526
|
+
});
|
|
2168
2527
|
process.exit(0);
|
|
2169
2528
|
} else if (lower.endsWith(".json")) {
|
|
2170
2529
|
await importJson(input, output, { forceJson });
|