@uurtech/jdf-cli 0.2.4 → 0.2.6

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 CHANGED
@@ -765,12 +765,13 @@ function columnBands(rows) {
765
765
  const countOf = /* @__PURE__ */ new Map();
766
766
  for (const m of body) countOf.set(m.r, (countOf.get(m.r) ?? 0) + 1);
767
767
  const maxCount = Math.max(...countOf.values());
768
- const anchorRows = new Set(rows.filter((r) => countOf.get(r) === maxCount));
768
+ let anchorRows = new Set(rows.filter((r) => countOf.get(r) === maxCount));
769
769
  let bands = clusterCells(body.filter((m) => anchorRows.has(m.r)));
770
770
  if (hasConflict(bands)) {
771
771
  const first = rows.find((r) => anchorRows.has(r));
772
772
  bands = clusterCells(body.filter((m) => m.r === first));
773
773
  if (hasConflict(bands)) return null;
774
+ anchorRows = /* @__PURE__ */ new Set([first]);
774
775
  }
775
776
  for (const m of body) {
776
777
  if (anchorRows.has(m.r)) continue;
@@ -893,7 +894,8 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
893
894
  y0: blockRows[0].y - blockRows[0].h * 2.5,
894
895
  y1: blockRows[blockRows.length - 1].y + blockRows[blockRows.length - 1].h * 3
895
896
  } : null;
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")) : [];
897
+ const thinH = (s) => (s.kind === "line" || s.kind === "rect") && s.height < 0.6 && s.width >= s.height && !!(s.fill || s.stroke);
898
+ const gridShapes = bbox ? shapes.map((s, k) => ({ s, k })).filter(({ s }) => s.y >= bbox.y0 - 1 && s.y + s.height <= bbox.y1 + 1 && (s.kind === "line" || s.kind === "rect") && (thinH(s) ? Math.min(s.x + s.width, bbox.x1) - Math.max(s.x, bbox.x0) >= s.width * 0.5 : s.x >= bbox.x0 - 1 && s.x + s.width <= bbox.x1 + 1)) : [];
897
899
  const tableW = bbox ? bbox.x1 - bbox.x0 : 0;
898
900
  const isRule = (s) => s.kind === "line" || s.kind === "rect" && (s.height < 0.6 || s.width < 0.6) && !!(s.fill || s.stroke);
899
901
  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);
@@ -978,10 +980,29 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
978
980
  for (let r = 1; r < blockRows.length; r++) pitches.push(blockRows[r].y - blockRows[r - 1].y);
979
981
  const medPitch = pitches.length ? pitches.slice().sort((a, b) => a - b)[Math.floor(pitches.length / 2)] : blockRows[0].h * 1.3;
980
982
  let pendingLabel = null;
983
+ const pendingCells = /* @__PURE__ */ new Map();
984
+ const flushPendingCells = () => {
985
+ if (!pendingCells.size) return;
986
+ const cells = bands.map(() => "");
987
+ let row = null;
988
+ for (const [b, pc] of pendingCells) {
989
+ cells[b] = pc.text;
990
+ lineIdx.push(...pc.idx);
991
+ row = row ?? pc.row;
992
+ }
993
+ grid.push(cells);
994
+ gridRows.push(row);
995
+ pendingCells.clear();
996
+ };
981
997
  const flushPending = () => {
982
998
  if (!pendingLabel) return;
983
999
  const cells = bands.map(() => "");
984
1000
  cells[0] = pendingLabel.text;
1001
+ for (const [b, pc] of pendingCells) {
1002
+ cells[b] = pc.text;
1003
+ lineIdx.push(...pc.idx);
1004
+ }
1005
+ pendingCells.clear();
985
1006
  grid.push(cells);
986
1007
  gridRows.push(pendingLabel.row);
987
1008
  lineIdx.push(...pendingLabel.idx);
@@ -993,6 +1014,15 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
993
1014
  const c = row.cells[0];
994
1015
  const b = assign.get(c) ?? bands.findIndex((bb) => overlaps(c, bb));
995
1016
  if (b > 0) {
1017
+ const prevRow2 = gridRows[gridRows.length - 1];
1018
+ if (denseRules && prevRow2 && ruleBetween(prevRow2, row) && !pendingLabel) {
1019
+ const pc = pendingCells.get(b);
1020
+ if (pc) {
1021
+ pc.text = `${pc.text} ${c.run.text.trim()}`.trim();
1022
+ pc.idx.push(...cellIdx(c));
1023
+ } else pendingCells.set(b, { text: c.run.text.trim(), idx: cellIdx(c), row });
1024
+ continue;
1025
+ }
996
1026
  flushPending();
997
1027
  grid[grid.length - 1][b] = (grid[grid.length - 1][b] + " " + c.run.text.trim()).trim();
998
1028
  lineIdx.push(...cellIdx(c));
@@ -1018,7 +1048,16 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1018
1048
  continue;
1019
1049
  }
1020
1050
  const prevRow = gridRows[gridRows.length - 1];
1021
- if (prevRow && !pendingLabel && row.y - prevRow.y > medPitch * 1.7 && grid.length) {
1051
+ const prevLastY = (() => {
1052
+ let y = prevRow?.y ?? row.y;
1053
+ for (let q = r - 1; q >= 0 && blockRows[q] !== prevRow; q--) {
1054
+ if (blockRows[q].cells.length === 1) {
1055
+ y = Math.max(y, blockRows[q].y);
1056
+ } else break;
1057
+ }
1058
+ return y;
1059
+ })();
1060
+ if (prevRow && !pendingLabel && !pendingCells.size && row.y - prevLastY > medPitch * 1.7 && grid.length) {
1022
1061
  grid.push(bands.map(() => ""));
1023
1062
  gridRows.push(row);
1024
1063
  }
@@ -1028,11 +1067,24 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1028
1067
  lineIdx.push(...pendingLabel.idx);
1029
1068
  pendingLabel = null;
1030
1069
  }
1070
+ if (pendingCells.size) {
1071
+ gridRows[gridRows.length - 1];
1072
+ const pcRow = [...pendingCells.values()][0].row;
1073
+ if (denseRules && ruleBetween(pcRow, row)) flushPendingCells();
1074
+ else {
1075
+ for (const [b, pc] of pendingCells) {
1076
+ cells[b] = `${pc.text} ${cells[b]}`.trim();
1077
+ lineIdx.push(...pc.idx);
1078
+ }
1079
+ pendingCells.clear();
1080
+ }
1081
+ }
1031
1082
  grid.push(cells);
1032
1083
  gridRows.push(row);
1033
1084
  for (const c of row.cells) lineIdx.push(...cellIdx(c));
1034
1085
  }
1035
1086
  flushPending();
1087
+ flushPendingCells();
1036
1088
  for (let r = 1; r < grid.length; r++) {
1037
1089
  const cur2 = grid[r], prev = grid[r - 1];
1038
1090
  if (cur2[0] !== "" || !prev[0] || !prev.some(Boolean) || !cur2.some(Boolean)) continue;
@@ -1066,6 +1118,13 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1066
1118
  i++;
1067
1119
  continue;
1068
1120
  }
1121
+ const fillRatio = grid.flat().filter(Boolean).length / Math.max(1, grid.length * bands.length);
1122
+ const multiFilled = grid.filter((r) => r.filter(Boolean).length >= 2).length;
1123
+ if (grid.length <= 3 && fillRatio < 0.6 || fillRatio < 0.5 && multiFilled <= 2 || bands.length >= 6 && fillRatio < 0.3) {
1124
+ dbg("reject sparse block", grid[0]);
1125
+ i++;
1126
+ continue;
1127
+ }
1069
1128
  const first = blockRows[0];
1070
1129
  const headerBg = rowFill(first);
1071
1130
  const firstBold = first.cells.every((c) => c.run.bold);
@@ -1082,7 +1141,8 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1082
1141
  const firstRule = ruleYs.find((y) => y > gridRows[0].y + gridRows[0].h * 0.5);
1083
1142
  if (firstRule != null) {
1084
1143
  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);
1144
+ const figuresAbove = grid.slice(0, above).some((row) => row.some((v, k) => isNumCol[k] && v && numeric(v)));
1145
+ if (above >= 1 && above <= 5 && gridRows.length - above >= 2 && !figuresAbove) headerRows = Math.max(headerRows, above);
1086
1146
  }
1087
1147
  }
1088
1148
  if (numCols > 0) {
@@ -1099,17 +1159,77 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1099
1159
  headerRows = Math.max(headerRows, h);
1100
1160
  }
1101
1161
  if (headerRows > 0 && grid.length - headerRows < 1) headerRows = 0;
