satoru-render 1.0.10 → 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 +6 -2
- package/dist/core.js +98 -48
- package/dist/satoru-single.js +0 -0
- package/dist/satoru.js +1 -1
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +113 -49
- package/dist/workers-parent.js +96 -32
- package/package.json +5 -5
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,7 +541,7 @@ 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;
|
|
@@ -548,8 +555,13 @@ var SatoruBase = class {
|
|
|
548
555
|
const cachedResolver = async (r) => {
|
|
549
556
|
const cacheKey = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
550
557
|
const cached = this.resourceCache.get(cacheKey);
|
|
551
|
-
if (cached)
|
|
558
|
+
if (cached) {
|
|
559
|
+
addProfile("resourceCacheHitsCount", 1);
|
|
560
|
+
return cached;
|
|
561
|
+
}
|
|
562
|
+
const resolveStart = now();
|
|
552
563
|
const result = await resolver(r);
|
|
564
|
+
addProfile("resolveResources", now() - resolveStart);
|
|
553
565
|
if (result) {
|
|
554
566
|
if (result instanceof Uint8Array) this.resourceCache.set(cacheKey, result);
|
|
555
567
|
else if ("css" in result && "fonts" in result) this.resourceCache.set(cacheKey, result);
|
|
@@ -571,23 +583,6 @@ var SatoruBase = class {
|
|
|
571
583
|
if (images) for (const img of images) mod.load_image(instancePtr, img.name, img.url, img.width ?? 0, img.height ?? 0);
|
|
572
584
|
if (css) mod.scan_css(instancePtr, css);
|
|
573
585
|
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
586
|
let typeInt = 1;
|
|
592
587
|
if (r.type === "image") typeInt = 2;
|
|
593
588
|
if (r.type === "css") typeInt = 3;
|
|
@@ -599,14 +594,53 @@ var SatoruBase = class {
|
|
|
599
594
|
for (const rawHtml of inputHtmls) {
|
|
600
595
|
let processedHtml = rawHtml;
|
|
601
596
|
for (let i = 0; i < 10; i++) {
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
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) => {
|
|
606
638
|
const key = `${r.type}:${r.url}:${r.characters ?? ""}`;
|
|
607
639
|
return !resolvedResources.has(key);
|
|
608
640
|
});
|
|
641
|
+
addProfile("parsePendingResources", now() - parseStart);
|
|
609
642
|
if (pending.length === 0) break;
|
|
643
|
+
const loadStart = now();
|
|
610
644
|
await Promise.all(pending.map(async (r) => {
|
|
611
645
|
try {
|
|
612
646
|
if (r.url.startsWith("data:")) return;
|
|
@@ -652,7 +686,9 @@ var SatoruBase = class {
|
|
|
652
686
|
console.warn(`Failed to resolve resource: ${r.url}`, e);
|
|
653
687
|
}
|
|
654
688
|
}));
|
|
689
|
+
addProfile("loadPendingResources", now() - loadStart);
|
|
655
690
|
}
|
|
691
|
+
const stripStart = now();
|
|
656
692
|
const resolvedUrls = /* @__PURE__ */ new Set();
|
|
657
693
|
resolvedResources.forEach((key) => {
|
|
658
694
|
const parts = key.split(":");
|
|
@@ -664,13 +700,29 @@ var SatoruBase = class {
|
|
|
664
700
|
processedHtml = processedHtml.replace(linkRegex, "");
|
|
665
701
|
});
|
|
666
702
|
processedHtmls.push(processedHtml);
|
|
703
|
+
addProfile("stripResolvedLinks", now() - stripStart);
|
|
667
704
|
}
|
|
668
|
-
const
|
|
705
|
+
const formatMap = {
|
|
669
706
|
svg: 0,
|
|
670
707
|
png: 1,
|
|
671
708
|
webp: 2,
|
|
672
709
|
pdf: 3
|
|
673
|
-
}
|
|
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, {
|
|
674
726
|
svgTextToPaths: options.textToPaths ?? true,
|
|
675
727
|
outputWidth: options.outputWidth ?? 0,
|
|
676
728
|
outputHeight: options.outputHeight ?? 0,
|
|
@@ -684,12 +736,24 @@ var SatoruBase = class {
|
|
|
684
736
|
backgroundColor: this.parseColor(options.backgroundColor),
|
|
685
737
|
mediaType: options.mediaType === "print" ? 1 : 0
|
|
686
738
|
});
|
|
739
|
+
addProfile("wasmRender", now() - renderStart);
|
|
687
740
|
if (!result) {
|
|
741
|
+
options.onProfile?.(profile);
|
|
688
742
|
if (format === "svg") return "";
|
|
689
743
|
return new Uint8Array();
|
|
690
744
|
}
|
|
691
|
-
if (format === "svg")
|
|
692
|
-
|
|
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;
|
|
693
757
|
} finally {
|
|
694
758
|
mod.destroy_instance(instancePtr);
|
|
695
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"
|