@openpresentation/opf-render 0.14.0 → 0.16.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 +145 -22
- package/dist/annotations.js +1 -1
- package/dist/charts.js +8 -12
- package/dist/converters.js +41 -0
- package/dist/deck-runtime.js +6 -3
- package/dist/element.d.ts +6 -0
- package/dist/element.js +31 -8
- package/dist/export-browser.d.ts +6 -0
- package/dist/export-browser.js +16 -2
- package/dist/font-registry.js +3 -3
- package/dist/fonts-node.js +24 -5
- package/dist/fonts.d.ts +3 -3
- package/dist/index.d.ts +21 -34
- package/dist/pdf-vector.js +2 -0
- package/dist/pdf.d.ts +2 -0
- package/dist/pdf.js +4 -0
- package/dist/player.d.ts +2 -1
- package/dist/png.d.ts +2 -0
- package/dist/png.js +4 -0
- package/dist/preview-fonts-node.js +2 -2
- package/dist/raster-images.js +3 -1
- package/dist/raster.js +4 -20
- package/dist/script-font-pack.js +9 -8
- package/dist/svg.d.ts +21 -34
- package/dist/svg.js +215 -179
- package/package.json +70 -17
package/README.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
10
|
+
**Lighter install (RR-63, unreleased, 0.16.0).** `pdf-lib`, `@resvg/resvg-js`, `sharp` and every `@expo-google-fonts/*` font package are optional peer dependencies, loaded on first use: an SVG-only install holds none of them. Add what you export (see [Install](#install-what-to-add-for-what-rr-63)); a missing converter rejects with `converter-missing`. New `/png` and `/pdf` entries beside `/svg`. Rendered output is unchanged.
|
|
11
|
+
|
|
3
12
|
**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
13
|
|
|
5
14
|
- `renderSvg(deck, options)` returns `string[]`, one SVG per slide; `renderSlideSvg(deck, index, options)` returns the SVG of one slide.
|
|
@@ -23,13 +32,13 @@ const pdf = await svgToPdf(slides, { fonts });
|
|
|
23
32
|
const pptx = await toPptx(presentation, { fonts });
|
|
24
33
|
```
|
|
25
34
|
|
|
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
|
|
35
|
+
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
36
|
|
|
28
37
|
An option a function does not know is not read: a top-level `textMeasurement` or `embeddedFonts` is ignored, so pass `{ fonts }`.
|
|
29
38
|
|
|
30
39
|
**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
40
|
|
|
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,
|
|
41
|
+
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
42
|
|
|
34
43
|
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
44
|
|
|
@@ -43,7 +52,7 @@ Version 0.8.0 adds optional font-registry vector outlines and shared heading/sca
|
|
|
43
52
|
|
|
44
53
|
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
54
|
|
|
46
|
-
Deterministic local renderer for Open Presentation Format documents. The shared SVG core implements validation,
|
|
55
|
+
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
56
|
|
|
48
57
|
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
58
|
|
|
@@ -77,16 +86,75 @@ Version 0.8.0 resolves OpenType preferred-family groups, so a Roboto request at
|
|
|
77
86
|
|
|
78
87
|
Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets) reject rendering with `invalid-code-text`, the OPF field path and UTF-16 offset. Input JSON stays unchanged. Tabs, line endings and valid supplementary Unicode remain accepted; schema validity and XML serialization do not certify font coverage or native fidelity.
|
|
79
88
|
|
|
89
|
+
## Install: what to add for what (RR-63)
|
|
90
|
+
|
|
91
|
+
`npm install @openpresentation/opf-render` brings the SVG renderer, layout and text measurement (`@openpresentation/opf`, `fontkit`, `bidi-js`, `pako`) and the vendored font files under `fonts/`. The heavy pieces are **optional peer dependencies**, so you install only what your output needs. npm 7+ and pnpm do not install optional peers; add them yourself:
|
|
92
|
+
|
|
93
|
+
| You want | Import | Also install |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| SVG in a browser (the host serves its own font files) | `@openpresentation/opf-render/svg` (+ `/fonts-browser`) | nothing |
|
|
96
|
+
| SVG in Node, no fonts handle (estimated widths) | `@openpresentation/opf-render/svg` | nothing |
|
|
97
|
+
| SVG in Node with exact text measurement | `/svg` and `loadFonts` from `/fonts-node` | the font packages of the pack (below) |
|
|
98
|
+
| PNG in Node | `/png` (or the root) | `npm install @resvg/resvg-js` (`sharp` too for WebP and rotated JPEG pictures) and the fonts of the pack |
|
|
99
|
+
| PDF in Node (vector, the default) | `/pdf` (or the root) | the fonts of the pack; `sharp` for pictures; `@resvg/resvg-js` for the rare element drawn as an image (a filter, a mask) |
|
|
100
|
+
| PDF in Node, `mode: "raster"` | `/pdf` (or the root) | `pdf-lib`, `@resvg/resvg-js`, `sharp` and the fonts of the pack |
|
|
101
|
+
| PNG and PDF in a browser | `/export-browser` | nothing (`mode: "raster"` needs `pdf-lib`, see below) |
|
|
102
|
+
| Slideshow, `<opf-deck>`, server markup | `/player`, `/element`, `/element/define` | nothing |
|
|
103
|
+
|
|
104
|
+
The font packages are pinned exactly (`@expo-google-fonts/*`, each a pinned optional peer; what each pack holds is described under `pack` above and in [Script fonts](#script-fonts-lang-and-right-to-left-text-ff-19)). `loadFonts()` (pack `base`) needs `@expo-google-fonts/roboto@0.4.3` and `roboto-mono@0.4.2`; `loadFonts({ pack: 'office' })` also needs `arimo`, `caladea`, `cousine`, `gelasio`, `tinos` and `noto-sans`. A script face (`scripts: ['Jpan']`, `scripts: 'auto'`) needs its own `@expo-google-fonts/noto-*` package. The `fonts/` directory (the vendored Carlito, Intos and open families, about 28 MB) ships inside the package.
|
|
105
|
+
|
|
106
|
+
Converter ranges: `@resvg/resvg-js@^2.6.2`, `sharp@^0.35.5`, `pdf-lib@^1.17.1`.
|
|
107
|
+
|
|
108
|
+
### The format entries
|
|
109
|
+
|
|
110
|
+
| Entry | What it is | Runs in |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| `@openpresentation/opf-render/svg` | `renderSvg`, `renderSlideSvg`, `resolvePresentation`, `OPFRenderError` and the types. No converter, no Node module. | browser and Node |
|
|
113
|
+
| `@openpresentation/opf-render/png` | `svgToPng` | Node |
|
|
114
|
+
| `@openpresentation/opf-render/pdf` | `svgToPdf` | Node |
|
|
115
|
+
| `@openpresentation/opf-render/export-browser` | `svgToPng` and `svgToPdf` for a page (canvas) | browser |
|
|
116
|
+
| `@openpresentation/opf-render/element`, `/element/define`, `/player` | `renderDeckHtml`, `<opf-deck>`, `present` (the HTML output) | browser (`renderDeckHtml` also on a server) |
|
|
117
|
+
| `@openpresentation/opf-render` | all of `/svg`, plus `svgToPng` and `svgToPdf` | Node (a browser bundler picks `/svg`) |
|
|
118
|
+
|
|
119
|
+
There is no `/html` entry: it would only repeat `/element` (`renderDeckHtml`, `defineOpfDeck`) and `/player` (`present`) under a second name. A bundler that bundles Node code (a server framework) sees the converters' dynamic `import()` in the root and in `/png` and `/pdf`; an app that only draws SVG should import `/svg`, which has none, and an app that exports should list the converters as installed (or external) in its bundler.
|
|
120
|
+
|
|
121
|
+
### A missing converter or font package
|
|
122
|
+
|
|
123
|
+
An export that needs a converter that is not installed rejects with `OPFRenderError` code `converter-missing`. `details` holds `package`, `range`, `install` (the command), `purpose` and `installed` (`false` when the package is absent; `true` when it is installed but cannot load, for example a native binary missing for the platform):
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
try { await svgToPng(svg); }
|
|
127
|
+
catch (error) {
|
|
128
|
+
// error.code === 'converter-missing'
|
|
129
|
+
// error.message: '@resvg/resvg-js is not installed. It is an optional peer dependency of @openpresentation/opf-render, used for PNG output, ...: run `npm install @resvg/resvg-js@^2.6.2`.'
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`svgToPng` needs `@resvg/resvg-js`; a WebP or rotated JPEG picture in it also needs `sharp`. `svgToPdf` (vector) needs no converter for text and shapes, `sharp` for pictures and `@resvg/resvg-js` for an element it has to draw as an image (reported as `pdf-raster-fallback`); `mode: "raster"` needs `pdf-lib` and `@resvg/resvg-js`. A picture is never silently dropped from a PDF because `sharp` is absent: the export rejects instead.
|
|
134
|
+
|
|
135
|
+
A font pack whose `@expo-google-fonts/*` packages are not all installed rejects `loadFonts` with `OPFFontError` code `font-resource-unavailable`; `details.packages` and the message name every missing package with the `npm install` command. `scripts: 'auto'` still reports a missing script package as the `script-font-not-installed` diagnostic instead of failing. In a browser, `export-browser` imports no converter: `mode: "raster"` PDF takes the module as an option (`svgToPdf(svgs, { mode: 'raster', pdfLib })` with `import * as pdfLib from 'pdf-lib'`), so a bundle of the entry holds no PDF library; without it raster mode rejects with `converter-missing`.
|
|
136
|
+
|
|
137
|
+
Installed size, measured on Windows with `npm install --ignore-scripts`, unpacked. sharp brings its 19 MiB libvips build on each platform; of the SVG-only install, 28 MiB is the vendored font files under `fonts/`, 13 MiB core and 6 MiB fontkit:
|
|
138
|
+
|
|
139
|
+
| Install | 0.15.0 | with RR-63 |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| SVG only, in a browser (`/svg` + `/fonts-browser`) | 135 MiB | **56 MiB** |
|
|
142
|
+
| Node SVG and PNG (base pack) | 135 MiB | **89 MiB** |
|
|
143
|
+
| Everything (office pack, all converters) | 135 MiB | 135 MiB |
|
|
144
|
+
|
|
80
145
|
## Scope
|
|
81
146
|
|
|
82
147
|
- Package: `@openpresentation/opf-render`
|
|
83
148
|
- Repository: `OpenPresentation/opf-render`
|
|
84
149
|
- License: MIT
|
|
85
150
|
- Compatibility target: `@openpresentation/opf`
|
|
151
|
+
- Entry points: root, `/svg`, `/png`, `/pdf`, `/export-browser`, `/fonts-node`, `/fonts-browser`, `/fonts`, `/player`, `/element`, `/element/define`, `/preview-fonts`, `/preview-fonts-node` (see [Install](#install-what-to-add-for-what-rr-63))
|
|
86
152
|
- 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
153
|
- 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
154
|
|
|
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
|
|
155
|
+
`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. With a `fonts` handle, each SVG embeds as `@font-face` data only the faces of `embeddedFonts` that its own text draws (by family, weight and style; a face flagged `embed: "always"` goes into every SVG), so the handle is safe to pass to every preview: over the 127 core example decks, an office-pack slide carries 2.4 MB on average (4.1 MB at most) instead of every eager face (15 MB), a base-pack slide 0.53 MB instead of 1.76 MB. Faces are embedded whole, not as glyph subsets. `embedFonts: false` writes no `@font-face` data at all (the handle still measures), for a browser host whose `loadFonts` handle already added the faces to the page; keep the default where an SVG must stand alone (a file, the browser PDF export). 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`.
|
|
156
|
+
|
|
157
|
+
Accessibility of the SVG (FA-30): the slide root is a labelled container, `role="group"` with `aria-roledescription="slide"` and `aria-label` set to the slide title, never `role="img"`, which would make every text node inside presentational. Titles, body text, list entries, quotes, metrics, table cells, captions, footnotes and header or footer text stay live text a screen reader reaches, in the order the renderer paints them. Purely decorative drawing (the background and its overlay, card frames, table cell fills and borders, dividers, timeline markers, watermarks, and an image whose `alt` is `""`) carries `aria-hidden="true"`. A picture with `alt` is `role="img"` with that label (a header or footer picture without `alt` is hidden); a chart with `alt` is a `role="img"` group, and one with `alt: ""` is hidden. `<opf-deck>`, the player and `renderDeckHtml` remove the root's `role`, `aria-roledescription` and `aria-label` (the slide section or figure around the drawing carries the one name), so a slide is announced once.
|
|
90
158
|
|
|
91
159
|
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
160
|
|
|
@@ -105,19 +173,19 @@ The installed open font pack works offline. The Aptos family previews with the m
|
|
|
105
173
|
|
|
106
174
|
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
175
|
|
|
108
|
-
Header/footer images and watermarks fit their complete artwork within their allocated regions.
|
|
176
|
+
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
177
|
|
|
110
|
-
An `image/svg+xml` data URI (base64 or text) is drawn as an image wherever a raster is (
|
|
178
|
+
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
179
|
|
|
112
|
-
|
|
180
|
+
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
181
|
|
|
114
|
-
|
|
182
|
+
Image blocks (core `item.image`, FA-22) draw from core's normalized geometry in the native picture's paint order:
|
|
115
183
|
|
|
116
|
-
1. The image, clipped by the preset outline
|
|
184
|
+
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
185
|
2. The centered border stroke on the same outline.
|
|
118
|
-
3. The overlay
|
|
186
|
+
3. The overlay, over the frame's shape or an edge band (`data-opf-image-overlay` names its path with `trace: true`).
|
|
119
187
|
|
|
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.
|
|
188
|
+
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
189
|
|
|
122
190
|
PNG and PDF conversion APIs are async because they load the local raster/PDF engines on demand:
|
|
123
191
|
|
|
@@ -162,7 +230,7 @@ The default changed from raster to vector in the release that added this section
|
|
|
162
230
|
|
|
163
231
|
`@openpresentation/opf-render/export-browser` exports `svgToPdf(svgs, options)` and `svgToPng(svg, options)` for a page. They take the SVG the preview draws and need no Node module, no network and no system font (a bundle of this entry contains no sharp, resvg, fs or crypto, and the vector converter's SHA-256 is plain JavaScript).
|
|
164
232
|
|
|
165
|
-
- **Vector PDF** (default) is the same converter the Node export uses (selectable real text in embedded font subsets, paths, gradients, patterns, clips, opacity, links, tagged structure, byte-identical output for the same SVG). Fonts come from the SVG's own `@font-face` data (pass a `fonts` handle with `embeddedFonts` to `renderSvg`;
|
|
233
|
+
- **Vector PDF** (default) is the same converter the Node export uses (selectable real text in embedded font subsets, paths, gradients, patterns, clips, opacity, links, tagged structure, byte-identical output for the same SVG). Fonts come from the SVG's own `@font-face` data (pass a `fonts` handle with `embeddedFonts` to `renderSvg`; each face is embedded only on the slides that draw it, so the PDF gets every face its pages draw), from the faces of the `fonts` handle you pass to `svgToPdf` (script faces an SVG does not carry reach the PDF too) or from `fontData: [{ data, family? }]`; `fontFiles` and `fontDirs` do not exist here and system fonts cannot be loaded. A face the handle never loaded is never embedded.
|
|
166
234
|
- **Pictures** are decoded by the browser (PNG, JPEG, WebP, GIF through `createImageBitmap`, EXIF orientation applied); an upright JPEG is passed through compressed, as in Node. An effect with no PDF form (filter, mask, a nested SVG picture) rasterizes only that element on a canvas and reports `pdf-raster-fallback`.
|
|
167
235
|
- **Raster PDF** (`mode: "raster"`, `scale` default 2) draws each slide on a canvas as an image.
|
|
168
236
|
- **PNG** is drawn by the browser's SVG renderer on a canvas (`scale`, `background`, up to 40 megapixels). With the fonts embedded in the SVG it agrees with the Node (resvg) PNG to anti-aliasing (mean channel error 0.43 of 255 on a text slide).
|
|
@@ -184,7 +252,7 @@ container.innerHTML = renderSlideSvg(presentation, 0, { fonts });
|
|
|
184
252
|
// fonts.dispose() when its canvases are unmounted.
|
|
185
253
|
```
|
|
186
254
|
|
|
187
|
-
Each entry of `faces` contains `url` or `data: Uint8Array`, with optional `family`, `weight`, `italic` and `license`. The loader registers browser FontFaces using the same bytes used for measurement. It fetches only URLs supplied by the host, supports an AbortSignal and custom fetch, and awaits font loading. Use pinned static faces and retain their licenses. A standalone SVG embeds the handle's `embeddedFonts` (the same `{ fonts }` option); embedding is unnecessary for each live draft after browser fonts are loaded.
|
|
255
|
+
Each entry of `faces` contains `url` or `data: Uint8Array`, with optional `family`, `weight`, `italic` and `license`. The loader registers browser FontFaces using the same bytes used for measurement. It fetches only URLs supplied by the host, supports an AbortSignal and custom fetch, and awaits font loading. Use pinned static faces and retain their licenses. A standalone SVG embeds the faces of the handle's `embeddedFonts` that its text draws (the same `{ fonts }` option); embedding is unnecessary for each live draft after browser fonts are loaded.
|
|
188
256
|
|
|
189
257
|
## Player and `<opf-deck>` (RR-28)
|
|
190
258
|
|
|
@@ -197,6 +265,8 @@ A slideshow player and an embeddable web component, both built on `renderSlideSv
|
|
|
197
265
|
<opf-deck src="/deck.opf.json" fonts="/opf-fonts/" thumbnails present></opf-deck>
|
|
198
266
|
```
|
|
199
267
|
|
|
268
|
+
This HTML must be processed by a bundler (for example Vite); ordinary static HTML cannot resolve a bare npm import. Serve your own `/deck.opf.json` and the font root copied below.
|
|
269
|
+
|
|
200
270
|
```js
|
|
201
271
|
// A framework page: register on the client, when you choose.
|
|
202
272
|
import { defineOpfDeck } from '@openpresentation/opf-render/element';
|
|
@@ -232,18 +302,71 @@ Events: `ready`, `slidechange` (the same detail as `currentSlide`; bubbles and i
|
|
|
232
302
|
|
|
233
303
|
Hidden slides (`hidden: true`) are skipped everywhere, so the counter, the `slide` attribute and the player's number-then-Enter all count the sequence that plays; `include-hidden` plays them all. Navigation: the buttons, ArrowLeft, ArrowRight, Page Up, Page Down, Home and End while the slide has focus (Up, Down and Space are left to the page), a horizontal swipe on touch, and the thumbnails. A deck whose `language` is right to left (and a page that sets `dir` or CSS `direction`) turns the controls and the Left and Right keys around.
|
|
234
304
|
|
|
235
|
-
Accessibility: the element is a labelled region (the deck name) holding a focusable group named `Slide 3 of 8: Revenue grew`; slide text is live SVG text in the order the renderer paints it (title,
|
|
305
|
+
Accessibility: the element is a labelled region (the deck name) holding a focusable group named `Slide 3 of 8: Revenue grew`; slide text is live SVG text in the order the renderer paints it (tag, title, content, footnotes, then header and footer), not an image, so a screen reader reads it; slide changes made by keyboard or thumbnail are announced through a polite live region; thumbnails are a list of buttons (one tab stop, arrow keys inside, `aria-current`) whose drawings are hidden from assistive technology; the buttons keep their focus ring and `forced-colors` support; and animation is limited to a hover colour that `prefers-reduced-motion: reduce` removes. The test suite runs axe-core (WCAG 2.0 to 2.2 A and AA plus best practice) over the element, the player and the speaker view with no violations. There are no transitions or builds (deferred, opf#250).
|
|
236
306
|
|
|
237
307
|
### Server markup
|
|
238
308
|
|
|
239
309
|
`renderDeckHtml(deck, options)` returns the tag with the deck's slides as inline SVG inside it. A visitor without JavaScript, a crawler or a reader sees the slides; when the element upgrades its shadow DOM replaces them. `slides: 'first'` (default: the first slide and a list of titles), `'all'` or slide numbers; `embed: true` adds the document as an `application/opf+json` child so the upgrade needs no request (it includes hidden slides and notes, as the `src` file does); `fonts` (an object) takes a handle, or any object with a `textMeasurement`, for exact widths; a string is the font root URL for the `fonts` attribute. In Next.js, render the string from a server component with `dangerouslySetInnerHTML` and call `defineOpfDeck()` from a client component. A strict `style-src` Content-Security-Policy needs `style-src-attr 'unsafe-inline'` for the renderer's `style="white-space:pre"` attributes; the element's own styles use constructable stylesheets.
|
|
240
310
|
|
|
311
|
+
Each slide's SVG embeds only the resolved faces it draws (see `renderSvg` above), including bold, italic and glyph fallback, so loading a whole pack for measurement does not put that pack into every slide. `renderDeckHtml` accepts `fontMode: 'standalone' | 'shared' | 'external'`:
|
|
312
|
+
|
|
313
|
+
- `standalone` (default) embeds fonts in each slide, so its SVG remains self-contained when extracted.
|
|
314
|
+
- `shared` emits the drawn font rules once inside the returned tag, retaining license metadata in the SVGs. Keep the returned markup together; an extracted SVG needs those shared rules to travel with it.
|
|
315
|
+
- `external` emits no font rules (the slides render with `embedFonts: false`). Supply page CSS with the exact pinned, licensed faces (including replacements and fallback faces) used by the handle, from your own origin. Keep the same handle for layout; omitting it changes layout to estimates. Copy license notices with the served files. An offline page needs those local files available too.
|
|
316
|
+
|
|
317
|
+
Shared and external rules apply at page scope. A strict CSP must permit their inline style or self-hosted stylesheet and the matching `font-src` (`data:` for embedded fonts). These modes change delivery, while measurement, text positions and font selection stay the same. The Node loader's large script faces still require `embedScriptFonts: true` for self-contained SVG or shared SSR; otherwise supply them through external CSS.
|
|
318
|
+
|
|
319
|
+
For a lightweight initial page, pre-render measured markup on the server and enable controls on demand. Here the server owns the fonts and the client owns the upgrade button:
|
|
320
|
+
|
|
321
|
+
```js
|
|
322
|
+
// Server/build step: write this HTML into your page, followed by the upgrade button.
|
|
323
|
+
import {loadFonts} from '@openpresentation/opf-render/fonts-node';
|
|
324
|
+
import {renderDeckHtml} from '@openpresentation/opf-render/element';
|
|
325
|
+
const fonts = await loadFonts({pack: 'office', scripts: 'auto', presentation: deck, embedScriptFonts: true});
|
|
326
|
+
const html = renderDeckHtml(deck, {
|
|
327
|
+
fonts, slides: 'all', fontMode: 'shared', embed: true,
|
|
328
|
+
attributes: {id: 'deck', fonts: '/opf-fonts/'},
|
|
329
|
+
});
|
|
330
|
+
// <button id="upgrade" type="button">Enable slide controls</button>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
// Bundler-processed client entry; do not also import /element/define at startup.
|
|
335
|
+
document.querySelector('#upgrade').addEventListener('click', async event => {
|
|
336
|
+
const button = event.currentTarget;
|
|
337
|
+
button.disabled = true;
|
|
338
|
+
try {
|
|
339
|
+
const {defineOpfDeck} = await import('@openpresentation/opf-render/element');
|
|
340
|
+
defineOpfDeck();
|
|
341
|
+
await document.querySelector('#deck').ready;
|
|
342
|
+
button.hidden = true;
|
|
343
|
+
} finally { button.disabled = false; }
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Copy the self-hosted font root as above, including any script packs the deck draws. No JavaScript is needed to read the pre-rendered slides; the upgrade loads the strict byte-matched measurement engine and fonts when requested. A page that only needs readable slides can omit the client entry and button entirely. A live element without a font handle/root uses estimated widths and system fonts; that is a different fidelity contract.
|
|
348
|
+
|
|
349
|
+
The reproducible production fixture (`npm run build` then `node scripts/measure-web-delivery.mjs`) measures minified split ESM over local HTTP gzip, with actual font requests counted separately. On 2026-10-08 (Node 24, Playwright Chromium, OPF 0.15), its three-slide Roboto deck measured:
|
|
350
|
+
|
|
351
|
+
| Delivery | HTML gzip | JavaScript gzip | External font requests |
|
|
352
|
+
|---|---:|---:|---|
|
|
353
|
+
| Standalone SSR, no scripts | 686,617 B | 0 B | None; fonts are in HTML |
|
|
354
|
+
| Shared SSR, no scripts | 229,260 B | 0 B | None; fonts are in HTML |
|
|
355
|
+
| External SSR, no scripts | 1,021 B | 0 B | Two faces, 191,628 B gzip, plus host CSS |
|
|
356
|
+
| Shared SSR, immediate upgrade | 229,286 B | 500,021 B | Two faces, 191,628 B gzip |
|
|
357
|
+
| Shared SSR, before deferred upgrade | 229,322 B | 199 B | None |
|
|
358
|
+
| Shared SSR, after deferred upgrade | 229,322 B | 500,355 B total | Two faces, 191,628 B gzip |
|
|
359
|
+
|
|
360
|
+
The base and Office pack standalone SVGs both contain two Roboto rules (430,764 B / 228,481 B gzip). All three SSR modes render identical pixels with JavaScript disabled, an extracted standalone SVG paints the same offline, and strict interactive upgrades preserve those pixels. These numbers describe this local candidate and fixture, vary with the deck and bundler, and are recorded in [the production delivery report](docs/evidence/rr-59-web-delivery.json). The measurement-enabled JavaScript cost remains about 500 KB gzip. Copying the approximately 34 MiB font root does not make the browser fetch it all: this fixture fetches only Roboto Regular and Bold; other documents fetch their own required faces.
|
|
361
|
+
|
|
241
362
|
### The slideshow
|
|
242
363
|
|
|
243
364
|
`present(source, options)` takes an OPF document, its URL or an `<opf-deck>` element and covers the page with a full-screen player (call it from a click or key handler; full screen and the speaker view need a user gesture). It resolves with a `PlayerSession` (`slide`, `total`, `next()`, `previous()`, `first()`, `last()`, `goto(n)`, `blank('black' | 'white' | 'none')`, `openPresenterView()`, `close()`; events `slidechange`, `blank`, `presenterview`, `close`). One show per document: a second call returns the running one.
|
|
244
365
|
|
|
245
366
|
Keys: Right, Down, Page Down, Space, Enter and `N` are next; Left, Up, Page Up, Backspace, Shift+Space and `P` are previous; Home and End; a slide number then Enter (Escape clears it); `B` and `W` toggle a black or white screen (any navigation key brings the slide back first); `S` opens the speaker view; `F` toggles full screen; Escape leaves (leaving full screen any other way ends the show too). Click or tap advances (the left third goes back) and a horizontal swipe navigates. The player is a modal dialog: the rest of the page is inert, focus stays inside it and returns to where it was when the show ends, and an element that started it follows the show and fires `slidechange`.
|
|
246
367
|
|
|
368
|
+
`element.present({returnFocus: target})` honors the explicit target; otherwise it captures the invoking element before asynchronous loading (including the Present button in its shadow root). If that target is removed, focus falls back to the deck viewport. Fetched deck failures include the source URL, status and content type in the error detail; HTML responses explain likely SPA fallback routing. A valid JSON body is accepted even if the server labels it with another content type.
|
|
369
|
+
|
|
247
370
|
The speaker view opens in a second window (`S`, the button, or `presenterView: true`): the current and next slide (the last slide says so), the speaker notes, the section, a timer (against the deck's `duration` in minutes, with pause and reset) and the clock, previous, next and blank buttons, the same keys, and a notes size control. Notes are the OPF `notes` string and are shown as plain text only (rich notes are deferred, opf#251). The windows follow each other over a `BroadcastChannel` named from the deck, so any number of windows of one deck on one origin stay in step (a logical clock settles two changes that cross), and `present(deck, { role: 'presenter' })` makes a second tab or window the speaker view. Pass `channel` to name the channel yourself or `false` for none. The audience window and the popup are driven by this page, so closing or navigating the page ends both.
|
|
248
371
|
|
|
249
372
|
Not in v1: transitions and builds, links between slides, rich notes, translated interface strings (the controls are English) and a Window Management (multi-screen) placement of the two windows.
|
|
@@ -287,7 +410,7 @@ Embedding: open faces are flagged `embed: "used"`. `registry.embeddedFonts` stay
|
|
|
287
410
|
|
|
288
411
|
Previews itemize text by Unicode script. Each run uses the OOXML script slot its script belongs to (`latin`, `eastAsian` or `complexScript`), as resolved by core `resolveScriptFonts` from the document's language and font scheme. Latin, Greek and Cyrillic text stays in the design font. The text's role picks the major (heading) or minor (body) slots: title, subtitle and tag text is heading, as in the exporter's heading shapes, and all other text is body. Licensed script fonts are never bundled. With a measured registry, a proprietary family (for example Meiryo, Microsoft YaHei, Malgun Gothic, Arabic Typesetting, David, Mangal or Angsana New) is previewed with its designated open replacement from `SCRIPT_FONT_REPLACEMENTS`, and each replacement is recorded in `registry.substitutions` as `visual`. The PPTX keeps the chosen family. If the slot's face has no glyph for a run, the run falls back by coverage to the designated OFL Noto family for its script.
|
|
289
412
|
|
|
290
|
-
The replacement faces are an optional, hash-pinned font pack: 70 static faces from 37 `@expo-google-fonts/*` packages (SIL OFL 1.1): 63 regular and bold Noto script faces from 31 packages, plus the FF-45 symbol packs (`noto-sans-symbols`, `noto-sans-symbols-2` and `noto-sans-math`, under `Zsym`: the Symbol, Wingdings and Webdings code tables draw with them), the FF-45 emoji pack (`noto-color-emoji`: Noto Color Emoji, COLRv1 and OT-SVG colour glyphs, 24.0 MiB; `noto-emoji`: the monochrome face the raster path draws) under the pseudo-script `Zsye` and the math pack (`stix-two-math`, `noto-sans-math`) under `Zmth` (Noto Sans Math serves both Zmth and Zsym). The pinned script faces total 66.9 MiB, of which 55.9 MiB is CJK; the emoji and math faces add 27.5 MiB and the symbol faces another 1.3 MiB. Installing all 37 packages takes about 361 MiB, because they also ship weights the manifest does not pin.
|
|
413
|
+
The replacement faces are an optional, hash-pinned font pack: 70 static faces from 37 `@expo-google-fonts/*` packages (SIL OFL 1.1): 63 regular and bold Noto script faces from 31 packages, plus the FF-45 symbol packs (`noto-sans-symbols`, `noto-sans-symbols-2` and `noto-sans-math`, under `Zsym`: the Symbol, Wingdings and Webdings code tables draw with them), the FF-45 emoji pack (`noto-color-emoji`: Noto Color Emoji, COLRv1 and OT-SVG colour glyphs, 24.0 MiB; `noto-emoji`: the monochrome face the raster path draws) under the pseudo-script `Zsye` and the math pack (`stix-two-math`, `noto-sans-math`) under `Zmth` (Noto Sans Math serves both Zmth and Zsym). The pinned script faces total 66.9 MiB, of which 55.9 MiB is CJK; the emoji and math faces add 27.5 MiB and the symbol faces another 1.3 MiB. Installing all 37 packages takes about 361 MiB, because they also ship weights the manifest does not pin. All 37 are exact optional peer dependencies, so the renderer install does not grow (RR-63: `@expo-google-fonts/noto-sans`, the default Latin, Cyrillic and Greek glyph-fallback face, about 14 MiB installed, was the one pinned runtime dependency and is now an optional peer like the rest; the office pack names it in its install error). It is the default glyph-fallback face: `loadFonts({ pack: 'office' })` always loads it, marked fallback-only (it serves glyph fallback and requests for Noto Sans, never stands in for another family, and is embedded in a standalone SVG only when its text draws it, like the open families (embed "used"); raster output reads it from `fontFiles`). Install only the other scripts you need, then load them. Segoe UI Emoji previews with Noto Color Emoji and Cambria Math with STIX Two Math once their packs are loaded (`scripts: 'auto'` loads them for text with emoji-presentation clusters or mathematical notation, or a scheme that names the family); emoji sequences stay one run and one glyph, browsers draw them in colour, and PNG/PDF output draws the monochrome Noto Emoji because resvg has no colour-glyph support. The emoji pack is opt-in by use: `scripts: 'auto'` (Node) and `ensureScripts` (browsers) load Noto Color Emoji (24 MiB) only for a deck with emoji-presentation text or a scheme that names Segoe UI Emoji, and it is an optional peer dependency, so it is not in the npm tarball; `scripts: 'all'` loads it too, so pass an explicit script list when memory or download size matters. The PPTX keeps the chosen family either way:
|
|
291
414
|
|
|
292
415
|
```js
|
|
293
416
|
import { loadFonts } from '@openpresentation/opf-render/fonts-node';
|
|
@@ -318,8 +441,8 @@ fonts.registry.scriptSelection; // { detected, scripts, unavailable, packages, n
|
|
|
318
441
|
const browserFonts = await loadBrowserFonts({ faces: baseEntries, substitutionPolicy: 'visual', fallbackFamily: 'Roboto', scriptBaseUrl: '/script-fonts/' });
|
|
319
442
|
if (browserFonts.pending(presentation).length) await browserFonts.ensure(presentation); // resolves { scripts, lazy, uncovered }
|
|
320
443
|
// 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.
|
|
444
|
+
// Text decides, so the analysis does not resolve layouts. Pass the render options (renderOptions: { catalogs: [defaultCatalog] }
|
|
445
|
+
// 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
446
|
```
|
|
324
447
|
|
|
325
448
|
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,14 +514,14 @@ The package runtime must stay local and deterministic:
|
|
|
391
514
|
|
|
392
515
|
Dependency policy:
|
|
393
516
|
|
|
394
|
-
- `@openpresentation/opf` is the compatibility source for schemas, validation, and
|
|
395
|
-
- `@resvg/resvg-js` is used only for local SVG rasterization in Node; it makes no network calls and does not require a browser.
|
|
396
|
-
- `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.
|
|
517
|
+
- `@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).
|
|
518
|
+
- `@resvg/resvg-js` (optional peer, RR-63) is used only for local SVG rasterization in Node; it makes no network calls and does not require a browser. `sharp` (optional peer) decodes WebP, rotated JPEG and the pictures of a vector PDF. Both load on first use; see [Install](#install-what-to-add-for-what-rr-63).
|
|
519
|
+
- `pdf-lib` (optional peer, RR-63) 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
520
|
- 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.
|
|
398
|
-
- Bundled OFL Roboto and Roboto Mono TTF files provide the default deterministic font fallback.
|
|
521
|
+
- Bundled OFL Roboto and Roboto Mono TTF files (the optional peers `@expo-google-fonts/roboto` and `roboto-mono`, which Node `loadFonts()` and the PNG/PDF default read) provide the default deterministic font fallback.
|
|
399
522
|
- `svgToPng` and `svgToPdf` disable system-font loading by default. Hosts that require branded fonts should pass a handle with explicit `fontFiles` (or `fontDirs`); `fonts: { loadSystemFonts: true }` is an opt-in escape hatch for PNG and raster-mode PDF output and can make it environment-dependent. Vector-mode PDF rejects it (`pdf-system-fonts-unsupported`): it embeds only fonts you supply, so a system font can never be embedded by accident.
|
|
400
523
|
|
|
401
|
-
Browser support boundary: `renderSvg`, `renderSlideSvg`, and `resolvePresentation` are browser-importable pure JavaScript APIs. The root `svgToPng` and `svgToPdf` are Node APIs because they depend on the Node builds of resvg and sharp. For a page, `@openpresentation/opf-render/export-browser` has the same two names (see below).
|
|
524
|
+
Browser support boundary: `renderSvg`, `renderSlideSvg`, and `resolvePresentation` are browser-importable pure JavaScript APIs. The root `svgToPng` and `svgToPdf` (also `/png` and `/pdf`) are Node APIs because they depend on the Node builds of resvg and sharp, which are optional peers loaded on first use (`converter-missing` when absent). For a page, `@openpresentation/opf-render/export-browser` has the same two names (see below).
|
|
402
525
|
|
|
403
526
|
Chartex chart previews (FF-22b) draw the constructs opf-pptx exports as Office 2016 chartex parts: `treemap` (squarified tiles of the first series, one colour per tile, category labels), `histogram` (a lone value column binned like PowerPoint with Scott's rule count and right-closed bins, or one column per category), `pareto` (columns sorted descending with the cumulative-percentage line on a 0-100% axis), `box-and-whisker` (rows grouped by category, one box per series, exclusive quartiles, whiskers within 1.5 IQR, mean markers, outlier points), `waterfall` (floating bars from the running total, increases and decreases in the first two palette colours, connector lines) and `funnel` (centred bars with value labels). `world` is an honest non-geographic preview: one tile per region shaded by value with its name and value. No geography data is shipped; PowerPoint draws the real map from Bing geodata it fetches itself, so the preview and the native map agree on labels, values and the series colour, not on shapes. Each chartex kind also accepts a lone value column (row numbers as categories; histogram and pareto bin the values). Every mark and label keeps a `data-opf-path` (bins and boxes trace to their value column).
|
|
404
527
|
|
package/dist/annotations.js
CHANGED
|
@@ -31,7 +31,7 @@ export function renderFootnotes(bound, options, draw) {
|
|
|
31
31
|
if (!footnotes) return "";
|
|
32
32
|
const untraced = { ...options, trace: false };
|
|
33
33
|
const { rule } = footnotes;
|
|
34
|
-
const line = draw.tag("line", { x1: rule.x, y1: rule.y + rule.thickness / 2, x2: rule.x + rule.width, y2: rule.y + rule.thickness / 2, stroke: bound.design.colors.border, "stroke-width": rule.thickness });
|
|
34
|
+
const line = draw.tag("line", { "aria-hidden": "true", x1: rule.x, y1: rule.y + rule.thickness / 2, x2: rule.x + rule.width, y2: rule.y + rule.thickness / 2, stroke: bound.design.colors.border, "stroke-width": rule.thickness });
|
|
35
35
|
const entries = footnotes.entries.map(entry => {
|
|
36
36
|
const config = { path: undefined, fit: entry.fit, textStyle: entry.textStyle, fontFamily: entry.textStyle?.fontFamily ?? bound.design.fonts.body, fontSize: 13, align: "left", fill: bound.design.colors.mutedText, diagnosticsHandled: true, options: untraced, rich: Array.isArray(entry.value) };
|
|
37
37
|
const body = Array.isArray(entry.value)
|
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
|
*/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// RR-63: the native and heavy converters are optional peer dependencies, loaded the first time an export needs them. A host that
|
|
2
|
+
// only draws SVG (a browser, a server that never exports) installs none of them. Each loader names its package with a literal
|
|
3
|
+
// `import()` so a Node bundler or file tracer still sees it; nothing here is reachable from the browser entries
|
|
4
|
+
// (`/svg`, `/fonts-browser`, `/export-browser`), and `scripts/build-browser-check.mjs` asserts that.
|
|
5
|
+
import { OPFRenderError } from "./svg.js";
|
|
6
|
+
|
|
7
|
+
/** What each converter is for and the range the renderer is tested with (the same ranges as `peerDependencies` in package.json). */
|
|
8
|
+
export const CONVERTERS = Object.freeze({
|
|
9
|
+
"@resvg/resvg-js": { range: "^2.6.2", purpose: "PNG output, raster-mode PDF and the raster fallback of vector PDF", load: () => import("@resvg/resvg-js") },
|
|
10
|
+
sharp: { range: "^0.35.5", purpose: "decoding WebP and rotated JPEG pictures for PNG output, and pictures in vector PDF output", load: () => import("sharp") },
|
|
11
|
+
"pdf-lib": { range: "^1.17.1", purpose: "raster-mode PDF output (`mode: \"raster\"`)", load: () => import("pdf-lib") },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
|
|
16
|
+
/** The module namespace of a converter, or an `OPFRenderError` with code `converter-missing` that names the package to install. */
|
|
17
|
+
export function loadConverter(name) {
|
|
18
|
+
const converter = CONVERTERS[name];
|
|
19
|
+
if (!converter) throw new TypeError(`Unknown converter ${name}.`);
|
|
20
|
+
if (!cache.has(name)) {
|
|
21
|
+
const pending = converter.load().catch((error) => {
|
|
22
|
+
cache.delete(name);
|
|
23
|
+
throw converterMissing(name, converter, error);
|
|
24
|
+
});
|
|
25
|
+
cache.set(name, pending);
|
|
26
|
+
}
|
|
27
|
+
return cache.get(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function converterMissing(name, converter, error) {
|
|
31
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
32
|
+
const absent = (error?.code === "ERR_MODULE_NOT_FOUND" || error?.code === "MODULE_NOT_FOUND") && (cause.includes(`'${name}'`) || cause.includes(`"${name}"`));
|
|
33
|
+
const install = `npm install ${name}@${converter.range}`;
|
|
34
|
+
return new OPFRenderError("converter-missing", absent
|
|
35
|
+
? `${name} is not installed. It is an optional peer dependency of @openpresentation/opf-render, used for ${converter.purpose}: run \`${install}\`.`
|
|
36
|
+
: `${name} is installed but could not be loaded (${cause}). It is used for ${converter.purpose}: reinstall it with \`${install}\` for this platform.`,
|
|
37
|
+
{ package: name, range: converter.range, install, purpose: converter.purpose, installed: !absent, cause });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True for the error `loadConverter` raises: callers that wrap errors let it through unchanged. */
|
|
41
|
+
export const isConverterMissing = (error) => error?.code === "converter-missing";
|
package/dist/deck-runtime.js
CHANGED
|
@@ -10,6 +10,7 @@ export class DeckError extends Error {
|
|
|
10
10
|
super(message, options);
|
|
11
11
|
this.name = "DeckError";
|
|
12
12
|
this.code = code;
|
|
13
|
+
if (options?.source) this.details = {source: options.source, status: options.status, contentType: options.contentType};
|
|
13
14
|
}
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -117,15 +118,17 @@ export function svgDimensions(svg) {
|
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
120
|
* The renderer's SVG made responsive and readable: the fixed `width` and `height` go (the viewBox scales it), and the root's
|
|
120
|
-
* `role="
|
|
121
|
-
*
|
|
121
|
+
* labelled-container attributes (`role="group"`, `aria-roledescription="slide"`, `aria-label`) go too, because the slide section or figure around it
|
|
122
|
+
* carries the one accessible name, so a slide is announced once. The slide's own text nodes reach a screen reader in document order,
|
|
123
|
+
* which is the reading order (tag, title, content, footnotes, then header and footer) the renderer paints; purely decorative drawing
|
|
124
|
+
* keeps its `aria-hidden`.
|
|
122
125
|
* `idPrefix` renames the ids the slide defines (gradients, patterns, clip paths) so a thumbnail of a slide and the slide
|
|
123
126
|
* itself never share an id in one tree.
|
|
124
127
|
*/
|
|
125
128
|
export function prepareSlideSvg(svg, { idPrefix } = {}) {
|
|
126
129
|
const match = /^<svg\b[^>]*>/.exec(svg);
|
|
127
130
|
if (!match) return svg;
|
|
128
|
-
let root = match[0].replace(/\s(?:width|height|role|aria-label)="[^"]*"/g, "");
|
|
131
|
+
let root = match[0].replace(/\s(?:width|height|role|aria-roledescription|aria-label)="[^"]*"/g, "");
|
|
129
132
|
root = root.replace(/^<svg\b/, '<svg class="opf-slide" preserveAspectRatio="xMidYMid meet" focusable="false"');
|
|
130
133
|
let rest = svg.slice(match[0].length);
|
|
131
134
|
if (idPrefix) {
|
package/dist/element.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface OpfDeckErrorDetail {
|
|
|
13
13
|
message: string;
|
|
14
14
|
/** False when the deck still shows (for example the fonts did not load and the layout is estimated). */
|
|
15
15
|
fatal: boolean;
|
|
16
|
+
/** URL, HTTP status and response content type when a fetched deck fails. */
|
|
17
|
+
source?: string;
|
|
18
|
+
status?: number;
|
|
19
|
+
contentType?: string;
|
|
16
20
|
}
|
|
17
21
|
export interface OpfDeckEventMap {
|
|
18
22
|
ready: CustomEvent<{ total: number; slide: number }>;
|
|
@@ -71,6 +75,8 @@ export interface RenderDeckHtmlOptions {
|
|
|
71
75
|
slide?: number;
|
|
72
76
|
/** A string is the font root URL written to the tag's `fonts` attribute; an object (a `loadFonts()` handle, or any `RenderFonts`) draws the markup with those fonts and writes no attribute. */
|
|
73
77
|
fonts?: string | RenderFonts;
|
|
78
|
+
/** Font delivery for server markup: standalone embeds faces per slide (default); shared embeds each rule once; external omits rules and requires matching host CSS. All use the supplied handle for measurement. */
|
|
79
|
+
fontMode?: "standalone" | "shared" | "external";
|
|
74
80
|
label?: string;
|
|
75
81
|
thumbnails?: boolean;
|
|
76
82
|
present?: boolean;
|