1102
- const columns = bands.map((b, k) => {
1162
+ const shortVerticals = gridShapes.filter(({ s }) => isRule(s) && s.height > s.width).length;
1163
+ const hasGrid = verticalRules >= 2 || shortVerticals >= 2 && denseRules && ruleYs.length >= 2;
1164
+ const anyVertical = shortVerticals >= 2;
1165
+ if (hasGrid || anyVertical) {
1166
+ const bx0 = Math.min(...bands.map((b) => b.x0)), bx1 = Math.max(...bands.map((b) => b.x1));
1167
+ const partial = gridShapes.filter(({ s }) => isRule(s) && s.width >= s.height && s.width >= tableW * 0.3 && (s.x > bx0 + 3 || s.x + s.width < bx1 - 3)).length;
1168
+ if (partial >= 2) {
1169
+ dbg("reject rowspan grid", grid[0]);
1170
+ i++;
1171
+ continue;
1172
+ }
1173
+ }
1174
+ let colBands = bands;
1175
+ let colGrid = grid;
1176
+ let colNum = isNumCol;
1177
+ let edges = null;
1178
+ if (hasGrid) {
1179
+ const acc = /* @__PURE__ */ new Map();
1180
+ const blockH = blockRows[blockRows.length - 1].y + blockRows[blockRows.length - 1].h - blockRows[0].y;
1181
+ for (const { s } of gridShapes) {
1182
+ if (!(isRule(s) && s.height > s.width)) continue;
1183
+ const cx = s.x + s.width / 2;
1184
+ const key = [...acc.keys()].find((x) => Math.abs(x - cx) < 1) ?? cx;
1185
+ acc.set(key, (acc.get(key) ?? 0) + s.height);
1186
+ }
1187
+ const xs = [...acc.entries()].filter(([, h]) => h >= blockH * 0.5).map(([x]) => x).sort((a, b) => a - b);
1188
+ dbg(`grid rules x=${xs.map((x) => x.toFixed(1)).join(",")} bands=${bands.length}`);
1189
+ if (xs.length >= 3) {
1190
+ const bandCol = bands.map((b) => {
1191
+ const c = (b.x0 + b.x1) / 2;
1192
+ return xs.findIndex((x, k) => k < xs.length - 1 && c > x && c < xs[k + 1]);
1193
+ });
1194
+ const straddles = (c) => xs.some((x) => c.x0 < x - 1.5 && c.x1 > x + 1.5);
1195
+ const crowded = bandCol.some((c, k) => c >= 0 && bandCol.indexOf(c) !== k);
1196
+ const inside = !crowded && !blockRows.some((row) => row.cells.some(straddles));
1197
+ if (!inside) {
1198
+ dbg("reject merged-cell grid", grid[0]);
1199
+ i++;
1200
+ continue;
1201
+ }
1202
+ if (xs.length - 1 >= bands.length && bandCol.every((c) => c >= 0) && new Set(bandCol).size === bandCol.length) {
1203
+ edges = xs;
1204
+ colBands = xs.slice(0, -1).map((x, k) => ({ x0: x, x1: xs[k + 1] }));
1205
+ colGrid = grid.map((row) => {
1206
+ const out2 = colBands.map(() => "");
1207
+ row.forEach((v, k) => {
1208
+ out2[bandCol[k]] = v;
1209
+ });
1210
+ return out2;
1211
+ });
1212
+ colNum = colBands.map((_, k) => {
1213
+ const b = bandCol.indexOf(k);
1214
+ return b >= 0 && isNumCol[b];
1215
+ });
1216
+ }
1217
+ }
1218
+ }
1219
+ const columns = colBands.map((b, k) => {
1103
1220
  const col = { width: Math.round((b.x1 - b.x0) * 10) / 10 };
1104
- if (isNumCol[k]) col.align = "right";
1221
+ if (colNum[k]) col.align = "right";
1105
1222
  return col;
1106
1223
  });
1107
- const x0 = Math.max(0, bands[0].x0 - 2.5);
1108
- const x1 = Math.min(pageW, bands[bands.length - 1].x1 + 2.5);
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;
1224
+ const x0 = edges ? edges[0] : Math.max(0, bands[0].x0 - 2.5);
1225
+ const x1 = edges ? edges[edges.length - 1] : Math.min(pageW, bands[bands.length - 1].x1 + 2.5);
1226
+ if (!edges) {
1227
+ edges = [x0];
1228
+ 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))));
1229
+ edges.push(Math.max(edges[edges.length - 1] + 2, x1));
1230
+ }
1231
+ for (let k = 0; k < colBands.length; k++) columns[k].width = Math.round((edges[k + 1] - edges[k]) * 10) / 10;
1232
+ const colOfBand = colBands === bands ? bands.map((_, k) => k) : bands.map((b) => colBands.findIndex((cb) => (b.x0 + b.x1) / 2 > cb.x0 && (b.x0 + b.x1) / 2 < cb.x1));
1113
1233
  const bodyRows = gridRows.slice(headerRows).filter((r, k, arr) => grid[headerRows + k]?.some(Boolean));
1114
1234
  const rowFills = bodyRows.map(rowFill).map((f) => f?.fill ?? null);
1115
1235
  const odd = rowFills.filter((_, k) => k % 2 === 1), even = rowFills.filter((_, k) => k % 2 === 0);
@@ -1118,7 +1238,6 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1118
1238
  const altColor = oddAlt || evenAlt;
1119
1239
  const borderShape = gridShapes.find(({ s }) => isRule(s));
1120
1240
  const borderColor = borderShape ? borderShape.s.stroke || borderShape.s.fill : void 0;
1121
- const hasGrid = verticalRules >= 2;
1122
1241
  const sizeChars = /* @__PURE__ */ new Map();
