@openpresentation/opf-render 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,20 @@
1
1
  # OPF Render
2
2
 
3
+ **OPF 0.18 (RR-73, RR-74, unreleased; requires core 0.18).** One shape for every engine: `toX(deck, slides?, options?)`. The second argument is a slide number (one result), a selection (`"1-3"`, `"1,3-5"`, `"2-"`, `[1, 3]`; a list of results) or the options, told apart by type, and slides count from 1. Renamed and removed outright (no aliases); an old option fails with `invalid-render-options` or `invalid-conversion-option` and names its replacement:
4
+
5
+ | 0.17 | 0.18 |
6
+ | --- | --- |
7
+ | `renderSvg(deck, options)` | `toSvg(deck, options)` |
8
+ | `renderSlideSvg(deck, index, options)` (from 0) | `toSvg(deck, index + 1, options)` (from 1; `slide-out-of-range`) |
9
+ | `svgToPng(svg, options)`, `svgToPdf(svgs, options)` | `toPng` and `toPdf`: they take a deck (and draw it with `toSvg`) or SVG |
10
+ | `renderDeckHtml(deck, { slides })` | `toHtml(deck, slides?, options?)` (`"1-"` for every slide) |
11
+ | `embedFonts: false`, `textAsPaths: true` | `text: "system"`, `text: "paths"` (default `"fonts"`) |
12
+ | `mode: "raster"` | `raster: true` (vector is the default) |
13
+ | `fontDirs` | `fonts`: the handle, or font folders (a path or a list) for SVG input |
14
+
3
15
  **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
16
 
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.
17
+ - The renderer bundles, looks up and fetches no catalog. Every entry point (`toSvg`, `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 `toSvg(deck, { catalogs: [defaultCatalog] })`. `strictReferences: true` fails with `unresolved-reference` instead of falling back.
6
18
  - 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
19
  - 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
20
  - 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.
@@ -19,16 +31,16 @@
19
31
 
20
32
  ```js
21
33
  import { loadFonts } from '@openpresentation/opf-render/fonts-node';
22
- import { renderSvg, renderSlideSvg, svgToPng, svgToPdf } from '@openpresentation/opf-render';
34
+ import { toSvg, toPng, toPdf } from '@openpresentation/opf-render';
23
35
  import { paginate } from '@openpresentation/opf/pagination';
24
36
  import { toPptx } from '@openpresentation/opf-pptx';
25
37
 
26
38
  const fonts = await loadFonts({ pack: 'office', substitutionPolicy: 'visual' });
27
39
  const { presentation } = paginate(opf, { fonts });
28
- const slides = renderSvg(presentation, { fonts }); // every slide
29
- const first = renderSlideSvg(presentation, 0, { fonts }); // one slide
30
- const png = await svgToPng(slides[0], { fonts });
31
- const pdf = await svgToPdf(slides, { fonts });
40
+ const slides = toSvg(presentation, { fonts }); // every slide
41
+ const first = toSvg(presentation, 1, { fonts }); // one slide
42
+ const png = await toPng(slides[0], { fonts });
43
+ const pdf = await toPdf(slides, { fonts });
32
44
  const pptx = await toPptx(presentation, { fonts });
33
45
  ```
34
46
 
@@ -36,9 +48,9 @@ A slide's canvas, layout, theme, colour scheme and font scheme come from core's
36
48
 
37
49
  An option a function does not know is not read: a top-level `textMeasurement` or `embeddedFonts` is ignored, so pass `{ fonts }`.
38
50
 
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.
51
+ **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 `toSvg` 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 `toSvg` 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.
40
52
 
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.
53
+ 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 `toSvg` 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.
42
54
 
43
55
  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.
44
56
 
@@ -64,7 +76,7 @@ Citations, footnotes and captions (core RR-34 fields `cite`, `footnote`, `refere
64
76
 
65
77
  ```js
66
78
  import { loadFonts } from '@openpresentation/opf-render/fonts-node';
67
- import { renderSvg, svgToPng } from '@openpresentation/opf-render';
79
+ import { toSvg, toPng } from '@openpresentation/opf-render';
68
80
  import { paginate } from '@openpresentation/opf/pagination';
69
81
  import { toPptx } from '@openpresentation/opf-pptx';
70
82
 
@@ -72,8 +84,8 @@ const fonts = await loadFonts({
72
84
  pack: 'office', substitutionPolicy: 'visual',
73
85
  });
74
86
  const { presentation } = paginate(opf, { fonts });
75
- const slides = renderSvg(presentation, { fonts });
76
- const png = await svgToPng(slides[0], { fonts });
87
+ const slides = toSvg(presentation, { fonts });
88
+ const png = await toPng(slides[0], { fonts });
77
89
  const pptx = await toPptx(presentation, { fonts });
78
90
  console.log(fonts.substitutions);
79
91
  ```
@@ -97,8 +109,8 @@ Code strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets
97
109
  | SVG in Node with exact text measurement | `/svg` and `loadFonts` from `/fonts-node` | the font packages of the pack (below) |
98
110
  | 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
111
  | 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) |
112
+ | PDF in Node, `raster: true` | `/pdf` (or the root) | `pdf-lib`, `@resvg/resvg-js`, `sharp` and the fonts of the pack |
113
+ | PNG and PDF in a browser | `/export-browser` | nothing (`raster: true` needs `pdf-lib`, see below) |
102
114
  | Slideshow, `<opf-deck>`, server markup | `/player`, `/element`, `/element/define` | nothing |
103
115
 
104
116
  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.
@@ -109,30 +121,30 @@ Converter ranges: `@resvg/resvg-js@^2.6.2`, `sharp@^0.35.5`, `pdf-lib@^1.17.1`.
109
121
 
110
122
  | Entry | What it is | Runs in |
111
123
  | --- | --- | --- |
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`) |
124
+ | `@openpresentation/opf-render/svg` | `toSvg`, `resolvePresentation`, `OPFRenderError` and the types. No converter, no Node module. | browser and Node |
125
+ | `@openpresentation/opf-render/png` | `toPng` | Node |
126
+ | `@openpresentation/opf-render/pdf` | `toPdf` | Node |
127
+ | `@openpresentation/opf-render/export-browser` | `toPng` and `toPdf` for a page (canvas) | browser |
128
+ | `@openpresentation/opf-render/element`, `/element/define`, `/player` | `toHtml`, `<opf-deck>`, `present` (the HTML output) | browser (`toHtml` also on a server) |
129
+ | `@openpresentation/opf-render` | all of `/svg`, plus `toPng` and `toPdf` | Node (a browser bundler picks `/svg`) |
118
130
 
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.
131
+ There is no `/html` entry: it would only repeat `/element` (`toHtml`, `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
132
 
