@openpresentation/opf-render 0.11.6 → 0.11.8

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,8 @@
1
1
  # OPF Render
2
2
 
3
- **Aptos previews with Intos.** The Office pack includes Intos, Intos Display, Intos Narrow and Intos Serif (16 faces, about 12 MB, vendored under `fonts/intos/`, SIL OFL 1.1, pinned to upstream commit fef9315c14da9e4b23b4c3cac8e718998d4e4736 and per-file SHA-256, unmodified from the upstream files). They are the metric-compatible previews for Aptos, Aptos Display, Aptos Narrow and Aptos Serif, the default `aptos` font scheme: 0.000% mean and maximum width difference against Aptos 2.01 in all four styles, with equal vertical metrics. The letter shapes are Intos's own, not Aptos's. The exported PPTX still names Aptos; no Aptos file is bundled or embedded. A registry built without these faces (the base pack) has no Aptos substitute: visual policy falls back to Roboto or Carlito. Like the open families, Intos is `embed: "used"`: it is not in `registry.embeddedFonts` (the 33 eager npm faces), `prepareNodeFonts().options.embeddedFonts` supplies it, and an SVG embeds only the Intos faces its text draws. `registry.lazyFonts` lists the 51 vendored faces (35 open, 16 Intos) with package-relative file and sha256, and `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})` loads them on demand: `await registry.ensureLazyFonts(presentation)` fetches and verifies only the faces the document draws, by family, weight and style (a plain Aptos deck: Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic run adds Intos Italic, a bold run Intos Bold; the code face only when a slide has code), then adds them to the document and the registry together so measurement and painting never disagree. `registry.pendingLazyFonts(presentation)` is the synchronous check hosts gate rendering on, and after an edit it reports just the newly needed faces. The faces are those the renderer itself measures and draws (`presentationFaces(presentation, options, registry)`, one layout-and-paint run with a recording measurement), resolved by the registry's own face selection over the faces it holds plus the vendored ones, so the browser loads exactly what Node draws with every vendored face loaded. A document resolves with the `renderSvg` options the host renders with: pass them as `loadBrowserFontRegistry(entries, { renderOptions: { catalogs } })`, or per call (`pendingLazyFonts(presentation, { catalogs })`, `ensureLazyFonts(presentation, { signal, catalogs })`), so a layout or font scheme id that only the host's catalogs know resolves here too; a document that does not resolve throws (rejects) what `renderSvg` throws for it instead of reporting nothing. Node's `prepareNodeFonts` and `loadOfficeFontRegistry` keep loading the whole open pack and Intos eagerly, so nothing there is face level; with `scripts: 'auto'` they accept `renderOptions: { catalogs }` too. Hosts copy the vendored directories next to the page.
3
+ **Aptos previews with Intos.** The Office pack includes Intos, Intos Display, Intos Narrow and Intos Serif (16 faces, about 12 MB, vendored under `fonts/intos/`, SIL OFL 1.1, pinned to upstream commit fef9315c14da9e4b23b4c3cac8e718998d4e4736 and per-file SHA-256, unmodified from the upstream files). They are the metric-compatible previews for Aptos, Aptos Display, Aptos Narrow and Aptos Serif, the default `aptos` font scheme: 0.000% mean and maximum width difference against Aptos 2.01 in all four styles, with equal vertical metrics. The letter shapes are Intos's own, not Aptos's. The exported PPTX still names Aptos; no Aptos file is bundled or embedded. A registry built without these faces (the base pack) has no Aptos substitute: visual policy falls back to Roboto or Carlito. Like the open families, Intos is `embed: "used"`: it is not in `registry.embeddedFonts` (the 33 eager npm faces), `prepareNodeFonts().options.embeddedFonts` supplies it, and an SVG embeds only the Intos faces its text draws. `registry.lazyFonts` lists the 51 vendored faces (35 open, 16 Intos) with package-relative file and sha256, and `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})` loads them on demand: `await registry.ensureLazyFonts(presentation)` fetches and verifies only the faces the document draws, by family, weight and style (a plain Aptos deck: Intos Display Bold and Intos Regular, 2 files, 1.5 MB; an italic run adds Intos Italic, a bold run Intos Bold; the code face only when a slide has code), then adds them to the document and the registry together so measurement and painting never disagree. Faces a host serves itself load the same way: `loadBrowserFontRegistry(startup, { extraLazyFonts: [{ family, weight, italic, url, sha256 }] })` adds them to the lazy list (`registry.lazyFonts`, `package: "host"`), so the registry can start with Roboto Regular only (`splitStartupFaces(faces)` splits a manifest into the startup face and the rest) and a document draws, fetches and verifies just the faces it needs, vendored and host faces in one `ensureLazyFonts` call. `sha256` is mandatory, loads are all or nothing, `dispose()` removes the faces, and, like the vendored ones, host faces are `embed: "used"`: they are not in `registry.embeddedFonts` (only the startup faces are), and a standalone SVG embeds one only when passed explicitly through `registry.selectEmbeddedFonts` and the slide's text uses its family. `registry.pendingLazyFonts(presentation)` is the synchronous check hosts gate rendering on, and after an edit it reports just the newly needed faces. The faces are those the renderer itself measures and draws (`presentationFaces(presentation, options, registry)`, one layout-and-paint run with a recording measurement), resolved by the registry's own face selection over the faces it holds plus the vendored ones, so the browser loads exactly what Node draws with every vendored face loaded. A document resolves with the `renderSvg` options the host renders with: pass them as `loadBrowserFontRegistry(entries, { renderOptions: { catalogs } })`, or per call (`pendingLazyFonts(presentation, { catalogs })`, `ensureLazyFonts(presentation, { signal, catalogs })`), so a layout or font scheme id that only the host's catalogs know resolves here too; a document that does not resolve throws (rejects) what `renderSvg` throws for it instead of reporting nothing. Node's `prepareNodeFonts` and `loadOfficeFontRegistry` keep loading the whole open pack and Intos eagerly, so nothing there is face level; with `scripts: 'auto'` they accept `renderOptions: { catalogs }` too. Hosts copy the vendored directories next to the page.
4
4
 