1123
1242
  for (const row of blockRows) for (const c of row.cells) {
1124
1243
  const k = Math.round(c.run.fontSize * 2) / 2;
@@ -1129,26 +1248,74 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1129
1248
  const padV = Math.max(0.2, Math.min(2.5, (medPitch - lineMm) / 2));
1130
1249
  const padH = Math.max(0.5, Math.min(3, fontSize * PT_TO_MM * 0.35));
1131
1250
  const y0 = headerBg ? Math.min(...headerBg.rects.map(({ s }) => s.y)) : first.y - padV;
1251
+ const tally = (pick) => {
1252
+ const m = /* @__PURE__ */ new Map();
1253
+ for (const row of blockRows) for (const c of row.cells) {
1254
+ const k = pick(c);
1255
+ if (k) m.set(k, (m.get(k) ?? 0) + c.run.text.trim().length);
1256
+ }
1257
+ return [...m.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
1258
+ };
1259
+ const tableFamily = tally((c) => c.run.family);
1260
+ const tableColor = tally((c) => c.run.color) ?? "#000000";
1261
+ const runsAt = (r, b) => {
1262
+ const src = gridRows[r];
1263
+ if (!src) return [];
1264
+ const own = src.cells.filter((c) => assign.get(c) === b).map((c) => c.run);
1265
+ if (own.length || b !== 0) return own;
1266
+ const at = blockRows.indexOf(src);
1267
+ for (let q = at - 1; q >= Math.max(0, at - 3); q--) {
1268
+ const row = blockRows[q];
1269
+ if (row.cells.length !== 1) break;
1270
+ if ((assign.get(row.cells[0]) ?? 0) === 0) return [row.cells[0].run];
1271
+ }
1272
+ return [];
1273
+ };
1274
+ const styledCell = (text, runs2) => {
1275
+ if (!text || !runs2.length) return text;
1276
+ const st = {};
1277
+ const chars = (f) => runs2.filter(f).reduce((a, r) => a + r.text.trim().length, 0) / Math.max(1, runs2.reduce((a, r) => a + r.text.trim().length, 0));
1278
+ if (chars((r) => !!r.bold) >= 0.5) st.fontWeight = "bold";
1279
+ if (chars((r) => !!r.italic) >= 0.5) st.fontStyle = "italic";
1280
+ const color = runs2.slice().sort((a, b) => b.text.trim().length - a.text.trim().length)[0].color;
1281
+ if (color && color !== tableColor) st.color = color;
1282
+ const fam = runs2.slice().sort((a, b) => b.text.trim().length - a.text.trim().length)[0].family;
1283
+ if (fam && tableFamily && fam !== tableFamily) st.fontFamily = fam;
1284
+ const size = Math.round(runs2[0].fontSize * 2) / 2;
1285
+ if (Math.abs(size - fontSize) >= 1) st.fontSize = size;
1286
+ return Object.keys(st).length ? { content: text, style: st } : text;
1287
+ };
1288
+ const bodyStart = (() => {
1289
+ let k = headerRows;
1290
+ while (k < colGrid.length - 1 && colGrid[k].every((c) => !c)) k++;
1291
+ return k;
1292
+ })();
1293
+ const styledRows = colGrid.slice(bodyStart).map((row, ri) => {
1294
+ const r = bodyStart + ri;
1295
+ return row.map((text, col) => {
1296
+ const b = colOfBand.indexOf(col);
1297
+ return styledCell(text, b >= 0 ? runsAt(r, b) : []);
1298
+ });
1299
+ });
1300
+ const tableStyle = { fontSize, lineHeight: 1.2, padding: `${Math.round(padV * 100) / 100}mm ${Math.round(padH * 100) / 100}mm` };
1301
+ if (tableFamily) tableStyle.fontFamily = tableFamily;
1302
+ if (tableColor !== "#000000") tableStyle.color = tableColor;
1132
1303
  const element = {
1133
1304
  type: "table",
1134
1305
  position: { x: Math.round(x0 * 100) / 100, y: Math.round(Math.max(0, y0) * 100) / 100 },
1135
1306
  width: Math.round((x1 - x0) * 100) / 100,
1136
1307
  columns,
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` }
1308
+ rows: styledRows,
1309
+ style: tableStyle
1143
1310
  };
1144
1311
  if (headerRows > 0) {
1145
- element.headers = bands.map((_, k) => grid.slice(0, headerRows).map((r) => r[k]).filter(Boolean).join(" ").trim());
1312
+ element.headers = colBands.map((_, k) => colGrid.slice(0, headerRows).map((r) => r[k]).filter(Boolean).join(" ").trim());
1146
1313
  const hs = { fontWeight: "bold" };
1147
1314
  if (headerBg) hs.backgroundColor = headerBg.fill;
1148
1315
  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];
1316
+ const tally2 = /* @__PURE__ */ new Map();
1317
+ for (const c of blockRows.slice(0, headerRows).flatMap((r) => r.cells)) tally2.set(c.run.color, (tally2.get(c.run.color) ?? 0) + c.run.text.length);
1318
+ const hc = [...tally2.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
1152
1319
  if (hc && hc !== "#000000") hs.color = hc;
1153
1320
  }
1154
1321
  element.headerStyle = hs;
@@ -1163,13 +1330,13 @@ function detectTables(runs, shapes, pageWidthMm, gutters = []) {
1163
1330
  if (headerBg) for (const { k } of headerBg.rects) fillRects.add(k);
1164
1331
  if (headerRows > 0) {
1165
1332
  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);
1333
+ for (const { s, k } of gridShapes) if (s.kind === "rect" && s.fill && s.fill.toLowerCase() !== "#ffffff" && !isRule(s) && s.width >= tableW * 0.5 && s.y < hy1 && s.y + s.height > hy0 && s.y >= hy0 - gridRows[0].h) fillRects.add(k);
1167
1334
  }
1168
1335
  if (altColor) for (const row of blockRows) {
1169
1336
  const f = rowFill(row);
1170
1337
  if (f) for (const { k } of f.rects) fillRects.add(k);
1171
1338
  }
1172
- const consumedShapes = gridShapes.filter(({ s, k }) => isRule(s) || fillRects.has(k)).map(({ k }) => k);
1339
+ const consumedShapes = gridShapes.filter(({ s, k }) => hasGrid && isRule(s) || fillRects.has(k)).map(({ k }) => k);
1173
1340
  dbg(`table ${grid.length} rows \xD7 ${bands.length} cols at y=${element.position.y}`, element.headers ?? grid[0]);
1174
1341
  for (const k of lineIdx) used.add(k);
1175
1342
  out.push({ element, lineIdx, shapeIdx: consumedShapes });
@@ -1317,9 +1484,12 @@ function merge(para, meta, pageWmm) {
1317
1484
  const pitch = (last.position.y - first.position.y) / (para.length - 1);
1318
1485
  const width = Math.min(pageWmm - x, (right - x) * 1.2 + lineH * 0.4);
1319
1486
  const joiner = () => "\n";
1320
- const style = { ...first.style ?? {}, lineHeight: Math.round(pitch / lineH * 100) / 100, ...indent > 0.5 ? { textIndent: Math.round(indent * 100) / 100 } : {} };
1487
+ const lineHeight = Math.round(pitch / lineH * 100) / 100;
1488
+ const style = { ...first.style ?? {}, lineHeight, ...indent > 0.5 ? { textIndent: Math.round(indent * 100) / 100 } : {} };
1321
1489
  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 };
1490
+ const y = Math.max(0, first.position.y - (lineHeight - 1) / 2 * lineH);
1491
+ const height = Math.round(para.length * lineHeight * lineH * 100) / 100;
1492
+ const base = { position: { x, y: Math.round(y * 100) / 100 }, width: Math.round(width * 100) / 100, height, style };
1323
1493
  if (allText) {
1324
1494
  const content = para.map((e) => String(e.content ?? "").trim().replace(/[ \t]+/g, " ")).join(joiner());
1325
1495
  const { height: _hh2, ...firstRest } = first;
@@ -1337,18 +1507,123 @@ function merge(para, meta, pageWmm) {
1337
1507
 
1338
1508
  // ../../packages/jdf-pdf-import/src/core.ts
1339
1509
  var PT_TO_MM3 = 0.352778;
1340
- function classifyFont(name) {
1341
- const n = (name || "").toLowerCase();
1342
- const bold = /bold|black|heavy|semibold|demibold|extrabold/.test(n);
1343
- const italic = /italic|oblique/.test(n);
1344
- let family = "Inter, Helvetica, Arial, sans-serif";
1345
- if (n.includes("times") || n.includes("serif") || n.includes("roman") || n.includes("georgia") || n.includes("garamond") || n.includes("baskerville")) {
1346
- family = "Times New Roman, serif";
1347
- } else if (n.includes("courier") || n.includes("mono") || n.includes("consolas") || n.includes("menlo") || n.includes("source code") || n.includes("fira code")) {
1348
- family = "JetBrains Mono, ui-monospace, monospace";
1349
- } else if (n.includes("helvetica") || n.includes("arial") || n.includes("sans") || n.includes("roboto") || n.includes("inter") || n.includes("noto")) {
1350
- family = "Inter, Helvetica, Arial, sans-serif";
1510
+ var SYMBOL_PUA = {
1511
+ 61623: "\u2022",
1512
+ 61607: "\u25AA",
1513
+ 61608: "\u25FB",
1514
+ 61618: "\u25A0",
1515
+ 61600: "\u25E6",
1516
+ 61616: "\u25E6",
1517
+ 61558: "\u2756",
1518
+ 61656: "\u27A2",
1519
+ 61692: "\u2713",
1520
+ 61694: "\u2611",
1521
+ 61603: "\u2610",
1522
+ 61680: "\u21E8",
1523
+ 61664: "\u2192",
1524
+ 61614: "\u2192",
1525
+ 61613: "\u2190",
1526
+ 61622: "\xB6",
1527
+ 61485: "\u2212",
1528
+ 61630: "\u2015",
1529
+ 61655: "\u2022",
1530
+ 61604: "\u25C6",
1531
+ 61686: "\u2716",
1532
+ 61691: "\u2717",
1533
+ 61672: "\u21D2",
1534
+ 61648: "\u2014"
1535
+ };
1536
+ function mapSymbolGlyphs(str) {
1537
+ let out = "";
1538
+ for (const ch of str) {
1539
+ const c = ch.codePointAt(0);
1540
+ if (c >= 57344 && c <= 63743) {
1541
+ const m = SYMBOL_PUA[c];
1542
+ out += m ?? "\u2022";
1543
+ } else out += ch;
1544
+ }
1545
+ return out;
1546
+ }
1547
+ function probeFontProgram(data) {
1548
+ if (!(data instanceof Uint8Array) || data.length < 12) return null;
1549
+ try {
1550
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
1551
+ let off = 0;
1552
+ if (dv.getUint32(0) === 1953784678) off = dv.getUint32(12);
1553
+ const numTables = dv.getUint16(off + 4);
1554
+ if (numTables > 64) return null;
1555
+ const out = { flags: {} };
1556
+ let found = false;
1557
+ for (let i = 0; i < numTables; i++) {
1558
+ const rec = off + 12 + i * 16;
1559
+ if (rec + 16 > data.length) break;
1560
+ const tag = String.fromCharCode(dv.getUint8(rec), dv.getUint8(rec + 1), dv.getUint8(rec + 2), dv.getUint8(rec + 3));
1561
+ const toff = dv.getUint32(rec + 8), tlen = dv.getUint32(rec + 12);
1562
+ if (toff + tlen > data.length) continue;
1563
+ if (tag === "name") {
1564
+ const count = dv.getUint16(toff + 2), strOff = dv.getUint16(toff + 4);
1565
+ for (let k = 0; k < count; k++) {
1566
+ const r = toff + 6 + k * 12;
1567
+ if (r + 12 > data.length) break;
1568
+ const pid = dv.getUint16(r), nid = dv.getUint16(r + 6), len = dv.getUint16(r + 8), so = dv.getUint16(r + 10);
1569
+ if (nid !== 1 && nid !== 2) continue;
1570
+ const start = toff + strOff + so;
1571
+ if (start + len > data.length) continue;
1572
+ let str = "";
1573
+ if (pid === 3 || pid === 0) {
1574
+ for (let j = 0; j + 1 < len; j += 2) str += String.fromCharCode(dv.getUint16(start + j));
1575
+ } else {
1576
+ for (let j = 0; j < len; j++) str += String.fromCharCode(dv.getUint8(start + j));
1577
+ }
1578
+ str = str.replace(/\0/g, "").trim();
1579
+ if (!str) continue;
1580
+ if (nid === 1 && !out.family) out.family = str;
1581
+ if (nid === 2 && !out.subfamily) out.subfamily = str;
1582
+ found = true;
1583
+ }
1584
+ } else if (tag === "OS/2" && tlen >= 42) {
1585
+ const p = Array.from(data.subarray(toff + 32, toff + 42));
1586
+ const familyClass = dv.getUint8(toff + 30);
1587
+ if (p[0] === 2) {
1588
+ if (p[1] >= 11 && p[1] <= 15) out.flags.serif = false;
1589
+ else if (p[1] >= 2 && p[1] <= 10) out.flags.serif = true;
1590
+ if (p[2] >= 7) out.flags.bold = true;
1591
+ if (p[3] === 9) out.flags.monospace = true;
1592
+ if (p[7] >= 9 && p[7] <= 15) out.flags.italic = true;
1593
+ found = true;
1594
+ } else if (familyClass >= 1 && familyClass <= 7) {
1595
+ out.flags.serif = true;
1596
+ found = true;
1597
+ } else if (familyClass === 8) {
1598
+ out.flags.serif = false;
1599
+ found = true;
1600
+ }
1601
+ }
1602
+ }
1603
+ if (out.family && (/^[A-Z]{6}\+/.test(out.family) || /^\d+$/.test(out.family))) out.family = void 0;
1604
+ return found ? out : null;
1605
+ } catch {
1606
+ return null;
1351
1607
  }
1608
+ }
1609
+ function classifyFont(name, flags = {}) {
1610
+ const n = (name || "").toLowerCase().replace(/[\s_-]+/g, "");
1611
+ const bold = flags.bold || /bold|black|heavy|semibold|demibold|extrabold/.test(n);
1612
+ const italic = flags.italic || /italic|oblique/.test(n);
1613
+ let family;
1614
+ if (n.includes("calibri") || n.includes("carlito")) family = "Carlito, Calibri, sans-serif";
1615
+ else if (n.includes("cambria")) family = "Caladea, Cambria, serif";
1616
+ else if (n.includes("georgia")) family = "Gelasio, Georgia, serif";
1617
+ else if (n.includes("courier") || n.includes("mono") || n.includes("consolas") || n.includes("menlo") || n.includes("sourcecode") || n.includes("firacode")) family = "Cousine, 'Courier New', JetBrains Mono, monospace";
1618
+ else if (n.includes("times") || n.includes("roman") || n.includes("garamond") || n.includes("baskerville") || n.includes("bookantiqua") || n.includes("palatino") || n.includes("minion")) family = "Tinos, 'Times New Roman', serif";
1619
+ else if (n.includes("plexsans")) family = "'IBM Plex Sans', Arimo, sans-serif";
1620
+ else if (n.includes("plexserif")) family = "'IBM Plex Serif', Tinos, serif";
1621
+ else if (n.includes("arial") || n.includes("helvetica") || n.includes("liberationsans") || n.includes("arimo")) family = "Arimo, Arial, Helvetica, sans-serif";
1622
+ else if (n.includes("segoe") || n.includes("verdana") || n.includes("tahoma") || n.includes("roboto") || n.includes("inter") || n.includes("noto") || n.includes("opensans") || n.includes("lato") || n.includes("aptos") || n.includes("sans")) family = "Arimo, Arial, Helvetica, sans-serif";
1623
+ else if (n.includes("serif")) family = "Tinos, 'Times New Roman', serif";
1624
+ else if (flags.monospace) family = "Cousine, 'Courier New', monospace";
1625
+ else if (flags.serif === true) family = "Tinos, 'Times New Roman', serif";
1626
+ else family = "Arimo, Arial, Helvetica, sans-serif";
1352
1627
  return {
1353
1628
  family,
1354
1629
  weight: bold ? "bold" : void 0,
@@ -1417,6 +1692,33 @@ function averageStops(stops) {
1417
1692
  }
1418
1693
  return n ? rgbToHex(r / n, g / n, b / n) : null;
1419
1694
  }
1695
+ function irToGradient(ir, toVp, m) {
1696
+ if (!Array.isArray(ir) || ir[0] !== "RadialAxial" || !Array.isArray(ir[3])) return null;
1697
+ const stops = ir[3].filter((st) => Array.isArray(st) && typeof st[1] === "string").map((st) => ({ offset: Math.round(st[0] * 1e3) / 1e3, color: st[1] }));
1698
+ if (stops.length < 2) return null;
1699
+ const type = ir[2] === 3 ? "radial" : "linear";
1700
+ const g = { type, stops };
1701
+ if (type === "linear" && Array.isArray(ir[4]) && Array.isArray(ir[5])) {
1702
+ const p = (pt) => {
1703
+ const q = m ? tx(m, pt[0], pt[1]) : { x: pt[0], y: pt[1] };
1704
+ return toVp(q.x, q.y);
1705
+ };
1706
+ const a = p(ir[4]), b = p(ir[5]);
1707
+ g.angle = Math.round((Math.atan2(b.x - a.x, -(b.y - a.y)) * 180 / Math.PI + 360) % 360);
1708
+ }
1709
+ return g;
1710
+ }
1711
+ function patternIR(page, arg) {
1712
+ if (!Array.isArray(arg) || arg[0] !== "Shading") return null;
1713
+ const id = arg[1];
1714
+ try {
1715
+ const store = typeof id === "string" && id.startsWith("g_") ? page.commonObjs : page.objs;
1716
+ if (typeof store?.has === "function" && !store.has(id)) return null;
1717
+ return store.get(id);
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ }
1420
1722
  function patternToColor(page, arg) {
1421
1723
  if (!Array.isArray(arg)) return null;
1422
1724
  if (arg[0] === "TilingPattern") {
@@ -1456,10 +1758,21 @@ async function walkOps(page, OPS, viewport) {
1456
1758
  const opList = await page.getOperatorList({ annotationMode: annotationModeDisable });
1457
1759
  const fnArr = opList.fnArray;
1458
1760
  const argsArr = opList.argsArray;
1761
+ let opIndex = 0;
1459
1762
  const gs = {
1460
1763
  ctm: [1, 0, 0, 1, 0, 0],
1764
+ // Gradient behind the current fill colour (a shading pattern set with scn);
1765
+ // `fill` keeps the average colour so exporters without gradients still paint.
1766
+ fillGradient: void 0,
1767
+ // Clip path in viewport space (set by W / W*); `sh` paints exactly this area.
1768
+ clip: void 0,
1461
1769
  fill: "#000000",
1462
1770
  stroke: "#000000",
1771
+ // Blend mode from the ExtGState (BM). JDF has no blend modes; a multiply /
1772
+ // darken layer is approximated by opacity ∝ how dark it is (a light-grey
1773
+ // "light streak" over a blue banner becomes nearly transparent instead of
1774
+ // an opaque grey blob), a screen / lighten layer by how light it is.
1775
+ blend: "normal",
1463
1776
  lineWidth: 1,
1464
1777
  fillAlpha: 1,
1465
1778
  strokeAlpha: 1,
@@ -1473,6 +1786,24 @@ async function walkOps(page, OPS, viewport) {
1473
1786
  rise: 0
1474
1787
  };
1475
1788
  const snapshot = () => ({ ...gs, ctm: [...gs.ctm] });
1789
+ const luma = (hex) => {
1790
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex || "");
1791
+ return m ? (0.2126 * parseInt(m[1], 16) + 0.7152 * parseInt(m[2], 16) + 0.0722 * parseInt(m[3], 16)) / 255 : 0.5;
1792
+ };
1793
+ const groupStack = [];
1794
+ const isNormal = (b) => b === "normal" || b === "source-over" || b === "compatible";
1795
+ const fillOpacity = (color) => {
1796
+ let alpha = gs.fillAlpha;
1797
+ let b = gs.blend;
1798
+ for (let q = groupStack.length - 1; q >= 0; q--) {
1799
+ alpha *= groupStack[q].alpha;
1800
+ if (isNormal(b)) b = groupStack[q].blend;
1801
+ }
1802
+ const l = luma(color ?? gs.fill);
1803
+ if (b === "multiply" || b === "darken" || b === "color-burn" || b === "colorburn") return alpha * Math.max(0.08, 1 - l);
1804
+ if (b === "screen" || b === "lighten" || b === "color-dodge" || b === "colordodge") return alpha * Math.max(0.08, l);
1805
+ return alpha;
1806
+ };
1476
1807
  const stack = [];
1477
1808
  const textOps = [];
1478
1809
  const shapes = [];
@@ -1486,6 +1817,7 @@ async function walkOps(page, OPS, viewport) {
1486
1817
  const minX = Math.min(...xs), maxX = Math.max(...xs);
1487
1818
  const minY = Math.min(...ys), maxY = Math.max(...ys);
1488
1819
  imagePositions.push({
1820
+ seq: opIndex,
1489
1821
  name,
1490
1822
  x: minX * PT_TO_MM3,
1491
1823
  y: minY * PT_TO_MM3,
@@ -1500,20 +1832,55 @@ async function walkOps(page, OPS, viewport) {
1500
1832
  let pathRects = [];
1501
1833
  let pathStart = null;
1502
1834
  let pathLast = null;
1835
+ function clipFromPath() {
1836
+ const rects = [...pathRects, ...pathRect ? [pathRect] : []];
1837
+ const pts = [];
1838
+ let d = "";
1839
+ if (pathSegments.length) {
1840
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1841
+ const vp = pathSegments.map((seg) => {
1842
+ if (seg.type === "Z") return seg;
1843
+ const out = [];
1844
+ for (let k = 0; k < seg.pts.length; k += 2) {
1845
+ const v = toViewport(seg.pts[k], seg.pts[k + 1]);
1846
+ out.push(v.x, v.y);
1847
+ if (v.x < minX) minX = v.x;
1848
+ if (v.x > maxX) maxX = v.x;
1849
+ if (v.y < minY) minY = v.y;
1850
+ if (v.y > maxY) maxY = v.y;
1851
+ }
1852
+ return { type: seg.type, pts: out };
1853
+ });
1854
+ if (!isFinite(minX)) return void 0;
1855
+ d = vp.map((seg) => seg.type === "Z" ? "Z" : `${seg.type} ${Array.from({ length: seg.pts.length / 2 }, (_, k) => `${((seg.pts[2 * k] - minX) * PT_TO_MM3).toFixed(2)} ${((seg.pts[2 * k + 1] - minY) * PT_TO_MM3).toFixed(2)}`).join(" ")}`).join(" ");
1856
+ return { d, x: minX, y: minY, w: maxX - minX, h: maxY - minY };
1857
+ }
1858
+ if (rects.length) {
1859
+ for (const r of rects) {
1860
+ const a = toViewport(r.x, r.y), b = toViewport(r.x + r.w, r.y + r.h);
1861
+ pts.push([Math.min(a.x, b.x), Math.min(a.y, b.y), Math.max(a.x, b.x), Math.max(a.y, b.y)]);
1862
+ }
1863
+ const x0 = Math.min(...pts.map((p) => p[0])), y0 = Math.min(...pts.map((p) => p[1])), x1 = Math.max(...pts.map((p) => p[2])), y1 = Math.max(...pts.map((p) => p[3]));
1864
+ return { d: "", x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
1865
+ }
1866
+ return void 0;
1867
+ }
1503
1868
  function flushPath(isFill, isStroke) {
1504
1869
  for (const r of pathRects) {
1505
1870
  const tl = toViewport(r.x, r.y + r.h);
1506
1871
  const br = toViewport(r.x + r.w, r.y);
1507
1872
  shapes.push({
1873
+ seq: opIndex,
1508
1874
  kind: "rect",
1509
1875
  x: Math.min(tl.x, br.x) * PT_TO_MM3,
1510
1876
  y: Math.min(tl.y, br.y) * PT_TO_MM3,
1511
1877
  width: Math.abs(br.x - tl.x) * PT_TO_MM3,
1512
1878
  height: Math.abs(br.y - tl.y) * PT_TO_MM3,
1513
1879
  fill: isFill ? gs.fill : void 0,
1880
+ gradient: isFill ? gs.fillGradient : void 0,
1514
1881
  stroke: isStroke ? gs.stroke : void 0,
1515
1882
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1516
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
1883
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha
1517
1884
  });
1518
1885
  }
1519
1886
  pathRects = [];
@@ -1525,15 +1892,17 @@ async function walkOps(page, OPS, viewport) {
1525
1892
  const w = Math.abs(br.x - tl.x);
1526
1893
  const h = Math.abs(br.y - tl.y);
1527
1894
  shapes.push({
1895
+ seq: opIndex,
1528
1896
  kind: "rect",
1529
1897
  x: x * PT_TO_MM3,
1530
1898
  y: y * PT_TO_MM3,
1531
1899
  width: w * PT_TO_MM3,
1532
1900
  height: h * PT_TO_MM3,
1533
1901
  fill: isFill ? gs.fill : void 0,
1902
+ gradient: isFill ? gs.fillGradient : void 0,
1534
1903
  stroke: isStroke ? gs.stroke : void 0,
1535
1904
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1536
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
1905
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha
1537
1906
  });
1538
1907
  } else if (pathSegments.length === 2 && pathSegments[0].type === "M" && pathSegments[1].type === "L") {
1539
1908
  const a = pathSegments[0].pts;
@@ -1555,6 +1924,7 @@ async function walkOps(page, OPS, viewport) {
1555
1924
  const axisAligned = dx < 0.5 || dy < 0.5;
1556
1925
  if (axisAligned) {
1557
1926
  shapes.push({
1927
+ seq: opIndex,
1558
1928
  kind: "line",
1559
1929
  x: minX * PT_TO_MM3,
1560
1930
  y: minY * PT_TO_MM3,
@@ -1566,6 +1936,7 @@ async function walkOps(page, OPS, viewport) {
1566
1936
  });
1567
1937
  } else {
1568
1938
  shapes.push({
1939
+ seq: opIndex,
1569
1940
  kind: "path",
1570
1941
  x: minX * PT_TO_MM3,
1571
1942
  y: minY * PT_TO_MM3,
@@ -1610,17 +1981,21 @@ async function walkOps(page, OPS, viewport) {
1610
1981
  }
1611
1982
  return `${seg.type} ${p.join(" ")}`;
1612
1983
  }).join(" ");
1984
+ const straight = vpSegments.every((seg) => seg.type === "M" || seg.type === "L" || seg.type === "Z");
1985
+ const thin = isFill && straight && Math.min(bw, bh) * PT_TO_MM3 < 0.6 && Math.max(bw, bh) * PT_TO_MM3 >= 1;
1613
1986
  shapes.push({
1614
- kind: "path",
1987
+ seq: opIndex,
1988
+ kind: thin ? "rect" : "path",
1615
1989
  x: minX * PT_TO_MM3,
1616
1990
  y: minY * PT_TO_MM3,
1617
1991
  width: bw * PT_TO_MM3,
1618
1992
  height: bh * PT_TO_MM3,
1619
1993
  fill: isFill ? gs.fill : void 0,
1994
+ gradient: isFill ? gs.fillGradient : void 0,
1620
1995
  stroke: isStroke ? gs.stroke : void 0,
1621
1996
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1622
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha,
1623
- path: d
1997
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha,
1998
+ path: thin ? void 0 : d
1624
1999
  });
1625
2000
  }
1626
2001
  }
@@ -1630,6 +2005,7 @@ async function walkOps(page, OPS, viewport) {
1630
2005
  pathLast = null;
1631
2006
  }
1632
2007
  for (let i = 0; i < fnArr.length; i++) {
2008
+ opIndex = i;
1633
2009
  const fn = fnArr[i];
1634
2010
  const args = argsArr[i] || [];
1635
2011
  if (fn === OPS.save) {
@@ -1639,11 +2015,15 @@ async function walkOps(page, OPS, viewport) {
1639
2015
  if (s) Object.assign(gs, s);
1640
2016
  } else if (fn === OPS.paintFormXObjectBegin) {
1641
2017
  stack.push(snapshot());
2018
+ groupStack.push({ blend: gs.blend, alpha: gs.fillAlpha });
2019
+ gs.blend = "normal";
2020
+ gs.fillAlpha = 1;
1642
2021
  const matrix = args[0];
1643
2022
  if (Array.isArray(matrix) && matrix.length === 6) gs.ctm = multiplyCtm(matrix, gs.ctm);
1644
2023
  } else if (fn === OPS.paintFormXObjectEnd) {
1645
2024
  const s = stack.pop();
1646
2025
  if (s) Object.assign(gs, s);
2026
+ groupStack.pop();
1647
2027
  } else if (fn === OPS.beginText) {
1648
2028
  tm = [1, 0, 0, 1, 0, 0];
1649
2029
  tlm = [1, 0, 0, 1, 0, 0];
@@ -1675,6 +2055,9 @@ async function walkOps(page, OPS, viewport) {
1675
2055
  } else if (fn === OPS.setFillColorN) {
1676
2056
  const c = patternToColor(page, args[0]);
1677
2057
  if (c) gs.fill = c;
2058
+ const ir = patternIR(page, args[0]);
2059
+ const pm = Array.isArray(args[0]) && Array.isArray(args[0][2]) ? args[0][2] : void 0;
2060
+ gs.fillGradient = ir ? irToGradient(ir, toViewport, pm) ?? void 0 : void 0;
1678
2061
  } else if (fn === OPS.setStrokeColorN) {
1679
2062
  const c = patternToColor(page, args[0]);
1680
2063
  if (c) gs.stroke = c;
@@ -1682,10 +2065,12 @@ async function walkOps(page, OPS, viewport) {
1682
2065
  gs.ctm = multiplyCtm(args, gs.ctm);
1683
2066
  } else if (fn === OPS.setFillRGBColor) {
1684
2067
  gs.fill = rgbToHex(args[0], args[1], args[2]);
2068
+ gs.fillGradient = void 0;
1685
2069
  } else if (fn === OPS.setStrokeRGBColor) {
1686
2070
  gs.stroke = rgbToHex(args[0], args[1], args[2]);
1687
2071
  } else if (fn === OPS.setFillGray) {
1688
2072
  gs.fill = rgbToHex(args[0], args[0], args[0]);
2073
+ gs.fillGradient = void 0;
1689
2074
  } else if (fn === OPS.setStrokeGray) {
1690
2075
  gs.stroke = rgbToHex(args[0], args[0], args[0]);
1691
2076
  } else if (fn === OPS.setFillCMYKColor || fn === OPS.setStrokeCMYKColor) {
@@ -1694,8 +2079,10 @@ async function walkOps(page, OPS, viewport) {
1694
2079
  const g = (1 - m) * (1 - k) * 255;
1695
2080
  const b = (1 - y) * (1 - k) * 255;
1696
2081
  const hex = rgbToHex(r, g, b);
1697
- if (fn === OPS.setFillCMYKColor) gs.fill = hex;
1698
- else gs.stroke = hex;
2082
+ if (fn === OPS.setFillCMYKColor) {
2083
+ gs.fill = hex;
2084
+ gs.fillGradient = void 0;
2085
+ } else gs.stroke = hex;
1699
2086
  } else if (fn === OPS.setLineWidth) {
1700
2087
  gs.lineWidth = args[0];
1701
2088
  } else if (fn === OPS.setTextRenderingMode) {
@@ -1709,6 +2096,7 @@ async function walkOps(page, OPS, viewport) {
1709
2096
  if (key === "LW") gs.lineWidth = val;
1710
2097
  else if (key === "ca") gs.fillAlpha = val;
1711
2098
  else if (key === "CA") gs.strokeAlpha = val;
2099
+ else if (key === "BM") gs.blend = String(Array.isArray(val) ? val[0] : val).toLowerCase();
1712
2100
  }
1713
2101
  }
1714
2102
  } else if (fn === OPS.showText) {
@@ -1793,11 +2181,50 @@ async function walkOps(page, OPS, viewport) {
1793
2181
  const isStroke = fn === OPS.stroke || fn === OPS.fillStroke || fn === OPS.eoFillStroke || fn === OPS.closeFillStroke || fn === OPS.closeStroke || fn === OPS.closeEOFillStroke;
1794
2182
  flushPath(isFill, isStroke);
1795
2183
  } else if (fn === OPS.endPath || fn === OPS.clip || fn === OPS.eoClip) {
2184
+ if (fn !== OPS.endPath) {
2185
+ const c = clipFromPath();
2186
+ const prev = gs.clip;
2187
+ if (c && prev) {
2188
+ const x0 = Math.max(c.x, prev.x), y0 = Math.max(c.y, prev.y);
2189
+ const x1 = Math.min(c.x + c.w, prev.x + prev.w), y1 = Math.min(c.y + c.h, prev.y + prev.h);
2190
+ if (x1 <= x0 || y1 <= y0) gs.clip = { d: "", x: x0, y: y0, w: 0, h: 0 };
2191
+ else if (c.d || !prev.d) gs.clip = c.d ? c : { d: "", x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
2192
+ else gs.clip = prev;
2193
+ } else gs.clip = c ?? prev;
2194
+ }
1796
2195
  pathSegments = [];
1797
2196
  pathRects = [];
1798
2197
  pathRect = null;
1799
2198
  pathStart = null;
1800
2199
  pathLast = null;
2200
+ } else if (fn === OPS.shadingFill) {
2201
+ let ir = args[0];
2202
+ if (typeof ir === "string") {
2203
+ try {
2204
+ const store = ir.startsWith("g_") ? page.commonObjs : page.objs;
2205
+ ir = typeof store?.has === "function" && store.has(ir) ? store.get(ir) : null;
2206
+ } catch {
2207
+ ir = null;
2208
+ }
2209
+ }
2210
+ const g = irToGradient(ir, (x, y) => toViewport(...Object.values(tx(gs.ctm, x, y))));
2211
+ const avg = Array.isArray(ir) && ir[0] === "RadialAxial" ? averageStops(ir[3]) : null;
2212
+ const clipArea = gs.clip;
2213
+ if ((g || avg) && !(clipArea && (clipArea.w < 0.5 || clipArea.h < 0.5))) {
2214
+ const area = clipArea ?? { d: "", x: 0, y: 0, w: viewport.width, h: viewport.height };
2215
+ shapes.push({
2216
+ seq: opIndex,
2217
+ kind: area.d ? "path" : "rect",
2218
+ x: area.x * PT_TO_MM3,
2219
+ y: area.y * PT_TO_MM3,
2220
+ width: Math.max(0.1, area.w * PT_TO_MM3),
2221
+ height: Math.max(0.1, area.h * PT_TO_MM3),
2222
+ fill: avg ?? (g ? g.stops[0].color : void 0),
2223
+ gradient: g ?? void 0,
2224
+ opacity: fillOpacity(avg ?? g?.stops[0].color),
2225
+ path: area.d || void 0
2226
+ });
2227
+ }
1801
2228
  } else if (fn === OPS.paintImageXObject) {
1802
2229
  pushImage(String(args[0]), gs.ctm);
1803
2230
  } else if (fn === OPS.paintImageXObjectRepeat) {
@@ -2108,6 +2535,9 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2108
2535
  // newer Node. Browser entry leaves this unset (= false = real worker
2109
2536
  // via GlobalWorkerOptions.workerSrc); node entry sets `true`.
2110
2537
  disableWorker: runtime.disableWorker === true,
2538
+ // Keep font flags (serif / monospace / bold / italic) on the font objects
2539
+ // so classifyFont can pick a metric-compatible family for unknown names.
2540
+ fontExtraProperties: true,
2111
2541
  isEvalSupported: false,
2112
2542
  // Keep going past malformed content streams instead of failing the page.
2113
2543
  stopAtErrors: false,
@@ -2186,6 +2616,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2186
2616
  for (const k of Object.keys(textContent.styles || {})) {
2187
2617
  const s = textContent.styles[k];
2188
2618
  let realName = s.fontFamily || k;
2619
+ const flags = {};
2189
2620
  try {
2190
2621
  const has = typeof page.commonObjs.has === "function" ? page.commonObjs.has(k) : false;
2191
2622
  if (has) {
@@ -2201,6 +2632,15 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2201
2632
  page.commonObjs.get(k, (font) => {
2202
2633
  if (font?.name) realName = font.name;
2203
2634
  else if (font?.loadedName) realName = font.loadedName;
2635
+ if (font) {
2636
+ const probe = probeFontProgram(font.data);
2637
+ const anonymous = /^(?:[A-Z]{6}\+)?(?:CIDFont\+)?F\d+$/i.test(String(font.name || "")) || !/[a-z]{3}/i.test(String(font.name || "").replace(/^[A-Z]{6}\+/, ""));
2638
+ if (probe?.family && anonymous) realName = probe.family + (probe.subfamily ? " " + probe.subfamily : "");
2639
+ else if (probe?.subfamily && /bold|italic|oblique/i.test(probe.subfamily) && !/bold|italic|oblique/i.test(realName)) realName += " " + probe.subfamily;
2640
+ if (probe) Object.assign(flags, probe.flags);
2641
+ if (flags.monospace == null && font.isMonospace) flags.monospace = true;
2642
+ if (flags.serif == null && !font.isSymbolicFont && font.isSerifFont) flags.serif = true;
2643
+ }
2204
2644
  done();
2205
2645
  });
2206
2646
  } catch {
@@ -2211,7 +2651,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2211
2651
  }
2212
2652
  } catch {
2213
2653
  }
2214
- const cls = classifyFont(realName);
2654
+ const cls = classifyFont(realName, flags);
2215
2655
  if (!cls.weight && /bold/i.test(s.fontFamily || "")) cls.weight = "bold";
2216
2656
  if (!cls.style && /italic|oblique/i.test(s.fontFamily || "")) cls.style = "italic";
2217
2657
  fontMap.set(k, cls);
@@ -2287,11 +2727,15 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2287
2727
  if (mode === 7) return;
2288
2728
  const invisible = mode === 3;
2289
2729
  if (invisible && !keepInvisible) return;
2290
- const ascent = it.height ? safeNum(it.height, fontSize) * 0.78 : fontSize * 0.78;
2730
+ const ascent = fontSize * 0.846;
2291
2731
  const yTop = vy - ascent;
2292
2732
  const w = safeNum(it.width, 0);
2733
+ const skew = fontSize > 0 && Math.abs(safeNum(tr?.[2], 0)) / fontSize > 0.1 && Math.abs(safeNum(tr?.[1], 0)) / fontSize < 0.1;
2734
+ const rotated = fontSize > 0 && Math.abs(safeNum(tr?.[1], 0)) / fontSize > 0.1;
2293
2735
  runs.push({
2294
- text: it.str,
2736
+ text: mapSymbolGlyphs(it.str),
2737
+ ...skew ? { italic: true } : {},
2738
+ ...rotated ? { rotated: true } : {},
2295
2739
  x: safeNum(vx * PT_TO_MM3, 0),
2296
2740
  y: safeNum(yTop * PT_TO_MM3, 0),
2297
2741
  fontSize: safeNum(fontSize, 10),
@@ -2331,7 +2775,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2331
2775
  continue;
2332
2776
  }
2333
2777
  const sameLine = Math.abs(last.y - r.y) <= Y_TOL;
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;
2778
+ const sameStyle = Math.abs(last.fontSize - r.fontSize) < 0.4 && (last.fontName === r.fontName || fontKey(last.fontName) === fontKey(r.fontName)) && last.color === r.color && !!last.italic === !!r.italic && !!last.rotated === !!r.rotated && Math.abs(last.opacity - r.opacity) < 0.05;
2335
2779
  const emMm = r.fontSize * PT_TO_MM3;
2336
2780
  const gapMm = r.x - (last.x + extent(last));
2337
2781
  const symAfterNumber = /^[$€£¥]$/.test(r.text.trim()) && /[\d)]\s*$/.test(last.text);
@@ -2352,7 +2796,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2352
2796
  for (const l of lines) {
2353
2797
  const t = l.text.trim();
2354
2798
  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;
2799
+ const dup = t.length ? recent.find((k) => Math.abs(k.y - l.y) <= 0.9 && Math.abs(k.x - l.x) <= 1.3 && Math.abs(k.fontSize - l.fontSize) < 0.6 && k.text.trim() === t) : void 0;
2356
2800
  if (dup) {
2357
2801
  dup.bold = true;
2358
2802
  continue;
@@ -2366,7 +2810,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2366
2810
  const lineMeta = /* @__PURE__ */ new WeakMap();
2367
2811
  const tRuns = lines.map((l) => {
2368
2812
  const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
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 };
2813
+ 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, italic: cls.style === "italic" || !!l.italic, family: cls.family };
2370
2814
  });
2371
2815
  const sizeChars = /* @__PURE__ */ new Map();
2372
2816
  for (const l of lines) {
@@ -2376,7 +2820,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2376
2820
  const bodyFontSize = [...sizeChars.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 0;
2377
2821
  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
2822
  const pageShapes = mergeCollinearShapes(ops.shapes);
2379
- const detected = options.detectTables === false ? [] : detectTables(tRuns, pageShapes, pageW * PT_TO_MM3, gutters);
2823
+ const isFormPage = formWidgets.length >= 5;
2824
+ const detected = options.detectTables === false || isFormPage ? [] : detectTables(tRuns, pageShapes, pageW * PT_TO_MM3, gutters);
2380
2825
  const consumedLines = /* @__PURE__ */ new Set();
2381
2826
  const consumedShapes = /* @__PURE__ */ new Set();
2382
2827
  const tableAtLine = /* @__PURE__ */ new Map();
@@ -2386,6 +2831,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2386
2831
  tableAtLine.set(Math.min(...t.lineIdx), t.element);
2387
2832
  }
2388
2833
  const pageWmm = pageW * PT_TO_MM3, pageHmm = pageH * PT_TO_MM3;
2834
+ const paintSeq = /* @__PURE__ */ new WeakMap();
2389
2835
  pageShapes.forEach((sh, shapeIdx) => {
2390
2836
  if (consumedShapes.has(shapeIdx)) return;
2391
2837
  if (sh.width < 0.3 && sh.height < 0.3) return;
@@ -2400,6 +2846,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2400
2846
  height: Math.max(0.1, Math.round(sh.height * 100) / 100)
2401
2847
  };
2402
2848
  if (sh.fill) shape.fill = sh.fill;
2849
+ if (sh.gradient) shape.gradient = sh.gradient;
2850
+ paintSeq.set(shape, sh.seq ?? 0);
2403
2851
  if (sh.stroke) shape.stroke = { color: sh.stroke, width: sh.strokeWidth || 0.3 };
2404
2852
  if (shapeType === "path" && sh.path) shape.path = sh.path;
2405
2853
  if (sh.opacity != null && sh.opacity < 0.999) {
@@ -2420,15 +2868,18 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2420
2868
  data: base64
2421
2869
  };
2422
2870
  }
2423
- elements.push({
2871
+ const imgEl = {
2424
2872
  type: "image",
2425
2873
  resource: resourceKey,
2426
2874
  position: { x: Math.round(pos.x * 100) / 100, y: Math.round(pos.y * 100) / 100 },
2427
2875
  width: Math.max(1, Math.round(pos.w * 100) / 100),
2428
2876
  height: Math.max(1, Math.round(pos.h * 100) / 100),
2429
2877
  fit: "fill"
2430
- });
2878
+ };
2879
+ paintSeq.set(imgEl, pos.seq ?? 0);
2880
+ elements.push(imgEl);
2431
2881
  }
2882
+ elements.sort((a, b) => (paintSeq.get(a) ?? 0) - (paintSeq.get(b) ?? 0));
2432
2883
  const rowOf = /* @__PURE__ */ new Map();
2433
2884
  const rowStartOf = /* @__PURE__ */ new Map();
2434
2885
  const nextOnRow = /* @__PURE__ */ new Map();
@@ -2440,7 +2891,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2440
2891
  for (let b = 0; b < order.length; b++) {
2441
2892
  const j = order[b], lj = lines[j];
2442
2893
  const tolY = Math.max(0.6, Math.max(li.fontSize, lj.fontSize) * PT_TO_MM3 * 0.5);
2443
- if (j === i || Math.abs(lj.y - li.y) > tolY || lj.x <= li.x) continue;
2894
+ if (j === i || lj.rotated || li.rotated || Math.abs(lj.y - li.y) > tolY || lj.x <= li.x) continue;
2444
2895
  if (lj.x < bestX) {
2445
2896
  bestX = lj.x;
2446
2897
  bestNext = j;
@@ -2448,19 +2899,27 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2448
2899
  }
2449
2900
  if (bestNext >= 0) nextOnRow.set(i, bestNext);
2450
2901
  }
2451
- const seen = /* @__PURE__ */ new Set();
2902
+ const joinsLeft = (cur, j) => {
2903
+ const lc = lines[cur], lj = lines[j];
2904
+ const em = Math.min(lc.fontSize, lj.fontSize) * PT_TO_MM3;
2905
+ const gap = lj.x - (lc.x + extent(lc));
2906
+ const prose = (a, b) => /\s$/.test(a.text) && /^[a-zà-ÿ]/.test(b.text.trim()) && a.text.trim().length >= 3 && b.text.trim().length >= 12 && !/\d[\d,.]*$/.test(a.text.trim());
2907
+ const maxGap = prose(lc, lj) ? em * 2 : em * 0.6;
2908
+ return !(gap < -em * 0.3 || gap > maxGap);
2909
+ };
2910
+ const joined = /* @__PURE__ */ new Set();
2911
+ for (const i of order) {
2912
+ const j = nextOnRow.get(i);
2913
+ if (j != null && joinsLeft(i, j)) joined.add(j);
2914
+ }
2452
2915
  for (const i of order) {
2453
- if (seen.has(i)) continue;
2916
+ if (joined.has(i)) continue;
2454
2917
  const row = [i];
2455
- seen.add(i);
2456
2918
  let cur = i;
2457
2919
  while (nextOnRow.has(cur)) {
2458
- const j = nextOnRow.get(cur), lc = lines[cur], lj = lines[j];
2459
- const em = Math.min(lc.fontSize, lj.fontSize) * PT_TO_MM3;
2460
- const gap = lj.x - (lc.x + extent(lc));
2461
- if (gap < -em * 0.3 || gap > em * 0.6) break;
2920
+ const j = nextOnRow.get(cur);
2921
+ if (!joinsLeft(cur, j)) break;
2462
2922
  row.push(j);
2463
- seen.add(j);
2464
2923
  cur = j;
2465
2924
  }
2466
2925
  rowOf.set(i, row);
@@ -2469,12 +2928,19 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2469
2928
  }
2470
2929
  const runStyle = (l) => {
2471
2930
  const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
2472
- return { cls, bold: cls.weight === "bold" || !!l.bold, italic: cls.style === "italic" };
2931
+ return { cls, bold: cls.weight === "bold" || !!l.bold, italic: cls.style === "italic" || !!l.italic };
2932
+ };
2933
+ const underWidgetValue = (l) => {
2934
+ const t = l.text.trim();
2935
+ if (!t) return false;
2936
+ const cx = l.x + extent(l) / 2, cy = l.y + l.height / 2;
2937
+ return formWidgets.some((w) => !w.pushButton && typeof w.fieldValue === "string" && w.fieldValue.trim() === t && cx >= w.rectMm.x && cx <= w.rectMm.x + w.rectMm.w && cy >= w.rectMm.y - 1 && cy <= w.rectMm.y + w.rectMm.h + 1);
2473
2938
  };
2474
2939
  lines.forEach((l, lineIdx) => {
2475
2940
  const tableEl = tableAtLine.get(lineIdx);
2476
2941
  if (tableEl) elements.push(tableEl);
2477
2942
  if (consumedLines.has(lineIdx)) return;
2943
+ if (underWidgetValue(l)) return;
2478
2944
  const row = rowOf.get(lineIdx);
2479
2945
  if (!row) return;
2480
2946
  if (row.length > 1) {
@@ -2504,13 +2970,15 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2504
2970
  if (lk) run.link = lk.url ? lk.url : lk.destPage != null ? { type: "internal", target: `#page-${lk.destPage + 1}` } : void 0;
2505
2971
  runs2.push(run);
2506
2972
  });
2507
- const style2 = { fontSize: Math.round(first.fontSize * 10) / 10, fontFamily: base.cls.family };
2973
+ const style2 = { fontSize: Math.round(first.fontSize * 10) / 10, fontFamily: base.cls.family, lineHeight: 1 };
2974
+ const rowHeight = Math.round(first.fontSize * PT_TO_MM3 * 100) / 100;
2508
2975
  if (first.opacity < 0.999) style2.opacity = Math.round(first.opacity * 100) / 100;
2509
2976
  const rt = {
2510
2977
  type: "richtext",
2511
2978
  runs: runs2,
2512
2979
  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) },
