satoru-render 1.0.9 → 1.0.11
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/dist/core.d.ts +7 -2
- package/dist/core.js +157 -76
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +1 -1
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +152 -65
- package/dist/workers-parent.js +125 -38
- package/package.json +5 -5
package/dist/workers-parent.js
CHANGED
|
@@ -250,14 +250,18 @@ let LogLevel = /* @__PURE__ */ function(LogLevel) {
|
|
|
250
250
|
}({});
|
|
251
251
|
//#endregion
|
|
252
252
|
//#region src/core.ts
|
|
253
|
+
const emojiUrl = "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2";
|
|
253
254
|
const DEFAULT_FONT_MAP = {
|
|
254
255
|
"sans-serif": "https://fonts.googleapis.com/css2?family=Noto+Sans+JP",
|
|
255
256
|
serif: "https://fonts.googleapis.com/css2?family=Noto+Serif+JP",
|
|
256
257
|
monospace: "https://fonts.googleapis.com/css2?family=M+PLUS+1+Code",
|
|
257
258
|
cursive: "https://fonts.googleapis.com/css2?family=Yuji+Syuku",
|
|
258
259
|
fantasy: "https://fonts.googleapis.com/css2?family=Reggae+One",
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
"Noto Color Emoji": emojiUrl,
|
|
261
|
+
emoji: emojiUrl,
|
|
262
|
+
"Noto Emoji": emojiUrl,
|
|
263
|
+
notocoloremoji: emojiUrl,
|
|
264
|
+
notoemoji: emojiUrl
|
|
261
265
|
};
|
|
262
266
|
/**
|
|
263
267
|
* Parse unicode-range string into an array of [start, end] codepoint ranges.
|
|
@@ -375,20 +379,20 @@ var SatoruBase = class {
|
|
|
375
379
|
}
|
|
376
380
|
async getModule() {
|
|
377
381
|
if (!this.modPromise) this.modPromise = (async () => {
|
|
378
|
-
let currentLogLevel =
|
|
382
|
+
let currentLogLevel = 0;
|
|
379
383
|
let currentUserOnLog;
|
|
380
384
|
const mod = await this.factory({
|
|
381
385
|
onLog: (level, message) => {
|
|
382
|
-
if (currentLogLevel !==
|
|
386
|
+
if (currentLogLevel !== 0 && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
|
|
383
387
|
},
|
|
384
388
|
print: (text) => {
|
|
385
|
-
if (currentLogLevel !==
|
|
389
|
+
if (currentLogLevel !== 0 && 3 <= currentLogLevel && currentUserOnLog) currentUserOnLog(3, text);
|
|
386
390
|
},
|
|
387
391
|
printErr: (text) => {
|
|
388
|
-
if (currentLogLevel !==
|
|
392
|
+
if (currentLogLevel !== 0 && 1 <= currentLogLevel && currentUserOnLog) currentUserOnLog(1, text);
|
|
389
393
|
}
|
|
390
394
|
});
|
|
391
|
-
mod.logLevel =
|
|
395
|
+
mod.logLevel = 0;
|
|
392
396
|
const originalSetLogLevel = mod.set_log_level;
|
|
393
397
|
mod.set_log_level = (level) => {
|
|
394
398
|
currentLogLevel = level;
|
|
@@ -455,6 +459,15 @@ var SatoruBase = class {
|
|
|
455
459
|
async destroyInstance(inst) {
|
|
456
460
|
(await this.getModule()).destroy_instance(inst);
|
|
457
461
|
}
|
|
462
|
+
async loadFont(name, data) {
|
|
463
|
+
const mod = await this.getModule();
|
|
464
|
+
const inst = mod.create_instance();
|
|
465
|
+
try {
|
|
466
|
+
mod.load_font(inst, name, data);
|
|
467
|
+
} finally {
|
|
468
|
+
mod.destroy_instance(inst);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
458
471
|
async loadFallbackFont(data) {
|
|
459
472
|
const mod = await this.getModule();
|
|
460
473
|
const inst = mod.create_instance();
|
|
@@ -489,6 +502,13 @@ var SatoruBase = class {
|
|
|
489
502
|
}
|
|
490
503
|
async render(options) {
|
|
491
504
|
let { format = "svg", value, url, baseUrl } = options;
|
|
505
|
+
const profileEnabled = options.profile === true;
|
|
506
|
+
const profile = {};
|
|
507
|
+
const now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
508
|
+
const addProfile = (name, elapsed) => {
|
|
509
|
+
if (!profileEnabled) return;
|
|
510
|
+
profile[name] = (profile[name] ?? 0) + elapsed;
|
|
511
|
+
};
|
|
492
512
|
if (format === "pdf" && Array.isArray(value) && value.length > 1) {
|
|
493
513
|
const pagePdfs = [];
|
|
494
514
|
const SatoruClass = this.constructor;
|
|
@@ -521,17 +541,13 @@ var SatoruBase = class {
|
|
|
521
541
|
const prevLogLevel = mod.logLevel;
|
|
522
542
|
const prevOnLog = mod.onLog;
|
|
523
543
|
const prevFontMap = this.currentFontMap;
|
|
524
|
-
mod.logLevel = logLevel ??
|
|
544
|
+
mod.logLevel = logLevel ?? 0;
|
|
525
545
|
mod.set_log_level(mod.logLevel);
|
|
526
546
|
mod.onLog = onLog;
|
|
527
547
|
this.currentFontMap = options.fontMap ?? DEFAULT_FONT_MAP;
|
|
528
548
|
const instancePtr = mod.create_instance();
|
|
529
549
|
mod.set_font_map(instancePtr, this.currentFontMap);
|
|
530
550
|
try {
|
|
531
|
-
if (fonts) for (const f of fonts) mod.load_font(instancePtr, f.name, f.data);
|
|
532
|
-
if (options.fallbackFonts) for (const data of options.fallbackFonts) mod.load_fallback_font(instancePtr, data);
|
|
533
|
-
if (images) for (const img of images) mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
|
|
534
|
-
if (css) mod.scan_css(instancePtr, css);
|
|
535
551
|
const defaultResolver = (r) => this.resolveDefaultResource(r, baseUrl, options.userAgent);
|
|
536
552
|
const resolver = options.resolveResource ? async (r) => {
|
|
537
553
|
return await options.resolveResource(r, defaultResolver);
|
|
@@ -539,32 +555,34 @@ var SatoruBase = class {
|
|
|
539
555
|
const cachedResolver = async (r) => {
|
|
540
556
|
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
541
557
|
const cached = this.resourceCache.get(cacheKey);
|
|
542
|
-
if (cached)
|
|
558
|
+
if (cached) {
|
|
559
|
+
addProfile("resourceCacheHitsCount", 1);
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
562
|
+
const resolveStart = now();
|
|
543
563
|
const result = await resolver(r);
|
|
564
|
+
addProfile("resolveResources", now() - resolveStart);
|
|
544
565
|
if (result) {
|
|
545
566
|
if (result instanceof Uint8Array) this.resourceCache.set(cacheKey, result);
|
|
546
567
|
else if ("css" in result && "fonts" in result) this.resourceCache.set(cacheKey, result);
|
|
547
568
|
}
|
|
548
569
|
return result;
|
|
549
570
|
};
|
|
571
|
+
if (this.currentFontMap["notocoloremoji"]) {
|
|
572
|
+
const emojiUrl = this.currentFontMap["notocoloremoji"];
|
|
573
|
+
const res = await cachedResolver({
|
|
574
|
+
type: "font",
|
|
575
|
+
url: emojiUrl,
|
|
576
|
+
name: "notocoloremoji"
|
|
577
|
+
});
|
|
578
|
+
if (res && res instanceof Uint8Array) mod.load_font(instancePtr, "notocoloremoji", res);
|
|
579
|
+
else if (res && "fonts" in res) for (const f of res.fonts) mod.load_font(instancePtr, "notocoloremoji", f.data);
|
|
580
|
+
}
|
|
581
|
+
if (fonts) for (const f of fonts) mod.load_font(instancePtr, f.name, f.data);
|
|
582
|
+
if (options.fallbackFonts) for (const data of options.fallbackFonts) mod.load_fallback_font(instancePtr, data);
|
|
583
|
+
if (images) for (const img of images) mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
|
|
584
|
+
if (css) mod.scan_css(instancePtr, css);
|
|
550
585
|
const loadResourceData = (r, uint8) => {
|
|
551
|
-
if (r.type === "image" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") return (async () => {
|
|
552
|
-
try {
|
|
553
|
-
const blob = new Blob([uint8.buffer]);
|
|
554
|
-
const bitmap = await createImageBitmap(blob);
|
|
555
|
-
const ctx = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
|
|
556
|
-
if (ctx) {
|
|
557
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
558
|
-
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
559
|
-
mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
} catch (e) {}
|
|
563
|
-
let typeInt = 1;
|
|
564
|
-
if (r.type === "image") typeInt = 2;
|
|
565
|
-
if (r.type === "css") typeInt = 3;
|
|
566
|
-
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
567
|
-
})();
|
|
568
586
|
let typeInt = 1;
|
|
569
587
|
if (r.type === "image") typeInt = 2;
|
|
570
588
|
if (r.type === "css") typeInt = 3;
|
|
@@ -576,14 +594,53 @@ var SatoruBase = class {
|
|
|
576
594
|
for (const rawHtml of inputHtmls) {
|
|
577
595
|
let processedHtml = rawHtml;
|
|
578
596
|
for (let i = 0; i < 10; i++) {
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
597
|
+
addProfile("collectResourcesCount", 1);
|
|
598
|
+
const collectStart = now();
|
|
599
|
+
mod.collect_resources(instancePtr, processedHtml, width, height, options.mediaType === "print" ? 1 : 0);
|
|
600
|
+
addProfile("collectResources", now() - collectStart);
|
|
601
|
+
const pendingStart = now();
|
|
602
|
+
const binary = mod.get_pending_resources(instancePtr);
|
|
603
|
+
addProfile("getPendingResources", now() - pendingStart);
|
|
604
|
+
if (!binary) break;
|
|
605
|
+
const parseStart = now();
|
|
606
|
+
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
607
|
+
let offset = 0;
|
|
608
|
+
const count = view.getUint32(offset, true);
|
|
609
|
+
offset += 4;
|
|
610
|
+
const resources = [];
|
|
611
|
+
for (let j = 0; j < count; j++) {
|
|
612
|
+
const typeInt = view.getUint8(offset++);
|
|
613
|
+
const redraw_on_ready = view.getUint8(offset++) !== 0;
|
|
614
|
+
const urlLen = view.getUint32(offset, true);
|
|
615
|
+
offset += 4;
|
|
616
|
+
const url = new TextDecoder().decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, urlLen));
|
|
617
|
+
offset += urlLen;
|
|
618
|
+
const nameLen = view.getUint32(offset, true);
|
|
619
|
+
offset += 4;
|
|
620
|
+
const name = new TextDecoder().decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, nameLen));
|
|
621
|
+
offset += nameLen;
|
|
622
|
+
const charsLen = view.getUint32(offset, true);
|
|
623
|
+
offset += 4;
|
|
624
|
+
const characters = new TextDecoder().decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, charsLen));
|
|
625
|
+
offset += charsLen;
|
|
626
|
+
let type = "font";
|
|
627
|
+
if (typeInt === 2) type = "image";
|
|
628
|
+
else if (typeInt === 3) type = "css";
|
|
629
|
+
resources.push({
|
|
630
|
+
type,
|
|
631
|
+
url,
|
|
632
|
+
name,
|
|
633
|
+
characters,
|
|
634
|
+
redraw_on_ready
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const pending = resources.filter((r) => {
|
|
583
638
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
584
639
|
return !resolvedResources.has(key);
|
|
585
640
|
});
|
|
641
|
+
addProfile("parsePendingResources", now() - parseStart);
|
|
586
642
|
if (pending.length === 0) break;
|
|
643
|
+
const loadStart = now();
|
|
587
644
|
await Promise.all(pending.map(async (r) => {
|
|
588
645
|
try {
|
|
589
646
|
if (r.url.startsWith("data:")) return;
|
|
@@ -629,7 +686,9 @@ var SatoruBase = class {
|
|
|
629
686
|
console.warn(`Failed to resolve resource: ${r.url}`, e);
|
|
630
687
|
}
|
|
631
688
|
}));
|
|
689
|
+
addProfile("loadPendingResources", now() - loadStart);
|
|
632
690
|
}
|
|
691
|
+
const stripStart = now();
|
|
633
692
|
const resolvedUrls = /* @__PURE__ */ new Set();
|
|
634
693
|
resolvedResources.forEach((key) => {
|
|
635
694
|
const parts = key.split(":");
|
|
@@ -641,13 +700,29 @@ var SatoruBase = class {
|
|
|
641
700
|
processedHtml = processedHtml.replace(linkRegex, "");
|
|
642
701
|
});
|
|
643
702
|
processedHtmls.push(processedHtml);
|
|
703
|
+
addProfile("stripResolvedLinks", now() - stripStart);
|
|
644
704
|
}
|
|
645
|
-
const
|
|
705
|
+
const formatMap = {
|
|
646
706
|
svg: 0,
|
|
647
707
|
png: 1,
|
|
648
708
|
webp: 2,
|
|
649
709
|
pdf: 3
|
|
650
|
-
}
|
|
710
|
+
};
|
|
711
|
+
const renderStart = now();
|
|
712
|
+
const result = processedHtmls.length === 1 ? mod.render_from_state(instancePtr, width, height, formatMap[format] ?? 0, {
|
|
713
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
714
|
+
outputWidth: options.outputWidth ?? 0,
|
|
715
|
+
outputHeight: options.outputHeight ?? 0,
|
|
716
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
717
|
+
cropX: options.crop?.x ?? 0,
|
|
718
|
+
cropY: options.crop?.y ?? 0,
|
|
719
|
+
cropWidth: options.crop?.width ?? 0,
|
|
720
|
+
cropHeight: options.crop?.height ?? 0,
|
|
721
|
+
fitPositionX: options.fitPosition?.x ?? .5,
|
|
722
|
+
fitPositionY: options.fitPosition?.y ?? .5,
|
|
723
|
+
backgroundColor: this.parseColor(options.backgroundColor),
|
|
724
|
+
mediaType: options.mediaType === "print" ? 1 : 0
|
|
725
|
+
}) : mod.render(instancePtr, processedHtmls, width, height, formatMap[format] ?? 0, {
|
|
651
726
|
svgTextToPaths: options.textToPaths ?? true,
|
|
652
727
|
outputWidth: options.outputWidth ?? 0,
|
|
653
728
|
outputHeight: options.outputHeight ?? 0,
|
|
@@ -661,12 +736,24 @@ var SatoruBase = class {
|
|
|
661
736
|
backgroundColor: this.parseColor(options.backgroundColor),
|
|
662
737
|
mediaType: options.mediaType === "print" ? 1 : 0
|
|
663
738
|
});
|
|
739
|
+
addProfile("wasmRender", now() - renderStart);
|
|
664
740
|
if (!result) {
|
|
741
|
+
options.onProfile?.(profile);
|
|
665
742
|
if (format === "svg") return "";
|
|
666
743
|
return new Uint8Array();
|
|
667
744
|
}
|
|
668
|
-
if (format === "svg")
|
|
669
|
-
|
|
745
|
+
if (format === "svg") {
|
|
746
|
+
const decodeStart = now();
|
|
747
|
+
const svg = new TextDecoder().decode(result);
|
|
748
|
+
addProfile("decodeResult", now() - decodeStart);
|
|
749
|
+
options.onProfile?.(profile);
|
|
750
|
+
return svg;
|
|
751
|
+
}
|
|
752
|
+
const copyStart = now();
|
|
753
|
+
const bytes = new Uint8Array(result);
|
|
754
|
+
addProfile("copyResult", now() - copyStart);
|
|
755
|
+
options.onProfile?.(profile);
|
|
756
|
+
return bytes;
|
|
670
757
|
} finally {
|
|
671
758
|
mod.destroy_instance(instancePtr);
|
|
672
759
|
mod.logLevel = prevLogLevel;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "satoru-render",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "High-fidelity HTML/CSS to SVG/PNG/PDF converter running in WebAssembly",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -123,12 +123,12 @@
|
|
|
123
123
|
"cloudflare-workers"
|
|
124
124
|
],
|
|
125
125
|
"devDependencies": {
|
|
126
|
-
"@types/jsdom": "28.0.
|
|
126
|
+
"@types/jsdom": "28.0.3",
|
|
127
127
|
"@types/react": "^19.2.14",
|
|
128
128
|
"@types/react-dom": "^19.2.3",
|
|
129
|
-
"rolldown": "1.0.
|
|
130
|
-
"typescript": "^
|
|
131
|
-
"vitest": "^4.1.
|
|
129
|
+
"rolldown": "1.0.1",
|
|
130
|
+
"typescript": "^6.0.3",
|
|
131
|
+
"vitest": "^4.1.6"
|
|
132
132
|
},
|
|
133
133
|
"dependencies": {
|
|
134
134
|
"worker-lib": "2.2.0"
|