@openpresentation/opf-render 0.4.0 → 0.5.0

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 (3) hide show
  1. package/README.md +14 -2
  2. package/dist/svg.js +51 -6
  3. package/package.json +6 -5
package/README.md CHANGED
@@ -114,7 +114,7 @@ Raster updates produce an HTML gallery and `artifacts/golden/candidate.json`; th
114
114
 
115
115
  Version 0.1.1 adds rich-text line offsets and measured boxes to opt-in SVG tracing, including blank lines, for editor caret placement. Normal SVG/PNG output is unchanged.
116
116
 
117
- ## Embedded WebP in PNG/PDF output (unreleased)
117
+ ## Embedded WebP in PNG/PDF output (since 0.2.0)
118
118
 
119
119
  PNG and PDF conversion now decode embedded WebP image data URIs to static PNG before rasterization. This preserves fit/crop, transparency, EXIF orientation and the first animation frame. The source SVG and OPF document remain unchanged; browser SVG output keeps the original embedded image. Both href and legacy xlink:href are supported, including base64 and percent-encoded data. This step does not resolve local filenames, download remote images or change the existing host imageResolver contract.
120
120
 
@@ -122,7 +122,7 @@ The Node-only raster path lazily loads pinned Sharp 0.35.4, requiring Node 20.9
122
122
 
123
123
  Regression checks compare six fixtures against independent Pillow pixels, verify fit/crop, inspect actual PDF image streams and transparency masks, and exercise input encodings and diagnostics. All 805 existing raster baselines remain unchanged. Test fixtures are project-authored and MIT licensed; the decoder and its bundled codecs retain their upstream licenses.
124
124
 
125
- ## JPEG EXIF orientation in raster output (unreleased)
125
+ ## JPEG EXIF orientation in raster output (since 0.2.0)
126
126
 
127
127
  PNG/PDF raster preparation now honors all eight JPEG EXIF orientations. JPEGs with orientation 2–8 are decoded to oriented PNG pixels for rasterization; JPEGs with orientation 1 or no orientation keep their original SVG attribute spelling and compressed bytes. This does not modify the source document or the browser SVG output. The Node path reads JPEG metadata through the same lazy Sharp dependency used for WebP.
128
128
 
@@ -131,3 +131,15 @@ Twenty-four cases verify fit/crop, PNG pixels and actual PDF image streams again
131
131
  `npm test` also builds the browser comparison and rejects native raster modules in browser output. Serve this repository and open `/artifacts/jpeg/browser/index.html` to run it. The harness permits mean channel error up to 1 and at most 2% of channels differing by more than 10; a deliberately unrotated image must fail that criterion.
132
132
 
133
133
  Version 0.2.0 requires Node 20.9 or later for native image decoding. Browser bundles continue using browser-safe entrypoints.