2513
2980
  width: Math.max(2, Math.round(Math.max(first.fontSize * PT_TO_MM3, Math.min(measuredW, cap2)) * 100) / 100),
2981
+ height: rowHeight,
2514
2982
  style: style2
2515
2983
  };
2516
2984
  const dominant = row.map((i) => lines[i]).sort((a, b) => b.text.trim().length - a.text.trim().length)[0];
@@ -2521,10 +2989,11 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2521
2989
  const cls = fontMap.get(l.fontName) || classifyFont(l.fontName || "");
2522
2990
  const style = {
2523
2991
  fontSize: Math.round(l.fontSize * 10) / 10,
2524
- fontFamily: cls.family
2992
+ fontFamily: cls.family,
2993
+ lineHeight: 1
2525
2994
  };
2526
2995
  if (cls.weight === "bold" || l.bold) style.fontWeight = "bold";
2527
- if (cls.style === "italic") style.fontStyle = "italic";
2996
+ if (cls.style === "italic" || l.italic) style.fontStyle = "italic";
2528
2997
  if (l.color !== "#000000") style.color = l.color;
2529
2998
  if (l.opacity < 0.999) style.opacity = Math.round(l.opacity * 100) / 100;
2530
2999
  const link = findLinkForRun2(l);
@@ -2538,6 +3007,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2538
3007
  content: l.text,