121
133
  ### A missing converter or font package
122
134
 
123
135
  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
136
 
125
137
  ```js
126
- try { await svgToPng(svg); }
138
+ try { await toPng(svg); }
127
139
  catch (error) {
128
140
  // error.code === 'converter-missing'
129
141
  // 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
142
  }
131
143
  ```
132
144
 
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.
145
+ `toPng` needs `@resvg/resvg-js`; a WebP or rotated JPEG picture in it also needs `sharp`. `toPdf` (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`); `raster: true` 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
146
 
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`.
147
+ 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: a raster PDF takes the module as an option (`toPdf(svgs, { raster: true, pdfLib })` with `import * as pdfLib from 'pdf-lib'`), so a bundle of the entry holds no PDF library; without it a raster PDF rejects with `converter-missing`.
136
148
 
137
149
  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
150
 
@@ -149,23 +161,23 @@ Installed size, measured on Windows with `npm install --ignore-scripts`, unpacke
149
161
  - License: MIT
150
162
  - Compatibility target: `@openpresentation/opf`
151
163
  - 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))
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`)
164
+ - Public API: `toSvg(opf, slides?, opts)` (every slide, one slide or a selection), `resolvePresentation(opf, opts)`, `toPng(opf or svg, slides?, opts)`, `toPdf(opf or svgs, slides?, opts)` and `loadFonts(opts)` (`/fonts-node`, `/fonts-browser`)
153
165
  - 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)
154
166
 
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`.
167
+ `toSvg(deck)` 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); `toSvg(deck, 3)` returns the SVG of the third slide (slides count from 1; out of range throws `slide-out-of-range`), and `toSvg(deck, "1-3")` or `toSvg(deck, [1, 3])` the selected slides, hidden or not (`invalid-slide-selection` for a malformed one). Every form checks 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. Each embedded face is also cut to the glyphs the slide draws (RR-65): the Node handle (and a browser handle given `subsetWasm`, the URL of `harfbuzzjs/dist/harfbuzz-subset.wasm` the host serves, or its bytes or compiled module) subsets each face with HarfBuzz's own subsetter (hb-subset from the pinned harfbuzzjs, MIT), keeping every layout feature, so the browser shapes and draws it exactly like the whole face (147 example slides with 384 embedded faces are pixel-identical in a browser) and the bytes are the same for the same slide. Over the 127 core example decks an office-pack slide falls from 2.4 MB to 59 KB on average (2.85 MB to 59 KB at the median, 1.95 GB to 48 MB for all 807 slides), a base-pack slide from 530 KB to 73 KB. Only a face whose license allows a modified version is cut: a bundled face (by sha256 against the pinned manifest) under a permitted license whose family and file names do not contain a Reserved Font Name of its package (a subset is a modified version, owner decision 2026-09-29) and whose OS/2 fsType allows subsetting; Carlito, Raleway, Lora, Playfair Display, PT Serif, Merriweather Sans, Source Sans 3 and every face a host supplies are embedded whole. `subsetFonts: false` embeds whole faces (for an SVG whose text will be edited outside the renderer). A face whose OS/2 fsType forbids embedding (Restricted License 0x0002, or bitmap-only 0x0200, since an SVG carries outlines) is never embedded (RR-76): the SVG names its family without the data, so a viewer without the font draws the generic family, and a `font-embedding-restricted` diagnostic lists the faces left out. `renderDeckHtml` with `fontMode: "shared"` embeds each face once for the page, cut to the characters all of its slides draw (a three-slide Roboto page: 53 KB, against 101 KB with per-slide subsets and 442 KB with whole shared faces). `text: "system"` 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
168
 
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.
169
+ 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 `toHtml` 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.
158
170
 
159
- Text as outlines (RR-64): `renderSvg(deck, { fonts, textAsPaths: true })` draws every `<text>` as glyph outlines, so the SVG needs no font and looks the same in every browser, image viewer and design tool. Each glyph is a `<use>` of an outline kept once per slide in `<defs>` (in font units, under an id made of the face's content hash and the glyph id, so SVGs inlined on one page never mix up glyphs); positions, widths and shaping are the vector PDF's (the same layout of the renderer's pinned lines), so over the core example decks resvg draws an outlined slide like its text (mean channel difference at most 0.006 of 255) and an outlined slide is about 38 KB at the median (58 KB at p90, 98 KB at most) against 0.5 to 4 MB with embedded faces. It needs the fonts handle `loadFonts()` returns, which carries the outline engine as `fonts.outlines` (`text-as-paths-needs-fonts` otherwise); the handle's faces are parsed for outlining once, on the first outlined slide. Each outlined element is a group that keeps the element's attributes and `data-opf-*` trace (a tspan's trace stays on a nested group), labelled `role="img"` with its text for assistive technology unless it is `aria-hidden`; links stay `<a>`; underline and line-through are rectangles from the font's own metrics. Text in a colour or bitmap font (emoji) stays text, reported as `text-as-paths-kept-text`, and the SVG embeds just that text's faces. The trade-offs, which make it the choice for thumbnails, previews and portable SVG files and not for an editing surface: the text is no longer text (it cannot be selected, copied, searched or translated, and assistive technology reads one label per text element instead of the text itself); a caret, selection and input method need live text; a vector PDF made from an outlined SVG has paths instead of selectable text, so `svgToPdf` should get the `<text>` SVG; browsers draw outlines without the hinting they give text, so small text looks a little lighter than browser text (resvg already draws text as outlines); and in `/fonts-browser` the outline engine adds about 85 KB (minified) to the bundle. Plan and later phases: [opf-render#164](https://github.com/OpenPresentation/opf-render/issues/164).
171
+ Text as outlines (RR-64): `toSvg(deck, { fonts, text: "paths" })` draws every `<text>` as glyph outlines, so the SVG needs no font and looks the same in every browser, image viewer and design tool. Each glyph is a `<use>` of an outline kept once per slide in `<defs>` (in font units, under an id made of the face's content hash and the glyph id, so SVGs inlined on one page never mix up glyphs); positions and widths are the vector PDF's layout of the renderer's pinned lines, and the glyphs are shaped by HarfBuzz, the shaper browsers use (the Node handle always; a browser handle given `shapeWasm`, the host's copy of `harfbuzzjs/dist/harfbuzz.wasm`; fontkit otherwise). Over the 30 script corpora every outlined glyph has HarfBuzz's id and position (fontkit, which the layout measures with, picks other glyphs on 10 samples: Bengali, Thai, Lao, Khmer, Myanmar, Syriac and Japanese combining marks); a run inside a pinned width is scaled to it, as the browser scales text. Over the core example decks resvg draws an outlined slide like its text (mean channel difference at most 0.006 of 255) and an outlined slide is about 40 KB at the median (60 KB at p90, 106 KB at most) against 0.5 to 4 MB with whole embedded faces. It needs the fonts handle `loadFonts()` returns, which carries the outline engine as `fonts.outlines` (`text-as-paths-needs-fonts` otherwise); the handle's faces are parsed for outlining once, on the first outlined slide. Each outlined element is a group that keeps the element's attributes and `data-opf-*` trace (a tspan's trace stays on a nested group); its glyphs are hidden from assistive technology and each outlined line is also real text, invisible (`fill="none"`, the generic family, so no face is embedded for it) and pinned to the drawn width, in reading order: a screen reader reads the words as text (the FA-30 tree, checked with axe), and selection, copy and find-in-page work. Links stay `<a>`; underline and line-through are rectangles from the font's own metrics. A run stays text where outlines would not be faithful, reported as `text-as-paths-fallback` with its `reason`: a colour or bitmap font (`colour-font`, emoji), a face whose fsType restricts embedding (`restricted`), or a run with no pinned width on which HarfBuzz and the layout's measurement disagree by more than 0.1 px (`shaping`); the SVG embeds just those runs' faces. The trade-offs, which make it the choice for thumbnails, previews and portable SVG files and not for an editing surface: the drawn text is not editable (a caret and an input method need live text in its own font); a vector PDF made from an outlined SVG has paths instead of selectable text, so `toPdf` should get the `<text>` SVG; browsers draw outlines without the hinting they give text, so small text looks a little lighter than browser text (resvg already draws text as outlines); and in `/fonts-browser` the outline engine adds about 85 KB (minified) to the bundle. Plan and later phases: [opf-render#164](https://github.com/OpenPresentation/opf-render/issues/164).
160
172
 
161
173
  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).
