wasm-html-to-image 0.1.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/browser-workers.d.ts +12 -0
  4. package/dist/browser-workers.js +26 -0
  5. package/dist/child-workers.d.ts +11 -0
  6. package/dist/child-workers.js +19 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +87 -0
  9. package/dist/core.d.ts +168 -0
  10. package/dist/core.js +824 -0
  11. package/dist/diagnostics.d.ts +93 -0
  12. package/dist/diagnostics.js +28 -0
  13. package/dist/edge-light.d.ts +18 -0
  14. package/dist/edge-light.js +70 -0
  15. package/dist/encode-args.d.ts +10 -0
  16. package/dist/encode-args.js +30 -0
  17. package/dist/html-to-image-single.js +0 -0
  18. package/dist/html-to-image-wasm.js +3 -0
  19. package/dist/html-to-image.js +2 -0
  20. package/dist/html-to-image.wasm +0 -0
  21. package/dist/index.d.ts +14 -0
  22. package/dist/index.js +14 -0
  23. package/dist/input.d.ts +23 -0
  24. package/dist/input.js +90 -0
  25. package/dist/loader.d.ts +132 -0
  26. package/dist/loader.js +199 -0
  27. package/dist/preact.d.ts +10 -0
  28. package/dist/preact.js +13 -0
  29. package/dist/react.d.ts +9 -0
  30. package/dist/react.js +11 -0
  31. package/dist/single.d.ts +27 -0
  32. package/dist/single.js +22 -0
  33. package/dist/tailwind.d.ts +1 -0
  34. package/dist/tailwind.js +6 -0
  35. package/dist/wasm-payload.d.ts +2 -0
  36. package/dist/wasm-payload.js +39 -0
  37. package/dist/web-workers.js +4411 -0
  38. package/dist/worker-actions.d.ts +10 -0
  39. package/dist/worker-actions.js +13 -0
  40. package/dist/worker-lib-loader.d.ts +19 -0
  41. package/dist/worker-lib-loader.js +34 -0
  42. package/dist/workerd.d.ts +18 -0
  43. package/dist/workerd.js +65 -0
  44. package/dist/workers-dummy.d.ts +12 -0
  45. package/dist/workers-dummy.js +6 -0
  46. package/dist/workers-parent.js +602 -0
  47. package/dist/workers.d.ts +86 -0
  48. package/dist/workers.js +163 -0
  49. package/package.json +166 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ ο»ΏMIT License