5
- Version 0.11.6 makes the category-axis labels of chart previews rotate and skip instead of wrapping mid-word (no API change; the preview of dense-axis chart slides changes) and requires `@openpresentation/opf` ^0.11.3 (the 70 legacy gallery layout ids and their geometry); use it with PPTX 0.11.4 and editor 0.10.4 so preview and export resolve the same core. Version 0.11.5 loads vendored preview fonts by face, not by family, and lets the font loaders resolve documents with the host's render options (`presentationFaces`, `lazyFacesNeeded`; `pendingLazyFonts`, `ensureLazyFonts`, `pendingScripts` and `ensureScripts` take `catalogs` and throw or reject with the `renderSvg` error for a document that does not resolve; `registry.lazyFonts` still lists 94 faces). Version 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'` in Node, shapes Noto Sans Mongolian again and bundles Raleway and Playfair Display (`registry.lazyFonts` lists 94 faces; no API change). Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
5
+ Version 0.11.8 draws the slide tag in the scheme primary colour, as opf-pptx 0.11.5 writes it (no API change; the preview of slides with a tag changes). Version 0.11.7 lets a browser registry load the host's own faces on demand (`extraLazyFonts`, `splitStartupFaces`: a registry can start with Roboto Regular only; additive, `registry.lazyFonts` still lists the 94 vendored faces plus the host's). Version 0.11.6 makes the category-axis labels of chart previews rotate and skip instead of wrapping mid-word (no API change; the preview of dense-axis chart slides changes) and requires `@openpresentation/opf` ^0.11.3 (the 70 legacy gallery layout ids and their geometry); use it with PPTX 0.11.4 and editor 0.10.4 so preview and export resolve the same core. Version 0.11.5 loads vendored preview fonts by face, not by family, and lets the font loaders resolve documents with the host's render options (`presentationFaces`, `lazyFacesNeeded`; `pendingLazyFonts`, `ensureLazyFonts`, `pendingScripts` and `ensureScripts` take `catalogs` and throw or reject with the `renderSvg` error for a document that does not resolve; `registry.lazyFonts` still lists 94 faces). Version 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'` in Node, shapes Noto Sans Mongolian again and bundles Raleway and Playfair Display (`registry.lazyFonts` lists 94 faces; no API change). Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
6
6
 
7
7
  Version 0.9.0 required `@openpresentation/opf` ^0.11.0 and resolved content ColorRef / `variables` through core `resolveColorRef()`. Authored `#RRGGBB` paint stays authored. It retains the existing rendering APIs.
8
8
 
