@openpresentation/opf-render 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # OPF Render
2
2
 
3
+ Version 0.9.0 requires `@openpresentation/opf` ^0.11.0 and resolves content ColorRef / `variables` through core `resolveColorRef()`. Authored `#RRGGBB` paint stays authored. It retains the existing rendering APIs.
4
+
5
+ Version 0.8.1 required core 0.10.1, including metric, quote and timeline layout placeholders and the corrected text-bullet contract.
6
+
7
+ Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
8
+
3
9
  Version 0.8.0 and this checkout require Node 24 (`24.x`). Use `.nvmrc` for local development. Earlier published versions retain their original engine declarations. Browser entrypoints remain browser-safe; native application compatibility is verified separately.
4
10
 
5
11
  Version 0.8.0 adds optional font-registry vector outlines and shared heading/scalar/rich line placement. Pass identical `textMeasurement` and `textRasterPadding` options to preview, pagination and export; padding defaults to one scaled reference pixel. SVG consumes accepted origins without remeasuring. See the [source contract and limits](https://github.com/OpenPresentation/opf/blob/f94125a1ff95bf0974a055fe1c081d348dad54f2/docs/plans/text-placement.md). Native raster certification remains separate.
package/dist/index.d.ts CHANGED
@@ -52,6 +52,7 @@ export interface RenderSvgOptions {
52
52
  strictAssets?: boolean;
53
53
  imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
54
54
  onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
55
+ validate?: boolean;
55
56
  slideIndex?: number;
56
57
  trace?: boolean;
57
58
  catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
package/dist/svg.d.ts CHANGED
@@ -54,6 +54,8 @@ export interface RenderSvgOptions {
54
54
  strictAssets?: boolean;
55
55
  imageResolver?: (src: string | undefined, context: { asset: unknown; path: string }) => string | null | undefined;
56
56
  onDiagnostic?: (diagnostic: RenderDiagnostic) => void;
57
+ /** When false, skip AJV boundary validation. Default true. */
58
+ validate?: boolean;
57
59
  slideIndex?: number;
58
60
  trace?: boolean;
59
61
  catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
package/dist/svg.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { layoutTable, fitList, fitRichText, composeSlide, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, fitText, textColorForFill, chartColorForFill } from "@openpresentation/opf/composition";
2
2
  import {
3
3
  catalogs as bundledCatalogs,
4
+ resolveColorRef as resolveCoreColorRef,
4
5
  validatePresentation
5
6
  } from "@openpresentation/opf";
6
7
 
@@ -291,6 +292,32 @@ function normalizeColor(value, fallback) {
291
292
  return fallback;
292
293
  }
293
294
 
295
+ /** Resolve content ColorRef via core; keep authored #RRGGBB / #RRGGBBAA casing. */
296
+ function resolveColorRef(value, bound, fallback) {
297
+ if (value == null || value === "") return fallback;
298
+ if (typeof value !== "string") return fallback;
299
+ const trimmed = value.trim();
300
+ // Core normalizeHexColor uppercases literals and strips #RRGGBBAA alpha.
301
+ // Packed-browser editor checks keep toolbar hex like #2563eb as authored.
302
+ if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(trimmed)) return trimmed;
303
+ if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) return normalizeColor(trimmed, fallback);
304
+ const colors = bound.design.colors ?? {};
305
+ return resolveCoreColorRef(trimmed, {
306
+ colorScheme: bound.design.colorScheme ?? {},
307
+ roles: {
308
+ primary: colors.primary,
309
+ secondary: colors.secondary,
310
+ accent: colors.accent,
311
+ background: colors.background,
312
+ surface: colors.surface,
313
+ text: colors.text,
314
+ textSecondary: colors.mutedText
315
+ },
316
+ variables: bound.design.variables,
317
+ fallback
318
+ });
319
+ }
320
+
294
321
  function resolveBackground(background, colorScheme) {
295
322
  if (!background) return colorFromScheme(colorScheme, "light1", "#FFFFFF");
296
323
  if (typeof background === "string") return colorFromScheme(colorScheme, background, "#FFFFFF");
@@ -346,6 +373,7 @@ function resolveDesign(presentation, slide, context) {
346
373
  colorScheme,
347
374
  fontScheme,
348
375
  dimensions,
376
+ variables: presentation.variables ?? {},
349
377
  background: backgroundDefinition,
350
378
  backgroundColor,
351
379
  colors: {
@@ -485,7 +513,7 @@ function bindSlide(presentation, slide, layout, index, context) {
485
513
 
486
514
  const design = resolveDesign(presentation, slide, context);
487
515
  for (const role of ["heading","body","code"]) design.fonts[role] = resolveTextStyle({fontFamily:design.fonts[role],fontWeight:role === "heading" ? 700 : 400},context.options.textMeasurement).fontFamily;
488
- const geometry = composeSlide(slide, { ...design.dimensions, layout, slideIndex: index, fonts: design.fonts, contentAlignment:design.contentAlignment, titleAlignment:design.titleAlignment, textRasterPadding:context.options.textRasterPadding, contentBox:design.contentBox, textMeasurement: context.options.textMeasurement });
516
+ const geometry = composeSlide(slide, { ...design.dimensions, layout, presentation, slideIndex: index, fonts: design.fonts, contentAlignment:design.contentAlignment, titleAlignment:design.titleAlignment, textRasterPadding:context.options.textRasterPadding, contentBox:design.contentBox, textMeasurement: context.options.textMeasurement });
489
517
  return {
490
518
  geometry,
491
519
  assets: presentation.assets ?? {},
@@ -502,7 +530,9 @@ function bindSlide(presentation, slide, layout, index, context) {
502
530
 
503
531
  export function resolvePresentation(input, options = {}) {
504
532
  const presentation = parseInput(input);
505
- assertValidBoundary(presentation);
533
+ if (options.validate !== false) {
534
+ assertValidBoundary(presentation);
535
+ }
506
536
 
507
537
  const context = { presentation, options };
508
538
  const slides = presentation.slides.map((slide, index) => {
@@ -903,7 +933,14 @@ function renderTable(item, box, bound, options) {
903
933
  const defaultEdges = [], explicitEdges = [];
904
934
  for (const row of layout.rows) for (const cell of row.cells) {
905
935
  const style = cell.style ?? {};
906
- const fill = style.fill ?? (cell.header ? bound.design.colors.primary : bound.design.colors.surface);
936
+ const defaultFill = cell.header ? bound.design.colors.primary : bound.design.colors.surface;
937
+ const fill = style.fill == null
938
+ ? defaultFill
939
+ : resolveColorRef(style.fill, bound, defaultFill);
940
+ const defaultText = textColorForFill(fill, cell.header ? "#FFFFFF" : bound.design.colors.text);
941
+ const textFill = style.color == null
942
+ ? defaultText
943
+ : resolveColorRef(style.color, bound, defaultText);
907
944
  children.push(tag("rect", {
908
945
  x: stableNumber(cell.box.x), y: stableNumber(cell.box.y),
909
946
  width: stableNumber(cell.box.width), height: stableNumber(cell.box.height),
@@ -923,17 +960,18 @@ function renderTable(item, box, bound, options) {
923
960
  children.push((cell.rich ? renderRichTextBox : renderTextBox)(cell.rich ? cell.value : flattenText(cell.value ?? ""), cell.textBox, bound, {
924
961
  path:cell.path, fontSize:15, fontFamily:bound.design.fonts.body,
925
962
  fontWeight:cell.header ? 700 : 400, textStyle:cell.textStyle, fit:cell.fit,
926
- fill:style.color ?? textColorForFill(fill, cell.header ? "#FFFFFF" : bound.design.colors.text), align:style.align, options
963
+ fill:textFill, align:style.align, options
927
964
  }));
928
965
  }
929
966
 
930
- return tag("g", traceAttrs(options, item.path), [...children, ...renderTableBorders(defaultEdges, explicitEdges, scale, bound.design.colors.border, options)].join("\n"));
967
+ return tag("g", traceAttrs(options, item.path), [...children, ...renderTableBorders(defaultEdges, explicitEdges, scale, bound, options)].join("\n"));
931
968
  }
932
969
 
933
970
  // Explicit edges own their shared segment, including invisible/zero-width edges.
934
971
  // Split implicit neighbors at merge boundaries so they cannot fill dashed gaps,
935
972
  // cover alpha strokes, or reintroduce a border the author removed.
936
- function renderTableBorders(defaultEdges, explicitEdges, scale, defaultColor, options) {
973
+ function renderTableBorders(defaultEdges, explicitEdges, scale, bound, options) {
974
+ const defaultColor = bound.design.colors.border;
937
975
  const epsilon = 1e-7;
938
976
  const segment = ({coordinates:[x1,y1,x2,y2]}) => y1 === y2
939
977
  ? {horizontal:true, fixed:y1, start:x1, end:x2}
@@ -955,9 +993,12 @@ function renderTableBorders(defaultEdges, explicitEdges, scale, defaultColor, op
955
993
  return [...defaults,...explicitEdges].flatMap(({coordinates:[x1,y1,x2,y2],border,path}) => {
956
994
  const width = border ? border.width * scale : 1;
957
995
  if (width === 0) return [];
996
+ const stroke = border?.color == null
997
+ ? defaultColor
998
+ : resolveColorRef(border.color, bound, defaultColor);
958
999
  return [tag('line', {
959
1000
  x1:stableNumber(x1), y1:stableNumber(y1), x2:stableNumber(x2), y2:stableNumber(y2),
960
- stroke:border?.color ?? defaultColor, 'stroke-width':stableNumber(width),
1001
+ stroke, 'stroke-width':stableNumber(width),
961
1002
  'stroke-dasharray':border?.dash === 'dash' ? `${width*4} ${width*3}` : border?.dash === 'dot' ? `${width} ${width*2}` : undefined,
962
1003
  ...traceAttrs(options,path)
963
1004
  })];
@@ -1138,20 +1179,16 @@ function inlineChartRows(data) {
1138
1179
  }
1139
1180
 
1140
1181
  function renderFurniture(bound, presentation, width, height, options, kind) {
1141
- const local=bound.slide.design?.[kind] !== undefined;
1142
- const furniture=local?bound.slide.design[kind]:presentation.design?.[kind];
1143
- if(!furniture)return "";
1144
- const root=local?`${bound.path}.design.${kind}`:`design.${kind}`;
1145
- const organizations=Array.isArray(presentation.organization)?presentation.organization:[presentation.organization];
1146
- const organization=organizations.find(item=>item?.role==='primary')??organizations.find(Boolean);
1147
- return tag('g',traceAttrs(options,root),['left','center','right'].map((zone,index)=>{
1148
- const item=furniture[zone];if(!item)return '';
1149
- const box={x:width*(.07+index*.3),y:kind==='header'?height*.025:height*.925,width:width*.26,height:height*.05};
1150
- if(item.image)return renderImage({value:item.image,path:`${root}.${zone}.image`},box,bound,{...options,imageFit:'contain'});
1151
- const pieces=[item.text,item.organization?organization?.name:null,item.section?bound.slide.section:null,item.slideNumber?String(bound.index+1):null,typeof item.date==='string'?item.date:null].filter(value=>value!==undefined&&value!==null&&value!=='');
1152
- if(item.date===true)reportDiagnostic({code:'date-needs-value',path:`${root}.${zone}.date`,message:'Use a literal date string for a reproducible preview; the document does not define a presentation date.'},options);
1153
- const anchor=['start','middle','end'][index],x=index===0?width*.07:index===1?width/2:width*.93;
1154
- return tag('text',{x:stableNumber(x),y:stableNumber(kind==='header'?height*.05:height-28),'text-anchor':anchor,'font-family':fontStack(bound.design.fonts.body,bound.design.fontScheme.type),'font-size':13,fill:bound.design.colors.mutedText,...traceAttrs(options,`${root}.${zone}`)},escapeText(pieces.join(' · ')));
1182
+ const layout=bound.geometry.furniture;
1183
+ if(!layout){
1184
+ const value=bound.slide.design?.[kind]!==undefined?bound.slide.design[kind]:presentation.design?.[kind];
1185
+ if(value)throw new OPFRenderError('missing-furniture-layout','Header/footer rendering requires coordinated core furniture geometry.',{path:bound.path});
1186
+ return '';
1187
+ }
1188
+ return tag('g',{},layout.parts.filter(part=>part.kind===kind).map(part=>{
1189
+ const trace=options.trace?{'data-opf-furniture-kind':kind,'data-opf-furniture-field':part.field,'data-opf-furniture-generated':String(part.generated),'data-opf-furniture-editable':!part.generated?'true':undefined,'data-opf-furniture-source':part.sourcePath}:{};
1190
+ if(part.type==='image')return tag('g',trace,renderImage({value:part.image,path:part.path},part.box,bound,{...options,imageFit:'contain'}));
1191
+ return tag('g',trace,renderTextBox(part.text,part.box,bound,{path:part.path,fit:part.fit,textStyle:part.style,fontFamily:part.style.fontFamily,align:part.alignment,fill:bound.design.colors.mutedText,diagnosticsHandled:true,options}));
1155
1192
  }).join(''));
1156
1193
  }
1157
1194
  function renderBranding(bound,presentation,width,height,options) {
@@ -1201,7 +1238,10 @@ function renderRichLines(value,fit,box,bound,config) {
1201
1238
  // avoids hinted browser advances; textLength also removes fractional-size
1202
1239
  // quantization drift. Height and baseline retain the selected font size.
1203
1240
  const position=flow?{'baseline-shift':fragment.baselineShift?stableNumber(-fragment.baselineShift):undefined}:{x:stableNumber((placed?.x??box.x+offset)+fragment.x),y:stableNumber((placed?.baseline??box.y+line.baseline)+fragment.baselineShift)};
1204
- const rendered=tag(flow?'tspan':'text',{...(config.options.trace?{'data-opf-text-start':runOffsets[fragment.runIndex]+fragment.start,'data-opf-text-end':runOffsets[fragment.runIndex]+fragment.end}:{}),...position,'xml:space':'preserve','text-rendering':flow?undefined:'geometricPrecision',textLength:placed&&fragment.width>0?stableNumber(fragment.width):undefined,lengthAdjust:placed&&fragment.width>0?'spacingAndGlyphs':undefined,'font-family':fontStack(fragment.style.fontFamily,bound.design.fontScheme.type),'font-size':stableNumber(fragment.fontSize),'font-weight':fragment.style.fontWeight,'font-style':fragment.style.italic?'italic':flow?'normal':undefined,'text-decoration':[run.underline?'underline':'',run.strikethrough?'line-through':''].filter(Boolean).join(' ')||undefined,fill:/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(run.color??'')?run.color:config.fill},escapeText(fragment.text));
1241
+ const runFill = run.color == null
1242
+ ? config.fill
1243
+ : resolveColorRef(run.color, bound, config.fill);
1244
+ const rendered=tag(flow?'tspan':'text',{...(config.options.trace?{'data-opf-text-start':runOffsets[fragment.runIndex]+fragment.start,'data-opf-text-end':runOffsets[fragment.runIndex]+fragment.end}:{}),...position,'xml:space':'preserve','text-rendering':flow?undefined:'geometricPrecision',textLength:placed&&fragment.width>0?stableNumber(fragment.width):undefined,lengthAdjust:placed&&fragment.width>0?'spacingAndGlyphs':undefined,'font-family':fontStack(fragment.style.fontFamily,bound.design.fontScheme.type),'font-size':stableNumber(fragment.fontSize),'font-weight':fragment.style.fontWeight,'font-style':fragment.style.italic?'italic':flow?'normal':undefined,'text-decoration':[run.underline?'underline':'',run.strikethrough?'line-through':''].filter(Boolean).join(' ')||undefined,fill:runFill},escapeText(fragment.text));
1205
1245
  if(run.link&&/^(https?:|mailto:)/i.test(run.link))return tag('a',{href:run.link,target:'_blank',rel:'noopener noreferrer'},rendered);
1206
1246
  return rendered;
1207
1247
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-render",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,9 +55,9 @@
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 && node --check test/styled-table.mjs && node --check test/chart-colors.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.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 && node --check test/color-references.mjs && node --check test/chart-colors.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.mjs && node --check test/furniture.mjs",
59
59
  "validate": "node scripts/validate-package.mjs",
60
- "test": "npm run build && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/chart-colors.mjs && 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/quote-footer.mjs && node test/shared-quote.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",
60
+ "test": "npm run build && node test/furniture.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/chart-colors.mjs && 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/quote-footer.mjs && node test/shared-quote.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/color-references.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
  "test:packed": "node test/packed-install.mjs",
63
63
  "test:browser": "npm run build:browser-check && node test/browser-check.mjs",
@@ -88,7 +88,7 @@
88
88
  "@expo-google-fonts/roboto": "0.4.3",
89
89
  "@expo-google-fonts/roboto-mono": "0.4.2",
90
90
  "@expo-google-fonts/tinos": "0.4.2",
91
- "@openpresentation/opf": "^0.10.0",
91
+ "@openpresentation/opf": "^0.11.0",
92
92
  "@resvg/resvg-js": "^2.6.2",
93
93
  "fontkit": "^2.0.4",
94
94
  "pdf-lib": "^1.17.1",