satoru-render 0.0.19 → 0.0.21
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 +76 -10
- package/dist/core.d.ts +6 -1
- package/dist/core.js +117 -7
- package/dist/index.d.ts +18 -1
- package/dist/index.js +163 -0
- package/dist/satoru-single.js +0 -0
- package/dist/satoru-single.wasm +0 -0
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +216 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,9 +103,9 @@ const png = await render({
|
|
|
103
103
|
|
|
104
104
|
## 🛠️ Advanced Usage
|
|
105
105
|
|
|
106
|
-
### 1. Dynamic Resource Resolution
|
|
106
|
+
### 1. Dynamic Resource Resolution & Caching
|
|
107
107
|
|
|
108
|
-
Satoru can automatically fetch missing fonts, images, or external CSS via a `resolveResource` callback.
|
|
108
|
+
Satoru can automatically fetch missing fonts, images, or external CSS via a `resolveResource` callback. You can also implement high-performance caching using the browser's `CacheStorage` API.
|
|
109
109
|
|
|
110
110
|
```typescript
|
|
111
111
|
const pdf = await render({
|
|
@@ -114,17 +114,58 @@ const pdf = await render({
|
|
|
114
114
|
format: "pdf",
|
|
115
115
|
baseUrl: "https://example.com/assets/",
|
|
116
116
|
resolveResource: async (resource, defaultResolver) => {
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
// 1. Open Cache storage
|
|
118
|
+
const cache = await caches.open("satoru-resource-cache");
|
|
119
|
+
const cachedResponse = await cache.match(resource.url);
|
|
120
|
+
|
|
121
|
+
// 2. Return cached data if available
|
|
122
|
+
if (cachedResponse) {
|
|
123
|
+
const buf = await cachedResponse.arrayBuffer();
|
|
124
|
+
return new Uint8Array(buf);
|
|
120
125
|
}
|
|
121
|
-
|
|
126
|
+
|
|
127
|
+
// 3. Fetch using default resolver and save to cache
|
|
128
|
+
const data = await defaultResolver(resource);
|
|
129
|
+
if (data?.length) {
|
|
130
|
+
await cache.put(resource.url, new Response(data));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return data;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 2. Avoiding CORS Issues with Proxy
|
|
139
|
+
|
|
140
|
+
If you encounter CORS errors when fetching images or fonts from other domains, you can use a proxy service within the `resolveResource` callback.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const png = await render({
|
|
144
|
+
value: html,
|
|
145
|
+
width: 800,
|
|
146
|
+
format: "png",
|
|
147
|
+
resolveResource: async (resource, defaultResolver) => {
|
|
148
|
+
// Intercept external images to avoid CORS issues
|
|
149
|
+
if (resource.type === "image" && !resource.url.startsWith("data:")) {
|
|
150
|
+
const proxyUrl = `https://your-proxy-service.com/?url=${encodeURIComponent(resource.url)}`;
|
|
151
|
+
try {
|
|
152
|
+
const resp = await fetch(proxyUrl);
|
|
153
|
+
if (resp.ok) {
|
|
154
|
+
const buf = await resp.arrayBuffer();
|
|
155
|
+
return new Uint8Array(buf);
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn(`Failed to fetch via proxy: ${resource.url}`, e);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fallback to default resolver for other resources
|
|
122
163
|
return defaultResolver(resource);
|
|
123
164
|
},
|
|
124
165
|
});
|
|
125
166
|
```
|
|
126
167
|
|
|
127
|
-
###
|
|
168
|
+
### 3. Multi-page PDF Generation
|
|
128
169
|
|
|
129
170
|
Generate complex documents by passing an array of HTML strings. Each element in the array becomes a new page.
|
|
130
171
|
|
|
@@ -254,6 +295,31 @@ const pngBytes = await render({
|
|
|
254
295
|
});
|
|
255
296
|
```
|
|
256
297
|
|
|
298
|
+
### 7. DOM Capture (html2canvas alternative)
|
|
299
|
+
|
|
300
|
+
Satoru can capture live DOM elements directly in the browser, preserving computed styles, pseudo-elements (`::before`/`::after`), canvas contents, and form states.
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { Satoru } from "satoru-render";
|
|
304
|
+
import createSatoruModule from "satoru-render/satoru.js";
|
|
305
|
+
|
|
306
|
+
const satoru = await Satoru.create(createSatoruModule);
|
|
307
|
+
const element = document.getElementById("target-element");
|
|
308
|
+
|
|
309
|
+
// 1. Capture to HTMLCanvasElement (Direct html2canvas replacement)
|
|
310
|
+
const canvas = await satoru.capture(element, {
|
|
311
|
+
format: "png",
|
|
312
|
+
});
|
|
313
|
+
document.body.appendChild(canvas);
|
|
314
|
+
|
|
315
|
+
// 2. Or render directly to binary (Uint8Array)
|
|
316
|
+
const png = await satoru.render({
|
|
317
|
+
value: element, // Accepts HTMLElement directly
|
|
318
|
+
width: 800,
|
|
319
|
+
format: "png",
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
257
323
|
---
|
|
258
324
|
|
|
259
325
|
## 💻 CLI Tool
|
|
@@ -280,9 +346,9 @@ npx satoru-render input.html -f webp --verbose
|
|
|
280
346
|
|
|
281
347
|
### Render Options
|
|
282
348
|
|
|
283
|
-
| Option | Type
|
|
284
|
-
| :---------------- |
|
|
285
|
-
| `value` | `string \| string[]
|
|
349
|
+
| Option | Type | Description |
|
|
350
|
+
| :---------------- | :--------------------------------------- | :------------------------------------------------------ |
|
|
351
|
+
| `value` | `string \| string[] \| HTMLElement \| ...` | HTML string, array of strings, or DOM element(s). |
|
|
286
352
|
| `url` | `string` | URL to fetch HTML from. |
|
|
287
353
|
| `width` | `number` | **Required.** Output width in pixels. |
|
|
288
354
|
| `height` | `number` | Output height. Default: `0` (auto-calculate). |
|
package/dist/core.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface RequiredResource {
|
|
|
28
28
|
}
|
|
29
29
|
export type ResourceResolver = (resource: RequiredResource, defaultResolver: (resource: RequiredResource) => Promise<Uint8Array | null>) => Promise<Uint8Array | ArrayBufferView | null>;
|
|
30
30
|
export interface RenderOptions {
|
|
31
|
-
value?: string | string[];
|
|
31
|
+
value?: string | string[] | any | any[];
|
|
32
32
|
url?: string;
|
|
33
33
|
width: number;
|
|
34
34
|
height?: number;
|
|
@@ -82,4 +82,9 @@ export declare abstract class SatoruBase {
|
|
|
82
82
|
format?: "svg";
|
|
83
83
|
}): Promise<string>;
|
|
84
84
|
render(options: RenderOptions): Promise<string | Uint8Array>;
|
|
85
|
+
/**
|
|
86
|
+
* Combines multiple simple PDF files into one.
|
|
87
|
+
* This is a minimal implementation specialized for Skia-generated PDFs.
|
|
88
|
+
*/
|
|
89
|
+
private mergeSimplePDFs;
|
|
85
90
|
}
|
package/dist/core.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LogLevel } from "./log-level.js";
|
|
2
2
|
export const DEFAULT_FONT_MAP = {
|
|
3
|
-
"sans-serif": "https://
|
|
4
|
-
serif: "https://
|
|
5
|
-
monospace: "https://
|
|
6
|
-
cursive: "https://
|
|
7
|
-
fantasy: "https://
|
|
3
|
+
"sans-serif": "https://fonts.googleapis.com/css2?family=Noto+Sans+JP",
|
|
4
|
+
serif: "https://fonts.googleapis.com/css2?family=Noto+Serif+JP",
|
|
5
|
+
monospace: "https://fonts.googleapis.com/css2?family=M+PLUS+1+Code",
|
|
6
|
+
cursive: "https://fonts.googleapis.com/css2?family=Yuji+Syuku",
|
|
7
|
+
fantasy: "https://fonts.googleapis.com/css2?family=Reggae+One",
|
|
8
8
|
emoji: "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
|
|
9
9
|
"Noto Color Emoji": "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
|
|
10
10
|
};
|
|
@@ -161,8 +161,23 @@ export class SatoruBase {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
async render(options) {
|
|
164
|
+
let { format = "svg", value, url, baseUrl } = options;
|
|
165
|
+
if (format === "pdf" && Array.isArray(value) && value.length > 1) {
|
|
166
|
+
const pagePdfs = [];
|
|
167
|
+
const SatoruClass = this.constructor;
|
|
168
|
+
for (const html of value) {
|
|
169
|
+
const engine = await SatoruClass.create();
|
|
170
|
+
const pagePdf = await engine.render({
|
|
171
|
+
...options,
|
|
172
|
+
value: html,
|
|
173
|
+
format: "pdf",
|
|
174
|
+
});
|
|
175
|
+
pagePdfs.push(pagePdf);
|
|
176
|
+
}
|
|
177
|
+
return this.mergeSimplePDFs(pagePdfs);
|
|
178
|
+
}
|
|
164
179
|
const mod = await this.getModule();
|
|
165
|
-
|
|
180
|
+
const { width, height = 0, fonts, images, css, logLevel, onLog, } = options;
|
|
166
181
|
if (!options.userAgent) {
|
|
167
182
|
options.userAgent =
|
|
168
183
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
@@ -240,7 +255,9 @@ export class SatoruBase {
|
|
|
240
255
|
const uint8 = data instanceof Uint8Array
|
|
241
256
|
? data
|
|
242
257
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
243
|
-
if (r.type === "image" &&
|
|
258
|
+
if (r.type === "image" &&
|
|
259
|
+
typeof createImageBitmap !== "undefined" &&
|
|
260
|
+
typeof OffscreenCanvas !== "undefined") {
|
|
244
261
|
try {
|
|
245
262
|
const blob = new Blob([uint8.buffer]);
|
|
246
263
|
const bitmap = await createImageBitmap(blob);
|
|
@@ -308,4 +325,97 @@ export class SatoruBase {
|
|
|
308
325
|
this.currentFontMap = prevFontMap;
|
|
309
326
|
}
|
|
310
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Combines multiple simple PDF files into one.
|
|
330
|
+
* This is a minimal implementation specialized for Skia-generated PDFs.
|
|
331
|
+
*/
|
|
332
|
+
mergeSimplePDFs(pdfs) {
|
|
333
|
+
if (pdfs.length === 0)
|
|
334
|
+
return new Uint8Array();
|
|
335
|
+
if (pdfs.length === 1)
|
|
336
|
+
return pdfs[0];
|
|
337
|
+
const decoder = new TextDecoder();
|
|
338
|
+
const encoder = new TextEncoder();
|
|
339
|
+
let maxObjNum = 0;
|
|
340
|
+
const allObjects = [];
|
|
341
|
+
const pageIds = [];
|
|
342
|
+
for (const pdf of pdfs) {
|
|
343
|
+
const offset = maxObjNum;
|
|
344
|
+
const content = decoder.decode(pdf);
|
|
345
|
+
// Find all objects
|
|
346
|
+
const objRegex = /(\d+)\s+0\s+obj([\s\S]*?)endobj/g;
|
|
347
|
+
let match;
|
|
348
|
+
let currentPdfMax = 0;
|
|
349
|
+
while ((match = objRegex.exec(content)) !== null) {
|
|
350
|
+
const oldId = parseInt(match[1], 10);
|
|
351
|
+
const objBody = match[2];
|
|
352
|
+
const newId = oldId + offset;
|
|
353
|
+
if (oldId > currentPdfMax)
|
|
354
|
+
currentPdfMax = oldId;
|
|
355
|
+
// Shift references within the object body: "N 0 R" -> "(N+offset) 0 R"
|
|
356
|
+
const shiftedBody = objBody.replace(/(\d+)\s+0\s+R/g, (_, id) => {
|
|
357
|
+
return `${parseInt(id, 10) + offset} 0 R`;
|
|
358
|
+
});
|
|
359
|
+
// Detect if this is a Page object
|
|
360
|
+
if (shiftedBody.includes("/Type /Page") &&
|
|
361
|
+
!shiftedBody.includes("/Type /Pages")) {
|
|
362
|
+
pageIds.push(newId);
|
|
363
|
+
}
|
|
364
|
+
// We skip Catalog and original Pages root, we'll create new ones
|
|
365
|
+
if (!shiftedBody.includes("/Type /Catalog") &&
|
|
366
|
+
!shiftedBody.includes("/Type /Pages")) {
|
|
367
|
+
allObjects.push({
|
|
368
|
+
id: newId,
|
|
369
|
+
content: encoder.encode(`${newId} 0 obj${shiftedBody}endobj\n`),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
maxObjNum += currentPdfMax;
|
|
374
|
+
}
|
|
375
|
+
// Create new Catalog and Pages root
|
|
376
|
+
const catalogId = ++maxObjNum;
|
|
377
|
+
const pagesRootId = ++maxObjNum;
|
|
378
|
+
const newPagesRoot = encoder.encode(`${pagesRootId} 0 obj\n<< /Type /Pages /Kids [${pageIds
|
|
379
|
+
.map((id) => `${id} 0 R`)
|
|
380
|
+
.join(" ")}] /Count ${pageIds.length} >>\nendobj\n`);
|
|
381
|
+
const newCatalog = encoder.encode(`${catalogId} 0 obj\n<< /Type /Catalog /Pages ${pagesRootId} 0 R >>\nendobj\n`);
|
|
382
|
+
allObjects.push({ id: pagesRootId, content: newPagesRoot });
|
|
383
|
+
allObjects.push({ id: catalogId, content: newCatalog });
|
|
384
|
+
// Build the final PDF
|
|
385
|
+
const resultParts = [encoder.encode("%PDF-1.4\n")];
|
|
386
|
+
const xref = [];
|
|
387
|
+
let currentOffset = resultParts[0].length;
|
|
388
|
+
// Sort objects by ID for a clean xref table
|
|
389
|
+
allObjects.sort((a, b) => a.id - b.id);
|
|
390
|
+
for (const obj of allObjects) {
|
|
391
|
+
xref.push({ id: obj.id, offset: currentOffset });
|
|
392
|
+
resultParts.push(obj.content);
|
|
393
|
+
currentOffset += obj.content.length;
|
|
394
|
+
}
|
|
395
|
+
const startXref = currentOffset;
|
|
396
|
+
resultParts.push(encoder.encode("xref\n"));
|
|
397
|
+
resultParts.push(encoder.encode(`0 ${maxObjNum + 1}\n`));
|
|
398
|
+
resultParts.push(encoder.encode("0000000000 65535 f \n"));
|
|
399
|
+
const xrefMap = new Map(xref.map((x) => [x.id, x.offset]));
|
|
400
|
+
for (let i = 1; i <= maxObjNum; i++) {
|
|
401
|
+
const offset = xrefMap.get(i);
|
|
402
|
+
if (offset !== undefined) {
|
|
403
|
+
resultParts.push(encoder.encode(`${offset.toString().padStart(10, "0")} 00000 n \n`));
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
resultParts.push(encoder.encode("0000000000 00001 f \n"));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
resultParts.push(encoder.encode(`trailer\n<< /Size ${maxObjNum + 1} /Root ${catalogId} 0 R >>\n`));
|
|
410
|
+
resultParts.push(encoder.encode(`startxref\n${startXref}\n%%EOF`));
|
|
411
|
+
// Combine all parts
|
|
412
|
+
const totalLength = resultParts.reduce((acc, part) => acc + part.length, 0);
|
|
413
|
+
const merged = new Uint8Array(totalLength);
|
|
414
|
+
let pos = 0;
|
|
415
|
+
for (const part of resultParts) {
|
|
416
|
+
merged.set(part, pos);
|
|
417
|
+
pos += part.length;
|
|
418
|
+
}
|
|
419
|
+
return merged;
|
|
420
|
+
}
|
|
311
421
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
import { SatoruBase, RequiredResource } from "./core.js";
|
|
1
|
+
import { SatoruBase, RequiredResource, RenderOptions } from "./core.js";
|
|
2
2
|
export * from "./core.js";
|
|
3
3
|
export * from "./log-level.js";
|
|
4
4
|
export type { SatoruWorker } from "./child-workers.js";
|
|
5
5
|
export declare class Satoru extends SatoruBase {
|
|
6
6
|
static create(createSatoruModuleFunc: any): Promise<Satoru>;
|
|
7
|
+
/**
|
|
8
|
+
* Captures an HTMLElement and returns an HTMLCanvasElement.
|
|
9
|
+
* Similar to html2canvas.
|
|
10
|
+
*/
|
|
11
|
+
capture(element: HTMLElement, options?: Partial<Omit<RenderOptions, "value" | "url">>): Promise<HTMLCanvasElement>;
|
|
12
|
+
private serializeElement;
|
|
13
|
+
private pngToCanvas;
|
|
14
|
+
/**
|
|
15
|
+
* Overrides render to support HTMLElement as value.
|
|
16
|
+
*/
|
|
17
|
+
render(options: RenderOptions & {
|
|
18
|
+
format: "png" | "webp" | "pdf";
|
|
19
|
+
}): Promise<Uint8Array>;
|
|
20
|
+
render(options: RenderOptions & {
|
|
21
|
+
format?: "svg";
|
|
22
|
+
}): Promise<string>;
|
|
23
|
+
render(options: RenderOptions): Promise<string | Uint8Array>;
|
|
7
24
|
static defaultResourceResolver(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
|
|
8
25
|
protected resolveDefaultResource(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | null>;
|
|
9
26
|
protected fetchHtml(url: string, userAgent?: string): Promise<string>;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,169 @@ export class Satoru extends SatoruBase {
|
|
|
5
5
|
static async create(createSatoruModuleFunc) {
|
|
6
6
|
return new Satoru(createSatoruModuleFunc);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Captures an HTMLElement and returns an HTMLCanvasElement.
|
|
10
|
+
* Similar to html2canvas.
|
|
11
|
+
*/
|
|
12
|
+
async capture(element, options = {}) {
|
|
13
|
+
const rect = element.getBoundingClientRect();
|
|
14
|
+
const width = options.width ?? Math.ceil(rect.width);
|
|
15
|
+
const height = options.height ?? Math.ceil(rect.height);
|
|
16
|
+
// 1. Serialize DOM with computed styles
|
|
17
|
+
const serializedHtml = this.serializeElement(element);
|
|
18
|
+
// 2. Render to PNG
|
|
19
|
+
const pngData = await this.render({
|
|
20
|
+
...options,
|
|
21
|
+
value: serializedHtml,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
format: "png",
|
|
25
|
+
});
|
|
26
|
+
// 3. Convert PNG to Canvas
|
|
27
|
+
return this.pngToCanvas(pngData, width, height);
|
|
28
|
+
}
|
|
29
|
+
serializeElement(element) {
|
|
30
|
+
const clone = element.cloneNode(true);
|
|
31
|
+
const absoluteUrl = (url) => {
|
|
32
|
+
if (!url || url.startsWith("data:") || url.startsWith("blob:"))
|
|
33
|
+
return url;
|
|
34
|
+
const a = document.createElement("a");
|
|
35
|
+
a.href = url;
|
|
36
|
+
return a.href;
|
|
37
|
+
};
|
|
38
|
+
const applyStyles = (src, dest) => {
|
|
39
|
+
const computed = window.getComputedStyle(src);
|
|
40
|
+
const styleArr = [];
|
|
41
|
+
for (let i = 0; i < computed.length; i++) {
|
|
42
|
+
const prop = computed[i];
|
|
43
|
+
let value = computed.getPropertyValue(prop);
|
|
44
|
+
// Convert relative URLs in styles (e.g., background-image)
|
|
45
|
+
if (value.includes("url(")) {
|
|
46
|
+
value = value.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, url) => {
|
|
47
|
+
return `url("${absoluteUrl(url)}")`;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
styleArr.push(`${prop}:${value}`);
|
|
51
|
+
}
|
|
52
|
+
dest.setAttribute("style", styleArr.join(";"));
|
|
53
|
+
// Handle pseudo-elements
|
|
54
|
+
const handlePseudo = (pseudoType) => {
|
|
55
|
+
const style = window.getComputedStyle(src, pseudoType);
|
|
56
|
+
const content = style.getPropertyValue("content");
|
|
57
|
+
if (!content || content === "none" || content === "normal")
|
|
58
|
+
return;
|
|
59
|
+
const pseudoEl = document.createElement("span");
|
|
60
|
+
const pseudoStyles = [];
|
|
61
|
+
for (let i = 0; i < style.length; i++) {
|
|
62
|
+
const prop = style[i];
|
|
63
|
+
pseudoStyles.push(`${prop}:${style.getPropertyValue(prop)}`);
|
|
64
|
+
}
|
|
65
|
+
pseudoEl.setAttribute("style", pseudoStyles.join(";"));
|
|
66
|
+
// Remove quotes from content for the innerText if it's just a string
|
|
67
|
+
if (content.startsWith('"') && content.endsWith('"')) {
|
|
68
|
+
pseudoEl.innerText = content.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
if (pseudoType === "::before") {
|
|
71
|
+
dest.prepend(pseudoEl);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
dest.append(pseudoEl);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
handlePseudo("::before");
|
|
78
|
+
handlePseudo("::after");
|
|
79
|
+
// Special handling for specific elements
|
|
80
|
+
if (typeof HTMLCanvasElement !== "undefined" && src instanceof HTMLCanvasElement) {
|
|
81
|
+
const img = document.createElement("img");
|
|
82
|
+
img.src = src.toDataURL();
|
|
83
|
+
img.setAttribute("style", dest.getAttribute("style") || "");
|
|
84
|
+
dest.replaceWith(img);
|
|
85
|
+
}
|
|
86
|
+
else if (typeof HTMLImageElement !== "undefined" && src instanceof HTMLImageElement) {
|
|
87
|
+
dest.setAttribute("src", absoluteUrl(src.src));
|
|
88
|
+
}
|
|
89
|
+
else if (typeof HTMLInputElement !== "undefined" && src instanceof HTMLInputElement) {
|
|
90
|
+
if (src.type === "checkbox" || src.type === "radio") {
|
|
91
|
+
if (src.checked)
|
|
92
|
+
dest.setAttribute("checked", "");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
dest.setAttribute("value", src.value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (typeof HTMLTextAreaElement !== "undefined" && src instanceof HTMLTextAreaElement) {
|
|
99
|
+
dest.innerText = src.value;
|
|
100
|
+
}
|
|
101
|
+
else if (typeof HTMLSelectElement !== "undefined" && src instanceof HTMLSelectElement) {
|
|
102
|
+
// Find the selected option and mark it
|
|
103
|
+
const index = src.selectedIndex;
|
|
104
|
+
const clonedSelect = dest;
|
|
105
|
+
if (clonedSelect.options[index]) {
|
|
106
|
+
clonedSelect.options[index].setAttribute("selected", "");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (let i = 0; i < src.children.length; i++) {
|
|
110
|
+
const srcChild = src.children[i];
|
|
111
|
+
const destChild = dest.children[i];
|
|
112
|
+
if (srcChild && destChild) {
|
|
113
|
+
applyStyles(srcChild, destChild);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
applyStyles(element, clone);
|
|
118
|
+
return `<!DOCTYPE html><html><body style="margin:0;padding:0;overflow:hidden;background:transparent;">${clone.outerHTML}</body></html>`;
|
|
119
|
+
}
|
|
120
|
+
async pngToCanvas(data, width, height) {
|
|
121
|
+
const blob = new Blob([data], { type: "image/png" });
|
|
122
|
+
const url = URL.createObjectURL(blob);
|
|
123
|
+
try {
|
|
124
|
+
const img = new Image();
|
|
125
|
+
img.crossOrigin = "anonymous";
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
img.onload = resolve;
|
|
128
|
+
img.onerror = reject;
|
|
129
|
+
img.src = url;
|
|
130
|
+
});
|
|
131
|
+
const canvas = document.createElement("canvas");
|
|
132
|
+
canvas.width = width;
|
|
133
|
+
canvas.height = height;
|
|
134
|
+
const ctx = canvas.getContext("2d");
|
|
135
|
+
if (ctx) {
|
|
136
|
+
ctx.drawImage(img, 0, 0);
|
|
137
|
+
}
|
|
138
|
+
return canvas;
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
URL.revokeObjectURL(url);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async render(options) {
|
|
145
|
+
if (options.value) {
|
|
146
|
+
const values = Array.isArray(options.value)
|
|
147
|
+
? options.value
|
|
148
|
+
: [options.value];
|
|
149
|
+
const processedValues = [];
|
|
150
|
+
let hasElement = false;
|
|
151
|
+
for (const val of values) {
|
|
152
|
+
if (typeof HTMLElement !== "undefined" && val instanceof HTMLElement) {
|
|
153
|
+
processedValues.push(this.serializeElement(val));
|
|
154
|
+
hasElement = true;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
processedValues.push(val);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (hasElement) {
|
|
161
|
+
options = {
|
|
162
|
+
...options,
|
|
163
|
+
value: Array.isArray(options.value)
|
|
164
|
+
? processedValues
|
|
165
|
+
: processedValues[0],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return super.render(options);
|
|
170
|
+
}
|
|
8
171
|
static async defaultResourceResolver(resource, baseUrl, userAgent) {
|
|
9
172
|
try {
|
|
10
173
|
if (resource.url.startsWith("provider:google-fonts")) {
|
package/dist/satoru-single.js
CHANGED
|
Binary file
|
|
Binary file
|
package/dist/satoru.wasm
CHANGED
|
Binary file
|