2539
3008
  position: { x: Math.max(0, Math.round(l.x * 100) / 100), y: Math.max(0, Math.round(l.y * 100) / 100) },
2540
3009
  width: Math.max(2, Math.round(elWidth * 100) / 100),
3010
+ // One line at lineHeight 1: the box is exactly one em tall.
3011
+ height: Math.round(l.fontSize * PT_TO_MM3 * 100) / 100,
2541
3012
  style
2542
3013
  };
2543
3014
  if ((cls.weight === "bold" || l.bold) && l.text.trim().length <= 120 && !consumedLines.has(lineIdx)) {
@@ -2556,6 +3027,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2556
3027
  prev.content = `${prev.content} ${text.content}`.replace(/\s+/g, " ");
2557
3028
  prev.tocEntry = prev.content;
2558
3029
  prev.width = Math.max(prev.width ?? 0, text.width ?? 0);
3030
+ prev.height = Math.round((text.position.y + (text.height ?? 0) - prev.position.y) * 100) / 100;
2559
3031
  return;
2560
3032
  }
2561
3033
  lineMeta.set(text, { w: Math.max(l.fontSize * PT_TO_MM3, extent(l)), size: l.fontSize, face: fontKey(l.fontName) });
@@ -2604,6 +3076,9 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2604
3076
  elements.push({ type: "signature", ...baseEl, value: "" });
