@openpresentation/opf-render 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -66
- package/dist/deck-runtime.js +4 -4
- package/dist/element.d.ts +12 -9
- package/dist/element.js +14 -12
- package/dist/export-browser.d.ts +21 -11
- package/dist/export-browser.js +33 -20
- package/dist/font-subset.js +1 -1
- package/dist/fonts-browser.d.ts +24 -24
- package/dist/fonts-browser.js +12 -12
- package/dist/fonts-node.d.ts +9 -9
- package/dist/fonts-node.js +4 -4
- package/dist/fonts.d.ts +6 -6
- package/dist/index.d.ts +51 -30
- package/dist/index.js +1 -1
- package/dist/lazy-fonts.js +10 -10
- package/dist/pdf-fonts.js +1 -1
- package/dist/pdf-vector.js +2 -2
- package/dist/pdf.d.ts +2 -2
- package/dist/pdf.js +3 -3
- package/dist/player.d.ts +3 -3
- package/dist/player.js +1 -1
- package/dist/png.d.ts +2 -2
- package/dist/png.js +2 -2
- package/dist/raster.js +33 -28
- package/dist/script-font-pack.js +5 -5
- package/dist/slide-sources.js +47 -0
- package/dist/svg.d.ts +24 -29
- package/dist/svg.js +45 -30
- package/dist/text-paths.js +3 -3
- package/package.json +2 -2
package/dist/svg.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RenderFonts, ScriptFonts } from "./fonts.js";
|
|
2
|
-
import type { Catalog, Finding, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
2
|
+
import type { Catalog, Finding, SlideSelection, UnresolvedReferenceDiagnostic } from "@openpresentation/opf";
|
|
3
|
+
export type { SlideSelection } from "@openpresentation/opf";
|
|
3
4
|
import type { SlideComposition, LayoutDiagnostic, TextMeasurement } from "@openpresentation/opf/composition";
|
|
4
5
|
export declare const packageName = "@openpresentation/opf-render";
|
|
5
6
|
|
|
@@ -49,8 +50,8 @@ export type RenderDiagnostic = LayoutDiagnostic | {
|
|
|
49
50
|
placeholder: "label" | "icon";
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
export interface
|
|
53
|
-
/** Leave out slides marked `hidden: true
|
|
53
|
+
export interface ToSvgOptions {
|
|
54
|
+
/** Leave out slides marked `hidden: true` when drawing the whole deck, as the player does. Default false: one SVG per slide. A slide number or selection draws exactly the slides it names. */
|
|
54
55
|
skipHidden?: boolean;
|
|
55
56
|
/**
|
|
56
57
|
* The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns (from `@openpresentation/opf-render/fonts-node` or
|
|
@@ -58,10 +59,12 @@ export interface RenderSvgOptions {
|
|
|
58
59
|
* core's portable width estimate and the SVG names the design fonts without embedding them.
|
|
59
60
|
*/
|
|
60
61
|
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
62
|
/**
|
|
64
|
-
*
|
|
63
|
+
* How the SVG draws text (RR-74). `"fonts"` (default): each SVG embeds, as `@font-face` data, the faces of `fonts.embeddedFonts` its
|
|
64
|
+
* own text draws (RR-61). `"system"`: no `@font-face` data; the fonts handle still measures and the SVG still names the families,
|
|
65
|
+
* for a host whose page already has the faces (a browser `loadFonts` handle adds them to the document).
|
|
66
|
+
*
|
|
67
|
+
* `"paths"`: every `<text>` drawn as glyph outlines (RR-64): `<use>` of outlines kept once per slide in `<defs>`, so the SVG needs
|
|
65
68
|
* 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
69
|
* faces). Needs the fonts handle `loadFonts()` returns (`text-as-paths-needs-fonts` otherwise). The text is no longer text: it
|
|
67
70
|
* cannot be edited in place. The glyphs are shaped by HarfBuzz (the browser's shaper; the Node handle always, a browser handle
|
|
@@ -70,9 +73,9 @@ export interface RenderSvgOptions {
|
|
|
70
73
|
* A run stays text, reported as `text-as-paths-fallback` with a `reason`, when its font is a colour or bitmap font
|
|
71
74
|
* (`"colour-font"`), its face's fsType restricts embedding (`"restricted"`), or it has no pinned width and HarfBuzz and the
|
|
72
75
|
* layout's measurement disagree on it (`"shaping"`). Use it for thumbnails, previews and portable SVG files, not for an editing
|
|
73
|
-
* surface or the vector PDF (which keeps real text from `<text>` SVG).
|
|
76
|
+
* surface or the vector PDF (which keeps real text from `<text>` SVG).
|
|
74
77
|
*/
|
|
75
|
-
|
|
78
|
+
text?: "fonts" | "system" | "paths";
|
|
76
79
|
/**
|
|
77
80
|
* `false` embeds whole faces (RR-65). By default, with a fonts handle that carries `subsets` (the Node `loadFonts` handle, a browser
|
|
78
81
|
* handle given `subsetWasm`), each face an SVG embeds is cut to the characters the slide draws, keeping every layout feature, so the
|
|
@@ -119,20 +122,8 @@ export interface RenderSvgOptions {
|
|
|
119
122
|
strictReferences?: boolean;
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
fonts?: RenderFonts;
|
|
125
|
-
scale?: number;
|
|
126
|
-
background?: string;
|
|
127
|
-
dpi?: number;
|
|
128
|
-
/** Extra directories of font files to draw with. */
|
|
129
|
-
fontDirs?: string[];
|
|
130
|
-
defaultFontFamily?: string;
|
|
131
|
-
sansSerifFamily?: string;
|
|
132
|
-
monospaceFamily?: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface SvgToPdfOptions extends SvgToPngOptions {}
|
|
125
|
+
/** The Node conversion options (types only: `toPng` and `toPdf` run in Node, or from `/export-browser`). */
|
|
126
|
+
export type { ToPngOptions, ToPdfOptions } from "./index.js";
|
|
136
127
|
|
|
137
128
|
export interface ResolvedPresentation {
|
|
138
129
|
presentation: unknown;
|
|
@@ -185,12 +176,16 @@ export declare class OPFRenderError extends Error {
|
|
|
185
176
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
186
177
|
}
|
|
187
178
|
|
|
188
|
-
export declare function resolvePresentation(input: unknown, options?:
|
|
189
|
-
|
|
190
|
-
/** The SVG of every slide of the deck, in order. */
|
|
191
|
-
export declare function renderSvg(input: unknown, options?: RenderSvgOptions): string[];
|
|
192
|
-
|
|
193
|
-
/** The SVG of one slide of the deck (`index` is zero-based; out of range throws `slide-index-out-of-range`). */
|
|
194
|
-
export declare function renderSlideSvg(input: unknown, index: number, options?: RenderSvgOptions): string;
|
|
179
|
+
export declare function resolvePresentation(input: unknown, options?: ToSvgOptions): ResolvedPresentation;
|
|
195
180
|
|
|
181
|
+
/** An SVG slide the renderer drew: its text, or its bytes. */
|
|
182
|
+
export type SvgSlide = string | Uint8Array;
|
|
196
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The deck as SVG (RR-73). `toSvg(deck)`: every slide, in order (without the hidden ones under `skipHidden`). `toSvg(deck, 3)`: the
|
|
186
|
+
* third slide (slides count from 1; out of range throws `slide-out-of-range`). `toSvg(deck, "1-3")` or `toSvg(deck, [1, 3])`: the
|
|
187
|
+
* selected slides (a malformed selection throws `invalid-slide-selection`). The second argument may be the options instead.
|
|
188
|
+
*/
|
|
189
|
+
export declare function toSvg(input: unknown, options?: ToSvgOptions): string[];
|
|
190
|
+
export declare function toSvg(input: unknown, slide: number, options?: ToSvgOptions): string;
|
|
191
|
+
export declare function toSvg(input: unknown, slides: Exclude<SlideSelection, number>, options?: ToSvgOptions): string[];
|
package/dist/svg.js
CHANGED
|
@@ -24,6 +24,7 @@ import { isDatasetChart, isDatasetTable, renderCatalogChart } from "./charts.js"
|
|
|
24
24
|
import { renderCaption, renderFootnotes } from "./annotations.js";
|
|
25
25
|
import { drawnFaces } from "./drawn-faces.js";
|
|
26
26
|
import { dataUrlFsType, embeddingAllowed } from "./font-fstype.js";
|
|
27
|
+
import { rejectRenamedOptions, selectSlides } from "./slide-sources.js";
|
|
27
28
|
|
|
28
29
|
export const packageName = "@openpresentation/opf-render";
|
|
29
30
|
|
|
@@ -640,22 +641,24 @@ function bindSlide(presentation, index, context) {
|
|
|
640
641
|
}
|
|
641
642
|
|
|
642
643
|
// `options.fonts` is the handle `loadFonts()` returns (or any object with a `textMeasurement` and, to embed faces in the SVG, an
|
|
643
|
-
// `embeddedFonts` list). The renderer reads the two as plain options from here on. `
|
|
644
|
-
//
|
|
645
|
-
// `
|
|
646
|
-
//
|
|
647
|
-
|
|
644
|
+
// `embeddedFonts` list). The renderer reads the two as plain options from here on. `text` (RR-74) says how the SVG draws text:
|
|
645
|
+
// `"fonts"` (default) embeds the faces the slide draws, cut to its glyphs when the handle carries `subsets` (RR-61, RR-65);
|
|
646
|
+
// `"system"` embeds no face and names the families, for a host whose page already holds them; `"paths"` draws glyph outlines with
|
|
647
|
+
// the handle's `outlines` engine, so the SVG needs no font (RR-64). `subsetFonts: false` embeds whole faces.
|
|
648
|
+
const TEXT_MODES = ["fonts", "system", "paths"];
|
|
648
649
|
function normalizeOptions(options) {
|
|
649
|
-
|
|
650
|
+
rejectRenamedOptions(options, "invalid-render-options");
|
|
651
|
+
const { fonts, text = "fonts", subsetFonts, ...rest } = options ?? {};
|
|
652
|
+
if (!TEXT_MODES.includes(text)) throw new OPFRenderError("invalid-render-options", 'text must be "fonts", "system" or "paths".', { option: "text", value: text });
|
|
650
653
|
if (subsetFonts !== undefined && typeof subsetFonts !== "boolean") throw new OPFRenderError("invalid-render-options", "subsetFonts must be true or false.", { option: "subsetFonts" });
|
|
651
|
-
if (
|
|
652
|
-
if (
|
|
653
|
-
return { ...rest, textMeasurement: fonts?.textMeasurement, embeddedFonts:
|
|
654
|
+
if (typeof fonts === "string" || Array.isArray(fonts)) throw new OPFRenderError("invalid-render-options", "Drawing a deck needs the fonts handle loadFonts() returns (from /fonts-node or /fonts-browser): it measures the text. Font folders are read by toPng and toPdf for SVG input.", { option: "fonts" });
|
|
655
|
+
if (text === "paths" && typeof fonts?.outlines?.outlineSlideText !== "function") throw new OPFRenderError("text-as-paths-needs-fonts", 'text: "paths" needs the fonts handle loadFonts() returns (from /fonts-node or /fonts-browser), passed as `fonts`.', { option: "text" });
|
|
656
|
+
return { ...rest, textMeasurement: fonts?.textMeasurement, embeddedFonts: text === "system" ? undefined : fonts?.embeddedFonts, ...(text === "paths" ? { outlines: fonts.outlines } : {}), ...(subsetFonts !== false && fonts?.subsets ? { subsets: fonts.subsets } : {}) };
|
|
654
657
|
}
|
|
655
658
|
|
|
656
659
|
/**
|
|
657
660
|
* Resolve a presentation for drawing: template variables filled, the boundary check passed, and every slide bound to its
|
|
658
|
-
* layout, design, script fonts and composed geometry. `
|
|
661
|
+
* layout, design, script fonts and composed geometry. `toSvg` draws from this.
|
|
659
662
|
*/
|
|
660
663
|
export function resolvePresentation(input, options = {}) {
|
|
661
664
|
return resolveDeck(input, normalizeOptions(options));
|
|
@@ -678,27 +681,24 @@ function resolveDeck(input, options) {
|
|
|
678
681
|
}
|
|
679
682
|
|
|
680
683
|
/**
|
|
681
|
-
* The SVG
|
|
682
|
-
*
|
|
684
|
+
* The deck as SVG (RR-73): `toSvg(deck)` gives every slide, in order, as an array; `toSvg(deck, 3)` the third slide as one string
|
|
685
|
+
* (slides count from 1); `toSvg(deck, "1-3")` (`"2,4"`, `"5-"`, the CLI's --slides syntax, or a list such as `[1, 3]`) the selected slides as an array. The
|
|
686
|
+
* second argument may be the options instead, told apart by type. `skipHidden: true` leaves slides marked `hidden` out of the
|
|
687
|
+
* whole deck (the sequence the player presents); a selection draws exactly the slides it names.
|
|
683
688
|
*/
|
|
684
|
-
export function
|
|
685
|
-
|
|
689
|
+
export function toSvg(input, slides, options) {
|
|
690
|
+
if (slides !== null && typeof slides === "object" && !Array.isArray(slides)) { options = slides; slides = undefined; }
|
|
691
|
+
const drawn = normalizeOptions(options ?? {});
|
|
686
692
|
const resolved = resolveDeck(input, drawn);
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/** The SVG of one slide of the deck (a zero-based index). */
|
|
692
|
-
export function renderSlideSvg(input, index, options = {}) {
|
|
693
|
-
const drawn = normalizeOptions(options);
|
|
694
|
-
const resolved = resolveDeck(input, drawn);
|
|
695
|
-
if (!Number.isInteger(index) || index < 0 || index >= resolved.slides.length) {
|
|
696
|
-
throw new OPFRenderError("slide-index-out-of-range", `Slide index ${index} is out of range.`, {
|
|
697
|
-
slideIndex: index,
|
|
698
|
-
slideCount: resolved.slides.length
|
|
699
|
-
});
|
|
693
|
+
const count = resolved.slides.length;
|
|
694
|
+
if (typeof slides === "number") {
|
|
695
|
+
if (!Number.isInteger(slides) || slides < 1 || slides > count) throw new OPFRenderError("slide-out-of-range", `Slide ${slides} is not in the presentation, which has ${count} slide${count === 1 ? "" : "s"} (slides count from 1).`, { slide: slides, slideCount: count });
|
|
696
|
+
return renderResolvedSlide(resolved, slides - 1, drawn);
|
|
700
697
|
}
|
|
701
|
-
|
|
698
|
+
const numbers = slides === undefined
|
|
699
|
+
? resolved.slides.map((_, index) => index + 1).filter(number => !(options?.skipHidden === true && resolved.presentation.slides[number - 1]?.hidden === true))
|
|
700
|
+
: selectSlides(slides, count);
|
|
701
|
+
return numbers.map(number => renderResolvedSlide(resolved, number - 1, drawn));
|
|
702
702
|
}
|
|
703
703
|
|
|
704
704
|
function renderResolvedSlide(resolved, slideIndex, options) {
|
|
@@ -1697,12 +1697,27 @@ function renderBranding(bound,presentation,width,height,options) {
|
|
|
1697
1697
|
// The group keeps its bare opacity attribute (opf-pptx's watermark parity test reads it); the picture itself is hidden.
|
|
1698
1698
|
pieces.push(tag('g',{opacity:typeof design.watermark==='object'?design.watermark.opacity??.08:.08},renderImage({value:design.watermark,path:rootFor('watermark')},{x:width*.3,y:height*.3,width:width*.4,height:height*.4},bound,{...options,imageFit:'contain',imageDecorative:true})));
|
|
1699
1699
|
}
|
|
1700
|
-
// Cover and section slides: the
|
|
1700
|
+
// Cover and section slides: the logo core composed (the primary organization's full logo, or the design.logo override, in the
|
|
1701
|
+
// variant for this slide's background) at the top-left of the free area, anchored left. RR-71: the picture is named by its
|
|
1702
|
+
// asset's alt text, else by the organization it belongs to ("Acme Corp logo"); an unnamed organization still says "Logo".
|
|
1701
1703
|
const logo=bound.geometry.logo;
|
|
1702
|
-
if(logo)pieces.push(renderImage({value:logo.source,path:logo.path},logo.box,bound,{...options,imageAnchor:logo.anchor??'left',imageLabel:
|
|
1704
|
+
if(logo)pieces.push(renderImage({value:logo.source,path:logo.path},logo.box,bound,{...options,imageAnchor:logo.anchor??'left',imageLabel:logoLabel(presentation,logo.path),imageGenerated:true}));
|
|
1703
1705
|
return pieces.join('');
|
|
1704
1706
|
}
|
|
1705
1707
|
|
|
1708
|
+
// The organization a logo path (`organization.logo.full`, `organization.1.logo.icon.onDark`) belongs to: the record at the index the path
|
|
1709
|
+
// names, else the single organization object.
|
|
1710
|
+
function logoOrganization(presentation,path){
|
|
1711
|
+
const match=/^organization(?:\.(\d+))?\.logo(?:\.|$)/.exec(path??'');
|
|
1712
|
+
if(!match)return undefined;
|
|
1713
|
+
const organization=presentation?.organization;
|
|
1714
|
+
return Array.isArray(organization)?organization[Number(match[1])]:match[1]===undefined?organization:undefined;
|
|
1715
|
+
}
|
|
1716
|
+
function logoLabel(presentation,path){
|
|
1717
|
+
const name=logoOrganization(presentation,path)?.name;
|
|
1718
|
+
return typeof name==='string'&&name.trim()?`${name.trim()} logo`:'Logo';
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1706
1721
|
// A family name is written unquoted only when it is a valid sequence of CSS identifiers; a word that starts with a digit
|
|
1707
1722
|
// ("Source Sans 3", "Noto Sans Symbols 2") or any other non-identifier character makes an unquoted name invalid CSS, which
|
|
1708
1723
|
// drops the whole font-family in a browser (FF-45). Such names are single-quoted (the attribute is double-quoted).
|
package/dist/text-paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// RR-64: `
|
|
1
|
+
// RR-64: `text: "paths"` draws every <text> of a slide as glyph outlines, so the SVG needs no font. The renderer has
|
|
2
2
|
// already decided every line, position and width (x, y, text-anchor, textLength); the vector PDF export's SVG text layout
|
|
3
3
|
// (layoutText: text chunks, bidirectional runs, per-character fallback, fontkit shaping, textLength) turns that into positioned
|
|
4
4
|
// glyph runs, and this module writes each glyph as a <use> of its outline. Outlines are kept once per slide in <defs>, in font
|
|
@@ -316,14 +316,14 @@ function precise(value) { return String(Number(Number(value).toPrecision(6))); }
|
|
|
316
316
|
|
|
317
317
|
|
|
318
318
|
/**
|
|
319
|
-
* The outline engine a fonts handle carries as `outlines` (RR-64): `
|
|
319
|
+
* The outline engine a fonts handle carries as `outlines` (RR-64): `toSvg(deck, { fonts, text: "paths" })` draws each
|
|
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
323
|
return Object.freeze({
|
|
324
324
|
outlineSlideText: (content, options) => {
|
|
325
325
|
const library = outlineLibrary(fonts);
|
|
326
|
-
if (!library) throw options.fail("text-as-paths-needs-fonts", "
|
|
326
|
+
if (!library) throw options.fail("text-as-paths-needs-fonts", 'text: "paths" needs font faces to outline: pass the handle loadFonts() returns as `fonts`.');
|
|
327
327
|
return outlineSlideText(content, { ...options, library, shaper: fonts.shaper });
|
|
328
328
|
},
|
|
329
329
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-render",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"test:preview-a11y-browser": "node test/preview-a11y-browser.mjs"
|
|
140
140
|
},
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@openpresentation/opf": "^0.
|
|
142
|
+
"@openpresentation/opf": "^0.18.1",
|
|
143
143
|
"bidi-js": "1.1.0",
|
|
144
144
|
"fontkit": "^2.0.4",
|
|
145
145
|
"harfbuzzjs": "1.6.2",
|