@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 +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/svg.d.ts +2 -0
- package/dist/svg.js +62 -22
- package/package.json +4 -4
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return tag('g',
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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",
|