@uurtech/jdf-cli 0.2.5 → 0.2.7

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
@@ -1488,7 +1488,8 @@ function merge(para, meta, pageWmm) {
1488
1488
  const style = { ...first.style ?? {}, lineHeight, ...indent > 0.5 ? { textIndent: Math.round(indent * 100) / 100 } : {} };
1489
1489
  const allText = para.every((e) => e.type === "text" && !e.link);
1490
1490
  const y = Math.max(0, first.position.y - (lineHeight - 1) / 2 * lineH);
1491
- const base = { position: { x, y: Math.round(y * 100) / 100 }, width: Math.round(width * 100) / 100, style };
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 };
1492
1493
  if (allText) {
1493
1494
  const content = para.map((e) => String(e.content ?? "").trim().replace(/[ \t]+/g, " ")).join(joiner());
1494
1495
  const { height: _hh2, ...firstRest } = first;
@@ -1767,6 +1768,11 @@ async function walkOps(page, OPS, viewport) {
1767
1768
  clip: void 0,
1768
1769
  fill: "#000000",
1769
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",
1770
1776
  lineWidth: 1,
1771
1777
  fillAlpha: 1,
1772
1778
  strokeAlpha: 1,
@@ -1780,6 +1786,24 @@ async function walkOps(page, OPS, viewport) {
1780
1786
  rise: 0
1781
1787
  };
1782
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
+ };
1783
1807
  const stack = [];
1784
1808
  const textOps = [];
1785
1809
  const shapes = [];
@@ -1856,7 +1880,7 @@ async function walkOps(page, OPS, viewport) {
1856
1880
  gradient: isFill ? gs.fillGradient : void 0,
1857
1881
  stroke: isStroke ? gs.stroke : void 0,
1858
1882
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1859
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
1883
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha
1860
1884
  });
1861
1885
  }
1862
1886
  pathRects = [];
@@ -1878,7 +1902,7 @@ async function walkOps(page, OPS, viewport) {
1878
1902
  gradient: isFill ? gs.fillGradient : void 0,
1879
1903
  stroke: isStroke ? gs.stroke : void 0,
1880
1904
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1881
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha
1905
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha
1882
1906
  });
1883
1907
  } else if (pathSegments.length === 2 && pathSegments[0].type === "M" && pathSegments[1].type === "L") {
1884
1908
  const a = pathSegments[0].pts;
@@ -1970,7 +1994,7 @@ async function walkOps(page, OPS, viewport) {
1970
1994
  gradient: isFill ? gs.fillGradient : void 0,
1971
1995
  stroke: isStroke ? gs.stroke : void 0,
1972
1996
  strokeWidth: isStroke ? gs.lineWidth * PT_TO_MM3 : void 0,
1973
- opacity: isFill ? gs.fillAlpha : gs.strokeAlpha,
1997
+ opacity: isFill ? fillOpacity() : gs.strokeAlpha,
1974
1998
  path: thin ? void 0 : d
1975
1999
  });
1976
2000
  }
@@ -1991,11 +2015,15 @@ async function walkOps(page, OPS, viewport) {
1991
2015
  if (s) Object.assign(gs, s);
1992
2016
  } else if (fn === OPS.paintFormXObjectBegin) {
1993
2017
  stack.push(snapshot());
2018
+ groupStack.push({ blend: gs.blend, alpha: gs.fillAlpha });
2019
+ gs.blend = "normal";
2020
+ gs.fillAlpha = 1;
1994
2021
  const matrix = args[0];
1995
2022
  if (Array.isArray(matrix) && matrix.length === 6) gs.ctm = multiplyCtm(matrix, gs.ctm);
1996
2023
  } else if (fn === OPS.paintFormXObjectEnd) {
1997
2024
  const s = stack.pop();
1998
2025
  if (s) Object.assign(gs, s);
2026
+ groupStack.pop();
1999
2027
  } else if (fn === OPS.beginText) {
2000
2028
  tm = [1, 0, 0, 1, 0, 0];
2001
2029
  tlm = [1, 0, 0, 1, 0, 0];
@@ -2068,6 +2096,7 @@ async function walkOps(page, OPS, viewport) {
2068
2096
  if (key === "LW") gs.lineWidth = val;
2069
2097
  else if (key === "ca") gs.fillAlpha = val;
2070
2098
  else if (key === "CA") gs.strokeAlpha = val;
2099
+ else if (key === "BM") gs.blend = String(Array.isArray(val) ? val[0] : val).toLowerCase();
2071
2100
  }
2072
2101
  }