@@ -68,6 +68,15 @@ export declare function loadBrowserFontRegistry(
68
68
  scriptBaseUrl?: string;
69
69
  /** Where the host serves this package's `fonts` directory, so vendored faces (Intos, the open families) load on demand from `<base>/<package-relative file>` (fonts/intos/..., fonts/<family>/...). */
70
70
  lazyFontsBaseUrl?: string;
71
+ /**
72
+ * FF-41: faces the host serves itself, loaded on demand like the vendored ones: hash-verified, all or nothing, in the same
73
+ * `lazyFacesNeeded` pass (a document that draws a vendored and an extra face fetches both in one `ensureLazyFonts`), removed
74
+ * by `dispose()`. They appear in `registry.lazyFonts` with `package: "host"`. Typically the faces of a manifest that
75
+ * `splitStartupFaces` left out of the startup set. Like the vendored faces they are `embed: "used"`: not in `registry.embeddedFonts` (only the startup faces are);
76
+ * pass `registry.selectEmbeddedFonts(...)` to embed them in a standalone SVG, and then only when a slide's text uses their family.
77
+ * A missing or malformed `sha256`, a duplicate `url` or an invalid family throws `invalid-font-source`.
78
+ */
79
+ extraLazyFonts?: readonly ExtraLazyFont[];
71
80
  /**
72
81
  * FF-41: the `renderSvg` options the documents resolve with (`catalogs` above all), the default for `pendingLazyFonts`,
73
82
  * `ensureLazyFonts`, `pendingScripts` and `ensureScripts`. A layout or font scheme id that only the host's catalogs have
@@ -85,7 +94,15 @@ export declare function scriptFontEntries(
85
94
  /** Every vendored face of this package: family, style, package-relative file, sha256. */
86
95
  export declare function lazyFontList(): readonly LazyFont[];
87
96
  /** Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. */
88
- export declare function lazyFontEntries(options: { baseUrl: string }, list?: readonly LazyFont[]): (BrowserFontInput & { url: string; family: string; weight: number; italic: boolean; sha256: string; package: string })[];
97
+ export declare function lazyFontEntries(options: { baseUrl?: string }, list?: readonly LazyFont[]): (BrowserFontInput & { url: string; family: string; weight: number; italic: boolean; sha256: string; package: string })[];
98
+ /** A face the host serves itself and loads on demand (FF-41): `sha256` is mandatory, the registry verifies the bytes before use. */
99
+ export interface ExtraLazyFont { family: string; weight: number; italic?: boolean; url: string; sha256: string; license?: string }
100
+ /**
101
+ * Split faces (a host's manifest, `registry.embeddedFonts`) into the ones a registry starts with and the rest, to pass as
102
+ * `extraLazyFonts`. The default startup set is Roboto Regular (the renderer's fallback family); `startup` chooses other faces, for
103
+ * example the ones `presentationFaces` says the first deck draws. Order is kept.
104
+ */
105
+ export declare function splitStartupFaces<T extends { family: string; weight: number; italic?: boolean }>(faces: readonly T[], options?: { startup?: (face: T) => boolean }): { startup: T[]; rest: T[] };
89
106
  /**
90
107
  * The font families a presentation's slides resolve (heading, body and code roles of every slide). `options` are the
91
108
  * `renderSvg` options the document resolves with (`catalogs`, ...). A document that does not resolve throws what `renderSvg`
@@ -1,9 +1,9 @@
1
1
  import { createFontRegistry, OPFFontError } from "./fonts.js";
2
- import { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
2
+ import { lazyFontEntries, lazyFontList, normalizeExtraLazyFonts } from "./lazy-font-list.js";
3
3
  import { lazyFacesNeeded } from "./lazy-fonts.js";
4
4
  import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptPackageEntries, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
5
5
  export { autoScriptSelection, detectPresentationScripts, scriptFontEntries, scriptFontPackages } from "./script-font-pack.js";
6
- export { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
6
+ export { lazyFontEntries, lazyFontList, splitStartupFaces } from "./lazy-font-list.js";
7
7
  export { lazyFacesNeeded, lazyFontsFor, presentationFaces, presentationFamilies } from "./lazy-fonts.js";
8
8
 
9
9
  async function verifyDigest(entry, data, subtle) {
@@ -22,6 +22,8 @@ async function verifyDigest(entry, data, subtle) {
22
22
 
23
23
  /** Fetch only host-selected font files, then use those exact bytes for shaping and CSS. */
24
24
  export async function loadBrowserFontRegistry(entries, options = {}) {
25
+ // Validate the host's extra faces first: a bad list must not leave startup faces registered in the document with no registry to dispose.
26
+ const extraLazy = normalizeExtraLazyFonts(options.extraLazyFonts);
25
27
  const documentRef = options.document ?? globalThis.document;
26
28
  const FontFaceRef = documentRef?.defaultView?.FontFace ?? globalThis.FontFace;
27
29
  if (!documentRef?.fonts || !FontFaceRef)
@@ -182,7 +184,9 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
182
184
  // FF-31: vendored faces (the open families and Intos) load on demand, once a document needs them. They share the
183
185
  // script loader's queue and disposed state, and the same all-or-nothing order: fetch and verify, load every FontFace,
184
186
  // then add them to the document and the registry together.
185
- const lazy = lazyFontList(), lazyLoaded = new Set();
187
+ // FF-41: the host's extra lazy faces (`options.extraLazyFonts`, validated above) load like the vendored ones, from their own urls,
188
+ // hash-verified, in the same `lazyFacesNeeded` pass, so a document that draws both fetches both in one call.
189
+ const lazy = Object.freeze([...lazyFontList(), ...extraLazy]), lazyLoaded = new Set();
186
190
  const policy = options.substitutionPolicy ?? "none";
187
191
  const aliasTargets = new Map(Object.entries(options.aliases ?? {}).map(([from, to]) => [from.toLowerCase(), to]));
188
192
  // Face level (FF-41): the faces the document draws, resolved as the registry resolves them with every vendored face loaded.
@@ -194,7 +198,7 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
194
198
  return options.lazyFontsBaseUrl;
195
199
  };
196
200
  Object.assign(registry, {
197
- /** Every vendored face a host can load on demand: family, style, package-relative file and pinned sha256. */
201
+ /** Every face a host can load on demand: the vendored ones (family, style, package-relative file, pinned sha256) and the host's `extraLazyFonts` (`package: "host"`, `file` and `url` the url it serves them from). */
198
202
  lazyFonts: lazy,
199
203
  /**
200
204
  * Load the vendored faces the presentation draws (FF-41: only the faces, by family, weight and style, that the renderer
@@ -211,7 +215,9 @@ export async function loadBrowserFontRegistry(entries, options = {}) {
211
215
  callOptions.signal?.throwIfAborted?.();
212
216
  const pending = neededLazy(presentation, callOptions);
213
217
  if (!pending.length) return [];
214
- const fetched = (await fetchFaces(lazyFontEntries({ baseUrl: lazyBaseUrl() }, pending), callOptions.signal)).map((entry) => ({ ...entry, embed: "used" }));
218
+ // The vendored faces need lazyFontsBaseUrl; the host's own faces carry their url. Both are embed "used": not in registry.embeddedFonts.
219
+ const entries = lazyFontEntries({ baseUrl: pending.some((face) => face.package !== "host") ? lazyBaseUrl() : undefined }, pending);
220
+ const fetched = (await fetchFaces(entries, callOptions.signal)).map((entry) => ({ ...entry, embed: "used" }));
215
221
  let browserFaces;
216
222
  try {
217
223
  browserFaces = await Promise.all(fetched.map(async (entry) => {
@@ -36,7 +36,7 @@ export interface ScriptPackOptions {
36
36
  onDiagnostic?: (diagnostic: {code: string; message: string; script?: string; package?: string; scripts?: string[]}) => void;
37
37
  }
38
38
  /** A vendored face that an SVG embeds only when its text names the family (the open families and Intos), and that browser hosts load on demand from a separate hash-pinned file. */
39
- export interface LazyFont { readonly package: string; readonly family: string; readonly weight: number; readonly italic: boolean; /** Path relative to the package root, for example fonts/intos/Intos-Regular.ttf. */ readonly file: string; readonly sha256: string; readonly license: string; readonly renamedFrom?: string }
39
+ export interface LazyFont { readonly package: string; readonly family: string; readonly weight: number; readonly italic: boolean; /** Path relative to the package root, for example fonts/intos/Intos-Regular.ttf. */ readonly file: string; readonly sha256: string; readonly license: string; readonly renamedFrom?: string; /** Set on a host's extra lazy face (`extraLazyFonts`, `package: "host"`): where the host serves the file from. */ readonly url?: string }
40
40
  export type ScriptFontRegistry = FontRegistry & {fontFiles:string[]; /** Set for `scripts: "auto"`. */ scriptSelection?: AppliedScriptSelection};
41
41
  export type NodeFontRegistry = ScriptFontRegistry & {/** The embed "used" faces this registry holds, with the files a host copies to serve them itself. */ lazyFonts: readonly LazyFont[]};
42
42
  export declare function loadBundledFontRegistry(options?: FontRegistryOptions & ScriptPackOptions): Promise<NodeFontRegistry>;
@@ -13,11 +13,55 @@ export function lazyFontList(manifest = BUNDLED_FONT_MANIFEST) {
13
13
  }))));
