satoru-render 1.0.10 → 1.0.12
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/cli.js +0 -0
- package/dist/core.d.ts +8 -2
- package/dist/core.js +138 -50
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +2 -2
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +369 -250
- package/dist/workers-parent.js +125 -34
- package/package.json +10 -10
package/dist/workers-parent.js
CHANGED
|
@@ -379,20 +379,20 @@ var SatoruBase = class {
|
|
|
379
379
|
}
|
|
380
380
|
async getModule() {
|
|
381
381
|
if (!this.modPromise) this.modPromise = (async () => {
|
|
382
|
-
let currentLogLevel =
|
|
382
|
+
let currentLogLevel = 0;
|
|
383
383
|
let currentUserOnLog;
|
|
384
384
|
const mod = await this.factory({
|
|
385
385
|
onLog: (level, message) => {
|
|
386
|
-
if (currentLogLevel !==
|
|
386
|
+
if (currentLogLevel !== 0 && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
|
|
387
387
|
},
|
|
388
388
|
print: (text) => {
|
|
389
|
-
if (currentLogLevel !==
|
|
389
|
+
if (currentLogLevel !== 0 && 3 <= currentLogLevel && currentUserOnLog) currentUserOnLog(3, text);
|
|
390
390
|
},
|
|
391
391
|
printErr: (text) => {
|
|
392
|
-
if (currentLogLevel !==
|
|
392
|
+
if (currentLogLevel !== 0 && 1 <= currentLogLevel && currentUserOnLog) currentUserOnLog(1, text);
|
|
393
393
|
}
|
|
394
394
|
});
|
|
395
|
-
mod.logLevel =
|
|
395
|
+
mod.logLevel = 0;
|
|
396
396
|
const originalSetLogLevel = mod.set_log_level;
|
|
397
397
|
mod.set_log_level = (level) => {
|
|
398
398
|
currentLogLevel = level;
|
|
@@ -502,6 +502,13 @@ var SatoruBase = class {
|
|
|
502
502
|
}
|
|
503
503
|
async render(options) {
|
|
504
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
|
+
};
|
|
505
512
|
if (format === "pdf" && Array.isArray(value) && value.length > 1) {
|
|
506
513
|
const pagePdfs = [];
|
|
507
514
|
const SatoruClass = this.constructor;
|
|
@@ -534,12 +541,13 @@ var SatoruBase = class {
|
|
|
534
541
|
const prevLogLevel = mod.logLevel;
|
|
535
542
|
const prevOnLog = mod.onLog;
|
|
536
543
|
const prevFontMap = this.currentFontMap;
|
|
537
|
-
mod.logLevel = logLevel ??
|
|
544
|
+
mod.logLevel = logLevel ?? 0;
|
|
538
545
|
mod.set_log_level(mod.logLevel);
|
|
539
546
|
mod.onLog = onLog;
|
|
540
547
|
this.currentFontMap = options.fontMap ?? DEFAULT_FONT_MAP;
|
|
541
548
|
const instancePtr = mod.create_instance();
|
|
542
549
|
mod.set_font_map(instancePtr, this.currentFontMap);
|
|
550
|
+
mod.set_collect_profile_enabled(instancePtr, profileEnabled);
|
|
543
551
|
try {
|
|
544
552
|
const defaultResolver = (r) => this.resolveDefaultResource(r, baseUrl, options.userAgent);
|
|
545
553
|
const resolver = options.resolveResource ? async (r) => {
|
|
@@ -548,8 +556,13 @@ var SatoruBase = class {
|
|
|
548
556
|
const cachedResolver = async (r) => {
|
|
549
557
|
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
550
558
|
const cached = this.resourceCache.get(cacheKey);
|
|
551
|
-
if (cached)
|
|
559
|
+
if (cached) {
|
|
560
|
+
addProfile("resourceCacheHitsCount", 1);
|
|
561
|
+
return cached;
|
|
562
|
+
}
|
|
563
|
+
const resolveStart = now();
|
|
552
564
|
const result = await resolver(r);
|
|
565
|
+
addProfile("resolveResources", now() - resolveStart);
|
|
553
566
|
if (result) {
|
|
554
567
|
if (result instanceof Uint8Array) this.resourceCache.set(cacheKey, result);
|
|
555
568
|
else if ("css" in result && "fonts" in result) this.resourceCache.set(cacheKey, result);
|
|
@@ -571,23 +584,6 @@ var SatoruBase = class {
|
|
|
571
584
|
if (images) for (const img of images) mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
|
|
572
585
|
if (css) mod.scan_css(instancePtr, css);
|
|
573
586
|
const loadResourceData = (r, uint8) => {
|
|
574
|
-
if (r.type === "image" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") return (async () => {
|
|
575
|
-
try {
|
|
576
|
-
const blob = new Blob([uint8.buffer]);
|
|
577
|
-
const bitmap = await createImageBitmap(blob);
|
|
578
|
-
const ctx = new OffscreenCanvas(bitmap.width, bitmap.height).getContext("2d");
|
|
579
|
-
if (ctx) {
|
|
580
|
-
ctx.drawImage(bitmap, 0, 0);
|
|
581
|
-
const imageData = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
582
|
-
mod.load_image_pixels(instancePtr, r.url, bitmap.width, bitmap.height, new Uint8Array(imageData.data.buffer), r.url);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
} catch (e) {}
|
|
586
|
-
let typeInt = 1;
|
|
587
|
-
if (r.type === "image") typeInt = 2;
|
|
588
|
-
if (r.type === "css") typeInt = 3;
|
|
589
|
-
mod.add_resource(instancePtr, r.url, typeInt, uint8);
|
|
590
|
-
})();
|
|
591
587
|
let typeInt = 1;
|
|
592
588
|
if (r.type === "image") typeInt = 2;
|
|
593
589
|
if (r.type === "css") typeInt = 3;
|
|
@@ -595,23 +591,88 @@ var SatoruBase = class {
|
|
|
595
591
|
};
|
|
596
592
|
const inputHtmls = Array.isArray(value) ? value : [value];
|
|
597
593
|
const processedHtmls = [];
|
|
594
|
+
const utf8Decoder = new TextDecoder();
|
|
595
|
+
const utf8Encoder = new TextEncoder();
|
|
598
596
|
const resolvedResources = /* @__PURE__ */ new Set();
|
|
599
597
|
for (const rawHtml of inputHtmls) {
|
|
600
598
|
let processedHtml = rawHtml;
|
|
601
599
|
for (let i = 0; i < 10; i++) {
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
const
|
|
600
|
+
addProfile("collectResourcesCount", 1);
|
|
601
|
+
const collectStart = now();
|
|
602
|
+
mod.collect_resources(instancePtr, processedHtml, width, height, options.mediaType === "print" ? 1 : 0);
|
|
603
|
+
const collectElapsed = now() - collectStart;
|
|
604
|
+
addProfile("collectResources", collectElapsed);
|
|
605
|
+
addProfile(`collectResourcesRound${i + 1}`, collectElapsed);
|
|
606
|
+
if (profileEnabled) try {
|
|
607
|
+
const collectProfile = JSON.parse(mod.get_collect_profile(instancePtr));
|
|
608
|
+
for (const [key, value] of Object.entries(collectProfile)) {
|
|
609
|
+
addProfile(key, value);
|
|
610
|
+
addProfile(`${key}Round${i + 1}`, value);
|
|
611
|
+
}
|
|
612
|
+
} catch {}
|
|
613
|
+
const pendingStart = now();
|
|
614
|
+
const binary = mod.get_pending_resources(instancePtr);
|
|
615
|
+
addProfile("getPendingResources", now() - pendingStart);
|
|
616
|
+
if (!binary) break;
|
|
617
|
+
const parseStart = now();
|
|
618
|
+
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
619
|
+
let offset = 0;
|
|
620
|
+
const count = view.getUint32(offset, true);
|
|
621
|
+
offset += 4;
|
|
622
|
+
const resources = [];
|
|
623
|
+
for (let j = 0; j < count; j++) {
|
|
624
|
+
const typeInt = view.getUint8(offset++);
|
|
625
|
+
const redraw_on_ready = view.getUint8(offset++) !== 0;
|
|
626
|
+
const urlLen = view.getUint32(offset, true);
|
|
627
|
+
offset += 4;
|
|
628
|
+
const url = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, urlLen));
|
|
629
|
+
offset += urlLen;
|
|
630
|
+
const nameLen = view.getUint32(offset, true);
|
|
631
|
+
offset += 4;
|
|
632
|
+
const name = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, nameLen));
|
|
633
|
+
offset += nameLen;
|
|
634
|
+
const charsLen = view.getUint32(offset, true);
|
|
635
|
+
offset += 4;
|
|
636
|
+
const characters = utf8Decoder.decode(new Uint8Array(binary.buffer, binary.byteOffset + offset, charsLen));
|
|
637
|
+
offset += charsLen;
|
|
638
|
+
let type = "font";
|
|
639
|
+
if (typeInt === 2) type = "image";
|
|
640
|
+
else if (typeInt === 3) type = "css";
|
|
641
|
+
resources.push({
|
|
642
|
+
type,
|
|
643
|
+
url,
|
|
644
|
+
name,
|
|
645
|
+
characters,
|
|
646
|
+
redraw_on_ready
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const pending = resources.filter((r) => {
|
|
606
650
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
651
|
+
if (r.type === "font" && resolvedResources.has(`font:${r.url}:`)) return false;
|
|
607
652
|
return !resolvedResources.has(key);
|
|
608
653
|
});
|
|
654
|
+
addProfile("pendingResourcesCount", pending.length);
|
|
655
|
+
addProfile(`pendingResourcesRound${i + 1}Count`, pending.length);
|
|
656
|
+
for (const r of pending) if (r.type === "font") {
|
|
657
|
+
addProfile("pendingFontResourcesCount", 1);
|
|
658
|
+
addProfile(`pendingFontResourcesRound${i + 1}Count`, 1);
|
|
659
|
+
} else if (r.type === "css") {
|
|
660
|
+
addProfile("pendingCssResourcesCount", 1);
|
|
661
|
+
addProfile(`pendingCssResourcesRound${i + 1}Count`, 1);
|
|
662
|
+
} else if (r.type === "image") {
|
|
663
|
+
addProfile("pendingImageResourcesCount", 1);
|
|
664
|
+
addProfile(`pendingImageResourcesRound${i + 1}Count`, 1);
|
|
665
|
+
}
|
|
666
|
+
addProfile("parsePendingResources", now() - parseStart);
|
|
609
667
|
if (pending.length === 0) break;
|
|
668
|
+
addProfile("pendingResourceRoundsCount", 1);
|
|
669
|
+
const loadStart = now();
|
|
610
670
|
await Promise.all(pending.map(async (r) => {
|
|
611
671
|
try {
|
|
612
672
|
if (r.url.startsWith("data:")) return;
|
|
613
673
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
614
674
|
resolvedResources.add(key);
|
|
675
|
+
if (r.type === "font") resolvedResources.add(`font:${r.url}:`);
|
|
615
676
|
const data = await cachedResolver({ ...r });
|
|
616
677
|
if (!data) return;
|
|
617
678
|
if (typeof data === "object" && "css" in data && "fonts" in data) {
|
|
@@ -627,7 +688,7 @@ var SatoruBase = class {
|
|
|
627
688
|
if (data instanceof Uint8Array || ArrayBuffer.isView(data)) {
|
|
628
689
|
let finalUint8 = data instanceof Uint8Array ? data : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
629
690
|
if (r.type === "css") {
|
|
630
|
-
const cssText =
|
|
691
|
+
const cssText = utf8Decoder.decode(finalUint8);
|
|
631
692
|
const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(r.url) || r.url.startsWith("data:");
|
|
632
693
|
let cssBaseUrl = r.url;
|
|
633
694
|
if (!isAbsolute && baseUrl) try {
|
|
@@ -643,7 +704,7 @@ var SatoruBase = class {
|
|
|
643
704
|
return match;
|
|
644
705
|
}
|
|
645
706
|
});
|
|
646
|
-
finalUint8 =
|
|
707
|
+
finalUint8 = utf8Encoder.encode(rewrittenCss);
|
|
647
708
|
}
|
|
648
709
|
}
|
|
649
710
|
await loadResourceData(r, finalUint8);
|
|
@@ -652,7 +713,9 @@ var SatoruBase = class {
|
|
|
652
713
|
console.warn(`Failed to resolve resource: ${r.url}`, e);
|
|
653
714
|
}
|
|
654
715
|
}));
|
|
716
|
+
addProfile("loadPendingResources", now() - loadStart);
|
|
655
717
|
}
|
|
718
|
+
const stripStart = now();
|
|
656
719
|
const resolvedUrls = /* @__PURE__ */ new Set();
|
|
657
720
|
resolvedResources.forEach((key) => {
|
|
658
721
|
const parts = key.split(":");
|
|
@@ -664,13 +727,29 @@ var SatoruBase = class {
|
|
|
664
727
|
processedHtml = processedHtml.replace(linkRegex, "");
|
|
665
728
|
});
|
|
666
729
|
processedHtmls.push(processedHtml);
|
|
730
|
+
addProfile("stripResolvedLinks", now() - stripStart);
|
|
667
731
|
}
|
|
668
|
-
const
|
|
732
|
+
const formatMap = {
|
|
669
733
|
svg: 0,
|
|
670
734
|
png: 1,
|
|
671
735
|
webp: 2,
|
|
672
736
|
pdf: 3
|
|
673
|
-
}
|
|
737
|
+
};
|
|
738
|
+
const renderStart = now();
|
|
739
|
+
const result = processedHtmls.length === 1 ? mod.render_from_state(instancePtr, width, height, formatMap[format] ?? 0, {
|
|
740
|
+
svgTextToPaths: options.textToPaths ?? true,
|
|
741
|
+
outputWidth: options.outputWidth ?? 0,
|
|
742
|
+
outputHeight: options.outputHeight ?? 0,
|
|
743
|
+
fitType: options.fit === "cover" ? 1 : options.fit === "fill" ? 2 : 0,
|
|
744
|
+
cropX: options.crop?.x ?? 0,
|
|
745
|
+
cropY: options.crop?.y ?? 0,
|
|
746
|
+
cropWidth: options.crop?.width ?? 0,
|
|
747
|
+
cropHeight: options.crop?.height ?? 0,
|
|
748
|
+
fitPositionX: options.fitPosition?.x ?? .5,
|
|
749
|
+
fitPositionY: options.fitPosition?.y ?? .5,
|
|
750
|
+
backgroundColor: this.parseColor(options.backgroundColor),
|
|
751
|
+
mediaType: options.mediaType === "print" ? 1 : 0
|
|
752
|
+
}) : mod.render(instancePtr, processedHtmls, width, height, formatMap[format] ?? 0, {
|
|
674
753
|
svgTextToPaths: options.textToPaths ?? true,
|
|
675
754
|
outputWidth: options.outputWidth ?? 0,
|
|
676
755
|
outputHeight: options.outputHeight ?? 0,
|
|
@@ -684,12 +763,24 @@ var SatoruBase = class {
|
|
|
684
763
|
backgroundColor: this.parseColor(options.backgroundColor),
|
|
685
764
|
mediaType: options.mediaType === "print" ? 1 : 0
|
|
686
765
|
});
|
|
766
|
+
addProfile("wasmRender", now() - renderStart);
|
|
687
767
|
if (!result) {
|
|
768
|
+
options.onProfile?.(profile);
|
|
688
769
|
if (format === "svg") return "";
|
|
689
770
|
return new Uint8Array();
|
|
690
771
|
}
|
|
691
|
-
if (format === "svg")
|
|
692
|
-
|
|
772
|
+
if (format === "svg") {
|
|
773
|
+
const decodeStart = now();
|
|
774
|
+
const svg = utf8Decoder.decode(result);
|
|
775
|
+
addProfile("decodeResult", now() - decodeStart);
|
|
776
|
+
options.onProfile?.(profile);
|
|
777
|
+
return svg;
|
|
778
|
+
}
|
|
779
|
+
const copyStart = now();
|
|
780
|
+
const bytes = new Uint8Array(result);
|
|
781
|
+
addProfile("copyResult", now() - copyStart);
|
|
782
|
+
options.onProfile?.(profile);
|
|
783
|
+
return bytes;
|
|
693
784
|
} finally {
|
|
694
785
|
mod.destroy_instance(instancePtr);
|
|
695
786
|
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.12",
|
|
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",
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
"bin": {
|
|
9
9
|
"satoru-render": "./dist/cli.js"
|
|
10
10
|
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"build": "tsc -b && rolldown -c rolldown.config.js",
|
|
13
|
-
"deploy": "npm publish"
|
|
14
|
-
},
|
|
15
11
|
"exports": {
|
|
16
12
|
".": {
|
|
17
13
|
"workerd": {
|
|
@@ -123,12 +119,12 @@
|
|
|
123
119
|
"cloudflare-workers"
|
|
124
120
|
],
|
|
125
121
|
"devDependencies": {
|
|
126
|
-
"@types/jsdom": "28.0.
|
|
122
|
+
"@types/jsdom": "28.0.3",
|
|
127
123
|
"@types/react": "^19.2.14",
|
|
128
124
|
"@types/react-dom": "^19.2.3",
|
|
129
|
-
"rolldown": "1.0.
|
|
130
|
-
"typescript": "^
|
|
131
|
-
"vitest": "^4.1.
|
|
125
|
+
"rolldown": "1.0.1",
|
|
126
|
+
"typescript": "^6.0.3",
|
|
127
|
+
"vitest": "^4.1.6"
|
|
132
128
|
},
|
|
133
129
|
"dependencies": {
|
|
134
130
|
"worker-lib": "2.2.0"
|
|
@@ -164,5 +160,9 @@
|
|
|
164
160
|
"@unocss/preset-wind4": {
|
|
165
161
|
"optional": true
|
|
166
162
|
}
|
|
163
|
+
},
|
|
164
|
+
"scripts": {
|
|
165
|
+
"build": "tsc -b && rolldown -c rolldown.config.js",
|
|
166
|
+
"deploy": "npm publish"
|
|
167
167
|
}
|
|
168
|
-
}
|
|
168
|
+
}
|