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.
@@ -379,20 +379,20 @@ var SatoruBase = class {
379
379
  }
380
380
  async getModule() {
381
381
  if (!this.modPromise) this.modPromise = (async () => {
382
- let currentLogLevel = LogLevel.None;
382
+ let currentLogLevel = 0;
383
383
  let currentUserOnLog;
384
384
  const mod = await this.factory({
385
385
  onLog: (level, message) => {
386
- if (currentLogLevel !== LogLevel.None && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
386
+ if (currentLogLevel !== 0 && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
387
387
  },
388
388
  print: (text) => {
389
- if (currentLogLevel !== LogLevel.None && LogLevel.Info <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Info, text);
389
+ if (currentLogLevel !== 0 && 3 <= currentLogLevel && currentUserOnLog) currentUserOnLog(3, text);
390
390
  },
391
391
  printErr: (text) => {
392
- if (currentLogLevel !== LogLevel.None && LogLevel.Error <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Error, text);
392
+ if (currentLogLevel !== 0 && 1 <= currentLogLevel && currentUserOnLog) currentUserOnLog(1, text);
393
393
  }
394
394
  });
395
- mod.logLevel = LogLevel.None;
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 ?? LogLevel.None;
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) return 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
- mod.collect_resources(instancePtr, processedHtml, width, height);
603
- const json = mod.get_pending_resources(instancePtr);
604
- if (!json) break;
605
- const pending = JSON.parse(json).filter((r) => {
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 = new TextDecoder().decode(finalUint8);
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 = new TextEncoder().encode(rewrittenCss);
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 result = mod.render(instancePtr, processedHtmls, width, height, {
732
+ const formatMap = {
669
733
  svg: 0,
670
734
  png: 1,
671
735
  webp: 2,
672
736
  pdf: 3
673
- }[format] ?? 0, {
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") return new TextDecoder().decode(result);
692
- return new Uint8Array(result);
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.10",
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.1",
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.0-rc.11",
130
- "typescript": "^5.9.3",
131
- "vitest": "^4.1.0"
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
+ }