162
174
 
163
175
  ```js
164
- import { renderSvg } from "@openpresentation/opf-render";
176
+ import { toSvg } from "@openpresentation/opf-render";
165
177
  import { loadFonts } from "@openpresentation/opf-render/fonts-node";
166
178
 
167
179
  const fonts = await loadFonts({ pack: "office", substitutionPolicy: "visual" });
168
- const svgs = renderSvg(opf, { trace: true, fonts });
180
+ const svgs = toSvg(opf, { trace: true, fonts });
169
181
  console.log(fonts.substitutions);
170
182
  ```
171
183
 
@@ -192,24 +204,24 @@ A placed block (`placement`) arrives with its edge band as its region, and headi
192
204
  PNG and PDF conversion APIs are async because they load the local raster/PDF engines on demand:
193
205
 
194
206
  ```js
195
- import { renderSvg, svgToPdf, svgToPng } from "@openpresentation/opf-render";
207
+ import { toSvg, toPdf, toPng } from "@openpresentation/opf-render";
196
208
  import { loadFonts } from "@openpresentation/opf-render/fonts-node";
197
209
 
198
210
  const fonts = await loadFonts({ pack: "office", substitutionPolicy: "visual" });
199
- const svgs = renderSvg(opf, { fonts });
200
- const png = await svgToPng(svgs[0], { fonts, scale: 1 });
201
- const pdf = await svgToPdf(svgs, { fonts });
211
+ const svgs = toSvg(opf, { fonts });
212
+ const png = await toPng(svgs[0], { fonts, scale: 1 });
213
+ const pdf = await toPdf(svgs, { fonts });
202
214
  ```
203
215
 
204
- `svgToPng` returns PNG bytes for one SVG. `svgToPdf` accepts one SVG or an array of SVGs and returns PDF bytes with one slide per page. The SVG page `width`/`height` or `viewBox` determines the PDF page size. **Units: one SVG pixel is one PDF point (1/72 inch), in both modes**, so a 1280 x 720 slide is a 1280 x 720 pt page (17.8 x 10 in, not the 13.33 x 7.5 in PowerPoint prints); a viewer or printer scales it to the paper; `scale` controls raster density only. `svgToPdf` has two modes: `"vector"` (the default, see below) and `"raster"` (each slide an image, the output of earlier releases): pass `{ mode: "raster" }` for the image-only compatibility output.
216
+ `toPng` returns PNG bytes for one slide or one SVG, and a list for a deck, a selection or a list of SVGs. `toPdf` takes a deck (every slide, or a selection: `toPdf(deck, "2-4")`), one SVG or a list of SVGs and returns PDF bytes with one slide per page. Given a deck, both draw it with `toSvg` and the same options (the fonts handle measures it); SVG input takes no selection. The SVG page `width`/`height` or `viewBox` determines the PDF page size. **Units: one SVG pixel is one PDF point (1/72 inch), in both modes**, so a 1280 x 720 slide is a 1280 x 720 pt page (17.8 x 10 in, not the 13.33 x 7.5 in PowerPoint prints); a viewer or printer scales it to the paper; `scale` controls raster density only. `toPdf` is vector by default (see below); `raster: true` draws each slide as an image, the output of earlier releases.
205
217
 
206
218
  ### Vector PDF with selectable text
207
219
 
208
- `svgToPdf(svgs, { mode: "vector" })` converts the same SVG the preview draws, without a second layout pass: every line, position and width comes from the SVG.
220
+ `toPdf(svgs)` converts the same SVG the preview draws, without a second layout pass: every line, position and width comes from the SVG.
209
221
 
210
222
  - Shapes, lines, polylines, paths and rounded rectangles are PDF paths; linear and radial gradients are PDF shadings and hatch/tile `<pattern>`s are tiling patterns; `clip-path`, stroke dashes, group `opacity` (a transparency-group form) and per-element opacity are native. Pictures are image XObjects (PNG with alpha as an SMask, unoriented JPEG passed through, WebP and oriented JPEG decoded as in raster mode; identical pictures are stored once).
211
223
  - Text is real text: TrueType subsets (the font program, a `ToUnicode` map and a `CIDToGIDMap`) of the same font files the PNG preview draws with, positioned glyph by glyph (kerning, ligatures, combining marks and mixed scripts as fontkit shapes them), with the SVG's `textLength` and `text-anchor`, underline and strike-through. The Unicode bidirectional algorithm orders right-to-left and mixed-direction lines. Text is selectable, searchable and copies as the authored text. The encoding is the one Chromium writes, so PDFium (Chrome, Edge), poppler and pdf.js read it back the way they read a Chrome-printed page: runs are drawn in visual order, right-to-left runs are marked `/ReversedChars`, and a glyph or cluster the glyph map cannot give (an Arabic ligature or mirrored bracket, a reordered Indic or Khmer syllable, a no-break space) carries its logical text as `/ActualText`, one span per glyph or cluster. A mark the font split off a letter (Arabic dots) is drawn as a filled outline, so no extractor sees an extra character. There is no hidden text layer and no outline-only text. Known reader limits, shared with Chrome's own PDFs: PDFium duplicates some Thai and Burmese marks, PDFium reorders the runs of a mixed-direction line, and pdf.js ignores `/ActualText` (it reports reordered Indic and Khmer clusters in drawing order).
