@openpresentation/opf-render 0.14.0 → 0.15.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 +21 -14
- package/dist/charts.js +8 -12
- package/dist/index.d.ts +18 -33
- package/dist/script-font-pack.js +9 -8
- package/dist/svg.d.ts +18 -33
- package/dist/svg.js +148 -150
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# OPF Render
|
|
2
2
|
|
|
3
|
+
**OPF 0.15 (FA-23, unreleased; requires core 0.15).** Catalogs are registered by the host, and every picture is an image background or an image block. The breaking changes are listed in `changes/fa-23-opf-0-15.md` (the next CHANGELOG section); the [0.15 design](https://github.com/OpenPresentation/opf/blob/main/docs/programs/format-audit/0.15-design.md) explains them.
|
|
4
|
+
|
|
5
|
+
- The renderer bundles, looks up and fetches no catalog. Every entry point (`renderSvg`, `renderSlideSvg`, `resolvePresentation`, the font loaders' `renderOptions`, `/player`, `<opf-deck>`) takes core's `catalogs: Catalog[]` and passes it unchanged to core resolution. Register the gallery snapshot yourself: `import { defaultCatalog } from '@openpresentation/opf/catalog'` and `renderSvg(deck, { catalogs: [defaultCatalog] })`. `strictReferences: true` fails with `unresolved-reference` instead of falling back.
|
|
6
|
+
- What resolves nowhere draws with core's engine defaults (`engineDefaults` is `{ theme, colorScheme, fontScheme, chartType }`: core's `ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME`, `ENGINE_DEFAULT_FONT_SCHEME`) and is reported once per reference as `unresolved-reference`.
|
|
7
|
+
- An image background (`design.background: { type: "image", src, alt, fit, focus, opacity, recolor, overlay }`, or an image source string) draws from core's `geometry.backgroundImage`: the canvas colour, the picture (cover, contain, stretch or tile; recolor and opacity on the pixels only), then the overlay (the whole slide or an edge band). `alt` is the picture's accessible name; without it the picture is decorative.
|
|
8
|
+
- Every content picture is an image block drawn from `item.image`: frame, fit (`cover`, `contain`, `stretch`; default `design.imageFit`, else `cover`), focus, shape mask, border, opacity, recolor and overlay, and a placed block (`placement`) at its edge band.
|
|
9
|
+
|
|
3
10
|
**0.14.0 (RR-55, with core 0.14.0, opf-pptx 0.14.0 and opf-editor 0.14.0).** One fonts handle, whole-deck and one-slide render functions, and core's slide context. This release renames and deletes outright (no aliases; the CHANGELOG has the old-to-new table):
|
|
4
11
|
|
|
5
12
|
- `renderSvg(deck, options)` returns `string[]`, one SVG per slide; `renderSlideSvg(deck, index, options)` returns the SVG of one slide.
|
|
@@ -23,13 +30,13 @@ const pdf = await svgToPdf(slides, { fonts });
|
|
|
23
30
|
const pptx = await toPptx(presentation, { fonts });
|
|
24
31
|
```
|
|
25
32
|
|
|
26
|
-
A slide's canvas, layout, theme, colour scheme and font scheme come from core's `resolveSlideContext` (slide design, then deck design, then theme, then the default), so preview, pagination, validation and export resolve them one way. A slide with no `layout
|
|
33
|
+
A slide's canvas, layout, theme, colour scheme and font scheme come from core's `resolveSlideContext` (slide design, then deck design, then theme, then the engine default), from the records the document embeds and the catalogs the host registers (`catalogs`), so preview, pagination, validation and export resolve them one way. A slide with no `layout` composes automatically. A reference that resolves nowhere never throws: the slide composes automatically or draws with core's engine defaults, and the render reports `unresolved-reference` (naming the reference and the catalog's source) through `onDiagnostic`; with `strictReferences: true` it throws `OPFRenderError` `unresolved-reference` instead.
|
|
27
34
|
|
|
28
35
|
An option a function does not know is not read: a top-level `textMeasurement` or `embeddedFonts` is ignored, so pass `{ fonts }`.
|
|
29
36
|
|
|
30
37
|
**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), the `loadFonts()` handle's `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 `loadFonts({ faces, lazyFontsBaseUrl })` from `/fonts-browser` loads them on demand: `await fonts.ensure(presentation)` (the registry's `ensureLazyFonts` and `ensureScripts` are the lower-level calls) 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: `loadFonts({ faces: 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. `fonts.pending(presentation)` (and the registry's `pendingLazyFonts`) 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 `loadFonts({ faces, 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 `loadFonts` keeps 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.
|
|
31
38
|
|
|
32
|
-
Version 0.12.0 requires `@openpresentation/opf` ^0.12.0 (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored) and draws what that core composes, plus the preview changes listed in the changelog; use it with opf-pptx 0.12.0 and opf-editor 0.11.0 so preview and export resolve one core. 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,
|
|
39
|
+
Version 0.12.0 requires `@openpresentation/opf` ^0.12.0 (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored) and draws what that core composes, plus the preview changes listed in the changelog; use it with opf-pptx 0.12.0 and opf-editor 0.11.0 so preview and export resolve one core. 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, 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.
|
|
33
40
|
|
|
34
41
|
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.
|
|
35
42
|
|
|
@@ -43,7 +50,7 @@ Version 0.8.0 adds optional font-registry vector outlines and shared heading/sca
|
|
|
43
50
|
|
|
44
51
|
Version 0.8.0 requires core 0.10.0 and renders accepted quote and code geometry without fitting it again. Code filename/language/body parts preserve source whitespace, literal tabs and metadata case and expose trace targets for editing. The [43 reviewed code raster changes](docs/evidence/shared-code/raster-review.json) retain the other 762 corpus hashes. Coordinated releases PPTX 0.8.0 and editor 0.7.0 add native source recovery and editing. Glyph containment and separation do not establish native pixel equivalence.
|
|
45
52
|
|
|
46
|
-
Deterministic local renderer for Open Presentation Format documents. The shared SVG core implements validation,
|
|
53
|
+
Deterministic local renderer for Open Presentation Format documents. The shared SVG core implements validation, reference resolution (through core, against the catalogs the host registers), placeholder binding and text layout. Node APIs additionally convert SVG to PNG and to PDF (vector with selectable text by default, or raster-backed).
|
|
47
54
|
|
|
48
55
|
In version 0.8.0, `design.contentBox` uses core's shared padded geometry. The card renders at `item.frameBox`; its payload uses `item.box` and accepted internals. Card padding participates in composition scoring, strict overflow and pagination. This requires core 0.10.0.
|
|
49
56
|
|
|
@@ -86,7 +93,7 @@ Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets
|
|
|
86
93
|
- Public API: `renderSvg(opf, opts)` (every slide), `renderSlideSvg(opf, index, opts)` (one slide), `resolvePresentation(opf, opts)`, `svgToPng(svg, opts)`, `svgToPdf(svgs, opts)` and `loadFonts(opts)` (`/fonts-node`, `/fonts-browser`)
|
|
87
94
|
- Player and embedding (RR-28): `<opf-deck>` and a slideshow with a speaker view, see [Player and `<opf-deck>`](#player-and-opf-deck-rr-28)
|
|
88
95
|
|
|
89
|
-
`renderSvg` returns one SVG string per slide (`skipHidden: true` leaves out slides marked `hidden`, the player's sequence, so the result is then shorter than the deck); `renderSlideSvg` returns the SVG of one slide (a zero-based index; out of range throws `slide-index-out-of-range`). Both check the format at the boundary (core `validate(deck, { only: ['format'] })`; a failure throws `OPFRenderError` `invalid-opf` whose `findings` are the error findings), resolve
|
|
96
|
+
`renderSvg` returns one SVG string per slide (`skipHidden: true` leaves out slides marked `hidden`, the player's sequence, so the result is then shorter than the deck); `renderSlideSvg` returns the SVG of one slide (a zero-based index; out of range throws `slide-index-out-of-range`). Both check the format at the boundary (core `validate(deck, { only: ['format'] })`; a failure throws `OPFRenderError` `invalid-opf` whose `findings` are the error findings), resolve references from the document's embedded records and the host's `catalogs` (core `Catalog[]`, never fetched), and emit byte-stable SVG for the same input. 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`.
|
|
90
97
|
|
|
91
98
|
The preview draws three accepted spec fields from tables shared with opf-pptx through core (`@openpresentation/opf`): `code.language` colours code (comments, strings, numbers, keywords, names and types, in theme-derived colours kept at 4.5:1 or more on the code panel; an unknown language stays plain and the text is never changed), `metric.trend` draws an arrow beside the trend word (green up, red down, neutral flat, with "Trend: up" as its text alternative) and colours the delta and trend text, and every one of the 54 DrawingML preset pattern backgrounds draws from its 8 x 8 bitmap (one pixel per 1/96 inch from the slide's top-left). With a core that lacks these exports the preview draws plain, without a new diagnostic. `scripts/derive-pattern-bitmaps.mjs` compares a PowerPoint export of the pattern slides with core's tiles (ECMA-376 names the presets without defining their pixels; core's tiles were measured with it from desktop PowerPoint, Office 365, Windows, 2026-10-01).
|
|
92
99
|
|
|
@@ -105,19 +112,19 @@ The installed open font pack works offline. The Aptos family previews with the m
|
|
|
105
112
|
|
|
106
113
|
Unresolved images produce an `unresolved-asset` diagnostic through `onDiagnostic`, including the OPF path, reason and complete description. The fallback shows a bounded status label when it fits above the selected readability floor, otherwise an icon with the full accessible description. The label is `Image unavailable` over the description (alt, else title, else `Image`) in semibold at 20 px on a 1280 x 720 canvas, and opf-pptx exports the same placeholder as native shapes. It preserves authored opacity, including faint decorative watermarks; a missing image is still missing even when its fallback fits. Use `strictAssets: true` to reject unresolved images. Supply embedded raster data URIs or a synchronous host `imageResolver` to resolve them; the renderer does not fetch URLs or read local paths. Caller descriptions and other metadata override referenced asset metadata through alias chains. These diagnostics identify unresolved sources, not malformed image bytes or native PowerPoint compatibility.
|
|
107
114
|
|
|
108
|
-
Header/footer images and watermarks fit their complete artwork within their allocated regions.
|
|
115
|
+
Header/footer images and watermarks fit their complete artwork within their allocated regions. Image blocks use their own `fit`, else `design.imageFit` (default `cover`), as core resolves it into `item.image.fit`.
|
|
109
116
|
|
|
110
|
-
An `image/svg+xml` data URI (base64 or text) is drawn as an image wherever a raster is (
|
|
117
|
+
An `image/svg+xml` data URI (base64 or text) is drawn as an image wherever a raster is (image block, header/footer, watermark, logo, background, picture bullet), with the raster's box and fit, when it is an `<svg>` in the SVG namespace with an intrinsic size (`width` and `height`, or a `viewBox`); anything else keeps the `Image unavailable` placeholder. opf-pptx exports the same picture as a native SVG picture over a PNG fallback. An SVG used as an image never runs script or loads anything outside its own document, in a browser or in PNG/PDF output; PNG and PDF output draw it with resvg, and its text uses the same bundled fonts.
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
Image backgrounds (core `geometry.backgroundImage`, FA-22) never move content. They paint in this order: the canvas colour (the colour scheme's default slide background), the picture, the overlay, then watermark, logo and content. `cover` (the default, and what an image source string means) covers the slide and keeps the `focus` point in view, `contain` centers the whole picture, `stretch` fills the slide, and `tile` repeats the picture at its intrinsic size (one picture pixel per reference pixel) from the top-left. `opacity` applies to the picture only. With `trace: true` the picture carries the background's path (`slides.N.design.background`, `design.background` or `theme`).
|
|
113
120
|
|
|
114
|
-
|
|
121
|
+
Image blocks (core `item.image`, FA-22) draw from core's normalized geometry in the native picture's paint order:
|
|
115
122
|
|
|
116
|
-
1. The image, clipped by the preset outline
|
|
123
|
+
1. The picture in `item.image.box` with `fit` and `focus` (a centered cover is the native center crop; another focus uses core's `fitImage` placement), clipped by the preset outline core computes from the DrawingML formula (`imageShape()`). `recolor` is applied as an sRGB `feColorMatrix` with Rec. 601 luminance weights (grayscale, or duotone from dark to light). `opacity` applies to the picture only.
|
|
117
124
|
2. The centered border stroke on the same outline.
|
|
118
|
-
3. The overlay
|
|
125
|
+
3. The overlay, over the frame's shape or an edge band (`data-opf-image-overlay` names its path with `trace: true`).
|
|
119
126
|
|
|
120
|
-
Unresolved sources draw no treatments. See core `docs/image-treatments.md` for the vocabulary and the unsupported effects: blur, shadows, soft edges and background removal.
|
|
127
|
+
A placed block (`placement`) arrives with its edge band as its region, and headings and body compose in the rest of the slide; with `trace: true` its group carries `data-opf-image-placement`. The 15 pptx.gallery image treatments, written this way, draw the same pixels as the 0.14 renderer drew them (`test/image-treatments.mjs`). Unresolved sources draw the ordinary placeholder and no treatments. See core `docs/image-treatments.md` for the vocabulary and the unsupported effects: blur, shadows, soft edges and background removal.
|
|
121
128
|
|
|
122
129
|
PNG and PDF conversion APIs are async because they load the local raster/PDF engines on demand:
|
|
123
130
|
|
|
@@ -318,8 +325,8 @@ fonts.registry.scriptSelection; // { detected, scripts, unavailable, packages, n
|
|
|
318
325
|
const browserFonts = await loadBrowserFonts({ faces: baseEntries, substitutionPolicy: 'visual', fallbackFamily: 'Roboto', scriptBaseUrl: '/script-fonts/' });
|
|
319
326
|
if (browserFonts.pending(presentation).length) await browserFonts.ensure(presentation); // resolves { scripts, lazy, uncovered }
|
|
320
327
|
// pending is synchronous: empty means nothing is missing, so render at once.
|
|
321
|
-
// Text decides, so the analysis does not resolve layouts
|
|
322
|
-
// loadFonts, or per call) so a font scheme only the host's catalogs have and that names a script font (Yu Gothic) counts.
|
|
328
|
+
// Text decides, so the analysis does not resolve layouts. Pass the render options (renderOptions: { catalogs: [defaultCatalog] }
|
|
329
|
+
// to loadFonts, or per call) so a font scheme only the host's catalogs have and that names a script font (Yu Gothic) counts.
|
|
323
330
|
```
|
|
324
331
|
|
|
325
332
|
Where the renderer has per-character glyph fallback (`glyphFallbackFamilies`), auto also loads Noto Sans when the text has Greek or Cyrillic (the chosen Latin face may lack it), and the next CJK face of the fallback chain when a drawn Han, kana or Hangul character is missing from the loaded ones (a Simplified-only hanzi in Japanese text, hanja). That is capped at one fallback package beyond the packages the text's own scripts need: a character no CJK face covers never pulls in all four (10 to 20 MiB each); it is listed in `ensureScripts().uncovered` (Node: `script-glyph-uncovered`, `registry.scriptSelection.uncovered`) and the renderer reports `missing-glyph`. Browser loads are all or nothing per package: a failed fetch, hash check or `FontFace.load()` leaves the registry, the document and `loadedScriptPackages` unchanged and the package pending. One failing package does not lose the others: `ensureScripts` loads every package it can, then rejects with an `OPFFontError` whose `details` are `{ loaded, failed: [{package, code, message}] }`, and the next call retries only the failures. `ensureScripts(presentation, { signal })` and `loadScripts(scripts, { signal })` take a per-call `AbortSignal` (the creation signal is not reused), and `dispose()` during a load leaves no face behind and rejects with `font-registry-disposed`. Script faces carry `embed: "used"`, like the open families: the eager `registry.embeddedFonts` leaves them out, and when you pass them explicitly (`registry.selectEmbeddedFonts(face => face.scripts)`) an SVG embeds one only when the slide's text uses its family, so a Latin slide never carries a CJK face.
|
|
@@ -391,7 +398,7 @@ The package runtime must stay local and deterministic:
|
|
|
391
398
|
|
|
392
399
|
Dependency policy:
|
|
393
400
|
|
|
394
|
-
- `@openpresentation/opf` is the compatibility source for schemas, validation, and
|
|
401
|
+
- `@openpresentation/opf` is the compatibility source for schemas, validation, resolution and composition. The renderer imports no catalog data; hosts register catalogs (`@openpresentation/opf/catalog` holds the gallery snapshot).
|
|
395
402
|
- `@resvg/resvg-js` is used only for local SVG rasterization in Node; it makes no network calls and does not require a browser.
|
|
396
403
|
- `pdf-lib` assembles raster-mode PDF bytes locally. Metadata timestamps are disabled so repeated PDF output is byte-stable for the same SVG input and options.
|
|
397
404
|
- Vector PDF output is written by a small deterministic PDF writer in this package, with `pako` for Flate compression (pure JavaScript, so independent of the platform zlib build), `fontkit` for shaping and font metrics and `bidi-js` for the Unicode bidirectional algorithm. No hosted service, network call or system font is involved.
|
package/dist/charts.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
// Chart previews for the OPF chart
|
|
1
|
+
// Chart previews for the OPF chart types (FF-22).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// The table below is carried here so the preview stays synchronous and
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// chart; any other id returns null so the caller keeps its legacy single-series preview.
|
|
3
|
+
// OPF 0.15 makes chart.type an engine vocabulary: core's schema enum and CHART_TYPES, one type per Office chart construct.
|
|
4
|
+
// The table below is carried here so the preview stays synchronous and self-contained; test/chart-catalog.mjs checks it
|
|
5
|
+
// against core's CHART_TYPES. Each type renders the construct PowerPoint shows for the exported chart; any other value
|
|
6
|
+
// returns null so the caller keeps its legacy single-series preview.
|
|
8
7
|
import { chartHighlightColors, chartHighlightMarks, chartNumber, chartPaletteForFill, formatDataNumber, numberFormatError, resolveChartData, resolveTextStyle, textColorForFill, textWidthMeasurer } from "@openpresentation/opf/composition";
|
|
9
8
|
import { drawAxisTitles, drawBarLabel, drawCenteredLabel, drawPointLabel, drawSliceLabel, labelString, outsideLabelReserve, reportOptionDiagnostics, reserveAxisTitles, resolveOptions } from "./chart-options.js";
|
|
10
9
|
|
|
@@ -81,12 +80,9 @@ export const CHART_TYPES = Object.freeze({
|
|
|
81
80
|
combo: { kind: "combo", dir: "col", grouping: "clustered", markers: true }
|
|
82
81
|
});
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** Resolve a chart type id to its kept catalog id, or null for ids outside the catalog. */
|
|
83
|
+
/** Resolve a chart type (core's CHART_TYPES vocabulary, OPF 0.15) to its preview spec id, or null for any other value. */
|
|
87
84
|
export function resolveChartType(type) {
|
|
88
|
-
const
|
|
89
|
-
const id = ALIASES[raw] ?? raw;
|
|
85
|
+
const id = String(type ?? "").trim().toLowerCase();
|
|
90
86
|
return Object.hasOwn(CHART_TYPES, id) ? id : null;
|
|
91
87
|
}
|
|
92
88
|
|
|
@@ -114,7 +110,7 @@ const RENDERERS = {
|
|
|
114
110
|
const CHARTEX_KINDS = new Set(["treemap", "histogram", "pareto", "box", "waterfall", "funnel", "map"]);
|
|
115
111
|
|
|
116
112
|
/**
|
|
117
|
-
* Render a
|
|
113
|
+
* Render a chart of a known type. Returns null when the type is outside CHART_TYPES, has
|
|
118
114
|
* no preview renderer, or carries no inline category-major data, so the caller
|
|
119
115
|
* can keep its legacy output ("No chart data" for unresolved sources).
|
|
120
116
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderFonts, ScriptFonts } from "./fonts.js";
|
|
2
|
-
import type { Finding } from "@openpresentation/opf";
|
|
2
|
+
import type { Catalog, Finding, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
3
3
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
4
4
|
export declare const packageName = "@openpresentation/opf-render";
|
|
5
5
|
|
|
@@ -17,19 +17,12 @@ export declare const runtimePolicy: Readonly<{
|
|
|
17
17
|
deterministicLocalExecution: true;
|
|
18
18
|
}>;
|
|
19
19
|
|
|
20
|
+
/** What draws when nothing resolves: core's engine defaults (OPF 0.15 ships no catalog records), and the chart type a chart without `type` previews as. */
|
|
20
21
|
export declare const engineDefaults: Readonly<{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
narrative: "classic-story";
|
|
26
|
-
tone: "formal";
|
|
27
|
-
audience: "executives";
|
|
28
|
-
fontScheme: Readonly<{
|
|
29
|
-
pptx: Readonly<{ latin: "aptos"; ea: "microsoft-yahei"; cs: "nirmala-ui" }>;
|
|
30
|
-
google: Readonly<{ latin: "roboto"; ea: "noto-sans-sc"; cs: "noto-sans" }>;
|
|
31
|
-
}>;
|
|
32
|
-
chartTypes: readonly ["stacked-column", "stacked-area", "line-with-markers"];
|
|
22
|
+
theme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_THEME;
|
|
23
|
+
colorScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_COLOR_SCHEME;
|
|
24
|
+
fontScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_FONT_SCHEME;
|
|
25
|
+
chartType: string;
|
|
33
26
|
}>;
|
|
34
27
|
|
|
35
28
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
@@ -45,20 +38,7 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
45
38
|
code: "unsupported-pattern" | "variable-example-used" | "variable-builtin-missing" | "date-needs-value" | "language-preview-unresolved";
|
|
46
39
|
path: string;
|
|
47
40
|
message: string;
|
|
48
|
-
} | {
|
|
49
|
-
/** A font-scheme id matched no record; the default font scheme (`aptos`) was used as the base. */
|
|
50
|
-
code: "unresolved-font-scheme";
|
|
51
|
-
path: string;
|
|
52
|
-
message: string;
|
|
53
|
-
id: string;
|
|
54
|
-
fallback: string;
|
|
55
|
-
} | {
|
|
56
|
-
/** A layout, theme or colour-scheme id matched no record: the slide composed with no layout record, the `minimal` theme or the `cool-horizon` colour scheme. Never an error. */
|
|
57
|
-
code: "unresolved-layout" | "unresolved-theme" | "unresolved-color-scheme";
|
|
58
|
-
path: string;
|
|
59
|
-
message: string;
|
|
60
|
-
id: string;
|
|
61
|
-
} | {
|
|
41
|
+
} | UnresolvedReferenceDiagnostic | {
|
|
62
42
|
code: "unresolved-asset";
|
|
63
43
|
path: string;
|
|
64
44
|
message: string;
|
|
@@ -100,13 +80,18 @@ export interface RenderSvgOptions {
|
|
|
100
80
|
*/
|
|
101
81
|
date?: string;
|
|
102
82
|
trace?: boolean;
|
|
103
|
-
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
104
83
|
/**
|
|
105
|
-
*
|
|
106
|
-
* `
|
|
107
|
-
*
|
|
84
|
+
* Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
|
|
85
|
+
* embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
|
|
86
|
+
* when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
|
|
87
|
+
* `import { defaultCatalog } from "@openpresentation/opf/catalog"` and `catalogs: [defaultCatalog]`. Omitted: only embedded
|
|
88
|
+
* records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
|
|
89
|
+
* A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
|
|
90
|
+
* (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
|
|
108
91
|
*/
|
|
109
|
-
|
|
92
|
+
catalogs?: readonly Catalog[];
|
|
93
|
+
/** Fail with `unresolved-reference` (an OPFRenderError whose `details.diagnostics` list the references) instead of falling back when a reference resolves nowhere. Default false. */
|
|
94
|
+
strictReferences?: boolean;
|
|
110
95
|
}
|
|
111
96
|
|
|
112
97
|
export interface SvgToPngOptions {
|
|
@@ -233,7 +218,7 @@ export interface ResolvedSlide {
|
|
|
233
218
|
index: number;
|
|
234
219
|
path: string;
|
|
235
220
|
slide: unknown;
|
|
236
|
-
/** The layout record the slide names; absent when it names none or
|
|
221
|
+
/** The layout record the slide names; absent when it names none or a reference that resolves nowhere (the slide then composes automatically). */
|
|
237
222
|
layout?: unknown;
|
|
238
223
|
design: {
|
|
239
224
|
theme: unknown;
|
package/dist/script-font-pack.js
CHANGED
|
@@ -92,10 +92,10 @@ export function* drawnStrings(presentation) {
|
|
|
92
92
|
/**
|
|
93
93
|
* The script profile of a presentation: core `resolveScriptFonts` output, exactly what the renderer plans
|
|
94
94
|
* with. When it throws, the renderer ignores the document language, so the profile is empty and Han text is
|
|
95
|
-
* Simplified Chinese, as drawn.
|
|
95
|
+
* Simplified Chinese, as drawn. `renderOptions.catalogs` (the host's registered catalogs) resolve as they do for the renderer.
|
|
96
96
|
*/
|
|
97
|
-
export function presentationScriptProfile(presentation) {
|
|
98
|
-
try { return resolveScriptFonts(presentation); } catch { /* the renderer falls back the same way */ }
|
|
97
|
+
export function presentationScriptProfile(presentation, renderOptions) {
|
|
98
|
+
try { return resolveScriptFonts(presentation, renderOptions?.catalogs !== undefined ? { catalogs: renderOptions.catalogs } : {}); } catch { /* the renderer falls back the same way */ }
|
|
99
99
|
return {};
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -123,9 +123,9 @@ function scriptOfFamily(family) {
|
|
|
123
123
|
* scheme (`slide.design`), so each such slide is resolved too. `han` is the CJK script the scheme names, which tells
|
|
124
124
|
* Han-only text apart when the language does not (a Yu Gothic deck whose language is English).
|
|
125
125
|
*
|
|
126
|
-
* `renderOptions` are the host's `renderSvg` options
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* `renderOptions` are the host's `renderSvg` options: their `catalogs` (core `Catalog[]`) go to core's `resolveScriptFonts`
|
|
127
|
+
* unchanged, and the families the renderer resolves per slide count too, so a scheme that exists only in the host's catalogs
|
|
128
|
+
* and names a script font still loads that font.
|
|
129
129
|
*/
|
|
130
130
|
function designScripts(presentation, profile, renderOptions) {
|
|
131
131
|
const scripts = new Set();
|
|
@@ -142,7 +142,7 @@ function designScripts(presentation, profile, renderOptions) {
|
|
|
142
142
|
if (Array.isArray(presentation?.slides)) {
|
|
143
143
|
presentation.slides.forEach((slide, slideIndex) => {
|
|
144
144
|
if (!slide || typeof slide !== "object" || !slide.design || typeof slide.design !== "object") return;
|
|
145
|
-
try { note(resolveScriptFonts(presentation, { slideIndex })); } catch { /* the renderer reports it when it draws the slide */ }
|
|
145
|
+
try { note(resolveScriptFonts(presentation, { slideIndex, ...(renderOptions?.catalogs !== undefined ? { catalogs: renderOptions.catalogs } : {}) })); } catch { /* the renderer reports it when it draws the slide */ }
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
if (renderOptions?.catalogs) {
|
|
@@ -172,7 +172,8 @@ function* namedFontFamilies(value, key) {
|
|
|
172
172
|
* `renderSvg` options) matter only for font schemes the host supplies in `renderOptions.catalogs`, which then resolve
|
|
173
173
|
* like the renderer resolves them; a document that then does not resolve throws what `renderSvg` throws for it.
|
|
174
174
|
*/
|
|
175
|
-
export function analyzePresentationScripts(presentation, profile
|
|
175
|
+
export function analyzePresentationScripts(presentation, profile, renderOptions) {
|
|
176
|
+
profile ??= presentationScriptProfile(presentation, renderOptions);
|
|
176
177
|
const scripts = new Set(), cjk = new Set();
|
|
177
178
|
const design = designScripts(presentation, profile, renderOptions);
|
|
178
179
|
// Han-only text in a deck whose language is not CJK draws with the scheme's own CJK face when the scheme names one.
|
package/dist/svg.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RenderFonts, ScriptFonts } from "./fonts.js";
|
|
2
|
-
import type { Finding } from "@openpresentation/opf";
|
|
2
|
+
import type { Catalog, Finding, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
3
3
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
4
4
|
export declare const packageName = "@openpresentation/opf-render";
|
|
5
5
|
|
|
@@ -17,19 +17,12 @@ export declare const runtimePolicy: Readonly<{
|
|
|
17
17
|
deterministicLocalExecution: true;
|
|
18
18
|
}>;
|
|
19
19
|
|
|
20
|
+
/** What draws when nothing resolves: core's engine defaults (OPF 0.15 ships no catalog records), and the chart type a chart without `type` previews as. */
|
|
20
21
|
export declare const engineDefaults: Readonly<{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
narrative: "classic-story";
|
|
26
|
-
tone: "formal";
|
|
27
|
-
audience: "executives";
|
|
28
|
-
fontScheme: Readonly<{
|
|
29
|
-
pptx: Readonly<{ latin: "aptos"; ea: "microsoft-yahei"; cs: "nirmala-ui" }>;
|
|
30
|
-
google: Readonly<{ latin: "roboto"; ea: "noto-sans-sc"; cs: "noto-sans" }>;
|
|
31
|
-
}>;
|
|
32
|
-
chartTypes: readonly ["stacked-column", "stacked-area", "line-with-markers"];
|
|
22
|
+
theme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_THEME;
|
|
23
|
+
colorScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_COLOR_SCHEME;
|
|
24
|
+
fontScheme: typeof import("@openpresentation/opf/composition").ENGINE_DEFAULT_FONT_SCHEME;
|
|
25
|
+
chartType: string;
|
|
33
26
|
}>;
|
|
34
27
|
|
|
35
28
|
export type RenderDiagnostic = LayoutDiagnostic | {
|
|
@@ -45,20 +38,7 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
45
38
|
code: "unsupported-pattern" | "variable-example-used" | "variable-builtin-missing" | "date-needs-value" | "language-preview-unresolved";
|
|
46
39
|
path: string;
|
|
47
40
|
message: string;
|
|
48
|
-
} | {
|
|
49
|
-
/** A font-scheme id matched no record; the default font scheme (`aptos`) was used as the base. */
|
|
50
|
-
code: "unresolved-font-scheme";
|
|
51
|
-
path: string;
|
|
52
|
-
message: string;
|
|
53
|
-
id: string;
|
|
54
|
-
fallback: string;
|
|
55
|
-
} | {
|
|
56
|
-
/** A layout, theme or colour-scheme id matched no record: the slide composed with no layout record, the `minimal` theme or the `cool-horizon` colour scheme. Never an error. */
|
|
57
|
-
code: "unresolved-layout" | "unresolved-theme" | "unresolved-color-scheme";
|
|
58
|
-
path: string;
|
|
59
|
-
message: string;
|
|
60
|
-
id: string;
|
|
61
|
-
} | {
|
|
41
|
+
} | UnresolvedReferenceDiagnostic | {
|
|
62
42
|
code: "unresolved-asset";
|
|
63
43
|
path: string;
|
|
64
44
|
message: string;
|
|
@@ -101,13 +81,18 @@ export interface RenderSvgOptions {
|
|
|
101
81
|
*/
|
|
102
82
|
date?: string;
|
|
103
83
|
trace?: boolean;
|
|
104
|
-
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
105
84
|
/**
|
|
106
|
-
*
|
|
107
|
-
* `
|
|
108
|
-
*
|
|
85
|
+
* Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
|
|
86
|
+
* embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
|
|
87
|
+
* when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
|
|
88
|
+
* `import { defaultCatalog } from "@openpresentation/opf/catalog"` and `catalogs: [defaultCatalog]`. Omitted: only embedded
|
|
89
|
+
* records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
|
|
90
|
+
* A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
|
|
91
|
+
* (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
|
|
109
92
|
*/
|
|
110
|
-
|
|
93
|
+
catalogs?: readonly Catalog[];
|
|
94
|
+
/** Fail with `unresolved-reference` (an OPFRenderError whose `details.diagnostics` list the references) instead of falling back when a reference resolves nowhere. Default false. */
|
|
95
|
+
strictReferences?: boolean;
|
|
111
96
|
}
|
|
112
97
|
|
|
113
98
|
export interface SvgToPngOptions {
|
|
@@ -140,7 +125,7 @@ export interface ResolvedSlide {
|
|
|
140
125
|
index: number;
|
|
141
126
|
path: string;
|
|
142
127
|
slide: unknown;
|
|
143
|
-
/** The layout record the slide names; absent when it names none or
|
|
128
|
+
/** The layout record the slide names; absent when it names none or a reference that resolves nowhere (the slide then composes automatically). */
|
|
144
129
|
layout?: unknown;
|
|
145
130
|
design: {
|
|
146
131
|
theme: unknown;
|
package/dist/svg.js
CHANGED
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
resolveColorRef as resolveCoreColorRef, resolveScriptFonts, paragraphDirection, physicalAlignment, patternRuns,
|
|
6
6
|
metricTrendMark as coreMetricTrendMark, tokenizeCode, codeLineRuns, codeSyntaxPaletteForScheme, resolveChartData,
|
|
7
7
|
resolveColorRoles, defaultSlideBackground, timelineMarkerShapes, timelineTextColor, layoutWatermark,
|
|
8
|
-
codeHighlightLines, codeLineNumbers, codeHighlightBands, codeHighlightColors
|
|
8
|
+
codeHighlightLines, codeLineNumbers, codeHighlightBands, codeHighlightColors, fitImage, intrinsicImageAspect, intrinsicImageSize,
|
|
9
|
+
ENGINE_DEFAULT_THEME, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_CHART_TYPES
|
|
9
10
|
} from "@openpresentation/opf/composition";
|
|
11
|
+
// The renderer is a library: it never imports a catalog. Hosts register theirs (`options.catalogs`, core `Catalog[]`, for
|
|
12
|
+
// example `defaultCatalog` from `@openpresentation/opf/catalog`) and core resolves every reference against them.
|
|
10
13
|
import {
|
|
11
|
-
catalogs as bundledCatalogs,
|
|
12
14
|
hasContentVariables,
|
|
13
15
|
isTemplate,
|
|
14
16
|
resolveSlideContext,
|
|
@@ -37,34 +39,13 @@ export const runtimePolicy = Object.freeze({
|
|
|
37
39
|
deterministicLocalExecution: true
|
|
38
40
|
});
|
|
39
41
|
|
|
42
|
+
// What draws when nothing resolves: core's engine defaults (OPF 0.15 ships no catalog records), and the chart type a chart
|
|
43
|
+
// without one previews as.
|
|
40
44
|
export const engineDefaults = Object.freeze({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
fontSchemes: Object.freeze({ source: "https://www.pptx.gallery/font-schemes" }),
|
|
46
|
-
languages: Object.freeze({ source: "https://www.pptx.gallery/languages" }),
|
|
47
|
-
layouts: Object.freeze({ source: "https://www.pptx.gallery/layouts" }),
|
|
48
|
-
chartTypes: Object.freeze({ source: "https://www.pptx.gallery/chart-types" }),
|
|
49
|
-
tones: Object.freeze({ source: "https://www.pptx.gallery/tones" }),
|
|
50
|
-
audiences: Object.freeze({ source: "https://www.pptx.gallery/audiences" }),
|
|
51
|
-
socialPlatforms: Object.freeze({ source: "https://www.pptx.gallery/social-platforms" })
|
|
52
|
-
}),
|
|
53
|
-
theme: "minimal",
|
|
54
|
-
colorScheme: "cool-horizon",
|
|
55
|
-
language: "english",
|
|
56
|
-
narrative: "classic-story",
|
|
57
|
-
tone: "formal",
|
|
58
|
-
audience: "executives",
|
|
59
|
-
fontScheme: Object.freeze({
|
|
60
|
-
pptx: Object.freeze({ latin: "aptos", ea: "microsoft-yahei", cs: "nirmala-ui" }),
|
|
61
|
-
google: Object.freeze({ latin: "roboto", ea: "noto-sans-sc", cs: "noto-sans" })
|
|
62
|
-
}),
|
|
63
|
-
chartTypes: Object.freeze([
|
|
64
|
-
"stacked-column",
|
|
65
|
-
"stacked-area",
|
|
66
|
-
"line-with-markers"
|
|
67
|
-
])
|
|
45
|
+
theme: ENGINE_DEFAULT_THEME,
|
|
46
|
+
colorScheme: ENGINE_DEFAULT_COLOR_SCHEME,
|
|
47
|
+
fontScheme: ENGINE_DEFAULT_FONT_SCHEME,
|
|
48
|
+
chartType: ENGINE_DEFAULT_CHART_TYPES[0]
|
|
68
49
|
});
|
|
69
50
|
|
|
70
51
|
/**
|
|
@@ -164,7 +145,6 @@ const FIELD_TYPE = Object.freeze({
|
|
|
164
145
|
});
|
|
165
146
|
|
|
166
147
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
167
|
-
const DEFAULT_SOURCE_PREFIX = "https://www.pptx.gallery/";
|
|
168
148
|
|
|
169
149
|
|
|
170
150
|
function parseInput(input) {
|
|
@@ -220,8 +200,8 @@ function resolveTemplateInput(presentation, options) {
|
|
|
220
200
|
|
|
221
201
|
// The boundary check is the format check only (schema and the cross-field rules a schema cannot say): layout, accessibility
|
|
222
202
|
// and content findings are `validate`'s job, and drawing a slide reports what it cannot fit.
|
|
223
|
-
function assertValidBoundary(presentation) {
|
|
224
|
-
const report = validate(presentation, { only: ["format"] });
|
|
203
|
+
function assertValidBoundary(presentation, options) {
|
|
204
|
+
const report = validate(presentation, { only: ["format"], ...(options.catalogs !== undefined ? { catalogs: options.catalogs } : {}) });
|
|
225
205
|
if (!report.valid) {
|
|
226
206
|
throw new OPFRenderError("invalid-opf", "OPF validation failed.", {
|
|
227
207
|
findings: report.findings.filter((finding) => finding.severity === "error"),
|
|
@@ -246,63 +226,6 @@ function isPlainObject(value) {
|
|
|
246
226
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
247
227
|
}
|
|
248
228
|
|
|
249
|
-
function normalizeSourceRecords(value) {
|
|
250
|
-
if (!value) return [];
|
|
251
|
-
if (Array.isArray(value)) return value;
|
|
252
|
-
if (Array.isArray(value.records)) return value.records;
|
|
253
|
-
return [];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function defaultCatalogFor(kind) {
|
|
257
|
-
return Array.isArray(bundledCatalogs[kind]) ? bundledCatalogs[kind] : [];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// CatalogEntry.source is one source or an ordered search path (first match wins; the
|
|
261
|
-
// default catalog is appended by the callers). An array concatenates each source's
|
|
262
|
-
// records in order, so the findById chain keeps the first match. Non-string entries are
|
|
263
|
-
// ignored. Nothing is fetched: a source resolves from options.catalogSources or, for the
|
|
264
|
-
// bundled default prefixes, from the bundled snapshot.
|
|
265
|
-
function sourceRecordsFor(kind, source, options) {
|
|
266
|
-
if (Array.isArray(source)) return source.flatMap((entry) => sourceRecordsFor(kind, entry, options));
|
|
267
|
-
if (typeof source !== "string" || !source) return [];
|
|
268
|
-
const bySource = options.catalogSources?.[source];
|
|
269
|
-
if (bySource) return normalizeSourceRecords(bySource);
|
|
270
|
-
if (source.startsWith(DEFAULT_SOURCE_PREFIX) || source.startsWith("pkg:@openpresentation/opf/")) {
|
|
271
|
-
return defaultCatalogFor(kind);
|
|
272
|
-
}
|
|
273
|
-
return [];
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// The records a host adds to core's `resolveSlideContext`, which looks a layout, theme, colour scheme or font scheme up in the
|
|
277
|
-
// deck's inline records, then these, then its bundled catalogs. In the renderer's order they are the records a
|
|
278
|
-
// `catalogs.<kind>.source` names (a bundled source, or `options.catalogSources`), then `options.catalogs`.
|
|
279
|
-
const CONTEXT_CATALOG_KINDS = ["themes", "colorSchemes", "fontSchemes", "layouts"];
|
|
280
|
-
function contextCatalogs(context) {
|
|
281
|
-
if (context.contextCatalogs) return context.contextCatalogs;
|
|
282
|
-
const out = {};
|
|
283
|
-
for (const kind of CONTEXT_CATALOG_KINDS) {
|
|
284
|
-
const records = [
|
|
285
|
-
...sourceRecordsFor(kind, context.presentation.catalogs?.[kind]?.source, context.options),
|
|
286
|
-
...normalizeSourceRecords(context.options.catalogs?.[kind]),
|
|
287
|
-
...sourceRecordsFor(kind, engineDefaults.catalogs[kind]?.source, context.options)
|
|
288
|
-
];
|
|
289
|
-
if (records.length) out[kind] = records;
|
|
290
|
-
}
|
|
291
|
-
return (context.contextCatalogs = out);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Generated socials furniture formats handles in core. Core applies inline
|
|
295
|
-
// document records first; the host supplies the rest in resolution order, ahead of the bundled ones.
|
|
296
|
-
function socialPlatformRecords(context, bundled) {
|
|
297
|
-
const kind = "socialPlatforms";
|
|
298
|
-
return [
|
|
299
|
-
...sourceRecordsFor(kind, context.presentation.catalogs?.[kind]?.source, context.options),
|
|
300
|
-
...normalizeSourceRecords(context.options.catalogs?.[kind]),
|
|
301
|
-
...sourceRecordsFor(kind, engineDefaults.catalogs[kind]?.source, context.options),
|
|
302
|
-
...bundled
|
|
303
|
-
];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
229
|
function colorFromScheme(scheme, slot, fallback) {
|
|
307
230
|
if (!slot) return fallback;
|
|
308
231
|
if (typeof slot === "string") {
|
|
@@ -368,12 +291,15 @@ function resolveBackgroundColor(value, design, fallback) {
|
|
|
368
291
|
return resolveColorRefIn(value, { colorScheme: design.colorScheme, colors: { primary, secondary, accent }, variables: design.variables }, fallback);
|
|
369
292
|
}
|
|
370
293
|
|
|
294
|
+
// Image sources the background string shorthand accepts (a cover image); any other string is a theme slot or a hex colour.
|
|
295
|
+
const IMAGE_BACKGROUND_SOURCE = /^(asset:|https:\/\/|data:|\.\/|\.\.\/)/;
|
|
296
|
+
|
|
371
297
|
// The slide's single background color, or null when there is none (a gradient, or no color the engine can read).
|
|
372
298
|
// With no definition, or a picture, the scheme's default slide background (core defaultSlideBackground) is the canvas.
|
|
373
299
|
function resolveBackground(background, colorScheme, design) {
|
|
374
300
|
const canvas = defaultSlideBackground(colorScheme);
|
|
375
301
|
if (!background) return canvas;
|
|
376
|
-
if (typeof background === "string") return colorFromScheme(colorScheme, background, canvas);
|
|
302
|
+
if (typeof background === "string") return IMAGE_BACKGROUND_SOURCE.test(background) ? canvas : colorFromScheme(colorScheme, background, canvas);
|
|
377
303
|
if (background.type === "theme") return colorFromScheme(colorScheme, background.slot, canvas);
|
|
378
304
|
if (background.type === "solid") return resolveBackgroundColor(background.color, design, "#FFFFFF");
|
|
379
305
|
if (background.type === "gradient") return null;
|
|
@@ -410,10 +336,9 @@ const LINK_URL = /^(https?:|mailto:|tel:)/i;
|
|
|
410
336
|
|
|
411
337
|
// What the renderer paints with, from the records core's `resolveSlideContext` resolved (slide design, then deck design, then
|
|
412
338
|
// theme, then the engine default, per field: the one order every engine uses). The theme and the colour scheme keep their
|
|
413
|
-
// sorted keys, so the output does not depend on the order a catalog lists a field in.
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// as render diagnostics.
|
|
339
|
+
// sorted keys, so the output does not depend on the order a catalog lists a field in. A reference that resolves nowhere never
|
|
340
|
+
// throws (unless `strictReferences`): the context falls back to core's engine defaults (ENGINE_DEFAULT_THEME, _COLOR_SCHEME,
|
|
341
|
+
// _FONT_SCHEME; a layout to automatic composition) and reports `unresolved-reference`, which is forwarded as a render diagnostic.
|
|
417
342
|
function resolveDesign(presentation, slide, slideContext) {
|
|
418
343
|
const deckDesign = presentation.design ?? {};
|
|
419
344
|
const slideDesign = slide.design ?? {};
|
|
@@ -468,7 +393,7 @@ function resolveDesign(presentation, slide, slideContext) {
|
|
|
468
393
|
*/
|
|
469
394
|
function scriptProfile(presentation, index, design, context, language) {
|
|
470
395
|
let resolved;
|
|
471
|
-
try { resolved = resolveScriptFonts(presentation, { slideIndex: index, ...(language === undefined ? {} : { language }) }); }
|
|
396
|
+
try { resolved = resolveScriptFonts(presentation, { slideIndex: index, ...(context.options.catalogs !== undefined ? { catalogs: context.options.catalogs } : {}), ...(language === undefined ? {} : { language }) }); }
|
|
472
397
|
catch (error) {
|
|
473
398
|
reportLanguageDiagnostic(context, { code: "language-preview-unresolved", path: "language",
|
|
474
399
|
message: "Script fonts could not be resolved (" + (error instanceof Error ? error.message : String(error)) + "), so the preview uses the design font for every script, sets no lang and lays out every paragraph left to right." });
|
|
@@ -594,16 +519,35 @@ function normalizeFutureContent(slide, slidePath) {
|
|
|
594
519
|
});
|
|
595
520
|
}
|
|
596
521
|
|
|
522
|
+
// The core catalog options every resolution takes: the host's catalogs, unchanged, and its strict reference policy.
|
|
523
|
+
function catalogOptions(options) {
|
|
524
|
+
return { ...(options.catalogs !== undefined ? { catalogs: options.catalogs } : {}), ...(options.strictReferences === true ? { strictReferences: true } : {}) };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function resolveContext(presentation, index, context) {
|
|
528
|
+
try {
|
|
529
|
+
return resolveSlideContext(presentation, index, {
|
|
530
|
+
fonts: { textMeasurement: context.options.textMeasurement },
|
|
531
|
+
...catalogOptions(context.options),
|
|
532
|
+
date: context.options.date
|
|
533
|
+
});
|
|
534
|
+
} catch (error) {
|
|
535
|
+
// Strict references: core refuses a reference that resolves nowhere (OPFUnresolvedReferenceError) instead of falling back.
|
|
536
|
+
if (Array.isArray(error?.diagnostics) && error.diagnostics.some(entry => entry?.code === "unresolved-reference")) {
|
|
537
|
+
throw new OPFRenderError("unresolved-reference", error.message, { diagnostics: error.diagnostics, path: error.diagnostics[0].path, cause: error.message });
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
597
543
|
function bindSlide(presentation, slide, index, context) {
|
|
598
544
|
const slidePath = `slides.${index}`;
|
|
599
545
|
// Core resolves the slide's canvas, layout, theme, colour scheme, font scheme and family names the one way every engine must
|
|
600
|
-
// (slide design, deck design, theme, default)
|
|
601
|
-
//
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
date: context.options.date
|
|
606
|
-
});
|
|
546
|
+
// (slide design, deck design, theme, engine default), from the records the document embeds and the catalogs the host
|
|
547
|
+
// registered (`options.catalogs`, passed unchanged); `fonts` carries the measurement the host loaded. A slide with no
|
|
548
|
+
// `layout` composes automatically; one whose reference resolves nowhere does too, with an `unresolved-reference` diagnostic,
|
|
549
|
+
// or the render fails under `strictReferences`.
|
|
550
|
+
const slideContext = resolveContext(presentation, index, context);
|
|
607
551
|
const layout = slideContext.options.layout;
|
|
608
552
|
const placeholders = Array.isArray(layout?.placeholders) ? layout.placeholders : [];
|
|
609
553
|
const titleBindings = [];
|
|
@@ -670,7 +614,7 @@ function bindSlide(presentation, slide, index, context) {
|
|
|
670
614
|
const named = design.fonts[role], resolved = resolveTextStyle({fontFamily:named,fontWeight:role === "heading" ? 700 : 400},textMeasurement).fontFamily;
|
|
671
615
|
design.fonts[role] = fontPolicyFor(named)?.replacement?.weight !== undefined && resolved.toLowerCase() !== named.toLowerCase() ? named : resolved;
|
|
672
616
|
}
|
|
673
|
-
const geometry = composeSlide(slide, { ...slideContext.options, fontFamilies: design.fonts, textRasterPadding:context.options.textRasterPadding, textMeasurement
|
|
617
|
+
const geometry = composeSlide(slide, { ...slideContext.options, fontFamilies: design.fonts, textRasterPadding:context.options.textRasterPadding, textMeasurement });
|
|
674
618
|
return {
|
|
675
619
|
scriptFonts,
|
|
676
620
|
scriptFontsFor,
|
|
@@ -708,7 +652,7 @@ export function resolvePresentation(input, options = {}) {
|
|
|
708
652
|
function resolveDeck(input, options) {
|
|
709
653
|
const presentation = resolveTemplateInput(parseInput(input), options);
|
|
710
654
|
if (options.validate !== false) {
|
|
711
|
-
assertValidBoundary(presentation);
|
|
655
|
+
assertValidBoundary(presentation, options);
|
|
712
656
|
}
|
|
713
657
|
|
|
714
658
|
const context = { presentation, options };
|
|
@@ -757,7 +701,6 @@ function renderResolvedSlide(resolved, slideIndex, options) {
|
|
|
757
701
|
const title = (Array.isArray(bound.slide.title) ? flattenText(bound.slide.title) : bound.slide.title) ?? resolved.presentation.name ?? `Slide ${slideIndex + 1}`;
|
|
758
702
|
const content = [
|
|
759
703
|
renderBackground(bound, width, height, options),
|
|
760
|
-
renderSlideImage(bound, options),
|
|
761
704
|
renderBranding(bound, resolved.presentation, width, height, options),
|
|
762
705
|
...renderSlideContent(bound, width, height, options),
|
|
763
706
|
// RR-34: the footnote area core reserved above the footer band, after the content and before the furniture.
|
|
@@ -793,6 +736,8 @@ function renderResolvedSlide(resolved, slideIndex, options) {
|
|
|
793
736
|
|
|
794
737
|
function renderBackground(bound, width, height, options) {
|
|
795
738
|
const background = bound.design.background;
|
|
739
|
+
// An image background (core SlideComposition.backgroundImage) fills the canvas and moves nothing.
|
|
740
|
+
if (bound.geometry.backgroundImage) return renderBackgroundImage(bound.geometry.backgroundImage, bound, width, height, options);
|
|
796
741
|
if (isPlainObject(background) && background.type === "gradient") {
|
|
797
742
|
const id = `opf-s${bound.index + 1}-background`;
|
|
798
743
|
const stops = Array.isArray(background.gradient?.stops) ? background.gradient.stops : [];
|
|
@@ -821,15 +766,6 @@ function renderBackground(bound, width, height, options) {
|
|
|
821
766
|
})
|
|
822
767
|
].join("\n");
|
|
823
768
|
}
|
|
824
|
-
|
|
825
|
-
if(isPlainObject(background) && background.type==='image'){
|
|
826
|
-
const imageItem={value:background.image,path:`${bound.path}.design.background.image`};
|
|
827
|
-
if(background.image.fit==='tile'){
|
|
828
|
-
const size=Math.min(width,height)/4,id=`opf-s${bound.index+1}-image-tile`;
|
|
829
|
-
return tag('g',{opacity:background.opacity??1},tag('defs',{},tag('pattern',{id,width:size,height:size,patternUnits:'userSpaceOnUse'},renderImage(imageItem,{x:0,y:0,width:size,height:size},bound,options)))+tag('rect',{width,height,fill:`url(#${id})`}));
|
|
830
|
-
}
|
|
831
|
-
return tag('g',{opacity:background.opacity??1},renderImage(imageItem,{x:0,y:0,width,height},bound,{...options,imageFit:background.image.fit??'cover'}));
|
|
832
|
-
}
|
|
833
769
|
if(isPlainObject(background) && background.type==='pattern'){
|
|
834
770
|
const pattern=background.pattern??{},id=`opf-s${bound.index+1}-pattern`,color=resolveBackgroundColor(pattern.foregroundColor,bound.design,bound.design.colors.text),preset=pattern.preset;
|
|
835
771
|
// RR-07: every DrawingML preset (core PATTERN_PRESETS) is an 8x8 bitmap, one unit per 1/96 inch, anchored at the slide origin.
|
|
@@ -844,43 +780,81 @@ function renderBackground(bound, width, height, options) {
|
|
|
844
780
|
y: 0,
|
|
845
781
|
width,
|
|
846
782
|
height,
|
|
847
|
-
opacity:
|
|
783
|
+
opacity: isPlainObject(background) && background.type !== 'image' ? background.opacity ?? 1 : 1,
|
|
848
784
|
fill: bound.design.backgroundColor ?? bound.design.colors.background,
|
|
849
785
|
...traceAttrs(options, `${bound.path}.design.background`)
|
|
850
786
|
});
|
|
851
787
|
}
|
|
852
788
|
|
|
853
|
-
//
|
|
854
|
-
//
|
|
855
|
-
//
|
|
856
|
-
// and
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const
|
|
861
|
-
const
|
|
862
|
-
const picture =
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
789
|
+
// FA-22 image background, in the paint order of the geometry contract: the canvas colour (the colour scheme's default slide
|
|
790
|
+
// background), the picture (recolor and opacity on its pixels only, as for an image block), then the overlay (the whole canvas
|
|
791
|
+
// or an edge band). cover, contain
|
|
792
|
+
// and stretch use the shared fit math with the focus point; tile repeats the picture at its intrinsic size (1 picture px = 1
|
|
793
|
+
// reference px) from the canvas top-left. alt is the picture's accessible name; without alt the picture is decorative.
|
|
794
|
+
function renderBackgroundImage(background, bound, width, height, options) {
|
|
795
|
+
const canvas = tag("rect", { x: 0, y: 0, width, height, fill: bound.design.backgroundColor ?? bound.design.colors.background, ...traceAttrs(options, `${bound.path}.design.background`) });
|
|
796
|
+
const item = { value: background.alt === undefined ? background.src : { src: background.src, alt: background.alt }, path: background.path };
|
|
797
|
+
const drawOptions = { ...options, imageDecorative: background.alt === undefined };
|
|
798
|
+
const picture = background.fit === "tile"
|
|
799
|
+
? renderTiledImage(item, background.box, bound, drawOptions)
|
|
800
|
+
: renderImage(item, background.box, bound, { ...drawOptions, imageFit: background.fit, imageFocus: background.focus, imagePicture: background.picture });
|
|
801
|
+
// An unresolved source draws the ordinary placeholder at the background's opacity (as 0.14 did), without recolor or overlay.
|
|
802
|
+
if (isImagePlaceholder(picture)) return [canvas, background.opacity !== undefined ? tag("g", { opacity: background.opacity }, picture) : picture].join("\n");
|
|
803
|
+
const id = `opf-s${bound.index + 1}-background`, matrix = imageRecolorMatrix(background.recolor, bound);
|
|
804
|
+
const defs = matrix ? tag("defs", {}, tag("filter", { id: `${id}-recolor`, "color-interpolation-filters": "sRGB" }, tag("feColorMatrix", { type: "matrix", values: matrix }))) : "";
|
|
805
|
+
const pixels = matrix || background.opacity !== undefined ? tag("g", { filter: matrix ? `url(#${id}-recolor)` : undefined, opacity: background.opacity === undefined ? undefined : preciseNumber(background.opacity) }, picture) : picture;
|
|
806
|
+
return [canvas, defs, pixels, background.overlay ? renderImageOverlay(background.overlay, bound, options) : ""].filter(Boolean).join("\n");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// tile: a pattern cell of the picture's intrinsic size, anchored at the canvas origin.
|
|
810
|
+
function renderTiledImage(item, box, bound, options) {
|
|
811
|
+
const { source, drawable } = resolveImageSource(item, bound, options);
|
|
812
|
+
if (!drawable) return renderImage(item, box, bound, options);
|
|
813
|
+
const size = intrinsicImageSize(source) ?? { width: Math.min(box.width, box.height) / 4, height: Math.min(box.width, box.height) / 4 };
|
|
814
|
+
const id = `opf-s${bound.index + 1}-background-tile`;
|
|
815
|
+
const cell = tag("image", { x: 0, y: 0, width: stableNumber(size.width), height: stableNumber(size.height), href: source, preserveAspectRatio: "none" });
|
|
816
|
+
return tag("defs", {}, tag("pattern", { id, x: 0, y: 0, width: stableNumber(size.width), height: stableNumber(size.height), patternUnits: "userSpaceOnUse" }, cell))
|
|
817
|
+
+ tag("rect", { x: box.x, y: box.y, width: box.width, height: box.height, fill: `url(#${id})`, ...imageAccessibility(normalizeAsset(item.value), options), ...traceAttrs(options, item.path) });
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// FA-22 image block (core ComposedItem.image): the picture in its frame (image.box) with the block's fit and focus, clipped to
|
|
821
|
+
// the frame's shape unless it is a rectangle, with recolor and opacity on the pixels only; then the line centered on the shape
|
|
822
|
+
// outline; then the overlay (the frame's shape, or an edge band). The outlines are core's imageShape() paths, so the preview
|
|
823
|
+
// draws exactly the native preset geometry, in the paint order the export writes. A placed block (image.placement) arrives
|
|
824
|
+
// with its band as the region and needs nothing else. An unresolved source keeps the ordinary placeholder without treatments.
|
|
825
|
+
function renderImageBlock(item, bound, options) {
|
|
826
|
+
const image = item.image;
|
|
827
|
+
if (!image) throw new OPFRenderError("missing-image-geometry", "Image rendering requires a coordinated core build with image block geometry (ComposedItem.image).", { path: item.path });
|
|
828
|
+
const picture = renderImage(item, image.box, bound, { ...options, imageFit: image.fit, imageFocus: image.focus, imagePicture: image.picture });
|
|
829
|
+
const trace = options.trace && image.placement ? { "data-opf-image-placement": image.placement.edge } : {};
|
|
830
|
+
if (isImagePlaceholder(picture)) return Object.keys(trace).length ? tag("g", trace, picture) : picture;
|
|
831
|
+
const id = `opf-s${bound.index + 1}-image-${item.path.replace(/[^A-Za-z0-9]+/g, "-")}`, shape = image.shape, defs = [];
|
|
832
|
+
const masked = shape && shape.preset !== "rect";
|
|
833
|
+
if (masked) defs.push(tag("clipPath", { id: `${id}-clip` }, tag("path", { d: shape.path })));
|
|
834
|
+
const matrix = imageRecolorMatrix(image.recolor, bound);
|
|
835
|
+
if (matrix) defs.push(tag("filter", { id: `${id}-recolor`, "color-interpolation-filters": "sRGB" }, tag("feColorMatrix", { type: "matrix", values: matrix })));
|
|
836
|
+
let pixels = matrix || image.opacity !== undefined ? tag("g", { filter: matrix ? `url(#${id}-recolor)` : undefined, opacity: image.opacity === undefined ? undefined : preciseNumber(image.opacity) }, picture) : picture;
|
|
837
|
+
if (masked) pixels = tag("g", { "clip-path": `url(#${id}-clip)` }, pixels);
|
|
838
|
+
const children = [defs.length ? tag("defs", {}, defs.join("")) : "", pixels];
|
|
873
839
|
if (image.border) {
|
|
874
|
-
const paint =
|
|
875
|
-
children.push(tag(
|
|
876
|
-
}
|
|
877
|
-
if (image.overlay) {
|
|
878
|
-
const paint = slideImagePaint(image.overlay.color, bound, bound.design.colors.text);
|
|
879
|
-
children.push(tag('path', { d: image.overlay.shape?.path ?? rectanglePath(image.overlay.box), fill: paint.color, 'fill-opacity': preciseNumber(paint.alpha * image.overlay.opacity), ...(options.trace ? { 'data-opf-slide-image-overlay': `${image.path}.overlay` } : {}) }));
|
|
840
|
+
const paint = imagePaint(image.border.color, bound, bound.design.colors.border);
|
|
841
|
+
children.push(tag("path", { d: shape?.path ?? rectanglePath(image.box), fill: "none", stroke: paint.color, "stroke-opacity": paint.alpha < 1 ? preciseNumber(paint.alpha) : undefined, "stroke-width": stableNumber(image.border.width), "stroke-linejoin": "miter", "stroke-miterlimit": 8 }));
|
|
880
842
|
}
|
|
881
|
-
|
|
843
|
+
if (image.overlay) children.push(renderImageOverlay(image.overlay, bound, options));
|
|
844
|
+
if (children.length === 2 && !children[0] && !Object.keys(trace).length) return pixels;
|
|
845
|
+
return tag("g", trace, children.filter(Boolean).join(""));
|
|
882
846
|
}
|
|
883
847
|
|
|
848
|
+
// One overlay (shared by image backgrounds and image blocks): its outline filled with the colour, the colour's alpha times the
|
|
849
|
+
// overlay opacity.
|
|
850
|
+
function renderImageOverlay(overlay, bound, options) {
|
|
851
|
+
const paint = imagePaint(overlay.color, bound, bound.design.colors.text);
|
|
852
|
+
return tag("path", { d: overlay.shape?.path ?? rectanglePath(overlay.box), fill: paint.color, "fill-opacity": preciseNumber(paint.alpha * overlay.opacity), ...(options.trace ? { "data-opf-image-overlay": overlay.path } : {}) });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// The placeholder renderImage draws for a source it cannot resolve.
|
|
856
|
+
const isImagePlaceholder = svg => svg.startsWith("<g") && svg.includes('data-opf-asset-status="unresolved"');
|
|
857
|
+
|
|
884
858
|
// Native alpha and color-matrix values keep 1/100000 precision; three decimals would drift.
|
|
885
859
|
const preciseNumber = value => String(Math.round(value * 1e6) / 1e6);
|
|
886
860
|
|
|
@@ -889,18 +863,18 @@ function rectanglePath(box) {
|
|
|
889
863
|
}
|
|
890
864
|
|
|
891
865
|
// ColorRef -> opaque #RRGGBB plus the AA byte as alpha, as the native srgbClr + alpha.
|
|
892
|
-
function
|
|
866
|
+
function imagePaint(value, bound, fallback) {
|
|
893
867
|
const hex = resolveColorRef(value, bound, fallback) ?? fallback;
|
|
894
868
|
const raw = normalizeColor(hex, fallback).slice(1);
|
|
895
869
|
return { color: `#${raw.slice(0, 6).toUpperCase()}`, alpha: raw.length === 8 ? parseInt(raw.slice(6), 16) / 255 : 1 };
|
|
896
870
|
}
|
|
897
871
|
|
|
898
872
|
// Rec. 601 luminance on sRGB values; duotone maps it linearly from dark to light.
|
|
899
|
-
function
|
|
873
|
+
function imageRecolorMatrix(recolor, bound) {
|
|
900
874
|
if (!recolor) return undefined;
|
|
901
875
|
const weights = [0.299, 0.587, 0.114];
|
|
902
876
|
const channels = recolor.type === 'duotone'
|
|
903
|
-
? [
|
|
877
|
+
? [imagePaint(recolor.dark, bound, '#000000'), imagePaint(recolor.light, bound, '#FFFFFF')].map(paint => [1, 3, 5].map(at => parseInt(paint.color.slice(at, at + 2), 16) / 255))
|
|
904
878
|
: [[0, 0, 0], [1, 1, 1]];
|
|
905
879
|
const [dark, light] = channels;
|
|
906
880
|
const rows = [0, 1, 2].map(channel => [...weights.map(weight => preciseNumber((light[channel] - dark[channel]) * weight)), 0, preciseNumber(dark[channel])].join(' '));
|
|
@@ -942,7 +916,7 @@ function renderPayload(item, box, bound, options) {
|
|
|
942
916
|
case "table":
|
|
943
917
|
return renderTable(item, box, bound, options);
|
|
944
918
|
case "image":
|
|
945
|
-
return
|
|
919
|
+
return renderImageBlock(item, bound, options);
|
|
946
920
|
case "video":
|
|
947
921
|
return renderMedia(item, box, bound, options);
|
|
948
922
|
case "code":
|
|
@@ -1025,6 +999,13 @@ function resolveImageSource(item, bound, options) {
|
|
|
1025
999
|
return { asset, source: svg ?? source, drawable, missingReference };
|
|
1026
1000
|
}
|
|
1027
1001
|
|
|
1002
|
+
// The accessible name of a drawn picture: its alt text, else the caller's label; a decorative picture (an image background
|
|
1003
|
+
// without alt) is hidden from assistive technology.
|
|
1004
|
+
function imageAccessibility(asset, options) {
|
|
1005
|
+
if (options.imageDecorative) return { "aria-hidden": "true" };
|
|
1006
|
+
return { role: "img", "aria-label": asset.alt ?? options.imageLabel ?? "Image" };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1028
1009
|
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
1029
1010
|
|
|
1030
1011
|
// The base64 data URI of a drawable SVG, else null. Drawable: a data URI whose type is image/svg+xml, whose text is
|
|
@@ -1171,12 +1152,29 @@ function resolveBulletImage(bulletImage, bound, options) {
|
|
|
1171
1152
|
return undefined;
|
|
1172
1153
|
}
|
|
1173
1154
|
|
|
1155
|
+
// A drawable picture in `box` with `options.imageFit`: cover (the default for content), contain or stretch. cover takes
|
|
1156
|
+
// `options.imageFocus`: centered, it is the native center crop; anywhere else the picture is placed by core's fit math
|
|
1157
|
+
// (`options.imagePicture`, core's ImageFitPlacement when core read the aspect, else fitImage on core's intrinsicImageAspect
|
|
1158
|
+
// of the resolved source, for pictures a host imageResolver supplies) and
|
|
1159
|
+
// clipped to the box by a nested viewport. A logo keeps its anchor (`options.imageAnchor`).
|
|
1174
1160
|
function renderImage(item, box, bound, options) {
|
|
1175
1161
|
const { asset, source, drawable, missingReference } = resolveImageSource(item, bound, options);
|
|
1176
1162
|
if (drawable) {
|
|
1163
|
+
const fit = options.imageFit ?? "contain", focus = options.imageFocus;
|
|
1164
|
+
const attrs = { href: source, ...imageAccessibility(asset, options), ...traceAttrs(options, item.path), ...generatedAttrs(options) };
|
|
1165
|
+
if (fit === "cover" && focus && !options.imageAnchor && (focus.x !== 0.5 || focus.y !== 0.5)) {
|
|
1166
|
+
// The drawable source (after asset references and the host imageResolver): core reads its size (intrinsicImageAspect).
|
|
1167
|
+
const aspect = options.imagePicture ? undefined : intrinsicImageAspect(source);
|
|
1168
|
+
const placement = options.imagePicture ?? (aspect ? fitImage(box, "cover", aspect, focus) : undefined);
|
|
1169
|
+
if (placement) {
|
|
1170
|
+
const picture = placement.image;
|
|
1171
|
+
return tag("svg", { x: box.x, y: box.y, width: box.width, height: box.height, overflow: "hidden" },
|
|
1172
|
+
tag("image", { x: stableNumber(picture.x - box.x), y: stableNumber(picture.y - box.y), width: stableNumber(picture.width), height: stableNumber(picture.height), preserveAspectRatio: "none", ...attrs }));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1177
1175
|
return tag("image", { x: box.x, y: box.y, width: box.width, height: box.height,
|
|
1178
|
-
|
|
1179
|
-
...
|
|
1176
|
+
preserveAspectRatio: options.imageAnchor === "left" ? "xMinYMid meet" : options.imageAnchor === "right" ? "xMaxYMid meet" : fit === "cover" ? "xMidYMid slice" : fit === "stretch" ? "none" : "xMidYMid meet",
|
|
1177
|
+
...attrs });
|
|
1180
1178
|
}
|
|
1181
1179
|
const reason=missingReference?'missing-reference':!asset.src?'missing-source':'unsupported-source';
|
|
1182
1180
|
const message=missingReference?`Image asset ${missingReference} is missing; supply the referenced asset or resolve it in the host.`:'Image requires an embedded raster or SVG data URI, or a host imageResolver.';
|
|
@@ -1398,7 +1396,7 @@ function renderQuote(item, box, bound, options) {
|
|
|
1398
1396
|
function renderQuotePhoto(photo, item, bound, options) {
|
|
1399
1397
|
const picture = renderImage({ value: photo.value, path: photo.path }, photo.box, bound, { ...options, imageFit: 'cover' });
|
|
1400
1398
|
// An unresolved source keeps the ordinary placeholder, unmasked, like the export.
|
|
1401
|
-
if (
|
|
1399
|
+
if (isImagePlaceholder(picture)) return picture;
|
|
1402
1400
|
const id = `opf-s${bound.index + 1}-quote-photo-${item.path.replace(/[^A-Za-z0-9]+/g, '-')}`;
|
|
1403
1401
|
return tag('g', {}, tag('defs', {}, tag('clipPath', { id }, tag('path', { d: photo.shape.path }))) + tag('g', { 'clip-path': `url(#${id})` }, picture));
|
|
1404
1402
|
}
|
|
@@ -1516,7 +1514,7 @@ function renderChart(item, box, bound, options) {
|
|
|
1516
1514
|
const rendered = renderCatalogChart(item, box, bound, options, { tag, traceAttrs, stableNumber, renderTextBox, reportDiagnostic });
|
|
1517
1515
|
if (rendered) return rendered;
|
|
1518
1516
|
const chart = item.value ?? {};
|
|
1519
|
-
const chartType = chart.type ?? engineDefaults.
|
|
1517
|
+
const chartType = chart.type ?? engineDefaults.chartType;
|
|
1520
1518
|
const data = inlineChartRows(legacyChartData(chart, bound));
|
|
1521
1519
|
// A dataset-backed chart has no data.rows of its own: its parts report the chart's authored path.
|
|
1522
1520
|
const part = isDatasetChart(item, bound) ? () => item.path : (path) => path;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"@expo-google-fonts/roboto": "0.4.3",
|
|
139
139
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
140
140
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
141
|
-
"@openpresentation/opf": "^0.
|
|
141
|
+
"@openpresentation/opf": "^0.15.1",
|
|
142
142
|
"@resvg/resvg-js": "^2.6.2",
|
|
143
143
|
"bidi-js": "1.1.0",
|
|
144
144
|
"fontkit": "^2.0.4",
|