@elastic/isomer-image-takumi 0.1.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/README.md CHANGED
@@ -1,20 +1,31 @@
1
- # `@elastic/isomer-image-takumi`
1
+ <!-- markdownlint-disable MD033 -->
2
+ <p align="center">
3
+ <img src="https://raw.githubusercontent.com/elastic/isomer/main/docs/logo.svg" alt="Isomer" width="96" height="96">
4
+ </p>
5
+ <h1 align="center">@elastic/isomer-image-takumi</h1>
6
+ <p align="center"><strong>isomer</strong> <i>n.</i> — one formula, many forms; the same composition rendered to every surface.</p>
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@elastic/isomer-image-takumi"><img src="https://img.shields.io/npm/v/@elastic/isomer-image-takumi.svg" alt="npm version"></a>
9
+ <a href="https://github.com/elastic/isomer/actions/workflows/ci.yml"><img src="https://github.com/elastic/isomer/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
10
+ <a href="https://github.com/elastic/isomer/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/License-Elastic%202.0-blue.svg" alt="License: Elastic License 2.0"></a>
11
+ </p>
12
+ <!-- markdownlint-enable MD033 -->
2
13
 
3
- Rasterizes the `svg` surface's output with [takumi](https://takumi.kane.tw): PNG, SVG, or PDF out, and a measured layout for the SDK's `checkLayout`.
14
+ Rasterizes the `snapshot` surface's output with [takumi](https://takumi.kane.tw): PNG, SVG, or PDF out, and a measured layout for the SDK's `checkLayout`.
4
15
 
5
16
  ```sh
6
- npm install @elastic/isomer-image-takumi react react-dom
17
+ npm install @elastic/isomer-image-takumi
7
18
  ```
8
19
 
9
20
  ```ts
10
21
  import { createTakumiImageBackend } from '@elastic/isomer-image-takumi';
11
22
 
12
23
  const takumi = createTakumiImageBackend({ fonts });
13
- const png = await takumi.png(runtime.surfaces.svg.render(composition));
14
- const pdf = await takumi.pdf(runtime.surfaces.svg.renderPages(deck));
24
+ const png = await takumi.png(runtime.surfaces.snapshot.render(composition));
25
+ const pdf = await takumi.pdf(runtime.surfaces.snapshot.renderPages(deck));
15
26
  ```
16
27
 
