@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/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 RenderSvgOptions {
53
- /** Leave out slides marked `hidden: true`, as the player does. Default false: one SVG per slide. With true the result holds only the visible slides, in order, so an index no longer names the slide at that index. Read by the deck-level `renderSvg`; `renderSlideSvg` draws the slide it is given. */
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
- * `true` draws every `<text>` as glyph outlines (RR-64): `<use>` of outlines kept once per slide in `<defs>`, so the SVG needs
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). Default false.
76
+ * surface or the vector PDF (which keeps real text from `<text>` SVG).
74
77
  */
75
- textAsPaths?: boolean;
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
- export interface SvgToPngOptions {
123
- /** The fonts the conversion draws with (Node): see `SvgToPngOptions` in the main entry. */
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?: RenderSvgOptions): ResolvedPresentation;
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. `embedFonts: false` (RR-61) keeps the
644
- // measurement and drops the faces, for a host whose page already holds them.
645
- // `textAsPaths: true` (RR-64) draws text as glyph outlines with the handle's `outlines` engine (a `loadFonts` handle carries one).
646
- // `subsetFonts: false` (RR-65) embeds whole faces; by default a handle with a subset engine (`fonts.subsets`) cuts each embedded
647
- // face to the characters the slide draws.
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
- const { fonts, embedFonts, textAsPaths, subsetFonts, ...rest } = options ?? {};
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 (textAsPaths !== undefined && typeof textAsPaths !== "boolean") throw new OPFRenderError("invalid-render-options", "textAsPaths must be true or false.", { option: "textAsPaths" });
652
- if (textAsPaths && typeof fonts?.outlines?.outlineSlideText !== "function") throw new OPFRenderError("text-as-paths-needs-fonts", "textAsPaths needs the fonts handle loadFonts() returns (from /fonts-node or /fonts-browser), passed as `fonts`.", { option: "textAsPaths" });
653
- return { ...rest, textMeasurement: fonts?.textMeasurement, embeddedFonts: embedFonts === false ? undefined : fonts?.embeddedFonts, ...(textAsPaths ? { outlines: fonts.outlines } : {}), ...(subsetFonts !== false && fonts?.subsets ? { subsets: fonts.subsets } : {}) };
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. `renderSvg` and `renderSlideSvg` draw from this.
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 of every slide of the deck, in order. `skipHidden: true` leaves out slides marked `hidden` (the sequence the player
682
- * presents); the result is then shorter than the deck, so an index no longer names the slide at that index.
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 renderSvg(input, options = {}) {
685
- const drawn = normalizeOptions(options);
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 indexes = resolved.slides.map((_, index) => index).filter(index => !(options.skipHidden === true && resolved.presentation.slides[index]?.hidden === true));
688
- return indexes.map(index => renderResolvedSlide(resolved, index, drawn));
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
- return renderResolvedSlide(resolved, index, drawn);
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 deck logo core composed at the top-left of the free area, anchored left.
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:'Logo',imageGenerated:true}));
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).
@@ -1,4 +1,4 @@
1
- // RR-64: `textAsPaths: true` draws every <text> of a slide as glyph outlines, so the SVG needs no font. The renderer has
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): `renderSvg(deck, { fonts, textAsPaths: true })` draws each
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", "textAsPaths needs font faces to outline: pass the handle loadFonts() returns as `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.17.1",
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.17.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",