satoru-render 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,201 +1,180 @@
1
- # Satoru Wasm: High-Performance HTML to SVG/PNG/PDF Engine
1
+ # Satoru Render: High-Performance HTML to Image/PDF Engine
2
2
 
3
3
  [![Playground](https://img.shields.io/badge/Demo-Playground-blueviolet)](https://sorakumo001.github.io/satoru/)
4
+ [![npm license](https://img.shields.io/npm/l/satoru-render.svg)](https://www.npmjs.com/package/satoru-render)
5
+ [![npm version](https://img.shields.io/npm/v/satoru-render.svg)](https://www.npmjs.com/package/satoru-render)
6
+ [![npm download](https://img.shields.io/npm/dw/satoru-render.svg)](https://www.npmjs.com/package/satoru-render)
7
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/SoraKumo001/satoru)
4
8
 
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.
9
+ **Satoru Render** is a high-fidelity HTML-to-Image/PDF conversion engine built with WebAssembly. It provides a lightweight, dependency-free solution for generating high-quality visuals and documents across **Node.js**, **Cloudflare Workers**, **Deno**, and **Web Browsers**.
6
10
 
7
- ## 🚀 Project Status: High-Fidelity Rendering & Edge Ready
11
+ By combining the **Skia** graphics engine with a custom **litehtml** layout core, Satoru performs all layout and drawing operations entirely within WASM, eliminating the need for headless browsers or system-level dependencies.
8
12
 
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.
13
+ ![Sample Output](./document/sample01.webp)
10
14
 
11
- ### Key Capabilities
15
+ ---
12
16
 
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).
17
+ ## Key Features
30
18
 
31
- ## 🛠️ Usage (TypeScript)
19
+ - **Pure WebAssembly Engine**: 100% independent of browser DOM or `<canvas>`. Runs anywhere WASM is supported.
20
+ - **Edge Native**: Specifically optimized for **Cloudflare Workers (workerd)** and other serverless environments.
21
+ - **Professional Graphics**:
22
+ - **SVG**: Lean, vector-based strings with post-processed filters and gradients.
23
+ - **PNG / WebP**: High-performance raster images with advanced Skia rendering.
24
+ - **PDF**: Multi-page vector documents with native text and gradient support.
25
+ - **Advanced CSS Capabilities**:
26
+ - **Box Model**: Precise margin, padding, borders, and **Border Radius**.
27
+ - **Shadows**: High-quality **Outer** and **Inset** shadows (using SVG filters or Skia blurs).
28
+ - **Gradients**: Linear, **Elliptical Radial**, and **Conic** (Sweep) support.
29
+ - **Text Styling**: Multi-shadows, decorations (solid/dotted/dashed), and automatic weight/style inference.
30
+ - **Internationalization**: Robust support for complex text layouts, including **Japanese** and multi-font fallback logic.
31
+ - **Smart Resource Management**: Dynamic loading of `.ttf`, `.woff2`, and `.ttc` fonts, plus native support for all major image formats (AVIF, WebP, PNG, JPEG, etc.).
32
32
 
33
- ### Standard Environment (Node.js / Browser)
33
+ ---
34
34
 
35
- Satoru provides a high-level `render` function for converting HTML to various formats. It automatically handles WASM instantiation and resource resolution.
35
+ ## 📦 Installation
36
36
 
37
- #### Basic Rendering (Automatic Resource Resolution)
37
+ ```bash
38
+ npm install satoru-render
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🚀 Quick Start
38
44
 
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.
45
+ ### Basic Usage (TypeScript)
46
+
47
+ The `render` function is the primary entry point. It handles WASM instantiation, resource resolution, and conversion in a single call.
40
48
 
41
49
  ```typescript
42
- import { render, LogLevel } from "satoru-render";
50
+ import { render } from "satoru-render";
43
51
 
44
52
  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;">
53
+ <div style="padding: 40px; background: #f8f9fa; border-radius: 12px; border: 2px solid #dee2e6;">
54
+ <h1 style="color: #007bff; font-family: sans-serif;">Hello Satoru!</h1>
55
+ <p style="color: #495057;">This document was rendered entirely in WebAssembly.</p>
54
56
  </div>
55
57
  `;
56
58
 
57
- // Render to PDF with automatic resource resolution from HTML string
58
- const pdf = await render({
59
+ // Render to PNG
60
+ const png = await render({
59
61
  value: html,
60
62
  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
63
  format: "png",
77
64
  });
78
65
  ```
79
66
 
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.
67
+ ---
102
68
 
103
- ```bash
104
- # Convert a local HTML file to PNG
105
- npx satoru-render input.html -o output.png
69
+ ## 🛠️ Advanced Usage
106
70
 
107
- # Convert a URL to PDF
108
- npx satoru-render https://example.com -o example.pdf -w 1280
71
+ ### 1. Dynamic Resource Resolution
109
72
 
110
- # Convert with custom options
111
- npx satoru-render input.html -w 1024 -f webp --verbose
112
- ```
113
-
114
- #### CLI Options
73
+ Satoru can automatically fetch missing fonts, images, or external CSS via a `resolveResource` callback.
115
74
 
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
75
+ ```typescript
76
+ const pdf = await render({
77
+ value: html,
78
+ width: 800,
79
+ format: "pdf",
80
+ baseUrl: "https://example.com/assets/",
81
+ resolveResource: async (resource, defaultResolver) => {
82
+ // Custom intercept logic
83
+ if (resource.url.startsWith("my-app://")) {
84
+ return myAssetBuffer;
85
+ }
86
+ // Fallback to default fetch/filesystem resolver
87
+ return defaultResolver(resource);
88
+ },
89
+ });
90
+ ```
123
91
 
124
- ### 📄 Multi-page PDF Generation
92
+ ### 2. Multi-page PDF Generation
125
93
 
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.
94
+ Generate complex documents by passing an array of HTML strings. Each element in the array becomes a new page.
127
95
 
128
96
  ```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,
97
+ const pdf = await render({
98
+ value: ["<h1>Page One</h1>", "<h1>Page Two</h1>", "<h1>Page Three</h1>"],
99
+ width: 595, // A4 width in points
136
100
  format: "pdf",
137
101
  });
138
102
  ```
139
103
 
140
- ### ☁️ Cloudflare Workers (Edge)
104
+ ### 3. Edge/Cloudflare Workers
141
105
 
142
- Satoru is optimized for Cloudflare Workers. Use the `workerd` specific export which handles the specific WASM instantiation requirements of the environment.
106
+ Use the specialized `workerd` export for serverless environments.
143
107
 
144
108
  ```typescript
145
109
  import { render } from "satoru-render";
146
110
 
147
111
  export default {
148
112
  async fetch(request) {
149
- const pdf = await render({
150
- value: "<h1>Edge Rendered</h1>",
113
+ const png = await render({
114
+ value: "<h1>Edge Generated Image</h1>",
151
115
  width: 800,
152
- format: "pdf",
153
- baseUrl: "https://example.com/",
116
+ format: "png",
154
117
  });
155
118
 
156
- return new Response(pdf, {
157
- headers: { "Content-Type": "application/pdf" },
158
- });
119
+ return new Response(png, { headers: { "Content-Type": "image/png" } });
159
120
  },
160
121
  };
161
122
  ```
162
123
 
163
- ### 📦 Single-file (Embedded WASM)
124
+ ### 6. Multi-threaded Rendering (Worker Proxy)
164
125
 
165
- For environments where deploying a separate `.wasm` file is difficult, use the `single` export which includes the WASM binary embedded.
126
+ Distribute rendering tasks across multiple background workers for high-throughput applications.
166
127
 
167
128
  ```typescript
168
- import { render } from "satoru-render";
129
+ import { createSatoruWorker } from "satoru-render/workers";
169
130
 
170
- const png = await render({
171
- value: "<div>Embedded WASM!</div>",
172
- width: 600,
131
+ const satoru = createSatoruWorker({ maxParallel: 4 });
132
+
133
+ const png = await satoru.render({
134
+ value: "<h1>Parallel Task</h1>",
135
+ width: 800,
173
136
  format: "png",
174
137
  });
175
138
  ```
176
139
 
177
- ### 🧵 Multi-threaded Rendering (Worker Proxy)
140
+ ---
178
141
 
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.
142
+ ## 💻 CLI Tool
180
143
 
181
- ```typescript
182
- import { createSatoruWorker, LogLevel } from "satoru-render/workers";
144
+ Convert files or URLs directly from your terminal.
183
145
 
184
- // Create a worker proxy with up to 4 parallel instances
185
- const satoru = createSatoruWorker({ maxParallel: 4 });
146
+ ```bash
147
+ # Local HTML to PNG
148
+ npx satoru-render input.html -o output.png
186
149
 
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
- });
150
+ # URL to PDF with specific width
151
+ npx satoru-render https://example.com -o site.pdf -w 1280
152
+
153
+ # WebP conversion with verbose logs
154
+ npx satoru-render input.html -f webp --verbose
197
155
  ```
198
156
 
157
+ ---
158
+
159
+ ## 📖 API Reference
160
+
161
+ ### Render Options
162
+
163
+ | Option | Type | Description |
164
+ | :---------------- | :---------------------------------- | :------------------------------------------------------ |
165
+ | `value` | `string \| string[]` | HTML string or array of strings (for multi-page PDF). |
166
+ | `url` | `string` | URL to fetch HTML from. |
167
+ | `width` | `number` | **Required.** Output width in pixels. |
168
+ | `height` | `number` | Output height. Default: `0` (auto-calculate). |
169
+ | `format` | `"svg" \| "png" \| "webp" \| "pdf"` | Output format. Default: `"svg"`. |
170
+ | `resolveResource` | `ResourceResolver` | Async callback to fetch assets (fonts, images, CSS). |
171
+ | `fonts` | `Object[]` | Pre-load fonts: `[{ name, data: Uint8Array }]`. |
172
+ | `css` | `string` | Extra CSS to inject into the document. |
173
+ | `baseUrl` | `string` | Base URL for relative path resolution. |
174
+ | `logLevel` | `LogLevel` | Verbosity: `None`, `Error`, `Warning`, `Info`, `Debug`. |
175
+
176
+ ---
177
+
199
178
  ## 📜 License
200
179
 
201
- MIT License
180
+ This project is licensed under the **MIT License**.
package/dist/core.d.ts CHANGED
@@ -2,7 +2,7 @@ import { LogLevel } from "./log-level.js";
2
2
  export interface SatoruModule {
3
3
  create_instance: () => any;
4
4
  destroy_instance: (inst: any) => void;
5
- collect_resources: (inst: any, html: string, width: number) => void;
5
+ collect_resources: (inst: any, html: string, width: number, height: number) => void;
6
6
  get_pending_resources: (inst: any) => string;
7
7
  add_resource: (inst: any, url: string, type: number, data: Uint8Array) => void;
8
8
  scan_css: (inst: any, css: string) => void;
@@ -10,7 +10,7 @@ export interface SatoruModule {
10
10
  load_image: (inst: any, name: string, url: string, width: number, height: number) => void;
11
11
  set_font_map: (inst: any, fontMap: Record<string, string>) => void;
12
12
  set_log_level: (level: number) => void;
13
- init_document: (inst: any, html: string, width: number) => void;
13
+ init_document: (inst: any, html: string, width: number, height: number) => void;
14
14
  layout_document: (inst: any, width: number) => void;
15
15
  render_from_state: (inst: any, width: number, height: number, format: number, svgTextToPaths: boolean) => Uint8Array | null;
16
16
  render: (inst: any, htmls: string | string[], width: number, height: number, format: number, svgTextToPaths: boolean) => Uint8Array | null;
@@ -21,6 +21,7 @@ export interface RequiredResource {
21
21
  type: "font" | "css" | "image";
22
22
  url: string;
23
23
  name: string;
24
+ characters?: string;
24
25
  redraw_on_ready?: boolean;
25
26
  }
26
27
  export type ResourceResolver = (resource: RequiredResource, defaultResolver: (resource: RequiredResource) => Promise<Uint8Array | null>) => Promise<Uint8Array | ArrayBufferView | null>;
package/dist/core.js CHANGED
@@ -15,6 +15,7 @@ export async function resolveGoogleFonts(resource, userAgent) {
15
15
  return null;
16
16
  const weight = urlObj.searchParams.get("weight") || "400";
17
17
  const italic = urlObj.searchParams.get("italic") === "1";
18
+ const text = urlObj.searchParams.get("text") || resource.characters;
18
19
  let targetFamily = family;
19
20
  let forceNormalStyle = false;
20
21
  if (targetFamily.includes("Noto Sans JP") ||
@@ -23,7 +24,14 @@ export async function resolveGoogleFonts(resource, userAgent) {
23
24
  forceNormalStyle = true;
24
25
  }
25
26
  const useItalic = italic && !forceNormalStyle;
26
- const googleFontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(targetFamily)}:ital,wght@${useItalic ? "1" : "0"},${weight}&display=swap`;
27
+ let googleFontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(targetFamily)}:ital,wght@${useItalic ? "1" : "0"},${weight}&display=swap`;
28
+ if (text) {
29
+ // Google Fonts text parameter has a limit (around 1000 chars is usually safe for URL length)
30
+ // If it's too long, we fall back to full character set to avoid URL too long errors
31
+ if (text.length < 800) {
32
+ googleFontUrl += `&text=${encodeURIComponent(text)}`;
33
+ }
34
+ }
27
35
  const headers = {};
28
36
  if (userAgent) {
29
37
  headers["User-Agent"] = userAgent;
@@ -109,7 +117,7 @@ export class SatoruBase {
109
117
  async initDocument(options) {
110
118
  const mod = await this.getModule();
111
119
  const inst = mod.create_instance();
112
- mod.init_document(inst, options.html, options.width);
120
+ mod.init_document(inst, options.html, options.width, options.height ?? 0);
113
121
  return inst;
114
122
  }
115
123
  async layoutDocument(inst, width) {
@@ -187,16 +195,19 @@ export class SatoruBase {
187
195
  : defaultResolver;
188
196
  const inputHtmls = Array.isArray(value) ? value : [value];
189
197
  const processedHtmls = [];
190
- const resolvedUrls = new Set();
198
+ const resolvedResources = new Set();
191
199
  for (const rawHtml of inputHtmls) {
192
200
  let processedHtml = rawHtml;
193
201
  for (let i = 0; i < 10; i++) {
194
- mod.collect_resources(instancePtr, processedHtml, width);
202
+ mod.collect_resources(instancePtr, processedHtml, width, height);
195
203
  const json = mod.get_pending_resources(instancePtr);
196
204
  if (!json)
197
205
  break;
198
206
  const resources = JSON.parse(json);
199
- const pending = resources.filter((r) => !resolvedUrls.has(r.url));
207
+ const pending = resources.filter((r) => {
208
+ const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
209
+ return !resolvedResources.has(key);
210
+ });
200
211
  if (pending.length === 0)
201
212
  break;
202
213
  await Promise.all(pending.map(async (r) => {
@@ -204,7 +215,8 @@ export class SatoruBase {
204
215
  if (r.url.startsWith("data:")) {
205
216
  return;
206
217
  }
207
- resolvedUrls.add(r.url);
218
+ const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
219
+ resolvedResources.add(key);
208
220
  const data = await resolver({ ...r });
209
221
  if (data &&
210
222
  (data instanceof Uint8Array || ArrayBuffer.isView(data))) {
@@ -224,6 +236,13 @@ export class SatoruBase {
224
236
  }
225
237
  }));
226
238
  }
239
+ const resolvedUrls = new Set();
240
+ resolvedResources.forEach((key) => {
241
+ const parts = key.split(":");
242
+ if (parts.length >= 2) {
243
+ resolvedUrls.add(parts.slice(1, -1).join(":"));
244
+ }
245
+ });
227
246
  resolvedUrls.forEach((url) => {
228
247
  const escapedUrl = url.replace(/[.*+?^${}()|[\]]/g, "\\$&");
229
248
  const linkRegex = new RegExp(`<link[^>]*href\\s*=\\s*(["'])${escapedUrl}\\1[^>]*>`, "gi");
Binary file