@openpresentation/opf-render 0.15.0 → 0.17.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 +129 -9
- package/dist/annotations.js +1 -1
- package/dist/converters.js +41 -0
- package/dist/deck-runtime.js +11 -4
- 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 +9 -6
- package/dist/fonts-browser.js +3 -0
- package/dist/fonts-node.js +27 -5
- package/dist/fonts.d.ts +9 -3
- package/dist/index.d.ts +13 -1
- package/dist/pdf-text.js +8 -5
- 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 +23 -16
- package/dist/svg.d.ts +13 -1
- package/dist/svg.js +133 -47
- package/dist/text-paths.js +278 -0
- package/package.json +70 -17
package/dist/fonts-browser.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createFontRegistry, OPFFontError } from "./font-registry.js";
|
|
|
2
2
|
import { lazyFontEntries, lazyFontList, normalizeExtraLazyFonts } from "./lazy-font-list.js";
|
|
3
3
|
import { lazyFacesNeeded } from "./lazy-fonts.js";
|
|
4
4
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
5
|
+
import { textOutlines } from "./text-paths.js";
|
|
5
6
|
import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
6
7
|
export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
|
|
7
8
|
export { lazyFontEntries, lazyFontList, splitStartupFaces } from "./lazy-font-list.js";
|
|
@@ -293,6 +294,8 @@ export async function loadFonts({ faces = [], ...options } = {}) {
|
|
|
293
294
|
textMeasurement: registry.textMeasurement,
|
|
294
295
|
get embeddedFonts() { if (stale) { embedded = registry.embeddedFonts; stale = false; } return embedded; },
|
|
295
296
|
registry,
|
|
297
|
+
// RR-64: the outline engine `renderSvg(deck, { fonts, textAsPaths: true })` draws text with, over this registry's faces.
|
|
298
|
+
outlines: textOutlines({ registry }),
|
|
296
299
|
manifest: BUNDLED_FONT_MANIFEST,
|
|
297
300
|
get substitutions() { return registry.substitutions; },
|
|
298
301
|
/**
|
package/dist/fonts-node.js
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { createFontRegistry, OPFFontError } from "./font-registry.js";
|
|
7
7
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
8
8
|
import { lazyFontList } from "./lazy-font-list.js";
|
|
9
|
+
import { textOutlines } from "./text-paths.js";
|
|
9
10
|
export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
10
11
|
import { analyzePresentationScripts, autoScriptSelection, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
|
|
11
12
|
export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
|
|
@@ -25,10 +26,24 @@ async function verifiedFile(file, expected, details) {
|
|
|
25
26
|
// The default Latin, Cyrillic and Greek glyph-fallback face (the office pack always loads it, fallback-only).
|
|
26
27
|
const FALLBACK_PACKAGE = "@expo-google-fonts/noto-sans";
|
|
27
28
|
|
|
29
|
+
// RR-63: every `@expo-google-fonts/*` package is an optional peer dependency, so a host installs only the faces it uses. A request that
|
|
30
|
+
// needs packages that are not installed fails once, naming all of them and the install command, instead of one package at a time.
|
|
31
|
+
function requireInstalled(packages, what) {
|
|
32
|
+
const missing = packages.filter(pkg => {
|
|
33
|
+
if (pkg.vendored) return false;
|
|
34
|
+
try { require.resolve(`${pkg.name}/package.json`); return false; } catch { return true; }
|
|
35
|
+
});
|
|
36
|
+
if (!missing.length) return;
|
|
37
|
+
const install = `npm install ${missing.map(pkg => `${pkg.name}@${pkg.version}`).join(" ")}`;
|
|
38
|
+
throw new OPFFontError("font-resource-unavailable", `${what} needs font packages that are not installed (optional peer dependencies of @openpresentation/opf-render): run \`${install}\`.`, {packages: missing.map(pkg => pkg.name), install});
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
// `skipped`: an optional npm package that is not installed is recorded there instead of failing (scripts: 'auto').
|
|
29
42
|
// `fallbackOnly`: the faces serve glyph fallback and requests by their own name, never a replacement for another family.
|
|
30
|
-
|
|
43
|
+
// `what`: how the request is named in the error for a package that is not installed, for example "The 'office' font pack".
|
|
44
|
+
async function loadPackages(packages, skipped, {fallbackOnly = false, what = "This font request"} = {}) {
|
|
31
45
|
const entries = [], fontFiles = [];
|
|
46
|
+
if (!skipped) requireInstalled(packages, what);
|
|
32
47
|
for (const pkg of packages) {
|
|
33
48
|
if (pkg.vendored) {
|
|
34
49
|
// FF-31: vendored faces ship inside this package (pkg.vendored, for example fonts/carlito), hash-pinned like the npm packs. The open pack is
|
|
@@ -53,7 +68,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false} = {}) {
|
|
|
53
68
|
installed = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
54
69
|
} catch (error) {
|
|
55
70
|
if (skipped) { skipped.push({package: pkg.name, version: pkg.version, scripts: [...pkg.scripts]}); continue; }
|
|
56
|
-
throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use
|
|
71
|
+
throw new OPFFontError("font-resource-unavailable", `Install ${pkg.name}@${pkg.version} to use ${what}.`, {package:pkg.name, cause:error.code});
|
|
57
72
|
}
|
|
58
73
|
if (installed.version !== pkg.version) throw new OPFFontError("font-version-mismatch", `Expected ${pkg.name}@${pkg.version}; reinstall the pinned package.`, {package:pkg.name, expected:pkg.version, actual:installed.version});
|
|
59
74
|
const directory = path.dirname(manifestPath);
|
|
@@ -73,7 +88,7 @@ async function loadPackages(packages, skipped, {fallbackOnly = false} = {}) {
|
|
|
73
88
|
const renamedAliases = () => Object.fromEntries(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === "open" && item.renamedFrom).map(item => [item.renamedFrom, item.faces[0].family]));
|
|
74
89
|
// The vendored faces this registry holds that are embed "used": a host that serves fonts itself lists them to copy their files.
|
|
75
90
|
const lazyOf = registry => lazyFontList().filter(face => registry.describeFaces().some(held => held.family === face.family && held.weight === face.weight && held.italic === face.italic));
|
|
76
|
-
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack));
|
|
91
|
+
const loadPack = pack => loadPackages(BUNDLED_FONT_MANIFEST.packages.filter(item => item.pack === pack), undefined, {what: `The '${pack}' font pack`});
|
|
77
92
|
|
|
78
93
|
/**
|
|
79
94
|
* Adds the script pack. `scripts` is 'all', ISO 15924 codes, or 'auto' (FF-19): detect the scripts the
|
|
@@ -86,7 +101,7 @@ async function withScripts(loaded, scripts, {presentation, onDiagnostic, renderO
|
|
|
86
101
|
// A package the registry already loaded (the default Noto Sans fallback) is never loaded twice.
|
|
87
102
|
const notLoaded = list => list.filter(pkg => !loaded.packages?.includes(pkg.name));
|
|
88
103
|
if (scripts !== "auto") {
|
|
89
|
-
const extra = await loadPackages(notLoaded(scriptFontPackages(scripts)));
|
|
104
|
+
const extra = await loadPackages(notLoaded(scriptFontPackages(scripts)), undefined, {what: `The requested script fonts (${Array.isArray(scripts) ? scripts.join(", ") : scripts})`});
|
|
90
105
|
return {...loaded, entries:[...loaded.entries, ...embedUsed(extra.entries)], fontFiles:[...loaded.fontFiles, ...extra.fontFiles], packages:[...(loaded.packages ?? []), ...extra.packages]};
|
|
91
106
|
}
|
|
92
107
|
if (presentation === null || typeof presentation !== "object") throw new OPFFontError("invalid-font-scripts", "scripts: 'auto' needs the presentation whose text decides the scripts.", {scripts});
|
|
@@ -160,6 +175,11 @@ async function withFaces(loaded, faces) {
|
|
|
160
175
|
*/
|
|
161
176
|
async function buildRegistry(pack, {scripts, faces, presentation, onDiagnostic, renderOptions, ...options}) {
|
|
162
177
|
let loaded, registryOptions = options;
|
|
178
|
+
// RR-63: name every npm font package the pack needs at once, before loading any of them.
|
|
179
|
+
if (pack === "base" || pack === "office") {
|
|
180
|
+
const packs = pack === "base" ? ["base"] : ["office", ...(options.includeBaseFonts === false ? [] : ["base"])];
|
|
181
|
+
requireInstalled([...BUNDLED_FONT_MANIFEST.packages.filter(item => packs.includes(item.pack)), ...(pack === "office" ? scriptFontPackages(["Latn"]) : [])], `The '${pack}' font pack`);
|
|
182
|
+
}
|
|
163
183
|
if (pack === "office") {
|
|
164
184
|
const {entries, fontFiles, packages} = await loadPack("office");
|
|
165
185
|
for (const [include, name] of [[options.includeOpenFonts, "open"], [options.includeBaseFonts, "base"]]) {
|
|
@@ -177,7 +197,7 @@ async function buildRegistry(pack, {scripts, faces, presentation, onDiagnostic,
|
|
|
177
197
|
// an explicit list does; a presentation that does not select Latn keeps it fallback-only.
|
|
178
198
|
const requested = scripts === "auto" && presentation !== null && typeof presentation === "object" ? scriptFontPackages(autoScriptSelection(presentation, renderOptions).scripts).map(pkg => pkg.name)
|
|
179
199
|
: scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
|
|
180
|
-
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
|
|
200
|
+
const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true, what: "The 'office' font pack (its default glyph-fallback face)"});
|
|
181
201
|
fontFiles.push(...fallback.fontFiles);
|
|
182
202
|
entries.push(...fallback.entries);
|
|
183
203
|
packages.push(...fallback.packages);
|
|
@@ -253,6 +273,8 @@ export async function loadFonts({pack = "base", embedScriptFonts = false, ...opt
|
|
|
253
273
|
useBundledFonts: false,
|
|
254
274
|
loadSystemFonts: false,
|
|
255
275
|
registry,
|
|
276
|
+
// RR-64: the outline engine `renderSvg(deck, { fonts, textAsPaths: true })` draws text with, over this registry's faces.
|
|
277
|
+
outlines: textOutlines({ registry }),
|
|
256
278
|
manifest: BUNDLED_FONT_MANIFEST,
|
|
257
279
|
get substitutions() { return registry.substitutions; },
|
|
258
280
|
/** Load the script faces the presentation's text needs (see `ensureFonts`); `embeddedFonts` and `fontFiles` then include them. */
|
package/dist/fonts.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export type { FontPolicyEntry } from "@openpresentation/opf/font-policy";
|
|
|
8
8
|
export { isSymbolEncodedFamily, mapSymbolText, symbolCodeOf } from "@openpresentation/opf/symbol-font-encodings";
|
|
9
9
|
export { scriptFontRole } from "@openpresentation/opf/composition";
|
|
10
10
|
export type { ScriptRole } from "@openpresentation/opf/composition";
|
|
11
|
-
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** Serves glyph fallback and requests by its own family only; never a replacement for another family. */ fallbackOnly?: boolean; /**
|
|
12
|
-
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; embed?: "used" }
|
|
11
|
+
export interface FontFaceInput { data: Uint8Array; family?: string; weight?: number; italic?: boolean; postscriptName?: string; license?: string; /** Serves glyph fallback and requests by its own family only; never a replacement for another family. */ fallbackOnly?: boolean; /** Every face is embedded in an SVG only when the slide's text draws it (RR-61). "used" also leaves the face out of the eager `registry.embeddedFonts` list (the open pack, FF-31); "always" embeds it in every SVG. */ embed?: "always" | "used"; /** ISO 15924 scripts a designated script replacement face serves (FF-19). */ scripts?: string[] }
|
|
12
|
+
export interface EmbeddedFont { family: string; weight: number; italic?: boolean; dataUrl: string; license?: string; /** "always": written into every SVG; otherwise only into an SVG whose text draws the face (RR-61). */ embed?: "always" | "used" }
|
|
13
13
|
export type FontCompatibility = "exact" | "metric" | "visual" | "generic";
|
|
14
14
|
export interface FontReplacementMeasurement { replacement:string; meanAbsWidthDelta:number; meanWidthDelta:number; maxAbsWidthDelta:number; styles:number; reference:string }
|
|
15
15
|
export interface FontResolution {
|
|
@@ -41,7 +41,7 @@ export declare const EXPERIMENTAL_FONT_CANDIDATES: readonly Readonly<{requestedF
|
|
|
41
41
|
export interface RenderFonts {
|
|
42
42
|
/** Measures text with the real faces. Without it layout uses core's portable estimate. */
|
|
43
43
|
textMeasurement?: TextMeasurement;
|
|
44
|
-
/** Faces `renderSvg`
|
|
44
|
+
/** Faces `renderSvg` may write into an SVG as @font-face data: each only into the SVGs whose text draws it (its family, weight and style), unless flagged `embed: "always"` (RR-61). */
|
|
45
45
|
embeddedFonts?: readonly EmbeddedFont[];
|
|
46
46
|
/** Font files the Node raster and PDF conversions draw with. */
|
|
47
47
|
fontFiles?: readonly string[];
|
|
@@ -49,7 +49,11 @@ export interface RenderFonts {
|
|
|
49
49
|
useBundledFonts?: boolean;
|
|
50
50
|
/** Whether the raster conversion loads system fonts. Default false (the output never depends on the machine). */
|
|
51
51
|
loadSystemFonts?: boolean;
|
|
52
|
+
/** The outline engine `textAsPaths: true` draws text with (RR-64); a `loadFonts` handle carries one over its registry's faces. */
|
|
53
|
+
outlines?: TextOutlines;
|
|
52
54
|
}
|
|
55
|
+
/** Draws a slide's text as glyph outlines (RR-64). Read by `renderSvg`; hosts pass the handle and `textAsPaths: true`. */
|
|
56
|
+
export interface TextOutlines { outlineSlideText(content: string[], options: object): { content: string[]; defs: string } }
|
|
53
57
|
/** What `fonts.ensure(presentation)` loaded: the script packages and vendored faces that were missing, and drawn CJK characters no face covers. */
|
|
54
58
|
export interface EnsureResult { scripts: string[]; lazy: LazyFont[]; uncovered: string[] }
|
|
55
59
|
/** The fonts handle `loadFonts()` returns from `/fonts-node` and `/fonts-browser`: pass it as `{ fonts }` to every deck-level function. */
|
|
@@ -58,6 +62,8 @@ export interface FontsHandle extends RenderFonts {
|
|
|
58
62
|
readonly embeddedFonts: EmbeddedFont[];
|
|
59
63
|
/** The face registry behind the handle (shaping, resolution, lazy and script loading). */
|
|
60
64
|
readonly registry: FontRegistry;
|
|
65
|
+
/** The outline engine for `textAsPaths: true` (RR-64), over this registry's faces, including faces `ensure` loads later. */
|
|
66
|
+
readonly outlines: TextOutlines;
|
|
61
67
|
/** The pinned manifest of the bundled font packages. */
|
|
62
68
|
readonly manifest: { readonly version: number; readonly packages: readonly object[] };
|
|
63
69
|
/** The substitutions made so far (a requested family drawn with another face), as `registry.substitutions`. */
|
package/dist/index.d.ts
CHANGED
|
@@ -54,10 +54,22 @@ export interface RenderSvgOptions {
|
|
|
54
54
|
skipHidden?: boolean;
|
|
55
55
|
/**
|
|
56
56
|
* The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns (from `@openpresentation/opf-render/fonts-node` or
|
|
57
|
-
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces
|
|
57
|
+
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces, an `embeddedFonts` list (each SVG carries only the faces its text draws). Without it layout uses
|
|
58
58
|
* core's portable width estimate and the SVG names the design fonts without embedding them.
|
|
59
59
|
*/
|
|
60
60
|
fonts?: RenderFonts;
|
|
61
|
+
/** `false` writes no `@font-face` data into the SVG (RR-61): the fonts handle still measures and the SVG still names the families, for a host whose page already has the faces (a browser `loadFonts` handle adds them to the document). Default true: each SVG embeds the faces of `fonts.embeddedFonts` its own text draws. */
|
|
62
|
+
embedFonts?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `true` draws every `<text>` as glyph outlines (RR-64): `<use>` of outlines kept once per slide in `<defs>`, so the SVG needs
|
|
65
|
+
* no font and looks the same in every viewer (about 40 KB a slide over the core example decks, against 0.5 to 4 MB of embedded
|
|
66
|
+
* faces). Needs the fonts handle `loadFonts()` returns (`text-as-paths-needs-fonts` otherwise). The text is no longer text: it
|
|
67
|
+
* cannot be selected, searched or edited in place, and each outlined element is a `role="img"` group labelled with its text for
|
|
68
|
+
* assistive technology. Each element keeps its `data-opf-*` trace. Text in a colour or bitmap font stays text
|
|
69
|
+
* (`text-as-paths-kept-text`). Use it for thumbnails, previews and portable SVG files, not for an editing surface or the
|
|
70
|
+
* vector PDF (which keeps real text from `<text>` SVG). Default false.
|
|
71
|
+
*/
|
|
72
|
+
textAsPaths?: boolean;
|
|
61
73
|
/** `"chain"` (default): a character the resolved face lacks is drawn with the first bundled face that has it, and reported as `font-glyph-fallback`. `"none"`: exact faces, a missing glyph raises `missing-glyph`. */
|
|
62
74
|
glyphFallback?: "chain" | "none";
|
|
63
75
|
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|
package/dist/pdf-text.js
CHANGED
|
@@ -59,7 +59,7 @@ export function layoutText(textNode, env, parentStyle, link) {
|
|
|
59
59
|
const preserve = own["xml:space"] === "preserve" || /^pre/.test(own["white-space"] ?? "");
|
|
60
60
|
for (const child of node.children) {
|
|
61
61
|
if (child.text !== undefined) {
|
|
62
|
-
for (const character of child.text.replace(/[\r\n\t]/g, " ")) chars.push({ ch: character, style: own, scope: nextScope, decoration: nextDecoration, link: activeLink, preserve });
|
|
62
|
+
for (const character of child.text.replace(/[\r\n\t]/g, " ")) chars.push({ ch: character, style: own, scope: nextScope, decoration: nextDecoration, link: activeLink, preserve, owner: node });
|
|
63
63
|
} else if (isElement(child)) {
|
|
64
64
|
if (child.name === "tspan" || child.name === "textPath" || child.name === "tref") walk(child, own, nextScope, nextDecoration, activeLink);
|
|
65
65
|
else if (child.name === "a") {
|
|
@@ -89,7 +89,8 @@ export function layoutText(textNode, env, parentStyle, link) {
|
|
|
89
89
|
const styleIds = new Map();
|
|
90
90
|
const faceCache = new Map();
|
|
91
91
|
const linkIds = new Map();
|
|
92
|
-
const
|
|
92
|
+
const ownerIds = new Map();
|
|
93
|
+
const runsByChunk = chunks.map((chunk) => buildRuns(chunk, env, styleIds, faceCache, linkIds, ownerIds));
|
|
93
94
|
|
|
94
95
|
// textLength: the renderer gives lines and tabs an exact width; reproduce it per scope over all its runs.
|
|
95
96
|
const scopeWidth = new Map();
|
|
@@ -155,7 +156,7 @@ function collapseWhitespace(chars) {
|
|
|
155
156
|
for (const char of kept) chars.push(char);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
function buildRuns(chunk, env, styleIds, faceCache, linkIds) {
|
|
159
|
+
function buildRuns(chunk, env, styleIds, faceCache, linkIds, ownerIds) {
|
|
159
160
|
const text = chunk.chars.map((char) => char.ch).join("");
|
|
160
161
|
const baseDirection = chunk.chars[0].style.direction === "rtl" ? "rtl" : "ltr";
|
|
161
162
|
// Levels per UTF-16 unit; characters take the level of their first unit.
|
|
@@ -174,9 +175,11 @@ function buildRuns(chunk, env, styleIds, faceCache, linkIds) {
|
|
|
174
175
|
if (IGNORED.test(char.ch)) return;
|
|
175
176
|
const level = levels[index];
|
|
176
177
|
const resolved = pickFace(char, env, styleIds, faceCache);
|
|
177
|
-
|
|
178
|
+
// RR-64: outlined text keeps each element's trace, so with `env.splitByOwner` a run never spans two elements.
|
|
179
|
+
const owner = env.splitByOwner ? linkId(ownerIds, { node: char.owner }) : 0;
|
|
180
|
+
const key = `${resolved.styleId}|${resolved.face.id}|${level}|${char.scope?.id ?? 0}|${linkId(linkIds, char.link)}|${[...char.decoration].sort().join(",")}|${owner}`;
|
|
178
181
|
if (!current || current.key !== key) {
|
|
179
|
-
current = { chunk: chunkInfo, key, face: resolved.face, style: char.style, level, scope: char.scope, link: char.link, decoration: char.decoration, chars: [], text: "", logical: "", weight: resolved.weight, italic: resolved.italic };
|
|
182
|
+
current = { chunk: chunkInfo, key, face: resolved.face, style: char.style, level, scope: char.scope, link: char.link, decoration: char.decoration, owner: char.owner, chars: [], text: "", logical: "", weight: resolved.weight, italic: resolved.italic };
|
|
180
183
|
runs.push(current);
|
|
181
184
|
}
|
|
182
185
|
current.logical += char.ch;
|
package/dist/pdf-vector.js
CHANGED
|
@@ -724,6 +724,8 @@ class Converter {
|
|
|
724
724
|
result = { resource, object, width: info.width, height: info.height };
|
|
725
725
|
}
|
|
726
726
|
} catch (error) {
|
|
727
|
+
// A missing converter is the host's install, not a bad picture: say so instead of writing a PDF without its pictures.
|
|
728
|
+
if (error?.code === "converter-missing") throw error;
|
|
727
729
|
this.unsupported("pdf-image-skipped", `An embedded image could not be decoded and is not drawn: ${error instanceof Error ? error.message : String(error)}`, { path: tracePath });
|
|
728
730
|
}
|
|
729
731
|
this.images.set(digest, result);
|
package/dist/pdf.d.ts
ADDED
package/dist/pdf.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// RR-63: the PDF format entry (Node). The default vector `svgToPdf` needs no converter for text and shapes; it loads the optional peer
|
|
2
|
+
// `sharp` for pictures and `@resvg/resvg-js` for the rare element it rasterizes, and `mode: "raster"` also loads `pdf-lib`. A converter
|
|
3
|
+
// that is not installed rejects with `OPFRenderError` code `converter-missing`. For a page use `@openpresentation/opf-render/export-browser`.
|
|
4
|
+
export { svgToPdf } from "./raster.js";
|
package/dist/player.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { RenderSvgOptions } from "./svg.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare class DeckError extends Error {
|
|
9
9
|
readonly code: string;
|
|
10
|
-
|
|
10
|
+
readonly details?: {source: string; status?: number; contentType?: string};
|
|
11
|
+
constructor(code: string, message: string, options?: { cause?: unknown; source?: string; status?: number; contentType?: string });
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/** The slide that is showing, in the sequence that plays (hidden slides are not counted). `notes` is plain text. */
|
package/dist/png.d.ts
ADDED
package/dist/png.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// RR-63: the PNG format entry (Node). `svgToPng` loads the optional peers `@resvg/resvg-js` (and `sharp` for WebP and rotated JPEG
|
|
2
|
+
// pictures) on first use and rejects with `OPFRenderError` code `converter-missing` when they are not installed. For a page use
|
|
3
|
+
// `@openpresentation/opf-render/export-browser`.
|
|
4
|
+
export { svgToPng } from "./raster.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// RR-28: fill a self-hosted font root (the layout of `preview-fonts.js`) from the installed packages, for hosts that serve
|
|
2
2
|
// `<opf-deck fonts="/opf-fonts/">`. Node only. Every file is copied unmodified and checked against its pinned SHA-256 and
|
|
3
3
|
// every license notice travels with it (LICENSES.txt). Nothing is downloaded: the faces come from this package's `fonts`
|
|
4
|
-
// directory and the installed `@expo-google-fonts/*` packages.
|
|
4
|
+
// directory and the installed `@expo-google-fonts/*` packages (optional peer dependencies of this package: install the ones you copy).
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
@@ -50,7 +50,7 @@ export function copyPreviewFonts({ outDir, scripts = false, cwd, manifest = BUND
|
|
|
50
50
|
const sourceDirectory = pkg.vendored ? within(rendererRoot, pkg.vendored) : resolvePackageDirectory(pkg.name, cwd);
|
|
51
51
|
if (!sourceDirectory || !fs.existsSync(sourceDirectory)) {
|
|
52
52
|
if (pkg.pack === "scripts") { missing.push(pkg.name); continue; }
|
|
53
|
-
throw new Error(`${pkg.name} is not installed:
|
|
53
|
+
throw new Error(`${pkg.name} is not installed: it is an optional peer dependency of @openpresentation/opf-render, so run "npm install ${pkg.name}@${pkg.version}".`);
|
|
54
54
|
}
|
|
55
55
|
if (!pkg.vendored) {
|
|
56
56
|
const installed = JSON.parse(fs.readFileSync(path.join(sourceDirectory, "package.json"), "utf8"));
|
package/dist/raster-images.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {OPFRenderError} from './svg.js';
|
|
2
|
+
import {isConverterMissing,loadConverter} from './converters.js';
|
|
2
3
|
|
|
3
4
|
// Resvg does not decode WebP or apply JPEG EXIF orientation, and draws no text of an SVG used as an image (its nested
|
|
4
5
|
// document has no fonts). Replace only affected embedded image hrefs in the private
|
|
@@ -27,6 +28,7 @@ export async function prepareRasterImages(svg, {nestedSvg} = {}) {
|
|
|
27
28
|
const replacement = href[0].slice(0, href[0].indexOf(href[1]) + 1) + png + href[1];
|
|
28
29
|
tag = tag.slice(0, href.index) + replacement + tag.slice(href.index + href[0].length);
|
|
29
30
|
} catch (error) {
|
|
31
|
+
if (isConverterMissing(error)) throw error;
|
|
30
32
|
throw new OPFRenderError('image-conversion-failed', 'Embedded raster image could not be prepared for PNG/PDF output.', {path, cause:error instanceof Error ? error.message : String(error)});
|
|
31
33
|
}
|
|
32
34
|
output += svg.slice(end, match.index) + tag;
|
|
@@ -74,7 +76,7 @@ async function prepareRasterImage(uri) {
|
|
|
74
76
|
} else if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff) {
|
|
75
77
|
throw new Error('Embedded data is not a JPEG image.');
|
|
76
78
|
}
|
|
77
|
-
const {default:sharp}=await
|
|
79
|
+
const {default:sharp}=await loadConverter('sharp');
|
|
78
80
|
const decoder=sharp(bytes,{limitInputPixels:40_000_000,animated:false,failOn:'warning'});
|
|
79
81
|
if (!webp) {
|
|
80
82
|
const {orientation}=await decoder.metadata();
|
package/dist/raster.js
CHANGED
|
@@ -3,6 +3,7 @@ import {OPFRenderError,packageName} from './svg.js';
|
|
|
3
3
|
import {separateLigatures} from './font-compatibility.js';
|
|
4
4
|
import {monochromeColorFonts,rasterFontFiles} from './color-fonts.js';
|
|
5
5
|
import {pinScriptClusters} from './raster-text.js';
|
|
6
|
+
import {loadConverter} from './converters.js';
|
|
6
7
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
7
8
|
const DEFAULT_RASTER_SCALE = 1;
|
|
8
9
|
const DEFAULT_RASTER_BACKGROUND = "#FFFFFF";
|
|
@@ -100,7 +101,7 @@ async function vectorPdf(inputs, options) {
|
|
|
100
101
|
// The picture decoder of the Node vector export (the browser entry passes a canvas one). sharp loads on first use.
|
|
101
102
|
function sharpImageCodec() {
|
|
102
103
|
let loaded;
|
|
103
|
-
const sharp = () => (loaded ??=
|
|
104
|
+
const sharp = () => (loaded ??= loadConverter("sharp").then((module) => module.default));
|
|
104
105
|
return {
|
|
105
106
|
metadata: async (bytes) => (await sharp())(bytes, { limitInputPixels: 40_000_000, animated: false }).metadata(),
|
|
106
107
|
rgba: async (bytes, { orient }) => {
|
|
@@ -162,25 +163,8 @@ async function rasterizeSvg(svgInput, options) {
|
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return await import("@resvg/resvg-js");
|
|
168
|
-
} catch (error) {
|
|
169
|
-
throw new OPFRenderError("png-renderer-unavailable", "PNG/PDF conversion requires the Node runtime dependency @resvg/resvg-js.", {
|
|
170
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function loadPdfLib() {
|
|
176
|
-
try {
|
|
177
|
-
return await import("pdf-lib");
|
|
178
|
-
} catch (error) {
|
|
179
|
-
throw new OPFRenderError("pdf-writer-unavailable", "PDF conversion requires the runtime dependency pdf-lib.", {
|
|
180
|
-
cause: error instanceof Error ? error.message : String(error)
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
166
|
+
const loadResvg = () => loadConverter("@resvg/resvg-js");
|
|
167
|
+
const loadPdfLib = () => loadConverter("pdf-lib");
|
|
184
168
|
|
|
185
169
|
async function bundledFontFiles() {
|
|
186
170
|
if (!bundledFontFilesCache) {
|
package/dist/script-font-pack.js
CHANGED
|
@@ -54,28 +54,23 @@ export function scriptPackageEntries(packages, {baseUrl}) {
|
|
|
54
54
|
// Everywhere: the schema URL, embedded assets, catalog records, speaker notes, ids, image alt text and sources,
|
|
55
55
|
// links, metadata and typed `extensions` passthrough. On the presentation: name (aria-label fallback), description,
|
|
56
56
|
// filename, author, audience, purpose, tone, takeaway, duration, tags, narrative, language and version.
|
|
57
|
-
// On a slide: beat. The organization (name), the speaker (name and title) and a slide's section are drawn only
|
|
58
|
-
//
|
|
59
|
-
// only when a
|
|
60
|
-
// categories and series names, code, quotes, metrics, timelines,
|
|
57
|
+
// On a slide: beat. The organization (name), the speaker (name and title) and a slide's section are drawn only through the
|
|
58
|
+
// built-in variables that read them ({{organization.name}}, {{speaker.title}}, {{slide.section}}, FA-31), so they count
|
|
59
|
+
// only when a string the preview draws holds such a token. Everything else is drawn text: titles, text blocks, list items,
|
|
60
|
+
// runs, table cells, chart labels, categories and series names, code, quotes, metrics, timelines, header and footer text.
|
|
61
61
|
const UNDRAWN_KEYS = Object.freeze(["$schema", "assets", "catalogs", "notes", "id", "alt", "altText", "src", "url", "href", "link", "metadata", "extensions"]);
|
|
62
62
|
const UNDRAWN_ROOT_KEYS = Object.freeze(["name", "description", "filename", "author", "authors", "creator", "audience", "purpose", "tone", "takeaway", "duration", "tags", "narrative", "language", "license", "version", "keywords"]);
|
|
63
63
|
const UNDRAWN_SLIDE_KEYS = Object.freeze(["beat"]);
|
|
64
64
|
const UNDRAWN_VALUE = /^(?:https?|data|blob|pkg|file|mailto):\S*$/i;
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
66
|
+
// The built-in variables that copy a document value into drawn text: the inline token and the whole-field reference
|
|
67
|
+
// (a logo or photo reference draws a picture, not text).
|
|
68
|
+
const ORGANIZATION_TOKEN = /\{\{\s*organization\.|var:organization\.(?!logo\b)/;
|
|
69
|
+
const SPEAKER_TOKEN = /\{\{\s*speaker\.|var:speaker\.(?!photo\b|image\b|avatar\b)/;
|
|
70
|
+
const SECTION_TOKEN = /\{\{\s*slide\.section\s*[|}]/;
|
|
72
71
|
|
|
73
|
-
/** The strings
|
|
74
|
-
|
|
75
|
-
const undrawn = new Set(UNDRAWN_KEYS), undrawnRoot = new Set(UNDRAWN_ROOT_KEYS), undrawnSlide = new Set(UNDRAWN_SLIDE_KEYS);
|
|
76
|
-
if (!furnitureUses(presentation, "organization")) undrawnRoot.add("organization");
|
|
77
|
-
if (!furnitureUses(presentation, "speaker")) undrawnRoot.add("speaker");
|
|
78
|
-
if (!furnitureUses(presentation, "section")) undrawnSlide.add("section");
|
|
72
|
+
/** The strings under `presentation`, skipping the keys in the three sets (root keys, slide keys, and everywhere). */
|
|
73
|
+
function* walkStrings(presentation, undrawn, undrawnRoot, undrawnSlide) {
|
|
79
74
|
function* visit(item, level) {
|
|
80
75
|
if (typeof item === "string") { if (!UNDRAWN_VALUE.test(item)) yield item; }
|
|
81
76
|
else if (Array.isArray(item)) for (const child of item) yield* visit(child, level === "slides" ? "slide" : level === "root" ? "deep" : level);
|
|
@@ -89,6 +84,18 @@ export function* drawnStrings(presentation) {
|
|
|
89
84
|
yield* visit(presentation, "root");
|
|
90
85
|
}
|
|
91
86
|
|
|
87
|
+
/** The strings a preview of this presentation draws. */
|
|
88
|
+
export function* drawnStrings(presentation) {
|
|
89
|
+
const undrawn = new Set(UNDRAWN_KEYS), undrawnRoot = new Set([...UNDRAWN_ROOT_KEYS, "organization", "speaker"]), undrawnSlide = new Set([...UNDRAWN_SLIDE_KEYS, "section"]);
|
|
90
|
+
// The organization, the speaker and a slide's section draw only where a drawn string reads them with a built-in variable.
|
|
91
|
+
for (const text of walkStrings(presentation, undrawn, undrawnRoot, undrawnSlide)) {
|
|
92
|
+
if (ORGANIZATION_TOKEN.test(text)) undrawnRoot.delete("organization");
|
|
93
|
+
if (SPEAKER_TOKEN.test(text)) undrawnRoot.delete("speaker");
|
|
94
|
+
if (SECTION_TOKEN.test(text)) undrawnSlide.delete("section");
|
|
95
|
+
}
|
|
96
|
+
yield* walkStrings(presentation, undrawn, undrawnRoot, undrawnSlide);
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
100
|
* The script profile of a presentation: core `resolveScriptFonts` output, exactly what the renderer plans
|
|
94
101
|
* with. When it throws, the renderer ignores the document language, so the profile is empty and Han text is
|
package/dist/svg.d.ts
CHANGED
|
@@ -54,10 +54,22 @@ export interface RenderSvgOptions {
|
|
|
54
54
|
skipHidden?: boolean;
|
|
55
55
|
/**
|
|
56
56
|
* The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns (from `@openpresentation/opf-render/fonts-node` or
|
|
57
|
-
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces
|
|
57
|
+
* `/fonts-browser`), or any object with a `textMeasurement` and, to embed faces, an `embeddedFonts` list (each SVG carries only the faces its text draws). Without it layout uses
|
|
58
58
|
* core's portable width estimate and the SVG names the design fonts without embedding them.
|
|
59
59
|
*/
|
|
60
60
|
fonts?: RenderFonts;
|
|
61
|
+
/** `false` writes no `@font-face` data into the SVG (RR-61): the fonts handle still measures and the SVG still names the families, for a host whose page already has the faces (a browser `loadFonts` handle adds them to the document). Default true: each SVG embeds the faces of `fonts.embeddedFonts` its own text draws. */
|
|
62
|
+
embedFonts?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `true` draws every `<text>` as glyph outlines (RR-64): `<use>` of outlines kept once per slide in `<defs>`, so the SVG needs
|
|
65
|
+
* no font and looks the same in every viewer (about 40 KB a slide over the core example decks, against 0.5 to 4 MB of embedded
|
|
66
|
+
* faces). Needs the fonts handle `loadFonts()` returns (`text-as-paths-needs-fonts` otherwise). The text is no longer text: it
|
|
67
|
+
* cannot be selected, searched or edited in place, and each outlined element is a `role="img"` group labelled with its text for
|
|
68
|
+
* assistive technology. Each element keeps its `data-opf-*` trace. Text in a colour or bitmap font stays text
|
|
69
|
+
* (`text-as-paths-kept-text`). Use it for thumbnails, previews and portable SVG files, not for an editing surface or the
|
|
70
|
+
* vector PDF (which keeps real text from `<text>` SVG). Default false.
|
|
71
|
+
*/
|
|
72
|
+
textAsPaths?: boolean;
|
|
61
73
|
/** `"chain"` (default): a character the resolved face lacks is drawn with the first bundled face that has it, and reported as `font-glyph-fallback`. `"none"`: exact faces, a missing glyph raises `missing-glyph`. */
|
|
62
74
|
glyphFallback?: "chain" | "none";
|
|
63
75
|
/** Unscaled reference-pixel clearance around supplied vector text outlines; default 1. */
|