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.
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/browser-workers.d.ts +12 -0
- package/dist/browser-workers.js +26 -0
- package/dist/child-workers.d.ts +11 -0
- package/dist/child-workers.js +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +87 -0
- package/dist/core.d.ts +168 -0
- package/dist/core.js +824 -0
- package/dist/diagnostics.d.ts +93 -0
- package/dist/diagnostics.js +28 -0
- package/dist/edge-light.d.ts +18 -0
- package/dist/edge-light.js +70 -0
- package/dist/encode-args.d.ts +10 -0
- package/dist/encode-args.js +30 -0
- package/dist/html-to-image-single.js +0 -0
- package/dist/html-to-image-wasm.js +3 -0
- package/dist/html-to-image.js +2 -0
- package/dist/html-to-image.wasm +0 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/input.d.ts +23 -0
- package/dist/input.js +90 -0
- package/dist/loader.d.ts +132 -0
- package/dist/loader.js +199 -0
- package/dist/preact.d.ts +10 -0
- package/dist/preact.js +13 -0
- package/dist/react.d.ts +9 -0
- package/dist/react.js +11 -0
- package/dist/single.d.ts +27 -0
- package/dist/single.js +22 -0
- package/dist/tailwind.d.ts +1 -0
- package/dist/tailwind.js +6 -0
- package/dist/wasm-payload.d.ts +2 -0
- package/dist/wasm-payload.js +39 -0
- package/dist/web-workers.js +4411 -0
- package/dist/worker-actions.d.ts +10 -0
- package/dist/worker-actions.js +13 -0
- package/dist/worker-lib-loader.d.ts +19 -0
- package/dist/worker-lib-loader.js +34 -0
- package/dist/workerd.d.ts +18 -0
- package/dist/workerd.js +65 -0
- package/dist/workers-dummy.d.ts +12 -0
- package/dist/workers-dummy.js +6 -0
- package/dist/workers-parent.js +602 -0
- package/dist/workers.d.ts +86 -0
- package/dist/workers.js +163 -0
- 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
|
+
[](https://www.npmjs.com/package/wasm-html-to-image)
|
|
4
|
+
[](https://www.npmjs.com/package/wasm-html-to-image)
|
|
5
|
+
[](https://github.com/SoraKumo001/wasm-html-to-image/blob/master/LICENSE)
|
|
6
|
+
[](https://sorakumo001.github.io/wasm-html-to-image/master)
|
|
7
|
+
[](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
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>>;
|