@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/index.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
|
|
@@ -118,18 +121,18 @@ export interface RenderSvgOptions {
|
|
|
118
121
|
strictReferences?: boolean;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
/** The options of `toPng`: the conversion's own, and the `toSvg` options that draw a deck. */
|
|
125
|
+
export interface ToPngOptions extends Omit<ToSvgOptions, "fonts"> {
|
|
122
126
|
/**
|
|
123
|
-
* The fonts the
|
|
124
|
-
* `loadSystemFonts` say whether the bundled base faces are added, default true, and whether
|
|
125
|
-
*
|
|
127
|
+
* The fonts the deck is laid out and drawn with: the handle `loadFonts()` returns from `/fonts-node` (it measures the deck, and its
|
|
128
|
+
* `fontFiles` draw; `useBundledFonts` and `loadSystemFonts` say whether the bundled base faces are added, default true, and whether
|
|
129
|
+
* system fonts load, default false), so the preview and the PNG use the same faces. For SVG input it may instead be font folders
|
|
130
|
+
* (a path or a list of paths), whose faces are added to the bundled base faces. Without it the bundled base faces draw.
|
|
126
131
|
*/
|
|
127
|
-
fonts?: RenderFonts;
|
|
132
|
+
fonts?: RenderFonts | string | readonly string[];
|
|
128
133
|
scale?: number;
|
|
129
134
|
background?: string;
|
|
130
135
|
dpi?: number;
|
|
131
|
-
/** Extra directories of font files to draw with. */
|
|
132
|
-
fontDirs?: string[];
|
|
133
136
|
defaultFontFamily?: string;
|
|
134
137
|
sansSerifFamily?: string;
|
|
135
138
|
monospaceFamily?: string;
|
|
@@ -198,14 +201,14 @@ export interface PdfMetadata {
|
|
|
198
201
|
modificationDate?: Date | string;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
|
-
export interface
|
|
204
|
+
export interface ToPdfOptions extends Omit<ToPngOptions, "onDiagnostic"> {
|
|
202
205
|
/**
|
|
203
|
-
* `
|
|
204
|
-
* with embedded font subsets (selectable, searchable, correct copy and paste); the fonts are
|
|
205
|
-
* `fontFiles` and the
|
|
206
|
+
* `true` draws each slide as an image (`scale`). Default false, a vector PDF: shapes, gradients and patterns as PDF vector graphics,
|
|
207
|
+
* pictures as images, text as real text with embedded font subsets (selectable, searchable, correct copy and paste); the fonts are
|
|
208
|
+
* the bundled files, the `fonts` handle's `fontFiles` and the font folders you supply, never system fonts.
|
|
206
209
|
*/
|
|
207
|
-
|
|
208
|
-
/**
|
|
210
|
+
raster?: boolean;
|
|
211
|
+
/** A vector PDF paints the page this colour first (default white, as a raster PDF composites on white); `"none"` leaves it unpainted. */
|
|
209
212
|
background?: string;
|
|
210
213
|
/** Vector only. Family drawn for the generic `serif`; default `defaultFontFamily`. */
|
|
211
214
|
serifFamily?: string;
|
|
@@ -219,8 +222,8 @@ export interface SvgToPdfOptions extends SvgToPngOptions {
|
|
|
219
222
|
strict?: boolean;
|
|
220
223
|
/** Vector only. Pixel density of the rare element rasterized because it has no vector form; default 2. */
|
|
221
224
|
rasterFallbackScale?: number;
|
|
222
|
-
/** Vector only
|
|
223
|
-
onDiagnostic?: (diagnostic: PdfDiagnostic) => void;
|
|
225
|
+
/** Vector only: font embedding reports, substitutions, fallbacks and unsupported features; given a deck, also the diagnostics of drawing it. */
|
|
226
|
+
onDiagnostic?: (diagnostic: PdfDiagnostic | RenderDiagnostic) => void;
|
|
224
227
|
/** Vector only. Checked between pages; an abort rejects with the signal's reason. */
|
|
225
228
|
signal?: AbortSignal;
|
|
226
229
|
/** Vector only. Called after each page is written. */
|
|
@@ -278,14 +281,32 @@ export declare class OPFRenderError extends Error {
|
|
|
278
281
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
279
282
|
}
|
|
280
283
|
|
|
281
|
-
export declare function resolvePresentation(input: unknown, options?:
|
|
284
|
+
export declare function resolvePresentation(input: unknown, options?: ToSvgOptions): ResolvedPresentation;
|
|
282
285
|
|
|
283
|
-
/**
|
|
284
|
-
export
|
|
286
|
+
/** An SVG slide the renderer drew: its text, or its bytes. */
|
|
287
|
+
export type SvgSlide = string | Uint8Array;
|
|
285
288
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
289
|
+
/**
|
|
290
|
+
* The deck as SVG (RR-73). `toSvg(deck)`: every slide, in order (without the hidden ones under `skipHidden`). `toSvg(deck, 3)`: the
|
|
291
|
+
* third slide (slides count from 1; out of range throws `slide-out-of-range`). `toSvg(deck, "1-3")` or `toSvg(deck, [1, 3])`: the
|
|
292
|
+
* selected slides (a malformed selection throws `invalid-slide-selection`). The second argument may be the options instead.
|
|
293
|
+
*/
|
|
294
|
+
export declare function toSvg(input: unknown, options?: ToSvgOptions): string[];
|
|
295
|
+
export declare function toSvg(input: unknown, slide: number, options?: ToSvgOptions): string;
|
|
296
|
+
export declare function toSvg(input: unknown, slides: Exclude<SlideSelection, number>, options?: ToSvgOptions): string[];
|
|
288
297
|
|
|
289
|
-
|
|
298
|
+
/**
|
|
299
|
+
* PNG (RR-73) of a deck, drawn with `toSvg` (the slide arguments are `toSvg`'s: one PNG for a slide number, a list otherwise), or of
|
|
300
|
+
* SVG the renderer drew (one SVG gives one PNG, a list a list). A slide selection with SVG input throws `invalid-conversion-option`.
|
|
301
|
+
*/
|
|
302
|
+
export declare function toPng(svg: Uint8Array | `<${string}`, options?: ToPngOptions): Promise<Uint8Array>;
|
|
303
|
+
export declare function toPng(svgs: readonly SvgSlide[], options?: ToPngOptions): Promise<Uint8Array[]>;
|
|
304
|
+
export declare function toPng(deck: unknown, slide: number, options?: ToPngOptions): Promise<Uint8Array>;
|
|
305
|
+
export declare function toPng(deck: unknown, slides: Exclude<SlideSelection, number>, options?: ToPngOptions): Promise<Uint8Array[]>;
|
|
306
|
+
export declare function toPng(deck: object, options?: ToPngOptions): Promise<Uint8Array[]>;
|
|
307
|
+
/** A string is SVG when it starts with `<`, and the deck's JSON text otherwise. */
|
|
308
|
+
export declare function toPng(source: string, options?: ToPngOptions): Promise<Uint8Array | Uint8Array[]>;
|
|
290
309
|
|
|
291
|
-
|
|
310
|
+
/** One PDF (RR-73), a page per slide: of a deck drawn with `toSvg` (every slide, or a selection: `toPdf(deck, "2-4")`), or of SVG slides. */
|
|
311
|
+
export declare function toPdf(source: unknown, options?: ToPdfOptions): Promise<Uint8Array>;
|
|
312
|
+
export declare function toPdf(deck: unknown, slides: SlideSelection, options?: ToPdfOptions): Promise<Uint8Array>;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './svg.js';
|
|
2
|
-
export {
|
|
2
|
+
export {toPng,toPdf} from './raster.js';
|
package/dist/lazy-fonts.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fontPolicyFor } from "@openpresentation/opf/font-policy";
|
|
2
2
|
import { pickFace } from "./font-registry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolvePresentation, toSvg } from "./svg.js";
|
|
4
4
|
export { lazyFontEntries, lazyFontList } from "./lazy-font-list.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -17,7 +17,7 @@ const lc = value => String(value).trim().toLowerCase();
|
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Render options that decide how a document resolves (catalogs, validate, date, ...) are forwarded from the host's
|
|
20
|
-
* `
|
|
20
|
+
* `toSvg` options. The analysis supplies its own measurement and reports nothing: callbacks, the host's fonts handle
|
|
21
21
|
* (measurement and embedded fonts), asset resolution and tracing are dropped.
|
|
22
22
|
*/
|
|
23
23
|
const NOT_RESOLVE_OPTIONS = new Set(["fonts", "onDiagnostic", "trace", "glyphFallback", "imageResolver", "strictAssets", "signal"]);
|
|
@@ -27,10 +27,10 @@ function resolveOptionsOf(options) {
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* The font families a presentation's slides resolve (heading, body and code roles of every slide, including per-slide
|
|
30
|
-
* design overrides), as `
|
|
30
|
+
* design overrides), as `toSvg` resolves them with the same `options` (`catalogs`, ...). This is the role families,
|
|
31
31
|
* not what is drawn: use `presentationFaces` for that.
|
|
32
32
|
*
|
|
33
|
-
* A document that does not resolve throws the error `
|
|
33
|
+
* A document that does not resolve throws the error `toSvg` throws for it (an OPFRenderError such as
|
|
34
34
|
* `catalog-resolution-failed` for a layout id no catalog has, or `invalid-opf`), never an empty set: a document whose layout comes only from
|
|
35
35
|
* `options.catalogs` needs those catalogs here too.
|
|
36
36
|
*/
|
|
@@ -57,7 +57,7 @@ function registryView({ faces, policy = "none", aliases = new Map(), fallbackFam
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* The faces a preview of the presentation draws, as `[{family, weight, italic}]`, sorted by family, style and weight. It is
|
|
60
|
-
* the renderer's own layout and painting run with a recording measurement, so it follows exactly what `
|
|
60
|
+
* the renderer's own layout and painting run with a recording measurement, so it follows exactly what `toSvg` measures
|
|
61
61
|
* and draws: the heading and body roles of every slide that draws them, bold and italic runs of rich text, table and cell
|
|
62
62
|
* styles, header and footer furniture, chart labels, metrics, quotes and timelines, the code role only where code is
|
|
63
63
|
* drawn, per-slide design overrides, font scheme and theme resolution, and script slots (a script run is requested in its
|
|
@@ -73,8 +73,8 @@ function registryView({ faces, policy = "none", aliases = new Map(), fallbackFam
|
|
|
73
73
|
* family found. The result is the faces the registry paints, for example `Intos 700` for a 600-weight label of an Aptos
|
|
74
74
|
* deck. A style the registry cannot resolve is left as named (drawing reports it).
|
|
75
75
|
*
|
|
76
|
-
* `options` are the `
|
|
77
|
-
* does not resolve throws what `
|
|
76
|
+
* `options` are the `toSvg` options the document resolves with (`catalogs`, `date`, `validate`, ...). A document that
|
|
77
|
+
* does not resolve throws what `toSvg` throws for it.
|
|
78
78
|
* @param {unknown} presentation
|
|
79
79
|
* @param {object} [options]
|
|
80
80
|
* @param {{faces: Iterable<{family: string, weight: number, italic: boolean}>, policy?: "none"|"metric"|"visual", aliases?: ReadonlyMap<string, string>, fallbackFamily?: string}} [registry]
|
|
@@ -98,7 +98,7 @@ export function presentationFaces(presentation, options = {}, registry) {
|
|
|
98
98
|
measure(text, size, style) { if (text !== "") record(settle(style)); return String(text).length * size * RECORDED_EM; },
|
|
99
99
|
resolveStyle: settle,
|
|
100
100
|
};
|
|
101
|
-
|
|
101
|
+
toSvg(presentation, { ...resolveOptionsOf(options), fonts: { textMeasurement } });
|
|
102
102
|
return [...seen.values()].sort((a, b) => a.family < b.family ? -1 : a.family > b.family ? 1 : Number(a.italic) - Number(b.italic) || a.weight - b.weight);
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -106,8 +106,8 @@ export function presentationFaces(presentation, options = {}, registry) {
|
|
|
106
106
|
* The vendored faces (of `lazy`) a presentation draws that the registry does not hold: what a registry loads before it can
|
|
107
107
|
* paint the document as Node paints it with every vendored face loaded. `held` lists the registry's faces
|
|
108
108
|
* (`registry.describeFaces()`), `loaded` the lazy files already loaded; `policy`, `aliases` and `fallbackFamily` are the
|
|
109
|
-
* registry's. `renderOptions` are the `
|
|
110
|
-
* does not resolve throws what `
|
|
109
|
+
* registry's. `renderOptions` are the `toSvg` options the document resolves with (`catalogs`, ...), and a document that
|
|
110
|
+
* does not resolve throws what `toSvg` throws for it. Face level: a bold or italic run added by an edit adds just that face.
|
|
111
111
|
*/
|
|
112
112
|
export function lazyFacesNeeded(presentation, renderOptions, { lazy, held, loaded = new Set(), policy = "none", aliases = new Map(), fallbackFamily }) {
|
|
113
113
|
const heldFaces = [...held], heldKeys = new Set(heldFaces.map(faceKeyOf));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// render#199: the files the Node entries read from disk (harfbuzzjs's .wasm, the installed `@expo-google-fonts/*` packages and this
|
|
2
|
+
// package's vendored `fonts/` directory) are located at run time, by Node's own resolution from this file, in a form bundlers leave
|
|
3
|
+
// alone. A bundler (Next.js Turbopack, webpack) treats `require.resolve` on `createRequire(import.meta.url)` as a module import: a
|
|
4
|
+
// literal .wasm specifier is compiled as a WebAssembly module (which fails `next build`), a computed one (`${name}/package.json`)
|
|
5
|
+
// becomes a stub that always throws, and `new URL(`../${dir}/`, import.meta.url)` becomes the URL of an unrelated emitted asset.
|
|
6
|
+
// Here `createRequire` is reached through the module namespace and the specifier is a variable behind the bundlers' ignore comments,
|
|
7
|
+
// so neither is traced, and the package root is computed from this file's path. Plain Node resolves exactly as before.
|
|
8
|
+
import * as nodeModule from "node:module";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
/** This package's root directory (the parent of `dist/`). */
|
|
13
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
14
|
+
|
|
15
|
+
const requireHere = nodeModule.createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The absolute path of an installed file (`harfbuzzjs/dist/harfbuzz.wasm`, `@expo-google-fonts/roboto/package.json`), resolved as
|
|
19
|
+
* `require.resolve` resolves it from this package, or from `directory` when given. Throws `MODULE_NOT_FOUND` like `require.resolve`.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveInstalled(specifier, directory) {
|
|
22
|
+
const resolver = directory === undefined ? requireHere : nodeModule.createRequire(path.join(directory, "package.json"));
|
|
23
|
+
return resolver.resolve(/* webpackIgnore: true */ /* turbopackIgnore: true */ specifier);
|
|
24
|
+
}
|
package/dist/pdf-fonts.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isTrueTypeOutlines, subsetTrueType } from "./pdf-subset.js";
|
|
|
4
4
|
import { hex, name, num, sha256, textString, utf16Hex } from "./pdf-writer.js";
|
|
5
5
|
|
|
6
6
|
// Font selection, shaping and embedding for the vector PDF export (RR-12). Fonts come only from the faces the
|
|
7
|
-
// caller supplies (the same bundled font files,
|
|
7
|
+
// caller supplies (the same bundled font files, fonts handle or font folders the PNG preview draws with, and the
|
|
8
8
|
// `@font-face` data of the SVG itself); nothing is read from the system and nothing is fetched. A face whose
|
|
9
9
|
// OS/2 embedding bits forbid embedding is never embedded.
|
|
10
10
|
|
package/dist/pdf-text.js
CHANGED
|
@@ -7,8 +7,8 @@ import { attributesOf, inheritStyle, parseFontFamilies, parseFontWeight, parseLe
|
|
|
7
7
|
// SVG text layout for the vector PDF export (RR-12): the renderer has already decided every line, position and
|
|
8
8
|
// width (x, y, text-anchor, textLength), so this module only turns that into positioned glyph runs: text chunks,
|
|
9
9
|
// the Unicode bidirectional algorithm (the SVG carries directional isolates around right-to-left paragraphs),
|
|
10
|
-
// per-character font fallback, OpenType shaping
|
|
11
|
-
// or truncates text.
|
|
10
|
+
// per-character font fallback, OpenType shaping (HarfBuzz through `env.shape` when the fonts handle has it, fontkit
|
|
11
|
+
// otherwise) and textLength adjustment. It never wraps, shrinks or truncates text.
|
|
12
12
|
|
|
13
13
|
const bidi = bidiFactory();
|
|
14
14
|
const IGNORED = /[--]/u;
|
|
@@ -245,8 +245,8 @@ function shapeRun(run, env) {
|
|
|
245
245
|
const size = parseFloat(style["font-size"]) || 16;
|
|
246
246
|
const direction = run.level % 2 === 1 ? "rtl" : "ltr";
|
|
247
247
|
const language = openTypeLanguage(style.lang ?? style["xml:lang"]);
|
|
248
|
-
// RR-64: outlined text
|
|
249
|
-
// HarfBuzz takes the BCP 47 tag (`lang`); fontkit the OpenType language system (`language`).
|
|
248
|
+
// RR-64, opf-render#188: outlined text and the vector PDF are shaped by HarfBuzz (env.shape), the browser's shaper, when the
|
|
249
|
+
// fonts handle carries it; fontkit otherwise. HarfBuzz takes the BCP 47 tag (`lang`); fontkit the OpenType language system (`language`).
|
|
250
250
|
const glyphs = (env.shape ?? shape)(run.face, run.text, { features: featuresOf(style), language, lang: style.lang ?? style["xml:lang"], direction });
|
|
251
251
|
const scale = size / run.face.upem;
|
|
252
252
|
const letterSpacing = parseLength(style["letter-spacing"], { fontSize: size }) ?? 0;
|
package/dist/pdf-vector.js
CHANGED
|
@@ -81,6 +81,8 @@ class Converter {
|
|
|
81
81
|
this.env = {
|
|
82
82
|
fonts: this.fonts,
|
|
83
83
|
defaults: this.defaults,
|
|
84
|
+
// opf-render#188: HarfBuzz, the browser's shaper, from the fonts handle (`fonts.outlines.shape`); fontkit without one.
|
|
85
|
+
...(typeof options.shape === "function" ? { shape: options.shape } : {}),
|
|
84
86
|
diagnostic: (d) => this.diagnostic(d),
|
|
85
87
|
// A feature the PDF cannot give: reported once per character and family, fatal under `strict`.
|
|
86
88
|
missing: (d) => {
|
|
@@ -89,7 +91,7 @@ class Converter {
|
|
|
89
91
|
this.reported.add(key);
|
|
90
92
|
this.unsupported(d.code, d.message, { fontFamily: d.fontFamily, codePoint: d.codePoint });
|
|
91
93
|
},
|
|
92
|
-
unavailable: (families) => this.error("pdf-font-unavailable", `No embeddable font face is available for ${families.length ? `'${families.join("', '")}'` : "the requested text"}.
|
|
94
|
+
unavailable: (families) => this.error("pdf-font-unavailable", `No embeddable font face is available for ${families.length ? `'${families.join("', '")}'` : "the requested text"}. Pass the fonts handle or font folders as fonts, or keep useBundledFonts enabled.`, {}),
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -797,16 +799,24 @@ class Converter {
|
|
|
797
799
|
// The text object starts at the glyph's natural position (PDFium merges an /ActualText span across adjacent objects only),
|
|
798
800
|
// and the font's horizontal placement is a leading adjustment inside it.
|
|
799
801
|
const shift = glyph.xOffset ? [num(-glyph.xOffset * 1000 / upem, 3)] : [];
|
|
800
|
-
|
|
802
|
+
// A glyph shown alone only because it follows a placed one keeps the baseline (the glyphs after it continue on its line).
|
|
803
|
+
operations.push({ at: [run.x + run.scaleX * penBefore, run.y - (placed ? glyph.yOffset * scale : 0)], items: [...shift, `<${code}>`] });
|
|
801
804
|
current = null;
|
|
802
|
-
|
|
805
|
+
// After a glyph that is not itself placed, the text continues from it: what its shaped advance (and its placement)
|
|
806
|
+
// differs from the font's width moves the next glyph (opf-render#188: a zero-width joiner after a mark put the rest of
|
|
807
|
+
// the run its nominal width off). After a placed glyph the next one is placed by its own Td.
|
|
808
|
+
pending = placed ? 0 : nominal - shaped + glyph.xOffset * 1000 / upem;
|
|
803
809
|
placedBefore = placed;
|
|
804
810
|
return;
|
|
805
811
|
}
|
|
806
812
|
if (current === null) { current = { items: [] }; operations.push(current); }
|
|
813
|
+
// A small horizontal placement (at most the threshold above) moves the glyph inside the TJ array and back after it
|
|
814
|
+
// (opf-render#188: it was dropped, which put an Arabic or Telugu glyph up to 0.025 em off).
|
|
815
|
+
const shift = glyph.xOffset * 1000 / upem;
|
|
816
|
+
pending -= shift;
|
|
807
817
|
addNumber();
|
|
808
818
|
hex += code;
|
|
809
|
-
pending += nominal - shaped;
|
|
819
|
+
pending += nominal - shaped + shift;
|
|
810
820
|
};
|
|
811
821
|
// Each unit is a glyph, or for a left-to-right run the glyph map cannot give back (reordered Indic or Khmer clusters, a
|
|
812
822
|
// no-break space) a cluster of glyphs. A unit whose text the map cannot give carries it as /ActualText, one span per
|
|
@@ -1228,7 +1238,7 @@ async function listFonts(directory, { readdir, path }) {
|
|
|
1228
1238
|
}
|
|
1229
1239
|
|
|
1230
1240
|
/**
|
|
1231
|
-
* Convert SVG slides to a vector PDF. `options` carries the resolved conversion settings (see
|
|
1241
|
+
* Convert SVG slides to a vector PDF. `options` carries the resolved conversion settings (see toPdf in raster.js).
|
|
1232
1242
|
*/
|
|
1233
1243
|
export async function svgsToVectorPdf(svgs, options) {
|
|
1234
1244
|
const converter = new Converter(options);
|
package/dist/pdf-writer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { deflate } from "pako";
|
|
2
2
|
import { sha256Bytes } from "./sha256.js";
|
|
3
3
|
|
|
4
4
|
// A small PDF object writer for the vector export (RR-12). Everything it emits is a pure function of the objects
|
|
@@ -62,8 +62,11 @@ export function ref(number) {
|
|
|
62
62
|
return `${number} 0 R`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// legacyHash keeps the classic zlib hash (canonical zlib bytes, as pako 1 wrote them). pako 3 defaults to the faster hash
|
|
66
|
+
// of Chromium zlib (Node), which writes different and about 1% larger streams: the inflated content is the same, the
|
|
67
|
+
// PDF bytes and their hashes are not.
|
|
65
68
|
export function flate(bytes) {
|
|
66
|
-
return
|
|
69
|
+
return deflate(bytes, { level: 9, legacyHash: true });
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function sha256(bytes) {
|
package/dist/pdf.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { toPdf } from "./index.js";
|
|
2
|
+
export type { ToPdfOptions, PdfDiagnostic, PdfMetadata } from "./index.js";
|
package/dist/pdf.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// RR-63: the PDF format entry (Node). The default vector `
|
|
2
|
-
// `sharp` for pictures and `@resvg/resvg-js` for the rare element it rasterizes, and `
|
|
1
|
+
// RR-63: the PDF format entry (Node). The default vector `toPdf` needs no converter for text and shapes; it loads the optional peer
|
|
2
|
+
// `sharp` for pictures and `@resvg/resvg-js` for the rare element it rasterizes, and `raster: true` also loads `pdf-lib`. A converter
|
|
3
3
|
// that is not installed rejects with `OPFRenderError` code `converter-missing`. For a page use `@openpresentation/opf-render/export-browser`.
|
|
4
|
-
export {
|
|
4
|
+
export { toPdf } from "./raster.js";
|
package/dist/player.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BrowserFontsHandle } from "./fonts-browser.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ToSvgOptions } from "./svg.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Why a deck could not be loaded, drawn or presented. `code` is stable: `invalid-document`, `fetch-failed`, `fonts-unavailable`,
|
|
@@ -19,8 +19,8 @@ export type BlankMode = "none" | "black" | "white";
|
|
|
19
19
|
export interface PresentOptions {
|
|
20
20
|
/** The self-hosted font root URL (see `copyPreviewFonts`), or a browser fonts handle the page already has (`loadFonts` from `/fonts-browser`). Without it layout uses estimated widths and system fonts. */
|
|
21
21
|
fonts?: string | BrowserFontsHandle;
|
|
22
|
-
/** Extra `
|
|
23
|
-
renderOptions?: Partial<
|
|
22
|
+
/** Extra `toSvg` options (catalogs, imageResolver, date, ...). */
|
|
23
|
+
renderOptions?: Partial<ToSvgOptions>;
|
|
24
24
|
/** 1-based slide to start on, in the sequence that plays. Default 1. */
|
|
25
25
|
startSlide?: number;
|
|
26
26
|
/** Play hidden slides too. Default false. */
|
package/dist/player.js
CHANGED
|
@@ -35,7 +35,10 @@ const AUDIENCE_CSS = `
|
|
|
35
35
|
.goto[hidden] { display: none; }
|
|
36
36
|
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
37
37
|
.msg { position: absolute; inset: 0; display: grid; place-items: center; padding: 24px; text-align: center; }
|
|
38
|
-
@media (forced-colors: active) {
|
|
38
|
+
@media (forced-colors: active) {
|
|
39
|
+
.hud button { border-color: ButtonText; }
|
|
40
|
+
.hud button[aria-disabled="true"] { opacity: 1; color: GrayText; border-color: GrayText; }
|
|
41
|
+
}
|
|
39
42
|
`;
|
|
40
43
|
|
|
41
44
|
const PRESENTER_CSS = `
|
|
@@ -63,7 +66,10 @@ button[aria-pressed="true"] { background: #3d5a99; }
|
|
|
63
66
|
button[aria-disabled="true"] { opacity: 0.45; cursor: default; }
|
|
64
67
|
.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
65
68
|
@media (max-width: 760px) { main { grid-template-columns: 1fr; overflow: auto; } }
|
|
66
|
-
@media (forced-colors: active) {
|
|
69
|
+
@media (forced-colors: active) {
|
|
70
|
+
button, .notes, .slidebox { border-color: ButtonText; }
|
|
71
|
+
button[aria-disabled="true"] { opacity: 1; color: GrayText; border-color: GrayText; }
|
|
72
|
+
}
|
|
67
73
|
`;
|
|
68
74
|
|
|
69
75
|
/** Document indexes in the presentation order, from the options. */
|
|
@@ -632,7 +638,7 @@ async function readSource(source, signal) {
|
|
|
632
638
|
* player covers the page at once (and asks for full screen), then resolves with the running session when the first slide is
|
|
633
639
|
* drawn. Call it from a click or key handler: full screen and the speaker view both need a user gesture.
|
|
634
640
|
*
|
|
635
|
-
* Options: `fonts` (the self-hosted font root URL, or a browser fonts handle from `loadFonts`), `renderOptions` (extra `
|
|
641
|
+
* Options: `fonts` (the self-hosted font root URL, or a browser fonts handle from `loadFonts`), `renderOptions` (extra `toSvg` options),
|
|
636
642
|
* `startSlide` (1-based), `includeHidden`, `fullscreen` (default true), `presenterView` (open the speaker view too),
|
|
637
643
|
* `role: "presenter"` (this window is the speaker view: for a second tab or window of your page), `channel` (name, or `false`
|
|
638
644
|
* for no sync), `container`, `signal` and `now` (an injectable clock).
|
package/dist/png.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { toPng } from "./index.js";
|
|
2
|
+
export type { ToPngOptions } from "./index.js";
|
package/dist/png.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// RR-63: the PNG format entry (Node). `
|
|
1
|
+
// RR-63: the PNG format entry (Node). `toPng` loads the optional peers `@resvg/resvg-js` (and `sharp` for WebP and rotated JPEG
|
|
2
2
|
// pictures) on first use and rejects with `OPFRenderError` code `converter-missing` when they are not installed. For a page use
|
|
3
3
|
// `@openpresentation/opf-render/export-browser`.
|
|
4
|
-
export {
|
|
4
|
+
export { toPng } from "./raster.js";
|
|
@@ -5,16 +5,14 @@
|
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
8
|
import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
|
|
11
9
|
import { previewFontLayout } from "./preview-fonts.js";
|
|
12
10
|
import { scriptFontPackages } from "./script-font-pack.js";
|
|
11
|
+
import { packageRoot as rendererRoot, resolveInstalled } from "./node-resolve.js";
|
|
13
12
|
|
|
14
13
|
const ALLOWED_LICENSES = ["OFL-1.1", "Apache-2.0", "MIT", "UFL-1.0"];
|
|
15
14
|
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
16
15
|
const safeFile = /^[A-Za-z0-9_./-]+$/;
|
|
17
|
-
const rendererRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
16
|
|
|
19
17
|
function within(directory, file) {
|
|
20
18
|
const resolved = path.resolve(directory, file);
|
|
@@ -23,9 +21,8 @@ function within(directory, file) {
|
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
function resolvePackageDirectory(name, from) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
try { return path.dirname(requireFrom.resolve(`${name}/package.json`)); } catch { /* try the next location */ }
|
|
24
|
+
for (const directory of [rendererRoot, from ?? process.cwd()]) {
|
|
25
|
+
try { return path.dirname(resolveInstalled(`${name}/package.json`, directory)); } catch { /* try the next location */ }
|
|
29
26
|
}
|
|
30
27
|
return undefined;
|
|
31
28
|
}
|
package/dist/raster-text.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// For the SVG about to be rasterized (never the emitted SVG), every <text> whose content has characters of an affected script (the
|
|
10
10
|
// Indic scripts, Thai, Lao, Khmer, Myanmar), or Hangul in a KOR-language text, becomes a <g> that carries the text's attributes and
|
|
11
11
|
// draws its content cluster by cluster at absolute positions:
|
|
12
|
-
// - the x of each cluster is the fontkit
|
|
12
|
+
// - the x of each cluster is the fontkit pen at its first character in its run shaped as a whole (opf-render#189: a prefix shaped
|
|
13
|
+
// alone loses the context of the next character, such as a Thai leading vowel kerned to its consonant; the prefix's advance is used
|
|
14
|
+
// where the run's glyphs do not map back to its characters, or the run has a kinzi), measured with the face resvg will pick for the run
|
|
13
15
|
// (fontdb's family, style and weight matching over the same font files, with the same glyph fallback order), the OpenType language
|
|
14
16
|
// of the SVG's lang and the policy features, scaled to the run's textLength when it has one; text-anchor middle and end are
|
|
15
17
|
// resolved into absolute starts, baseline-shift into the y;
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
import { readFile } from "node:fs/promises";
|
|
28
30
|
import { create } from "fontkit";
|
|
29
31
|
import { disabledFeaturesFor } from "./font-compatibility.js";
|
|
32
|
+
import { pinGlyphCodePoints } from "./font-registry.js";
|
|
30
33
|
import { openTypeLanguage } from "./script-fonts.js";
|
|
31
34
|
|
|
32
35
|
const AFFECTED = /[\p{scx=Devanagari}\p{scx=Bengali}\p{scx=Gurmukhi}\p{scx=Gujarati}\p{scx=Oriya}\p{scx=Tamil}\p{scx=Telugu}\p{scx=Kannada}\p{scx=Malayalam}\p{scx=Sinhala}\p{scx=Thai}\p{scx=Lao}\p{scx=Khmer}\p{scx=Myanmar}]/u;
|
|
@@ -353,6 +356,8 @@ async function readFace(file) {
|
|
|
353
356
|
const data = new Uint8Array(await readFile(file));
|
|
354
357
|
const font = create(data);
|
|
355
358
|
if (!font?.layout || !font.unitsPerEm) return null;
|
|
359
|
+
// opf-render#125: an outline drawn for one cluster (ऱ, whose component is the nukta glyph) must not change how a later nukta shapes.
|
|
360
|
+
pinGlyphCodePoints(font);
|
|
356
361
|
const records = font.name?.records ?? {};
|
|
357
362
|
const names = Object.values(records.preferredFamily ?? {}).length ? Object.values(records.preferredFamily) : Object.values(records.fontFamily ?? {});
|
|
358
363
|
const os2 = font["OS/2"], selection = os2?.fsSelection;
|
|
@@ -433,16 +438,42 @@ function measurePrefixes(runs, ends, props) {
|
|
|
433
438
|
};
|
|
434
439
|
const widths = [];
|
|
435
440
|
try {
|
|
441
|
+
const pens = new Map();
|
|
442
|
+
// opf-render#189: the pen at a cluster boundary inside a run is read from the run shaped as a whole, so an advance its context
|
|
443
|
+
// changes (a Thai leading vowel kerned to its consonant) is the one the browser draws; a prefix shaped alone ends without it.
|
|
444
|
+
const prefix = (run, length) => {
|
|
445
|
+
if (!pens.has(run)) pens.set(run, contextPens(run.face, run.text, props));
|
|
446
|
+
return pens.get(run)?.get(length) ?? width(run.face, run.text.slice(0, length));
|
|
447
|
+
};
|
|
436
448
|
let runIndex = 0, before = 0;
|
|
437
449
|
for (const end of ends) {
|
|
438
450
|
while (runIndex < runs.length && runs[runIndex].start + runs[runIndex].text.length <= end) { before += width(runs[runIndex].face, runs[runIndex].text); runIndex++; }
|
|
439
451
|
const run = runs[runIndex];
|
|
440
|
-
widths.push(run && end > run.start ? before +
|
|
452
|
+
widths.push(run && end > run.start ? before + prefix(run, end - run.start) : before);
|
|
441
453
|
}
|
|
442
454
|
} catch { return null; }
|
|
443
455
|
return widths;
|
|
444
456
|
}
|
|
445
457
|
|
|
458
|
+
/**
|
|
459
|
+
* The pen (px) at each character offset of `value` where a glyph carrying characters starts, with `value` shaped as one run: the sum
|
|
460
|
+
* of the advances of the glyphs before it. Null for text with a kinzi (measured without it, so its offsets are not the text's).
|
|
461
|
+
*/
|
|
462
|
+
function contextPens(face, value, props) {
|
|
463
|
+
if (value.includes(KINZI)) return null;
|
|
464
|
+
const { run } = shapeRun(face, value, props);
|
|
465
|
+
const pens = new Map(), scale = props.fontSize / face.font.unitsPerEm;
|
|
466
|
+
let consumed = 0, pen = 0;
|
|
467
|
+
for (const [index, glyph] of run.glyphs.entries()) {
|
|
468
|
+
if (glyph.codePoints?.length) {
|
|
469
|
+
if (!pens.has(consumed)) pens.set(consumed, pen * scale);
|
|
470
|
+
for (const point of glyph.codePoints) consumed += point > 0xffff ? 2 : 1;
|
|
471
|
+
}
|
|
472
|
+
pen += run.positions[index].xAdvance;
|
|
473
|
+
}
|
|
474
|
+
return consumed === value.length ? pens : null;
|
|
475
|
+
}
|
|
476
|
+
|
|
446
477
|
/** The glyph outlines of one cluster, or null when fontkit needed the mark retry (its glyph placement is then not the browser's). */
|
|
447
478
|
function shapeOutlines(face, value, props) {
|
|
448
479
|
let shaped;
|