satoru-render 0.0.1

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 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,201 @@
1
+ # Satoru Wasm: High-Performance HTML to SVG/PNG/PDF Engine
2
+
3
+ [![Playground](https://img.shields.io/badge/Demo-Playground-blueviolet)](https://sorakumo001.github.io/satoru/)
4
+
5
+ **Satoru** is a portable, WebAssembly-powered HTML rendering engine. It combines the **Skia Graphics Engine** and **litehtml** to provide high-quality, pixel-perfect SVG, PNG, and PDF generation entirely within WebAssembly.
6
+
7
+ ## 🚀 Project Status: High-Fidelity Rendering & Edge Ready
8
+
9
+ The engine supports full text layout with custom fonts, complex CSS styling, and efficient binary data transfer. It is now compatible with **Cloudflare Workers (workerd)**, allowing for serverless, edge-side image and document generation.
10
+
11
+ ### Key Capabilities
12
+
13
+ - **Pure Wasm Pipeline**: Performs all layout and drawing operations inside Wasm. Zero dependencies on browser DOM or `<canvas>`.
14
+ - **Edge Native**: Specialized wrapper for Cloudflare Workers ensures smooth execution in restricted environments.
15
+ - **Triple Output Modes**:
16
+ - **SVG**: Generates lean, vector-based Pure SVG strings with post-processed effects (Filters, Gradients).
17
+ - **PNG**: Generates high-quality raster images via Skia, transferred as binary data for maximum performance.
18
+ - **PDF**: Generates high-fidelity vector documents via Skia's PDF backend, including native support for text, gradients, images, and **multi-page output**.
19
+ - **High-Level TS Wrapper**: Includes a `Satoru` class that abstracts Wasm memory management and provides a clean async API.
20
+ - **Dynamic Font Loading**: Supports loading `.ttf` / `.woff2` / `.ttc` files at runtime with automatic weight/style inference.
21
+ - **Japanese Support**: Full support for Japanese rendering with multi-font fallback logic.
22
+ - **Image Format Support**: Native support for **PNG**, **JPEG**, **WebP**, **AVIF**, **GIF**, **BMP**, and **ICO** image formats.
23
+ - **Advanced CSS Support**:
24
+ - **Box Model**: Margin, padding, border, and accurate **Border Radius**.
25
+ - **Box Shadow**: High-quality **Outer** and **Inset** shadows using advanced SVG filters (SVG) or Skia blurs (PNG/PDF).
26
+ - **Gradients**: Linear, **Elliptical Radial**, and **Conic** (Sweep) gradient support.
27
+ - **Standard Tags**: Full support for `<b>`, `<strong>`, `<i>`, `<u>`, and `<h1>`-`<h6>` via integrated master CSS.
28
+ - **Text Decoration**: Supports `underline`, `line-through`, `overline` with `solid`, `dotted`, and `dashed` styles.
29
+ - **Text Shadow**: Multiple shadows with blur, offset, and color support (PNG/SVG/PDF).
30
+
31
+ ## 🛠️ Usage (TypeScript)
32
+
33
+ ### Standard Environment (Node.js / Browser)
34
+
35
+ Satoru provides a high-level `render` function for converting HTML to various formats. It automatically handles WASM instantiation and resource resolution.
36
+
37
+ #### Basic Rendering (Automatic Resource Resolution)
38
+
39
+ The `render` function supports automated multi-pass resource resolution. It identifies missing fonts, images, and external CSS and requests them via the `resolveResource` callback.
40
+
41
+ ```typescript
42
+ import { render, LogLevel } from "satoru-render";
43
+
44
+ const html = `
45
+ <style>
46
+ @font-face {
47
+ font-family: 'Roboto';
48
+ src: url('https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxK.woff2');
49
+ }
50
+ </style>
51
+ <div style="font-family: 'Roboto'; color: #2196F3; font-size: 40px;">
52
+ Hello Satoru!
53
+ <img src="logo.png" style="width: 50px;">
54
+ </div>
55
+ `;
56
+
57
+ // Render to PDF with automatic resource resolution from HTML string
58
+ const pdf = await render({
59
+ value: html,
60
+ width: 600,
61
+ format: "pdf",
62
+ baseUrl: "https://example.com/assets/", // Optional: resolve relative URLs
63
+ logLevel: LogLevel.Info,
64
+ resolveResource: async (resource, defaultResolver) => {
65
+ if (resource.url.startsWith("custom://")) {
66
+ return myCustomData;
67
+ }
68
+ return defaultResolver(resource);
69
+ },
70
+ });
71
+
72
+ // Render from a URL directly
73
+ const png = await render({
74
+ url: "https://example.com/page.html",
75
+ width: 1024,
76
+ format: "png",
77
+ });
78
+ ```
79
+
80
+ #### Render Options
81
+
82
+ | Option | Type | Description |
83
+ | ----------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
84
+ | `value` | `string \| string[]` | HTML string or array of HTML strings. (One of `value` or `url` is required) |
85
+ | `url` | `string` | URL to fetch HTML from. (One of `value` or `url` is required) |
86
+ | `width` | `number` | **Required.** Width of the output in pixels. |
87
+ | `height` | `number` | Height of the output in pixels. Default is `0` (automatic height). |
88
+ | `format` | `"svg" \| "png" \| "webp" \| "pdf"` | Output format. Default is `"svg"`. |
89
+ | `textToPaths` | `boolean` | Whether to convert SVG text to paths. Default is `true`. |
90
+ | `resolveResource` | `ResourceResolver` | Async callback to fetch missing fonts, images, or CSS. Returns `Uint8Array`, `ArrayBufferView` or `null`. It receives a second argument `defaultResolver` to fallback to the standard resolution logic. |
91
+ | `fonts` | `Object[]` | Array of `{ name, data: Uint8Array }` to pre-load fonts. |
92
+ | `images` | `Object[]` | Array of `{ name, url, width?, height? }` to pre-load images. |
93
+ | `css` | `string` | Extra CSS to inject into the rendering process. |
94
+ | `baseUrl` | `string` | Base URL used to resolve relative URLs in fonts, images, and links. |
95
+ | `userAgent` | `string` | User-Agent header for fetching resources (Node.js environment). |
96
+ | `logLevel` | `LogLevel` | Logging verbosity (`None`, `Error`, `Warning`, `Info`, `Debug`). |
97
+ | `onLog` | `(level, msg) => void` | Custom callback for receiving log messages. |
98
+
99
+ ### 💻 CLI Usage
100
+
101
+ Satoru includes a command-line interface for easy conversion.
102
+
103
+ ```bash
104
+ # Convert a local HTML file to PNG
105
+ npx satoru-render input.html -o output.png
106
+
107
+ # Convert a URL to PDF
108
+ npx satoru-render https://example.com -o example.pdf -w 1280
109
+
110
+ # Convert with custom options
111
+ npx satoru-render input.html -w 1024 -f webp --verbose
112
+ ```
113
+
114
+ #### CLI Options
115
+
116
+ - `<input>`: Input file path or URL (**Required**)
117
+ - `-o, --output <path>`: Output file path
118
+ - `-w, --width <number>`: Viewport width (default: 800)
119
+ - `-h, --height <number>`: Viewport height (default: 0, auto-calculate)
120
+ - `-f, --format <format>`: Output format: `svg`, `png`, `webp`, `pdf`
121
+ - `--verbose`: Enable detailed logging
122
+ - `--help`: Show help message
123
+
124
+ ### 📄 Multi-page PDF Generation
125
+
126
+ You can generate a multi-page PDF by passing an array of HTML strings to the `value` property. Each string in the array will be rendered as a new page.
127
+
128
+ ```typescript
129
+ const pdf = await satoru.render({
130
+ value: [
131
+ "<h1>Page 1</h1><p>First page content.</p>",
132
+ "<h1>Page 2</h1><p>Second page content.</p>",
133
+ "<h1>Page 3</h1><p>Third page content.</p>",
134
+ ],
135
+ width: 600,
136
+ format: "pdf",
137
+ });
138
+ ```
139
+
140
+ ### ☁️ Cloudflare Workers (Edge)
141
+
142
+ Satoru is optimized for Cloudflare Workers. Use the `workerd` specific export which handles the specific WASM instantiation requirements of the environment.
143
+
144
+ ```typescript
145
+ import { render } from "satoru-render";
146
+
147
+ export default {
148
+ async fetch(request) {
149
+ const pdf = await render({
150
+ value: "<h1>Edge Rendered</h1>",
151
+ width: 800,
152
+ format: "pdf",
153
+ baseUrl: "https://example.com/",
154
+ });
155
+
156
+ return new Response(pdf, {
157
+ headers: { "Content-Type": "application/pdf" },
158
+ });
159
+ },
160
+ };
161
+ ```
162
+
163
+ ### 📦 Single-file (Embedded WASM)
164
+
165
+ For environments where deploying a separate `.wasm` file is difficult, use the `single` export which includes the WASM binary embedded.
166
+
167
+ ```typescript
168
+ import { render } from "satoru-render";
169
+
170
+ const png = await render({
171
+ value: "<div>Embedded WASM!</div>",
172
+ width: 600,
173
+ format: "png",
174
+ });
175
+ ```
176
+
177
+ ### 🧵 Multi-threaded Rendering (Worker Proxy)
178
+
179
+ For high-throughput applications, the Worker proxy distributes rendering tasks across multiple threads. You can configure all resources in a single `render` call for stateless operation.
180
+
181
+ ```typescript
182
+ import { createSatoruWorker, LogLevel } from "satoru-render/workers";
183
+
184
+ // Create a worker proxy with up to 4 parallel instances
185
+ const satoru = createSatoruWorker({ maxParallel: 4 });
186
+
187
+ // Render with full configuration in one go
188
+ const png = await satoru.render({
189
+ value: "<h1>Parallel Rendering</h1><img src='icon.png'>",
190
+ width: 800,
191
+ format: "png",
192
+ baseUrl: "https://example.com/assets/",
193
+ logLevel: LogLevel.Debug, // Enable debug logs for this task
194
+ fonts: [{ name: "CustomFont", data: fontData }], // Pre-load fonts
195
+ css: "h1 { color: red; }", // Inject extra CSS
196
+ });
197
+ ```
198
+
199
+ ## 📜 License
200
+
201
+ MIT License
@@ -0,0 +1,6 @@
1
+ import { type RenderOptions } from "./single.js";
2
+ declare const map: {
3
+ render(options: RenderOptions): Promise<string | Uint8Array<ArrayBufferLike>>;
4
+ };
5
+ export type SatoruWorker = typeof map;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ import { initWorker } from "worker-lib";
2
+ import { Satoru } from "./single.js";
3
+ let satoru;
4
+ const getSatoru = async () => {
5
+ if (!satoru) {
6
+ satoru = await Satoru.create();
7
+ }
8
+ return satoru;
9
+ };
10
+ /**
11
+ * Worker-side implementation for Satoru.
12
+ * Exposes Satoru methods via worker-lib.
13
+ */
14
+ const actions = {
15
+ async render(options) {
16
+ const s = await getSatoru();
17
+ return await s.render(options);
18
+ },
19
+ };
20
+ const map = initWorker(actions);
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,110 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { Satoru, LogLevel } from "./single.js";
5
+ async function main() {
6
+ const args = process.argv.slice(2);
7
+ const options = {
8
+ width: 800,
9
+ format: "png",
10
+ };
11
+ let input;
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg === "-w" || arg === "--width") {
15
+ options.width = parseInt(args[++i]);
16
+ }
17
+ else if (arg === "-h" || arg === "--height") {
18
+ options.height = parseInt(args[++i]);
19
+ }
20
+ else if (arg === "-f" || arg === "--format") {
21
+ options.format = args[++i];
22
+ }
23
+ else if (arg === "-o" || arg === "--output") {
24
+ options.output = args[++i];
25
+ }
26
+ else if (arg === "--verbose") {
27
+ options.verbose = true;
28
+ }
29
+ else if (arg === "--help") {
30
+ printHelp();
31
+ return;
32
+ }
33
+ else if (!arg.startsWith("-")) {
34
+ input = arg;
35
+ }
36
+ }
37
+ if (!input) {
38
+ console.error("Error: Input file or URL is required.");
39
+ printHelp();
40
+ process.exit(1);
41
+ }
42
+ const isUrl = /^[a-z][a-z0-9+.-]*:/i.test(input) && !input.startsWith("data:");
43
+ if (!options.output) {
44
+ const ext = `.${options.format}`;
45
+ if (isUrl) {
46
+ options.output = "output" + ext;
47
+ }
48
+ else {
49
+ const basename = path.basename(input, path.extname(input));
50
+ options.output = basename + ext;
51
+ }
52
+ }
53
+ // Deduce format from output extension if not explicitly set
54
+ if (!args.includes("-f") && !args.includes("--format")) {
55
+ const ext = path.extname(options.output).toLowerCase().slice(1);
56
+ if (["svg", "png", "webp", "pdf"].includes(ext)) {
57
+ options.format = ext;
58
+ }
59
+ }
60
+ const satoru = await Satoru.create();
61
+ const renderOptions = {
62
+ width: options.width,
63
+ height: options.height,
64
+ format: options.format,
65
+ logLevel: options.verbose ? LogLevel.Debug : LogLevel.None,
66
+ css: "body { background-color: white; }",
67
+ };
68
+ if (options.verbose) {
69
+ renderOptions.onLog = (level, message) => {
70
+ console.error(`[Satoru] ${LogLevel[level]}: ${message}`);
71
+ };
72
+ }
73
+ if (isUrl) {
74
+ renderOptions.url = input;
75
+ }
76
+ else {
77
+ if (!fs.existsSync(input)) {
78
+ console.error(`Error: File not found: ${input}`);
79
+ process.exit(1);
80
+ }
81
+ renderOptions.value = fs.readFileSync(input, "utf-8");
82
+ renderOptions.baseUrl = path.dirname(path.resolve(input));
83
+ }
84
+ try {
85
+ const result = await satoru.render(renderOptions);
86
+ fs.writeFileSync(options.output, result);
87
+ console.log(`Successfully rendered to ${options.output}`);
88
+ }
89
+ catch (err) {
90
+ console.error("Error during rendering:", err);
91
+ process.exit(1);
92
+ }
93
+ }
94
+ function printHelp() {
95
+ console.log(`
96
+ Usage: satoru <input-file-or-url> [options]
97
+
98
+ Options:
99
+ -o, --output <path> Output file path
100
+ -w, --width <number> Viewport width (default: 800)
101
+ -h, --height <number> Viewport height (default: 0, auto-calculate)
102
+ -f, --format <format> Output format: svg, png, webp, pdf
103
+ --verbose Enable detailed logging
104
+ --help Show this help message
105
+ `);
106
+ }
107
+ main().catch((err) => {
108
+ console.error("Fatal error:", err);
109
+ process.exit(1);
110
+ });
package/dist/core.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { LogLevel } from "./log-level.js";
2
+ export interface SatoruModule {
3
+ create_instance: () => any;
4
+ destroy_instance: (inst: any) => void;
5
+ collect_resources: (inst: any, html: string, width: number) => void;
6
+ get_pending_resources: (inst: any) => string;
7
+ add_resource: (inst: any, url: string, type: number, data: Uint8Array) => void;
8
+ scan_css: (inst: any, css: string) => void;
9
+ load_font: (inst: any, name: string, data: Uint8Array) => void;
10
+ load_image: (inst: any, name: string, url: string, width: number, height: number) => void;
11
+ set_font_map: (inst: any, fontMap: Record<string, string>) => void;
12
+ set_log_level: (level: number) => void;
13
+ init_document: (inst: any, html: string, width: number) => void;
14
+ layout_document: (inst: any, width: number) => void;
15
+ render_from_state: (inst: any, width: number, height: number, format: number, svgTextToPaths: boolean) => Uint8Array | null;
16
+ render: (inst: any, htmls: string | string[], width: number, height: number, format: number, svgTextToPaths: boolean) => Uint8Array | null;
17
+ onLog?: (level: LogLevel, message: string) => void;
18
+ logLevel: LogLevel;
19
+ }
20
+ export interface RequiredResource {
21
+ type: "font" | "css" | "image";
22
+ url: string;
23
+ name: string;
24
+ redraw_on_ready?: boolean;
25
+ }
26
+ export type ResourceResolver = (resource: RequiredResource, defaultResolver: (resource: RequiredResource) => Promise<Uint8Array | null>) => Promise<Uint8Array | ArrayBufferView | null>;
27
+ export interface RenderOptions {
28
+ value?: string | string[];
29
+ url?: string;
30
+ width: number;
31
+ height?: number;
32
+ format?: "svg" | "png" | "webp" | "pdf";
33
+ textToPaths?: boolean;
34
+ resolveResource?: ResourceResolver;
35
+ fonts?: {
36
+ name: string;
37
+ data: Uint8Array;
38
+ }[];
39
+ images?: {
40
+ name: string;
41
+ url: string;
42
+ width?: number;
43
+ height?: number;
44
+ }[];
45
+ css?: string;
46
+ baseUrl?: string;
47
+ userAgent?: string;
48
+ fontMap?: Record<string, string>;
49
+ logLevel?: LogLevel;
50
+ onLog?: (level: LogLevel, message: string) => void;
51
+ }
52
+ export declare const DEFAULT_FONT_MAP: Record<string, string>;
53
+ export declare function resolveGoogleFonts(resource: RequiredResource, userAgent?: string): Promise<Uint8Array | null>;
54
+ export declare abstract class SatoruBase {
55
+ private factory;
56
+ private modPromise?;
57
+ protected currentFontMap: Record<string, string>;
58
+ protected constructor(factory: any);
59
+ private getModule;
60
+ initDocument(options: Omit<RenderOptions, "value"> & {
61
+ html: string;
62
+ }): Promise<any>;
63
+ layoutDocument(inst: any, width: number): Promise<void>;
64
+ renderFromState(inst: any, options: {
65
+ width: number;
66
+ height?: number;
67
+ format?: "svg" | "png" | "webp" | "pdf";
68
+ textToPaths?: boolean;
69
+ }): Promise<string | Uint8Array>;
70
+ destroyInstance(inst: any): Promise<void>;
71
+ protected abstract resolveDefaultResource(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
72
+ protected abstract fetchHtml(url: string, userAgent?: string): Promise<string>;
73
+ render(options: RenderOptions & {
74
+ format: "png" | "webp" | "pdf";
75
+ }): Promise<Uint8Array>;
76
+ render(options: RenderOptions & {
77
+ format?: "svg";
78
+ }): Promise<string>;
79
+ render(options: RenderOptions): Promise<string | Uint8Array>;
80
+ }