17
- `renderPng` and `renderPdf` wrap validation, the `svg` surface, and the backend in one call and return the findings beside the bytes. A host-side rasterizer: it depends on no Isomer package, `react` and `react-dom` are its only peers, and it is the one published package that pulls a native dependency. Docs: [Takumi image backend](https://elastic.github.io/isomer/image-takumi/).
28
+ `renderPng` and `renderPdf` wrap validation, the `snapshot` surface, and the backend in one call and return the findings beside the bytes. A host-side rasterizer: it reads the snapshot's markup, so it depends on no Isomer package and no React, and it is the one published package that pulls a native dependency. Pass `takumi.formats` to `createIsomerRuntime({ formats })` so `getCapabilities()` reports PNG, SVG, and PDF. Docs: [Takumi image backend](https://elastic.github.io/isomer/image-takumi/).
18
29
 
19
30
  ## License
20
31
 
package/dist/backend.d.ts CHANGED
@@ -1,30 +1,31 @@
1
- import type { ReactNode } from 'react';
2
1
  import { type FontLoader } from '@takumi-rs/core';
3
2
  import type { ImagesInput } from 'takumi-pdf';
4
3
  /**
5
- * What the `svg` surface returns: a React tree and the stylesheet it is laid
6
- * out against, plus the viewport it was measured for.
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.
7
6
  *
8
7
  * Declared structurally rather than imported from `@elastic/isomer-runtime`,
9
- * so this backend depends on no isomer package. `SvgRenderResult` satisfies it.
8
+ * so this backend depends on no isomer package. `SnapshotRenderResult` satisfies it.
10
9
  */
11
10
  export interface ImageInput {
12
- /** A single root, as image layout requires: one element, string, or number, never an array or fragment. */
13
- element: ReactNode;
14
- /** The stylesheet the tree is laid out against, `light-dark(…)` already resolved. */
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. */
15
14
  css: string;
16
15
  /** The viewport, in CSS pixels; the output is this size. */
17
16
  width: number;
18
17
  height: number;
19
18
  }
20
19
  /**
21
- * What the `svg` surface's `renderPages` returns: one root per page, all laid
22
- * out against one stylesheet at one size. Structural like {@link ImageInput};
23
- * `SvgPagesResult` satisfies it.
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.
24
23
  */
25
24
  export interface PdfInput {
26
- /** One root per page, in order. */
27
- pages: readonly ReactNode[];
25
+ /** One page per entry, in order, each static markup with a single root. */
26
+ pages: readonly {
27
+ html: string;
28
+ }[];
28
29
  /** One stylesheet collected across every page. */
29
30
  css: string;
30
31
  /** Every page's size, in CSS pixels. */
@@ -50,9 +51,17 @@ export interface TakumiImageBackendOptions {
50
51
  /** Per-render overrides. Geometry comes from the input, not from here. */
51
52
  export interface TakumiRenderOptions {
52
53
  /**
53
- * Raises rendering fidelity (sharper text and gradients) at a fixed output
54
- * size — the PNG stays sized to `input.width` / `input.height` regardless
55
- * of this value; it does not produce a larger raster.
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}.
56
65
  */
57
66
  devicePixelRatio?: number;
58
67
  }
@@ -117,7 +126,7 @@ export interface LayoutBox {
117
126
  children: LayoutBox[];
118
127
  }
119
128
  /**
120
- * Rasterizes the `svg` surface's output.
129
+ * Rasterizes the `snapshot` surface's output.
121
130
  *
122
131
  * Both methods are one call over takumi: the tree is serialized, `fromHtml`
123
132
  * turns it into a takumi node tree and lifts the stylesheet out, and takumi
@@ -129,7 +138,7 @@ export interface TakumiImageBackend {
129
138
  /** Renders to an SVG document. Vector output, so raster options do not apply. */
130
139
  svg(input: ImageInput): Promise<string>;
131
140
  }
132
- /** Writes the `svg` surface's pages as a PDF, one page each. */
141
+ /** Writes the `snapshot` surface's pages as a PDF, one page each. */
133
142
  export interface TakumiPdfBackend {
134
143
  /**
135
144
  * Renders one page per entry in `pages`, each the input's size with no
@@ -143,8 +152,12 @@ export interface TakumiMeasuringBackend extends TakumiImageBackend {
143
152
  /** Lays the input out as {@link TakumiImageBackend.png} would, and returns every box, e.g. to find content past its area. */
144
153
  measure(input: ImageInput): Promise<LayoutBox>;
145
154
  }
155
+ /** The formats a {@link TakumiBackend} writes, for `createIsomerRuntime({ formats })`. */
156
+ export declare const TAKUMI_FORMATS: readonly ["png", "svg", "pdf"];
146
157
  /** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */
147
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;
148
161
  }
149
162
  /**
150
163
  * Builds a {@link TakumiBackend} over one takumi renderer. `fonts` are
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
package/dist/backend.js CHANGED
@@ -4,9 +4,10 @@
4
4
  * 2.0; you may not use this file except in compliance with the Elastic License
5
5
  * 2.0.
6
6
  */
7
- import { renderToStaticMarkup } from 'react-dom/server';
8
7
  import { Renderer, } from '@takumi-rs/core';
9
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'];
10
11
  /** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */
11
12
  const mapRect = ([a, b, c, d, e, f], x, y, width, height) => {
12
13
  const corners = [
@@ -57,7 +58,12 @@ const escapeStyleEndTags = (css) => css.replace(/<\/style/gi, (tag) => tag.repla
57
58
  * a different path through takumi and changes the committed PNG bytes.
58
59
  */
59
60
  const withStylesheet = (css, markup) => `<style>${escapeStyleEndTags(css)}</style>${markup}`;
60
- const toTakumiSource = ({ element, css }) => fromHtml(withStylesheet(css, renderToStaticMarkup(element)));
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));
61
67
  /** Inline, so no pack class name can collide with it. */
62
68
  const PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';
63
69
  /** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */
@@ -66,7 +72,7 @@ const toPagedSource = ({ pages, css, width, height }) => {
66
72
  throw new Error('pdf: no pages; a document needs at least one');
67
73
  }
68
74
  const markup = pages
69
- .map((page) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${renderToStaticMarkup(page)}</div>`)
75
+ .map(({ html }) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${html}</div>`)
70
76
  .join('');
71
77
  return {
72
78
  ...fromHtml(withStylesheet(css, `<div>${markup}</div>`)),
@@ -110,17 +116,21 @@ export const createTakumiImageBackend = ({ fonts = [], cacheMaxBytes, } = {}) =>
110
116
  return registerFonts(new PdfRenderer());
111
117
  });
112
118
  return {
113
- png: async (input, options = {}) => {
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
+ }
114
124
  await ready();
115
125
  const { node, css } = toTakumiSource(input);
116
126
  const renderOptions = {
117
- width: input.width,
118
- height: input.height,
127
+ width: toRasterSide(input.width, scale),
128
+ height: toRasterSide(input.height, scale),
119
129
  format: 'png',
120
130
  css,
121
131
  };
122
- if (options.devicePixelRatio !== undefined) {
123
- renderOptions.devicePixelRatio = options.devicePixelRatio;
132
+ if (devicePixelRatio !== undefined || scale !== 1) {
133
+ renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;
124
134
  }
125
135
  return renderer.render(node, renderOptions);
126
136
  },
@@ -1 +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 type { ReactNode } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\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 * What the `svg` surface returns: a React tree and the stylesheet it is laid\n * out against, plus 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. `SvgRenderResult` satisfies it.\n */\nexport interface ImageInput {\n /** A single root, as image layout requires: one element, string, or number, never an array or fragment. */\n element: ReactNode;\n /** The stylesheet the tree 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 * What the `svg` surface's `renderPages` returns: one root per page, all laid\n * out against one stylesheet at one size. Structural like {@link ImageInput};\n * `SvgPagesResult` satisfies it.\n */\nexport interface PdfInput {\n /** One root per page, in order. */\n pages: readonly ReactNode[];\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 * Raises rendering fidelity (sharper text and gradients) at a fixed output\n * size — the PNG stays sized to `input.width` / `input.height` regardless\n * of this value; it does not produce a larger raster.\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 `svg` 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 `svg` 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/** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */\nexport interface TakumiBackend\n extends TakumiMeasuringBackend, TakumiPdfBackend {}\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 = ({ element, css }: ImageInput) =>\n fromHtml(withStylesheet(css, renderToStaticMarkup(element)));\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 (page) =>\n `<div style=\"width:${width}px;height:${height}px;${PAGE_STYLE}\">${renderToStaticMarkup(page)}</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 png: async (input, options = {}) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n const renderOptions: RenderOptions = {\n width: input.width,\n height: input.height,\n format: 'png',\n css,\n };\n if (options.devicePixelRatio !== undefined) {\n renderOptions.devicePixelRatio = options.devicePixelRatio;\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"]}
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"]}
@@ -39,10 +39,11 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
- exports.createTakumiImageBackend = void 0;
43
- const server_1 = require("react-dom/server");
42
+ exports.createTakumiImageBackend = exports.TAKUMI_FORMATS = void 0;
44
43
  const core_1 = require("@takumi-rs/core");
45
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'];
46
47
  /** The canvas rectangle `transform` maps a local `width` × `height` box at `x`, `y` to. */
47
48
  const mapRect = ([a, b, c, d, e, f], x, y, width, height) => {
48
49
  const corners = [
@@ -93,7 +94,12 @@ const escapeStyleEndTags = (css) => css.replace(/<\/style/gi, (tag) => tag.repla
93
94
  * a different path through takumi and changes the committed PNG bytes.
94
95
  */
95
96
  const withStylesheet = (css, markup) => `<style>${escapeStyleEndTags(css)}</style>${markup}`;
96
- const toTakumiSource = ({ element, css }) => (0, html_1.fromHtml)(withStylesheet(css, (0, server_1.renderToStaticMarkup)(element)));
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));
97
103
  /** Inline, so no pack class name can collide with it. */
98
104
  const PAGE_STYLE = 'overflow:hidden;break-after:page;break-inside:avoid';
99
105
  /** Each page in a fixed-size block that ends the page, so a frame never spills onto the next. */
@@ -102,7 +108,7 @@ const toPagedSource = ({ pages, css, width, height }) => {
102
108
  throw new Error('pdf: no pages; a document needs at least one');
103
109
  }
104
110
  const markup = pages
105
- .map((page) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${(0, server_1.renderToStaticMarkup)(page)}</div>`)
111
+ .map(({ html }) => `<div style="width:${width}px;height:${height}px;${PAGE_STYLE}">${html}</div>`)
106
112
  .join('');
107
113
  return {
108
114
  ...(0, html_1.fromHtml)(withStylesheet(css, `<div>${markup}</div>`)),
@@ -146,17 +152,21 @@ const createTakumiImageBackend = ({ fonts = [], cacheMaxBytes, } = {}) => {
146
152
  return registerFonts(new PdfRenderer());
147
153
  });
148
154
  return {
149
- png: async (input, options = {}) => {
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
+ }
150
160
  await ready();
151
161
  const { node, css } = toTakumiSource(input);
152
162
  const renderOptions = {
153
- width: input.width,
154
- height: input.height,
163
+ width: toRasterSide(input.width, scale),
164
+ height: toRasterSide(input.height, scale),
155
165
  format: 'png',
156
166
  css,
157
167
  };
158
- if (options.devicePixelRatio !== undefined) {
159
- renderOptions.devicePixelRatio = options.devicePixelRatio;
168
+ if (devicePixelRatio !== undefined || scale !== 1) {
169
+ renderOptions.devicePixelRatio = (devicePixelRatio ?? 1) * scale;
160
170
  }
161
171
  return renderer.render(node, renderOptions);
162
172
  },
@@ -1 +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 type { ReactNode } from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\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 * What the `svg` surface returns: a React tree and the stylesheet it is laid\n * out against, plus 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. `SvgRenderResult` satisfies it.\n */\nexport interface ImageInput {\n /** A single root, as image layout requires: one element, string, or number, never an array or fragment. */\n element: ReactNode;\n /** The stylesheet the tree 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 * What the `svg` surface's `renderPages` returns: one root per page, all laid\n * out against one stylesheet at one size. Structural like {@link ImageInput};\n * `SvgPagesResult` satisfies it.\n */\nexport interface PdfInput {\n /** One root per page, in order. */\n pages: readonly ReactNode[];\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 * Raises rendering fidelity (sharper text and gradients) at a fixed output\n * size — the PNG stays sized to `input.width` / `input.height` regardless\n * of this value; it does not produce a larger raster.\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 `svg` 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 `svg` 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/** Everything {@link createTakumiImageBackend} returns: raster, measure, and PDF. */\nexport interface TakumiBackend\n extends TakumiMeasuringBackend, TakumiPdfBackend {}\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 = ({ element, css }: ImageInput) =>\n fromHtml(withStylesheet(css, renderToStaticMarkup(element)));\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 (page) =>\n `<div style=\"width:${width}px;height:${height}px;${PAGE_STYLE}\">${renderToStaticMarkup(page)}</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 png: async (input, options = {}) => {\n await ready();\n const { node, css } = toTakumiSource(input);\n const renderOptions: RenderOptions = {\n width: input.width,\n height: input.height,\n format: 'png',\n css,\n };\n if (options.devicePixelRatio !== undefined) {\n renderOptions.devicePixelRatio = options.devicePixelRatio;\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"]}
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"]}
package/dist/cjs/index.js CHANGED
@@ -6,8 +6,9 @@
6
6
  * 2.0.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.renderPng = exports.renderPdf = exports.createTakumiImageBackend = void 0;
9
+ exports.renderPng = exports.renderPdf = exports.createTakumiImageBackend = exports.TAKUMI_FORMATS = void 0;
10
10
  var backend_1 = require("./backend");
11
+ Object.defineProperty(exports, "TAKUMI_FORMATS", { enumerable: true, get: function () { return backend_1.TAKUMI_FORMATS; } });
11
12
  Object.defineProperty(exports, "createTakumiImageBackend", { enumerable: true, get: function () { return backend_1.createTakumiImageBackend; } });
12
13
  var render_pdf_1 = require("./render_pdf");
13
14
  Object.defineProperty(exports, "renderPdf", { enumerable: true, get: function () { return render_pdf_1.renderPdf; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,qCAamB;AADjB,mHAAA,wBAAwB,OAAA;AAE1B,2CAKsB;AADpB,uGAAA,SAAS,OAAA;AAEX,2CAQsB;AADpB,uGAAA,SAAS,OAAA","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\nexport {\n type ImageInput,\n type LayoutBox,\n type PdfInput,\n type TakumiBackend,\n type TakumiImageBackend,\n type TakumiImageBackendOptions,\n type TakumiMeasuringBackend,\n type TakumiPdfBackend,\n type TakumiPdfMetadata,\n type TakumiPdfOptions,\n type TakumiRenderOptions,\n createTakumiImageBackend,\n} from './backend';\nexport {\n type PdfRuntime,\n type RenderPdfOptions,\n type RenderPdfResult,\n renderPdf,\n} from './render_pdf';\nexport {\n type PngCheckedValidationResult,\n type PngRuntime,\n type PngSvgOptions,\n type PngValidationResult,\n type RenderPngOptions,\n type RenderPngResult,\n renderPng,\n} from './render_png';\nexport type { Font, FontDetails, FontLoader } from '@takumi-rs/core';\nexport type { ImagesInput } from 'takumi-pdf';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,qCAcmB;AAFjB,yGAAA,cAAc,OAAA;AACd,mHAAA,wBAAwB,OAAA;AAE1B,2CAKsB;AADpB,uGAAA,SAAS,OAAA;AAEX,2CAQsB;AADpB,uGAAA,SAAS,OAAA","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\nexport {\n type ImageInput,\n type LayoutBox,\n type PdfInput,\n type TakumiBackend,\n type TakumiImageBackend,\n type TakumiImageBackendOptions,\n type TakumiMeasuringBackend,\n type TakumiPdfBackend,\n type TakumiPdfMetadata,\n type TakumiPdfOptions,\n type TakumiRenderOptions,\n TAKUMI_FORMATS,\n createTakumiImageBackend,\n} from './backend';\nexport {\n type PdfRuntime,\n type RenderPdfOptions,\n type RenderPdfResult,\n renderPdf,\n} from './render_pdf';\nexport {\n type PngCheckedValidationResult,\n type PngRuntime,\n type PngValidationResult,\n type RenderPngOptions,\n type RenderPngResult,\n type SnapshotOptions,\n renderPng,\n} from './render_png';\nexport type { Font, FontDetails, FontLoader } from '@takumi-rs/core';\nexport type { ImagesInput } from 'takumi-pdf';\n"]}
@@ -18,11 +18,11 @@ const render_png_1 = require("./render_png");
18
18
  * `renderPages`, which discards its own result. An empty deck throws the
19
19
  * runtime's `EMPTY_PAGES`.
20
20
  */
21
- const renderPdf = async (runtime, deck, backend, { svg, ...options } = {}) => {
21
+ const renderPdf = async (runtime, deck, backend, { snapshot, ...options } = {}) => {
22
22
  const pages = deck.map((composition) => (0, render_png_1.checkedForDrawing)('renderPdf', runtime.validate(composition)));
23
23
  const validations = pages.map(({ validation }) => validation);
24
- const rendered = runtime.surfaces.svg.renderPages(pages.map(({ checked }) => checked), {
25
- ...svg,
24
+ const rendered = runtime.surfaces.snapshot.renderPages(pages.map(({ checked }) => checked), {
25
+ ...snapshot,
26
26
  onValidationError: 'collect',
27
27
  });
28
28
  const pdf = await backend.pdf(rendered, options);
@@ -1 +1 @@
1
- {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../../src/render_pdf.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAGH,6CAKsB;AAkCtB;;;;;;;;;GASG;AACI,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,IAAwB,EACxB,OAAyB,EACzB,EAAE,GAAG,EAAE,GAAG,OAAO,KAAuB,EAAE,EAChB,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CACrC,IAAA,8BAAiB,EAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAC9D,CAAC;IACF,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAC/C,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EACnC;QACE,GAAG,GAAG;QACN,iBAAiB,EAAE,SAAS;KAC7B,CACF,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;QAChC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW;KACZ,CAAC;AACJ,CAAC,CAAC;AAzBW,QAAA,SAAS,aAyBpB","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 type { PdfInput, TakumiPdfBackend, TakumiPdfOptions } from './backend';\nimport {\n checkedForDrawing,\n type PngCheckedValidationResult,\n type PngSvgOptions,\n type PngValidationResult,\n} from './render_png';\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link PdfInput}. A runtime built with `frames` satisfies it.\n */\nexport interface PdfRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n svg: {\n renderPages(\n compositions: readonly unknown[],\n options?: PngSvgOptions & { onValidationError?: 'collect' | 'throw' }\n ): PdfInput;\n };\n };\n}\n\n/** Options for {@link renderPdf}. */\nexport interface RenderPdfOptions extends TakumiPdfOptions {\n /** Forwarded to `runtime.surfaces.svg.renderPages`, so every page shares one frame. */\n svg?: PngSvgOptions;\n}\n\n/** What {@link renderPdf} resolves to: the bytes, the page count and size, and one finding set per composition. */\nexport interface RenderPdfResult {\n pdf: Buffer;\n pageCount: number;\n width: number;\n height: number;\n /** One per composition, in order. */\n validations: PngValidationResult[];\n}\n\n/**\n * Validates, renders, and writes a deck as one PDF in one call, a page per\n * composition.\n *\n * Every composition is rendered, even an invalid one: `validations` is how a\n * caller finds out, rather than a thrown error. What is drawn is the copy\n * validation checked. Validation runs twice, once here and once inside\n * `renderPages`, which discards its own result. An empty deck throws the\n * runtime's `EMPTY_PAGES`.\n */\nexport const renderPdf = async (\n runtime: PdfRuntime,\n deck: readonly unknown[],\n backend: TakumiPdfBackend,\n { svg, ...options }: RenderPdfOptions = {}\n): Promise<RenderPdfResult> => {\n const pages = deck.map((composition) =>\n checkedForDrawing('renderPdf', runtime.validate(composition))\n );\n const validations = pages.map(({ validation }) => validation);\n const rendered = runtime.surfaces.svg.renderPages(\n pages.map(({ checked }) => checked),\n {\n ...svg,\n onValidationError: 'collect',\n }\n );\n const pdf = await backend.pdf(rendered, options);\n return {\n pdf,\n pageCount: rendered.pages.length,\n width: rendered.width,\n height: rendered.height,\n validations,\n };\n};\n"]}
1
+ {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../../src/render_pdf.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAGH,6CAKsB;AAkCtB;;;;;;;;;GASG;AACI,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,IAAwB,EACxB,OAAyB,EACzB,EAAE,QAAQ,EAAE,GAAG,OAAO,KAAuB,EAAE,EACrB,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CACrC,IAAA,8BAAiB,EAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAC9D,CAAC;IACF,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CACpD,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EACnC;QACE,GAAG,QAAQ;QACX,iBAAiB,EAAE,SAAS;KAC7B,CACF,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;QAChC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW;KACZ,CAAC;AACJ,CAAC,CAAC;AAzBW,QAAA,SAAS,aAyBpB","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 type { PdfInput, TakumiPdfBackend, TakumiPdfOptions } from './backend';\nimport {\n checkedForDrawing,\n type PngCheckedValidationResult,\n type PngValidationResult,\n type SnapshotOptions,\n} from './render_png';\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link PdfInput}. A runtime built with `frames` satisfies it.\n */\nexport interface PdfRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n snapshot: {\n renderPages(\n compositions: readonly unknown[],\n options?: SnapshotOptions & { onValidationError?: 'collect' | 'throw' }\n ): PdfInput;\n };\n };\n}\n\n/** Options for {@link renderPdf}. */\nexport interface RenderPdfOptions extends TakumiPdfOptions {\n /** Forwarded to `runtime.surfaces.snapshot.renderPages`, so every page shares one frame. */\n snapshot?: SnapshotOptions;\n}\n\n/** What {@link renderPdf} resolves to: the bytes, the page count and size, and one finding set per composition. */\nexport interface RenderPdfResult {\n pdf: Buffer;\n pageCount: number;\n width: number;\n height: number;\n /** One per composition, in order. */\n validations: PngValidationResult[];\n}\n\n/**\n * Validates, renders, and writes a deck as one PDF in one call, a page per\n * composition.\n *\n * Every composition is rendered, even an invalid one: `validations` is how a\n * caller finds out, rather than a thrown error. What is drawn is the copy\n * validation checked. Validation runs twice, once here and once inside\n * `renderPages`, which discards its own result. An empty deck throws the\n * runtime's `EMPTY_PAGES`.\n */\nexport const renderPdf = async (\n runtime: PdfRuntime,\n deck: readonly unknown[],\n backend: TakumiPdfBackend,\n { snapshot, ...options }: RenderPdfOptions = {}\n): Promise<RenderPdfResult> => {\n const pages = deck.map((composition) =>\n checkedForDrawing('renderPdf', runtime.validate(composition))\n );\n const validations = pages.map(({ validation }) => validation);\n const rendered = runtime.surfaces.snapshot.renderPages(\n pages.map(({ checked }) => checked),\n {\n ...snapshot,\n onValidationError: 'collect',\n }\n );\n const pdf = await backend.pdf(rendered, options);\n return {\n pdf,\n pageCount: rendered.pages.length,\n width: rendered.width,\n height: rendered.height,\n validations,\n };\n};\n"]}
@@ -37,10 +37,10 @@ exports.checkedForDrawing = checkedForDrawing;
37
37
  * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which
38
38
  * discards its own result.
39
39
  */
40
- const renderPng = async (runtime, composition, backend, { svg, ...options } = {}) => {
40
+ const renderPng = async (runtime, composition, backend, { snapshot, ...options } = {}) => {
41
41
  const { checked, validation } = (0, exports.checkedForDrawing)('renderPng', runtime.validate(composition));
42
- const rendered = runtime.surfaces.svg.render(checked, {
43
- ...svg,
42
+ const rendered = runtime.surfaces.snapshot.render(checked, {
43
+ ...snapshot,
44
44
  onValidationError: 'collect',
45
45
  });
46
46
  const png = await backend.png(rendered, options);
@@ -1 +1 @@
1
- {"version":3,"file":"render_png.js","sourceRoot":"","sources":["../../src/render_png.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAmBH;;;;;;GAMG;AACI,MAAM,iBAAiB,GAAG,CAC/B,MAAc,EACd,EAAE,WAAW,EAAE,GAAG,UAAU,EAA8B,EACH,EAAE;IACzD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,4DAA4D,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CAAC,GAAG,MAAM,sDAAsD,CAAC,EAC1E;QACE,IAAI,EAAE,4BAA4B;QAClC,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CACF,CAAC;AACJ,CAAC,CAAC;AApBW,QAAA,iBAAiB,qBAoB5B;AAiDF;;;;;;;;GAQG;AACI,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,WAAoB,EACpB,OAA2B,EAC3B,EAAE,GAAG,EAAE,GAAG,OAAO,KAAuB,EAAE,EAChB,EAAE;IAC5B,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAA,yBAAiB,EAC/C,WAAW,EACX,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAC9B,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE;QACpD,GAAG,GAAG;QACN,iBAAiB,EAAE,SAAS;KAC7B,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;AAC7E,CAAC,CAAC;AAhBW,QAAA,SAAS,aAgBpB","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 type {\n ImageInput,\n TakumiImageBackend,\n TakumiRenderOptions,\n} from './backend';\n\n/** A validation result shaped like the SDK's `ValidationResult`, declared structurally. */\nexport interface PngValidationResult {\n valid: boolean;\n errors: readonly unknown[];\n}\n\n/** What `runtime.validate` returns: findings, and the copy they describe, `undefined` only for refused input. */\nexport type PngCheckedValidationResult = PngValidationResult & {\n composition: unknown;\n};\n\n/**\n * Splits a {@link PngCheckedValidationResult} into the copy to draw and the findings to report.\n *\n * Throws rather than draw the caller's value: shaped as the SDK's\n * `CompositionValidationError` when the runtime refused the input, and a plain\n * `Error` when the runtime returned no copy at all.\n */\nexport const checkedForDrawing = (\n caller: string,\n { composition, ...validation }: PngCheckedValidationResult\n): { checked: unknown; validation: PngValidationResult } => {\n if (composition !== undefined) {\n return { checked: composition, validation };\n }\n if (validation.valid) {\n throw new Error(\n `${caller}: runtime.validate returned no checked composition to draw`\n );\n }\n throw Object.assign(\n new Error(`${caller}: the runtime refused the composition before parsing`),\n {\n name: 'CompositionValidationError',\n code: 'COMPOSITION_INVALID',\n errors: validation.errors,\n }\n );\n};\n\n/**\n * The `svg` surface's per-render options, mirroring the runtime's\n * `SvgRenderOptions` so a caller gets completion without importing it.\n */\nexport interface PngSvgOptions {\n /** Which of the runtime's frames this render uses; defaults to its `defaultFrame`. */\n frame?: string;\n /** Overrides the frame's `defaultWidth`. */\n width?: number;\n /** Overrides the frame's estimated height. */\n height?: number;\n /** Theme mode; falls back to the composition's own `theme`. `auto` resolves light. */\n theme?: 'light' | 'dark' | 'auto';\n}\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link ImageInput} so this package depends on no isomer package. `surfaces.svg`\n * is required where the runtime's generic form has `SvgSurface | undefined`; a\n * runtime built with `frames` carries `SvgSurface` there and satisfies this.\n */\nexport interface PngRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n svg: {\n render(\n composition: unknown,\n options?: PngSvgOptions & { onValidationError?: 'collect' | 'throw' }\n ): ImageInput;\n };\n };\n}\n\n/** Options for {@link renderPng}. */\nexport interface RenderPngOptions extends TakumiRenderOptions {\n /** Forwarded to `runtime.surfaces.svg.render`. */\n svg?: PngSvgOptions;\n}\n\n/** What {@link renderPng} resolves to: the bytes, their size, and the findings on the composition drawn. */\nexport interface RenderPngResult {\n png: Buffer;\n width: number;\n height: number;\n validation: PngValidationResult;\n}\n\n/**\n * Validates, renders, and rasterizes a composition in one call.\n *\n * The composition is rendered even when invalid: `validation` is how a caller\n * finds out, rather than a thrown error. What is drawn is the copy validation\n * checked, and input refused before parsing, which has none, throws\n * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which\n * discards its own result.\n */\nexport const renderPng = async (\n runtime: PngRuntime,\n composition: unknown,\n backend: TakumiImageBackend,\n { svg, ...options }: RenderPngOptions = {}\n): Promise<RenderPngResult> => {\n const { checked, validation } = checkedForDrawing(\n 'renderPng',\n runtime.validate(composition)\n );\n const rendered = runtime.surfaces.svg.render(checked, {\n ...svg,\n onValidationError: 'collect',\n });\n const png = await backend.png(rendered, options);\n return { png, width: rendered.width, height: rendered.height, validation };\n};\n"]}
1
+ {"version":3,"file":"render_png.js","sourceRoot":"","sources":["../../src/render_png.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAmBH;;;;;;GAMG;AACI,MAAM,iBAAiB,GAAG,CAC/B,MAAc,EACd,EAAE,WAAW,EAAE,GAAG,UAAU,EAA8B,EACH,EAAE;IACzD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,4DAA4D,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CAAC,GAAG,MAAM,sDAAsD,CAAC,EAC1E;QACE,IAAI,EAAE,4BAA4B;QAClC,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CACF,CAAC;AACJ,CAAC,CAAC;AApBW,QAAA,iBAAiB,qBAoB5B;AAkDF;;;;;;;;GAQG;AACI,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,WAAoB,EACpB,OAA2B,EAC3B,EAAE,QAAQ,EAAE,GAAG,OAAO,KAAuB,EAAE,EACrB,EAAE;IAC5B,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,IAAA,yBAAiB,EAC/C,WAAW,EACX,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAC9B,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE;QACzD,GAAG,QAAQ;QACX,iBAAiB,EAAE,SAAS;KAC7B,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;AAC7E,CAAC,CAAC;AAhBW,QAAA,SAAS,aAgBpB","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 type {\n ImageInput,\n TakumiImageBackend,\n TakumiRenderOptions,\n} from './backend';\n\n/** A validation result shaped like the SDK's `ValidationResult`, declared structurally. */\nexport interface PngValidationResult {\n valid: boolean;\n errors: readonly unknown[];\n}\n\n/** What `runtime.validate` returns: findings, and the copy they describe, `undefined` only for refused input. */\nexport type PngCheckedValidationResult = PngValidationResult & {\n composition: unknown;\n};\n\n/**\n * Splits a {@link PngCheckedValidationResult} into the copy to draw and the findings to report.\n *\n * Throws rather than draw the caller's value: shaped as the SDK's\n * `CompositionValidationError` when the runtime refused the input, and a plain\n * `Error` when the runtime returned no copy at all.\n */\nexport const checkedForDrawing = (\n caller: string,\n { composition, ...validation }: PngCheckedValidationResult\n): { checked: unknown; validation: PngValidationResult } => {\n if (composition !== undefined) {\n return { checked: composition, validation };\n }\n if (validation.valid) {\n throw new Error(\n `${caller}: runtime.validate returned no checked composition to draw`\n );\n }\n throw Object.assign(\n new Error(`${caller}: the runtime refused the composition before parsing`),\n {\n name: 'CompositionValidationError',\n code: 'COMPOSITION_INVALID',\n errors: validation.errors,\n }\n );\n};\n\n/**\n * The `snapshot` surface's per-render options, mirroring the runtime's\n * `SnapshotRenderOptions` so a caller gets completion without importing it.\n */\nexport interface SnapshotOptions {\n /** Which of the runtime's frames this render uses; defaults to its `defaultFrame`. */\n frame?: string;\n /** Overrides the frame's `defaultWidth`. */\n width?: number;\n /** Overrides the frame's estimated height. */\n height?: number;\n /** Theme mode; falls back to the composition's own `theme`. `auto` resolves light. */\n theme?: 'light' | 'dark' | 'auto';\n}\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link ImageInput} so this package depends on no isomer package. `surfaces.snapshot`\n * is required where the runtime's generic form has `SnapshotSurface | undefined`; a\n * runtime built with `frames` carries `SnapshotSurface` there and satisfies this.\n */\nexport interface PngRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n snapshot: {\n render(\n composition: unknown,\n options?: SnapshotOptions & { onValidationError?: 'collect' | 'throw' }\n ): ImageInput;\n };\n };\n}\n\n/** Options for {@link renderPng}. */\nexport interface RenderPngOptions extends TakumiRenderOptions {\n /** Forwarded to `runtime.surfaces.snapshot.render`. */\n snapshot?: SnapshotOptions;\n}\n\n/** What {@link renderPng} resolves to: the bytes, their size, and the findings on the composition drawn. */\nexport interface RenderPngResult {\n png: Buffer;\n /** The viewport, in CSS pixels; the raster is this times `scale`, rounded. */\n width: number;\n height: number;\n validation: PngValidationResult;\n}\n\n/**\n * Validates, renders, and rasterizes a composition in one call.\n *\n * The composition is rendered even when invalid: `validation` is how a caller\n * finds out, rather than a thrown error. What is drawn is the copy validation\n * checked, and input refused before parsing, which has none, throws\n * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which\n * discards its own result.\n */\nexport const renderPng = async (\n runtime: PngRuntime,\n composition: unknown,\n backend: TakumiImageBackend,\n { snapshot, ...options }: RenderPngOptions = {}\n): Promise<RenderPngResult> => {\n const { checked, validation } = checkedForDrawing(\n 'renderPng',\n runtime.validate(composition)\n );\n const rendered = runtime.surfaces.snapshot.render(checked, {\n ...snapshot,\n onValidationError: 'collect',\n });\n const png = await backend.png(rendered, options);\n return { png, width: rendered.width, height: rendered.height, validation };\n};\n"]}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- export { type ImageInput, type LayoutBox, type PdfInput, type TakumiBackend, type TakumiImageBackend, type TakumiImageBackendOptions, type TakumiMeasuringBackend, type TakumiPdfBackend, type TakumiPdfMetadata, type TakumiPdfOptions, type TakumiRenderOptions, createTakumiImageBackend, } from './backend.js';
1
+ export { type ImageInput, type LayoutBox, type PdfInput, type TakumiBackend, type TakumiImageBackend, type TakumiImageBackendOptions, type TakumiMeasuringBackend, type TakumiPdfBackend, type TakumiPdfMetadata, type TakumiPdfOptions, type TakumiRenderOptions, TAKUMI_FORMATS, createTakumiImageBackend, } from './backend.js';
2
2
  export { type PdfRuntime, type RenderPdfOptions, type RenderPdfResult, renderPdf, } from './render_pdf.js';
3
- export { type PngCheckedValidationResult, type PngRuntime, type PngSvgOptions, type PngValidationResult, type RenderPngOptions, type RenderPngResult, renderPng, } from './render_png.js';
3
+ export { type PngCheckedValidationResult, type PngRuntime, type PngValidationResult, type RenderPngOptions, type RenderPngResult, type SnapshotOptions, renderPng, } from './render_png.js';
4
4
  export type { Font, FontDetails, FontLoader } from '@takumi-rs/core';
5
5
  export type { ImagesInput } from 'takumi-pdf';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,wBAAwB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,cAAc,EACd,wBAAwB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * 2.0; you may not use this file except in compliance with the Elastic License
5
5
  * 2.0.
6
6
  */
7
- export { createTakumiImageBackend, } from './backend.js';
7
+ export { TAKUMI_FORMATS, createTakumiImageBackend, } from './backend.js';
8
8
  export { renderPdf, } from './render_pdf.js';
9
9
  export { renderPng, } from './render_png.js';
10
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAYL,wBAAwB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EAIL,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAOL,SAAS,GACV,MAAM,cAAc,CAAC","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\nexport {\n type ImageInput,\n type LayoutBox,\n type PdfInput,\n type TakumiBackend,\n type TakumiImageBackend,\n type TakumiImageBackendOptions,\n type TakumiMeasuringBackend,\n type TakumiPdfBackend,\n type TakumiPdfMetadata,\n type TakumiPdfOptions,\n type TakumiRenderOptions,\n createTakumiImageBackend,\n} from './backend';\nexport {\n type PdfRuntime,\n type RenderPdfOptions,\n type RenderPdfResult,\n renderPdf,\n} from './render_pdf';\nexport {\n type PngCheckedValidationResult,\n type PngRuntime,\n type PngSvgOptions,\n type PngValidationResult,\n type RenderPngOptions,\n type RenderPngResult,\n renderPng,\n} from './render_png';\nexport type { Font, FontDetails, FontLoader } from '@takumi-rs/core';\nexport type { ImagesInput } from 'takumi-pdf';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAYL,cAAc,EACd,wBAAwB,GACzB,MAAM,WAAW,CAAC;AACnB,OAAO,EAIL,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAOL,SAAS,GACV,MAAM,cAAc,CAAC","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\nexport {\n type ImageInput,\n type LayoutBox,\n type PdfInput,\n type TakumiBackend,\n type TakumiImageBackend,\n type TakumiImageBackendOptions,\n type TakumiMeasuringBackend,\n type TakumiPdfBackend,\n type TakumiPdfMetadata,\n type TakumiPdfOptions,\n type TakumiRenderOptions,\n TAKUMI_FORMATS,\n createTakumiImageBackend,\n} from './backend';\nexport {\n type PdfRuntime,\n type RenderPdfOptions,\n type RenderPdfResult,\n renderPdf,\n} from './render_pdf';\nexport {\n type PngCheckedValidationResult,\n type PngRuntime,\n type PngValidationResult,\n type RenderPngOptions,\n type RenderPngResult,\n type SnapshotOptions,\n renderPng,\n} from './render_png';\nexport type { Font, FontDetails, FontLoader } from '@takumi-rs/core';\nexport type { ImagesInput } from 'takumi-pdf';\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { PdfInput, TakumiPdfBackend, TakumiPdfOptions } from './backend.js';
2
- import { type PngCheckedValidationResult, type PngSvgOptions, type PngValidationResult } from './render_png.js';
2
+ import { type PngCheckedValidationResult, type PngValidationResult, type SnapshotOptions } from './render_png.js';
3
3
  /**
4
4
  * The slice of `IsomerRuntime` this helper needs, declared structurally like
5
5
  * {@link PdfInput}. A runtime built with `frames` satisfies it.
@@ -7,8 +7,8 @@ import { type PngCheckedValidationResult, type PngSvgOptions, type PngValidation
7
7
  export interface PdfRuntime {
8
8
  validate(composition: unknown): PngCheckedValidationResult;
9
9
  surfaces: {
10
- svg: {
11
- renderPages(compositions: readonly unknown[], options?: PngSvgOptions & {
10
+ snapshot: {
11
+ renderPages(compositions: readonly unknown[], options?: SnapshotOptions & {
12
12
  onValidationError?: 'collect' | 'throw';
13
13
  }): PdfInput;
14
14
  };
@@ -16,8 +16,8 @@ export interface PdfRuntime {
16
16
  }
17
17
  /** Options for {@link renderPdf}. */
18
18
  export interface RenderPdfOptions extends TakumiPdfOptions {
19
- /** Forwarded to `runtime.surfaces.svg.renderPages`, so every page shares one frame. */
20
- svg?: PngSvgOptions;
19
+ /** Forwarded to `runtime.surfaces.snapshot.renderPages`, so every page shares one frame. */
20
+ snapshot?: SnapshotOptions;
21
21
  }
22
22
  /** What {@link renderPdf} resolves to: the bytes, the page count and size, and one finding set per composition. */
23
23
  export interface RenderPdfResult {
@@ -38,5 +38,5 @@ export interface RenderPdfResult {
38
38
  * `renderPages`, which discards its own result. An empty deck throws the
39
39
  * runtime's `EMPTY_PAGES`.
40
40
  */
41
- export declare const renderPdf: (runtime: PdfRuntime, deck: readonly unknown[], backend: TakumiPdfBackend, { svg, ...options }?: RenderPdfOptions) => Promise<RenderPdfResult>;
41
+ export declare const renderPdf: (runtime: PdfRuntime, deck: readonly unknown[], backend: TakumiPdfBackend, { snapshot, ...options }?: RenderPdfOptions) => Promise<RenderPdfResult>;
42
42
  //# sourceMappingURL=render_pdf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render_pdf.d.ts","sourceRoot":"","sources":["../src/render_pdf.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAC3D,QAAQ,EAAE;QACR,GAAG,EAAE;YACH,WAAW,CACT,YAAY,EAAE,SAAS,OAAO,EAAE,EAChC,OAAO,CAAC,EAAE,aAAa,GAAG;gBAAE,iBAAiB,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;aAAE,GACpE,QAAQ,CAAC;SACb,CAAC;KACH,CAAC;CACH;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,uFAAuF;IACvF,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB;AAED,mHAAmH;AACnH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GACpB,SAAS,UAAU,EACnB,MAAM,SAAS,OAAO,EAAE,EACxB,SAAS,gBAAgB,EACzB,sBAAqB,gBAAqB,KACzC,OAAO,CAAC,eAAe,CAoBzB,CAAC"}
1
+ {"version":3,"file":"render_pdf.d.ts","sourceRoot":"","sources":["../src/render_pdf.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EAEL,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAC3D,QAAQ,EAAE;QACR,QAAQ,EAAE;YACR,WAAW,CACT,YAAY,EAAE,SAAS,OAAO,EAAE,EAChC,OAAO,CAAC,EAAE,eAAe,GAAG;gBAAE,iBAAiB,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;aAAE,GACtE,QAAQ,CAAC;SACb,CAAC;KACH,CAAC;CACH;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,mHAAmH;AACnH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GACpB,SAAS,UAAU,EACnB,MAAM,SAAS,OAAO,EAAE,EACxB,SAAS,gBAAgB,EACzB,2BAA0B,gBAAqB,KAC9C,OAAO,CAAC,eAAe,CAoBzB,CAAC"}
@@ -15,11 +15,11 @@ import { checkedForDrawing, } from './render_png.js';
15
15
  * `renderPages`, which discards its own result. An empty deck throws the
16
16
  * runtime's `EMPTY_PAGES`.
17
17
  */
18
- export const renderPdf = async (runtime, deck, backend, { svg, ...options } = {}) => {
18
+ export const renderPdf = async (runtime, deck, backend, { snapshot, ...options } = {}) => {
19
19
  const pages = deck.map((composition) => checkedForDrawing('renderPdf', runtime.validate(composition)));
20
20
  const validations = pages.map(({ validation }) => validation);
21
- const rendered = runtime.surfaces.svg.renderPages(pages.map(({ checked }) => checked), {
22
- ...svg,
21
+ const rendered = runtime.surfaces.snapshot.renderPages(pages.map(({ checked }) => checked), {
22
+ ...snapshot,
23
23
  onValidationError: 'collect',
24
24
  });
25
25
  const pdf = await backend.pdf(rendered, options);
@@ -1 +1 @@
1
- {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../src/render_pdf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,iBAAiB,GAIlB,MAAM,cAAc,CAAC;AAkCtB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,IAAwB,EACxB,OAAyB,EACzB,EAAE,GAAG,EAAE,GAAG,OAAO,KAAuB,EAAE,EAChB,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CACrC,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAC9D,CAAC;IACF,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAC/C,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EACnC;QACE,GAAG,GAAG;QACN,iBAAiB,EAAE,SAAS;KAC7B,CACF,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;QAChC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW;KACZ,CAAC;AACJ,CAAC,CAAC","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 type { PdfInput, TakumiPdfBackend, TakumiPdfOptions } from './backend';\nimport {\n checkedForDrawing,\n type PngCheckedValidationResult,\n type PngSvgOptions,\n type PngValidationResult,\n} from './render_png';\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link PdfInput}. A runtime built with `frames` satisfies it.\n */\nexport interface PdfRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n svg: {\n renderPages(\n compositions: readonly unknown[],\n options?: PngSvgOptions & { onValidationError?: 'collect' | 'throw' }\n ): PdfInput;\n };\n };\n}\n\n/** Options for {@link renderPdf}. */\nexport interface RenderPdfOptions extends TakumiPdfOptions {\n /** Forwarded to `runtime.surfaces.svg.renderPages`, so every page shares one frame. */\n svg?: PngSvgOptions;\n}\n\n/** What {@link renderPdf} resolves to: the bytes, the page count and size, and one finding set per composition. */\nexport interface RenderPdfResult {\n pdf: Buffer;\n pageCount: number;\n width: number;\n height: number;\n /** One per composition, in order. */\n validations: PngValidationResult[];\n}\n\n/**\n * Validates, renders, and writes a deck as one PDF in one call, a page per\n * composition.\n *\n * Every composition is rendered, even an invalid one: `validations` is how a\n * caller finds out, rather than a thrown error. What is drawn is the copy\n * validation checked. Validation runs twice, once here and once inside\n * `renderPages`, which discards its own result. An empty deck throws the\n * runtime's `EMPTY_PAGES`.\n */\nexport const renderPdf = async (\n runtime: PdfRuntime,\n deck: readonly unknown[],\n backend: TakumiPdfBackend,\n { svg, ...options }: RenderPdfOptions = {}\n): Promise<RenderPdfResult> => {\n const pages = deck.map((composition) =>\n checkedForDrawing('renderPdf', runtime.validate(composition))\n );\n const validations = pages.map(({ validation }) => validation);\n const rendered = runtime.surfaces.svg.renderPages(\n pages.map(({ checked }) => checked),\n {\n ...svg,\n onValidationError: 'collect',\n }\n );\n const pdf = await backend.pdf(rendered, options);\n return {\n pdf,\n pageCount: rendered.pages.length,\n width: rendered.width,\n height: rendered.height,\n validations,\n };\n};\n"]}
1
+ {"version":3,"file":"render_pdf.js","sourceRoot":"","sources":["../src/render_pdf.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,iBAAiB,GAIlB,MAAM,cAAc,CAAC;AAkCtB;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,IAAwB,EACxB,OAAyB,EACzB,EAAE,QAAQ,EAAE,GAAG,OAAO,KAAuB,EAAE,EACrB,EAAE;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CACrC,iBAAiB,CAAC,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAC9D,CAAC;IACF,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,CACpD,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,EACnC;QACE,GAAG,QAAQ;QACX,iBAAiB,EAAE,SAAS;KAC7B,CACF,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO;QACL,GAAG;QACH,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,MAAM;QAChC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,WAAW;KACZ,CAAC;AACJ,CAAC,CAAC","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 type { PdfInput, TakumiPdfBackend, TakumiPdfOptions } from './backend';\nimport {\n checkedForDrawing,\n type PngCheckedValidationResult,\n type PngValidationResult,\n type SnapshotOptions,\n} from './render_png';\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link PdfInput}. A runtime built with `frames` satisfies it.\n */\nexport interface PdfRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n snapshot: {\n renderPages(\n compositions: readonly unknown[],\n options?: SnapshotOptions & { onValidationError?: 'collect' | 'throw' }\n ): PdfInput;\n };\n };\n}\n\n/** Options for {@link renderPdf}. */\nexport interface RenderPdfOptions extends TakumiPdfOptions {\n /** Forwarded to `runtime.surfaces.snapshot.renderPages`, so every page shares one frame. */\n snapshot?: SnapshotOptions;\n}\n\n/** What {@link renderPdf} resolves to: the bytes, the page count and size, and one finding set per composition. */\nexport interface RenderPdfResult {\n pdf: Buffer;\n pageCount: number;\n width: number;\n height: number;\n /** One per composition, in order. */\n validations: PngValidationResult[];\n}\n\n/**\n * Validates, renders, and writes a deck as one PDF in one call, a page per\n * composition.\n *\n * Every composition is rendered, even an invalid one: `validations` is how a\n * caller finds out, rather than a thrown error. What is drawn is the copy\n * validation checked. Validation runs twice, once here and once inside\n * `renderPages`, which discards its own result. An empty deck throws the\n * runtime's `EMPTY_PAGES`.\n */\nexport const renderPdf = async (\n runtime: PdfRuntime,\n deck: readonly unknown[],\n backend: TakumiPdfBackend,\n { snapshot, ...options }: RenderPdfOptions = {}\n): Promise<RenderPdfResult> => {\n const pages = deck.map((composition) =>\n checkedForDrawing('renderPdf', runtime.validate(composition))\n );\n const validations = pages.map(({ validation }) => validation);\n const rendered = runtime.surfaces.snapshot.renderPages(\n pages.map(({ checked }) => checked),\n {\n ...snapshot,\n onValidationError: 'collect',\n }\n );\n const pdf = await backend.pdf(rendered, options);\n return {\n pdf,\n pageCount: rendered.pages.length,\n width: rendered.width,\n height: rendered.height,\n validations,\n };\n};\n"]}
@@ -20,10 +20,10 @@ export declare const checkedForDrawing: (caller: string, { composition, ...valid
20
20
  validation: PngValidationResult;
21
21
  };
22
22
  /**
23
- * The `svg` surface's per-render options, mirroring the runtime's
24
- * `SvgRenderOptions` so a caller gets completion without importing it.
23
+ * The `snapshot` surface's per-render options, mirroring the runtime's
24
+ * `SnapshotRenderOptions` so a caller gets completion without importing it.
25
25
  */
26
- export interface PngSvgOptions {
26
+ export interface SnapshotOptions {
27
27
  /** Which of the runtime's frames this render uses; defaults to its `defaultFrame`. */
28
28
  frame?: string;
29
29
  /** Overrides the frame's `defaultWidth`. */
@@ -35,15 +35,15 @@ export interface PngSvgOptions {
35
35
  }
36
36
  /**
37
37
  * The slice of `IsomerRuntime` this helper needs, declared structurally like
38
- * {@link ImageInput} so this package depends on no isomer package. `surfaces.svg`
39
- * is required where the runtime's generic form has `SvgSurface | undefined`; a
40
- * runtime built with `frames` carries `SvgSurface` there and satisfies this.
38
+ * {@link ImageInput} so this package depends on no isomer package. `surfaces.snapshot`
39
+ * is required where the runtime's generic form has `SnapshotSurface | undefined`; a
40
+ * runtime built with `frames` carries `SnapshotSurface` there and satisfies this.
41
41
  */
42
42
  export interface PngRuntime {
43
43
  validate(composition: unknown): PngCheckedValidationResult;
44
44
  surfaces: {
45
- svg: {
46
- render(composition: unknown, options?: PngSvgOptions & {
45
+ snapshot: {
46
+ render(composition: unknown, options?: SnapshotOptions & {
47
47
  onValidationError?: 'collect' | 'throw';
48
48
  }): ImageInput;
49
49
  };
@@ -51,12 +51,13 @@ export interface PngRuntime {
51
51
  }
52
52
  /** Options for {@link renderPng}. */
53
53
  export interface RenderPngOptions extends TakumiRenderOptions {
54
- /** Forwarded to `runtime.surfaces.svg.render`. */
55
- svg?: PngSvgOptions;
54
+ /** Forwarded to `runtime.surfaces.snapshot.render`. */
55
+ snapshot?: SnapshotOptions;
56
56
  }
57
57
  /** What {@link renderPng} resolves to: the bytes, their size, and the findings on the composition drawn. */
58
58
  export interface RenderPngResult {
59
59
  png: Buffer;
60
+ /** The viewport, in CSS pixels; the raster is this times `scale`, rounded. */
60
61
  width: number;
61
62
  height: number;
62
63
  validation: PngValidationResult;
@@ -70,5 +71,5 @@ export interface RenderPngResult {
70
71
  * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which
71
72
  * discards its own result.
72
73
  */
73
- export declare const renderPng: (runtime: PngRuntime, composition: unknown, backend: TakumiImageBackend, { svg, ...options }?: RenderPngOptions) => Promise<RenderPngResult>;
74
+ export declare const renderPng: (runtime: PngRuntime, composition: unknown, backend: TakumiImageBackend, { snapshot, ...options }?: RenderPngOptions) => Promise<RenderPngResult>;
74
75
  //# sourceMappingURL=render_png.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render_png.d.ts","sourceRoot":"","sources":["../src/render_png.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAEnB,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CAC5B;AAED,iHAAiH;AACjH,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,MAAM,EACd,gCAAgC,0BAA0B,KACzD;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,mBAAmB,CAAA;CAiBrD,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAC3D,QAAQ,EAAE;QACR,GAAG,EAAE;YACH,MAAM,CACJ,WAAW,EAAE,OAAO,EACpB,OAAO,CAAC,EAAE,aAAa,GAAG;gBAAE,iBAAiB,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;aAAE,GACpE,UAAU,CAAC;SACf,CAAC;KACH,CAAC;CACH;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,kDAAkD;IAClD,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB;AAED,4GAA4G;AAC5G,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,mBAAmB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,SAAS,UAAU,EACnB,aAAa,OAAO,EACpB,SAAS,kBAAkB,EAC3B,sBAAqB,gBAAqB,KACzC,OAAO,CAAC,eAAe,CAWzB,CAAC"}
1
+ {"version":3,"file":"render_png.d.ts","sourceRoot":"","sources":["../src/render_png.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,WAAW,CAAC;AAEnB,2FAA2F;AAC3F,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CAC5B;AAED,iHAAiH;AACjH,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,MAAM,EACd,gCAAgC,0BAA0B,KACzD;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,mBAAmB,CAAA;CAiBrD,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,sFAAsF;IACtF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,0BAA0B,CAAC;IAC3D,QAAQ,EAAE;QACR,QAAQ,EAAE;YACR,MAAM,CACJ,WAAW,EAAE,OAAO,EACpB,OAAO,CAAC,EAAE,eAAe,GAAG;gBAAE,iBAAiB,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;aAAE,GACtE,UAAU,CAAC;SACf,CAAC;KACH,CAAC;CACH;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,uDAAuD;IACvD,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,4GAA4G;AAC5G,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,mBAAmB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,SAAS,UAAU,EACnB,aAAa,OAAO,EACpB,SAAS,kBAAkB,EAC3B,2BAA0B,gBAAqB,KAC9C,OAAO,CAAC,eAAe,CAWzB,CAAC"}
@@ -33,10 +33,10 @@ export const checkedForDrawing = (caller, { composition, ...validation }) => {
33
33
  * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which
34
34
  * discards its own result.
35
35
  */
36
- export const renderPng = async (runtime, composition, backend, { svg, ...options } = {}) => {
36
+ export const renderPng = async (runtime, composition, backend, { snapshot, ...options } = {}) => {
37
37
  const { checked, validation } = checkedForDrawing('renderPng', runtime.validate(composition));
38
- const rendered = runtime.surfaces.svg.render(checked, {
39
- ...svg,
38
+ const rendered = runtime.surfaces.snapshot.render(checked, {
39
+ ...snapshot,
40
40
  onValidationError: 'collect',
41
41
  });
42
42
  const png = await backend.png(rendered, options);
@@ -1 +1 @@
1
- {"version":3,"file":"render_png.js","sourceRoot":"","sources":["../src/render_png.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmBH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,MAAc,EACd,EAAE,WAAW,EAAE,GAAG,UAAU,EAA8B,EACH,EAAE;IACzD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,4DAA4D,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CAAC,GAAG,MAAM,sDAAsD,CAAC,EAC1E;QACE,IAAI,EAAE,4BAA4B;QAClC,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CACF,CAAC;AACJ,CAAC,CAAC;AAiDF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,WAAoB,EACpB,OAA2B,EAC3B,EAAE,GAAG,EAAE,GAAG,OAAO,KAAuB,EAAE,EAChB,EAAE;IAC5B,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAC/C,WAAW,EACX,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAC9B,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE;QACpD,GAAG,GAAG;QACN,iBAAiB,EAAE,SAAS;KAC7B,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;AAC7E,CAAC,CAAC","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 type {\n ImageInput,\n TakumiImageBackend,\n TakumiRenderOptions,\n} from './backend';\n\n/** A validation result shaped like the SDK's `ValidationResult`, declared structurally. */\nexport interface PngValidationResult {\n valid: boolean;\n errors: readonly unknown[];\n}\n\n/** What `runtime.validate` returns: findings, and the copy they describe, `undefined` only for refused input. */\nexport type PngCheckedValidationResult = PngValidationResult & {\n composition: unknown;\n};\n\n/**\n * Splits a {@link PngCheckedValidationResult} into the copy to draw and the findings to report.\n *\n * Throws rather than draw the caller's value: shaped as the SDK's\n * `CompositionValidationError` when the runtime refused the input, and a plain\n * `Error` when the runtime returned no copy at all.\n */\nexport const checkedForDrawing = (\n caller: string,\n { composition, ...validation }: PngCheckedValidationResult\n): { checked: unknown; validation: PngValidationResult } => {\n if (composition !== undefined) {\n return { checked: composition, validation };\n }\n if (validation.valid) {\n throw new Error(\n `${caller}: runtime.validate returned no checked composition to draw`\n );\n }\n throw Object.assign(\n new Error(`${caller}: the runtime refused the composition before parsing`),\n {\n name: 'CompositionValidationError',\n code: 'COMPOSITION_INVALID',\n errors: validation.errors,\n }\n );\n};\n\n/**\n * The `svg` surface's per-render options, mirroring the runtime's\n * `SvgRenderOptions` so a caller gets completion without importing it.\n */\nexport interface PngSvgOptions {\n /** Which of the runtime's frames this render uses; defaults to its `defaultFrame`. */\n frame?: string;\n /** Overrides the frame's `defaultWidth`. */\n width?: number;\n /** Overrides the frame's estimated height. */\n height?: number;\n /** Theme mode; falls back to the composition's own `theme`. `auto` resolves light. */\n theme?: 'light' | 'dark' | 'auto';\n}\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link ImageInput} so this package depends on no isomer package. `surfaces.svg`\n * is required where the runtime's generic form has `SvgSurface | undefined`; a\n * runtime built with `frames` carries `SvgSurface` there and satisfies this.\n */\nexport interface PngRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n svg: {\n render(\n composition: unknown,\n options?: PngSvgOptions & { onValidationError?: 'collect' | 'throw' }\n ): ImageInput;\n };\n };\n}\n\n/** Options for {@link renderPng}. */\nexport interface RenderPngOptions extends TakumiRenderOptions {\n /** Forwarded to `runtime.surfaces.svg.render`. */\n svg?: PngSvgOptions;\n}\n\n/** What {@link renderPng} resolves to: the bytes, their size, and the findings on the composition drawn. */\nexport interface RenderPngResult {\n png: Buffer;\n width: number;\n height: number;\n validation: PngValidationResult;\n}\n\n/**\n * Validates, renders, and rasterizes a composition in one call.\n *\n * The composition is rendered even when invalid: `validation` is how a caller\n * finds out, rather than a thrown error. What is drawn is the copy validation\n * checked, and input refused before parsing, which has none, throws\n * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which\n * discards its own result.\n */\nexport const renderPng = async (\n runtime: PngRuntime,\n composition: unknown,\n backend: TakumiImageBackend,\n { svg, ...options }: RenderPngOptions = {}\n): Promise<RenderPngResult> => {\n const { checked, validation } = checkedForDrawing(\n 'renderPng',\n runtime.validate(composition)\n );\n const rendered = runtime.surfaces.svg.render(checked, {\n ...svg,\n onValidationError: 'collect',\n });\n const png = await backend.png(rendered, options);\n return { png, width: rendered.width, height: rendered.height, validation };\n};\n"]}
1
+ {"version":3,"file":"render_png.js","sourceRoot":"","sources":["../src/render_png.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmBH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,MAAc,EACd,EAAE,WAAW,EAAE,GAAG,UAAU,EAA8B,EACH,EAAE;IACzD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC9C,CAAC;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,4DAA4D,CACtE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CAAC,GAAG,MAAM,sDAAsD,CAAC,EAC1E;QACE,IAAI,EAAE,4BAA4B;QAClC,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CACF,CAAC;AACJ,CAAC,CAAC;AAkDF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,KAAK,EAC5B,OAAmB,EACnB,WAAoB,EACpB,OAA2B,EAC3B,EAAE,QAAQ,EAAE,GAAG,OAAO,KAAuB,EAAE,EACrB,EAAE;IAC5B,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAC/C,WAAW,EACX,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAC9B,CAAC;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE;QACzD,GAAG,QAAQ;QACX,iBAAiB,EAAE,SAAS;KAC7B,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;AAC7E,CAAC,CAAC","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 type {\n ImageInput,\n TakumiImageBackend,\n TakumiRenderOptions,\n} from './backend';\n\n/** A validation result shaped like the SDK's `ValidationResult`, declared structurally. */\nexport interface PngValidationResult {\n valid: boolean;\n errors: readonly unknown[];\n}\n\n/** What `runtime.validate` returns: findings, and the copy they describe, `undefined` only for refused input. */\nexport type PngCheckedValidationResult = PngValidationResult & {\n composition: unknown;\n};\n\n/**\n * Splits a {@link PngCheckedValidationResult} into the copy to draw and the findings to report.\n *\n * Throws rather than draw the caller's value: shaped as the SDK's\n * `CompositionValidationError` when the runtime refused the input, and a plain\n * `Error` when the runtime returned no copy at all.\n */\nexport const checkedForDrawing = (\n caller: string,\n { composition, ...validation }: PngCheckedValidationResult\n): { checked: unknown; validation: PngValidationResult } => {\n if (composition !== undefined) {\n return { checked: composition, validation };\n }\n if (validation.valid) {\n throw new Error(\n `${caller}: runtime.validate returned no checked composition to draw`\n );\n }\n throw Object.assign(\n new Error(`${caller}: the runtime refused the composition before parsing`),\n {\n name: 'CompositionValidationError',\n code: 'COMPOSITION_INVALID',\n errors: validation.errors,\n }\n );\n};\n\n/**\n * The `snapshot` surface's per-render options, mirroring the runtime's\n * `SnapshotRenderOptions` so a caller gets completion without importing it.\n */\nexport interface SnapshotOptions {\n /** Which of the runtime's frames this render uses; defaults to its `defaultFrame`. */\n frame?: string;\n /** Overrides the frame's `defaultWidth`. */\n width?: number;\n /** Overrides the frame's estimated height. */\n height?: number;\n /** Theme mode; falls back to the composition's own `theme`. `auto` resolves light. */\n theme?: 'light' | 'dark' | 'auto';\n}\n\n/**\n * The slice of `IsomerRuntime` this helper needs, declared structurally like\n * {@link ImageInput} so this package depends on no isomer package. `surfaces.snapshot`\n * is required where the runtime's generic form has `SnapshotSurface | undefined`; a\n * runtime built with `frames` carries `SnapshotSurface` there and satisfies this.\n */\nexport interface PngRuntime {\n validate(composition: unknown): PngCheckedValidationResult;\n surfaces: {\n snapshot: {\n render(\n composition: unknown,\n options?: SnapshotOptions & { onValidationError?: 'collect' | 'throw' }\n ): ImageInput;\n };\n };\n}\n\n/** Options for {@link renderPng}. */\nexport interface RenderPngOptions extends TakumiRenderOptions {\n /** Forwarded to `runtime.surfaces.snapshot.render`. */\n snapshot?: SnapshotOptions;\n}\n\n/** What {@link renderPng} resolves to: the bytes, their size, and the findings on the composition drawn. */\nexport interface RenderPngResult {\n png: Buffer;\n /** The viewport, in CSS pixels; the raster is this times `scale`, rounded. */\n width: number;\n height: number;\n validation: PngValidationResult;\n}\n\n/**\n * Validates, renders, and rasterizes a composition in one call.\n *\n * The composition is rendered even when invalid: `validation` is how a caller\n * finds out, rather than a thrown error. What is drawn is the copy validation\n * checked, and input refused before parsing, which has none, throws\n * `CompositionValidationError`. Validation runs twice, once here and once inside `render`, which\n * discards its own result.\n */\nexport const renderPng = async (\n runtime: PngRuntime,\n composition: unknown,\n backend: TakumiImageBackend,\n { snapshot, ...options }: RenderPngOptions = {}\n): Promise<RenderPngResult> => {\n const { checked, validation } = checkedForDrawing(\n 'renderPng',\n runtime.validate(composition)\n );\n const rendered = runtime.surfaces.snapshot.render(checked, {\n ...snapshot,\n onValidationError: 'collect',\n });\n const png = await backend.png(rendered, options);\n return { png, width: rendered.width, height: rendered.height, validation };\n};\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elastic/isomer-image-takumi",
3
- "version": "0.1.0",
4
- "description": "Takumi image backend for Isomer: rasterizes the svg surface's output to PNG, SVG, or PDF.",
3
+ "version": "0.2.0",
4
+ "description": "Takumi image backend for Isomer: rasterizes the snapshot surface's output to PNG, SVG, or PDF.",
5
5
  "license": "Elastic-2.0",
6
6
  "author": "Elastic",
7
7
  "repository": {
@@ -32,10 +32,6 @@
32
32
  "@takumi-rs/helpers": "2.14.0",
33
33
  "takumi-pdf": "0.15.0"
34
34
  },
35
- "peerDependencies": {
36
- "react": ">=18 <20",
37
- "react-dom": ">=18 <20"
38
- },
39
35
  "devDependencies": {
40
36
  "@fontsource/inter": "^5.3.0",
41
37
  "@types/react": "^18.2.14",