@openpresentation/opf-render 0.17.1 → 0.18.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 +157 -70
- package/dist/deck-runtime.js +4 -4
- package/dist/element.d.ts +12 -9
- package/dist/element.js +28 -19
- package/dist/export-browser.d.ts +21 -11
- package/dist/export-browser.js +35 -20
- package/dist/font-manifest.js +17 -17
- package/dist/font-registry.js +31 -0
- package/dist/font-subset.js +1 -1
- package/dist/fonts-browser.d.ts +26 -25
- package/dist/fonts-browser.js +12 -12
- package/dist/fonts-node.d.ts +15 -11
- package/dist/fonts-node.js +24 -21
- package/dist/fonts.d.ts +14 -7
- package/dist/hb-shape.js +16 -6
- package/dist/index.d.ts +51 -30
- package/dist/index.js +1 -1
- package/dist/lazy-fonts.js +10 -10
- package/dist/node-resolve.js +24 -0
- package/dist/pdf-fonts.js +1 -1
- package/dist/pdf-text.js +4 -4
- package/dist/pdf-vector.js +15 -5
- package/dist/pdf-writer.js +5 -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 +9 -3
- package/dist/png.d.ts +2 -2
- package/dist/png.js +2 -2
- package/dist/preview-fonts-node.js +3 -6
- package/dist/raster-text.js +33 -2
- package/dist/raster.js +47 -38
- package/dist/script-font-pack.js +5 -5
- package/dist/script-fonts.js +4 -2
- package/dist/slide-sources.js +47 -0
- package/dist/svg.d.ts +24 -29
- package/dist/svg.js +50 -31
- package/dist/text-paths.js +16 -4
- package/package.json +23 -20
package/dist/deck-runtime.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// RR-28: the part of the slideshow player and the <opf-deck> element that has no DOM of its own. It reads an OPF document,
|
|
2
|
-
// decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (
|
|
2
|
+
// decides which slides play (hidden ones do not), renders each slide once with the renderer's own SVG output (toSvg:
|
|
3
3
|
// there is no second layout engine) behind a face-level font gate, and synchronizes windows over a BroadcastChannel.
|
|
4
4
|
// Nothing here fetches anything but the font files the host's font root serves.
|
|
5
5
|
import { resolveSlideVariables } from "@openpresentation/opf";
|
|
6
|
-
import {
|
|
6
|
+
import { toSvg } from "./svg.js";
|
|
7
7
|
|
|
8
8
|
/** Error raised for a deck that cannot be read, rendered or loaded. */
|
|
9
9
|
export class DeckError extends Error {
|
|
@@ -206,7 +206,7 @@ export function createFontGate(fonts) {
|
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* One deck's rendering, shared by the element, the full-screen player and the speaker view: font loading once, each slide
|
|
209
|
-
* rendered once by `
|
|
209
|
+
* rendered once by `toSvg` and kept, so moving between slides never draws twice and every surface shows the same markup.
|
|
210
210
|
*
|
|
211
211
|
* `fonts` is a browser fonts handle (or nothing: layout then uses estimated widths and the visitor's system sans-serif).
|
|
212
212
|
*/
|
|
@@ -237,7 +237,7 @@ export function createDeckStore({ document, fonts, renderOptions = {}, date } =
|
|
|
237
237
|
let svg = cache.get(index);
|
|
238
238
|
if (svg === undefined) {
|
|
239
239
|
try {
|
|
240
|
-
svg =
|
|
240
|
+
svg = toSvg(deck, index + 1, { ...options(), validate: !validated });
|
|
241
241
|
} catch (cause) {
|
|
242
242
|
if (cause instanceof DeckError) throw cause;
|
|
243
243
|
// A document the schema rejects says where: the first finding's path and message.
|
package/dist/element.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BrowserFontsHandle } from "./fonts-browser.js";
|
|
2
2
|
import type { RenderFonts } from "./fonts.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { SlideSelection, ToSvgOptions } from "./svg.js";
|
|
4
4
|
import type { PlayerSession, PresentOptions, PlayerSlideChange } from "./player.js";
|
|
5
5
|
export { DeckError } from "./player.js";
|
|
6
6
|
export { loadPreviewFonts, previewBaseFaces, previewFontLayout } from "./preview-fonts.js";
|
|
@@ -43,8 +43,8 @@ export interface OpfDeckElement extends HTMLElement {
|
|
|
43
43
|
* attribute's URL (an empty string without either).
|
|
44
44
|
*/
|
|
45
45
|
fonts: string | BrowserFontsHandle;
|
|
46
|
-
/** Extra `
|
|
47
|
-
renderOptions: Partial<
|
|
46
|
+
/** Extra `toSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
|
|
47
|
+
renderOptions: Partial<ToSvgOptions>;
|
|
48
48
|
includeHidden: boolean;
|
|
49
49
|
thumbnails: boolean;
|
|
50
50
|
readonly ready: Promise<void>;
|
|
@@ -66,10 +66,8 @@ export declare function getOpfDeckElement(): { new (): OpfDeckElement; readonly
|
|
|
66
66
|
/** Register `<opf-deck>` (or another tag name). Safe to call twice and on a server, where it does nothing. */
|
|
67
67
|
export declare function defineOpfDeck(tagName?: string): CustomElementConstructor | undefined;
|
|
68
68
|
|
|
69
|
-
export interface
|
|
69
|
+
export interface ToHtmlOptions {
|
|
70
70
|
tagName?: string;
|
|
71
|
-
/** `"first"` (default: the first slide and a list of titles), `"all"`, or 1-based slide numbers. */
|
|
72
|
-
slides?: "first" | "all" | readonly number[];
|
|
73
71
|
includeHidden?: boolean;
|
|
74
72
|
src?: string;
|
|
75
73
|
slide?: number;
|
|
@@ -86,11 +84,16 @@ export interface RenderDeckHtmlOptions {
|
|
|
86
84
|
embed?: boolean;
|
|
87
85
|
/** Today's date (ISO) for `date: true` furniture. */
|
|
88
86
|
date?: string;
|
|
89
|
-
renderOptions?: Partial<
|
|
87
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
90
88
|
}
|
|
91
89
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Server-side markup (RR-73): the deck's slides as inline SVG inside the tag, shown as is without JavaScript and replaced when the
|
|
92
|
+
* element upgrades. `toHtml(deck)` draws the first slide and a list of titles; `toHtml(deck, 3)`, `toHtml(deck, "1-3")` (`"1-"` for
|
|
93
|
+
* every slide) or `toHtml(deck, [1, 3])` draw those slides of the presented sequence, counting from 1.
|
|
94
|
+
*/
|
|
95
|
+
export declare function toHtml(document: unknown, options?: ToHtmlOptions): string;
|
|
96
|
+
export declare function toHtml(document: unknown, slides: SlideSelection, options?: ToHtmlOptions): string;
|
|
94
97
|
|
|
95
98
|
declare global {
|
|
96
99
|
interface HTMLElementTagNameMap { "opf-deck": OpfDeckElement }
|
package/dist/element.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// RR-28: <opf-deck>, a framework-free web component that embeds an OPF deck. It draws the renderer's own SVG (
|
|
1
|
+
// RR-28: <opf-deck>, a framework-free web component that embeds an OPF deck. It draws the renderer's own SVG (toSvg), so
|
|
2
2
|
// the slide a page shows is the slide the preview, the editor and the PDF show. Importing this module touches no DOM, so it
|
|
3
3
|
// is safe on a server (Next.js); `defineOpfDeck()` registers the tag in a browser.
|
|
4
4
|
import {
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
prepareSlideSvg, sanitizeSvgTree, slideInfo, slideLabel, svgDimensions, plainText,
|
|
7
7
|
} from "./deck-runtime.js";
|
|
8
8
|
import { loadPreviewFonts } from "./preview-fonts.js";
|
|
9
|
-
import {
|
|
9
|
+
import { toSvg } from "./svg.js";
|
|
10
10
|
import { drawnFaces } from "./drawn-faces.js";
|
|
11
|
+
import { selectSlides, slideArguments } from "./slide-sources.js";
|
|
11
12
|
|
|
12
13
|
export { DeckError } from "./deck-runtime.js";
|
|
13
14
|
export { loadPreviewFonts, previewBaseFaces, previewFontLayout } from "./preview-fonts.js";
|
|
@@ -36,17 +37,22 @@ button[aria-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
|
36
37
|
.counter { min-width: 4.5em; text-align: center; font-variant-numeric: tabular-nums; }
|
|
37
38
|
.thumbs-wrap { min-width: 0; }
|
|
38
39
|
.thumbs { position: relative; display: flex; gap: 8px; margin: 0; padding: 4px 2px; list-style: none; overflow-x: auto; scroll-snap-type: x proximity; }
|
|
39
|
-
.thumbs li { flex: 0 0 auto; width: 132px; scroll-snap-align: center; }
|
|
40
|
-
.
|
|
40
|
+
.thumbs li { position: relative; flex: 0 0 auto; width: 132px; scroll-snap-align: center; }
|
|
41
|
+
.thumbs .frame { display: block; box-sizing: content-box; aspect-ratio: var(--ratio, 16 / 9); overflow: hidden; border: 1px solid transparent; border-radius: var(--opf-deck-radius, 6px); background: var(--opf-deck-stage, #f1f1f1); }
|
|
42
|
+
.thumbs .frame svg.opf-slide { display: block; width: 100%; height: 100%; pointer-events: none; }
|
|
43
|
+
.thumb, .thumbs .thumb:hover { position: absolute; inset: 0; width: 100%; height: 100%; padding: 0; min-height: 0; background: transparent; }
|
|
41
44
|
.thumb[aria-current="true"] { outline: 3px solid var(--opf-deck-accent, Highlight); outline-offset: 0; }
|
|
42
|
-
.thumb .frame { display: block; width: 100%; aspect-ratio: var(--ratio, 16 / 9); }
|
|
43
|
-
.thumb svg.opf-slide { display: block; width: 100%; height: 100%; pointer-events: none; }
|
|
44
45
|
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
45
46
|
.flip { display: inline-block; }
|
|
46
47
|
.rtl .flip { transform: scaleX(-1); }
|
|
47
48
|
.rtl .bar, .rtl .thumbs { direction: rtl; }
|
|
48
49
|
@media (prefers-reduced-motion: no-preference) { button { transition: background-color 120ms; } }
|
|
49
|
-
@media (forced-colors: active) {
|
|
50
|
+
@media (forced-colors: active) {
|
|
51
|
+
.viewport, button, .thumb[aria-current="true"] { border-color: CanvasText; }
|
|
52
|
+
.thumb[aria-current="true"] { outline-color: Highlight; }
|
|
53
|
+
/* A disabled control is drawn in GrayText at full opacity: fading would blend its frame and its focus ring into Canvas. */
|
|
54
|
+
button[aria-disabled="true"] { opacity: 1; color: GrayText; border-color: GrayText; }
|
|
55
|
+
}
|
|
50
56
|
`;
|
|
51
57
|
|
|
52
58
|
const supportsCustomElements = () => typeof customElements !== "undefined" && typeof HTMLElement !== "undefined";
|
|
@@ -140,7 +146,7 @@ function createElementClass() {
|
|
|
140
146
|
if (had && before === this.getAttribute("fonts") && this.isConnected && this.#built) this.#start();
|
|
141
147
|
}
|
|
142
148
|
|
|
143
|
-
/** Extra `
|
|
149
|
+
/** Extra `toSvg` options (catalogs, imageResolver, date, ...). Set before the deck loads, or call `reload()`. */
|
|
144
150
|
get renderOptions() { return this.#renderOptions; }
|
|
145
151
|
set renderOptions(value) { this.#renderOptions = value && typeof value === "object" ? value : {}; }
|
|
146
152
|
|
|
@@ -455,13 +461,15 @@ function createElementClass() {
|
|
|
455
461
|
button.setAttribute("part", "thumbnail");
|
|
456
462
|
button.setAttribute("aria-label", `Go to ${slideLabel(info, position + 1, sequence.length)}`);
|
|
457
463
|
button.tabIndex = position === cursor.position ? 0 : -1;
|
|
464
|
+
// The drawing is a sibling under the button, not its content: the slide's own text is not the button's visible
|
|
465
|
+
// label (axe label-content-name-mismatch, WCAG 2.5.3), and the button's name is the slide's label. The button
|
|
466
|
+
// stays the first child (#markThumbnail) and covers the drawing (CSS), so it takes every click.
|
|
458
467
|
const frame = doc.createElement("span");
|
|
459
468
|
frame.className = "frame";
|
|
460
469
|
frame.style.setProperty("--ratio", `${first.width} / ${first.height}`);
|
|
461
470
|
frame.setAttribute("aria-hidden", "true");
|
|
462
|
-
button.append(frame);
|
|
463
471
|
button.addEventListener("click", () => { this.#announce = true; this.#go(() => this.#cursor?.goto(position + 1)); });
|
|
464
|
-
li.append(button);
|
|
472
|
+
li.append(button, frame);
|
|
465
473
|
li._frame = frame; li._index = index;
|
|
466
474
|
return li;
|
|
467
475
|
});
|
|
@@ -513,21 +521,22 @@ const escapeHtml = (value) => String(value).replace(/&/g, "&").replace(/</g,
|
|
|
513
521
|
* `/fonts-browser`, or one with a `textMeasurement` and `embeddedFonts`). A `fonts` string is the font root URL written to the
|
|
514
522
|
* element's `fonts` attribute.
|
|
515
523
|
*
|
|
516
|
-
* `
|
|
517
|
-
* `
|
|
524
|
+
* `toHtml(deck)` draws the first slide plus a list of titles; `toHtml(deck, 3)`, `toHtml(deck, "1-3")` (`"1-"` for every slide) or
|
|
525
|
+
* `toHtml(deck, [1, 3])` draw the selected slides of the presented sequence (counting from 1); the second argument may be the
|
|
526
|
+
* options instead, told apart by type (RR-73). `embed` puts the document in an `application/opf+json` script child, so the element upgrades without a request.
|
|
518
527
|
* `fontMode`: `"standalone"` (default, embeds per slide), `"shared"` (one set of font rules in the returned tag), or
|
|
519
528
|
* `"external"` (no font rules; the host must supply CSS with the exact pinned faces the handle measures).
|
|
520
529
|
*/
|
|
521
|
-
export function
|
|
530
|
+
export function toHtml(input, slides, options) {
|
|
531
|
+
({ slides, options } = slideArguments(slides, options));
|
|
522
532
|
const deck = parseDeckDocument(input);
|
|
523
533
|
const tag = options.tagName ?? OPF_DECK_TAG;
|
|
524
534
|
const sequence = presentableIndexes(deck, { includeHidden: options.includeHidden });
|
|
525
|
-
const
|
|
526
|
-
const shown = wanted === "all" ? sequence.map((_, position) => position) : wanted === "first" ? [0] : wanted.map((number) => number - 1).filter((position) => position >= 0 && position < sequence.length);
|
|
535
|
+
const shown = slides === undefined ? [0] : selectSlides(slides, sequence.length).map((number) => number - 1);
|
|
527
536
|
const handle = options.fonts !== null && typeof options.fonts === "object" ? options.fonts : undefined;
|
|
528
537
|
const fontMode = options.fontMode ?? "standalone";
|
|
529
538
|
if (!["standalone", "shared", "external"].includes(fontMode)) throw new DeckError("invalid-font-mode", "Choose standalone, shared or external SSR fonts.");
|
|
530
|
-
// Every mode measures with the same handle; external writes no @font-face data (
|
|
539
|
+
// Every mode measures with the same handle; external writes no @font-face data (`text: "system"`).
|
|
531
540
|
const renderFonts = handle ?? options.renderOptions?.fonts;
|
|
532
541
|
const sharedRules = new Set(), sharedCharacters = new Map();
|
|
533
542
|
const attributes = { src: options.src, slide: options.slide, fonts: handle ? undefined : options.fonts, label: options.label, ...options.attributes };
|
|
@@ -538,11 +547,11 @@ export function renderDeckHtml(input, options = {}) {
|
|
|
538
547
|
const index = sequence[position];
|
|
539
548
|
// Shared rules are deduplicated across slides, so the slides embed whole faces and the shared set is cut below to what
|
|
540
549
|
// every slide draws (RR-65); per-slide subsets would differ and repeat each face once per slide.
|
|
541
|
-
let svg =
|
|
550
|
+
let svg = toSvg(deck, index + 1, { ...options.renderOptions, fonts: renderFonts, ...(fontMode === "external" ? { text: "system" } : {}), ...(fontMode === "shared" ? { subsetFonts: false } : {}), ...(date ? { date } : {}) });
|
|
542
551
|
if (fontMode === "shared") {
|
|
543
552
|
drawnFaces(svg, sharedCharacters);
|
|
544
553
|
svg = svg.replace(/<style>(@font-face[\s\S]*?)<\/style>/g, (_, css) => {
|
|
545
|
-
// These rules are produced by
|
|
554
|
+
// These rules are produced by toSvg after its used-face selection and validation.
|
|
546
555
|
for (const rule of css.split("\n")) sharedRules.add(rule);
|
|
547
556
|
return "";
|
|
548
557
|
});
|
|
@@ -552,7 +561,7 @@ export function renderDeckHtml(input, options = {}) {
|
|
|
552
561
|
const label = slideLabel(slideInfo(deck, index), position + 1, sequence.length);
|
|
553
562
|
return `<figure role="group" aria-roledescription="slide" aria-label="${escapeHtml(label)}" style="margin:0 0 8px">${prepared}</figure>`;
|
|
554
563
|
});
|
|
555
|
-
const titles =
|
|
564
|
+
const titles = slides === undefined && sequence.length > 1
|
|
556
565
|
? `<nav aria-label="Slides"><ol>${sequence.map((index) => `<li>${escapeHtml(slideInfo(deck, index).title)}</li>`).join("")}</ol></nav>` : "";
|
|
557
566
|
const embedded = options.embed ? `<script type="application/opf+json">${JSON.stringify(deck).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}</script>` : "";
|
|
558
567
|
const subsets = options.renderOptions?.subsetFonts === false ? undefined : renderFonts?.subsets;
|
package/dist/export-browser.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { PdfDiagnostic, PdfMetadata,
|
|
1
|
+
import type { PdfDiagnostic, PdfMetadata, SlideSelection, SvgSlide, ToPdfOptions, ToSvgOptions } from "./index.js";
|
|
2
2
|
import type { FontsHandle } from "./fonts.js";
|
|
3
3
|
export type { PdfDiagnostic, PdfMetadata } from "./index.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/** The options of `toPng` in a browser: the canvas's own, and the `toSvg` options that draw a deck (`fonts` is the browser handle). */
|
|
6
|
+
export interface BrowserToPngOptions extends ToSvgOptions {
|
|
6
7
|
/** Multiplies the SVG's own size (default 1). The result is limited to 40 megapixels. */
|
|
7
8
|
scale?: number;
|
|
8
9
|
/** A CSS colour painted first (default white); `"transparent"` keeps the alpha channel. */
|
|
@@ -10,18 +11,19 @@ export interface BrowserSvgToPngOptions {
|
|
|
10
11
|
signal?: AbortSignal;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
/** The options of `toPdf` in a browser: the PDF's own, and the `toSvg` options that draw a deck. */
|
|
15
|
+
export interface BrowserToPdfOptions extends Omit<ToSvgOptions, "fonts" | "onDiagnostic">, Pick<ToPdfOptions, "raster" | "background" | "serifFamily" | "metadata" | "tagged" | "compress" | "strict" | "rasterFallbackScale" | "defaultFontFamily" | "sansSerifFamily" | "monospaceFamily" | "onDiagnostic"> {
|
|
16
|
+
/** `raster: true`: pixel density of each page image (default 2). */
|
|
15
17
|
scale?: number;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
18
|
-
* bundle of it holds no PDF library. Without it raster
|
|
19
|
-
* when the page cannot. The default vector
|
|
19
|
+
* `raster: true` only: the pdf-lib module (`import * as pdfLib from "pdf-lib"`), an optional peer of the renderer that this entry never imports itself, so a
|
|
20
|
+
* bundle of it holds no PDF library. Without it a raster PDF tries to import "pdf-lib" by name and rejects with `OPFRenderError` `converter-missing`
|
|
21
|
+
* when the page cannot. The default vector PDF needs no option.
|
|
20
22
|
*/
|
|
21
23
|
pdfLib?: { PDFDocument: { create(options?: { updateMetadata?: boolean }): Promise<any> } };
|
|
22
24
|
/**
|
|
23
|
-
* The browser fonts handle (`loadFonts` from `/fonts-browser`): every face it holds can be embedded in the PDF, including
|
|
24
|
-
* a standalone SVG does not carry. Faces an SVG embeds as `@font-face` data (the `embeddedFonts` of the handle `
|
|
25
|
+
* The browser fonts handle (`loadFonts` from `/fonts-browser`): it draws a deck, and every face it holds can be embedded in the PDF, including
|
|
26
|
+
* script faces a standalone SVG does not carry. Faces an SVG embeds as `@font-face` data (the `embeddedFonts` of the handle `toSvg` was given) need no entry.
|
|
25
27
|
*/
|
|
26
28
|
fonts?: FontsHandle;
|
|
27
29
|
/** Extra font faces as bytes (TrueType outlines) when there is no handle. */
|
|
@@ -32,7 +34,15 @@ export interface BrowserSvgToPdfOptions extends Pick<SvgToPdfOptions, "mode" | "
|
|
|
32
34
|
onProgress?: (progress: { page: number; pages: number }) => void;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
export declare function
|
|
37
|
+
/** PNG with a canvas (RR-73): the arguments and results of the Node `toPng`. */
|
|
38
|
+
export declare function toPng(svg: Uint8Array | `<${string}`, options?: BrowserToPngOptions): Promise<Uint8Array>;
|
|
39
|
+
export declare function toPng(svgs: readonly SvgSlide[], options?: BrowserToPngOptions): Promise<Uint8Array[]>;
|
|
40
|
+
export declare function toPng(deck: unknown, slide: number, options?: BrowserToPngOptions): Promise<Uint8Array>;
|
|
41
|
+
export declare function toPng(deck: unknown, slides: Exclude<SlideSelection, number>, options?: BrowserToPngOptions): Promise<Uint8Array[]>;
|
|
42
|
+
export declare function toPng(deck: object, options?: BrowserToPngOptions): Promise<Uint8Array[]>;
|
|
43
|
+
export declare function toPng(source: string, options?: BrowserToPngOptions): Promise<Uint8Array | Uint8Array[]>;
|
|
44
|
+
/** One PDF (RR-73), a page per slide, of a deck drawn with `toSvg` (every slide or a selection) or of SVG slides. */
|
|
45
|
+
export declare function toPdf(source: unknown, options?: BrowserToPdfOptions): Promise<Uint8Array>;
|
|
46
|
+
export declare function toPdf(deck: unknown, slides: SlideSelection, options?: BrowserToPdfOptions): Promise<Uint8Array>;
|
|
37
47
|
/** Format of an embedded picture and, for a JPEG, its size, component count and EXIF orientation. */
|
|
38
48
|
export declare function sniffImage(bytes: Uint8Array): { format: "png" | "webp" | "gif" | "jpeg" | undefined; width?: number; height?: number; channels?: number; orientation?: number; space?: string };
|
package/dist/export-browser.js
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { svgsToVectorPdf } from "./pdf-vector.js";
|
|
2
|
-
import { OPFRenderError, packageName } from "./svg.js";
|
|
2
|
+
import { OPFRenderError, packageName, toSvg } from "./svg.js";
|
|
3
|
+
import { slideArguments, slideSources } from "./slide-sources.js";
|
|
3
4
|
|
|
4
|
-
// RR-23: PNG and PDF output in a browser. `
|
|
5
|
+
// RR-23: PNG and PDF output in a browser. `toPng` and `toPdf` of the root entry need Node (resvg, sharp); this entry
|
|
5
6
|
// has the same two names for a page. It uses the same vector PDF converter as Node (fontkit, bidi-js and pako, all pure
|
|
6
7
|
// JavaScript) over the SVG the preview draws, so the text stays real text in the same embedded subsets. What differs from
|
|
7
8
|
// Node is only how pixels are made: a canvas decodes pictures and draws PNGs. No system font is read (a browser has no way
|
|
8
9
|
// to hand one over) and nothing is fetched: faces come from the SVG's own @font-face data (the `embeddedFonts` of the fonts handle
|
|
9
|
-
// `
|
|
10
|
+
// `toSvg` was given), from the faces the `fonts` handle holds (`loadFonts` from `/fonts-browser`, so script faces the SVG does not
|
|
10
11
|
// embed reach the PDF too), or from `fontData`.
|
|
11
12
|
//
|
|
12
|
-
// RR-63: this entry imports no converter. The one optional piece is pdf-lib, for `
|
|
13
|
+
// RR-63: this entry imports no converter. The one optional piece is pdf-lib, for `raster: true` only (the default vector PDF
|
|
13
14
|
// needs nothing): the host imports it and passes it as `options.pdfLib`, so a bundler never has to resolve it.
|
|
14
15
|
|
|
15
16
|
const DEFAULT_DIMENSIONS = { width: 1280, height: 720 };
|
|
16
17
|
const MAX_PIXELS = 40_000_000;
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* PNG with a canvas (RR-73): a deck drawn with `toSvg` (all slides, a slide number or a selection, as for the Node `toPng`) or SVG
|
|
21
|
+
* the renderer drew; one PNG for a slide number or one SVG, a list otherwise. `scale` multiplies the SVG's own size; `background`
|
|
22
|
+
* is a CSS colour (default white, `"transparent"` for none).
|
|
23
|
+
*/
|
|
24
|
+
export async function toPng(source, slides, options) {
|
|
25
|
+
({ slides, options } = slideArguments(slides, options));
|
|
26
|
+
const { svgs, one } = slideSources(source, slides, options, toSvg);
|
|
27
|
+
const pngs = [];
|
|
28
|
+
for (const svg of svgs) pngs.push(await svgPng(svg, options));
|
|
29
|
+
return one ? pngs[0] : pngs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function svgPng(svg, options = {}) {
|
|
20
33
|
const text = svgText(svg);
|
|
21
34
|
const size = pageSize(text);
|
|
22
35
|
const scale = positive(options.scale, "scale", 1);
|
|
@@ -32,24 +45,26 @@ export async function svgToPng(svg, options = {}) {
|
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
35
|
-
*
|
|
36
|
-
* real text, paths, gradients and images; `
|
|
37
|
-
* the Node `
|
|
38
|
-
* names, `rasterFallbackScale`), `pdfLib` (raster
|
|
48
|
+
* One PDF (RR-73) of a deck drawn with `toSvg` (all slides or a selection) or of SVG slides, one slide per page (1 SVG pixel is 1
|
|
49
|
+
* PDF point, as in Node). Vector by default (real text, paths, gradients and images); `raster: true` draws each slide as an image
|
|
50
|
+
* (`scale`, default 2). Takes the options of the Node `toPdf` that make sense here (`metadata`, `tagged`, `strict`, `compress`,
|
|
51
|
+
* `onDiagnostic`, the generic family names, `rasterFallbackScale`), `pdfLib` (`raster: true` only: `import * as pdfLib from "pdf-lib"`), `fonts` (the handle `loadFonts()` returns: every face it holds can be embedded), `fontData`
|
|
39
52
|
* (`[{ data, family? }]`, extra face bytes without a handle), `signal` (an AbortSignal, checked between pages) and
|
|
40
53
|
* `onProgress({ page, pages })`.
|
|
41
54
|
*/
|
|
42
|
-
export async function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
if (
|
|
55
|
+
export async function toPdf(source, slides, options) {
|
|
56
|
+
({ slides, options } = slideArguments(slides, options));
|
|
57
|
+
const inputs = slideSources(source, slides, options, toSvg).svgs.map(svgText);
|
|
58
|
+
if (!inputs.length) throw new OPFRenderError("empty-pdf", "toPdf needs at least one slide.");
|
|
59
|
+
if (options.raster !== undefined && typeof options.raster !== "boolean") throw new OPFRenderError("invalid-conversion-option", "raster must be true or false.", { option: "raster", value: options.raster });
|
|
60
|
+
if (options.raster === true) return rasterPdf(inputs, options);
|
|
48
61
|
const scale = positive(options.rasterFallbackScale, "rasterFallbackScale", 2);
|
|
49
62
|
const fontCss = fontFaceCss(inputs);
|
|
50
63
|
return svgsToVectorPdf(inputs, {
|
|
51
64
|
fontFiles: [], fontDirs: [],
|
|
52
65
|
fontData: [...(options.fonts?.registry?.exportFaces?.() ?? []), ...(Array.isArray(options.fontData) ? options.fontData : [])],
|
|
66
|
+
// opf-render#188: shaped by HarfBuzz when the handle has the shaper (`shapeWasm`); fontkit otherwise.
|
|
67
|
+
shape: options.fonts?.outlines?.shape,
|
|
53
68
|
imageCodec: canvasImageCodec(),
|
|
54
69
|
defaultFontFamily: options.defaultFontFamily ?? "Roboto",
|
|
55
70
|
sansSerifFamily: options.sansSerifFamily ?? options.defaultFontFamily ?? "Roboto",
|
|
@@ -68,7 +83,7 @@ export async function svgToPdf(svgs, options = {}) {
|
|
|
68
83
|
producer: packageName,
|
|
69
84
|
ErrorClass: OPFRenderError,
|
|
70
85
|
// The element's own text has no fonts in a standalone SVG image: give it the faces the slides embed.
|
|
71
|
-
rasterize: async (svg, factor) => ({ png: await
|
|
86
|
+
rasterize: async (svg, factor) => ({ png: await svgPng(fontCss && /<text[\s>]/.test(svg) ? svg.replace(/(<svg\b[^>]*>)/, `$1<style>${fontCss}</style>`) : svg, { scale: factor, background: "transparent", signal: options.signal }) }),
|
|
72
87
|
});
|
|
73
88
|
}
|
|
74
89
|
|
|
@@ -81,7 +96,7 @@ async function rasterPdf(inputs, options) {
|
|
|
81
96
|
for (const [index, svg] of inputs.entries()) {
|
|
82
97
|
options.signal?.throwIfAborted?.();
|
|
83
98
|
const size = pageSize(svg);
|
|
84
|
-
const png = await
|
|
99
|
+
const png = await svgPng(svg, { scale, background: options.background ?? "#FFFFFF", signal: options.signal });
|
|
85
100
|
const image = await pdf.embedPng(png);
|
|
86
101
|
pdf.addPage([size.width, size.height]).drawImage(image, { x: 0, y: 0, width: size.width, height: size.height });
|
|
87
102
|
options.onProgress?.({ page: index + 1, pages: inputs.length });
|
|
@@ -90,14 +105,14 @@ async function rasterPdf(inputs, options) {
|
|
|
90
105
|
return pdf.save({ addDefaultPage: false, useObjectStreams: false });
|
|
91
106
|
}
|
|
92
107
|
|
|
93
|
-
// pdf-lib for raster
|
|
108
|
+
// pdf-lib for `raster: true` when the host did not pass it. The specifier is not a literal, so a browser bundler leaves it alone (and
|
|
94
109
|
// never fails for a host that does not install pdf-lib); it resolves where the page can import it by name, for example with an import map.
|
|
95
110
|
async function importPdfLib() {
|
|
96
111
|
const name = "pdf-lib";
|
|
97
112
|
try {
|
|
98
113
|
return await import(/* webpackIgnore: true */ /* @vite-ignore */ name);
|
|
99
114
|
} catch (error) {
|
|
100
|
-
throw new OPFRenderError("converter-missing", '
|
|
115
|
+
throw new OPFRenderError("converter-missing", 'A raster PDF (raster: true) needs pdf-lib, an optional peer dependency of @openpresentation/opf-render: run `npm install pdf-lib@^1.17.1` and pass it as the pdfLib option (import * as pdfLib from "pdf-lib"). The default vector PDF needs nothing.', { package: "pdf-lib", range: "^1.17.1", install: "npm install pdf-lib@^1.17.1", cause: error instanceof Error ? error.message : String(error) });
|
|
101
116
|
}
|
|
102
117
|
}
|
|
103
118
|
|
package/dist/font-manifest.js
CHANGED
|
@@ -1620,12 +1620,12 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
1620
1620
|
},
|
|
1621
1621
|
{
|
|
1622
1622
|
"name": "@expo-google-fonts/noto-sans-jp",
|
|
1623
|
-
"version": "0.4.
|
|
1623
|
+
"version": "0.4.4",
|
|
1624
1624
|
"pack": "scripts",
|
|
1625
1625
|
"scripts": [
|
|
1626
1626
|
"Jpan"
|
|
1627
1627
|
],
|
|
1628
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-jp/v/0.4.
|
|
1628
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-jp/v/0.4.4",
|
|
1629
1629
|
"license": "OFL-1.1",
|
|
1630
1630
|
"licenseFile": "LICENSE_FONT",
|
|
1631
1631
|
"licenseSha256": "1c05c68c34f9708415aada51f17e1b0092d2cea709bf4a94cd38114f9e73d7d9",
|
|
@@ -1640,25 +1640,25 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
1640
1640
|
"family": "Noto Sans JP",
|
|
1641
1641
|
"weight": 400,
|
|
1642
1642
|
"italic": false,
|
|
1643
|
-
"sha256": "
|
|
1643
|
+
"sha256": "6255ea4692e97ef27330304fb96fd650f9111f868cd585fcf2802d42823930e5"
|
|
1644
1644
|
},
|
|
1645
1645
|
{
|
|
1646
1646
|
"file": "700Bold/NotoSansJP_700Bold.ttf",
|
|
1647
1647
|
"family": "Noto Sans JP",
|
|
1648
1648
|
"weight": 700,
|
|
1649
1649
|
"italic": false,
|
|
1650
|
-
"sha256": "
|
|
1650
|
+
"sha256": "7c03e14d8a03e8f5181dbd130712d51e4ae01ac648cbd59442ae8b16c0951835"
|
|
1651
1651
|
}
|
|
1652
1652
|
]
|
|
1653
1653
|
},
|
|
1654
1654
|
{
|
|
1655
1655
|
"name": "@expo-google-fonts/noto-sans-sc",
|
|
1656
|
-
"version": "0.4.
|
|
1656
|
+
"version": "0.4.4",
|
|
1657
1657
|
"pack": "scripts",
|
|
1658
1658
|
"scripts": [
|
|
1659
1659
|
"Hans"
|
|
1660
1660
|
],
|
|
1661
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-sc/v/0.4.
|
|
1661
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-sc/v/0.4.4",
|
|
1662
1662
|
"license": "OFL-1.1",
|
|
1663
1663
|
"licenseFile": "LICENSE_FONT",
|
|
1664
1664
|
"licenseSha256": "1c05c68c34f9708415aada51f17e1b0092d2cea709bf4a94cd38114f9e73d7d9",
|
|
@@ -1686,12 +1686,12 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
1686
1686
|
},
|
|
1687
1687
|
{
|
|
1688
1688
|
"name": "@expo-google-fonts/noto-sans-tc",
|
|
1689
|
-
"version": "0.4.
|
|
1689
|
+
"version": "0.4.4",
|
|
1690
1690
|
"pack": "scripts",
|
|
1691
1691
|
"scripts": [
|
|
1692
1692
|
"Hant"
|
|
1693
1693
|
],
|
|
1694
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-tc/v/0.4.
|
|
1694
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-tc/v/0.4.4",
|
|
1695
1695
|
"license": "OFL-1.1",
|
|
1696
1696
|
"licenseFile": "LICENSE_FONT",
|
|
1697
1697
|
"licenseSha256": "1c05c68c34f9708415aada51f17e1b0092d2cea709bf4a94cd38114f9e73d7d9",
|
|
@@ -1719,12 +1719,12 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
1719
1719
|
},
|
|
1720
1720
|
{
|
|
1721
1721
|
"name": "@expo-google-fonts/noto-sans-kr",
|
|
1722
|
-
"version": "0.4.
|
|
1722
|
+
"version": "0.4.4",
|
|
1723
1723
|
"pack": "scripts",
|
|
1724
1724
|
"scripts": [
|
|
1725
1725
|
"Kore"
|
|
1726
1726
|
],
|
|
1727
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-kr/v/0.4.
|
|
1727
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-sans-kr/v/0.4.4",
|
|
1728
1728
|
"license": "OFL-1.1",
|
|
1729
1729
|
"licenseFile": "LICENSE_FONT",
|
|
1730
1730
|
"licenseSha256": "1c05c68c34f9708415aada51f17e1b0092d2cea709bf4a94cd38114f9e73d7d9",
|
|
@@ -1739,14 +1739,14 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
1739
1739
|
"family": "Noto Sans KR",
|
|
1740
1740
|
"weight": 400,
|
|
1741
1741
|
"italic": false,
|
|
1742
|
-
"sha256": "
|
|
1742
|
+
"sha256": "b4a747e78805bc25b610dcfc3003cb5fb2e2ec034763774b24216ed15889eef4"
|
|
1743
1743
|
},
|
|
1744
1744
|
{
|
|
1745
1745
|
"file": "700Bold/NotoSansKR_700Bold.ttf",
|
|
1746
1746
|
"family": "Noto Sans KR",
|
|
1747
1747
|
"weight": 700,
|
|
1748
1748
|
"italic": false,
|
|
1749
|
-
"sha256": "
|
|
1749
|
+
"sha256": "c76f806dc46b8e7981410cb083801f7f124b7f8bc0d181cdae80e78f16d0e9cc"
|
|
1750
1750
|
}
|
|
1751
1751
|
]
|
|
1752
1752
|
},
|
|
@@ -2551,7 +2551,7 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
2551
2551
|
},
|
|
2552
2552
|
{
|
|
2553
2553
|
"name": "@expo-google-fonts/noto-color-emoji",
|
|
2554
|
-
"version": "0.4.
|
|
2554
|
+
"version": "0.4.7",
|
|
2555
2555
|
"pack": "scripts",
|
|
2556
2556
|
"scripts": [
|
|
2557
2557
|
"Zsye"
|
|
@@ -2560,7 +2560,7 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
2560
2560
|
"format": "COLRv1, SVG",
|
|
2561
2561
|
"rasterFamily": "Noto Emoji"
|
|
2562
2562
|
},
|
|
2563
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-color-emoji/v/0.4.
|
|
2563
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-color-emoji/v/0.4.7",
|
|
2564
2564
|
"license": "OFL-1.1",
|
|
2565
2565
|
"licenseFile": "LICENSE_FONT",
|
|
2566
2566
|
"licenseSha256": "ac564676d10054a8445923dfc2dfb13c042d97888bd27c1b6ec6dfe89a9d8d62",
|
|
@@ -2573,18 +2573,18 @@ export const BUNDLED_FONT_MANIFEST=freeze({
|
|
|
2573
2573
|
"family": "Noto Color Emoji",
|
|
2574
2574
|
"weight": 400,
|
|
2575
2575
|
"italic": false,
|
|
2576
|
-
"sha256": "
|
|
2576
|
+
"sha256": "ad4a3478943b88660a194d1434793bc02c8007819f2d1e7da9ede289e78f9ca7"
|
|
2577
2577
|
}
|
|
2578
2578
|
]
|
|
2579
2579
|
},
|
|
2580
2580
|
{
|
|
2581
2581
|
"name": "@expo-google-fonts/noto-emoji",
|
|
2582
|
-
"version": "0.4.
|
|
2582
|
+
"version": "0.4.8",
|
|
2583
2583
|
"pack": "scripts",
|
|
2584
2584
|
"scripts": [
|
|
2585
2585
|
"Zsye"
|
|
2586
2586
|
],
|
|
2587
|
-
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-emoji/v/0.4.
|
|
2587
|
+
"source": "https://www.npmjs.com/package/@expo-google-fonts/noto-emoji/v/0.4.8",
|
|
2588
2588
|
"license": "OFL-1.1",
|
|
2589
2589
|
"licenseFile": "LICENSE_FONT",
|
|
2590
2590
|
"licenseSha256": "500bb1ccf43df7bbb522112f9133a52b16e1c35e809632f5d8609b179152de5b",
|
package/dist/font-registry.js
CHANGED
|
@@ -68,6 +68,32 @@ export function skipUndecodableLookups(font) {
|
|
|
68
68
|
return guarded;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// opf-render#125: fontkit 2.0.4 caches one Glyph per glyph id (font._glyphs) with the code points of the call that created it, and
|
|
72
|
+
// its layout reads the input's code points back from that cache (glyphsForString -> getGlyph(id, [codePoint]) -> GlyphInfo). A glyph
|
|
73
|
+
// first reached another way keeps other code points: a composite outline (run.bbox for outlineBounds, glyph.path) reaches its
|
|
74
|
+
// components with none, and a substitution's output carries its input's. Noto Sans Bengali draws ra (U+09B0) with the nukta glyph
|
|
75
|
+
// (U+09BC) as a component, so once any outline with ra was bounded, a later run with a nukta shaped the nukta as an unknown
|
|
76
|
+
// character, the Indic shaper broke its cluster and inserted a dotted circle (0.51 em each): the measured width of a string
|
|
77
|
+
// depended on what the process had measured before. Every request for a glyph with code points now gets a glyph that carries
|
|
78
|
+
// exactly those (a view of the cached glyph when they differ), so shaping is a function of the text alone. The PDF writer's own
|
|
79
|
+
// faces do the same (pdf-fonts.js parseFace).
|
|
80
|
+
const codePointsPinned = new WeakSet();
|
|
81
|
+
export function pinGlyphCodePoints(font) {
|
|
82
|
+
if (codePointsPinned.has(font) || typeof font?.getGlyph !== "function") return font;
|
|
83
|
+
codePointsPinned.add(font);
|
|
84
|
+
const getGlyph = font.getGlyph.bind(font);
|
|
85
|
+
font.getGlyph = (id, codePoints = []) => {
|
|
86
|
+
const glyph = getGlyph(id, codePoints);
|
|
87
|
+
if (!glyph || !codePoints.length) return glyph;
|
|
88
|
+
const cached = glyph.codePoints;
|
|
89
|
+
if (cached?.length === codePoints.length && codePoints.every((codePoint, index) => cached[index] === codePoint)) return glyph;
|
|
90
|
+
const view = Object.create(glyph);
|
|
91
|
+
view.codePoints = [...codePoints];
|
|
92
|
+
return view;
|
|
93
|
+
};
|
|
94
|
+
return font;
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
/**
|
|
72
98
|
* The face a registry draws for `family` (a validated concrete family: theme tokens are already resolved) in `style`, chosen
|
|
73
99
|
* from `faces` ({family, familyGroup, weight, italic, fallbackOnly?, scripts?}) under the substitution policy, aliases and
|
|
@@ -143,6 +169,7 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
143
169
|
let font;
|
|
144
170
|
try { font = create(data, entry.postscriptName); } catch (error) { throw new OPFFontError("invalid-font-data", error.message); }
|
|
145
171
|
if (!font?.layout || !font.unitsPerEm) throw new OPFFontError("font-collection", "Select one font from a collection with postscriptName.");
|
|
172
|
+
pinGlyphCodePoints(font);
|
|
146
173
|
const family = validFamily(entry.family ?? font.familyName);
|
|
147
174
|
// OpenType groups Medium/SemiBold/ExtraBold under a preferred family, while
|
|
148
175
|
// retaining separate legacy families for four-style native font selectors.
|
|
@@ -227,6 +254,10 @@ export function createFontRegistry(entries, options = {}) {
|
|
|
227
254
|
if(face.fontFace) resolved.fontFace={...face.fontFace};
|
|
228
255
|
// FF-45: the planner maps a symbol-encoded family's codes (symbol-fonts.js); the face only lends its glyphs.
|
|
229
256
|
if(resolution.symbolEncoding) resolved.symbolEncoding=resolution.symbolEncoding;
|
|
257
|
+
// render#220: resolving a resolved style again keeps its encoding. Core composes each list paragraph from its block's resolved
|
|
258
|
+
// style, so the item asks for the open symbol face (Noto Sans Symbols 2) by name; that face is the encoding's own, so the codes
|
|
259
|
+
// still map. Any other family drops a stale encoding.
|
|
260
|
+
else if(style.symbolEncoding && symbolPreviewFaces(style.symbolEncoding).some(name=>name.toLowerCase()===face.family.toLowerCase())) resolved.symbolEncoding=symbolEncodingFamily(style.symbolEncoding);
|
|
230
261
|
return resolved;
|
|
231
262
|
};
|
|
232
263
|
const metrics = (text,size,style,includeOutline=false) => {
|
package/dist/font-subset.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `harfbuzz-subset.wasm` of the pinned harfbuzzjs package, MIT) keeps the glyphs a slide's characters reach through the
|
|
3
3
|
// font's layout tables (every GSUB and GPOS feature is kept, so the browser shapes the subset exactly as the whole face),
|
|
4
4
|
// renumbers them and trims cmap, hmtx, GPOS and the rest to them. The module has no imports, so it is instantiated
|
|
5
|
-
// synchronously from its bytes and `
|
|
5
|
+
// synchronously from its bytes and `toSvg` stays synchronous; the same input always gives the same bytes.
|
|
6
6
|
//
|
|
7
7
|
// Only a face whose license allows it is subset: a bundled face (matched by the sha256 of its bytes against the pinned
|
|
8
8
|
// manifest) under an allowed license, whose family and file names do not contain one of its package's Reserved Font Names
|