wasm-html-to-image 0.1.3 โ†’ 0.1.5

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
@@ -17,7 +17,7 @@ No headless browsers (Chromium, Puppeteer, or Playwright) required. Runs seamles
17
17
 
18
18
  - โšก **Single Unified WASM Binary**: HTML parsing, CSS layout, vector painting, and image encoding execute entirely inside WebAssembly without copying intermediate framebuffers across JS boundaries.
19
19
  - ๐ŸŽจ **Versatile Inputs**: Renders HTML strings, remote Web URLs, multi-page HTML arrays, or raw image buffers (PNG, JPEG, WebP, GIF, AVIF, BMP, Data URLs).
20
- - ๐Ÿ“ฆ **Rich Output Formats**: Direct SVG vector streams, high-quality PNG, JPEG, WebP, AVIF, uncompressed RAW pixels, ThumbHash placeholders, and multi-page vector PDFs.
20
+ - ๐Ÿ“ฆ **Rich Output Formats**: Direct SVG vector streams, high-quality PNG, JPEG, WebP, AVIF, JXL, uncompressed RAW pixels, ThumbHash placeholders, and multi-page vector PDFs.
21
21
  - ๐Ÿš€ **Zero-Config Single Bundle**: Use `wasm-html-to-image/single` to call `render()` immediately without manual WASM loading or build setup.
22
22
  - ๐ŸŒ **Edge & Serverless Native**: Dedicated subpaths (`/workerd` for Cloudflare Workers, `/edge-light` for Vercel Edge) tailored for strict serverless constraints.
23
23
  - ๐Ÿงต **Multi-Threaded Worker Pool**: Process thousands of images in parallel using `wasm-html-to-image/workers` (Node.js worker threads / Web Workers).