212
- - Fonts: only the bundled pack (unless the handle says `useBundledFonts: false`), the handle's `fontFiles`, `fontDirs` and the SVG's own `@font-face` data are used; a handle with `loadSystemFonts: true` is rejected in vector mode because system fonts could be proprietary. A face whose OS/2 `fsType` forbids embedding is never embedded (a diagnostic says so); one that forbids subsetting is embedded whole. A requested family that has no face is drawn with the family the PNG preview draws (`defaultFontFamily`, `sansSerifFamily`, `monospaceFamily`, `serifFamily`) and reported.
224
+ - Fonts: only the bundled pack (unless the handle says `useBundledFonts: false`), the handle's `fontFiles` (or the font folders given as `fonts`) and the SVG's own `@font-face` data are used; a handle with `loadSystemFonts: true` is rejected for a vector PDF because system fonts could be proprietary. A face whose OS/2 `fsType` forbids embedding is never embedded (a diagnostic says so); one that forbids subsetting is embedded whole. A requested family that has no face is drawn with the family the PNG preview draws (`defaultFontFamily`, `sansSerifFamily`, `monospaceFamily`, `serifFamily`) and reported.
213
225
  - Document metadata: `metadata: { title, author, subject, keywords, language, creator, creationDate }`. The language defaults to the first SVG's `lang`. No date is written unless you supply one, there is no random data, and the file identifier is a hash of the content, so identical input gives identical bytes on every machine.
214
226
  - Accessibility basics (not a conformance claim): the catalog carries `/Lang`, `/MarkInfo`, `/DisplayDocTitle` (with a title) and XMP metadata; with `tagged` (default) the structure tree lists, per slide in source order, headings (the `.title` placeholder), paragraphs, figures with their `aria-label` as `/Alt` and link elements bound to their link annotations, and decoration (backgrounds, rules, bullets) is marked as an artifact. Not done: PDF/UA or PDF/A claims, list and table structure, per-span languages; text inside a translucent group is not in the structure tree.
215
227
  - Links: `<a href>` with an http, https, mailto or tel target becomes a link annotation on each run of its text (other schemes are refused with a diagnostic).
@@ -219,24 +231,24 @@ const pdf = await svgToPdf(svgs, { fonts });
219
231
  `onDiagnostic` also reports each embedded face (`pdf-font-embedded`: family, weight, glyph count, size, subset or full, `fsType`, license text), font substitution and per-character fallback. Output is byte-identical across runs and operating systems for the same SVG, font files and options.
220
232
 
221
233
  ```js
222
- const pdf = await svgToPdf(svgs, {
234
+ const pdf = await toPdf(svgs, {
223
235
  fontFiles: fonts.fontFiles, useBundledFonts: false,
224
236
  metadata: { title: "Quarterly review", author: "Finance", language: "en-GB" },
225
237
  onDiagnostic: (d) => d.code === "pdf-raster-fallback" && console.warn(d.path, d.reason),
226
238
  });
227
239
  ```
228
240
 
229
- The default changed from raster to vector in the release that added this section; callers that need the previous output pass `mode: "raster"`. Vector mode ignores `scale` and `dpi`. The page is painted white by default, as raster mode composites on white; `background: "none"` leaves it unpainted and any other colour paints that.
241
+ The default changed from raster to vector in the release that added this section; callers that need the previous output pass `raster: true`. A vector PDF ignores `scale` and `dpi`. The page is painted white by default, as a raster PDF composites on white; `background: "none"` leaves it unpainted and any other colour paints that.
230
242
 
231
243
  ## PNG and PDF in a browser
232
244
 
233
- `@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).
245
+ `@openpresentation/opf-render/export-browser` exports `toPdf` and `toPng` for a page, with the arguments of the Node functions. They take a deck (drawn with the browser fonts handle) or 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).
234
246
 
235
- - **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.
247
+ - **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 `toSvg`; 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 `toPdf` (script faces an SVG does not carry reach the PDF too) or from `fontData: [{ data, family? }]`; `fontFiles` and font folders do not exist here and system fonts cannot be loaded. A face the handle never loaded is never embedded.
236
248
  - **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`.
237
- - **Raster PDF** (`mode: "raster"`, `scale` default 2) draws each slide on a canvas as an image.
249
+ - **Raster PDF** (`raster: true`, `scale` default 2) draws each slide on a canvas as an image.
238
250
  - **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).
239
- - `signal` (an `AbortSignal`, checked between pages) and `onProgress({ page, pages })` serve large decks; the same two options also work in the Node `svgToPdf`. The browser export yields to the page between slides.
251
+ - `signal` (an `AbortSignal`, checked between pages) and `onProgress({ page, pages })` serve large decks; the same two options also work in the Node `toPdf`. The browser export yields to the page between slides.
240
252
 
241
253
  Verified in Chromium by `test/export-browser.mjs` (`npm run test:export-browser`), which also checks the text pdf.js extracts, the eight JPEG orientations, offline operation and determinism. Safari and Firefox are not exercised by this repository's CI.
242
254
  Complex scripts in raster output: resvg draws a HarfBuzz cluster with the advance of its widest glyph, so Indic, Thai, Lao, Khmer and Myanmar text lost the advance of vowel signs and ran together, and it ignores the SVG `lang` (Korean spacing). The raster path therefore rewrites its private copy of such text cluster by cluster at fontkit's positions (`src/raster-text.js`): Devanagari, Gujarati, Oriya, Tamil, Kannada and Sinhala as fontkit glyph outlines, the other scripts and Korean as single clusters resvg shapes in isolation. The emitted SVG is unchanged, text in other scripts is rasterized exactly as before, and `test/script-corpora-raster.mjs` holds every script to a HarfBuzz outline reference (the fixture's `rasterLimits` records what still differs: the KOR punctuation forms).
@@ -246,19 +258,26 @@ Complex scripts in raster output: resvg draws a HarfBuzz cluster with the advanc
246
258
  Use `@openpresentation/opf-render/svg` for browser rendering without Node dependencies. Browser-aware bundlers also select this shared SVG implementation for the root import; Node's root import retains PNG/PDF conversion.
247
259
 