2605
3077
  }
2606
3078
  }
3079
+ elements.forEach((e, k) => {
3080
+ if (e.id == null) e.id = `p${pi}-e${k + 1}`;
3081
+ });
2607
3082
  pages.push({
2608
3083
  id: `page-${pi}`,
2609
3084
  pageSize: { width: Math.round(pageW * PT_TO_MM3 * 100) / 100, height: Math.round(pageH * PT_TO_MM3 * 100) / 100 },
@@ -203,7 +203,7 @@
203
203
  "type": "object",
204
204
  "required": ["type", "name"],
205
205
  "properties": {
206
- "type": { "const": "input" },
206
+ "type": { "const": "input" }, "id": { "type": "string" },
207
207
  "name": { "type": "string", "minLength": 1 },
208
208
  "inputType": { "type": "string", "enum": ["text","number","email","url","tel","date","time","datetime-local","password","color"] },
209
209
  "value": { "type": "string" },
@@ -222,7 +222,7 @@
222
222
  "type": "object",
223
223
  "required": ["type", "name"],
224
224
  "properties": {
225
- "type": { "const": "textarea" },
225
+ "type": { "const": "textarea" }, "id": { "type": "string" },
226
226
  "name": { "type": "string", "minLength": 1 },
227
227
  "value": { "type": "string" },
228
228
  "placeholder": { "type": "string" },
@@ -240,7 +240,7 @@
240
240
  "type": "object",
241
241
  "required": ["type", "name"],
242
242
  "properties": {
243
- "type": { "const": "checkbox" },
243
+ "type": { "const": "checkbox" }, "id": { "type": "string" },
244
244
  "name": { "type": "string", "minLength": 1 },
245
245
  "checked": { "type": "boolean" },
246
246
  "label": { "type": "string" },
@@ -256,7 +256,7 @@
256
256
  "type": "object",
257
257
  "required": ["type", "name", "options"],
258
258
  "properties": {
259
- "type": { "const": "select" },
259
+ "type": { "const": "select" }, "id": { "type": "string" },
260
260
  "name": { "type": "string", "minLength": 1 },
261
261
  "options": {
262
262
  "type": "array",
@@ -285,7 +285,7 @@
285
285
  "type": "object",
286
286
  "required": ["type", "name"],
287
287
  "properties": {
288
- "type": { "const": "signature" },
288
+ "type": { "const": "signature" }, "id": { "type": "string" },
289
289
  "name": { "type": "string", "minLength": 1 },
290
290
  "value": { "type": "string" },
291
291
  "label": { "type": "string" },
@@ -301,7 +301,7 @@
301
301
  "type": "object",
302
302
  "required": ["type", "content"],
303
303
  "properties": {
304
- "type": { "const": "text" },
304
+ "type": { "const": "text" }, "id": { "type": "string" },
305
305
  "content": { "type": "string" },
306
306
  "style": { "$ref": "#/definitions/StyleRef" },
307
307
  "position": { "$ref": "#/definitions/Position" },
@@ -318,7 +318,7 @@
318
318
  "type": "object",
319
319
  "required": ["type", "runs"],
320
320
  "properties": {
321
- "type": { "const": "richtext" },
321
+ "type": { "const": "richtext" }, "id": { "type": "string" },
322
322
  "runs": {
323
323
  "type": "array",
324
324
  "items": {
@@ -395,7 +395,7 @@
395
395
  "type": "object",
396
396
  "required": ["type", "rows"],
397
397
  "properties": {
398
- "type": { "const": "table" },
398
+ "type": { "const": "table" }, "id": { "type": "string" },
399
399
  "columns": {
400
400
  "type": "array",
401
401
  "items": { "type": "object", "properties": { "width": { "oneOf": [{ "type": "string" }, { "type": "number" }] }, "header": { "type": "string" }, "align": { "type": "string", "enum": ["left","center","right","justify"] } } }
@@ -418,7 +418,7 @@
418
418
  "type": "object",
419
419
  "required": ["type", "items"],
420
420
  "properties": {
421
- "type": { "const": "list" },
421
+ "type": { "const": "list" }, "id": { "type": "string" },
422
422
  "items": {
423
423
  "type": "array",
424
424
  "items": { "type": "object", "required": ["content"], "properties": { "content": { "type": "string" }, "style": { "$ref": "#/definitions/StyleRef" }, "children": { "type": "array", "items": { "$ref": "#/definitions/ListElement/properties/items/items" } }, "listType": { "type": "string", "enum": ["ordered","unordered"] } } }
@@ -433,13 +433,25 @@
433
433
  "type": "object",
434
434
  "required": ["type", "shape"],
435
435
  "properties": {
436
- "type": { "const": "shape" },
436
+ "type": { "const": "shape" }, "id": { "type": "string" },
437
437
  "shape": { "type": "string", "enum": ["rect","circle","ellipse","line","path"] },
438
438
  "position": { "$ref": "#/definitions/Position" }, "width": { "type": "number" }, "height": { "type": "number" },
439
439
  "fill": { "type": "string" },
440
440
  "stroke": { "oneOf": [{ "type": "string" }, { "type": "object", "properties": { "color": { "type": "string" }, "width": { "type": "number" } } }] },
441
441
  "strokeWidth": { "type": "number" }, "borderRadius": { "type": "number" }, "path": { "type": "string" },
442
442
  "points": { "type": "array", "items": { "$ref": "#/definitions/Position" } },
443
+ "gradient": {
444
+ "type": "object",
445
+ "required": ["type", "stops"],
446
+ "properties": {
447
+ "type": { "type": "string", "enum": ["linear", "radial"] },
448
+ "angle": { "type": "number" },
449
+ "stops": {
450
+ "type": "array", "minItems": 2,
451
+ "items": { "type": "object", "required": ["offset", "color"], "properties": { "offset": { "type": "number" }, "color": { "type": "string" } } }
452
+ }
453
+ }
454
+ },
443
455
  "style": { "$ref": "#/definitions/StyleRef" }
444
456
  }
445
457
  },
@@ -447,7 +459,7 @@
447
459
  "type": "object",
448
460
  "required": ["type", "title", "elements"],
449
461
  "properties": {
450
- "type": { "const": "collapsible" },
462
+ "type": { "const": "collapsible" }, "id": { "type": "string" },
451
463
  "title": { "type": "string" },
452
464
  "elements": { "type": "array", "items": { "$ref": "#/definitions/Element" } },
453
465
  "expanded": { "type": "boolean" },
@@ -459,7 +471,7 @@
459
471
  "type": "object",
460
472
  "required": ["type"],
461
473
  "properties": {
462
- "type": { "const": "toc" }, "title": { "type": "string" },
474
+ "type": { "const": "toc" }, "id": { "type": "string" }, "title": { "type": "string" },
463
475
  "position": { "$ref": "#/definitions/Position" }, "width": { "type": "number" },
464
476
  "depth": { "type": "integer", "minimum": 1, "maximum": 6 },
465
477
  "style": { "$ref": "#/definitions/StyleRef" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uurtech/jdf-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",