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.
@@ -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
- 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"
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 = LogLevel.None;
382
+ let currentLogLevel = 0;
379
383
  let currentUserOnLog;
380
384
  const mod = await this.factory({
381
385
  onLog: (level, message) => {
382
- if (currentLogLevel !== LogLevel.None && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
386
+ if (currentLogLevel !== 0 && level <= currentLogLevel && currentUserOnLog) currentUserOnLog(level, message);
383
387
  },
384
388
  print: (text) => {
385
- if (currentLogLevel !== LogLevel.None && LogLevel.Info <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Info, text);
389
+ if (currentLogLevel !== 0 && 3 <= currentLogLevel && currentUserOnLog) currentUserOnLog(3, text);
386
390
  },
387
391
  printErr: (text) => {
388
- if (currentLogLevel !== LogLevel.None && LogLevel.Error <= currentLogLevel && currentUserOnLog) currentUserOnLog(LogLevel.Error, text);
392
+ if (currentLogLevel !== 0 && 1 <= currentLogLevel && currentUserOnLog) currentUserOnLog(1, text);
389
393
  }
390
394
  });
391
- mod.logLevel = LogLevel.None;
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 ?? LogLevel.None;
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) return 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
- mod.collect_resources(instancePtr, processedHtml, width, height);
580
- const json = mod.get_pending_resources(instancePtr);
581
- if (!json) break;
582
- const pending = JSON.parse(json).filter((r) => {
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 result = mod.render(instancePtr, processedHtmls, width, height, {
705
+ const formatMap = {
646
706
  svg: 0,
647
707
  png: 1,
648
708
  webp: 2,
649
709
  pdf: 3
650
- }[format] ?? 0, {
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") return new TextDecoder().decode(result);
669
- return new Uint8Array(result);
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.9",
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.1",
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.0-rc.11",
130
- "typescript": "^5.9.3",
131
- "vitest": "^4.1.0"
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"