248
260
  ```js
249
- import { renderSlideSvg } from '@openpresentation/opf-render/svg';
261
+ import { toSvg } from '@openpresentation/opf-render/svg';
250
262
  import { loadFonts } from '@openpresentation/opf-render/fonts-browser';
251
263
 
252
264
  const fonts = await loadFonts({ faces: fontFileEntries });
253
- container.innerHTML = renderSlideSvg(presentation, 0, { fonts });
265
+ container.innerHTML = toSvg(presentation, 1, { fonts });
254
266
  // fonts.dispose() when its canvases are unmounted.
255
267
  ```
256
268
 
257
269
  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.
258
270
 
271
+ Subset embedded faces in the browser (RR-65): pass `subsetWasm` to `loadFonts` to cut each face a standalone SVG embeds to the glyphs its slide draws, as the Node handle always does. It is the `harfbuzz-subset.wasm` file of the `harfbuzzjs` dependency (`harfbuzzjs/dist/harfbuzz-subset.wasm`, MIT), served by the host like the font files: its URL, its bytes or a compiled `WebAssembly.Module`. The loader instantiates it asynchronously and fetches nothing else; without it a browser handle embeds whole faces. Live previews in a page that already holds the faces should use `text: "system"` instead, which embeds nothing.
272
+
273
+ ```js
274
+ const fonts = await loadFonts({ faces: fontFileEntries, subsetWasm: '/vendor/harfbuzz-subset.wasm' });
275
+ const standalone = toSvg(presentation, 1, { fonts }); // each @font-face is a glyph subset
276
+ ```
277
+
259
278
  ## Player and `<opf-deck>` (RR-28)
260
279
 
261
- A slideshow player and an embeddable web component, both built on `renderSlideSvg`: the slide a page shows is the slide the preview, the editor and the PDF show, with no second layout engine. They are plain ES modules, typed, framework-free and tree-shakeable, and importing them touches no DOM, so they are safe in Next.js and other server renderers.
280
+ A slideshow player and an embeddable web component, both built on `toSvg`: the slide a page shows is the slide the preview, the editor and the PDF show, with no second layout engine. They are plain ES modules, typed, framework-free and tree-shakeable, and importing them touches no DOM, so they are safe in Next.js and other server renderers.
262
281
 
263
282
  ```html
264
283
  <script type="module">
@@ -276,7 +295,7 @@ useEffect(() => { defineOpfDeck(); }, []);
276
295
  // ...and render <opf-deck src="/deck.opf.json" fonts="/opf-fonts/" />.
277
296
  ```
278
297
 
279
- Entry points (package `exports`): `/element` (`defineOpfDeck`, `getOpfDeckElement`, `renderDeckHtml`, `loadPreviewFonts`), `/element/define` (importing it registers the tag; the only file with a side effect), `/player` (`present`), `/preview-fonts` (the font root layout and `loadPreviewFonts`) and `/preview-fonts-node` (`copyPreviewFonts`, also the `opf-preview-fonts` command). The element loads the player on first use, so a page that only embeds decks does not carry slideshow code.
298
+ Entry points (package `exports`): `/element` (`defineOpfDeck`, `getOpfDeckElement`, `toHtml`, `loadPreviewFonts`), `/element/define` (importing it registers the tag; the only file with a side effect), `/player` (`present`), `/preview-fonts` (the font root layout and `loadPreviewFonts`) and `/preview-fonts-node` (`copyPreviewFonts`, also the `opf-preview-fonts` command). The element loads the player on first use, so a page that only embeds decks does not carry slideshow code.
280
299
 
281
300
  ### Fonts: one self-hosted directory
282
301
 
@@ -298,7 +317,7 @@ It is the layout the editor playground and the OpenPresentation sites serve, and
298
317
 
299
318
  ### `<opf-deck>`
300
319
 
301
- Attributes: `src` (OPF JSON; the other request the element makes), `slide` (1-based; counts the slides that play), `fonts`, `thumbnails` (a strip of slide thumbnails), `controls="none"`, `present` (a Present button), `include-hidden`, `label`, `keyboard="off"`. Instead of `src` set the `document` property (an object or JSON text), or put the document in a child `<script type="application/opf+json">`. Properties and methods: `document`, `slide`, `total`, `fonts` (the font root URL, or a handle that wins over the attribute), `renderOptions` (extra `renderSlideSvg` options such as `catalogs` and `imageResolver`), `currentSlide` (`{ slide, total, index, id, title, notes, section }`; `notes` is plain text), `ready`, `next()`, `previous()`, `first()`, `last()`, `goto(n)`, `reload()` and `present(options)`.
320
+ Attributes: `src` (OPF JSON; the other request the element makes), `slide` (1-based; counts the slides that play), `fonts`, `thumbnails` (a strip of slide thumbnails), `controls="none"`, `present` (a Present button), `include-hidden`, `label`, `keyboard="off"`. Instead of `src` set the `document` property (an object or JSON text), or put the document in a child `<script type="application/opf+json">`. Properties and methods: `document`, `slide`, `total`, `fonts` (the font root URL, or a handle that wins over the attribute), `renderOptions` (extra `toSvg` options such as `catalogs` and `imageResolver`), `currentSlide` (`{ slide, total, index, id, title, notes, section }`; `notes` is plain text), `ready`, `next()`, `previous()`, `first()`, `last()`, `goto(n)`, `reload()` and `present(options)`.
302
321
 
303
322
  Events: `ready`, `slidechange` (the same detail as `currentSlide`; bubbles and is composed), `error` (`{ code, message, fatal }`, not bubbling), `presentstart` and `presentend`. Style it with custom properties (`--opf-deck-fg`, `--opf-deck-border`, `--opf-deck-radius`, `--opf-deck-focus`, `--opf-deck-accent`, `--opf-deck-stage`) and the parts `deck`, `viewport`, `slide`, `bar`, `button`, `previous`, `next`, `present`, `counter`, `thumbnails` and `thumbnail`.
304
323
 
@@ -308,13 +327,13 @@ Accessibility: the element is a labelled region (the deck name) holding a focusa
308
327
 
309
328
  ### Server markup
310
329
 