2073
2102
  } else if (fn === OPS.showText) {
@@ -2152,18 +2181,37 @@ async function walkOps(page, OPS, viewport) {
2152
2181
  const isStroke = fn === OPS.stroke || fn === OPS.fillStroke || fn === OPS.eoFillStroke || fn === OPS.closeFillStroke || fn === OPS.closeStroke || fn === OPS.closeEOFillStroke;
2153
2182
  flushPath(isFill, isStroke);
2154
2183
  } else if (fn === OPS.endPath || fn === OPS.clip || fn === OPS.eoClip) {
2155
- if (fn !== OPS.endPath) gs.clip = clipFromPath();
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
+ }
2156
2195
  pathSegments = [];
2157
2196
  pathRects = [];
2158
2197
  pathRect = null;
2159
2198
  pathStart = null;
2160
2199
  pathLast = null;
2161
2200
  } else if (fn === OPS.shadingFill) {
2162
- const ir = args[0];
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
+ }
2163
2210
  const g = irToGradient(ir, (x, y) => toViewport(...Object.values(tx(gs.ctm, x, y))));
2164
2211
  const avg = Array.isArray(ir) && ir[0] === "RadialAxial" ? averageStops(ir[3]) : null;
2165
- if (g || avg) {
2166
- const area = gs.clip ?? { d: "", x: 0, y: 0, w: viewport.width, h: viewport.height };
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 };
2167
2215
  shapes.push({
2168
2216
  seq: opIndex,
2169
2217
  kind: area.d ? "path" : "rect",
@@ -2173,7 +2221,7 @@ async function walkOps(page, OPS, viewport) {
2173
2221
  height: Math.max(0.1, area.h * PT_TO_MM3),
2174
2222
  fill: avg ?? (g ? g.stops[0].color : void 0),
2175
2223
  gradient: g ?? void 0,
2176
- opacity: gs.fillAlpha,
2224
+ opacity: fillOpacity(avg ?? g?.stops[0].color),
2177
2225
  path: area.d || void 0
2178
2226
  });
2179
2227
  }
@@ -2923,12 +2971,14 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2923
2971
  runs2.push(run);
2924
2972
  });
2925
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;
2926
2975
  if (first.opacity < 0.999) style2.opacity = Math.round(first.opacity * 100) / 100;
2927
2976
  const rt = {
2928
2977
  type: "richtext",
2929
2978
  runs: runs2,
2930
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) },
2931
2980
  width: Math.max(2, Math.round(Math.max(first.fontSize * PT_TO_MM3, Math.min(measuredW, cap2)) * 100) / 100),
2981
+ height: rowHeight,
2932
2982
  style: style2
2933
2983
  };
2934
2984
  const dominant = row.map((i) => lines[i]).sort((a, b) => b.text.trim().length - a.text.trim().length)[0];
@@ -2957,6 +3007,8 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2957
3007
  content: l.text,
2958
3008
  position: { x: Math.max(0, Math.round(l.x * 100) / 100), y: Math.max(0, Math.round(l.y * 100) / 100) },
2959
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,
2960
3012
  style
2961
3013
  };
2962
3014
  if ((cls.weight === "bold" || l.bold) && l.text.trim().length <= 120 && !consumedLines.has(lineIdx)) {
@@ -2975,6 +3027,7 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
2975
3027
  prev.content = `${prev.content} ${text.content}`.replace(/\s+/g, " ");
2976
3028
  prev.tocEntry = prev.content;
2977
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;
2978
3031
  return;
2979
3032
  }
2980
3033
  lineMeta.set(text, { w: Math.max(l.fontSize * PT_TO_MM3, extent(l)), size: l.fontSize, face: fontKey(l.fontName) });
@@ -3023,6 +3076,9 @@ async function importPdfToJdf(source, title, runtime, options = {}) {
3023
3076
  elements.push({ type: "signature", ...baseEl, value: "" });
3024
3077
  }
3025
3078
  }
3079
+ elements.forEach((e, k) => {
3080
+ if (e.id == null) e.id = `p${pi}-e${k + 1}`;
3081
+ });
3026
3082
  pages.push({
3027
3083
  id: `page-${pi}`,
3028
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,7 +433,7 @@
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" },
@@ -459,7 +459,7 @@
459
459
  "type": "object",
460
460
  "required": ["type", "title", "elements"],
461
461
  "properties": {
462
- "type": { "const": "collapsible" },
462
+ "type": { "const": "collapsible" }, "id": { "type": "string" },
463
463
  "title": { "type": "string" },
464
464
  "elements": { "type": "array", "items": { "$ref": "#/definitions/Element" } },
465
465
  "expanded": { "type": "boolean" },
@@ -471,7 +471,7 @@
471
471
  "type": "object",
472
472
  "required": ["type"],
473
473
  "properties": {
474
- "type": { "const": "toc" }, "title": { "type": "string" },
474
+ "type": { "const": "toc" }, "id": { "type": "string" }, "title": { "type": "string" },
475
475
  "position": { "$ref": "#/definitions/Position" }, "width": { "type": "number" },
476
476
  "depth": { "type": "integer", "minimum": 1, "maximum": 6 },
477
477
  "style": { "$ref": "#/definitions/StyleRef" }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@uurtech/jdf-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Command-line tool for the JDF (JSON Document Format) — validate and convert documents.",
5
5
  "license": "MIT",
6
- "author": "Ugur Kazdal",
6
+ "author": "Ugur Kazdal <ugur@jdf.dev>",
7
7
  "homepage": "https://github.com/uurtech/jdf#readme",
8
8
  "repository": {
9
9
  "type": "git",