@@ -149,6 +149,7 @@ All formats return a `RenderResult<T>` object containing `.data` (`Uint8Array` o
149
149
  | `jpeg` | `Uint8Array` | Skia render โ†’ JPEG encode (`quality`) | Decodes and compresses JPEG |
150
150
  | `webp` | `Uint8Array` | Skia render โ†’ WebP encode (`quality`) | Decodes and compresses WebP |
151
151
  | `avif` | `Uint8Array` | Skia render โ†’ AVIF encode (`quality`, `speed`) | Decodes and compresses AVIF |
152
+ | `jxl` | `Uint8Array` | Skia render โ†’ JXL encode (`quality`, `speed`) | Decodes and compresses JXL |
152
153
  | `raw` | `Uint8Array` | Uncompressed RGBA pixel bytes | Uncompressed RGBA pixel bytes |
153
154
  | `thumbhash` | `Uint8Array` | Computes ThumbHash from render | Computes ThumbHash from image |
154
155
  | `svg` | `string` | Skia vector drawing stream | Single `<image>` wrapper SVG |
@@ -1,12 +1 @@
1
- /**
2
- * Browser worker-side implementation (pre-bundled `dist/web-workers.js`).
3
- * Mirrors `./child-workers.js`, but the WASM is the compressed payload
4
- * (`dist/html-to-image-wasm.js`, restored via `DecompressionStream`) handed
5
- * to the regular glue as `wasmBinary`, so no `.wasm` fetch/`locateFile`
6
- * is needed.
7
- */
8
- declare const map: {
9
- render(options: import("./core.js").HtmlToImageOptions): Promise<import("./core.js").RenderResult<Uint8Array | string>>;
10
- };
11
- export type HtmlToImageWorker = typeof map;
12
- export {};
1
+ export type { HtmlToImageWorker } from "./workers.js";
@@ -22,5 +22,6 @@ const getModule = async () => {
22
22
  * (`dist/html-to-image-wasm.js`, restored via `DecompressionStream`) handed
23
23
  * to the regular glue as `wasmBinary`, so no `.wasm` fetch/`locateFile`
24
24
  * is needed.
25
+ * Canonical `HtmlToImageWorker` type lives in `./workers.js`.
25
26
  */
26
- const map = initWorker(createRenderActions(getModule));
27
+ initWorker(createRenderActions(getModule));
@@ -1,11 +1 @@
1
- /**
2
- * Worker-side implementation.
3
- * Exposes the unified render (HTML and image inputs) via worker-lib.
4
- * Each worker thread loads its own module instance (modules cannot cross
5
- * thread boundaries, so nothing is shared with the parent).
6
- */
7
- declare const map: {
8
- render(options: import("./core.js").HtmlToImageOptions): Promise<import("./core.js").RenderResult<Uint8Array | string>>;
9
- };
10
- export type HtmlToImageWorker = typeof map;
11
- export {};
1
+ export type { HtmlToImageWorker } from "./workers.js";
@@ -15,5 +15,6 @@ const getModule = async () => {
15
15
  * Exposes the unified render (HTML and image inputs) via worker-lib.
16
16
  * Each worker thread loads its own module instance (modules cannot cross
17
17
  * thread boundaries, so nothing is shared with the parent).
18
+ * Canonical `HtmlToImageWorker` type lives in `./workers.js`.
18
19
  */
19
- const map = initWorker(createRenderActions(getModule));
20
+ initWorker(createRenderActions(getModule));
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const OUTPUT_FORMATS = [
11
11
  "jpeg",
12
12
  "webp",
13
13
  "avif",
14
+ "jxl",
14
15
  "raw",
15
16
  "thumbhash",
16
17
  "none",
@@ -36,7 +37,7 @@ async function readInput(input) {
36
37
  }
37
38
  program
38
39
  .name("wasm-html-to-image")
39
- .description("HTML to image converter on the single unified WASM module (svg/pdf direct, png/jpeg/webp/avif/raw/thumbhash via encode or html_to_image)")
40
+ .description("HTML to image converter on the single unified WASM module (svg/pdf direct, png/jpeg/webp/avif/jxl/raw/thumbhash via encode or html_to_image)")
40
41
  .version("0.1.0")
41
42
  .argument("<input>", "input HTML/image file path or URL")
42
43
  .option("-o, --output <path>", "output file path")
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Orchestration + pipeline layer split from `./core.ts` (behavior unchanged).
3
+ *
4
+ * Owns: binding-error helper, orchestration context (log gate, timings,
5
+ * diagnostics state), HTML source resolution (`value`/`url`), the image-input
6
+ * pipeline (`converter_*`), the HTML instance pipeline (`satoru_render` +
7
+ * `converter_encode`), and the legacy `html_to_image` fallback.
8
+ *
9
+ * `core.ts` keeps public types and the `htmlToImage` overloads and delegates
10
+ * here. `isImageInput` (`./input.ts`) is the single branch selector;
11
+ * satoru option objects come from `./encode-args.ts` (canonical builder).
12
+ */
13
+ import type { HtmlToImageModule } from "./loader.js";
14
+ import { LogLevel, type RenderLimits, type ResolveResourceHook } from "./diagnostics.js";
15
+ import { type DiagState, type ResolveContext } from "./core-resources.js";
16
+ import type { HtmlToImageOptions, OutputFormat, RenderResult } from "./core.js";
17
+ declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
18
+ export { DEFAULT_USER_AGENT };
19
+ /** Binding names reported by {@link requireBindingError}. */
20
+ export type WasmBindingName = "satoru_render" | "converter_encode" | "converter_encode_svg" | "converter_encode_pdf" | "converter_load_image" | "html_to_image";
21
+ export declare function requireBindingError(name: WasmBindingName): Error;
22
+ /** Mutable orchestration state for one `htmlToImage` call. */
23
+ export interface Orchestration {
24
+ format: OutputFormat;
25
+ quality: number;
26
+ speed: number;
27
+ animation: boolean;
28
+ width: number;
29
+ height: number | undefined;
30
+ crop: HtmlToImageOptions["crop"];
31
+ fit: HtmlToImageOptions["fit"];
32
+ value: unknown;
33
+ url: string | undefined;
34
+ baseUrl: string | undefined;
35
+ fontMap: Record<string, string> | undefined;
36
+ userAgent: string | undefined;
37
+ fallbackFonts: (Uint8Array | ArrayBuffer | string)[] | undefined;
38
+ logLevel: LogLevel;
39
+ onLog: HtmlToImageOptions["onLog"];
40
+ diagnostics: boolean;
41
+ onDiagnostics: HtmlToImageOptions["onDiagnostics"];
42
+ resolveResource: ResolveResourceHook | undefined;
43
+ mediaType: "screen" | "print";
44
+ textToPaths: boolean | undefined;
45
+ css: string | undefined;
46
+ preloadFonts: {
47
+ name: string;
48
+ data: Uint8Array;
49
+ }[] | undefined;
50
+ limits: RenderLimits;
51
+ pdfTitle: string | undefined;
52
+ pdfAuthor: string | undefined;
53
+ pdfSubject: string | undefined;
54
+ pdfKeywords: string | undefined;
55
+ pdfCreator: string | undefined;
56
+ pdfProducer: string | undefined;
57
+ pdfMargin: HtmlToImageOptions["pdfMargin"];
58
+ pdfHeader: string | undefined;
59
+ pdfFooter: string | undefined;
60
+ fmtInt: number;
61
+ mediaTypeInt: number;
62
+ t0: number;
63
+ diag: DiagState | null;
64
+ timings: Record<string, number>;
65
+ emitLog: (level: LogLevel, message: string) => void;
66
+ logged: (message: string) => Error;
67
+ loggedBinding: (name: WasmBindingName) => Error;
68
+ addTime: (name: string, ms: number) => void;
69
+ checkTimeout: () => void;
70
+ deliverReport: () => void;
71
+ resolveCtx: ResolveContext;
72
+ }
73
+ /** Build per-call orchestration state (log gate, timings, diag, resolve ctx). */
74
+ export declare function createOrchestration(module: HtmlToImageModule, options: HtmlToImageOptions): Orchestration;
75
+ /**
76
+ * Resolve `value`/`url` to HTML source. `isImageInput` callers branch before
77
+ * this; HTML `value` passes through, `url` is fetched (hook-aware).
78
+ */
79
+ export declare function resolveHtmlSource(orch: Orchestration): Promise<string | string[]>;
80
+ /**
81
+ * Image-input pipeline: skip rendering, straight to `converter_*`.
82
+ * Call only when `isImageInput(value)` holds; byte normalization uses
83
+ * `imageBytesFromUnknown` (`unknown` in, no `as` casts).
84
+ */
85
+ export declare function runImageInput(module: HtmlToImageModule, value: unknown, orch: Orchestration): Promise<RenderResult<Uint8Array | string>>;
86
+ /**
87
+ * HTML instance pipeline (`satoru_render` direct for svg/pdf, PNG
88
+ * intermediate + `converter_encode` otherwise). Returns `null` when the
89
+ * instance render bindings are absent so the caller can try the legacy
90
+ * fallback (unified `html_to_image` builds its own instance and cannot see
91
+ * pre-resolved resources, so it stays a fallback only).
92
+ */
93
+ export declare function tryRunHtmlInstance(module: HtmlToImageModule, htmls: string | string[], satoruOpts: Record<string, unknown>, defaultFontMap: Record<string, string>, orch: Orchestration): Promise<RenderResult<Uint8Array | string> | null>;
94
+ /**
95
+ * Legacy fallback for builds without instance render bindings.
96
+ * Returns `null` when the unified binding is absent.
97
+ */
98
+ export declare function tryRunLegacyFallback(module: HtmlToImageModule, htmls: string | string[], satoruOpts: Record<string, unknown>, orch: Orchestration): Promise<RenderResult<Uint8Array | string> | null>;
@@ -0,0 +1,435 @@
1
+ import { FIT_INT, FORMAT_INT } from "./encode-args.js";
2
+ import { imageBytesFromUnknown, toBytes, } from "./input.js";
3
+ import { DIAGNOSTIC_CODES, LogLevel, } from "./diagnostics.js";
4
+ import { now, resolveHtmlResources, } from "./core-resources.js";
5
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
6
+ export { DEFAULT_USER_AGENT };
7
+ export function requireBindingError(name) {
8
+ return new Error(`wasm-html-to-image: single-WASM binding "${name}" is required but not exposed by this build ` +
9
+ `(rebuild the unified module; single-WASM only).`);
10
+ }
11
+ /** Build per-call orchestration state (log gate, timings, diag, resolve ctx). */
12
+ export function createOrchestration(module, options) {
13
+ const { format = "png", quality = 85, speed = 6, animation = false, width, height, crop, fit, value, url, baseUrl, fontMap, userAgent, fallbackFonts, logLevel = LogLevel.None, onLog, diagnostics = false, onDiagnostics, resolveResource, mediaType = "screen", textToPaths, css, fonts: preloadFonts, limits = {}, pdfTitle, pdfAuthor, pdfSubject, pdfKeywords, pdfCreator, pdfProducer, pdfMargin, pdfHeader, pdfFooter, } = options;
14
+ const fmtInt = FORMAT_INT[format];
15
+ const mediaTypeInt = mediaType === "print" ? 1 : 0;
16
+ /** JS-side log hook, gated by `logLevel` (satoru parity). */
17
+ const emitLog = (level, message) => {
18
+ if (onLog && logLevel !== LogLevel.None && level <= logLevel) {
19
+ try {
20
+ onLog(level, message);
21
+ }
22
+ catch {
23
+ // User hooks must not break rendering.
24
+ }
25
+ }
26
+ };
27
+ /** Build an Error with an Error-level log line (`throw logged(...)`). */
28
+ const logged = (message) => {
29
+ emitLog(LogLevel.Error, message);
30
+ return new Error(message);
31
+ };
32
+ const loggedBinding = (name) => {
33
+ const e = requireBindingError(name);
34
+ emitLog(LogLevel.Error, e.message);
35
+ return e;
36
+ };
37
+ const t0 = now();
38
+ const diag = diagnostics
39
+ ? {
40
+ resources: [],
41
+ fonts: [],
42
+ warnings: [],
43
+ errors: [],
44
+ totalResourceBytes: 0,
45
+ resourceCount: 0,
46
+ }
47
+ : null;
48
+ const timings = {};
49
+ const addTime = (name, ms) => {
50
+ if (diag)
51
+ timings[name] = (timings[name] ?? 0) + ms;
52
+ };
53
+ const checkTimeout = () => {
54
+ if (limits.timeoutMs !== undefined && now() - t0 >= limits.timeoutMs) {
55
+ const message = `Render timed out after ${limits.timeoutMs}ms`;
56
+ diag?.errors.push({ code: DIAGNOSTIC_CODES.LIMIT_TIMEOUT, message });
57
+ throw logged(message);
58
+ }
59
+ };
60
+ /** Deliver the diagnostics report on success (satoru parity). */
61
+ const deliverReport = () => {
62
+ if (!diag || !onDiagnostics)
63
+ return;
64
+ try {
65
+ onDiagnostics({
66
+ version: 1,
67
+ format: format,
68
+ width,
69
+ height,
70
+ mediaType,
71
+ timings,
72
+ resources: diag.resources,
73
+ fonts: diag.fonts,
74
+ warnings: diag.warnings,
75
+ errors: diag.errors,
76
+ });
77
+ }
78
+ catch {
79
+ // User hooks must not break rendering.
80
+ }
81
+ };
82
+ const resolveCtx = {
83
+ mediaTypeInt,
84
+ css,
85
+ fonts: preloadFonts,
86
+ limits,
87
+ resolveResource,
88
+ diag,
89
+ t0,
90
+ emitLog,
91
+ addTime,
92
+ checkTimeout,
93
+ };
94
+ if (logLevel !== LogLevel.None) {
95
+ try {
96
+ module.satoru_set_log_level(logLevel);
97
+ }
98
+ catch {
99
+ // Non-fatal; JS-side logging still works.
100
+ }
101
+ }
102
+ return {
103
+ format: format,
104
+ quality,
105
+ speed,
106
+ animation,
107
+ width,
108
+ height,
109
+ crop,
110
+ fit,
111
+ value,
112
+ url,
113
+ baseUrl,
114
+ fontMap,
115
+ userAgent,
116
+ fallbackFonts,
117
+ logLevel,
118
+ onLog,
119
+ diagnostics,
120
+ onDiagnostics,
121
+ resolveResource,
122
+ mediaType,
123
+ textToPaths,
124
+ css,
125
+ preloadFonts,
126
+ limits,
127
+ pdfTitle,
128
+ pdfAuthor,
129
+ pdfSubject,
130
+ pdfKeywords,
131
+ pdfCreator,
132
+ pdfProducer,
133
+ pdfMargin,
134
+ pdfHeader,
135
+ pdfFooter,
136
+ fmtInt,
137
+ mediaTypeInt,
138
+ t0,
139
+ diag,
140
+ timings,
141
+ emitLog,
142
+ logged,
143
+ loggedBinding,
144
+ addTime,
145
+ checkTimeout,
146
+ deliverReport,
147
+ resolveCtx,
148
+ };
149
+ }
150
+ /**
151
+ * Resolve `value`/`url` to HTML source. `isImageInput` callers branch before
152
+ * this; HTML `value` passes through, `url` is fetched (hook-aware).
153
+ */
154
+ export async function resolveHtmlSource(orch) {
155
+ const { value, url, userAgent, resolveResource, addTime, logged } = orch;
156
+ if (value !== undefined) {
157
+ return value;
158
+ }
159
+ if (typeof url === "string") {
160
+ const headers = { "User-Agent": userAgent ?? DEFAULT_USER_AGENT };
161
+ const fetchStart = now();
162
+ let htmls;
163
+ if (resolveResource) {
164
+ const fallbackFetch = async () => {
165
+ const res = await fetch(url, { headers });
166
+ if (!res.ok)
167
+ return null;
168
+ return toBytes(await res.arrayBuffer());
169
+ };
170
+ const raw = await resolveResource(url, fallbackFetch);
171
+ if (!raw) {
172
+ throw logged(`wasm-html-to-image: failed to fetch HTML from URL: ${url}`);
173
+ }
174
+ htmls = new TextDecoder().decode(raw);
175
+ }
176
+ else {
177
+ const res = await fetch(url, { headers });
178
+ if (!res.ok) {
179
+ throw logged(`wasm-html-to-image: failed to fetch HTML from URL: ${url} (${res.status})`);
180
+ }
181
+ htmls = await res.text();
182
+ }
183
+ addTime("fetchHtml", now() - fetchStart);
184
+ return htmls;
185
+ }
186
+ throw logged("wasm-html-to-image: either 'value' or 'url' must be provided.");
187
+ }
188
+ /**
189
+ * Image-input pipeline: skip rendering, straight to `converter_*`.
190
+ * Call only when `isImageInput(value)` holds; byte normalization uses
191
+ * `imageBytesFromUnknown` (`unknown` in, no `as` casts).
192
+ */
193
+ export async function runImageInput(module, value, orch) {
194
+ const { format, quality, speed, animation, width, height, crop, fit, fmtInt, t0, emitLog, logged, loggedBinding, addTime, deliverReport, } = orch;
195
+ // Narrowed here: caller selected this branch via `isImageInput(value)`.
196
+ const loadImage = module.converter_load_image;
197
+ if (typeof loadImage !== "function")
198
+ throw loggedBinding("converter_load_image");
199
+ const bytes = imageBytesFromUnknown(value);
200
+ const inst = module.converter_create_instance();
201
+ try {
202
+ if (!loadImage(inst, bytes)) {
203
+ throw logged("wasm-html-to-image: failed to load image input (unsupported or corrupt)");
204
+ }
205
+ const originalWidth = module.converter_get_original_width?.(inst) ?? 0;
206
+ const originalHeight = module.converter_get_original_height?.(inst) ?? 0;
207
+ const originalFormat = module.converter_get_original_format?.(inst) ?? "";
208
+ const originalAnimation = module.converter_is_original_animation?.(inst) ?? false;
209
+ if (format === "none") {
210
+ addTime("total", now() - t0);
211
+ emitLog(LogLevel.Info, "render done (image input, none/passthrough)");
212
+ deliverReport();
213
+ return {
214
+ data: bytes,
215
+ width: originalWidth,
216
+ height: originalHeight,
217
+ format: "none",
218
+ originalWidth,
219
+ originalHeight,
220
+ originalFormat,
221
+ originalAnimation,
222
+ animation: originalAnimation,
223
+ };
224
+ }
225
+ if (crop) {
226
+ module.converter_crop(inst, crop.x, crop.y, crop.width, crop.height);
227
+ }
228
+ if (width !== undefined || height !== undefined) {
229
+ module.converter_resize(inst, width ?? 0, height ?? 0, FIT_INT[fit ?? "contain"]);
230
+ }
231
+ const outWidth = module.converter_get_width?.(inst) ?? width ?? originalWidth;
232
+ const outHeight = module.converter_get_height?.(inst) ?? height ?? originalHeight;
233
+ const outAnimation = module.converter_is_animation?.(inst) ?? animation;
234
+ const outFormat = module.converter_get_format?.(inst) || format;
235
+ if (format === "svg") {
236
+ const svgBinding = module.converter_encode_svg;
237
+ if (typeof svgBinding !== "function")
238
+ throw loggedBinding("converter_encode_svg");
239
+ const encodeStart = now();
240
+ const svg = (await svgBinding(inst));
241
+ addTime("encode", now() - encodeStart);
242
+ if (!svg) {
243
+ throw logged("wasm-html-to-image: failed to encode image to svg");
244
+ }
245
+ addTime("total", now() - t0);
246
+ emitLog(LogLevel.Info, "render done (image input, svg)");
247
+ deliverReport();
248
+ return {
249
+ data: svg,
250
+ width: outWidth,
251
+ height: outHeight,
252
+ format: "svg",
253
+ originalWidth,
254
+ originalHeight,
255
+ originalFormat,
256
+ originalAnimation,
257
+ animation: false,
258
+ };
259
+ }
260
+ if (format === "pdf") {
261
+ const pdfBinding = module.converter_encode_pdf;
262
+ if (typeof pdfBinding !== "function")
263
+ throw loggedBinding("converter_encode_pdf");
264
+ const encodeStart = now();
265
+ const out = (await pdfBinding(inst));
266
+ addTime("encode", now() - encodeStart);
267
+ if (out == null) {
268
+ throw logged("wasm-html-to-image: failed to encode image to pdf");
269
+ }
270
+ addTime("total", now() - t0);
271
+ emitLog(LogLevel.Info, "render done (image input, pdf)");
272
+ deliverReport();
273
+ return {
274
+ data: new Uint8Array(out instanceof Uint8Array ? out : new Uint8Array(out)),
275
+ width: outWidth,
276
+ height: outHeight,
277
+ format: "pdf",
278
+ originalWidth,
279
+ originalHeight,
280
+ originalFormat,
281
+ originalAnimation,
282
+ animation: false,
283
+ };
284
+ }
285
+ const encodeBinding = module.converter_encode;
286
+ if (typeof encodeBinding !== "function")
287
+ throw loggedBinding("converter_encode");
288
+ const encodeStart = now();
289
+ const out = (await encodeBinding(inst, fmtInt, quality, speed, animation));
290
+ addTime("encode", now() - encodeStart);
291
+ if (out == null) {
292
+ throw logged("wasm-html-to-image: failed to encode image");
293
+ }
294
+ addTime("total", now() - t0);
295
+ emitLog(LogLevel.Info, `render done (image input, ${format})`);
296
+ deliverReport();
297
+ return {
298
+ data: new Uint8Array(out instanceof Uint8Array ? out : new Uint8Array(out)),
299
+ width: outWidth,
300
+ height: outHeight,
301
+ format: outFormat,
302
+ originalWidth,
303
+ originalHeight,
304
+ originalFormat,
305
+ originalAnimation,
306
+ animation: outAnimation,
307
+ };
308
+ }
309
+ finally {
310
+ module.converter_destroy_instance(inst);
311
+ }
312
+ }
313
+ /**
314
+ * HTML instance pipeline (`satoru_render` direct for svg/pdf, PNG
315
+ * intermediate + `converter_encode` otherwise). Returns `null` when the
316
+ * instance render bindings are absent so the caller can try the legacy
317
+ * fallback (unified `html_to_image` builds its own instance and cannot see
318
+ * pre-resolved resources, so it stays a fallback only).
319
+ */
320
+ export async function tryRunHtmlInstance(module, htmls, satoruOpts, defaultFontMap, orch) {
321
+ const { format, quality, speed, animation, width, height, baseUrl, fontMap, userAgent, fallbackFonts, fmtInt, t0, emitLog, logged, loggedBinding, addTime, deliverReport, resolveCtx, } = orch;
322
+ const renderBinding = module.satoru_render;
323
+ const encodeBinding = module.converter_encode;
324
+ if (typeof renderBinding !== "function" ||
325
+ typeof encodeBinding !== "function") {
326
+ return null;
327
+ }
328
+ const loadImage = module.converter_load_image;
329
+ if (typeof loadImage !== "function")
330
+ throw loggedBinding("converter_load_image");
331
+ const sInst = module.satoru_create_instance();
332
+ try {
333
+ const resolveStart = now();
334
+ await resolveHtmlResources(module, sInst, htmls, width, height, baseUrl, fontMap ?? defaultFontMap, userAgent ?? DEFAULT_USER_AGENT, fallbackFonts, resolveCtx);
335
+ addTime("resolveResources", now() - resolveStart);
336
+ emitLog(LogLevel.Info, "resources resolved");
337
+ if (format === "svg" || format === "pdf") {
338
+ const renderStart = now();
339
+ const out = (await renderBinding(sInst, htmls, width, height ?? 0, fmtInt, satoruOpts));
340
+ addTime("render", now() - renderStart);
341
+ if (out == null) {
342
+ throw logged("wasm-html-to-image: satoru_render returned null");
343
+ }
344
+ const bytes = new Uint8Array(out instanceof Uint8Array ? out : new Uint8Array(out));
345
+ addTime("total", now() - t0);
346
+ emitLog(LogLevel.Info, `render done: format=${format}`);
347
+ deliverReport();
348
+ if (format === "svg") {
349
+ const svgStr = new TextDecoder().decode(bytes);
350
+ let outHeight = height ?? 0;
351
+ if (outHeight === 0) {
352
+ const matchH = svgStr.match(/height="(\d+(?:\.\d+)?)"/);
353
+ if (matchH) {
354
+ outHeight = Math.round(parseFloat(matchH[1]));
355
+ }
356
+ }
357
+ return {
358
+ data: svgStr,
359
+ width,
360
+ height: outHeight,
361
+ format: "svg",
362
+ };
363
+ }
364
+ return {
365
+ data: bytes,
366
+ width,
367
+ height: height ?? 0,
368
+ format: "pdf",
369
+ };
370
+ }
371
+ const renderStart = now();
372
+ const png = (await renderBinding(sInst, htmls, width, height ?? 0, FORMAT_INT.png, satoruOpts));
373
+ addTime("render", now() - renderStart);
374
+ if (png == null) {
375
+ throw logged("wasm-html-to-image: satoru_render returned null");
376
+ }
377
+ const cInst = module.converter_create_instance();
378
+ try {
379
+ if (!loadImage(cInst, png)) {
380
+ throw logged("wasm-html-to-image: failed to load render output");
381
+ }
382
+ const outWidth = module.converter_get_width?.(cInst) ?? width;
383
+ const outHeight = module.converter_get_height?.(cInst) ?? height ?? 0;
384
+ const outAnimation = module.converter_is_animation?.(cInst) ?? animation;
385
+ const outFormat = module.converter_get_format?.(cInst) || format;
386
+ const encodeStart = now();
387
+ const out = (await encodeBinding(cInst, fmtInt, quality, speed, animation));
388
+ addTime("encode", now() - encodeStart);
389
+ if (out == null) {
390
+ throw logged("wasm-html-to-image: failed to encode image");
391
+ }
392
+ addTime("total", now() - t0);
393
+ emitLog(LogLevel.Info, `render done: format=${format}`);
394
+ deliverReport();
395
+ return {
396
+ data: new Uint8Array(out instanceof Uint8Array ? out : new Uint8Array(out)),
397
+ width: outWidth,
398
+ height: outHeight,
399
+ format: outFormat,
400
+ animation: outAnimation,
401
+ };
402
+ }
403
+ finally {
404
+ module.converter_destroy_instance(cInst);
405
+ }
406
+ }
407
+ finally {
408
+ module.satoru_destroy_instance(sInst);
409
+ }
410
+ }
411
+ /**
412
+ * Legacy fallback for builds without instance render bindings.
413
+ * Returns `null` when the unified binding is absent.
414
+ */
415
+ export async function tryRunLegacyFallback(module, htmls, satoruOpts, orch) {
416
+ const { format, quality, speed, animation, width, height, fmtInt, t0, emitLog, logged, addTime, deliverReport, } = orch;
417
+ const unified = module.html_to_image;
418
+ if (typeof unified !== "function") {
419
+ return null;
420
+ }
421
+ const out = (await unified(htmls, width, height ?? 0, fmtInt, satoruOpts, quality, speed, animation));
422
+ if (out == null) {
423
+ throw logged("wasm-html-to-image: html_to_image returned null");
424
+ }
425
+ const bytes = new Uint8Array(out instanceof Uint8Array ? out : new Uint8Array(out));
426
+ addTime("total", now() - t0);
427
+ emitLog(LogLevel.Info, `render done (legacy): format=${format}`);
428
+ deliverReport();
429
+ return {
430
+ data: format === "svg" ? new TextDecoder().decode(bytes) : bytes,
431
+ width,
432
+ height: height ?? 0,
433
+ format,
434
+ };
435
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Resource layer split from `./core.ts` (behavior unchanged).
3
+ *
4
+ * Owns: fetched-bytes cache, `fetchResourceBytes`, pending-resources
5
+ * parsing, allow-list checks, and the `resolveHtmlResources` discovery loop.
6
+ * Orchestration context types (`DiagState`, `ResolveContext`) live here so
7
+ * both the loop and the orchestration/pipeline layer share them.
8
+ */
9
+ import { type HtmlToImageModule } from "./loader.js";
10
+ import { LogLevel, type DiagnosticMessage, type FontDiagnostic, type RenderLimits, type ResolveResourceHook, type ResourceDiagnostic } from "./diagnostics.js";
11
+ /**
12
+ * Fetch external resource bytes. `data:` URLs resolve inline in C++
13
+ * (`request()`), so they are skipped here. Relative URLs resolve against
14
+ * `baseUrl` (http base via fetch, fs path via node:fs on Node).
15
+ */
16
+ export declare function fetchResourceBytes(url: string, baseUrl?: string, userAgent?: string, resolveResource?: ResolveResourceHook): Promise<Uint8Array | null>;
17
+ export interface PendingResource {
18
+ type: "font" | "image" | "css";
19
+ url: string;
20
+ name: string;
21
+ }
22
+ /** Parse the `get_pending_resources` binary form (see loader.ts). */
23
+ export declare function parsePendingResources(bin: Uint8Array | null | undefined): PendingResource[];
24
+ /** Monotonic-ish clock for timings (satoru parity). */
25
+ export declare function now(): number;
26
+ /** Mutable diagnostics collection state (internal, `diagnostics: true` only). */
27
+ export interface DiagState {
28
+ resources: ResourceDiagnostic[];
29
+ fonts: FontDiagnostic[];
30
+ warnings: DiagnosticMessage[];
31
+ errors: DiagnosticMessage[];
32
+ totalResourceBytes: number;
33
+ resourceCount: number;
34
+ }
35
+ /** Resolution context threaded through HTML discovery (internal). */
36
+ export interface ResolveContext {
37
+ mediaTypeInt: number;
38
+ css?: string;
39
+ fonts?: {
40
+ name: string;
41
+ data: Uint8Array;
42
+ }[];
43
+ limits: RenderLimits;
44
+ resolveResource?: ResolveResourceHook;
45
+ diag: DiagState | null;
46
+ t0: number;
47
+ emitLog: (level: LogLevel, message: string) => void;
48
+ /** Accumulate a timing (no-op unless diagnostics are enabled). */
49
+ addTime: (name: string, ms: number) => void;
50
+ /** Throw a timeout error when `limits.timeoutMs` is exceeded. */
51
+ checkTimeout: () => void;
52
+ }
53
+ /**
54
+ * Protocol/host allow-list check. Returns a block reason, or `null` when
55
+ * allowed. Relative URLs skip the check (satoru parity).
56
+ */
57
+ export declare function checkResourceAllowed(url: string, limits: RenderLimits): string | null;
58
+ /** Limit-violation code for a block reason (satoru parity). */
59
+ export declare function limitCodeForReason(reason: string): string;
60
+ /**
61
+ * Upstream-style discovery loop: collect pending URLs on the satoru
62
+ * instance, fetch them (file/http), and inject the bytes back, so the
63
+ * subsequent render on the SAME instance sees cached fonts/images.
64
+ * Missing bindings (old builds) skip the loop silently.
65
+ */
66
+ export declare function resolveHtmlResources(module: HtmlToImageModule, satoruInst: unknown, htmls: string | string[], width: number, height: number | undefined, baseUrl: string | undefined, fontMap: Record<string, string> | undefined, userAgent: string | undefined, fallbackFonts: (Uint8Array | ArrayBuffer | string)[] | undefined, ctx: ResolveContext): Promise<void>;