311
- `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.
330
+ `toHtml(deck, slides?, 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. `toHtml(deck)` draws the first slide and a list of titles; `toHtml(deck, '1-')` every slide, and `toHtml(deck, 3)`, `'1-3'` or `[1, 3]` those slides of the presented sequence; `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.
312
331
 
313
- 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'`:
332
+ Each slide's SVG embeds only the resolved faces it draws (see `toSvg` above), including bold, italic and glyph fallback, so loading a whole pack for measurement does not put that pack into every slide. `toHtml` accepts `fontMode: 'standalone' | 'shared' | 'external'`:
314
333
 
315
334
  - `standalone` (default) embeds fonts in each slide, so its SVG remains self-contained when extracted.
316
335
  - `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.
317
- - `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.
336
+ - `external` emits no font rules (the slides render with `text: "system"`). 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.
318
337
 
319
338
  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.
320
339
 
@@ -323,12 +342,9 @@ For a lightweight initial page, pre-render measured markup on the server and ena
323
342
  ```js
324
343
  // Server/build step: write this HTML into your page, followed by the upgrade button.
325
344
  import {loadFonts} from '@openpresentation/opf-render/fonts-node';
326
- import {renderDeckHtml} from '@openpresentation/opf-render/element';
345
+ import {toHtml} from '@openpresentation/opf-render/element';
327
346
  const fonts = await loadFonts({pack: 'office', scripts: 'auto', presentation: deck, embedScriptFonts: true});
328
- const html = renderDeckHtml(deck, {
329
- fonts, slides: 'all', fontMode: 'shared', embed: true,
330
- attributes: {id: 'deck', fonts: '/opf-fonts/'},
331
- });
347
+ const html = toHtml(deck, '1-', {fonts, fontMode: 'shared', embed: true, attributes: {id: 'deck', fonts: '/opf-fonts/'}});
332
348
  // <button id="upgrade" type="button">Enable slide controls</button>
333
349
  ```
334
350
 
@@ -406,7 +422,7 @@ Source Sans Pro was renamed Source Sans 3. A request for the old name draws the
406
422
 
407
423
  The 70 vendored faces are 12.0 MiB (FF-43 added 35 faces, 4.6 MiB uncompressed and about 2.4 MB in the tarball; the first 35 were 7.4 MiB) and are the only bytes added; there are no new dependencies. Browser hosts fetch them lazily by family, so the eager list stays the 33 Office and base faces. Every face and notice is checked against its manifest sha256 when loaded (`font-integrity-mismatch`, `font-resource-unavailable`), and `npm run test:packed` verifies the packed copies. `node scripts/update-font-manifest.mjs --vendor` re-downloads each recorded commit (or npm version), reads the license from the shipped notice, refuses a non-permissive one (only OFL-1.1, Apache-2.0, MIT and UFL-1.0 are allowed), copies only the listed faces and rewrites the hashes and provenance; `.gitattributes` keeps the binaries byte-exact.
408
424
 
409
- Embedding: open faces are flagged `embed: "used"`. `registry.embeddedFonts` stays the 33 Office and base faces, while the `loadFonts()` handle's `embeddedFonts` (or `registry.selectEmbeddedFonts`) supplies every face and `renderSvg` embeds an open face only when the slide's text names its family, so a Montserrat slide carries Montserrat and not the whole pack. Raster output reads the files from `fontFiles`. `includeOpenFonts: false` leaves the open families out of the office pack entirely.
425
+ Embedding: open faces are flagged `embed: "used"`. `registry.embeddedFonts` stays the 33 Office and base faces, while the `loadFonts()` handle's `embeddedFonts` (or `registry.selectEmbeddedFonts`) supplies every face and `toSvg` embeds an open face only when the slide's text names its family, so a Montserrat slide carries Montserrat and not the whole pack. Raster output reads the files from `fontFiles`. `includeOpenFonts: false` leaves the open families out of the office pack entirely.
410
426
 
411
427
  ## Script fonts, lang and right-to-left text (FF-19)
412
428
 
@@ -416,12 +432,12 @@ The replacement faces are an optional, hash-pinned font pack: 70 static faces fr
416
432
 
417
433
  ```js
418
434
  import { loadFonts } from '@openpresentation/opf-render/fonts-node';
419
- import { renderSvg, svgToPng } from '@openpresentation/opf-render';
435
+ import { toSvg, toPng } from '@openpresentation/opf-render';
420
436
 
421
437
  // npm install @expo-google-fonts/noto-sans-jp@0.4.3 @expo-google-fonts/noto-naskh-arabic@0.4.5
422
438
  const fonts = await loadFonts({ pack: 'office', substitutionPolicy: 'visual', scripts: ['Jpan', 'Arab'] });
423
- const slides = renderSvg(presentation, { fonts });
424
- const png = await svgToPng(slides[0], { fonts });
439
+ const slides = toSvg(presentation, { fonts });
440
+ const png = await toPng(slides[0], { fonts });
425
441
  ```
426
442
 
427
443
  `scripts` accepts ISO 15924 codes (`Jpan`, `Hans`, `Hant`, `Kore`, `Arab`, `Hebr`, `Deva`, `Beng`, `Thai` and others; see `scriptFontPackages('all')`) or `'all'`. `detectScripts(presentation)` from `/fonts` lists the scripts a document's text needs. Every face and license notice is checked against `BUNDLED_FONT_MANIFEST`. A missing package fails with `font-resource-unavailable` and names the exact version to install. Script faces are left out of `fonts.embeddedFonts` unless you pass `embedScriptFonts: true`, because a CJK face is 5 to 10 MB. Raster output reads them from `fontFiles`. In a browser, serve the installed packages and load `scriptFontEntries(scripts, { baseUrl })` from `/fonts-browser` with `loadFonts({ faces })`. The loader verifies each entry's SHA-256 with Web Crypto before use.
@@ -457,7 +473,7 @@ The SVG root carries `lang` and `xml:lang` from the document's `language`. Measu
457
473
 
458
474
  If core's `resolveScriptFonts` throws for the document, the preview uses the design font for every script, sets no `lang`, lays out every paragraph left to right and reports `language-preview-unresolved` once through `onDiagnostic`.
459
475
 
460
- A face that lacks a character never fails a preview. As in a browser or PowerPoint font linking, each character the resolved face lacks is measured and drawn with the first loaded face that has it, along a fixed chain (`glyphFallbackFamilies(character, profile)`): the character's own script face, the deck language's script face, Noto Sans (Latin, Cyrillic and Greek), the other CJK faces (Japanese, Simplified, Traditional, Korean), then every other Noto script face. A Greek or Cyrillic word under Georgia (Gelasio) or a script scheme's face (Constantia previews with the open PT Serif, which has Cyrillic, so only its Greek text falls back), Greek under Meiryo, Yu Gothic, Microsoft YaHei or Malgun Gothic, Japanese-only kanji beside Hangul, and Simplified-only hanzi in a Japanese deck therefore draw with Noto Sans or the CJK face that has them, and each word stays in one face when one has all of it. Each substitution is reported once per family pair and path as a `font-glyph-fallback` diagnostic through `onDiagnostic` (`fontFamily`, `fallbackFamily`, `scripts`, `characters`); it is a note, not an error. Pass `glyphFallback: 'none'` to `renderSvg` or `createScriptTextMeasurement` to keep exact faces, so a missing glyph raises `missing-glyph` again. Coverage is checked per grapheme cluster and per word: a base letter and the combining marks after it (NFD Vietnamese, Cyrillic U+0306) move together to the first face that has the whole cluster, and only their word moves, not the sentence. Without a registry, a Greek or Cyrillic run names `Noto Sans` after the design font in its font stack. With `scripts: 'auto'`, the CJK fallback faces are loaded for you: the language's face plus at most one fallback face (opf-render#55's cap). Greek and Cyrillic need no package, because the office registry always carries Noto Sans. Serif decks prefer the serif face of each script where one is pinned (Noto Serif Hebrew, Tibetan); no Noto Serif is bundled for Latin, Cyrillic, Greek or CJK, so their fallback text draws in the sans face. A character that no loaded face has still raises `missing-glyph`. The PPTX export is unchanged: it names the chosen font and PowerPoint links its own fallback fonts.
476
+ A face that lacks a character never fails a preview. As in a browser or PowerPoint font linking, each character the resolved face lacks is measured and drawn with the first loaded face that has it, along a fixed chain (`glyphFallbackFamilies(character, profile)`): the character's own script face, the deck language's script face, Noto Sans (Latin, Cyrillic and Greek), the other CJK faces (Japanese, Simplified, Traditional, Korean), then every other Noto script face. A Greek or Cyrillic word under Georgia (Gelasio) or a script scheme's face (Constantia previews with the open PT Serif, which has Cyrillic, so only its Greek text falls back), Greek under Meiryo, Yu Gothic, Microsoft YaHei or Malgun Gothic, Japanese-only kanji beside Hangul, and Simplified-only hanzi in a Japanese deck therefore draw with Noto Sans or the CJK face that has them, and each word stays in one face when one has all of it. Each substitution is reported once per family pair and path as a `font-glyph-fallback` diagnostic through `onDiagnostic` (`fontFamily`, `fallbackFamily`, `scripts`, `characters`); it is a note, not an error. Pass `glyphFallback: 'none'` to `toSvg` or `createScriptTextMeasurement` to keep exact faces, so a missing glyph raises `missing-glyph` again. Coverage is checked per grapheme cluster and per word: a base letter and the combining marks after it (NFD Vietnamese, Cyrillic U+0306) move together to the first face that has the whole cluster, and only their word moves, not the sentence. Without a registry, a Greek or Cyrillic run names `Noto Sans` after the design font in its font stack. With `scripts: 'auto'`, the CJK fallback faces are loaded for you: the language's face plus at most one fallback face (opf-render#55's cap). Greek and Cyrillic need no package, because the office registry always carries Noto Sans. Serif decks prefer the serif face of each script where one is pinned (Noto Serif Hebrew, Tibetan); no Noto Serif is bundled for Latin, Cyrillic, Greek or CJK, so their fallback text draws in the sans face. A character that no loaded face has still raises `missing-glyph`. The PPTX export is unchanged: it names the chosen font and PowerPoint links its own fallback fonts.
461
477
 
462
478
  Characters are assigned to slots following PowerPoint where its rules are known:
463
479
 
@@ -484,9 +500,9 @@ These browser entrypoints are included in the published package. For coordinated
484
500
  A deck that declares content variables, or is marked `"template": true`, is resolved by core `resolveVariables` before it is composed, so the preview and the PPTX exporter agree. Pass the values as `variables`:
485
501
 
486
502
  ```js
487
- import { renderSvg } from '@openpresentation/opf-render';
503
+ import { toSvg } from '@openpresentation/opf-render';
488
504
 
489
- renderSvg(template, { variables: { client: 'Globex', revenue: 1250000 } });
505
+ toSvg(template, { variables: { client: 'Globex', revenue: 1250000 } });
490
506
  ```
491
507
 
492
508
  A template previews with each unfilled variable's `example` and reports `variable-example-used` through `onDiagnostic` (a built-in variable such as `{{speaker.name}}` whose document field is missing reports `variable-builtin-missing`; built-ins read the document's own `speaker`, `organization` and `name`, and a template preview keeps an absent one visible); a variable with no example keeps its `{{id}}` text. A normal deck with an unfilled required variable throws `OPFRenderError` with code `unfilled-variables`, and a value of the wrong kind throws `invalid-variables`. `resolvePresentation(...).presentation` is the concrete deck. `variables: false` draws the document as authored, with tokens and `var:` references visible (the editor canvas's view of a template, so inline edits never overwrite a token). Decks without content variables are untouched. See [templates and variables](https://github.com/OpenPresentation/opf/blob/main/docs/templates-and-variables.md).
@@ -523,9 +539,9 @@ Dependency policy:
523
539
  - `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.
524
540
  - 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.
525
541
  - 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.
526
- - `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.
542
+ - `toPng` and `toPdf` disable system-font loading by default. Hosts that require branded fonts should pass a handle with explicit `fontFiles` (or font folders as `fonts`); `fonts: { loadSystemFonts: true }` is an opt-in escape hatch for PNG and raster PDF output and can make it environment-dependent. A vector PDF rejects it (`pdf-system-fonts-unsupported`): it embeds only fonts you supply, so a system font can never be embedded by accident.
527
543
 
528
- 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).
544
+ Browser support boundary: `toSvg`, `toSvg`, and `resolvePresentation` are browser-importable pure JavaScript APIs. The root `toPng` and `toPdf` (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).
529
545
 
530
546
  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).
531
547
 
@@ -1,9 +1,9 @@
1
1
  // RR-28: the part of the slideshow player and the <opf-deck> element that has no DOM of its own. It reads an OPF document,
2
- // decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (renderSlideSvg:
2
+ // decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (toSvg:
3
3
  // there is no second layout engine) behind a face-level font gate, and synchronizes windows over a BroadcastChannel.
4
4
  // Nothing here fetches anything but the font files the host's font root serves.
5
5
  import { resolveSlideVariables } from "@openpresentation/opf";
6
- import { renderSlideSvg } from "./svg.js";
6
+ import { toSvg } from "./svg.js";
7
7
 
8
8
  /** Error raised for a deck that cannot be read, rendered or loaded. */
9
9
  export class DeckError extends Error {
@@ -206,7 +206,7 @@ export function createFontGate(fonts) {
206
206
 
207
207
  /**
208
208
  * One deck's rendering, shared by the element, the full-screen player and the speaker view: font loading once, each slide
209
- * rendered once by `renderSlideSvg` and kept, so moving between slides never draws twice and every surface shows the same markup.
209
+ * rendered once by `toSvg` and kept, so moving between slides never draws twice and every surface shows the same markup.
210
210
  *
211
211
  * `fonts` is a browser fonts handle (or nothing: layout then uses estimated widths and the visitor's system sans-serif).
212
212
  */
@@ -237,7 +237,7 @@ export function createDeckStore({ document, fonts, renderOptions = {}, date } =
237
237
  let svg = cache.get(index);
238
238
  if (svg === undefined) {
239
239
  try {
240
- svg = renderSlideSvg(deck, index, { ...options(), validate: !validated });
240
+ svg = toSvg(deck, index + 1, { ...options(), validate: !validated });
241
241
  } catch (cause) {
242
242
  if (cause instanceof DeckError) throw cause;
243
243
  // A document the schema rejects says where: the first finding's path and message.
@@ -0,0 +1,38 @@
1
+ // The faces and characters an SVG's text draws, read from its markup: the families of each text's font-family list, at the
2
+ // font-weight and font-style the text takes (attributes are inherited down the element tree, as in SVG). RR-61 embeds only the
3
+ // faces a slide draws; RR-65 cuts each to the characters drawn in its family. Shared by the SVG writer and the SSR deck markup.
4
+
5
+ const FONT_WEIGHT_KEYWORDS = { normal: "400", bold: "700" };
6
+ const TEXT_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
7
+ export function decodeTextEntities(text) {
8
+ if (!text.includes("&")) return text;
9
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code) => code[0] === "#" ? String.fromCodePoint(code[1].toLowerCase() === "x" ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10)) : TEXT_ENTITIES[code] ?? match);
10
+ }
11
+ /** family (lowercase) to the set of "weight|style" pairs the markup draws text in; with `characters` (a Map), also the code points each family draws. */
12
+ export function drawnFaces(markup, characters) {
13
+ const drawn = new Map();
14
+ const tokens = /<(\/?)([A-Za-z][\w:.-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>|<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|[^<]+/g;
15
+ const stack = [{ families: [], weight: "400", style: "normal", text: false }];
16
+ const attribute = (attributes, name) => { const found = new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`).exec(attributes); return found ? found[1] ?? found[2] : undefined; };
17
+ for (const [token, close, name, attributes, selfClose] of markup.matchAll(tokens)) {
18
+ const top = stack.at(-1);
19
+ if (name === undefined) {
20
+ if (top.text && /\S/.test(token)) for (const family of top.families) { const set = drawn.get(family) ?? new Set(); set.add(`${top.weight}|${top.style}`); drawn.set(family, set); }
21
+ if (characters && top.text && token[0] !== "<") {
22
+ const points = [...decodeTextEntities(token)].map(character => character.codePointAt(0));
23
+ for (const family of top.families) { const set = characters.get(family) ?? new Set(); for (const point of points) set.add(point); characters.set(family, set); }
24
+ }
25
+ continue;
26
+ }
27
+ if (close) { if (stack.length > 1) stack.pop(); continue; }
28
+ const family = attribute(attributes, "font-family"), weight = attribute(attributes, "font-weight"), style = attribute(attributes, "font-style");
29
+ const next = {
30
+ families: family === undefined ? top.families : family.split(",").map(item => item.trim().replace(/^&quot;|&quot;$|^["']|["']$/g, "").toLowerCase()).filter(Boolean),
31
+ weight: weight === undefined ? top.weight : FONT_WEIGHT_KEYWORDS[weight.trim()] ?? weight.trim(),
32
+ style: style === undefined ? top.style : /italic|oblique/.test(style) ? "italic" : "normal",
33
+ text: top.text || name === "text",
34
+ };
35
+ if (!selfClose) stack.push(next);
36
+ }
37
+ return drawn;
38
+ }
package/dist/element.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { BrowserFontsHandle } from "./fonts-browser.js";
2
2
  import type { RenderFonts } from "./fonts.js";
3
- import type { RenderSvgOptions } from "./svg.js";
3
+ import type { SlideSelection, ToSvgOptions } from "./svg.js";
4
4
  import type { PlayerSession, PresentOptions, PlayerSlideChange } from "./player.js";
5
5
  export { DeckError } from "./player.js";
6
6
  export { loadPreviewFonts, previewBaseFaces, previewFontLayout } from "./preview-fonts.js";
@@ -43,8 +43,8 @@ export interface OpfDeckElement extends HTMLElement {
43
43
  * attribute's URL (an empty string without either).
44
44
  */
45
45
  fonts: string | BrowserFontsHandle;
46
- /** Extra `renderSlideSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
47
- renderOptions: Partial<RenderSvgOptions>;
46
+ /** Extra `toSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
47
+ renderOptions: Partial<ToSvgOptions>;
48
48
  includeHidden: boolean;
49
49
  thumbnails: boolean;
50
50
  readonly ready: Promise<void>;
@@ -66,10 +66,8 @@ export declare function getOpfDeckElement(): { new (): OpfDeckElement; readonly
66
66
  /** Register `<opf-deck>` (or another tag name). Safe to call twice and on a server, where it does nothing. */
67
67
  export declare function defineOpfDeck(tagName?: string): CustomElementConstructor | undefined;
68
68
 
69
- export interface RenderDeckHtmlOptions {
69
+ export interface ToHtmlOptions {
70
70
  tagName?: string;
71
- /** `"first"` (default: the first slide and a list of titles), `"all"`, or 1-based slide numbers. */
72
- slides?: "first" | "all" | readonly number[];
73
71
  includeHidden?: boolean;
74
72
  src?: string;
75
73
  slide?: number;
@@ -86,11 +84,16 @@ export interface RenderDeckHtmlOptions {
86
84
  embed?: boolean;
87
85
  /** Today's date (ISO) for `date: true` furniture. */
88
86
  date?: string;
89
- renderOptions?: Partial<RenderSvgOptions>;
87
+ renderOptions?: Partial<ToSvgOptions>;
90
88
  }
91
89
 
92
- /** Server-side markup: the deck's slides as inline SVG inside the tag, shown as is without JavaScript and replaced when the element upgrades. */
93
- export declare function renderDeckHtml(document: unknown, options?: RenderDeckHtmlOptions): string;
90
+ /**
91
+ * Server-side markup (RR-73): the deck's slides as inline SVG inside the tag, shown as is without JavaScript and replaced when the
92
+ * element upgrades. `toHtml(deck)` draws the first slide and a list of titles; `toHtml(deck, 3)`, `toHtml(deck, "1-3")` (`"1-"` for
93
+ * every slide) or `toHtml(deck, [1, 3])` draw those slides of the presented sequence, counting from 1.
94
+ */
95
+ export declare function toHtml(document: unknown, options?: ToHtmlOptions): string;
96
+ export declare function toHtml(document: unknown, slides: SlideSelection, options?: ToHtmlOptions): string;
94
97
 
95
98
  declare global {
96
99
  interface HTMLElementTagNameMap { "opf-deck": OpfDeckElement }