satoru-render 0.0.22 → 0.0.23

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.
@@ -0,0 +1,9 @@
1
+ export type { SatoruWorker } from "./child-workers.js";
2
+ import { type RenderOptions } from "./index.js";
3
+ export type { RenderOptions };
4
+ export declare const render: (_params: RenderOptions) => Promise<string | Uint8Array<ArrayBufferLike>>;
5
+ export declare const setLimit: (_limit: number) => void;
6
+ export declare const close: () => void;
7
+ export declare const waitAll: () => Promise<void>;
8
+ export declare const waitReady: (_retryTime?: number) => Promise<void>;
9
+ export declare const launchWorker: () => Promise<void>;
@@ -0,0 +1,8 @@
1
+ export const render = (_params) => {
2
+ return undefined;
3
+ };
4
+ export const setLimit = (_limit) => { };
5
+ export const close = () => { };
6
+ export const waitAll = () => Promise.resolve();
7
+ export const waitReady = (_retryTime) => Promise.resolve();
8
+ export const launchWorker = () => Promise.resolve();
@@ -0,0 +1,682 @@
1
+ //#region ../../node_modules/.pnpm/worker-lib@2.2.0/node_modules/worker-lib/dist/esm/common.js
2
+ const FUNCTION_PLACEHOLDER = "__worker_lib_function__";
3
+ const isPlaceholder = (v) => v && typeof v === "object" && "__worker_lib_function__" in v;
4
+ const isPlainObject = (v) => {
5
+ return v && typeof v === "object" && Object.prototype.toString.call(v) === "[object Object]" && !(v instanceof Uint8Array) && !(v instanceof ArrayBuffer) && !ArrayBuffer.isView(v);
6
+ };
7
+ const getTransferables = (v, result = []) => {
8
+ if (v instanceof ArrayBuffer) result.push(v);
9
+ else if (ArrayBuffer.isView(v)) result.push(v.buffer);
10
+ else if (Array.isArray(v)) for (const item of v) getTransferables(item, result);
11
+ else if (v && typeof v === "object") for (const key in v) getTransferables(v[key], result);
12
+ return result;
13
+ };
14
+ const createRegistry = () => {
15
+ const callbacks = /* @__PURE__ */ new Map();
16
+ const callbackProxies = /* @__PURE__ */ new Map();
17
+ const registerCallback = (requestId, fn) => {
18
+ const id = `${requestId}:${Math.random().toString(36).slice(2)}`;
19
+ callbacks.set(id, fn);
20
+ return id;
21
+ };
22
+ const clearCallbacks = (requestId) => {
23
+ for (const key of callbacks.keys()) if (key.startsWith(`${requestId}:`)) callbacks.delete(key);
24
+ for (const key of callbackProxies.keys()) if (key.startsWith(`${requestId}:`)) callbackProxies.delete(key);
25
+ };
26
+ const transformArgs = (requestId, args) => {
27
+ if (typeof args === "function") return { [FUNCTION_PLACEHOLDER]: registerCallback(requestId, args) };
28
+ if (ArrayBuffer.isView(args) || args instanceof ArrayBuffer) return args;
29
+ if (Array.isArray(args)) return args.map((v) => transformArgs(requestId, v));
30
+ if (isPlainObject(args)) {
31
+ const result = {};
32
+ for (const key in args) result[key] = transformArgs(requestId, args[key]);
33
+ return result;
34
+ }
35
+ return args;
36
+ };
37
+ const resolveArgs = (requestId, args, createProxy) => {
38
+ if (isPlaceholder(args)) return createProxy(args[FUNCTION_PLACEHOLDER]);
39
+ if (Array.isArray(args)) return args.map((v) => resolveArgs(requestId, v, createProxy));
40
+ if (isPlainObject(args)) {
41
+ const result = {};
42
+ for (const key in args) result[key] = resolveArgs(requestId, args[key], createProxy);
43
+ return result;
44
+ }
45
+ return args;
46
+ };
47
+ return {
48
+ callbacks,
49
+ callbackProxies,
50
+ registerCallback,
51
+ clearCallbacks,
52
+ transformArgs,
53
+ resolveArgs
54
+ };
55
+ };
56
+ //#endregion
57
+ //#region ../../node_modules/.pnpm/worker-lib@2.2.0/node_modules/worker-lib/dist/esm/pool.js
58
+ let requestIdCounter = 0;
59
+ const exec = (worker, name, registry, ...args) => {
60
+ const requestId = requestIdCounter++;
61
+ const { callbacks, callbackProxies, clearCallbacks, transformArgs, resolveArgs } = registry;
62
+ return new Promise((resolve, reject) => {
63
+ const createProxy = (callbackId) => {
64
+ const key = `${requestId}:${callbackId}`;
65
+ if (callbackProxies.has(key)) return callbackProxies.get(key);
66
+ const proxy = (...proxyArgs) => {
67
+ const callId = Math.random().toString(36).slice(2);
68
+ return new Promise((res) => {
69
+ const handler = (data) => {
70
+ if (data.type === "callback_result" && data.payload.id === callId) {
71
+ worker.removeEventListener("message", handler);
72
+ res(resolveArgs(requestId, data.payload.result, createProxy));
73
+ }
74
+ };
75
+ worker.addEventListener("message", handler);
76
+ const transformedProxyArgs = transformArgs(requestId, proxyArgs);
77
+ worker.postMessage({
78
+ type: "callback_call",
79
+ payload: {
80
+ id: requestId,
81
+ callbackId,
82
+ args: transformedProxyArgs,
83
+ callId
84
+ }
85
+ }, getTransferables(transformedProxyArgs));
86
+ });
87
+ };
88
+ callbackProxies.set(key, proxy);
89
+ return proxy;
90
+ };
91
+ const messageHandler = async (data) => {
92
+ if (!data || typeof data !== "object") return;
93
+ const payload = data.payload;
94
+ if (payload?.id !== requestId) return;
95
+ if (data.type === "result") {
96
+ worker.removeEventListener("message", messageHandler);
97
+ const result = resolveArgs(requestId, payload.result, createProxy);
98
+ clearCallbacks(requestId);
99
+ resolve(result);
100
+ } else if (data.type === "error") {
101
+ worker.removeEventListener("message", messageHandler);
102
+ clearCallbacks(requestId);
103
+ reject(payload.error);
104
+ } else if (data.type === "callback_call") {
105
+ const { callbackId, args: callArgs, callId } = payload;
106
+ const fn = callbacks.get(callbackId);
107
+ if (fn) try {
108
+ const transformedResult = transformArgs(requestId, await fn(...resolveArgs(requestId, callArgs, createProxy)));
109
+ worker.postMessage({
110
+ type: "callback_result",
111
+ payload: {
112
+ id: callId,
113
+ result: transformedResult
114
+ }
115
+ }, getTransferables(transformedResult));
116
+ } catch (e) {
117
+ console.error("[worker-lib] Callback execution failed:", e);
118
+ }
119
+ }
120
+ };
121
+ worker.addEventListener("message", messageHandler);
122
+ const transformedArgs = transformArgs(requestId, args);
123
+ worker.postMessage({
124
+ type: "function",
125
+ payload: {
126
+ id: requestId,
127
+ name,
128
+ args: transformedArgs
129
+ }
130
+ }, getTransferables(transformedArgs));
131
+ });
132
+ };
133
+ const createWorkerPool = (builder, limit = 4) => {
134
+ let workers = Array(limit).fill(void 0).map(() => ({}));
135
+ const emptyWaits = [];
136
+ let isEmptyWait = false;
137
+ const registry = createRegistry();
138
+ const getResolver = async () => {
139
+ while (true) {
140
+ const target = workers.find(({ resultResolver }) => !resultResolver);
141
+ if (target) {
142
+ target.resultResolver = Promise.withResolvers();
143
+ if (!target.worker) target.worker = await builder();
144
+ return target;
145
+ }
146
+ await Promise.race(workers.map(({ resultResolver }) => resultResolver?.promise));
147
+ }
148
+ };
149
+ const execute = async (name, ...value) => {
150
+ const target = await getResolver();
151
+ const { resultResolver } = target;
152
+ if (!resultResolver) throw new Error("Unexpected error");
153
+ exec(target.worker, name, registry, ...value).then(resultResolver.resolve).catch(resultResolver.reject).finally(() => {
154
+ target.resultResolver = void 0;
155
+ });
156
+ return resultResolver.promise;
157
+ };
158
+ const launchWorker = async () => {
159
+ return Promise.all(workers.map(async (target) => {
160
+ if (!target.worker) target.worker = await builder();
161
+ }));
162
+ };
163
+ const waitAll = async () => {
164
+ while (workers.find(({ resultResolver }) => resultResolver)) await Promise.all(workers.flatMap(({ resultResolver }) => resultResolver ? [resultResolver.promise] : []));
165
+ };
166
+ const waitReady = async (retryTime = 1) => {
167
+ const p = Promise.withResolvers();
168
+ emptyWaits.push(p);
169
+ (async () => {
170
+ if (!isEmptyWait) {
171
+ isEmptyWait = true;
172
+ do {
173
+ const actives = workers.flatMap(({ resultResolver }) => resultResolver ? [resultResolver.promise] : []);
174
+ if (workers.length === actives.length) await Promise.race(actives);
175
+ emptyWaits.shift()?.resolve();
176
+ if (retryTime) await new Promise((r) => setTimeout(r, retryTime));
177
+ else await Promise.resolve();
178
+ } while (emptyWaits.length);
179
+ isEmptyWait = false;
180
+ }
181
+ })();
182
+ return p.promise;
183
+ };
184
+ const close = () => {
185
+ for (const target of workers) {
186
+ target.worker?.terminate();
187
+ target.worker = void 0;
188
+ target.resultResolver = void 0;
189
+ }
190
+ };
191
+ const setLimit = (newLimit) => {
192
+ workers.forEach((w) => w.worker?.terminate());
193
+ workers = Array(newLimit).fill(void 0).map(() => ({}));
194
+ };
195
+ return {
196
+ execute,
197
+ waitAll,
198
+ waitReady,
199
+ close,
200
+ setLimit,
201
+ launchWorker
202
+ };
203
+ };
204
+ //#endregion
205
+ //#region ../../node_modules/.pnpm/worker-lib@2.2.0/node_modules/worker-lib/dist/esm/index.js
206
+ const w = Worker;
207
+ const wrapWorker = (worker) => ({
208
+ postMessage: (message, transfer) => worker.postMessage(message, transfer),
209
+ addEventListener: (type, listener) => {
210
+ const handler = (e) => {
211
+ if (e instanceof MessageEvent) listener(e.data);
212
+ };
213
+ listener._handler = handler;
214
+ worker.addEventListener(type, handler);
215
+ },
216
+ removeEventListener: (type, listener) => {
217
+ const handler = listener._handler;
218
+ worker.removeEventListener(type, handler);
219
+ },
220
+ terminate: () => worker.terminate()
221
+ });
222
+ const init = (worker) => {
223
+ return new Promise((resolve) => {
224
+ const handler = (e) => {
225
+ if (e.data === "ready") {
226
+ worker.removeEventListener("message", handler);
227
+ resolve(worker);
228
+ }
229
+ };
230
+ worker.addEventListener("message", handler);
231
+ });
232
+ };
233
+ const createWorker = (builder, limit = 4) => {
234
+ return createWorkerPool(async () => {
235
+ const result = builder();
236
+ const worker = result instanceof Worker ? result : new Worker(result);
237
+ await init(worker);
238
+ return wrapWorker(worker);
239
+ }, limit);
240
+ };
241
+ //#endregion
242
+ //#region src/log-level.ts
243
+ let LogLevel = /* @__PURE__ */ function(LogLevel) {
244
+ LogLevel[LogLevel["None"] = 0] = "None";
245
+ LogLevel[LogLevel["Error"] = 1] = "Error";
246
+ LogLevel[LogLevel["Warning"] = 2] = "Warning";
247
+ LogLevel[LogLevel["Info"] = 3] = "Info";
248
+ LogLevel[LogLevel["Debug"] = 4] = "Debug";
249
+ return LogLevel;
250
+ }({});
251
+ //#endregion
252
+ //#region src/core.ts
253
+ const DEFAULT_FONT_MAP = {
254
+ "sans-serif": "https://fonts.googleapis.com/css2?family=Noto+Sans+JP",
255
+ serif: "https://fonts.googleapis.com/css2?family=Noto+Serif+JP",
256
+ monospace: "https://fonts.googleapis.com/css2?family=M+PLUS+1+Code",
257
+ cursive: "https://fonts.googleapis.com/css2?family=Yuji+Syuku",
258
+ fantasy: "https://fonts.googleapis.com/css2?family=Reggae+One",
259
+ emoji: "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
260
+ "Noto Color Emoji": "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2"
261
+ };
262
+ async function resolveGoogleFonts(resource, userAgent) {
263
+ if (!resource.url.startsWith("provider:google-fonts")) return null;
264
+ const urlObj = new URL(resource.url);
265
+ const family = urlObj.searchParams.get("family");
266
+ if (!family) return null;
267
+ const weight = urlObj.searchParams.get("weight") || "400";
268
+ const italic = urlObj.searchParams.get("italic") === "1";
269
+ const text = urlObj.searchParams.get("text") || resource.characters;
270
+ let targetFamily = family;
271
+ let forceNormalStyle = false;
272
+ if (targetFamily.includes("Noto Sans JP") || targetFamily.includes("Noto Serif JP") || targetFamily.includes("CJK")) forceNormalStyle = true;
273
+ const useItalic = italic && !forceNormalStyle;
274
+ let googleFontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(targetFamily)}:ital,wght@${useItalic ? "1" : "0"},${weight}&display=swap`;
275
+ if (text) {
276
+ if (text.length < 800) googleFontUrl += `&text=${encodeURIComponent(text)}`;
277
+ }
278
+ const headers = {};
279
+ if (userAgent) headers["User-Agent"] = userAgent;
280
+ try {
281
+ const resp = await fetch(googleFontUrl, { headers });
282
+ if (!resp.ok) return null;
283
+ const buf = await resp.arrayBuffer();
284
+ return new Uint8Array(buf);
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ var SatoruBase = class {
290
+ factory;
291
+ modPromise;
292
+ currentFontMap = DEFAULT_FONT_MAP;
293
+ constructor(factory) {
294
+ this.factory = factory;
295
+ }
296
+ async getModule() {
297
+ if (!this.modPromise) this.modPromise = (async () => {
298
+ let currentLogLevel = LogLevel.None;
299
+ let currentUserOnLog;
300
+ const mod = await this.factory({
301
+ onLog: (level, message) => {
302
+ if (currentLogLevel !== LogLevel.None && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
303
+ },
304
+ print: (text) => {
305
+ if (currentLogLevel !== LogLevel.None && LogLevel.Info <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Info, text);
306
+ },
307
+ printErr: (text) => {
308
+ if (currentLogLevel !== LogLevel.None && LogLevel.Error <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Error, text);
309
+ }
310
+ });
311
+ mod.logLevel = LogLevel.None;
312
+ const originalSetLogLevel = mod.set_log_level;
313
+ mod.set_log_level = (level) => {
314
+ currentLogLevel = level;
315
+ originalSetLogLevel(level);
316
+ };
317
+ let internalOnLog = mod.onLog;
318
+ Object.defineProperty(mod, "onLog", {
319
+ get: () => internalOnLog,
320
+ set: (val) => {
321
+ internalOnLog = val;
322
+ currentUserOnLog = val;
323
+ }
324
+ });
325
+ let internalLogLevel = mod.logLevel;
326
+ Object.defineProperty(mod, "logLevel", {
327
+ get: () => internalLogLevel,
328
+ set: (val) => {
329
+ internalLogLevel = val;
330
+ currentLogLevel = val;
331
+ }
332
+ });
333
+ return mod;
334
+ })();
335
+ return this.modPromise;
336
+ }
337
+ async initDocument(options) {
338
+ const mod = await this.getModule();
339
+ const inst = mod.create_instance();
340
+ mod.init_document(inst, options.html, options.width, options.height ?? 0);
341
+ return inst;
342
+ }
343
+ async layoutDocument(inst, width) {
344
+ (await this.getModule()).layout_document(inst, width);
345
+ }
346
+ async renderFromState(inst, options) {
347
+ const mod = await this.getModule();
348
+ const format = {
349
+ svg: 0,
350
+ png: 1,
351
+ webp: 2,
352
+ pdf: 3
353
+ }[options.format ?? "svg"] ?? 0;
354
+ const result = mod.render_from_state(inst, options.width, options.height ?? 0, format, options.textToPaths ?? true);
355
+ if (!result) {
356
+ if (options.format === "svg") return "";
357
+ return new Uint8Array();
358
+ }
359
+ if (options.format === "svg") return new TextDecoder().decode(result);
360
+ return new Uint8Array(result);
361
+ }
362
+ async destroyInstance(inst) {
363
+ (await this.getModule()).destroy_instance(inst);
364
+ }
365
+ async loadFallbackFont(data) {
366
+ const mod = await this.getModule();
367
+ const inst = mod.create_instance();
368
+ try {
369
+ mod.load_fallback_font(inst, data);
370
+ } finally {
371
+ mod.destroy_instance(inst);
372
+ }
373
+ }
374
+ async render(options) {
375
+ let { format = "svg", value, url, baseUrl } = options;
376
+ if (format === "pdf" && Array.isArray(value) && value.length > 1) {
377
+ const pagePdfs = [];
378
+ const SatoruClass = this.constructor;
379
+ for (const html of value) {
380
+ const pagePdf = await (await SatoruClass.create()).render({
381
+ ...options,
382
+ value: html,
383
+ format: "pdf"
384
+ });
385
+ pagePdfs.push(pagePdf);
386
+ }
387
+ const mod = await this.getModule();
388
+ const instancePtr = mod.create_instance();
389
+ try {
390
+ const result = mod.merge_pdfs(instancePtr, pagePdfs);
391
+ if (!result) return new Uint8Array();
392
+ return new Uint8Array(result.slice());
393
+ } finally {
394
+ mod.destroy_instance(instancePtr);
395
+ }
396
+ }
397
+ const mod = await this.getModule();
398
+ const { width, height = 0, fonts, images, css, logLevel, onLog } = options;
399
+ if (!options.userAgent) options.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
400
+ if (url && !value) {
401
+ if (!baseUrl) baseUrl = url;
402
+ value = await this.fetchHtml(url, options.userAgent);
403
+ }
404
+ if (!value) throw new Error("Either 'value' or 'url' must be provided.");
405
+ const prevLogLevel = mod.logLevel;
406
+ const prevOnLog = mod.onLog;
407
+ const prevFontMap = this.currentFontMap;
408
+ mod.logLevel = logLevel ?? LogLevel.None;
409
+ mod.set_log_level(mod.logLevel);
410
+ mod.onLog = onLog;
411
+ this.currentFontMap = options.fontMap ?? DEFAULT_FONT_MAP;
412
+ const instancePtr = mod.create_instance();
413
+ mod.set_font_map(instancePtr, this.currentFontMap);
414
+ try {
415
+ if (fonts) for (const f of fonts) mod.load_font(instancePtr, f.name, f.data);
416
+ if (options.fallbackFonts) for (const data of options.fallbackFonts) mod.load_fallback_font(instancePtr, data);
417
+ if (images) for (const img of images) mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
418
+ if (css) mod.scan_css(instancePtr, css);
419
+ const defaultResolver = (r) => this.resolveDefaultResource(r, baseUrl, options.userAgent);
420
+ const resolver = options.resolveResource ? async (r) => {
421
+ return await options.resolveResource(r, defaultResolver);
422
+ } : defaultResolver;
423
+ const inputHtmls = Array.isArray(value) ? value : [value];
424
+ const processedHtmls = [];
425
+ const resolvedResources = /* @__PURE__ */ new Set();
426
+ for (const rawHtml of inputHtmls) {
427
+ let processedHtml = rawHtml;
428
+ for (let i = 0; i < 10; i++) {
429
+ mod.collect_resources(instancePtr, processedHtml, width, height);
430
+ const json = mod.get_pending_resources(instancePtr);
431
+ if (!json) break;
432
+ const pending = JSON.parse(json).filter((r) => {
433
+ const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
434
+ return !resolvedResources.has(key);
435
+ });
436
+ if (pending.length === 0) break;
437
+ await Promise.all(pending.map(async (r) => {
438
+ try {
439
+ if (r.url.startsWith("data:")) return;
440
+ const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
441
+ resolvedResources.add(key);
442
+ const data = await resolver({ ...r });
443
+ if (data && (data instanceof Uint8Array || ArrayBuffer.isView(data))) {
444
+ const uint8 = data instanceof Uint8Array ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
445
+ if (r.type === "image" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") try {
446
+ const blob = new Blob([uint8.buffer]);
447
+ const bitmap = await createImageBitmap(blob);
448
+ const ctx = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
449
+ if (ctx) {
450
+ ctx.drawImage(bitmap, 0, 0);
451
+ const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
452
+ mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
453
+ return;
454
+ }
455
+ } catch (e) {}
456
+ let typeInt = 1;
457
+ if (r.type === "image") typeInt = 2;
458
+ if (r.type === "css") typeInt = 3;
459
+ mod.add_resource(instancePtr, r.url, typeInt, uint8);
460
+ }
461
+ } catch (e) {
462
+ console.warn(`Failed to resolve resource: ${r.url}`, e);
463
+ }
464
+ }));
465
+ }
466
+ const resolvedUrls = /* @__PURE__ */ new Set();
467
+ resolvedResources.forEach((key) => {
468
+ const parts = key.split(":");
469
+ if (parts.length >= 2) resolvedUrls.add(parts.slice(1, -1).join(":"));
470
+ });
471
+ resolvedUrls.forEach((url) => {
472
+ const escapedUrl = url.replace(/[.*+?^${}()|[\]]/g, "\\$&");
473
+ const linkRegex = new RegExp(`<link[^>]*href\\s*=\\s*(["']?)${escapedUrl}\\1[^>]*>`, "gi");
474
+ processedHtml = processedHtml.replace(linkRegex, "");
475
+ });
476
+ processedHtmls.push(processedHtml);
477
+ }
478
+ const result = mod.render(instancePtr, processedHtmls, width, height, {
479
+ svg: 0,
480
+ png: 1,
481
+ webp: 2,
482
+ pdf: 3
483
+ }[format] ?? 0, options.textToPaths ?? true);
484
+ if (!result) {
485
+ if (format === "svg") return "";
486
+ return new Uint8Array();
487
+ }
488
+ if (format === "svg") return new TextDecoder().decode(result);
489
+ return new Uint8Array(result.slice());
490
+ } finally {
491
+ mod.destroy_instance(instancePtr);
492
+ mod.logLevel = prevLogLevel;
493
+ mod.onLog = prevOnLog;
494
+ this.currentFontMap = prevFontMap;
495
+ }
496
+ }
497
+ };
498
+ //#endregion
499
+ //#region src/index.ts
500
+ var Satoru = class Satoru extends SatoruBase {
501
+ static async create(createSatoruModuleFunc) {
502
+ return new Satoru(createSatoruModuleFunc);
503
+ }
504
+ /**
505
+ * Captures an HTMLElement and returns an HTMLCanvasElement.
506
+ * Similar to html2canvas.
507
+ */
508
+ async capture(element, options = {}) {
509
+ const rect = element.getBoundingClientRect();
510
+ const width = options.width ?? Math.ceil(rect.width);
511
+ const height = options.height ?? Math.ceil(rect.height);
512
+ const serializedHtml = this.serializeElement(element);
513
+ const pngData = await this.render({
514
+ ...options,
515
+ value: serializedHtml,
516
+ width,
517
+ height,
518
+ format: "png"
519
+ });
520
+ return this.pngToCanvas(pngData, width, height);
521
+ }
522
+ serializeElement(element) {
523
+ const clone = element.cloneNode(true);
524
+ const absoluteUrl = (url) => {
525
+ if (!url || url.startsWith("data:") || url.startsWith("blob:")) return url;
526
+ const a = document.createElement("a");
527
+ a.href = url;
528
+ return a.href;
529
+ };
530
+ const applyStyles = (src, dest) => {
531
+ const computed = window.getComputedStyle(src);
532
+ const styleArr = [];
533
+ for (let i = 0; i < computed.length; i++) {
534
+ const prop = computed[i];
535
+ let value = computed.getPropertyValue(prop);
536
+ if (value.includes("url(")) value = value.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, url) => {
537
+ return `url("${absoluteUrl(url)}")`;
538
+ });
539
+ styleArr.push(`${prop}:${value}`);
540
+ }
541
+ dest.setAttribute("style", styleArr.join(";"));
542
+ const handlePseudo = (pseudoType) => {
543
+ const style = window.getComputedStyle(src, pseudoType);
544
+ const content = style.getPropertyValue("content");
545
+ if (!content || content === "none" || content === "normal") return;
546
+ const pseudoEl = document.createElement("span");
547
+ const pseudoStyles = [];
548
+ for (let i = 0; i < style.length; i++) {
549
+ const prop = style[i];
550
+ pseudoStyles.push(`${prop}:${style.getPropertyValue(prop)}`);
551
+ }
552
+ pseudoEl.setAttribute("style", pseudoStyles.join(";"));
553
+ if (content.startsWith("\"") && content.endsWith("\"")) pseudoEl.innerText = content.slice(1, -1);
554
+ if (pseudoType === "::before") dest.prepend(pseudoEl);
555
+ else dest.append(pseudoEl);
556
+ };
557
+ handlePseudo("::before");
558
+ handlePseudo("::after");
559
+ if (typeof HTMLCanvasElement !== "undefined" && src instanceof HTMLCanvasElement) {
560
+ const img = document.createElement("img");
561
+ img.src = src.toDataURL();
562
+ img.setAttribute("style", dest.getAttribute("style") || "");
563
+ dest.replaceWith(img);
564
+ } else if (typeof HTMLImageElement !== "undefined" && src instanceof HTMLImageElement) dest.setAttribute("src", absoluteUrl(src.src));
565
+ else if (typeof HTMLInputElement !== "undefined" && src instanceof HTMLInputElement) if (src.type === "checkbox" || src.type === "radio") {
566
+ if (src.checked) dest.setAttribute("checked", "");
567
+ } else dest.setAttribute("value", src.value);
568
+ else if (typeof HTMLTextAreaElement !== "undefined" && src instanceof HTMLTextAreaElement) dest.innerText = src.value;
569
+ else if (typeof HTMLSelectElement !== "undefined" && src instanceof HTMLSelectElement) {
570
+ const index = src.selectedIndex;
571
+ const clonedSelect = dest;
572
+ if (clonedSelect.options[index]) clonedSelect.options[index].setAttribute("selected", "");
573
+ }
574
+ for (let i = 0; i < src.children.length; i++) {
575
+ const srcChild = src.children[i];
576
+ const destChild = dest.children[i];
577
+ if (srcChild && destChild) applyStyles(srcChild, destChild);
578
+ }
579
+ };
580
+ applyStyles(element, clone);
581
+ return `<!DOCTYPE html><html><body style="margin:0;padding:0;overflow:hidden;background:transparent;">${clone.outerHTML}</body></html>`;
582
+ }
583
+ async pngToCanvas(data, width, height) {
584
+ const blob = new Blob([data], { type: "image/png" });
585
+ const url = URL.createObjectURL(blob);
586
+ try {
587
+ const img = new Image();
588
+ img.crossOrigin = "anonymous";
589
+ await new Promise((resolve, reject) => {
590
+ img.onload = resolve;
591
+ img.onerror = reject;
592
+ img.src = url;
593
+ });
594
+ const canvas = document.createElement("canvas");
595
+ canvas.width = width;
596
+ canvas.height = height;
597
+ const ctx = canvas.getContext("2d");
598
+ if (ctx) ctx.drawImage(img, 0, 0);
599
+ return canvas;
600
+ } finally {
601
+ URL.revokeObjectURL(url);
602
+ }
603
+ }
604
+ async render(options) {
605
+ if (options.value) {
606
+ const values = Array.isArray(options.value) ? options.value : [options.value];
607
+ const processedValues = [];
608
+ let hasElement = false;
609
+ for (const val of values) if (typeof HTMLElement !== "undefined" && val instanceof HTMLElement) {
610
+ processedValues.push(this.serializeElement(val));
611
+ hasElement = true;
612
+ } else processedValues.push(val);
613
+ if (hasElement) options = {
614
+ ...options,
615
+ value: Array.isArray(options.value) ? processedValues : processedValues[0]
616
+ };
617
+ }
618
+ return super.render(options);
619
+ }
620
+ static async defaultResourceResolver(resource, baseUrl, userAgent) {
621
+ try {
622
+ if (resource.url.startsWith("provider:google-fonts")) return resolveGoogleFonts(resource, userAgent);
623
+ const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(resource.url);
624
+ let finalUrl = null;
625
+ if (isAbsolute) finalUrl = resource.url;
626
+ else if (baseUrl && /^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) finalUrl = new URL(resource.url, baseUrl).href;
627
+ if (!finalUrl) return null;
628
+ const headers = {};
629
+ if (userAgent) headers["User-Agent"] = userAgent;
630
+ const resp = await fetch(finalUrl, { headers });
631
+ if (!resp.ok) return null;
632
+ const buf = await resp.arrayBuffer();
633
+ return new Uint8Array(buf);
634
+ } catch (e) {
635
+ console.warn(`[Satoru] Failed to resolve resource: ${resource.url}`, e);
636
+ return null;
637
+ }
638
+ }
639
+ resolveDefaultResource(resource, baseUrl, userAgent) {
640
+ return Satoru.defaultResourceResolver(resource, baseUrl, userAgent);
641
+ }
642
+ async fetchHtml(url, userAgent) {
643
+ const headers = {};
644
+ if (userAgent) headers["User-Agent"] = userAgent;
645
+ const resp = await fetch(url, { headers });
646
+ if (!resp.ok) throw new Error(`Failed to fetch HTML from URL: ${url} (${resp.status})`);
647
+ return await resp.text();
648
+ }
649
+ };
650
+ //#endregion
651
+ //#region src/workers.ts
652
+ /**
653
+ * Create a Satoru worker proxy using worker-lib.
654
+ * @param params Initialization parameters
655
+ * @param params.worker Optional: Path to the worker file, a URL, or a factory function.
656
+ * Defaults to the bundled workers.js in the same directory.
657
+ * @param params.maxParallel Maximum number of parallel workers
658
+ */
659
+ const createSatoruWorker = (params) => {
660
+ const { worker, maxParallel = 4 } = params ?? {};
661
+ const factory = () => {
662
+ let w$1;
663
+ if (worker) w$1 = typeof worker === "function" ? worker() : worker;
664
+ else {
665
+ const workerUrl = typeof window !== "undefined" ? new URL("./web-workers.js", import.meta.url) : new URL("./child-workers.js", import.meta.url);
666
+ if (typeof w !== "undefined") w$1 = new w(workerUrl, { type: "module" });
667
+ }
668
+ if (!w$1) throw new Error("Worker is not supported in this environment.");
669
+ return w$1;
670
+ };
671
+ const workerInstance = createWorker(factory, maxParallel);
672
+ return new Proxy(workerInstance, { get(target, prop, receiver) {
673
+ if (prop === "render") return async (options) => {
674
+ return await target.execute("render", options);
675
+ };
676
+ if (prop in target) return Reflect.get(target, prop, receiver);
677
+ return (...args) => target.execute(prop, ...args);
678
+ } });
679
+ };
680
+ const { close, render, launchWorker, setLimit, waitAll, waitReady } = createSatoruWorker({ maxParallel: 1 });
681
+ //#endregion
682
+ export { DEFAULT_FONT_MAP, LogLevel, Satoru, SatoruBase, close, createSatoruWorker, launchWorker, render, resolveGoogleFonts, setLimit, waitAll, waitReady };