2
+
3
+ Copyright (c) 2026 SoraKumo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the \"Software\"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # wasm-html-to-image
2
+
3
+ [![npm version](https://img.shields.io/npm/v/wasm-html-to-image.svg)](https://www.npmjs.com/package/wasm-html-to-image)
4
+ [![npm downloads](https://img.shields.io/npm/dw/wasm-html-to-image.svg)](https://www.npmjs.com/package/wasm-html-to-image)
5
+ [![license](https://img.shields.io/npm/l/wasm-html-to-image.svg)](https://github.com/SoraKumo001/wasm-html-to-image/blob/master/LICENSE)
6
+ [![Playground](https://img.shields.io/badge/Demo-Playground-blueviolet)](https://sorakumo001.github.io/wasm-html-to-image/master)
7
+ [![Documentation](https://img.shields.io/badge/Docs-Docusaurus-blue)](https://sorakumo001.github.io/wasm-html-to-image/)
8
+
9
+ A high-performance, WebAssembly-powered HTML-to-image renderer and image optimization engine. It consolidates HTML rendering (**Satoru** / **Skia** / **litehtml**) and native image encoding (**WebP / AVIF / JPEG**) into a single, unified WASM binary.
10
+
11
+ No headless browsers (Chromium, Puppeteer, or Playwright) required. Runs seamlessly across Node.js, Cloudflare Workers, Edge runtimes, browsers, and the CLI.
12
+
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - ⚑ **Single Unified WASM Binary**: HTML parsing, CSS layout, vector painting, and image encoding execute entirely inside WebAssembly without copying intermediate framebuffers across JS boundaries.
18
+ - 🎨 **Versatile Inputs**: Renders HTML strings, remote Web URLs, multi-page HTML arrays, or raw image buffers (PNG, JPEG, WebP, GIF, AVIF, BMP, Data URLs).
19
+ - πŸ“¦ **Rich Output Formats**: Direct SVG vector streams, high-quality PNG, JPEG, WebP, AVIF, uncompressed RAW pixels, ThumbHash placeholders, and multi-page vector PDFs.
20
+ - πŸš€ **Zero-Config Single Bundle**: Use `wasm-html-to-image/single` to call `render()` immediately without manual WASM loading or build setup.
21
+ - 🌐 **Edge & Serverless Native**: Dedicated subpaths (`/workerd` for Cloudflare Workers, `/edge-light` for Vercel Edge) tailored for strict serverless constraints.
22
+ - 🧡 **Multi-Threaded Worker Pool**: Process thousands of images in parallel using `wasm-html-to-image/workers` (Node.js worker threads / Web Workers).
23
+ - βš›οΈ **Ecosystem Ready**: First-class support for React / Preact JSX components and Tailwind CSS inlined styling.
24
+
25
+ ---
26
+
27
+ ## πŸ“¦ Installation
28
+
29
+ ```bash npm2yarn
30
+ npm install wasm-html-to-image
31
+ ```
32
+
33
+ ---
34
+
35
+ ## πŸš€ Quick Start
36
+
37
+ ### Zero-Config Rendering (`wasm-html-to-image`)
38
+
39
+ The easiest way to get started. WASM is embedded and loaded automatically (in Cloudflare Workers, the workerd-optimized module is automatically selected):
40
+
41
+ ```typescript
42
+ import { render } from "wasm-html-to-image";
43
+
44
+ // 1. Render HTML to PNG (returns RenderResult with .data and dimensions)
45
+ const result = await render({
46
+ value: `<div style="background: linear-gradient(135deg, #667eea, #764ba2); padding: 40px; color: white; border-radius: 16px; font-family: sans-serif;">
47
+ <h1 style="margin: 0; font-size: 32px;">Hello wasm-html-to-image</h1>
48
+ <p style="margin-top: 8px; opacity: 0.9;">High-performance serverless rendering</p>
49
+ </div>`,
50
+ width: 800,
51
+ format: "png",
52
+ });
53
+
54
+ console.log(result.data); // Uint8Array
55
+ console.log(result.width, result.height); // 800, 600
56
+
57
+ // 2. Convert and resize image with the same render() function
58
+ const webp = await render({
59
+ value: result.data, // Pass raw image buffer directly
60
+ width: 400, // Target resized width
61
+ format: "webp",
62
+ quality: 80,
63
+ });
64
+
65
+ console.log(webp.originalWidth, webp.originalHeight); // Original image dimensions
66
+ console.log(webp.width, webp.height); // Output image dimensions
67
+ ```
68
+
69
+ ---
70
+
71
+ ## πŸ–ΌοΈ Image-to-Image Conversion & Optimization
72
+
73
+ `wasm-html-to-image` is not only an HTML renderer β€” it also serves as a high-speed, self-contained **image-to-image converter, resizer, and compressor** without requiring heavy native libraries like Sharp or ImageMagick.
74
+
75
+ Simply pass raw image bytes (`Uint8Array` or `Buffer`) or a `data:image/...` URL to `render()`. It automatically identifies input magic bytes, skips the HTML layout pass, and routes straight into the native Skia image pipeline. Every call returns rich metadata (`width`, `height`, `originalWidth`, `originalHeight`, `format`, `isAnimated`) along with the output data in `.data`:
76
+
77
+ ```typescript
78
+ import fs from "node:fs/promises";
79
+ import { render } from "wasm-html-to-image/single";
80
+
81
+ const imageBuffer = await fs.readFile("photo.jpg");
82
+
83
+ // 1. Convert JPEG -> AVIF (highest compression)
84
+ const avif = await render({
85
+ value: imageBuffer,
86
+ width: 800,
87
+ format: "avif",
88
+ quality: 75,
89
+ speed: 6, // 0 (best quality) - 10 (fastest)
90
+ });
91
+ await fs.writeFile("photo.avif", Buffer.from(avif.data));
92
+
93
+ // 2. Crop & Resize -> PNG
94
+ const cropped = await render({
95
+ value: imageBuffer,
96
+ crop: { x: 50, y: 50, width: 300, height: 300 },
97
+ width: 150,
98
+ height: 150,
99
+ format: "png",
100
+ });
101
+ await fs.writeFile("cropped.png", Buffer.from(cropped.data));
102
+
103
+ // 3. Generate ThumbHash Blur Placeholder
104
+ const thumbhash = await render({
105
+ value: imageBuffer,
106
+ width: 100,
107
+ format: "thumbhash",
108
+ });
109
+ console.log("ThumbHash bytes:", thumbhash.data);
110
+
111
+ // 4. Convert Image -> Single-Page Vector PDF
112
+ const pdf = await render({
113
+ value: imageBuffer,
114
+ format: "pdf",
115
+ });
116
+ await fs.writeFile("photo.pdf", Buffer.from(pdf.data));
117
+
118
+ // 5. Convert Animated GIF -> Animated WebP
119
+ const animatedWebp = await render({
120
+ value: await fs.readFile("animation.gif"),
121
+ format: "webp",
122
+ animation: true,
123
+ });
124
+ await fs.writeFile("animation.webp", Buffer.from(animatedWebp.data));
125
+ ```
126
+
127
+ ---
128
+
129
+ ## πŸ“Š Format Matrix
130
+
131
+ ### Inputs (`value` / `url`)
132
+
133
+ | Input Type | Detection | Description |
134
+ | --------------------------- | -------------------------------- | ---------------------------------------------------------- |
135
+ | **HTML String** | Text starting with tags / markup | Standard HTML/CSS rendering |
136
+ | **HTML Array (`string[]`)** | Array of HTML strings | Multi-page PDF generation (one page per item) |
137
+ | **URL (`url`)** | `http://` or `https://` | Automatically fetches and renders remote web pages |
138
+ | **Image Buffer** | `Uint8Array` / `Buffer` | Magic-byte recognition for PNG, JPEG, WebP, GIF, AVIF, BMP |
139
+ | **Data URL** | `data:image/*;base64,...` | Inlined image data URL, routed straight to image pipeline |
140
+
141
+ ### Outputs (`format`)
142
+
143
+ All formats return a `RenderResult<T>` object containing `.data` (`Uint8Array` or `string` for SVG) and metadata (`width`, `height`, `originalWidth`, `originalHeight`, `format`, `isAnimated`):
144
+
145
+ | Format | `result.data` Type | HTML Input | Image Input |
146
+ | ----------- | ------------------ | ---------------------------------------------- | --------------------------------------------- |
147
+ | `png` | `Uint8Array` | Skia render β†’ PNG encode | Decodes and converts to PNG |
148
+ | `jpeg` | `Uint8Array` | Skia render β†’ JPEG encode (`quality`) | Decodes and compresses JPEG |
149
+ | `webp` | `Uint8Array` | Skia render β†’ WebP encode (`quality`) | Decodes and compresses WebP |
150
+ | `avif` | `Uint8Array` | Skia render β†’ AVIF encode (`quality`, `speed`) | Decodes and compresses AVIF |
151
+ | `raw` | `Uint8Array` | Uncompressed RGBA pixel bytes | Uncompressed RGBA pixel bytes |
152
+ | `thumbhash` | `Uint8Array` | Computes ThumbHash from render | Computes ThumbHash from image |
153
+ | `svg` | `string` | Skia vector drawing stream | Single `<image>` wrapper SVG |
154
+ | `pdf` | `Uint8Array` | SkPDFDocument vector PDF | Single-page centered PDF |
155
+ | `none` | `Uint8Array` | N/A (throws error) | Passthrough original bytes + extract metadata |
156
+
157
+ ---
158
+
159
+ ## 🧩 Subpaths & Usage
160
+
161
+ Choose the optimal entry point for your application and environment:
162
+
163
+ | Subpath | Target Environment | Highlights |
164
+ | ------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------- |
165
+ | `wasm-html-to-image` | Universal (Node, Edge, Cloudflare, Deno) | Zero-config default: auto routes to single/workerd, instant `render()` |
166
+ | `wasm-html-to-image/single` | Node.js, Bundlers | Single bundled WASM entry point |
167
+ | `wasm-html-to-image/index` | High-throughput servers | Explicit `loadHtmlToImageModule()` & manual instance reuse |
168
+ | `wasm-html-to-image/workerd` | Cloudflare Workers | WebAssembly.Module compilation compatible |
169
+ | `wasm-html-to-image/edge-light` | Vercel Edge Runtime | Optimized for Edge Runtime constraints |
170
+ | `wasm-html-to-image/workers` | Node.js, Browsers | Multi-threaded worker pool for high concurrency |
171
+ | `wasm-html-to-image/react` | React integration | Directly render React JSX element trees |
172
+ | `wasm-html-to-image/preact` | Preact integration | Directly render Preact JSX element trees |
173
+ | `wasm-html-to-image/tailwind` | Utility styling | Inlines UnoCSS / Tailwind classes |
174
+
175
+ ---
176
+
177
+ ## πŸ’‘ Practical Examples
178
+
179
+ ### 1. High-Throughput Node.js Server (Instance Reuse)
180
+
181
+ Reuse the WASM module across incoming HTTP requests for maximum performance:
182
+
183
+ ```typescript
184
+ import { loadHtmlToImageModule, htmlToImage } from "wasm-html-to-image";
185
+ import express from "express";
186
+
187
+ const app = express();
188
+ const mod = await loadHtmlToImageModule(); // Load once at startup
189
+
190
+ app.get("/ogp", async (req, res) => {
191
+ const result = await htmlToImage(mod, {
192
+ value: `<h1>${req.query.title}</h1>`,
193
+ width: 1200,
194
+ height: 630,
195
+ format: "webp",
196
+ quality: 85,
197
+ });
198
+
199
+ res.type("image/webp").send(Buffer.from(result.data));
200
+ });
201
+
202
+ app.listen(3000);
203
+ ```
204
+
205
+ ### 2. Cloudflare Workers (`workerd`)
206
+
207
+ Generate dynamic social preview images at the edge:
208
+
209
+ ```typescript
210
+ // wrangler.jsonc:
211
+ // { "rules": [{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }] }
212
+
213
+ import { render } from "wasm-html-to-image/workerd";
214
+
215
+ export default {
216
+ async fetch(request: Request): Promise<Response> {
217
+ const result = await render({
218
+ value: `<div style="padding: 40px; font-family: sans-serif; background: #0f172a; color: white;">
219
+ <h1>Edge OGP Generator</h1>
220
+ </div>`,
221
+ width: 1200,
222
+ height: 630,
223
+ format: "png",
224
+ });
225
+
226
+ return new Response(result.data, {
227
+ headers: {
228
+ "Content-Type": "image/png",
229
+ "Cache-Control": "public, max-age=86400",
230
+ },
231
+ });
232
+ },
233
+ };
234
+ ```
235
+
236
+ ### 3. Multi-Threaded Batch Generation (`workers`)
237
+
238
+ Leverage multi-core CPUs to process large batches of images in parallel:
239
+
240
+ ```typescript
241
+ import { render } from "wasm-html-to-image/workers";
242
+
243
+ const tasks = items.map((item) =>
244
+ render({
245
+ value: `<h1>${item.title}</h1>`,
246
+ width: 600,
247
+ height: 400,
248
+ format: "webp",
249
+ }),
250
+ );
251
+
252
+ const images = await Promise.all(tasks);
253
+ ```
254
+
255
+ ### 4. Command Line Interface (CLI)
256
+
257
+ Render directly from your shell without writing code:
258
+
259
+ ```bash
260
+ # Render HTML to WebP
261
+ npx wasm-html-to-image template.html -o banner.webp -w 1200 -h 630 -f webp -q 90
262
+
263
+ # Capture screenshot from URL
264
+ npx wasm-html-to-image https://example.com -o site.png -w 1280 -h 720
265
+
266
+ # Convert and resize an image
267
+ npx wasm-html-to-image photo.jpg -o photo.avif -w 800 -f avif -q 80
268
+ ```
269
+
270
+ ---
271
+
272
+ ## πŸ“– Documentation
273
+
274
+ For full guides, architecture deep-dives, font management, and advanced recipes:
275
+
276
+ πŸ‘‰ **[Read the Official Documentation](https://sorakumo001.github.io/wasm-html-to-image/)**
277
+
278
+ - [Overview & Getting Started](https://sorakumo001.github.io/wasm-html-to-image/docs/overview)
279
+ - [Architecture & Pipelines](https://sorakumo001.github.io/wasm-html-to-image/docs/architecture)
280
+ - [API Reference](https://sorakumo001.github.io/wasm-html-to-image/docs/api-reference)
281
+ - [Runtime Guide (Node / Workers / Edge / Browser)](https://sorakumo001.github.io/wasm-html-to-image/docs/api-reference/runtime-guide)
282
+ - [Production Recipes](https://sorakumo001.github.io/wasm-html-to-image/docs/recipes)
283
+
284
+ ---
285
+
286
+ ## πŸ› οΈ Development
287
+
288
+ <details>
289
+ <summary>Click to view repository build and test instructions</summary>
290
+
291
+ ### Prerequisites
292
+
293
+ - Node.js >= 20, pnpm >= 9
294
+ - Emscripten SDK (emsdk), CMake, Ninja, vcpkg (for C++ WASM builds)
295
+
296
+ ### Building
297
+
298
+ ```bash
299
+ # Install workspace dependencies
300
+ pnpm install
301
+
302
+ # Build TypeScript packages
303
+ pnpm --filter wasm-html-to-image build
304
+ pnpm build:js
305
+
306
+ # Build Documentation
307
+ pnpm docs:build
308
+ ```
309
+
310
+ ### Testing
311
+
312
+ ```bash
313
+ # Unit tests (Vitest)
314
+ pnpm --filter wasm-html-to-image test
315
+
316
+ # Visual regression tests
317
+ pnpm --filter visual-test test
318
+ ```
319
+
320
+ </details>
321
+
322
+ ---
323
+
324
+ ## πŸ“„ License
325
+
326
+ MIT Β© [SoraKumo](https://github.com/SoraKumo001)
@@ -0,0 +1,12 @@
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 {};
@@ -0,0 +1,26 @@
1
+ import { loadWorkerLib } from "./worker-lib-loader.js";
2
+ import { loadHtmlToImageModule, } from "./loader.js";
3
+ import { loadCompressedWasmBinary } from "./wasm-payload.js";
4
+ import { createRenderActions } from "./worker-actions.js";
5
+ // @ts-expect-error β€” dist/html-to-image.js is generated at build time
6
+ import createHtmlToImageModule from "../dist/html-to-image.js";
7
+ const { initWorker } = await loadWorkerLib();
8
+ let mod;
9
+ /** Lazily load (and reuse) the module inside this worker. */
10
+ const getModule = async () => {
11
+ if (!mod) {
12
+ mod = await loadHtmlToImageModule({
13
+ factory: createHtmlToImageModule,
14
+ moduleArg: { wasmBinary: await loadCompressedWasmBinary() },
15
+ });
16
+ }
17
+ return mod;
18
+ };
19
+ /**
20
+ * Browser worker-side implementation (pre-bundled `dist/web-workers.js`).
21
+ * Mirrors `./child-workers.js`, but the WASM is the compressed payload
22
+ * (`dist/html-to-image-wasm.js`, restored via `DecompressionStream`) handed
23
+ * to the regular glue as `wasmBinary`, so no `.wasm` fetch/`locateFile`
24
+ * is needed.
25
+ */
26
+ const map = initWorker(createRenderActions(getModule));
@@ -0,0 +1,11 @@
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 {};
@@ -0,0 +1,19 @@
1
+ import { loadWorkerLib } from "./worker-lib-loader.js";
2
+ import { getDefaultModule } from "./single.js";
3
+ import { createRenderActions } from "./worker-actions.js";
4
+ const { initWorker } = await loadWorkerLib();
5
+ let mod;
6
+ /** Lazily load (and reuse) the single module inside this worker. */
7
+ const getModule = async () => {
8
+ if (!mod) {
9
+ mod = await getDefaultModule();
10
+ }
11
+ return mod;
12
+ };
13
+ /**
14
+ * Worker-side implementation.
15
+ * Exposes the unified render (HTML and image inputs) via worker-lib.
16
+ * Each worker thread loads its own module instance (modules cannot cross
17
+ * thread boundaries, so nothing is shared with the parent).
18
+ */
19
+ const map = initWorker(createRenderActions(getModule));
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { render, } from "./single.js";
6
+ import { isImageInput } from "./core.js";
7
+ const OUTPUT_FORMATS = [
8
+ "svg",
9
+ "png",
10
+ "pdf",
11
+ "jpeg",
12
+ "webp",
13
+ "avif",
14
+ "raw",
15
+ "thumbhash",
16
+ "none",
17
+ ];
18
+ function isOutputFormat(value) {
19
+ return OUTPUT_FORMATS.includes(value);
20
+ }
21
+ /**
22
+ * Read a local input file as binary; image files (magic-byte sniffed) are
23
+ * passed through as bytes, everything else as UTF-8 text (HTML).
24
+ */
25
+ async function readInput(input) {
26
+ const data = await fs.readFile(input);
27
+ let value = data.toString("utf-8");
28
+ try {
29
+ if (isImageInput(data))
30
+ value = data;
31
+ }
32
+ catch {
33
+ // Unknown binary magic -> treat as text and let rendering decide.
34
+ }
35
+ return { value, baseUrl: path.dirname(path.resolve(input)) };
36
+ }
37
+ program
38
+ .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
+ .version("0.1.0")
41
+ .argument("<input>", "input HTML/image file path or URL")
42
+ .option("-o, --output <path>", "output file path")
43
+ .option("-w, --width <number>", "viewport width", (v) => parseInt(v, 10), 800)
44
+ .option("-h, --height <number>", "viewport height (omit for auto)", (v) => parseInt(v, 10))
45
+ .option("-f, --format <format>", `output format (${OUTPUT_FORMATS.join(", ")})`, "png")
46
+ .option("-q, --quality <number>", "encode quality 0-100 (encode stage only)", (v) => parseFloat(v), 85)
47
+ .action(async (input, options) => {
48
+ try {
49
+ const format = String(options.format).toLowerCase();
50
+ if (!isOutputFormat(format)) {
51
+ console.error(`Error: unsupported format "${options.format}". Choose from: ${OUTPUT_FORMATS.join(", ")}`);
52
+ process.exit(1);
53
+ }
54
+ const isWindowsPath = /^[a-zA-Z]:[\\/]/.test(input);
55
+ const isUrl = !isWindowsPath &&
56
+ /^[a-z][a-z0-9+.-]*:/i.test(input) &&
57
+ !input.startsWith("data:");
58
+ let outputPath = options.output;
59
+ if (!outputPath) {
60
+ const ext = format;
61
+ outputPath = isUrl
62
+ ? `output.${ext}`
63
+ : path.format({
64
+ dir: path.dirname(input),
65
+ name: path.basename(input, path.extname(input)),
66
+ ext: `.${ext}`,
67
+ });
68
+ }
69
+ const renderOptions = {
70
+ width: options.width,
71
+ height: options.height,
72
+ format,
73
+ quality: options.quality,
74
+ ...(isUrl ? { url: input, baseUrl: input } : await readInput(input)),
75
+ };
76
+ const result = await render(renderOptions);
77
+ await fs.writeFile(outputPath, typeof result.data === "string"
78
+ ? result.data
79
+ : Buffer.from(result.data));
80
+ console.log(`Successfully rendered to ${outputPath}`);
81
+ }
82
+ catch (err) {
83
+ console.error("Error during rendering:", err);
84
+ process.exit(1);
85
+ }
86
+ });
87
+ program.parseAsync(process.argv);
package/dist/core.d.ts ADDED
@@ -0,0 +1,168 @@
1
+ import type { HtmlToImageModule } from "./loader.js";
2
+ import { isImageInput } from "./input.js";
3
+ import { DIAGNOSTIC_CODES, LogLevel, type RenderDiagnostics, type RenderLimits, type ResolveResourceHook } from "./diagnostics.js";
4
+ export { isImageInput };
5
+ export { DIAGNOSTIC_CODES, LogLevel };
6
+ export type { DiagnosticMessage, FontDiagnostic, RenderDiagnostics, RenderLimits, ResolveResourceHook, ResourceDiagnostic, } from "./diagnostics.js";
7
+ /**
8
+ * Final output formats. Single-WASM internal routing:
9
+ * - HTML input: resources are resolved on the instance first
10
+ * (`satoru_collect_resources` -> fetch -> `satoru_add_resource`), then
11
+ * `satoru_render` direct (svg/pdf) or `satoru_render` PNG intermediate ->
12
+ * `converter_encode`. The unified `html_to_image` binding is only a legacy
13
+ * fallback for builds without instance render bindings.
14
+ * - Image input: render stage skipped, straight to `converter_*`
15
+ * (`converter_load_image` -> optional crop/resize -> `converter_encode`,
16
+ * or `converter_encode_svg` / `converter_encode_pdf`).
17
+ */
18
+ export type OutputFormat = "svg" | "png" | "pdf" | "jpeg" | "webp" | "avif" | "raw" | "thumbhash" | "none";
19
+ /** Result of rendering an HTML document or converting an image. */
20
+ export interface RenderResult<T = Uint8Array | string> {
21
+ /** Output binary or text (string for svg, Uint8Array for all others). */
22
+ data: T;
23
+ /** Result image width in pixels. */
24
+ width: number;
25
+ /** Result image height in pixels. */
26
+ height: number;
27
+ /** Output format. */
28
+ format: OutputFormat;
29
+ /** Original input image width (image inputs only). */
30
+ originalWidth?: number;
31
+ /** Original input image height (image inputs only). */
32
+ originalHeight?: number;
33
+ /** Original input image format (image inputs only). */
34
+ originalFormat?: string;
35
+ /** Whether the original input image is animated (image inputs only). */
36
+ originalAnimation?: boolean;
37
+ /** Whether the output is animated. */
38
+ animation?: boolean;
39
+ }
40
+ /**
41
+ * Render input (self-defined; no `satoru-render` dependency).
42
+ * `value` accepts HTML (`string` / `string[]`), a `data:image/` URL string,
43
+ * or raw image bytes (`Uint8Array` / `ArrayBuffer`).
44
+ * Extra keys are passed through to the WASM binding options object.
45
+ */
46
+ export interface RenderOptions {
47
+ value?: string | string[] | Uint8Array | ArrayBuffer;
48
+ url?: string;
49
+ baseUrl?: string;
50
+ width: number;
51
+ height?: number;
52
+ format?: OutputFormat;
53
+ }
54
+ /**
55
+ * Unified options for HTML and image inputs.
56
+ *
57
+ * - HTML input: `width`/`height` are the layout viewport; `crop`/`fit`
58
+ * are forwarded to the satoru render options.
59
+ * - Image input: render stage is skipped; `width`/`height`/`crop`/`fit`
60
+ * are applied as output resize on the encode stage.
61
+ */
62
+ export interface HtmlToImageOptions extends RenderOptions {
63
+ /** Encode quality 0-100 (encode stage only, default 85; ignored for svg/pdf). */
64
+ quality?: number;
65
+ /** Encode speed 0-10, mainly for AVIF (encode stage only, default 6). */
66
+ speed?: number;
67
+ /** Keep animation frames where supported (encode stage only). */
68
+ animation?: boolean;
69
+ /** Crop rectangle (encode stage for image input, render options for HTML). */
70
+ crop?: {
71
+ x: number;
72
+ y: number;
73
+ width: number;
74
+ height: number;
75
+ };
76
+ /** Resize strategy (encode stage for image input, render options for HTML). */
77
+ fit?: "contain" | "cover" | "fill";
78
+ /**
79
+ * Generic family -> font URL map (upstream `DEFAULT_FONT_MAP` strategy).
80
+ * Applied via `satoru_set_font_map` before resource discovery, so
81
+ * `@font-face`-less text (sans-serif/serif/...) resolves through Google
82
+ * Fonts instead of rendering blank. Defaults to `DEFAULT_FONT_MAP`.
83
+ */
84
+ fontMap?: Record<string, string>;
85
+ /** User-Agent for resource fetches (default: Chrome, for woff2 CSS). */
86
+ userAgent?: string;
87
+ /**
88
+ * User-supplied fallback font(s), applied to the instance before resource
89
+ * discovery (upstream `fallbackFonts` idiom). Entries may be raw bytes
90
+ * (`Uint8Array` / `ArrayBuffer`), a `data:` URL, or a fetchable URL
91
+ * (http(s) or baseUrl-relative file on Node).
92
+ */
93
+ fallbackFonts?: (Uint8Array | ArrayBuffer | string)[];
94
+ /**
95
+ * Log severity gate for `onLog` (satoru `LogLevel` parity; default
96
+ * `None` silences all JS-side log calls). Also forwarded to the WASM
97
+ * `satoru_set_log_level` binding when set.
98
+ */
99
+ logLevel?: LogLevel;
100
+ /** JS-side log hook, called at stage boundaries, warnings, and errors. */
101
+ onLog?: (level: LogLevel, message: string) => void;
102
+ /** Collect a full `RenderDiagnostics` report (delivered to `onDiagnostics`). */
103
+ diagnostics?: boolean;
104
+ /** Receives the diagnostics report when `diagnostics` is enabled. */
105
+ onDiagnostics?: (report: RenderDiagnostics) => void;
106
+ /**
107
+ * Override hook for resource resolution, applied to every fetch path
108
+ * (discovery fetches, fallback-font URLs, HTML `url` fetch). Return
109
+ * bytes to inject, or `null` to skip the resource.
110
+ */
111
+ resolveResource?: ResolveResourceHook;
112
+ /** Media type for CSS `@media` queries (default `"screen"`). */
113
+ mediaType?: "screen" | "print";
114
+ /** Render SVG text as paths (default `true`, satoru parity). */
115
+ textToPaths?: boolean;
116
+ /** Extra CSS pre-scanned on the instance before discovery. */
117
+ css?: string;
118
+ /** Named fonts pre-loaded on the instance before discovery. */
119
+ fonts?: {
120
+ name: string;
121
+ data: Uint8Array;
122
+ }[];
123
+ /** Safety/performance limits, enforced in JS around resource resolution. */
124
+ limits?: RenderLimits;
125
+ /** PDF Title metadata. */
126
+ pdfTitle?: string;
127
+ /** PDF Author metadata. */
128
+ pdfAuthor?: string;
129
+ /** PDF Subject metadata. */
130
+ pdfSubject?: string;
131
+ /** PDF Keywords metadata. */
132
+ pdfKeywords?: string;
133
+ /** PDF Creator metadata. */
134
+ pdfCreator?: string;
135
+ /** PDF Producer metadata. */
136
+ pdfProducer?: string;
137
+ /** PDF page margins in pixels. */
138
+ pdfMargin?: {
139
+ top?: number;
140
+ right?: number;
141
+ bottom?: number;
142
+ left?: number;
143
+ };
144
+ /** PDF header HTML template. */
145
+ pdfHeader?: string;
146
+ /** PDF footer HTML template. */
147
+ pdfFooter?: string;
148
+ /** Extra keys are forwarded to the WASM render options object. */
149
+ [key: string]: unknown;
150
+ }
151
+ /**
152
+ * Upstream default font strategy (verbatim from satoru `DEFAULT_FONT_MAP`):
153
+ * generic families resolve to Google Fonts css2 URLs (fetched with a browser
154
+ * UA so woff2 is served); fetched at render time, nothing bundled.
155
+ */
156
+ export declare const DEFAULT_FONT_MAP: Record<string, string>;
157
+ /** Binding names reported by {@link requireBindingError}. */
158
+ export type WasmBindingName = "satoru_render" | "converter_encode" | "converter_encode_svg" | "converter_encode_pdf" | "converter_load_image" | "html_to_image";
159
+ /**
160
+ * Render HTML β€” or convert an image β€” through the single unified WASM
161
+ * module. Image input skips the render stage and goes straight to
162
+ * `converter_encode`. Returns bytes (`Uint8Array`); only `svg` resolves
163
+ * to `string`.
164
+ */
165
+ export declare function htmlToImage(module: HtmlToImageModule, options: HtmlToImageOptions & {
166
+ format: "svg";
167
+ }): Promise<RenderResult<string>>;
168
+ export declare function htmlToImage(module: HtmlToImageModule, options: HtmlToImageOptions): Promise<RenderResult<Uint8Array | string>>;