134
+
135
+ ## Styled and merged table cells
136
+
137
+ Renderer 0.5.0 requires core 0.7.0 and uses its shared styled-cell geometry. A cell may wrap a scalar or rich value with styles and spans:
138
+
139
+ ```json
140
+ {"rows":[[{"value":"Merged heading","colSpan":2,"style":{"fill":"#DBEAFE","align":"center","padding":{"top":12},"borders":{"bottom":{"color":"#445566","width":2,"dash":"dot"}}}},null],["Left","Right"]]}
141
+ ```
142
+
143
+ Every covered position is explicit `null`. Each merged anchor renders once; traced text uses its `.value` path for editing. Styles support RGB/RGBA fills and text colors, horizontal and vertical alignment, reference-pixel padding and per-edge solid/dashed/dotted borders. Rich-run colors override the cell text color. Columns remain equal width; rows grow from measured text, padding and span constraints. Pagination preserves connected vertical merge groups.
144
+
145
+ The Node20/24 renderer suite retains all 805 existing raster baselines. Browser fixtures verify merged text containment and styled-cell typing, formatting, cancellation and undo with actual loaded fonts. Native PowerPoint appearance remains a separate converter verification boundary.
package/dist/svg.js CHANGED
@@ -913,22 +913,68 @@ function renderTable(item, box, bound, options) {
913
913
  const scale = Math.min(bound.design.dimensions.width, bound.design.dimensions.height) / 720;
914
914
  const layout = layoutTable(item.value, box, {scale, minFontSize:(bound.composition ?? bound.geometry.composition).minFontSize, fontFamily:bound.design.fonts.body, textMeasurement:options.textMeasurement, path:item.path});
915
915
  const children = [];
916
+ const separateBorders = layout.rows.some(row => row.cells.some(cell => cell.style?.borders));
917
+ const defaultEdges = [], explicitEdges = [];
916
918
  for (const row of layout.rows) for (const cell of row.cells) {
919
+ const style = cell.style ?? {};
917
920
  children.push(tag("rect", {
918
921
  x: stableNumber(cell.box.x), y: stableNumber(cell.box.y),
919
922
  width: stableNumber(cell.box.width), height: stableNumber(cell.box.height),
920
- fill: cell.header ? bound.design.colors.primary : bound.design.colors.surface,
921
- stroke: bound.design.colors.border, "stroke-width":1,
922
- ...traceAttrs(options, cell.path)
923
+ fill: style.fill ?? (cell.header ? bound.design.colors.primary : bound.design.colors.surface),
924
+ stroke: separateBorders ? undefined : bound.design.colors.border, "stroke-width":separateBorders ? undefined : 1,
925
+ ...traceAttrs(options, cell.sourcePath ?? cell.path)
923
926
  }));
927
+ if (separateBorders) {
928
+ const {x, y, width, height} = cell.box;
929
+ const edges = {top:[x,y,x+width,y],right:[x+width,y,x+width,y+height],bottom:[x,y+height,x+width,y+height],left:[x,y,x,y+height]};
930
+ for (const [edge, coordinates] of Object.entries(edges)) {
931
+ const border = style.borders?.[edge];
932
+ (border ? explicitEdges : defaultEdges).push({coordinates, border, path:`${cell.sourcePath ?? cell.path}.style.borders.${edge}`});
933
+ }
934
+ }
935
+ // Core layout has already applied vertical alignment to cell.textBox.y.
924
936
  children.push((cell.rich ? renderRichTextBox : renderTextBox)(cell.rich ? cell.value : flattenText(cell.value ?? ""), cell.textBox, bound, {
925
937
  path:cell.path, fontSize:15, fontFamily:bound.design.fonts.body,
926
938
  fontWeight:cell.header ? 700 : 400, textStyle:cell.textStyle, fit:cell.fit,
927
- fill:cell.header ? "#FFFFFF" : bound.design.colors.text, options
939
+ fill:style.color ?? (cell.header ? "#FFFFFF" : bound.design.colors.text), align:style.align, options
928
940
  }));
929
941
  }
930
942
 
931
- return tag("g", traceAttrs(options, item.path), children.join("\n"));
943
+ return tag("g", traceAttrs(options, item.path), [...children, ...renderTableBorders(defaultEdges, explicitEdges, scale, bound.design.colors.border, options)].join("\n"));
944
+ }
945
+
946
+ // Explicit edges own their shared segment, including invisible/zero-width edges.
947
+ // Split implicit neighbors at merge boundaries so they cannot fill dashed gaps,
948
+ // cover alpha strokes, or reintroduce a border the author removed.
949
+ function renderTableBorders(defaultEdges, explicitEdges, scale, defaultColor, options) {
950
+ const epsilon = 1e-7;
951
+ const segment = ({coordinates:[x1,y1,x2,y2]}) => y1 === y2
952
+ ? {horizontal:true, fixed:y1, start:x1, end:x2}
953
+ : {horizontal:false, fixed:x1, start:y1, end:y2};
954
+ const blockers = explicitEdges.map(segment);
955
+ const defaults = defaultEdges.flatMap(edge => {
956
+ const axis = segment(edge);
957
+ let intervals = [[axis.start, axis.end]];
958
+ for (const blocker of blockers) {
959
+ if (axis.horizontal !== blocker.horizontal || Math.abs(axis.fixed - blocker.fixed) > epsilon) continue;
960
+ intervals = intervals.flatMap(([start,end]) => {
961
+ if (blocker.end <= start + epsilon || blocker.start >= end - epsilon) return [[start,end]];
962
+ return [[start,Math.min(end,blocker.start)],[Math.max(start,blocker.end),end]].filter(([a,b]) => b-a > epsilon);
963
+ });
964
+ }
965
+ return intervals.map(([start,end]) => ({...edge,coordinates:axis.horizontal
966
+ ? [start,axis.fixed,end,axis.fixed] : [axis.fixed,start,axis.fixed,end]}));
967
+ });
968
+ return [...defaults,...explicitEdges].flatMap(({coordinates:[x1,y1,x2,y2],border,path}) => {
969
+ const width = border ? border.width * scale : 1;
970
+ if (width === 0) return [];
971
+ return [tag('line', {
972
+ x1:stableNumber(x1), y1:stableNumber(y1), x2:stableNumber(x2), y2:stableNumber(y2),
973
+ stroke:border?.color ?? defaultColor, 'stroke-width':stableNumber(width),
974
+ 'stroke-dasharray':border?.dash === 'dash' ? `${width*4} ${width*3}` : border?.dash === 'dot' ? `${width} ${width*2}` : undefined,
975
+ ...traceAttrs(options,path)
976
+ })];
977
+ });
932
978
  }
933
979
 
934
980
  function renderImportedChart(item, box, bound, options) {
@@ -1280,4 +1326,3 @@ function tag(name, attrs = {}, children = "") {
1280
1326
  if (children === "") return `<${name}${serializedAttrs}/>`;
1281
1327
  return `<${name}${serializedAttrs}>${children}</${name}>`;
1282
1328
  }
1283
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-render",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,13 +55,14 @@
55
55
  "sideEffects": false,
56
56
  "scripts": {
57
57
  "build": "node scripts/build.mjs",
58
- "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs",
58
+ "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs && node --check test/styled-table.mjs",
59
59
  "validate": "node scripts/validate-package.mjs",
60
- "test": "npm run build && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/timeline.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/design-preview.mjs && npm run build:browser-check",
60
+ "test": "npm run build && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/timeline.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/design-preview.mjs && npm run build:browser-check && npm run test:styled-table",
61
61
  "test:golden": "npm run build && node test/golden.mjs",
62
62
  "golden:update": "npm run build && node test/golden.mjs --update",
63
63
  "prepack": "npm run build",
64
- "build:browser-check": "node scripts/build-browser-check.mjs"
64
+ "build:browser-check": "node scripts/build-browser-check.mjs",
65
+ "test:styled-table": "node test/styled-table.mjs"
65
66
  },
66
67
  "dependencies": {
67
68
  "@expo-google-fonts/arimo": "0.4.3",
@@ -72,7 +73,7 @@
72
73
  "@expo-google-fonts/roboto": "^0.4.3",
73
74
  "@expo-google-fonts/roboto-mono": "^0.4.2",
74
75
  "@expo-google-fonts/tinos": "0.4.2",
75
- "@openpresentation/opf": "^0.6.0",
76
+ "@openpresentation/opf": "^0.7.0",
76
77
  "@resvg/resvg-js": "^2.6.2",
77
78
  "fontkit": "^2.0.4",
78
79
  "pdf-lib": "^1.17.1",