@elastic/isomer-image-takumi 0.0.0 → 0.2.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/LICENSE.txt +93 -0
- package/NOTICE.txt +1861 -0
- package/README.md +32 -0
- package/THIRD_PARTY_LICENSES.md +957 -0
- package/dist/backend.d.ts +168 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +170 -0
- package/dist/backend.js.map +1 -0
- package/dist/cjs/backend.js +207 -0
- package/dist/cjs/backend.js.map +1 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/render_pdf.js +38 -0
- package/dist/cjs/render_pdf.js.map +1 -0
- package/dist/cjs/render_png.js +50 -0
- package/dist/cjs/render_png.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/render_pdf.d.ts +42 -0
- package/dist/render_pdf.d.ts.map +1 -0
- package/dist/render_pdf.js +34 -0
- package/dist/render_pdf.js.map +1 -0
- package/dist/render_png.d.ts +75 -0
- package/dist/render_png.d.ts.map +1 -0
- package/dist/render_png.js +45 -0
- package/dist/render_png.js.map +1 -0
- package/package.json +44 -5
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type FontLoader } from '@takumi-rs/core';
|
|
2
|
+
import type { ImagesInput } from 'takumi-pdf';
|
|
3
|
+
/**
|
|
4
|
+
* The part of a `snapshot` surface result this backend reads: the markup, the
|
|
5
|
+
* stylesheet it is laid out against, and the viewport it was measured for.
|
|
6
|
+
*
|
|
7
|
+
* Declared structurally rather than imported from `@elastic/isomer-runtime`,
|
|
8
|
+
* so this backend depends on no isomer package. `SnapshotRenderResult` satisfies it.
|
|
9
|
+
*/
|
|
10
|
+
export interface ImageInput {
|
|
11
|
+
/** Static markup with a single root, as image layout requires. */
|
|
12
|
+
html: string;
|
|
13
|
+
/** The stylesheet the markup is laid out against, `light-dark(…)` already resolved. */
|
|
14
|
+
css: string;
|
|
15
|
+
/** The viewport, in CSS pixels; the output is this size. */
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The part of a `snapshot` surface's `renderPages` result this backend reads:
|
|
21
|
+
* one page's markup each, all laid out against one stylesheet at one size.
|
|
22
|
+
* Structural like {@link ImageInput}; `SnapshotPagesResult` satisfies it.
|
|
23
|
+
*/
|
|
24
|
+
export interface PdfInput {
|
|
25
|
+
/** One page per entry, in order, each static markup with a single root. */
|
|
26
|
+
pages: readonly {
|
|
27
|
+
html: string;
|
|
28
|
+
}[];
|
|
29
|
+
/** One stylesheet collected across every page. */
|
|
30
|
+
css: string;
|
|
31
|
+
/** Every page's size, in CSS pixels. */
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
}
|
|
35
|
+
/** Options for {@link createTakumiImageBackend}. */
|
|
36
|
+
export interface TakumiImageBackendOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Fonts to register, in fallback order. A pack names families in its theme
|
|
39
|
+
* and this backend holds no opinion about which; nothing is registered by
|
|
40
|
+
* default, so an unregistered family falls back to takumi's built-in face.
|
|
41
|
+
*
|
|
42
|
+
* WOFF and WOFF2 are accepted as well as raw sfnt — `@takumi-rs/core` builds
|
|
43
|
+
* with takumi's `woff`/`woff2` features, so a `@fontsource/*` file can be
|
|
44
|
+
* passed as-is, as raw bytes or a lazy `data()`. A `FontLoader` string is a
|
|
45
|
+
* URL fetched on demand, not a filesystem path.
|
|
46
|
+
*/
|
|
47
|
+
fonts?: readonly FontLoader[];
|
|
48
|
+
/** Byte budget for takumi's resource cache. `0` disables it. */
|
|
49
|
+
cacheMaxBytes?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Per-render overrides. Geometry comes from the input, not from here. */
|
|
52
|
+
export interface TakumiRenderOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Raster pixels per CSS pixel: the PNG is `input.width * scale` by
|
|
55
|
+
* `input.height * scale`, rounded. A layout fixed in CSS pixels is
|
|
56
|
+
* unchanged; rounding moves the viewport by under half a raster pixel. `2`
|
|
57
|
+
* gives a 2x raster. Defaults to `1`.
|
|
58
|
+
*/
|
|
59
|
+
scale?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Takumi's zoom at a fixed canvas: the tree is laid out at
|
|
62
|
+
* `input.width / devicePixelRatio` CSS pixels and magnified to fill the
|
|
63
|
+
* canvas, so above `1` the PNG keeps its size and shows less of the frame.
|
|
64
|
+
* For a larger raster, use {@link TakumiRenderOptions.scale}.
|
|
65
|
+
*/
|
|
66
|
+
devicePixelRatio?: number;
|
|
67
|
+
}
|
|
68
|
+
/** Document properties written into a PDF. */
|
|
69
|
+
export interface TakumiPdfMetadata {
|
|
70
|
+
title?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
authors?: string[];
|
|
73
|
+
keywords?: string[];
|
|
74
|
+
creator?: string;
|
|
75
|
+
/** UTC, `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`. Fixing it makes the bytes stable across renders. */
|
|
76
|
+
creationDate?: string;
|
|
77
|
+
}
|
|
78
|
+
/** Per-document options for {@link TakumiPdfBackend.pdf}. Geometry comes from the input. */
|
|
79
|
+
export interface TakumiPdfOptions {
|
|
80
|
+
metadata?: TakumiPdfMetadata;
|
|
81
|
+
/**
|
|
82
|
+
* What a glyph no registered font covers becomes. The default, `'error'`,
|
|
83
|
+
* rejects the render naming the code point, where `png` draws takumi's
|
|
84
|
+
* built-in face instead; `'placeholder'` draws the font's glyph 0 and
|
|
85
|
+
* `'blank'` draws nothing.
|
|
86
|
+
*/
|
|
87
|
+
uncoveredText?: 'error' | 'placeholder' | 'blank';
|
|
88
|
+
/**
|
|
89
|
+
* Bytes for the `img` sources in the tree. The PDF engine fetches nothing,
|
|
90
|
+
* so a remote `src` with no entry here draws blank; a `data:` URI needs none.
|
|
91
|
+
*/
|
|
92
|
+
images?: ImagesInput;
|
|
93
|
+
/** Writes a document outline from the headings. */
|
|
94
|
+
outline?: boolean;
|
|
95
|
+
/** BCP 47 tag for the document's language. */
|
|
96
|
+
lang?: string;
|
|
97
|
+
/** Paper color painted under every page; unset leaves the page empty. */
|
|
98
|
+
backgroundColor?: string;
|
|
99
|
+
}
|
|
100
|
+
/** A laid-out element on the canvas, in pixels, with the elements nested in it. */
|
|
101
|
+
export interface LayoutBox {
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
/** How much the box's x axis is scaled on the canvas, e.g. by `transform: scale()`. */
|
|
107
|
+
scaleX: number;
|
|
108
|
+
/** How much the box's y axis is scaled on the canvas. */
|
|
109
|
+
scaleY: number;
|
|
110
|
+
/** Whichever of `scaleX` and `scaleY` is further from 1, so any value but 1 means the box is scaled. */
|
|
111
|
+
scale: number;
|
|
112
|
+
/** Text laid out in this box, each run positioned on the canvas. */
|
|
113
|
+
runs: {
|
|
114
|
+
text: string;
|
|
115
|
+
x: number;
|
|
116
|
+
y: number;
|
|
117
|
+
width: number;
|
|
118
|
+
height: number;
|
|
119
|
+
}[];
|
|
120
|
+
/**
|
|
121
|
+
* The element's attributes other than `class`, `id`, and `style`, such as
|
|
122
|
+
* `data-*`. Absent when it has none, or when its parent's laid-out children
|
|
123
|
+
* do not line up with its elements, as when inline content folds into runs.
|
|
124
|
+
*/
|
|
125
|
+
attributes?: Record<string, string>;
|
|
126
|
+
children: LayoutBox[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Rasterizes the `snapshot` surface's output.
|
|
130
|
+
*
|
|
131
|
+
* Both methods are one call over takumi: the tree is serialized, `fromHtml`
|
|
132
|
+
* turns it into a takumi node tree and lifts the stylesheet out, and takumi
|
|
133
|
+
* lays the result out at the input's viewport.
|
|
134
|
+
*/
|
|
135
|
+
export interface TakumiImageBackend {
|
|
136
|
+
/** Renders to an encoded PNG. */
|
|
137
|
+
png(input: ImageInput, options?: TakumiRenderOptions): Promise<Buffer>;
|
|
138
|
+
/** Renders to an SVG document. Vector output, so raster options do not apply. */
|
|
139
|
+
svg(input: ImageInput): Promise<string>;
|
|
140
|
+
}
|
|
141
|
+
/** Writes the `snapshot` surface's pages as a PDF, one page each. */
|
|
142
|
+
export interface TakumiPdfBackend {
|
|
143
|
+
/**
|
|
144
|
+
* Renders one page per entry in `pages`, each the input's size with no
|
|
145
|
+
* margin, so a frame fills its page. Vector output with selectable text and
|
|
146
|
+
* the registered fonts subset and embedded. Throws on an empty `pages` list.
|
|
147
|
+
*/
|
|
148
|
+
pdf(input: PdfInput, options?: TakumiPdfOptions): Promise<Buffer>;
|
|
149
|
+
}
|
|
150
|
+
/** A {@link TakumiImageBackend} that also measures layout. */
|
|
151
|
+
export interface TakumiMeasuringBackend extends TakumiImageBackend {
|
|
152
|
+
/** Lays the input out as {@link TakumiImageBackend.png} would, and returns every box, e.g. to find content past its area. */
|
|
153
|
+
measure(input: ImageInput): Promise<LayoutBox>;
|
|
154
|
+
}
|
|
155
|
+
/** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */
|
|
156
|
+
export declare const TAKUMI_FORMATS: readonly ["png", "svg", "pdf"];
|
|
157
|
+
/** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */
|
|
158
|
+
export interface TakumiBackend extends TakumiMeasuringBackend, TakumiPdfBackend {
|
|
159
|
+
/** {@link TAKUMI_FORMATS}: one entry per method that writes a format. */
|
|
160
|
+
readonly formats: typeof TAKUMI_FORMATS;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are
|
|
164
|
+
* registered once, in order, before the first render; the PDF engine is
|
|
165
|
+
* imported and created on the first `pdf`, and registers them again.
|
|
166
|
+
*/
|
|
167
|
+
export declare const createTakumiImageBackend: ({ fonts, cacheMaxBytes, }?: TakumiImageBackendOptions) => TakumiBackend;
|
|
168
|
+
//# sourceMappingURL=backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
3
|
+
* or more contributor license agreements. Licensed under the Elastic License
|
|
4
|
+
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
5
|
+
* 2.0.
|
|
6
|
+
*/
|
|
7
|
+
import { Renderer, } from '@takumi-rs/core';
|
|
8
|
+
import { fromHtml } from '@takumi-rs/helpers/html';
|
|
9
|
+
/** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */
|
|
10
|
+
export const TAKUMI_FORMATS = ['png', 'svg', 'pdf'];
|
|
11
|
+
/** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */
|
|
12
|
+
const mapRect = ([a, b, c, d, e, f], x, y, width, height) => {
|
|
13
|
+
const corners = [
|
|
14
|
+
[x, y],
|
|
15
|
+
[x + width, y],
|
|
16
|
+
[x, y + height],
|
|
17
|
+
[x + width, y + height],
|
|
18
|
+
].map(([px = 0, py = 0]) => [a * px + c * py + e, b * px + d * py + f]);
|
|
19
|
+
const xs = corners.map(([cx = 0]) => cx);
|
|
20
|
+
const ys = corners.map(([, cy = 0]) => cy);
|
|
21
|
+
const left = Math.min(...xs);
|
|
22
|
+
const top = Math.min(...ys);
|
|
23
|
+
return {
|
|
24
|
+
x: left,
|
|
25
|
+
y: top,
|
|
26
|
+
width: Math.max(...xs) - left,
|
|
27
|
+
height: Math.max(...ys) - top,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Takumi reports each node's size in its own units with its full canvas
|
|
32
|
+
* transform, and carries no attributes, so those come from the `fromHtml`
|
|
33
|
+
* node at the same position.
|
|
34
|
+
*/
|
|
35
|
+
const toLayoutBox = ({ width, height, transform, runs, children }, source) => {
|
|
36
|
+
const [a, b, c, d] = transform;
|
|
37
|
+
const scaleX = Math.hypot(a, b);
|
|
38
|
+
const scaleY = Math.hypot(c, d);
|
|
39
|
+
const sources = source?.type === 'container' ? (source.children ?? []) : [];
|
|
40
|
+
const paired = sources.length === children.length;
|
|
41
|
+
return {
|
|
42
|
+
...mapRect(transform, 0, 0, width, height),
|
|
43
|
+
scaleX,
|
|
44
|
+
scaleY,
|
|
45
|
+
scale: Math.abs(scaleY - 1) > Math.abs(scaleX - 1) ? scaleY : scaleX,
|
|
46
|
+
runs: runs.map((run) => ({
|
|
47
|
+
text: run.text,
|
|
48
|
+
...mapRect(transform, run.x, run.y, run.width, run.height),
|
|
49
|
+
})),
|
|
50
|
+
...(source?.attributes && { attributes: { ...source.attributes } }),
|
|
51
|
+
children: children.map((child, index) => toLayoutBox(child, paired ? sources[index] : undefined)),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
const escapeStyleEndTags = (css) => css.replace(/<\/style/gi, (tag) => tag.replace('/', '\\/'));
|
|
55
|
+
/**
|
|
56
|
+
* Inlines the stylesheet ahead of the markup, the form `fromHtml` lifts into
|
|
57
|
+
* its own `css` list. Passing the stylesheet as a separate `CssInput` entry is
|
|
58
|
+
* a different path through takumi and changes the committed PNG bytes.
|
|
59
|
+
*/
|
|
60
|
+
const withStylesheet = (css, markup) => `<style>${escapeStyleEndTags(css)}</style>${markup}`;
|
|
61
|
+
const toTakumiSource = ({ html, css }) => fromHtml(withStylesheet(css, html));
|
|
62
|
+
/**
|
|
63
|
+
* One side of a scaled canvas, in whole pixels. Takumi takes one ratio for
|
|
64
|
+
* both axes, so the ratio stays `scale` and the canvas absorbs the rounding.
|
|
65
|
+
*/
|
|
66
|
+
const toRasterSide = (side, scale) => scale === 1 ? side : Math.max(1, Math.round(side * scale));
|
|
67
|
+
/** Inline, so no pack class name can collide with it. */
|
|
68
|
+
const PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';
|
|
69
|
+
/** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */
|
|
70
|
+
const toPagedSource = ({ pages, css, width, height }) => {
|
|
71
|
+
if (pages.length === 0) {
|
|
72
|
+
throw new Error('pdf: no pages; a document needs at least one');
|
|
73
|
+
}
|
|
74
|
+
const markup = pages
|
|
75
|
+
.map(({ html }) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${html}</div>`)
|
|
76
|
+
.join('');
|
|
77
|
+
return {
|
|
78
|
+
...fromHtml(withStylesheet(css, `<div>${markup}</div>`)),
|
|
79
|
+
width,
|
|
80
|
+
height,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/** Runs `create` once for every caller. A rejection is forgotten, so the next call retries instead of inheriting it. */
|
|
84
|
+
const once = (create) => {
|
|
85
|
+
let pending;
|
|
86
|
+
return () => {
|
|
87
|
+
pending ??= create().catch((error) => {
|
|
88
|
+
pending = undefined;
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
return pending;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are
|
|
96
|
+
* registered once, in order, before the first render; the PDF engine is
|
|
97
|
+
* imported and created on the first `pdf`, and registers them again.
|
|
98
|
+
*/
|
|
99
|
+
export const createTakumiImageBackend = ({ fonts = [], cacheMaxBytes, } = {}) => {
|
|
100
|
+
const renderer = new Renderer(cacheMaxBytes === undefined ? undefined : { cacheMaxBytes });
|
|
101
|
+
/**
|
|
102
|
+
* Registration is async and every render needs it, so each engine registers
|
|
103
|
+
* once and is awaited. Sequential because the registration order is the
|
|
104
|
+
* default fallback order.
|
|
105
|
+
*/
|
|
106
|
+
const registerFonts = async (engine) => {
|
|
107
|
+
for (const font of fonts) {
|
|
108
|
+
await engine.registerFont(font);
|
|
109
|
+
}
|
|
110
|
+
return engine;
|
|
111
|
+
};
|
|
112
|
+
const ready = once(() => registerFonts(renderer));
|
|
113
|
+
/** A second engine, imported and created on the first PDF, so a raster-only host never loads its wasm. */
|
|
114
|
+
const pdfReady = once(async () => {
|
|
115
|
+
const { PdfRenderer } = await import('takumi-pdf');
|
|
116
|
+
return registerFonts(new PdfRenderer());
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
formats: TAKUMI_FORMATS,
|
|
120
|
+
png: async (input, { scale = 1, devicePixelRatio } = {}) => {
|
|
121
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
122
|
+
throw new RangeError(`png: scale must be a positive finite number, got ${scale}`);
|
|
123
|
+
}
|
|
124
|
+
await ready();
|
|
125
|
+
const { node, css } = toTakumiSource(input);
|
|
126
|
+
const renderOptions = {
|
|
127
|
+
width: toRasterSide(input.width, scale),
|
|
128
|
+
height: toRasterSide(input.height, scale),
|
|
129
|
+
format: 'png',
|
|
130
|
+
css,
|
|
131
|
+
};
|
|
132
|
+
if (devicePixelRatio !== undefined || scale !== 1) {
|
|
133
|
+
renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;
|
|
134
|
+
}
|
|
135
|
+
return renderer.render(node, renderOptions);
|
|
136
|
+
},
|
|
137
|
+
svg: async (input) => {
|
|
138
|
+
await ready();
|
|
139
|
+
const { node, css } = toTakumiSource(input);
|
|
140
|
+
return renderer.renderSvg(node, {
|
|
141
|
+
width: input.width,
|
|
142
|
+
height: input.height,
|
|
143
|
+
css,
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
measure: async (input) => {
|
|
147
|
+
await ready();
|
|
148
|
+
const { node, css } = toTakumiSource(input);
|
|
149
|
+
const measured = await renderer.measure(node, {
|
|
150
|
+
width: input.width,
|
|
151
|
+
height: input.height,
|
|
152
|
+
css,
|
|
153
|
+
});
|
|
154
|
+
return toLayoutBox(measured, node);
|
|
155
|
+
},
|
|
156
|
+
pdf: async (input, options = {}) => {
|
|
157
|
+
const engine = await pdfReady();
|
|
158
|
+
const { node, css, width, height } = toPagedSource(input);
|
|
159
|
+
const renderOptions = {
|
|
160
|
+
...options,
|
|
161
|
+
size: { width, height },
|
|
162
|
+
margin: 0,
|
|
163
|
+
css,
|
|
164
|
+
};
|
|
165
|
+
// Copied out of wasm memory, which a later render can move.
|
|
166
|
+
return Buffer.from(await engine.render(node, renderOptions));
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one\n * or more contributor license agreements. Licensed under the Elastic License\n * 2.0; you may not use this file except in compliance with the Elastic License\n * 2.0.\n */\n\nimport {\n type FontLoader,\n type MeasuredNode,\n Renderer,\n type RenderOptions,\n} from '@takumi-rs/core';\nimport { fromHtml } from '@takumi-rs/helpers/html';\nimport type {\n ImagesInput,\n RenderOptions as PdfRenderOptions,\n} from 'takumi-pdf';\n\n/**\n * The part of a `snapshot` surface result this backend reads: the markup, the\n * stylesheet it is laid out against, and the viewport it was measured for.\n *\n * Declared structurally rather than imported from `@elastic/isomer-runtime`,\n * so this backend depends on no isomer package. `SnapshotRenderResult` satisfies it.\n */\nexport interface ImageInput {\n /** Static markup with a single root, as image layout requires. */\n html: string;\n /** The stylesheet the markup is laid out against, `light-dark(…)` already resolved. */\n css: string;\n /** The viewport, in CSS pixels; the output is this size. */\n width: number;\n height: number;\n}\n\n/**\n * The part of a `snapshot` surface's `renderPages` result this backend reads:\n * one page's markup each, all laid out against one stylesheet at one size.\n * Structural like {@link ImageInput}; `SnapshotPagesResult` satisfies it.\n */\nexport interface PdfInput {\n /** One page per entry, in order, each static markup with a single root. */\n pages: readonly { html: string }[];\n /** One stylesheet collected across every page. */\n css: string;\n /** Every page's size, in CSS pixels. */\n width: number;\n height: number;\n}\n\n/** Options for {@link createTakumiImageBackend}. */\nexport interface TakumiImageBackendOptions {\n /**\n * Fonts to register, in fallback order. A pack names families in its theme\n * and this backend holds no opinion about which; nothing is registered by\n * default, so an unregistered family falls back to takumi's built-in face.\n *\n * WOFF and WOFF2 are accepted as well as raw sfnt — `@takumi-rs/core` builds\n * with takumi's `woff`/`woff2` features, so a `@fontsource/*` file can be\n * passed as-is, as raw bytes or a lazy `data()`. A `FontLoader` string is a\n * URL fetched on demand, not a filesystem path.\n */\n fonts?: readonly FontLoader[];\n /** Byte budget for takumi's resource cache. `0` disables it. */\n cacheMaxBytes?: number;\n}\n\n/** Per-render overrides. Geometry comes from the input, not from here. */\nexport interface TakumiRenderOptions {\n /**\n * Raster pixels per CSS pixel: the PNG is `input.width * scale` by\n * `input.height * scale`, rounded. A layout fixed in CSS pixels is\n * unchanged; rounding moves the viewport by under half a raster pixel. `2`\n * gives a 2x raster. Defaults to `1`.\n */\n scale?: number;\n /**\n * Takumi's zoom at a fixed canvas: the tree is laid out at\n * `input.width / devicePixelRatio` CSS pixels and magnified to fill the\n * canvas, so above `1` the PNG keeps its size and shows less of the frame.\n * For a larger raster, use {@link TakumiRenderOptions.scale}.\n */\n devicePixelRatio?: number;\n}\n\n/** Document properties written into a PDF. */\nexport interface TakumiPdfMetadata {\n title?: string;\n description?: string;\n authors?: string[];\n keywords?: string[];\n creator?: string;\n /** UTC, `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`. Fixing it makes the bytes stable across renders. */\n creationDate?: string;\n}\n\n/** Per-document options for {@link TakumiPdfBackend.pdf}. Geometry comes from the input. */\nexport interface TakumiPdfOptions {\n metadata?: TakumiPdfMetadata;\n /**\n * What a glyph no registered font covers becomes. The default, `'error'`,\n * rejects the render naming the code point, where `png` draws takumi's\n * built-in face instead; `'placeholder'` draws the font's glyph 0 and\n * `'blank'` draws nothing.\n */\n uncoveredText?: 'error' | 'placeholder' | 'blank';\n /**\n * Bytes for the `img` sources in the tree. The PDF engine fetches nothing,\n * so a remote `src` with no entry here draws blank; a `data:` URI needs none.\n */\n images?: ImagesInput;\n /** Writes a document outline from the headings. */\n outline?: boolean;\n /** BCP 47 tag for the document's language. */\n lang?: string;\n /** Paper color painted under every page; unset leaves the page empty. */\n backgroundColor?: string;\n}\n\n/** A laid-out element on the canvas, in pixels, with the elements nested in it. */\nexport interface LayoutBox {\n x: number;\n y: number;\n width: number;\n height: number;\n /** How much the box's x axis is scaled on the canvas, e.g. by `transform: scale()`. */\n scaleX: number;\n /** How much the box's y axis is scaled on the canvas. */\n scaleY: number;\n /** Whichever of `scaleX` and `scaleY` is further from 1, so any value but 1 means the box is scaled. */\n scale: number;\n /** Text laid out in this box, each run positioned on the canvas. */\n runs: { text: string; x: number; y: number; width: number; height: number }[];\n /**\n * The element's attributes other than `class`, `id`, and `style`, such as\n * `data-*`. Absent when it has none, or when its parent's laid-out children\n * do not line up with its elements, as when inline content folds into runs.\n */\n attributes?: Record<string, string>;\n children: LayoutBox[];\n}\n\n/**\n * Rasterizes the `snapshot` surface's output.\n *\n * Both methods are one call over takumi: the tree is serialized, `fromHtml`\n * turns it into a takumi node tree and lifts the stylesheet out, and takumi\n * lays the result out at the input's viewport.\n */\nexport interface TakumiImageBackend {\n /** Renders to an encoded PNG. */\n png(input: ImageInput, options?: TakumiRenderOptions): Promise<Buffer>;\n /** Renders to an SVG document. Vector output, so raster options do not apply. */\n svg(input: ImageInput): Promise<string>;\n}\n\n/** Writes the `snapshot` surface's pages as a PDF, one page each. */\nexport interface TakumiPdfBackend {\n /**\n * Renders one page per entry in `pages`, each the input's size with no\n * margin, so a frame fills its page. Vector output with selectable text and\n * the registered fonts subset and embedded. Throws on an empty `pages` list.\n */\n pdf(input: PdfInput, options?: TakumiPdfOptions): Promise<Buffer>;\n}\n\n/** A {@link TakumiImageBackend} that also measures layout. */\nexport interface TakumiMeasuringBackend extends TakumiImageBackend {\n /** Lays the input out as {@link TakumiImageBackend.png} would, and returns every box, e.g. to find content past its area. */\n measure(input: ImageInput): Promise<LayoutBox>;\n}\n\n/** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */\nexport const TAKUMI_FORMATS = ['png', 'svg', 'pdf'] as const;\n\n/** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */\nexport interface TakumiBackend\n extends TakumiMeasuringBackend, TakumiPdfBackend {\n /** {@link TAKUMI_FORMATS}: one entry per method that writes a format. */\n readonly formats: typeof TAKUMI_FORMATS;\n}\n\ntype Matrix = MeasuredNode['transform'];\n\ntype SourceNode = ReturnType<typeof fromHtml>['node'];\n\n/** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */\nconst mapRect = (\n [a, b, c, d, e, f]: Matrix,\n x: number,\n y: number,\n width: number,\n height: number\n): { x: number; y: number; width: number; height: number } => {\n const corners = [\n [x, y],\n [x + width, y],\n [x, y + height],\n [x + width, y + height],\n ].map(([px = 0, py = 0]) => [a * px + c * py + e, b * px + d * py + f]);\n const xs = corners.map(([cx = 0]) => cx);\n const ys = corners.map(([, cy = 0]) => cy);\n const left = Math.min(...xs);\n const top = Math.min(...ys);\n return {\n x: left,\n y: top,\n width: Math.max(...xs) - left,\n height: Math.max(...ys) - top,\n };\n};\n\n/**\n * Takumi reports each node's size in its own units with its full canvas\n * transform, and carries no attributes, so those come from the `fromHtml`\n * node at the same position.\n */\nconst toLayoutBox = (\n { width, height, transform, runs, children }: MeasuredNode,\n source: SourceNode | undefined\n): LayoutBox => {\n const [a, b, c, d] = transform;\n const scaleX = Math.hypot(a, b);\n const scaleY = Math.hypot(c, d);\n const sources = source?.type === 'container' ? (source.children ?? []) : [];\n const paired = sources.length === children.length;\n return {\n ...mapRect(transform, 0, 0, width, height),\n scaleX,\n scaleY,\n scale: Math.abs(scaleY - 1) > Math.abs(scaleX - 1) ? scaleY : scaleX,\n runs: runs.map((run) => ({\n text: run.text,\n ...mapRect(transform, run.x, run.y, run.width, run.height),\n })),\n ...(source?.attributes && { attributes: { ...source.attributes } }),\n children: children.map((child, index) =>\n toLayoutBox(child, paired ? sources[index] : undefined)\n ),\n };\n};\n\nconst escapeStyleEndTags = (css: string) =>\n css.replace(/<\\/style/gi, (tag) => tag.replace('/', '\\\\/'));\n\n/**\n * Inlines the stylesheet ahead of the markup, the form `fromHtml` lifts into\n * its own `css` list. Passing the stylesheet as a separate `CssInput` entry is\n * a different path through takumi and changes the committed PNG bytes.\n */\nconst withStylesheet = (css: string, markup: string) =>\n `<style>${escapeStyleEndTags(css)}</style>${markup}`;\n\nconst toTakumiSource = ({ html, css }: ImageInput) =>\n fromHtml(withStylesheet(css, html));\n\n/**\n * One side of a scaled canvas, in whole pixels. Takumi takes one ratio for\n * both axes, so the ratio stays `scale` and the canvas absorbs the rounding.\n */\nconst toRasterSide = (side: number, scale: number) =>\n scale === 1 ? side : Math.max(1, Math.round(side * scale));\n\n/** Inline, so no pack class name can collide with it. */\nconst PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';\n\n/** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */\nconst toPagedSource = ({ pages, css, width, height }: PdfInput) => {\n if (pages.length === 0) {\n throw new Error('pdf: no pages; a document needs at least one');\n }\n const markup = pages\n .map(\n ({ html }) =>\n `<div style=\"width:${width}px;height:${height}px;${PAGE_STYLE}\">${html}</div>`\n )\n .join('');\n return {\n ...fromHtml(withStylesheet(css, `<div>${markup}</div>`)),\n width,\n height,\n };\n};\n\n/** Runs `create` once for every caller. A rejection is forgotten, so the next call retries instead of inheriting it. */\nconst once = <T>(create: () => Promise<T>): (() => Promise<T>) => {\n let pending: Promise<T> | undefined;\n return () => {\n pending ??= create().catch((error: unknown) => {\n pending = undefined;\n throw error;\n });\n return pending;\n };\n};\n\n/**\n * Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are\n * registered once, in order, before the first render; the PDF engine is\n * imported and created on the first `pdf`, and registers them again.\n */\nexport const createTakumiImageBackend = ({\n fonts = [],\n cacheMaxBytes,\n}: TakumiImageBackendOptions = {}): TakumiBackend => {\n const renderer = new Renderer(\n cacheMaxBytes === undefined ? undefined : { cacheMaxBytes }\n );\n\n /**\n * Registration is async and every render needs it, so each engine registers\n * once and is awaited. Sequential because the registration order is the\n * default fallback order.\n */\n const registerFonts = async <\n TEngine extends { registerFont(font: FontLoader): Promise<unknown> },\n >(\n engine: TEngine\n ): Promise<TEngine> => {\n for (const font of fonts) {\n await engine.registerFont(font);\n }\n return engine;\n };\n const ready = once(() => registerFonts(renderer));\n /** A second engine, imported and created on the first PDF, so a raster-only host never loads its wasm. */\n const pdfReady = once(async () => {\n const { PdfRenderer } = await import('takumi-pdf');\n return registerFonts(new PdfRenderer());\n });\n\n return {\n formats: TAKUMI_FORMATS,\n png: async (input, { scale = 1, devicePixelRatio } = {}) => {\n if (!Number.isFinite(scale) || scale <= 0) {\n throw new RangeError(\n `png: scale must be a positive finite number, got ${scale}`\n );\n }\n await ready();\n const { node, css } = toTakumiSource(input);\n const renderOptions: RenderOptions = {\n width: toRasterSide(input.width, scale),\n height: toRasterSide(input.height, scale),\n format: 'png',\n css,\n };\n if (devicePixelRatio !== undefined || scale !== 1) {\n renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;\n }\n return renderer.render(node, renderOptions);\n },\n svg: async (input) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n return renderer.renderSvg(node, {\n width: input.width,\n height: input.height,\n css,\n });\n },\n measure: async (input) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n const measured = await renderer.measure(node, {\n width: input.width,\n height: input.height,\n css,\n });\n return toLayoutBox(measured, node);\n },\n pdf: async (input, options = {}) => {\n const engine = await pdfReady();\n const { node, css, width, height } = toPagedSource(input);\n const renderOptions: PdfRenderOptions = {\n ...options,\n size: { width, height },\n margin: 0,\n css,\n };\n // Copied out of wasm memory, which a later render can move.\n return Buffer.from(await engine.render(node, renderOptions));\n },\n };\n};\n"]}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
4
|
+
* or more contributor license agreements. Licensed under the Elastic License
|
|
5
|
+
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
6
|
+
* 2.0.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.createTakumiImageBackend = exports.TAKUMI_FORMATS = void 0;
|
|
43
|
+
const core_1 = require("@takumi-rs/core");
|
|
44
|
+
const html_1 = require("@takumi-rs/helpers/html");
|
|
45
|
+
/** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */
|
|
46
|
+
exports.TAKUMI_FORMATS = ['png', 'svg', 'pdf'];
|
|
47
|
+
/** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */
|
|
48
|
+
const mapRect = ([a, b, c, d, e, f], x, y, width, height) => {
|
|
49
|
+
const corners = [
|
|
50
|
+
[x, y],
|
|
51
|
+
[x + width, y],
|
|
52
|
+
[x, y + height],
|
|
53
|
+
[x + width, y + height],
|
|
54
|
+
].map(([px = 0, py = 0]) => [a * px + c * py + e, b * px + d * py + f]);
|
|
55
|
+
const xs = corners.map(([cx = 0]) => cx);
|
|
56
|
+
const ys = corners.map(([, cy = 0]) => cy);
|
|
57
|
+
const left = Math.min(...xs);
|
|
58
|
+
const top = Math.min(...ys);
|
|
59
|
+
return {
|
|
60
|
+
x: left,
|
|
61
|
+
y: top,
|
|
62
|
+
width: Math.max(...xs) - left,
|
|
63
|
+
height: Math.max(...ys) - top,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Takumi reports each node's size in its own units with its full canvas
|
|
68
|
+
* transform, and carries no attributes, so those come from the `fromHtml`
|
|
69
|
+
* node at the same position.
|
|
70
|
+
*/
|
|
71
|
+
const toLayoutBox = ({ width, height, transform, runs, children }, source) => {
|
|
72
|
+
const [a, b, c, d] = transform;
|
|
73
|
+
const scaleX = Math.hypot(a, b);
|
|
74
|
+
const scaleY = Math.hypot(c, d);
|
|
75
|
+
const sources = source?.type === 'container' ? (source.children ?? []) : [];
|
|
76
|
+
const paired = sources.length === children.length;
|
|
77
|
+
return {
|
|
78
|
+
...mapRect(transform, 0, 0, width, height),
|
|
79
|
+
scaleX,
|
|
80
|
+
scaleY,
|
|
81
|
+
scale: Math.abs(scaleY - 1) > Math.abs(scaleX - 1) ? scaleY : scaleX,
|
|
82
|
+
runs: runs.map((run) => ({
|
|
83
|
+
text: run.text,
|
|
84
|
+
...mapRect(transform, run.x, run.y, run.width, run.height),
|
|
85
|
+
})),
|
|
86
|
+
...(source?.attributes && { attributes: { ...source.attributes } }),
|
|
87
|
+
children: children.map((child, index) => toLayoutBox(child, paired ? sources[index] : undefined)),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const escapeStyleEndTags = (css) => css.replace(/<\/style/gi, (tag) => tag.replace('/', '\\/'));
|
|
91
|
+
/**
|
|
92
|
+
* Inlines the stylesheet ahead of the markup, the form `fromHtml` lifts into
|
|
93
|
+
* its own `css` list. Passing the stylesheet as a separate `CssInput` entry is
|
|
94
|
+
* a different path through takumi and changes the committed PNG bytes.
|
|
95
|
+
*/
|
|
96
|
+
const withStylesheet = (css, markup) => `<style>${escapeStyleEndTags(css)}</style>${markup}`;
|
|
97
|
+
const toTakumiSource = ({ html, css }) => (0, html_1.fromHtml)(withStylesheet(css, html));
|
|
98
|
+
/**
|
|
99
|
+
* One side of a scaled canvas, in whole pixels. Takumi takes one ratio for
|
|
100
|
+
* both axes, so the ratio stays `scale` and the canvas absorbs the rounding.
|
|
101
|
+
*/
|
|
102
|
+
const toRasterSide = (side, scale) => scale === 1 ? side : Math.max(1, Math.round(side * scale));
|
|
103
|
+
/** Inline, so no pack class name can collide with it. */
|
|
104
|
+
const PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';
|
|
105
|
+
/** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */
|
|
106
|
+
const toPagedSource = ({ pages, css, width, height }) => {
|
|
107
|
+
if (pages.length === 0) {
|
|
108
|
+
throw new Error('pdf: no pages; a document needs at least one');
|
|
109
|
+
}
|
|
110
|
+
const markup = pages
|
|
111
|
+
.map(({ html }) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${html}</div>`)
|
|
112
|
+
.join('');
|
|
113
|
+
return {
|
|
114
|
+
...(0, html_1.fromHtml)(withStylesheet(css, `<div>${markup}</div>`)),
|
|
115
|
+
width,
|
|
116
|
+
height,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/** Runs `create` once for every caller. A rejection is forgotten, so the next call retries instead of inheriting it. */
|
|
120
|
+
const once = (create) => {
|
|
121
|
+
let pending;
|
|
122
|
+
return () => {
|
|
123
|
+
pending ??= create().catch((error) => {
|
|
124
|
+
pending = undefined;
|
|
125
|
+
throw error;
|
|
126
|
+
});
|
|
127
|
+
return pending;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are
|
|
132
|
+
* registered once, in order, before the first render; the PDF engine is
|
|
133
|
+
* imported and created on the first `pdf`, and registers them again.
|
|
134
|
+
*/
|
|
135
|
+
const createTakumiImageBackend = ({ fonts = [], cacheMaxBytes, } = {}) => {
|
|
136
|
+
const renderer = new core_1.Renderer(cacheMaxBytes === undefined ? undefined : { cacheMaxBytes });
|
|
137
|
+
/**
|
|
138
|
+
* Registration is async and every render needs it, so each engine registers
|
|
139
|
+
* once and is awaited. Sequential because the registration order is the
|
|
140
|
+
* default fallback order.
|
|
141
|
+
*/
|
|
142
|
+
const registerFonts = async (engine) => {
|
|
143
|
+
for (const font of fonts) {
|
|
144
|
+
await engine.registerFont(font);
|
|
145
|
+
}
|
|
146
|
+
return engine;
|
|
147
|
+
};
|
|
148
|
+
const ready = once(() => registerFonts(renderer));
|
|
149
|
+
/** A second engine, imported and created on the first PDF, so a raster-only host never loads its wasm. */
|
|
150
|
+
const pdfReady = once(async () => {
|
|
151
|
+
const { PdfRenderer } = await Promise.resolve().then(() => __importStar(require('takumi-pdf')));
|
|
152
|
+
return registerFonts(new PdfRenderer());
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
formats: exports.TAKUMI_FORMATS,
|
|
156
|
+
png: async (input, { scale = 1, devicePixelRatio } = {}) => {
|
|
157
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
158
|
+
throw new RangeError(`png: scale must be a positive finite number, got ${scale}`);
|
|
159
|
+
}
|
|
160
|
+
await ready();
|
|
161
|
+
const { node, css } = toTakumiSource(input);
|
|
162
|
+
const renderOptions = {
|
|
163
|
+
width: toRasterSide(input.width, scale),
|
|
164
|
+
height: toRasterSide(input.height, scale),
|
|
165
|
+
format: 'png',
|
|
166
|
+
css,
|
|
167
|
+
};
|
|
168
|
+
if (devicePixelRatio !== undefined || scale !== 1) {
|
|
169
|
+
renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;
|
|
170
|
+
}
|
|
171
|
+
return renderer.render(node, renderOptions);
|
|
172
|
+
},
|
|
173
|
+
svg: async (input) => {
|
|
174
|
+
await ready();
|
|
175
|
+
const { node, css } = toTakumiSource(input);
|
|
176
|
+
return renderer.renderSvg(node, {
|
|
177
|
+
width: input.width,
|
|
178
|
+
height: input.height,
|
|
179
|
+
css,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
measure: async (input) => {
|
|
183
|
+
await ready();
|
|
184
|
+
const { node, css } = toTakumiSource(input);
|
|
185
|
+
const measured = await renderer.measure(node, {
|
|
186
|
+
width: input.width,
|
|
187
|
+
height: input.height,
|
|
188
|
+
css,
|
|
189
|
+
});
|
|
190
|
+
return toLayoutBox(measured, node);
|
|
191
|
+
},
|
|
192
|
+
pdf: async (input, options = {}) => {
|
|
193
|
+
const engine = await pdfReady();
|
|
194
|
+
const { node, css, width, height } = toPagedSource(input);
|
|
195
|
+
const renderOptions = {
|
|
196
|
+
...options,
|
|
197
|
+
size: { width, height },
|
|
198
|
+
margin: 0,
|
|
199
|
+
css,
|
|
200
|
+
};
|
|
201
|
+
// Copied out of wasm memory, which a later render can move.
|
|
202
|
+
return Buffer.from(await engine.render(node, renderOptions));
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
exports.createTakumiImageBackend = createTakumiImageBackend;
|
|
207
|
+
//# sourceMappingURL=backend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one\n * or more contributor license agreements. Licensed under the Elastic License\n * 2.0; you may not use this file except in compliance with the Elastic License\n * 2.0.\n */\n\nimport {\n type FontLoader,\n type MeasuredNode,\n Renderer,\n type RenderOptions,\n} from '@takumi-rs/core';\nimport { fromHtml } from '@takumi-rs/helpers/html';\nimport type {\n ImagesInput,\n RenderOptions as PdfRenderOptions,\n} from 'takumi-pdf';\n\n/**\n * The part of a `snapshot` surface result this backend reads: the markup, the\n * stylesheet it is laid out against, and the viewport it was measured for.\n *\n * Declared structurally rather than imported from `@elastic/isomer-runtime`,\n * so this backend depends on no isomer package. `SnapshotRenderResult` satisfies it.\n */\nexport interface ImageInput {\n /** Static markup with a single root, as image layout requires. */\n html: string;\n /** The stylesheet the markup is laid out against, `light-dark(…)` already resolved. */\n css: string;\n /** The viewport, in CSS pixels; the output is this size. */\n width: number;\n height: number;\n}\n\n/**\n * The part of a `snapshot` surface's `renderPages` result this backend reads:\n * one page's markup each, all laid out against one stylesheet at one size.\n * Structural like {@link ImageInput}; `SnapshotPagesResult` satisfies it.\n */\nexport interface PdfInput {\n /** One page per entry, in order, each static markup with a single root. */\n pages: readonly { html: string }[];\n /** One stylesheet collected across every page. */\n css: string;\n /** Every page's size, in CSS pixels. */\n width: number;\n height: number;\n}\n\n/** Options for {@link createTakumiImageBackend}. */\nexport interface TakumiImageBackendOptions {\n /**\n * Fonts to register, in fallback order. A pack names families in its theme\n * and this backend holds no opinion about which; nothing is registered by\n * default, so an unregistered family falls back to takumi's built-in face.\n *\n * WOFF and WOFF2 are accepted as well as raw sfnt — `@takumi-rs/core` builds\n * with takumi's `woff`/`woff2` features, so a `@fontsource/*` file can be\n * passed as-is, as raw bytes or a lazy `data()`. A `FontLoader` string is a\n * URL fetched on demand, not a filesystem path.\n */\n fonts?: readonly FontLoader[];\n /** Byte budget for takumi's resource cache. `0` disables it. */\n cacheMaxBytes?: number;\n}\n\n/** Per-render overrides. Geometry comes from the input, not from here. */\nexport interface TakumiRenderOptions {\n /**\n * Raster pixels per CSS pixel: the PNG is `input.width * scale` by\n * `input.height * scale`, rounded. A layout fixed in CSS pixels is\n * unchanged; rounding moves the viewport by under half a raster pixel. `2`\n * gives a 2x raster. Defaults to `1`.\n */\n scale?: number;\n /**\n * Takumi's zoom at a fixed canvas: the tree is laid out at\n * `input.width / devicePixelRatio` CSS pixels and magnified to fill the\n * canvas, so above `1` the PNG keeps its size and shows less of the frame.\n * For a larger raster, use {@link TakumiRenderOptions.scale}.\n */\n devicePixelRatio?: number;\n}\n\n/** Document properties written into a PDF. */\nexport interface TakumiPdfMetadata {\n title?: string;\n description?: string;\n authors?: string[];\n keywords?: string[];\n creator?: string;\n /** UTC, `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`. Fixing it makes the bytes stable across renders. */\n creationDate?: string;\n}\n\n/** Per-document options for {@link TakumiPdfBackend.pdf}. Geometry comes from the input. */\nexport interface TakumiPdfOptions {\n metadata?: TakumiPdfMetadata;\n /**\n * What a glyph no registered font covers becomes. The default, `'error'`,\n * rejects the render naming the code point, where `png` draws takumi's\n * built-in face instead; `'placeholder'` draws the font's glyph 0 and\n * `'blank'` draws nothing.\n */\n uncoveredText?: 'error' | 'placeholder' | 'blank';\n /**\n * Bytes for the `img` sources in the tree. The PDF engine fetches nothing,\n * so a remote `src` with no entry here draws blank; a `data:` URI needs none.\n */\n images?: ImagesInput;\n /** Writes a document outline from the headings. */\n outline?: boolean;\n /** BCP 47 tag for the document's language. */\n lang?: string;\n /** Paper color painted under every page; unset leaves the page empty. */\n backgroundColor?: string;\n}\n\n/** A laid-out element on the canvas, in pixels, with the elements nested in it. */\nexport interface LayoutBox {\n x: number;\n y: number;\n width: number;\n height: number;\n /** How much the box's x axis is scaled on the canvas, e.g. by `transform: scale()`. */\n scaleX: number;\n /** How much the box's y axis is scaled on the canvas. */\n scaleY: number;\n /** Whichever of `scaleX` and `scaleY` is further from 1, so any value but 1 means the box is scaled. */\n scale: number;\n /** Text laid out in this box, each run positioned on the canvas. */\n runs: { text: string; x: number; y: number; width: number; height: number }[];\n /**\n * The element's attributes other than `class`, `id`, and `style`, such as\n * `data-*`. Absent when it has none, or when its parent's laid-out children\n * do not line up with its elements, as when inline content folds into runs.\n */\n attributes?: Record<string, string>;\n children: LayoutBox[];\n}\n\n/**\n * Rasterizes the `snapshot` surface's output.\n *\n * Both methods are one call over takumi: the tree is serialized, `fromHtml`\n * turns it into a takumi node tree and lifts the stylesheet out, and takumi\n * lays the result out at the input's viewport.\n */\nexport interface TakumiImageBackend {\n /** Renders to an encoded PNG. */\n png(input: ImageInput, options?: TakumiRenderOptions): Promise<Buffer>;\n /** Renders to an SVG document. Vector output, so raster options do not apply. */\n svg(input: ImageInput): Promise<string>;\n}\n\n/** Writes the `snapshot` surface's pages as a PDF, one page each. */\nexport interface TakumiPdfBackend {\n /**\n * Renders one page per entry in `pages`, each the input's size with no\n * margin, so a frame fills its page. Vector output with selectable text and\n * the registered fonts subset and embedded. Throws on an empty `pages` list.\n */\n pdf(input: PdfInput, options?: TakumiPdfOptions): Promise<Buffer>;\n}\n\n/** A {@link TakumiImageBackend} that also measures layout. */\nexport interface TakumiMeasuringBackend extends TakumiImageBackend {\n /** Lays the input out as {@link TakumiImageBackend.png} would, and returns every box, e.g. to find content past its area. */\n measure(input: ImageInput): Promise<LayoutBox>;\n}\n\n/** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */\nexport const TAKUMI_FORMATS = ['png', 'svg', 'pdf'] as const;\n\n/** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */\nexport interface TakumiBackend\n extends TakumiMeasuringBackend, TakumiPdfBackend {\n /** {@link TAKUMI_FORMATS}: one entry per method that writes a format. */\n readonly formats: typeof TAKUMI_FORMATS;\n}\n\ntype Matrix = MeasuredNode['transform'];\n\ntype SourceNode = ReturnType<typeof fromHtml>['node'];\n\n/** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */\nconst mapRect = (\n [a, b, c, d, e, f]: Matrix,\n x: number,\n y: number,\n width: number,\n height: number\n): { x: number; y: number; width: number; height: number } => {\n const corners = [\n [x, y],\n [x + width, y],\n [x, y + height],\n [x + width, y + height],\n ].map(([px = 0, py = 0]) => [a * px + c * py + e, b * px + d * py + f]);\n const xs = corners.map(([cx = 0]) => cx);\n const ys = corners.map(([, cy = 0]) => cy);\n const left = Math.min(...xs);\n const top = Math.min(...ys);\n return {\n x: left,\n y: top,\n width: Math.max(...xs) - left,\n height: Math.max(...ys) - top,\n };\n};\n\n/**\n * Takumi reports each node's size in its own units with its full canvas\n * transform, and carries no attributes, so those come from the `fromHtml`\n * node at the same position.\n */\nconst toLayoutBox = (\n { width, height, transform, runs, children }: MeasuredNode,\n source: SourceNode | undefined\n): LayoutBox => {\n const [a, b, c, d] = transform;\n const scaleX = Math.hypot(a, b);\n const scaleY = Math.hypot(c, d);\n const sources = source?.type === 'container' ? (source.children ?? []) : [];\n const paired = sources.length === children.length;\n return {\n ...mapRect(transform, 0, 0, width, height),\n scaleX,\n scaleY,\n scale: Math.abs(scaleY - 1) > Math.abs(scaleX - 1) ? scaleY : scaleX,\n runs: runs.map((run) => ({\n text: run.text,\n ...mapRect(transform, run.x, run.y, run.width, run.height),\n })),\n ...(source?.attributes && { attributes: { ...source.attributes } }),\n children: children.map((child, index) =>\n toLayoutBox(child, paired ? sources[index] : undefined)\n ),\n };\n};\n\nconst escapeStyleEndTags = (css: string) =>\n css.replace(/<\\/style/gi, (tag) => tag.replace('/', '\\\\/'));\n\n/**\n * Inlines the stylesheet ahead of the markup, the form `fromHtml` lifts into\n * its own `css` list. Passing the stylesheet as a separate `CssInput` entry is\n * a different path through takumi and changes the committed PNG bytes.\n */\nconst withStylesheet = (css: string, markup: string) =>\n `<style>${escapeStyleEndTags(css)}</style>${markup}`;\n\nconst toTakumiSource = ({ html, css }: ImageInput) =>\n fromHtml(withStylesheet(css, html));\n\n/**\n * One side of a scaled canvas, in whole pixels. Takumi takes one ratio for\n * both axes, so the ratio stays `scale` and the canvas absorbs the rounding.\n */\nconst toRasterSide = (side: number, scale: number) =>\n scale === 1 ? side : Math.max(1, Math.round(side * scale));\n\n/** Inline, so no pack class name can collide with it. */\nconst PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';\n\n/** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */\nconst toPagedSource = ({ pages, css, width, height }: PdfInput) => {\n if (pages.length === 0) {\n throw new Error('pdf: no pages; a document needs at least one');\n }\n const markup = pages\n .map(\n ({ html }) =>\n `<div style=\"width:${width}px;height:${height}px;${PAGE_STYLE}\">${html}</div>`\n )\n .join('');\n return {\n ...fromHtml(withStylesheet(css, `<div>${markup}</div>`)),\n width,\n height,\n };\n};\n\n/** Runs `create` once for every caller. A rejection is forgotten, so the next call retries instead of inheriting it. */\nconst once = <T>(create: () => Promise<T>): (() => Promise<T>) => {\n let pending: Promise<T> | undefined;\n return () => {\n pending ??= create().catch((error: unknown) => {\n pending = undefined;\n throw error;\n });\n return pending;\n };\n};\n\n/**\n * Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are\n * registered once, in order, before the first render; the PDF engine is\n * imported and created on the first `pdf`, and registers them again.\n */\nexport const createTakumiImageBackend = ({\n fonts = [],\n cacheMaxBytes,\n}: TakumiImageBackendOptions = {}): TakumiBackend => {\n const renderer = new Renderer(\n cacheMaxBytes === undefined ? undefined : { cacheMaxBytes }\n );\n\n /**\n * Registration is async and every render needs it, so each engine registers\n * once and is awaited. Sequential because the registration order is the\n * default fallback order.\n */\n const registerFonts = async <\n TEngine extends { registerFont(font: FontLoader): Promise<unknown> },\n >(\n engine: TEngine\n ): Promise<TEngine> => {\n for (const font of fonts) {\n await engine.registerFont(font);\n }\n return engine;\n };\n const ready = once(() => registerFonts(renderer));\n /** A second engine, imported and created on the first PDF, so a raster-only host never loads its wasm. */\n const pdfReady = once(async () => {\n const { PdfRenderer } = await import('takumi-pdf');\n return registerFonts(new PdfRenderer());\n });\n\n return {\n formats: TAKUMI_FORMATS,\n png: async (input, { scale = 1, devicePixelRatio } = {}) => {\n if (!Number.isFinite(scale) || scale <= 0) {\n throw new RangeError(\n `png: scale must be a positive finite number, got ${scale}`\n );\n }\n await ready();\n const { node, css } = toTakumiSource(input);\n const renderOptions: RenderOptions = {\n width: toRasterSide(input.width, scale),\n height: toRasterSide(input.height, scale),\n format: 'png',\n css,\n };\n if (devicePixelRatio !== undefined || scale !== 1) {\n renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;\n }\n return renderer.render(node, renderOptions);\n },\n svg: async (input) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n return renderer.renderSvg(node, {\n width: input.width,\n height: input.height,\n css,\n });\n },\n measure: async (input) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n const measured = await renderer.measure(node, {\n width: input.width,\n height: input.height,\n css,\n });\n return toLayoutBox(measured, node);\n },\n pdf: async (input, options = {}) => {\n const engine = await pdfReady();\n const { node, css, width, height } = toPagedSource(input);\n const renderOptions: PdfRenderOptions = {\n ...options,\n size: { width, height },\n margin: 0,\n css,\n };\n // Copied out of wasm memory, which a later render can move.\n return Buffer.from(await engine.render(node, renderOptions));\n },\n };\n};\n"]}
|