14
14
  }
15
15
 
16
- /** Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. */
16
+ /**
17
+ * Hash-pinned browser entries for the vendored faces, served by the host from `baseUrl` at their package-relative paths. A face
18
+ * that carries its own `url` (a host's extra lazy face, FF-41) keeps it; `baseUrl` is then needed only for the faces without one.
19
+ */
17
20
  export function lazyFontEntries(options, list = lazyFontList()) {
18
21
  const baseUrl = options?.baseUrl;
19
- if (typeof baseUrl !== "string" || !baseUrl) throw new OPFFontError("invalid-font-source", "lazyFontEntries needs a baseUrl, where the host serves the package's fonts directory.");
20
- const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
21
- return list.map(face => ({ url: `${base}${face.file}`, family: face.family, weight: face.weight, italic: face.italic, sha256: face.sha256, package: face.package }));
22
+ const needsBase = list.some(face => typeof face.url !== "string");
23
+ if (needsBase && (typeof baseUrl !== "string" || !baseUrl)) throw new OPFFontError("invalid-font-source", "lazyFontEntries needs a baseUrl, where the host serves the package's fonts directory.");
24
+ const base = needsBase ? (baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`) : "";
25
+ return list.map(face => ({ url: typeof face.url === "string" ? face.url : `${base}${face.file}`, family: face.family, weight: face.weight, italic: face.italic, sha256: face.sha256, package: face.package, ...(face.license ? { license: face.license } : {}) }));
26
+ }
27
+
28
+ /** Roboto Regular, the renderer's fallback family: the face a browser registry starts with. */
29
+ const isRobotoRegular = face => face?.family === "Roboto" && face.weight === 400 && !face.italic;
30
+
31
+ /**
32
+ * Split a list of faces (a host's manifest, `registry.embeddedFonts`) into the ones a registry starts with and the rest, which a
33
+ * host passes as `extraLazyFonts` so they load on demand. By default the startup set is Roboto Regular, the fallback family; pass
34
+ * `startup` to choose other faces (for example the ones `presentationFaces` says the first deck draws). Order is kept.
35
+ * @template {{family: string, weight: number, italic?: boolean}} T
36
+ * @param {readonly T[]} faces
37
+ * @param {{startup?: (face: T) => boolean}} [options]
38
+ * @returns {{startup: T[], rest: T[]}}
39
+ */
40
+ export function splitStartupFaces(faces, options = {}) {
41
+ const test = options.startup ?? isRobotoRegular;
42
+ if (typeof test !== "function") throw new OPFFontError("invalid-font-source", "splitStartupFaces: startup must be a function.");
43
+ const startup = [], rest = [];
44
+ for (const face of faces) (test(face) ? startup : rest).push(face);
45
+ return { startup, rest };
46
+ }
47
+
48
+ /**
49
+ * Validate a host's extra lazy faces (`{family, weight, italic, url, sha256, license?}`) and shape them like the vendored ones
50
+ * (`package: "host"`, `file` = `url`). The SHA-256 is mandatory: the registry verifies every byte it adds.
51
+ */
52
+ export function normalizeExtraLazyFonts(faces) {
53
+ if (faces === undefined) return [];
54
+ if (!Array.isArray(faces)) throw new OPFFontError("invalid-font-source", "extraLazyFonts must be an array of {family, weight, italic, url, sha256} entries.");
55
+ const seen = new Set();
56
+ return faces.map((face, index) => {
57
+ const label = `extraLazyFonts[${index}]`;
58
+ if (typeof face?.family !== "string" || !face.family.trim() || /[\u0000-\u001f"'\\<>;]/.test(face.family)) throw new OPFFontError("invalid-font-source", `${label} needs a plain family name.`);
59
+ if (!Number.isInteger(face.weight) || face.weight < 1 || face.weight > 1000) throw new OPFFontError("invalid-font-source", `${label} needs a weight between 1 and 1000.`);
60
+ if (typeof face.url !== "string" || !face.url) throw new OPFFontError("invalid-font-source", `${label} needs the url the host serves the font file from.`);
61
+ if (typeof face.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(face.sha256)) throw new OPFFontError("invalid-font-source", `${label} needs the font file's SHA-256 (64 hex digits): extra faces are verified before use.`);
62
+ if (seen.has(face.url)) throw new OPFFontError("invalid-font-source", `${label} repeats url ${face.url}.`);
63
+ seen.add(face.url);
64
+ return Object.freeze({ package: "host", family: face.family, weight: face.weight, italic: face.italic === true, file: face.url, url: face.url, sha256: face.sha256.toLowerCase(), ...(typeof face.license === "string" ? { license: face.license } : {}) });
65
+ });
22
66
  }
