@uurtech/jdf-cli 0.1.24 → 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.
Files changed (2) hide show
  1. package/dist/index.js +449 -90
  2. 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 textColors = [];
675
- const textOpacities = [];
676
- const textRenderingModes = [];
714
+ const textOps = [];
677
715
  const shapes = [];
678
716
  const imagePositions = [];
679
- let textIdx = 0;
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({ ctm: [...gs.ctm], fill: gs.fill, stroke: gs.stroke, lineWidth: gs.lineWidth, fillAlpha: gs.fillAlpha, strokeAlpha: gs.strokeAlpha, textRenderingMode: gs.textRenderingMode });
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 || fn === OPS.showSpacedText || fn === OPS.nextLineShowText || fn === OPS.nextLineSetSpacingShowText) {
839
- textColors[textIdx] = gs.fill;
840
- textOpacities[textIdx] = gs.fillAlpha;
841
- textRenderingModes[textIdx] = gs.textRenderingMode;
842
- textIdx++;
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 || fn === OPS.paintImageMaskXObject || fn === OPS.paintInlineImageXObject) {
907
- const name = args[0];
908
- const c = gs.ctm;
909
- const corners = [tx(c, 0, 0), tx(c, 1, 0), tx(c, 1, 1), tx(c, 0, 1)];
910
- const vpCorners = corners.map((p) => toViewport(p.x, p.y));
911
- const xs = vpCorners.map((p) => p.x), ys = vpCorners.map((p) => p.y);
912
- const minX = Math.min(...xs), maxX = Math.max(...xs);
913
- const minY = Math.min(...ys), maxY = Math.max(...ys);
914
- imagePositions.push({
915
- name,
916
- x: minX * PT_TO_MM,
917
- y: minY * PT_TO_MM,
918
- w: (maxX - minX) * PT_TO_MM,
919
- h: (maxY - minY) * PT_TO_MM
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 { textColors, textOpacities, textRenderingModes, shapes, imagePositions };
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 new Promise((resolve) => {
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
- if (!imgObj || !imgObj.data || !imgObj.width || !imgObj.height) return null;
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 extractLinks(page, viewport) {
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
- out.push({ rectMm, url });
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
- try {
1051
- let dest = item.dest;
1052
- if (typeof dest === "string") {
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 doc = await pdfjs.getDocument({
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
- }).promise;
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
- await flattenOutline(doc, outline);
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
- items.forEach((it, idx) => {
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: ops.textColors[idx] || "#000000",
1192
- opacity: safeNum(ops.textOpacities[idx], 1)
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 origWarn = console.warn;
1361
- console.warn = (...args) => {
1362
- if (typeof args[0] === "string" && args[0].includes("legacy")) return;
1363
- origWarn.apply(console, args);
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.warn = origWarn;
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, { forceJson });
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uurtech/jdf-cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Command-line tool for the JDF (JSON Document Format) — validate and convert documents.",
5
5
  "license": "MIT",
6
6
  "author": "Ugur Kazdal",