@openpresentation/opf-render 0.11.7 → 0.11.9
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 +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/svg.d.ts +5 -0
- package/dist/svg.js +104 -25
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Aptos previews with Intos.** The Office pack includes Intos, Intos Display, Intos Narrow and Intos Serif (16 faces, about 12 MB, vendored under `fonts/intos/`, SIL OFL 1.1, pinned to upstream commit fef9315c14da9e4b23b4c3cac8e718998d4e4736 and per-file SHA-256, unmodified from the upstream files). They are the metric-compatible previews for Aptos, Aptos Display, Aptos Narrow and Aptos Serif, the default `aptos` font scheme: 0.000% mean and maximum width difference against Aptos 2.01 in all four styles, with equal vertical metrics. The letter shapes are Intos's own, not Aptos's. The exported PPTX still names Aptos; no Aptos file is bundled or embedded. A registry built without these faces (the base pack) has no Aptos substitute: visual policy falls back to Roboto or Carlito. Like the open families, Intos is `embed: "used"`: it is not in `registry.embeddedFonts` (the 33 eager npm faces), `prepareNodeFonts().options.embeddedFonts` supplies it, and an SVG embeds only the Intos faces its text draws. `registry.lazyFonts` lists the 51 vendored faces (35 open, 16 Intos) with package-relative file and sha256, and `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})` loads them on demand: `await registry.ensureLazyFonts(presentation)` fetches and verifies only the faces the document draws, by family, weight and style (a plain Aptos deck: Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic run adds Intos Italic, a bold run Intos Bold; the code face only when a slide has code), then adds them to the document and the registry together so measurement and painting never disagree. Faces a host serves itself load the same way: `loadBrowserFontRegistry(startup, { extraLazyFonts: [{ family, weight, italic, url, sha256 }] })` adds them to the lazy list (`registry.lazyFonts`, `package: "host"`), so the registry can start with Roboto Regular only (`splitStartupFaces(faces)` splits a manifest into the startup face and the rest) and a document draws, fetches and verifies just the faces it needs, vendored and host faces in one `ensureLazyFonts` call. `sha256` is mandatory, loads are all or nothing, `dispose()` removes the faces, and, like the vendored ones, host faces are `embed: "used"`: they are not in `registry.embeddedFonts` (only the startup faces are), and a standalone SVG embeds one only when passed explicitly through `registry.selectEmbeddedFonts` and the slide's text uses its family. `registry.pendingLazyFonts(presentation)` is the synchronous check hosts gate rendering on, and after an edit it reports just the newly needed faces. The faces are those the renderer itself measures and draws (`presentationFaces(presentation, options, registry)`, one layout-and-paint run with a recording measurement), resolved by the registry's own face selection over the faces it holds plus the vendored ones, so the browser loads exactly what Node draws with every vendored face loaded. A document resolves with the `renderSvg` options the host renders with: pass them as `loadBrowserFontRegistry(entries, { renderOptions: { catalogs } })`, or per call (`pendingLazyFonts(presentation, { catalogs })`, `ensureLazyFonts(presentation, { signal, catalogs })`), so a layout or font scheme id that only the host's catalogs know resolves here too; a document that does not resolve throws (rejects) what `renderSvg` throws for it instead of reporting nothing. Node's `prepareNodeFonts` and `loadOfficeFontRegistry` keep loading the whole open pack and Intos eagerly, so nothing there is face level; with `scripts: 'auto'` they accept `renderOptions: { catalogs }` too. Hosts copy the vendored directories next to the page.
|
|
4
4
|
|
|
5
|
-
Version 0.11.7 lets a browser registry load the host's own faces on demand (`extraLazyFonts`, `splitStartupFaces`: a registry can start with Roboto Regular only; additive, `registry.lazyFonts` still lists the 94 vendored faces plus the host's). Version 0.11.6 makes the category-axis labels of chart previews rotate and skip instead of wrapping mid-word (no API change; the preview of dense-axis chart slides changes) and requires `@openpresentation/opf` ^0.11.3 (the 70 legacy gallery layout ids and their geometry); use it with PPTX 0.11.4 and editor 0.10.4 so preview and export resolve the same core. Version 0.11.5 loads vendored preview fonts by face, not by family, and lets the font loaders resolve documents with the host's render options (`presentationFaces`, `lazyFacesNeeded`; `pendingLazyFonts`, `ensureLazyFonts`, `pendingScripts` and `ensureScripts` take `catalogs` and throw or reject with the `renderSvg` error for a document that does not resolve; `registry.lazyFonts` still lists 94 faces). Version 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'` in Node, shapes Noto Sans Mongolian again and bundles Raleway and Playfair Display (`registry.lazyFonts` lists 94 faces; no API change). Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
|
|
5
|
+
Version 0.11.9 requires `@openpresentation/opf` ^0.11.4 and draws the design fields (deck and organization logos on covers and section slides, header and footer `logo` parts, `design.listBullet: image` picture bullets at the size PowerPoint draws them, the accent font, `contentDirection` and `chartPrimary` through core composition), and the slide tag falls back to the text colour when the scheme primary is under 4.5:1 against the background; furniture images sit on their zone edge. Use it with opf-pptx 0.11.7 and opf-editor 0.10.6 so preview and export resolve one core (no API change; the preview of covers, list slides with picture bullets, chart slides with a primary chart and slides with furniture images changes). Version 0.11.8 draws the slide tag in the scheme primary colour, as opf-pptx 0.11.5 writes it (no API change; the preview of slides with a tag changes). Version 0.11.7 lets a browser registry load the host's own faces on demand (`extraLazyFonts`, `splitStartupFaces`: a registry can start with Roboto Regular only; additive, `registry.lazyFonts` still lists the 94 vendored faces plus the host's). Version 0.11.6 makes the category-axis labels of chart previews rotate and skip instead of wrapping mid-word (no API change; the preview of dense-axis chart slides changes) and requires `@openpresentation/opf` ^0.11.3 (the 70 legacy gallery layout ids and their geometry); use it with PPTX 0.11.4 and editor 0.10.4 so preview and export resolve the same core. Version 0.11.5 loads vendored preview fonts by face, not by family, and lets the font loaders resolve documents with the host's render options (`presentationFaces`, `lazyFacesNeeded`; `pendingLazyFonts`, `ensureLazyFonts`, `pendingScripts` and `ensureScripts` take `catalogs` and throw or reject with the `renderSvg` error for a document that does not resolve; `registry.lazyFonts` still lists 94 faces). Version 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'` in Node, shapes Noto Sans Mongolian again and bundles Raleway and Playfair Display (`registry.lazyFonts` lists 94 faces; no API change). Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
|
|
6
6
|
|
|
7
7
|
Version 0.9.0 required `@openpresentation/opf` ^0.11.0 and resolved content ColorRef / `variables` through core `resolveColorRef()`. Authored `#RRGGBB` paint stays authored. It retains the existing rendering APIs.
|
|
8
8
|
|
|
@@ -56,7 +56,7 @@ Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets
|
|
|
56
56
|
- Compatibility target: `@openpresentation/opf`
|
|
57
57
|
- Public API: `renderSvg(opf, opts)`, `renderSvgDeck(opf, opts)`, `resolvePresentation(opf, opts)`, `svgToPng(svg, opts)`, and `svgToPdf(svgs, opts)`
|
|
58
58
|
|
|
59
|
-
`renderSvg` renders a single slide selected by `opts.slideIndex` (default `0`). `renderSvgDeck` returns one SVG string per slide. Both validate OPF at the boundary via `@openpresentation/opf`, resolve inline and bundled catalogs locally, and emit byte-stable SVG for the same input.
|
|
59
|
+
`renderSvg` renders a single slide selected by `opts.slideIndex` (default `0`). `renderSvgDeck` returns one SVG string per slide. Both validate OPF at the boundary via `@openpresentation/opf`, resolve inline and bundled catalogs locally, and emit byte-stable SVG for the same input. A catalog `source` may be one string or an ordered array (first match wins, the bundled default is appended); the renderer never fetches, so non-bundled sources resolve only from `catalogSources`. Slide background colours (solid, gradient stops, pattern colours) accept the same colour references as text and table colours: a hex value, a `var:` variable, or a colour scheme slot or role such as `accent2` or `primary`.
|
|
60
60
|
|
|
61
61
|
```js
|
|
62
62
|
import { renderSvgDeck } from "@openpresentation/opf-render";
|
package/dist/index.d.ts
CHANGED
|
@@ -81,6 +81,11 @@ export interface RenderSvgOptions {
|
|
|
81
81
|
date?: string;
|
|
82
82
|
trace?: boolean;
|
|
83
83
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Records for catalog sources the host has already fetched, keyed by the source string a document's
|
|
86
|
+
* `catalogs.<kind>.source` names (a single source or an ordered array; first match wins, the bundled
|
|
87
|
+
* default catalog is appended). The renderer never fetches a source itself.
|
|
88
|
+
*/
|
|
84
89
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
85
90
|
}
|
|
86
91
|
|
package/dist/svg.d.ts
CHANGED
|
@@ -82,6 +82,11 @@ export interface RenderSvgOptions {
|
|
|
82
82
|
date?: string;
|
|
83
83
|
trace?: boolean;
|
|
84
84
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Records for catalog sources the host has already fetched, keyed by the source string a document's
|
|
87
|
+
* `catalogs.<kind>.source` names (a single source or an ordered array; first match wins, the bundled
|
|
88
|
+
* default catalog is appended). The renderer never fetches a source itself.
|
|
89
|
+
*/
|
|
85
90
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
86
91
|
}
|
|
87
92
|
|
package/dist/svg.js
CHANGED
|
@@ -228,8 +228,14 @@ function defaultCatalogFor(kind) {
|
|
|
228
228
|
return Array.isArray(bundledCatalogs[kind]) ? bundledCatalogs[kind] : [];
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// CatalogEntry.source is one source or an ordered search path (first match wins; the
|
|
232
|
+
// default catalog is appended by the callers). An array concatenates each source's
|
|
233
|
+
// records in order, so the findById chain keeps the first match. Non-string entries are
|
|
234
|
+
// ignored. Nothing is fetched: a source resolves from options.catalogSources or, for the
|
|
235
|
+
// bundled default prefixes, from the bundled snapshot.
|
|
231
236
|
function sourceRecordsFor(kind, source, options) {
|
|
232
|
-
if (
|
|
237
|
+
if (Array.isArray(source)) return source.flatMap((entry) => sourceRecordsFor(kind, entry, options));
|
|
238
|
+
if (typeof source !== "string" || !source) return [];
|
|
233
239
|
const bySource = options.catalogSources?.[source];
|
|
234
240
|
if (bySource) return normalizeSourceRecords(bySource);
|
|
235
241
|
if (source.startsWith(DEFAULT_SOURCE_PREFIX) || source.startsWith("pkg:@openpresentation/opf/")) {
|
|
@@ -341,6 +347,12 @@ function normalizeColor(value, fallback) {
|
|
|
341
347
|
|
|
342
348
|
/** Resolve content ColorRef via core; keep authored #RRGGBB / #RRGGBBAA casing. */
|
|
343
349
|
function resolveColorRef(value, bound, fallback) {
|
|
350
|
+
return resolveColorRefIn(value, bound.design, fallback);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// `design` supplies colorScheme, colors (roles) and variables; resolveDesign passes a
|
|
354
|
+
// partial one for the background, before the text-dependent roles exist.
|
|
355
|
+
function resolveColorRefIn(value, design, fallback) {
|
|
344
356
|
if (value == null || value === "") return fallback;
|
|
345
357
|
if (typeof value !== "string") return fallback;
|
|
346
358
|
const trimmed = value.trim();
|
|
@@ -348,9 +360,9 @@ function resolveColorRef(value, bound, fallback) {
|
|
|
348
360
|
// Packed-browser editor checks keep toolbar hex like #2563eb as authored.
|
|
349
361
|
if (/^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/.test(trimmed)) return trimmed;
|
|
350
362
|
if (/^#[0-9a-fA-F]{3}$/.test(trimmed)) return normalizeColor(trimmed, fallback);
|
|
351
|
-
const colors =
|
|
363
|
+
const colors = design.colors ?? {};
|
|
352
364
|
return resolveCoreColorRef(trimmed, {
|
|
353
|
-
colorScheme:
|
|
365
|
+
colorScheme: design.colorScheme ?? {},
|
|
354
366
|
roles: {
|
|
355
367
|
primary: colors.primary,
|
|
356
368
|
secondary: colors.secondary,
|
|
@@ -360,18 +372,30 @@ function resolveColorRef(value, bound, fallback) {
|
|
|
360
372
|
text: colors.text,
|
|
361
373
|
textSecondary: colors.mutedText
|
|
362
374
|
},
|
|
363
|
-
variables:
|
|
375
|
+
variables: design.variables,
|
|
364
376
|
fallback
|
|
365
377
|
});
|
|
366
378
|
}
|
|
367
379
|
|
|
368
|
-
|
|
380
|
+
// Background fills: a literal hex keeps normalizeColor's behaviour (uppercase, alpha kept);
|
|
381
|
+
// anything else is a ColorRef (var:id, scheme slot or role) resolved like table fills and run
|
|
382
|
+
// colours, falling back exactly as an unparseable literal did.
|
|
383
|
+
// Roles resolve through the colour scheme only: background, surface and text are derived from the
|
|
384
|
+
// background itself (the contrast text), so a reference must not see them.
|
|
385
|
+
function resolveBackgroundColor(value, design, fallback) {
|
|
386
|
+
const literal = normalizeColor(value, null);
|
|
387
|
+
if (literal) return literal;
|
|
388
|
+
const { primary, secondary, accent } = design.colors ?? {};
|
|
389
|
+
return resolveColorRefIn(value, { colorScheme: design.colorScheme, colors: { primary, secondary, accent }, variables: design.variables }, fallback);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function resolveBackground(background, colorScheme, design) {
|
|
369
393
|
if (!background) return colorFromScheme(colorScheme, "light1", "#FFFFFF");
|
|
370
394
|
if (typeof background === "string") return colorFromScheme(colorScheme, background, "#FFFFFF");
|
|
371
395
|
if (background.type === "theme") return colorFromScheme(colorScheme, background.slot, "#FFFFFF");
|
|
372
|
-
if (background.type === "solid") return
|
|
396
|
+
if (background.type === "solid") return resolveBackgroundColor(background.color, design, "#FFFFFF");
|
|
373
397
|
if (background.type === "gradient") return null;
|
|
374
|
-
if (background.type === "pattern") return
|
|
398
|
+
if (background.type === "pattern") return resolveBackgroundColor(background.pattern?.backgroundColor, design, "#FFFFFF");
|
|
375
399
|
return colorFromScheme(colorScheme, "light1", "#FFFFFF");
|
|
376
400
|
}
|
|
377
401
|
|
|
@@ -385,6 +409,21 @@ function colorLuminance(color) {
|
|
|
385
409
|
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
|
386
410
|
}
|
|
387
411
|
|
|
412
|
+
// FF-61: WCAG 2.x contrast ratio, (L1 + 0.05) / (L2 + 0.05) with L1 the lighter relative luminance. The slide tag
|
|
413
|
+
// draws in the primary colour unless that is under 4.5:1 against the slide background; opf-pptx applies the same
|
|
414
|
+
// rule with the same arithmetic (test/tag-colour.mjs in both repositories pins the same colour pairs).
|
|
415
|
+
const TAG_MIN_CONTRAST = 4.5;
|
|
416
|
+
function contrastRatio(first, second) {
|
|
417
|
+
const a = colorLuminance(first), b = colorLuminance(second);
|
|
418
|
+
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
|
419
|
+
}
|
|
420
|
+
// The slide background the tag sits on: the resolved solid or pattern colour, else (gradient) the scheme light1,
|
|
421
|
+
// the colour the background rect also falls back to and opf-pptx uses for a background that is not one colour.
|
|
422
|
+
function tagFill(design) {
|
|
423
|
+
const background = design.backgroundColor ?? design.colors.background;
|
|
424
|
+
return contrastRatio(design.colors.primary, background) < TAG_MIN_CONTRAST ? design.colors.text : design.colors.primary;
|
|
425
|
+
}
|
|
426
|
+
|
|
388
427
|
function resolveDesign(presentation, slide, context, index) {
|
|
389
428
|
const deckDesign = presentation.design ?? {};
|
|
390
429
|
const slideDesign = slide.design ?? {};
|
|
@@ -411,7 +450,12 @@ function resolveDesign(presentation, slide, context, index) {
|
|
|
411
450
|
);
|
|
412
451
|
const dimensions = resolveDimensions(slideDesign.dimensions ?? deckDesign.dimensions ?? theme.dimensions);
|
|
413
452
|
const backgroundDefinition = slideDesign.background ?? deckDesign.background ?? theme.background;
|
|
414
|
-
const
|
|
453
|
+
const primary = normalizeColor(colorScheme.primary, null) ?? colorFromScheme(colorScheme, "accent1", "#2563EB");
|
|
454
|
+
const secondary = normalizeColor(colorScheme.secondary, null) ?? colorFromScheme(colorScheme, "accent2", "#0F766E");
|
|
455
|
+
const accent = normalizeColor(colorScheme.accent, null) ?? colorFromScheme(colorScheme, "accent3", "#F59E0B");
|
|
456
|
+
const variables = presentation.variables ?? {};
|
|
457
|
+
// background/surface/text roles depend on the background itself, so a background reference sees only the scheme and the three accent roles.
|
|
458
|
+
const backgroundColor = resolveBackground(backgroundDefinition, colorScheme, { colorScheme, colors: { primary, secondary, accent }, variables });
|
|
415
459
|
const darkBackground = colorLuminance(backgroundColor ?? "#FFFFFF") < 0.179;
|
|
416
460
|
const textColor = colorFromScheme(colorScheme, darkBackground ? "light1" : "dark1", darkBackground ? "#FFFFFF" : "#111827");
|
|
417
461
|
|
|
@@ -422,7 +466,7 @@ function resolveDesign(presentation, slide, context, index) {
|
|
|
422
466
|
colorScheme,
|
|
423
467
|
fontScheme,
|
|
424
468
|
dimensions,
|
|
425
|
-
variables
|
|
469
|
+
variables,
|
|
426
470
|
background: backgroundDefinition,
|
|
427
471
|
backgroundColor,
|
|
428
472
|
colors: {
|
|
@@ -430,12 +474,13 @@ function resolveDesign(presentation, slide, context, index) {
|
|
|
430
474
|
surface: colorFromScheme(colorScheme, darkBackground ? "dark2" : "light2", darkBackground ? "#1E293B" : "#F8FAFC"),
|
|
431
475
|
text: textColor,
|
|
432
476
|
mutedText: colorFromScheme(colorScheme, darkBackground ? "light2" : "dark2", darkBackground ? "#E2E8F0" : "#334155"),
|
|
433
|
-
primary
|
|
434
|
-
secondary
|
|
435
|
-
accent
|
|
477
|
+
primary,
|
|
478
|
+
secondary,
|
|
479
|
+
accent,
|
|
436
480
|
border: colorFromScheme(colorScheme, "accent5", "#CBD5E1")
|
|
437
481
|
},
|
|
438
482
|
fonts: resolveFontFamilies(fontScheme),
|
|
483
|
+
darkBackground,
|
|
439
484
|
diagnostics: fontSchemeDiagnostic ? [fontSchemeDiagnostic] : []
|
|
440
485
|
};
|
|
441
486
|
}
|
|
@@ -638,8 +683,9 @@ function bindSlide(presentation, slide, layout, index, context) {
|
|
|
638
683
|
onFallback: note => reportGlyphFallback(context, note)
|
|
639
684
|
});
|
|
640
685
|
const textMeasurement = scriptFonts.textMeasurement ?? context.options.textMeasurement;
|
|
641
|
-
|
|
642
|
-
|
|
686
|
+
// fontScheme.accent (the tag and quote text) exists only with a core that resolves it; it takes the same look-alike policy.
|
|
687
|
+
for (const role of ["heading","body","code","accent"]) if (design.fonts[role] !== undefined) design.fonts[role] =resolveTextStyle({fontFamily:design.fonts[role],fontWeight:role === "heading" ? 700 : 400},textMeasurement).fontFamily;
|
|
688
|
+
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, darkBackground: design.darkBackground, textMeasurement, date: context.options.date, socialPlatforms: socialPlatformRecords(context) });
|
|
643
689
|
return {
|
|
644
690
|
scriptFonts,
|
|
645
691
|
textMeasurement,
|
|
@@ -737,7 +783,7 @@ function renderBackground(bound, width, height, options) {
|
|
|
737
783
|
const dx=Math.cos(angle)*50,dy=Math.sin(angle)*50;
|
|
738
784
|
const stopTags = stops.map((stop, index) => tag("stop", {
|
|
739
785
|
offset: stableNumber((stop.position ?? index / Math.max(1, stops.length - 1)) * 100) + "%",
|
|
740
|
-
"stop-color":
|
|
786
|
+
"stop-color": resolveBackgroundColor(stop.color, bound.design, bound.design.colors.background)
|
|
741
787
|
}));
|
|
742
788
|
return [
|
|
743
789
|
tag("defs", traceAttrs(options, `${bound.path}.design.background`), tag("linearGradient", {
|
|
@@ -768,13 +814,13 @@ function renderBackground(bound, width, height, options) {
|
|
|
768
814
|
return tag('g',{opacity:background.opacity??1},renderImage(imageItem,{x:0,y:0,width,height},bound,{...options,imageFit:background.image.fit??'cover'}));
|
|
769
815
|
}
|
|
770
816
|
if(isPlainObject(background) && background.type==='pattern'){
|
|
771
|
-
const pattern=background.pattern??{},id=`opf-s${bound.index+1}-pattern`,color=
|
|
817
|
+
const pattern=background.pattern??{},id=`opf-s${bound.index+1}-pattern`,color=resolveBackgroundColor(pattern.foregroundColor,bound.design,bound.design.colors.text),preset=pattern.preset;
|
|
772
818
|
// 8px cells approximating DrawingML presets. diagStripe is the engine id that
|
|
773
819
|
// PPTX export writes as wdUpDiag, so both draw the same rising stripe.
|
|
774
820
|
const stroke=(d,width=1)=>tag('path',{d,fill:'none',stroke:color,'stroke-width':width});
|
|
775
821
|
const mark=preset==='ltHorz'?tag('path',{d:'M0 4H8',stroke:color,'stroke-width':1}):preset==='diagStripe'||preset==='wdUpDiag'?tag('path',{d:'M-2 2L2 -2M0 8L8 0M6 10L10 6',stroke:color,'stroke-width':2}):preset==='pct5'?tag('circle',{cx:2,cy:2,r:.8,fill:color}):preset==='openDmnd'?stroke('M0 4L4 0L8 4L4 8Z'):preset==='wave'?stroke('M0 4C2 1 2 1 4 4S6 7 8 4'):'';
|
|
776
822
|
if(!mark)reportDiagnostic({code:'unsupported-pattern',path:`${bound.path}.design.background.pattern.preset`,message:`Pattern ${preset} is not implemented by the SVG preview.`},options);
|
|
777
|
-
return tag('g',{opacity:background.opacity??1},tag('rect',{width,height,fill:
|
|
823
|
+
return tag('g',{opacity:background.opacity??1},tag('rect',{width,height,fill:bound.design.backgroundColor??'#FFFFFF'})+tag('defs',{},tag('pattern',{id,width:8,height:8,patternUnits:'userSpaceOnUse'},mark))+tag('rect',{width,height,fill:`url(#${id})`}));
|
|
778
824
|
}
|
|
779
825
|
return tag("rect", {
|
|
780
826
|
x: 0,
|
|
@@ -895,9 +941,13 @@ function renderTextPayload(item, box, bound, options) {
|
|
|
895
941
|
// follow design.titleAlignment only (unset is left, as in core composition).
|
|
896
942
|
align: item.alignment ?? (item.field === "title" ? bound.design.titleAlignment ?? "left" : bound.design.contentAlignment),
|
|
897
943
|
fontSize: item.field === "title" ? 54 : item.field === "tag" ? 16 : 25,
|
|
898
|
-
|
|
944
|
+
// fontScheme.accent draws the tag; core's textStyle (item.textStyle) already carries it, this is the fallback family.
|
|
945
|
+
fontFamily: item.field === "title" ? bound.design.fonts.heading : item.field === "tag" ? bound.design.fonts.accent ?? bound.design.fonts.body : bound.design.fonts.body,
|
|
899
946
|
fontWeight: item.field === "title" ? 700 : 400,
|
|
900
|
-
|
|
947
|
+
// The slide tag is the eyebrow label: it draws in the primary colour (scheme accent1), as the
|
|
948
|
+
// PPTX export writes it, or in the text colour when the primary is under 4.5:1 against the
|
|
949
|
+
// slide background (FF-61). Every other text payload here uses the text colour.
|
|
950
|
+
fill: item.field === "tag" ? tagFill(bound.design) : bound.design.colors.text,
|
|
901
951
|
fit: item.text,
|
|
902
952
|
textStyle: item.textStyle,
|
|
903
953
|
diagnosticsHandled: Boolean(item.text),
|
|
@@ -909,8 +959,11 @@ function renderList(item, box, bound, options) {
|
|
|
909
959
|
const scale=Math.min(bound.design.dimensions.width,bound.design.dimensions.height)/720;
|
|
910
960
|
const fit=item.text?.listEntries?item.text:fitList(item.value,box,25*scale,((bound.composition??bound.geometry.composition).minFontSize??16)*scale,{style:{fontFamily:bound.design.fonts.body,fontWeight:400,path:item.path},textMeasurement:options.textMeasurement});
|
|
911
961
|
const children=[];
|
|
962
|
+
// design.listBullet=image: core attaches the icon logo as item.bulletImage and its box (entry.bulletBox: 0.65 em, as PowerPoint draws a:buBlip). An icon that cannot be drawn keeps the glyph marker.
|
|
963
|
+
const bullet=item.bulletImage?resolveBulletImage(item.bulletImage,bound,options):undefined;
|
|
912
964
|
for(const entry of fit.listEntries){
|
|
913
|
-
|
|
965
|
+
if(bullet){const box=entry.bulletBox??{x:entry.marker.x,y:entry.marker.y-entry.marker.fontSize*.65,width:entry.marker.fontSize*.65,height:entry.marker.fontSize*.65};children.push(tag('image',{x:stableNumber(box.x),y:stableNumber(box.y),width:stableNumber(box.width),height:stableNumber(box.height),href:bullet,preserveAspectRatio:'xMidYMid meet','aria-hidden':'true',...(options.trace?{'data-opf-generated':'true'}:{})}));}
|
|
966
|
+
else children.push(tag('text',{x:stableNumber(entry.marker.x),y:stableNumber(entry.marker.y),'font-family':fontStack(entry.marker.style.fontFamily,bound.design.fontScheme.type),'font-size':stableNumber(entry.marker.fontSize),fill:bound.design.colors.text,'aria-hidden':'true'},escapeText(entry.marker.text)));
|
|
914
967
|
const config={path:entry.textPath,align:'left',fill:bound.design.colors.text,rich:Array.isArray(entry.value),options};
|
|
915
968
|
children.push(renderRichLines(typeof entry.value==='string'?[entry.value]:entry.value,entry.text,entry.textBox,bound,config));
|
|
916
969
|
if(entry.description)children.push(renderRichLines(typeof entry.descriptionValue==='string'?[entry.descriptionValue]:entry.descriptionValue,entry.description,entry.descriptionBox,bound,{...config,path:entry.descriptionPath,rich:Array.isArray(entry.descriptionValue),fill:bound.design.colors.mutedText}));
|
|
@@ -918,7 +971,8 @@ function renderList(item, box, bound, options) {
|
|
|
918
971
|
return tag('g',{...traceAttrs(options,item.path),...(fit.overflow?{'data-opf-overflow':'true'}:{})},children.join('\n'));
|
|
919
972
|
}
|
|
920
973
|
|
|
921
|
-
|
|
974
|
+
// Follows asset: references and the host imageResolver to the drawable source of an image value.
|
|
975
|
+
function resolveImageSource(item, bound, options) {
|
|
922
976
|
let asset = normalizeAsset(item.value);
|
|
923
977
|
const seen = new Set();
|
|
924
978
|
let missingReference;
|
|
@@ -931,10 +985,27 @@ function renderImage(item, box, bound, options) {
|
|
|
931
985
|
asset = {...normalizeAsset(bound.assets[id]),...overrides};
|
|
932
986
|
}
|
|
933
987
|
const source = options.imageResolver?.(asset.src, { asset, path: item.path }) ?? asset.src;
|
|
934
|
-
|
|
988
|
+
const drawable = typeof source === "string" && /^data:image\/(png|jpeg|gif|webp);base64,/i.test(source);
|
|
989
|
+
return { asset, source, drawable, missingReference };
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// The drawable data URI of a picture bullet, or undefined (after reporting unresolved-asset once) so the glyph marker stays.
|
|
993
|
+
function resolveBulletImage(bulletImage, bound, options) {
|
|
994
|
+
const { drawable, source, asset, missingReference } = resolveImageSource({ value: bulletImage.source, path: bulletImage.path }, bound, options);
|
|
995
|
+
if (drawable) return source;
|
|
996
|
+
const reason=missingReference?'missing-reference':!asset.src?'missing-source':'unsupported-source';
|
|
997
|
+
const message=missingReference?`Bullet image asset ${missingReference} is missing; the preview draws the character bullet.`:'A picture bullet requires an embedded raster data URI or a host imageResolver; the preview draws the character bullet.';
|
|
998
|
+
if (options.strictAssets) throw new OPFRenderError("unresolved-asset", message, { path: bulletImage.path,reason });
|
|
999
|
+
reportDiagnostic({code:'unresolved-asset',path:bulletImage.path,message,reason,source:asset.src,assetId:missingReference,description:asset.alt??asset.title??'Bullet image',placeholder:'glyph'},options);
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function renderImage(item, box, bound, options) {
|
|
1004
|
+
const { asset, source, drawable, missingReference } = resolveImageSource(item, bound, options);
|
|
1005
|
+
if (drawable) {
|
|
935
1006
|
return tag("image", { x: box.x, y: box.y, width: box.width, height: box.height,
|
|
936
|
-
href: source, preserveAspectRatio: (options.imageFit ?? (bound.design.imageFill === "crop" ? "cover" : "contain")) === "cover" ? "xMidYMid slice" : "xMidYMid meet", role: "img", "aria-label": asset.alt ?? "Image",
|
|
937
|
-
...traceAttrs(options, item.path) });
|
|
1007
|
+
href: source, preserveAspectRatio: options.imageAnchor === "left" ? "xMinYMid meet" : (options.imageFit ?? (bound.design.imageFill === "crop" ? "cover" : "contain")) === "cover" ? "xMidYMid slice" : "xMidYMid meet", role: "img", "aria-label": asset.alt ?? options.imageLabel ?? "Image",
|
|
1008
|
+
...traceAttrs(options, item.path), ...generatedAttrs(options) });
|
|
938
1009
|
}
|
|
939
1010
|
const reason=missingReference?'missing-reference':!asset.src?'missing-source':'unsupported-source';
|
|
940
1011
|
const message=missingReference?`Image asset ${missingReference} is missing; supply the referenced asset or resolve it in the host.`:'Image requires an embedded raster data URI or a host imageResolver.';
|
|
@@ -979,7 +1050,7 @@ function renderImage(item, box, bound, options) {
|
|
|
979
1050
|
const size=Math.max(0,Math.min(inner.width,inner.height,24*scale)),icon=centeredBox(inner,size,size);
|
|
980
1051
|
children.push(tag('path',{d:`M ${icon.x} ${icon.y} L ${icon.x+size} ${icon.y+size} M ${icon.x+size} ${icon.y} L ${icon.x} ${icon.y+size}`,fill:'none',stroke:textColor,'stroke-width':Math.min(2*scale,size/8),'aria-hidden':'true'}));
|
|
981
1052
|
}
|
|
982
|
-
return tag("g", {...traceAttrs(options,item.path),'data-opf-asset-status':'unresolved',role:'img','aria-label':`Image unavailable: ${description}`},children.join("\n"));
|
|
1053
|
+
return tag("g", {...traceAttrs(options,item.path),...generatedAttrs(options),'data-opf-asset-status':'unresolved',role:'img','aria-label':`Image unavailable: ${description}`},children.join("\n"));
|
|
983
1054
|
}
|
|
984
1055
|
|
|
985
1056
|
function renderMedia(item, box, bound, options) {
|
|
@@ -1346,6 +1417,9 @@ function renderBranding(bound,presentation,width,height,options) {
|
|
|
1346
1417
|
if(design.watermark){
|
|
1347
1418
|
pieces.push(tag('g',{opacity:typeof design.watermark==='object'?design.watermark.opacity??.08:.08},renderImage({value:design.watermark,path:rootFor('watermark')},{x:width*.3,y:height*.3,width:width*.4,height:height*.4},bound,{...options,imageFit:'contain'})));
|
|
1348
1419
|
}
|
|
1420
|
+
// Cover and section slides: the deck logo core composed at the top-left of the free area, anchored left.
|
|
1421
|
+
const logo=bound.geometry.logo;
|
|
1422
|
+
if(logo)pieces.push(renderImage({value:logo.source,path:logo.path},logo.box,bound,{...options,imageAnchor:'left',imageLabel:'Logo',imageGenerated:true}));
|
|
1349
1423
|
return pieces.join('');
|
|
1350
1424
|
}
|
|
1351
1425
|
|
|
@@ -1679,6 +1753,11 @@ function traceAttrs(options, path) {
|
|
|
1679
1753
|
return options.trace ? { "data-opf-path": path } : {};
|
|
1680
1754
|
}
|
|
1681
1755
|
|
|
1756
|
+
// Generated pictures (the deck logo) are drawn from design fields, not authored content: editors skip them.
|
|
1757
|
+
function generatedAttrs(options) {
|
|
1758
|
+
return options.trace && options.imageGenerated ? { "data-opf-generated": "true" } : {};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1682
1761
|
function stableNumber(value) {
|
|
1683
1762
|
if (Number.isInteger(value)) return String(value);
|
|
1684
1763
|
return Number(value).toFixed(3).replace(/\.?0+$/, "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-render",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.9",
|
|
4
4
|
"description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"sideEffects": false,
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "node scripts/build.mjs",
|
|
59
|
-
"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/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/theme-slots.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/mongolian-shaping.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/extra-lazy-fonts.mjs && node --check test/extra-lazy-fonts-browser.mjs && node --check test/lazy-faces.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.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/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check test/chart-category-labels.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.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 && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check test/gallery-layout-alignment.mjs",
|
|
59
|
+
"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/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/theme-slots.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/mongolian-shaping.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/extra-lazy-fonts.mjs && node --check test/extra-lazy-fonts-browser.mjs && node --check test/lazy-faces.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.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/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/catalog-source-array-and-background-refs.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check test/chart-category-labels.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.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 && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/tag-colour.mjs && node --check test/design-fields.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check test/gallery-layout-alignment.mjs",
|
|
60
60
|
"validate": "node scripts/validate-package.mjs",
|
|
61
|
-
"test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-category-labels.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.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/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/extra-lazy-fonts.mjs && node test/lazy-faces.mjs && node test/lazy-face-fallback.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/rich-tab.mjs && node test/color-references.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/theme-slots.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/mongolian-shaping.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
|
|
61
|
+
"test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-category-labels.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.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/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/extra-lazy-fonts.mjs && node test/lazy-faces.mjs && node test/lazy-face-fallback.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/rich-tab.mjs && node test/color-references.mjs && node test/catalog-source-array-and-background-refs.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/theme-slots.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/mongolian-shaping.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/tag-colour.mjs && node test/design-fields.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
|
|
62
62
|
"check:fonts": "node test/font-licenses.mjs && npm run check:font-hotlinks",
|
|
63
63
|
"check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
|
|
64
64
|
"test:golden": "npm run build && node test/golden.mjs",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@expo-google-fonts/roboto": "0.4.3",
|
|
98
98
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
99
99
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
100
|
-
"@openpresentation/opf": "^0.11.
|
|
100
|
+
"@openpresentation/opf": "^0.11.4",
|
|
101
101
|
"@resvg/resvg-js": "^2.6.2",
|
|
102
102
|
"fontkit": "^2.0.4",
|
|
103
103
|
"pdf-lib": "^1.17.1",
|