23
67
 
package/dist/svg.js CHANGED
@@ -897,7 +897,9 @@ function renderTextPayload(item, box, bound, options) {
897
897
  fontSize: item.field === "title" ? 54 : item.field === "tag" ? 16 : 25,
898
898
  fontFamily: item.field === "title" ? bound.design.fonts.heading : bound.design.fonts.body,
899
899
  fontWeight: item.field === "title" ? 700 : 400,
900
- fill: bound.design.colors.text,
900
+ // The slide tag is the eyebrow label: it draws in the primary colour (scheme accent1), as the
901
+ // PPTX export writes it. Every other text payload here uses the text colour.
902
+ fill: item.field === "tag" ? bound.design.colors.primary : bound.design.colors.text,
901
903
  fit: item.text,
902
904
  textStyle: item.textStyle,
903
905
  diagnosticsHandled: Boolean(item.text),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-render",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,9 +56,9 @@
56
56
  "sideEffects": false,
57
57
  "scripts": {
58
58
  "build": "node scripts/build.mjs",
59
- "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/theme-slots.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/mongolian-shaping.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/lazy-faces.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs && node --check test/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check test/chart-category-labels.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.mjs && node --check test/furniture.mjs && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check test/gallery-layout-alignment.mjs",
59
+ "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/theme-slots.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/mongolian-shaping.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/extra-lazy-fonts.mjs && node --check test/extra-lazy-fonts-browser.mjs && node --check test/lazy-faces.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs && node --check test/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check test/chart-category-labels.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.mjs && node --check test/furniture.mjs && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/tag-colour.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs && node --check test/gallery-layout-alignment.mjs",
60
60
  "validate": "node scripts/validate-package.mjs",
61
- "test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-category-labels.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.mjs && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/lazy-faces.mjs && node test/lazy-face-fallback.mjs && node test/timeline.mjs && node test/quote-footer.mjs && node test/shared-quote.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/rich-tab.mjs && node test/color-references.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/theme-slots.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/mongolian-shaping.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
61
+ "test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-category-labels.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.mjs && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/extra-lazy-fonts.mjs && node test/lazy-faces.mjs && node test/lazy-face-fallback.mjs && node test/timeline.mjs && node test/quote-footer.mjs && node test/shared-quote.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/rich-tab.mjs && node test/color-references.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/theme-slots.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/mongolian-shaping.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/tag-colour.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
62
62
  "check:fonts": "node test/font-licenses.mjs && npm run check:font-hotlinks",
63
63
  "check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
64
64
  "test:golden": "npm run build && node test/golden.mjs",
@@ -79,6 +79,7 @@
79
79
  "test:aptos-preview-browser": "node test/aptos-preview-browser.mjs artifacts/aptos-preview-browser.json",
80
80
  "test:lazy-face-fallback-browser": "node test/lazy-face-fallback-browser.mjs artifacts/lazy-face-fallback",
81
81
  "test:lazy-fonts-browser": "node test/lazy-fonts-browser.mjs artifacts/lazy-fonts",
82
+ "test:extra-lazy-fonts-browser": "node test/extra-lazy-fonts-browser.mjs artifacts/extra-lazy-fonts",
82
83
  "test:script-fonts-browser": "node test/script-fonts-browser.mjs artifacts/script-fonts-browser.json",
83
84
  "test:script-fonts-auto-browser": "node test/script-fonts-auto-browser.mjs artifacts/script-fonts-auto",
84
85
  "test:text": "node test/font-outlines.mjs && node test/accepted-text.mjs",