@openpresentation/opf-render 0.18.2 → 0.19.1
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 +8 -4
- package/dist/font-registry.js +40 -3
- package/dist/index.d.ts +6 -4
- package/dist/svg.d.ts +6 -4
- package/dist/svg.js +31 -41
- package/dist/text-paths.js +3 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
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.
|
|
16
16
|
|
|
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 {
|
|
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 { gallery } from '@openpresentation/gallery'` and `toSvg(deck, { catalogs: [gallery] })`. `strictReferences: true` fails with `unresolved-reference` instead of falling back.
|
|
18
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`.
|
|
19
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.
|
|
20
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.
|
|
@@ -120,7 +120,9 @@ const pptx = await toPptx(presentation, { fonts }); // the same lines in
|
|
|
120
120
|
|
|
121
121
|
One handle carries the measurement for core's `validate`, `paginate` and slide context, for `toSvg`, `toPng` and `toPdf`, and for opf-pptx's `toPptx`. The same handle gives identical text geometry across those engines: every engine reads core's one composition, so the measurement decides the lines. [opf-pptx's `layout-parity` test](https://github.com/OpenPresentation/opf-pptx/blob/main/test/layout-parity.mjs) checks that the exported PowerPoint paragraphs agree with this renderer's SVG and with core's composed items, with and without `fonts`.
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
Node handles, and browser handles loaded with `shapeWasm`, measure advances and glyph ink with the same HarfBuzz shaper used by vector PDF and text outlines. Handles without shaping WASM retain fontkit measurement. This can change line wrapping for Myanmar and some Syriac text; [the reviewed corpus report](docs/evidence/harfbuzz-layout-measurement/README.md) records the differences and the unchanged example output.
|
|
124
|
+
|
|
125
|
+
A call without `fonts` is not an error. It uses core's deterministic estimate instead of measured font advances. Core applies script-specific widths, including the six script estimates improved in OPF 0.19; nonspacing and enclosing marks have no independent advance, while spacing combining marks retain width. The estimate needs no font files, but line breaks, overflow findings and page breaks can differ from what the selected faces actually draw. Treat an estimated preview as a draft. Core's Node `convert` and the `opf` CLI prepare an office-pack handle for you; a library call to `toSvg`, `toPng` or `toPdf` never loads fonts on its own, so pass the handle you want. Compare [opf#364](https://github.com/OpenPresentation/opf/issues/364).
|
|
124
126
|
|
|
125
127
|
|
|
126
128
|
## Install: what to add for what (RR-63)
|
|
@@ -210,6 +212,8 @@ console.log(fonts.substitutions);
|
|
|
210
212
|
|
|
211
213
|
Set `trace: true` to stamp rendered SVG elements with `data-opf-path` values such as `slides.0.title`; omit it for smaller production SVG.
|
|
212
214
|
|
|
215
|
+
Layout templates (OPF 0.19, core `opf-layout/v2`: `areas`, `rows`, `columns` and `regions`) draw from core's composed geometry, as every slide does: the renderer adds no arrangement of its own. Core places each block in its region (`geometry.regions`, `item.region`); a card is the `item.frameBox` core composed in the block's cell (`design.contentBox`), and a region with `bleed` arrives with its outer edges on the slide edge and no card. A list core laid out in columns (`item.listColumns`: a lone list in a region with `listColumns: "auto"`, or a list payload with `columns`) draws each column's own fit in its own box, at one shared size, with numbering continuing across the columns; `item.text` (the whole list) is not drawn then. With `trace: true` everything an item of a region draws (card, payload, caption) is grouped in `<g data-opf-region="<name>">`, and each list column is a group with `data-opf-list-column` (its index), `data-opf-list-start` and `data-opf-list-end` (the half-open range of the payload's items it holds) and its `data-opf-box-*`. OPF 0.19 removes `opf-layout/v1` records with no alias or migration shim. Slides without a layout use core's automatic composition; only items bound to template regions carry these region attributes. `test/layout-template-goldens.mjs` pins the SVG and PNG of the 28 built-in templates with 1 to 6 short and long blocks, and of the pages core's `paginate` makes of the ones that overflow.
|
|
216
|
+
|
|
213
217
|
The installed open font pack works offline. The Aptos family previews with the metric-compatible Intos even under the default metric policy; this example additionally permits visual-only look-alikes for families with no metric replacement yet (a documented fallback), and the substitution report identifies requested and resolved faces. A visual substitute is not a claim of metric or pixel equivalence, and it is a known layout-fidelity gap rather than the end state. Replacements apply to rendering only; the document keeps the selected font name. Omit `substitutionPolicy` to restrict the Office pack to its metric mappings, or supply your own licensed faces (`faces`). Missing fonts/glyphs fail explicitly. Without `fonts`, synchronous SVG APIs use estimated widths; named fonts alone do not make rich-run spacing reliable. Browser hosts should load the same bytes using the loader below.
|
|
214
218
|
|
|
215
219
|
Unresolved images produce an `unresolved-asset` diagnostic through `onDiagnostic`, including the OPF path, reason and complete description. The fallback shows a bounded status label when it fits above the selected readability floor, otherwise an icon with the full accessible description. The label is `Image unavailable` over the description (alt, else title, else `Image`) in semibold at 20 px on a 1280 x 720 canvas, and opf-pptx exports the same placeholder as native shapes. It preserves authored opacity, including faint decorative watermarks; a missing image is still missing even when its fallback fits. Use `strictAssets: true` to reject unresolved images. Supply embedded raster data URIs or a synchronous host `imageResolver` to resolve them; the renderer does not fetch URLs or read local paths. Caller descriptions and other metadata override referenced asset metadata through alias chains. These diagnostics identify unresolved sources, not malformed image bytes or native PowerPoint compatibility.
|
|
@@ -502,7 +506,7 @@ fonts.registry.scriptSelection; // { detected, scripts, unavailable, packages, n
|
|
|
502
506
|
const browserFonts = await loadBrowserFonts({ faces: baseEntries, substitutionPolicy: 'visual', fallbackFamily: 'Roboto', scriptBaseUrl: '/script-fonts/' });
|
|
503
507
|
if (browserFonts.pending(presentation).length) await browserFonts.ensure(presentation); // resolves { scripts, lazy, uncovered }
|
|
504
508
|
// pending is synchronous: empty means nothing is missing, so render at once.
|
|
505
|
-
// Text decides, so the analysis does not resolve layouts. Pass the render options (renderOptions: { catalogs: [
|
|
509
|
+
// Text decides, so the analysis does not resolve layouts. Pass the render options (renderOptions: { catalogs: [gallery] }
|
|
506
510
|
// to loadFonts, or per call) so a font scheme only the host's catalogs have and that names a script font (Yu Gothic) counts.
|
|
507
511
|
```
|
|
508
512
|
|
|
@@ -577,7 +581,7 @@ The package runtime must stay local and deterministic:
|
|
|
577
581
|
|
|
578
582
|
Dependency policy:
|
|
579
583
|
|
|
580
|
-
- `@openpresentation/opf` is the compatibility source for schemas, validation, resolution and composition. The renderer imports no catalog data; hosts register catalogs (`@openpresentation/
|
|
584
|
+
- `@openpresentation/opf` is the compatibility source for schemas, validation, resolution and composition. The renderer imports no catalog data; hosts register catalogs (`@openpresentation/gallery` holds the gallery snapshot).
|
|
581
585
|
- `@resvg/resvg-js` (optional peer, RR-63) is used only for local SVG rasterization in Node; it makes no network calls and does not require a browser. `sharp` (optional peer) decodes WebP, rotated JPEG and the pictures of a vector PDF. Both load on first use; see [Install](#install-what-to-add-for-what-rr-63).
|
|
582
586
|
- `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.
|
|
583
587
|
- 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), HarfBuzz (the fonts handle's `harfbuzz.wasm` of the pinned harfbuzzjs) for shaping, `fontkit` for font metrics (and for shaping without a shaper) and `bidi-js` for the Unicode bidirectional algorithm. No hosted service, network call or system font is involved.
|
package/dist/font-registry.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sha256Bytes } from "./sha256.js";
|
|
2
|
+
import bidiFactory from "bidi-js";
|
|
1
3
|
import { create } from "fontkit";
|
|
2
4
|
import { FONT_COMPATIBILITY, disabledFeaturesFor } from "./font-compatibility.js";
|
|
3
5
|
import { adjustedFontSize, createScriptFonts, openTypeLanguage, scriptFontAliases, sizeAdjustFor } from "./script-fonts.js";
|
|
@@ -11,6 +13,7 @@ import { SYMBOL_SCRIPT, isSymbolEncodedFamily, symbolEncodingFamily, symbolPrevi
|
|
|
11
13
|
export class OPFFontError extends Error {
|
|
12
14
|
constructor(code, message, details = {}) { super(message); this.name = "OPFFontError"; this.code = code; this.details = details; }
|
|
13
15
|
}
|
|
16
|
+
const bidi = bidiFactory();
|
|
14
17
|
const key = (family, weight, italic) => `${family.toLowerCase()}:${weight}:${!!italic}`;
|
|
15
18
|
function base64(data) {
|
|
16
19
|
let binary = "";
|
|
@@ -190,8 +193,15 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
190
193
|
if (entry.embed !== undefined && entry.embed !== "always" && entry.embed !== "used") throw new OPFFontError("invalid-font-embed", "Font embed must be 'always' or 'used'.");
|
|
191
194
|
// A fallback-only face (the default Noto Sans) serves glyph fallback and explicit requests by its own name,
|
|
192
195
|
// but never stands in for another family, so loading it does not change what any other font previews with.
|
|
193
|
-
return {family,familyGroup,fontFace,weight,italic,font,data,format,
|
|
196
|
+
return {family,familyGroup,fontFace,weight,italic,font,data,format,
|
|
197
|
+
// The Node shaper is shared across handles: reuse its immutable font by content, never by a new byte-array identity.
|
|
198
|
+
get hash() {
|
|
199
|
+
const hash = Array.from(sha256Bytes(data), byte => byte.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
200
|
+
Object.defineProperty(this, "hash", {value: hash});
|
|
201
|
+
return hash;
|
|
202
|
+
},license:entry.license,embed:entry.embed,scripts,fallbackOnly:entry.fallbackOnly===true,cache:new Map()};
|
|
194
203
|
};
|
|
204
|
+
let shaper;
|
|
195
205
|
const faces = [], duplicates = new Set(), familySlots = new Map();
|
|
196
206
|
// Adds faces atomically: every check runs before any face is registered.
|
|
197
207
|
const register = added => {
|
|
@@ -265,7 +275,9 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
265
275
|
const {face,resolution}=resolve(style);
|
|
266
276
|
// A BCP-47 `lang` selects the font's OpenType language system, as a browser
|
|
267
277
|
// does for an element's lang (FF-19). Without it, shaping is unchanged.
|
|
268
|
-
const language=openTypeLanguage(style.lang)
|
|
278
|
+
const language=openTypeLanguage(style.lang);
|
|
279
|
+
const direction = style.direction ?? (bidi.getEmbeddingLevels(text).paragraphs[0]?.level % 2 ? "rtl" : "ltr");
|
|
280
|
+
const key = `${direction}\u0000${style.lang ?? ""}\u0000${text}`;
|
|
269
281
|
let value=face.cache.get(key);
|
|
270
282
|
if (value===undefined) {
|
|
271
283
|
if (options.strictGlyphs!==false) for(const character of text) {
|
|
@@ -284,7 +296,27 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
284
296
|
const off=disabledFeaturesFor(face.family),policyFeatures=off&&Object.fromEntries(off.map(tag=>[tag,false]));
|
|
285
297
|
const shape=extra=>{const features=policyFeatures||extra?{...policyFeatures,...extra}:undefined;return language?face.font.layout(text,features,undefined,language):features?face.font.layout(text,features):face.font.layout(text);};
|
|
286
298
|
let run;
|
|
287
|
-
|
|
299
|
+
if (shaper) {
|
|
300
|
+
try {
|
|
301
|
+
const glyphs = shaper.shape(face, text, {direction, lang: style.lang, features: policyFeatures});
|
|
302
|
+
let pen = 0, bounds = null;
|
|
303
|
+
for (const glyph of glyphs) {
|
|
304
|
+
if (includeOutline) {
|
|
305
|
+
// Colour/bitmap glyphs may have no fontkit outline (the em-box fallback below still applies).
|
|
306
|
+
let box;
|
|
307
|
+
try { box = face.font.getGlyph(glyph.gid).bbox; } catch { box = null; }
|
|
308
|
+
if (box && [box.minX, box.minY, box.maxX, box.maxY].every(Number.isFinite) && box.width > 0 && box.height > 0) {
|
|
309
|
+
const next = {minX: pen + glyph.xOffset + box.minX, maxX: pen + glyph.xOffset + box.maxX, minY: glyph.yOffset + box.minY, maxY: glyph.yOffset + box.maxY};
|
|
310
|
+
bounds = bounds ? {minX: Math.min(bounds.minX, next.minX), maxX: Math.max(bounds.maxX, next.maxX), minY: Math.min(bounds.minY, next.minY), maxY: Math.max(bounds.maxY, next.maxY)} : next;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
pen += glyph.advance;
|
|
314
|
+
}
|
|
315
|
+
run = {positions: [{xAdvance: pen}], bbox: bounds && {...bounds, width: bounds.maxX - bounds.minX, height: bounds.maxY - bounds.minY}};
|
|
316
|
+
} catch (error) {
|
|
317
|
+
throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text with HarfBuzz.`, {path:style.path,fontFamily:face.family,cause:error?.message});
|
|
318
|
+
}
|
|
319
|
+
} else try { run=shape(); }
|
|
288
320
|
catch (error) {
|
|
289
321
|
// A lookup fontkit cannot decode (GSUB type 8 in Noto Sans Mongolian) applies nothing; shape again with the rest (FF-44).
|
|
290
322
|
if (skipUndecodableLookups(face.font)) { try { run=shape(); } catch { /* the mark retry below */ } }
|
|
@@ -326,6 +358,11 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
326
358
|
textMeasurement.forScripts=profile=>createScriptFonts(profile,textMeasurement).textMeasurement;
|
|
327
359
|
return {
|
|
328
360
|
textMeasurement,
|
|
361
|
+
// Attach the handle's outline shaper after its WASM loads. Earlier measurements must not survive the engine switch.
|
|
362
|
+
setShaper(engine) {
|
|
363
|
+
shaper = engine;
|
|
364
|
+
for (const face of faces) face.cache.clear();
|
|
365
|
+
},
|
|
329
366
|
resolveFont(style) { return resolve(style).resolution; },
|
|
330
367
|
clearSubstitutions() { substitutions.clear(); },
|
|
331
368
|
get substitutions() { return [...substitutions.values()]; },
|
package/dist/index.d.ts
CHANGED
|
@@ -106,12 +106,17 @@ export interface ToSvgOptions {
|
|
|
106
106
|
* never reads a clock; without it a current date is reported as unresolved content.
|
|
107
107
|
*/
|
|
108
108
|
date?: string;
|
|
109
|
+
/**
|
|
110
|
+
* Stamp drawn elements with their OPF path (`data-opf-path`) and the other `data-opf-*` trace attributes. On a slide with an
|
|
111
|
+
* OPF 0.19 layout template, everything an item of a region draws is grouped in `<g data-opf-region="<name>">`, and each
|
|
112
|
+
* column of a list laid out in columns is a group with `data-opf-list-column`, `data-opf-list-start` and `data-opf-list-end`.
|
|
113
|
+
*/
|
|
109
114
|
trace?: boolean;
|
|
110
115
|
/**
|
|
111
116
|
* Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
|
|
112
117
|
* embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
|
|
113
118
|
* when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
|
|
114
|
-
* `import {
|
|
119
|
+
* `import { gallery } from "@openpresentation/gallery"` and `catalogs: [gallery]`. Omitted: only embedded
|
|
115
120
|
* records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
|
|
116
121
|
* A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
|
|
117
122
|
* (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
|
|
@@ -257,7 +262,6 @@ export interface ResolvedSlide {
|
|
|
257
262
|
colors: Record<string, string>;
|
|
258
263
|
fonts: Record<"heading" | "body" | "code", string>;
|
|
259
264
|
};
|
|
260
|
-
titleBindings: RenderBinding[];
|
|
261
265
|
contentItems: RenderBinding[];
|
|
262
266
|
blocks: RenderBinding[];
|
|
263
267
|
}
|
|
@@ -265,10 +269,8 @@ export interface ResolvedSlide {
|
|
|
265
269
|
export interface RenderBinding {
|
|
266
270
|
type: string;
|
|
267
271
|
field?: string;
|
|
268
|
-
slot?: string;
|
|
269
272
|
value: unknown;
|
|
270
273
|
path: string;
|
|
271
|
-
placeholderIndex?: number;
|
|
272
274
|
regionKey?: string;
|
|
273
275
|
}
|
|
274
276
|
|
package/dist/svg.d.ts
CHANGED
|
@@ -107,12 +107,17 @@ export interface ToSvgOptions {
|
|
|
107
107
|
* never reads a clock; without it a current date is reported as unresolved content.
|
|
108
108
|
*/
|
|
109
109
|
date?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Stamp drawn elements with their OPF path (`data-opf-path`) and the other `data-opf-*` trace attributes. On a slide with an
|
|
112
|
+
* OPF 0.19 layout template, everything an item of a region draws is grouped in `<g data-opf-region="<name>">`, and each
|
|
113
|
+
* column of a list laid out in columns is a group with `data-opf-list-column`, `data-opf-list-start` and `data-opf-list-end`.
|
|
114
|
+
*/
|
|
110
115
|
trace?: boolean;
|
|
111
116
|
/**
|
|
112
117
|
* Catalogs the host registered (core `Catalog[]`), passed unchanged to core resolution: a reference the document does not
|
|
113
118
|
* embed resolves against the catalog whose `source` its group names, and the first entry is the default catalog for bare ids
|
|
114
119
|
* when the document omits `catalogs.default`. The renderer bundles and fetches no catalog; register the gallery snapshot with
|
|
115
|
-
* `import {
|
|
120
|
+
* `import { gallery } from "@openpresentation/gallery"` and `catalogs: [gallery]`. Omitted: only embedded
|
|
116
121
|
* records resolve, and anything else draws with core's engine defaults (reported as `unresolved-reference`).
|
|
117
122
|
* A malformed value throws core's `OPFCatalogsOptionError` (`code: "invalid-catalogs"`); a reference with an undeclared prefix
|
|
118
123
|
* (`foo:id` without `catalogs.foo`) fails the boundary check (`invalid-opf`, finding `opf/undeclared-catalog`).
|
|
@@ -152,7 +157,6 @@ export interface ResolvedSlide {
|
|
|
152
157
|
colors: Record<string, string>;
|
|
153
158
|
fonts: Record<"heading" | "body" | "code", string>;
|
|
154
159
|
};
|
|
155
|
-
titleBindings: RenderBinding[];
|
|
156
160
|
contentItems: RenderBinding[];
|
|
157
161
|
blocks: RenderBinding[];
|
|
158
162
|
}
|
|
@@ -160,10 +164,8 @@ export interface ResolvedSlide {
|
|
|
160
164
|
export interface RenderBinding {
|
|
161
165
|
type: string;
|
|
162
166
|
field?: string;
|
|
163
|
-
slot?: string;
|
|
164
167
|
value: unknown;
|
|
165
168
|
path: string;
|
|
166
|
-
placeholderIndex?: number;
|
|
167
169
|
regionKey?: string;
|
|
168
170
|
}
|
|
169
171
|
|
package/dist/svg.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ENGINE_DEFAULT_THEME, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_CHART_TYPES
|
|
10
10
|
} from "@openpresentation/opf/composition";
|
|
11
11
|
// The renderer is a library: it never imports a catalog. Hosts register theirs (`options.catalogs`, core `Catalog[]`, for
|
|
12
|
-
// example `defaultCatalog` from `@openpresentation/
|
|
12
|
+
// example `defaultCatalog` from `@openpresentation/gallery`) and core resolves every reference against them.
|
|
13
13
|
import {
|
|
14
14
|
hasContentVariables,
|
|
15
15
|
isTemplate,
|
|
@@ -132,7 +132,6 @@ const PROMOTED_REGION_KEYS = [
|
|
|
132
132
|
"top+middle+bottom:left+center+right"
|
|
133
133
|
];
|
|
134
134
|
|
|
135
|
-
const TITLE_PLACEHOLDERS = new Set(["title", "subtitle", "tag"]);
|
|
136
135
|
|
|
137
136
|
const FIELD_TYPE = Object.freeze({
|
|
138
137
|
text: "text",
|
|
@@ -457,9 +456,9 @@ function reportGlyphFallback(context, note) {
|
|
|
457
456
|
});
|
|
458
457
|
}
|
|
459
458
|
|
|
460
|
-
function contentPayloadFromHost(host, path
|
|
459
|
+
function contentPayloadFromHost(host, path) {
|
|
461
460
|
if (!isPlainObject(host)) {
|
|
462
|
-
return { type: "text", field: "text", value: host,
|
|
461
|
+
return { type: "text", field: "text", value: host, path };
|
|
463
462
|
}
|
|
464
463
|
|
|
465
464
|
for (const field of ROOT_PAYLOAD_FIELDS) {
|
|
@@ -468,7 +467,6 @@ function contentPayloadFromHost(host, path, slot) {
|
|
|
468
467
|
type: host.type ?? FIELD_TYPE[field],
|
|
469
468
|
field,
|
|
470
469
|
value: host[field],
|
|
471
|
-
slot: host.slot ?? slot ?? host.type ?? FIELD_TYPE[field],
|
|
472
470
|
path: `${path}.${field}`
|
|
473
471
|
};
|
|
474
472
|
}
|
|
@@ -478,7 +476,6 @@ function contentPayloadFromHost(host, path, slot) {
|
|
|
478
476
|
type: "text",
|
|
479
477
|
field: "text",
|
|
480
478
|
value: stableJson(host),
|
|
481
|
-
slot: slot ?? "text",
|
|
482
479
|
path
|
|
483
480
|
};
|
|
484
481
|
}
|
|
@@ -491,7 +488,6 @@ function slideRootContent(slide, slidePath) {
|
|
|
491
488
|
type: slide.type ?? FIELD_TYPE[field],
|
|
492
489
|
field,
|
|
493
490
|
value: slide[field],
|
|
494
|
-
slot: slide.type ?? FIELD_TYPE[field],
|
|
495
491
|
path: `${slidePath}.${field}`
|
|
496
492
|
});
|
|
497
493
|
}
|
|
@@ -503,8 +499,8 @@ function promotedRegionContent(slide, slidePath) {
|
|
|
503
499
|
const items = [];
|
|
504
500
|
for (const key of PROMOTED_REGION_KEYS) {
|
|
505
501
|
if (slide[key] !== undefined) {
|
|
506
|
-
const payload = contentPayloadFromHost(slide[key], `${slidePath}.${key}
|
|
507
|
-
items.push({ ...payload,
|
|
502
|
+
const payload = contentPayloadFromHost(slide[key], `${slidePath}.${key}`);
|
|
503
|
+
items.push({ ...payload, regionKey: key });
|
|
508
504
|
}
|
|
509
505
|
}
|
|
510
506
|
return items;
|
|
@@ -512,15 +508,7 @@ function promotedRegionContent(slide, slidePath) {
|
|
|
512
508
|
|
|
513
509
|
function blockContent(slide, slidePath) {
|
|
514
510
|
if (!Array.isArray(slide.blocks)) return [];
|
|
515
|
-
return slide.blocks.map((block, index) => contentPayloadFromHost(block, `${slidePath}.blocks.${index}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function normalizeFutureContent(slide, slidePath) {
|
|
519
|
-
if (!Array.isArray(slide.content)) return [];
|
|
520
|
-
return slide.content.map((item, index) => {
|
|
521
|
-
const payload = contentPayloadFromHost(item, `${slidePath}.content.${index}`, item.slot);
|
|
522
|
-
return { ...payload, slot: item.slot ?? payload.slot, path: `${slidePath}.content.${index}` };
|
|
523
|
-
});
|
|
511
|
+
return slide.blocks.map((block, index) => contentPayloadFromHost(block, `${slidePath}.blocks.${index}`));
|
|
524
512
|
}
|
|
525
513
|
|
|
526
514
|
// The core catalog options every resolution takes: the host's catalogs, unchanged, and its strict reference policy.
|
|
@@ -556,27 +544,7 @@ function bindSlide(presentation, index, context) {
|
|
|
556
544
|
// {{deck.slideCount}} hold this slide's number, its section and the deck's slide count, the same values the furniture draws. Core resolved them.
|
|
557
545
|
const slide = slideContext.slide;
|
|
558
546
|
const layout = slideContext.options.layout;
|
|
559
|
-
const placeholders = Array.isArray(layout?.placeholders) ? layout.placeholders : [];
|
|
560
|
-
const titleBindings = [];
|
|
561
|
-
|
|
562
|
-
for (let i = 0; i < placeholders.length; i += 1) {
|
|
563
|
-
const type = placeholders[i]?.type;
|
|
564
|
-
if (!TITLE_PLACEHOLDERS.has(type)) continue;
|
|
565
|
-
const value = slide[type] ?? presentation[type] ?? "";
|
|
566
|
-
if (value) {
|
|
567
|
-
titleBindings.push({
|
|
568
|
-
type: "text",
|
|
569
|
-
field: type,
|
|
570
|
-
slot: type,
|
|
571
|
-
value,
|
|
572
|
-
placeholderIndex: i,
|
|
573
|
-
path: `${slidePath}.${type}`
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
547
|
const contentItems = [
|
|
579
|
-
...normalizeFutureContent(slide, slidePath),
|
|
580
548
|
...slideRootContent(slide, slidePath),
|
|
581
549
|
...promotedRegionContent(slide, slidePath)
|
|
582
550
|
];
|
|
@@ -638,7 +606,6 @@ function bindSlide(presentation, index, context) {
|
|
|
638
606
|
slide,
|
|
639
607
|
layout,
|
|
640
608
|
design,
|
|
641
|
-
titleBindings,
|
|
642
609
|
contentItems,
|
|
643
610
|
blocks
|
|
644
611
|
};
|
|
@@ -911,10 +878,15 @@ function imageRecolorMatrix(recolor, bound) {
|
|
|
911
878
|
function renderSlideContent(bound, width, height, options) {
|
|
912
879
|
for (const diagnostic of [...bound.design.diagnostics, ...bound.geometry.diagnostics]) reportDiagnostic(diagnostic, options);
|
|
913
880
|
return bound.geometry.items.map(item => {
|
|
881
|
+
// Cards come from core's geometry: item.frameBox is the card core allocated in the item's cell (OPF 0.19: a template
|
|
882
|
+
// region's flow cell; core never gives an item of a bled region a card).
|
|
914
883
|
const frame=item.frameBox;
|
|
915
884
|
const surface=frame ? tag('rect',{...HIDDEN,x:frame.x,y:frame.y,width:frame.width,height:frame.height,rx:8*Math.min(width,height)/720,fill:bound.design.colors.surface,stroke:bound.design.colors.border,...traceAttrs(options,item.path)}) : '';
|
|
916
885
|
// RR-34: a captioned item draws its media in item.box and its caption band after it (src/annotations.js).
|
|
917
|
-
|
|
886
|
+
const drawn=surface+renderPayload(item, item.box, { ...bound, composition: item.composition }, options)+(item.caption?renderCaption(item,{ ...bound, composition: item.composition },options,drawHelpers):'');
|
|
887
|
+
// OPF 0.19 (RR-81): with trace, everything an item of a layout template region draws (card, payload, caption) is
|
|
888
|
+
// grouped under data-opf-region, the region's name (ComposedItem.region). Items outside a template are unchanged.
|
|
889
|
+
return options.trace && item.region !== undefined && drawn ? tag('g',{'data-opf-region':item.region},drawn) : drawn;
|
|
918
890
|
});
|
|
919
891
|
}
|
|
920
892
|
// Draw helpers injected into src/annotations.js (captions and footnote areas).
|
|
@@ -985,8 +957,26 @@ function renderTextPayload(item, box, bound, options) {
|
|
|
985
957
|
}
|
|
986
958
|
|
|
987
959
|
function renderList(item, box, bound, options) {
|
|
960
|
+
// OPF 0.19 (RR-81): a list core laid out in columns (item.listColumns) draws every column's own fit in its own box, and
|
|
961
|
+
// only those: item.text is then the whole list's fit and is never drawn, or the list would draw twice. Core renumbers each
|
|
962
|
+
// column's entries (indexes, paths and markers), so numbering continues across columns. With trace, each column is a
|
|
963
|
+
// group carrying its index, its box and the half-open item range it holds.
|
|
964
|
+
if (Array.isArray(item.listColumns) && item.listColumns.length > 1) {
|
|
965
|
+
const columns=item.listColumns.map((column,index)=>{
|
|
966
|
+
const entries=listEntriesSvg(item,column.text,bound,options).join('\n');
|
|
967
|
+
return options.trace?tag('g',{'data-opf-list-column':index,'data-opf-list-start':column.start,'data-opf-list-end':column.end,
|
|
968
|
+
'data-opf-box-x':column.box.x,'data-opf-box-y':column.box.y,'data-opf-box-width':column.box.width,'data-opf-box-height':column.box.height,
|
|
969
|
+
...(column.text.overflow?{'data-opf-overflow':'true'}:{})},entries):entries;
|
|
970
|
+
});
|
|
971
|
+
return tag('g',{...traceAttrs(options,item.path),...(item.listColumns.some(column=>column.text.overflow)?{'data-opf-overflow':'true'}:{})},columns.join('\n'));
|
|
972
|
+
}
|
|
988
973
|
const scale=Math.min(bound.design.dimensions.width,bound.design.dimensions.height)/720;
|
|
989
974
|
const fit=item.text?.listEntries?item.text:fitList(item.value,box,25*scale,((bound.composition??bound.geometry.composition).minFontSize??16)*scale,{style:{fontFamily:bound.design.fonts.body,fontWeight:400,path:item.path},...(bound.design.fonts.code?{codeFontFamily:bound.design.fonts.code}:{}),textMeasurement:options.textMeasurement,...(item.payload?.numbering!==undefined?{numbering:item.payload.numbering}:{})});
|
|
975
|
+
return tag('g',{...traceAttrs(options,item.path),...(fit.overflow?{'data-opf-overflow':'true'}:{})},listEntriesSvg(item,fit,bound,options).join('\n'));
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// The markers, item text and descriptions of one list fit: a whole list, or one column of a list in columns.
|
|
979
|
+
function listEntriesSvg(item, fit, bound, options) {
|
|
990
980
|
const children=[];
|
|
991
981
|
// design.listBullet=image: core attaches the icon logo as item.bulletImage and its box (entry.bulletBox: 0.65 em, as PowerPoint draws a:buBlip). An icon that cannot be drawn keeps the glyph marker.
|
|
992
982
|
const bullet=item.bulletImage?resolveBulletImage(item.bulletImage,bound,options):undefined;
|
|
@@ -997,7 +987,7 @@ function renderList(item, box, bound, options) {
|
|
|
997
987
|
children.push(renderRichLines(typeof entry.value==='string'?[entry.value]:entry.value,entry.text,entry.textBox,bound,config));
|
|
998
988
|
if(entry.description)children.push(renderRichLines(typeof entry.descriptionValue==='string'?[entry.descriptionValue]:entry.descriptionValue,entry.description,entry.descriptionBox,bound,{...config,path:entry.descriptionPath,rich:Array.isArray(entry.descriptionValue),fill:bound.design.colors.mutedText}));
|
|
999
989
|
}
|
|
1000
|
-
return
|
|
990
|
+
return children;
|
|
1001
991
|
}
|
|
1002
992
|
|
|
1003
993
|
// A numbered list's marker (numbering) draws the number with the weight and slant core measured it at: PowerPoint draws an
|
package/dist/text-paths.js
CHANGED
|
@@ -320,6 +320,8 @@ function precise(value) { return String(Number(Number(value).toPrecision(6))); }
|
|
|
320
320
|
* slide's text with it. The faces are read from the handle when a slide is outlined, so faces it loads later are used too.
|
|
321
321
|
*/
|
|
322
322
|
export function textOutlines(fonts) {
|
|
323
|
+
const shape = fonts.shaper ? harfBuzzShape(fonts.shaper, { clusterLevel: 2 }) : undefined;
|
|
324
|
+
fonts.registry?.setShaper?.(shape ? { shape } : undefined);
|
|
323
325
|
return Object.freeze({
|
|
324
326
|
outlineSlideText: (content, options) => {
|
|
325
327
|
const library = outlineLibrary(fonts);
|
|
@@ -329,7 +331,7 @@ export function textOutlines(fonts) {
|
|
|
329
331
|
// opf-render#188: the vector PDF shapes its runs with the same HarfBuzz. Each character stays with the glyph it became (cluster
|
|
330
332
|
// level 2: a mark, a reordered vowel sign), so the PDF's glyph-to-Unicode map and /ActualText spans give the text as before;
|
|
331
333
|
// the glyphs and positions are the outlines'.
|
|
332
|
-
...(
|
|
334
|
+
...(shape ? { shape } : {}),
|
|
333
335
|
});
|
|
334
336
|
}
|
|
335
337
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"test:forced-colors-browser": "node test/forced-colors-browser.mjs artifacts/forced-colors"
|
|
143
143
|
},
|
|
144
144
|
"dependencies": {
|
|
145
|
-
"@openpresentation/opf": "^0.
|
|
145
|
+
"@openpresentation/opf": "^0.19.0",
|
|
146
146
|
"bidi-js": "1.1.0",
|
|
147
147
|
"fontkit": "^2.0.4",
|
|
148
148
|
"harfbuzzjs": "1.6.3",
|
|
@@ -341,6 +341,7 @@
|
|
|
341
341
|
}
|
|
342
342
|
},
|
|
343
343
|
"devDependencies": {
|
|
344
|
+
"@openpresentation/gallery": "^2.0.0",
|
|
344
345
|
"@expo-google-fonts/arimo": "0.4.3",
|
|
345
346
|
"@expo-google-fonts/caladea": "0.4.2",
|
|
346
347
|
"@expo-google-fonts/cousine": "0.4.3",
|