@zumer/snapdom 1.9.10-dev.2 → 1.9.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/README.md CHANGED
@@ -42,6 +42,46 @@ It captures any HTML element as a scalable SVG image, preserving styles, fonts,
42
42
 
43
43
  [https://zumerlab.github.io/snapdom/](https://zumerlab.github.io/snapdom/)
44
44
 
45
+
46
+ ## Table of Contents
47
+
48
+ - [Installation](#installation)
49
+ - [NPM / Yarn](#npm--yarn)
50
+ - [CDN](#cdn)
51
+ - [Script tag (local)](#script-tag-local)
52
+ - [ES Module](#es-module)
53
+ - [Module via CDN](#module-via-cdn)
54
+ - [Basic usage](#basic-usage)
55
+ - [Reusable capture](#reusable-capture)
56
+ - [One-step shortcuts](#one-step-shortcuts)
57
+ - [API](#api)
58
+ - [snapdom(el, options?)](#snapdomel-options)
59
+ - [Shortcut methods](#shortcut-methods)
60
+ - [Options](#options)
61
+ - [Fallback image on `<img>` load failure](#fallback-image-on-img-load-failure)
62
+ - [Dimensions (`scale`, `width`, `height`)](#dimensions-scale-width-height)
63
+ - [Cross-Origin Images & Fonts (`useProxy`)](#cross-origin-images--fonts-useproxy)
64
+ - [Fonts](#fonts)
65
+ - [embedFonts](#embedfonts)
66
+ - [localFonts](#localfonts)
67
+ - [iconFonts](#iconfonts)
68
+ - [excludeFonts](#excludefonts)
69
+ - [Filtering nodes: `exclude` vs `filter`](#filtering-nodes-exclude-vs-filter)
70
+ - [preCache() – Optional helper](#precache--optional-helper)
71
+ - [Cache control](#cache-control)
72
+ - [Limitations](#limitations)
73
+ - [⚡ Performance Benchmarks (Chromium)](#performance-benchmarks)
74
+ - [Simple elements](#simple-elements)
75
+ - [Complex elements](#complex-elements)
76
+ - [Run the benchmarks](#run-the-benchmarks)
77
+ - [Roadmap](#roadmap)
78
+ - [Development](#development)
79
+ - [Contributors 🙌](#contributors)
80
+ - [💖 Sponsors](#sponsors)
81
+ - [Star History](#star-history)
82
+ - [License](#license)
83
+
84
+
45
85
  ## Installation
46
86
 
47
87
  ### NPM / Yarn
@@ -335,50 +375,32 @@ await snapdom.toPng(el, { cache: 'disabled' });
335
375
  * `@font-face` CSS rule is well supported, but if need to use JS `FontFace()`, see this workaround [`#43`](https://github.com/zumerlab/snapdom/issues/43)
336
376
 
337
377
 
338
- ## ⚡ Performance Benchmarks
339
-
340
- Snapdom has received **significant performance improvements** since version `v1.8.0`. The following benchmarks compare:
341
-
342
- * **Snapdom (current)**
343
- * **Snapdom v1.8.0**
344
- * `html2canvas`
345
- * `html-to-image`
378
+ ## Performance Benchmarks
346
379
 
380
+ **Setup.** Vitest benchmarks on Chromium, repo tests. Hardware may affect results.
381
+ Values are **average capture time (ms)** → lower is better.
347
382
 
348
383
  ### Simple elements
349
384
 
350
- | Scenario | Snapdom (current) | Snapdom v1.8.0 | html2canvas | html-to-image |
351
- | ------------------------ | ----------------- | -------------- | ----------- | ------------- |
352
- | Small (200×100) | **0.4 ms** | 1.2 ms | 70.3 ms | 3.6 ms |
353
- | Modal (400×300) | **0.4 ms** | 1.1 ms | 68.8 ms | 3.6 ms |
354
- | Page View (1200×800) | **0.4 ms** | 1.0 ms | 100.5 ms | 3.4 ms |
355
- | Large Scroll (2000×1500) | **0.4 ms** | 1.0 ms | 153.1 ms | 3.4 ms |
356
- | Very Large (4000×2000) | **0.4 ms** | 1.0 ms | 278.9 ms | 4.3 ms |
357
-
385
+ | Scenario | SnapDOM v1.9.10 | SnapDOM v1.9.9 | html2canvas | html-to-image |
386
+ | ------------------------ | --------------- | -------------- | ----------- | ------------- |
387
+ | Small (200×100) | **0.5 ms** | 0.8 ms | 67.7 ms | 3.1 ms |
388
+ | Modal (400×300) | **0.5 ms** | 0.8 ms | 75.5 ms | 3.6 ms |
389
+ | Page View (1200×800) | **0.5 ms** | 0.8 ms | 114.2 ms | 3.3 ms |
390
+ | Large Scroll (2000×1500) | **0.5 ms** | 0.8 ms | 186.3 ms | 3.2 ms |
391
+ | Very Large (4000×2000) | **0.5 ms** | 0.9 ms | 425.9 ms | 3.3 ms |
358
392
 
393
+ ---
359
394
 
360
395
  ### Complex elements
361
396
 
362
- | Scenario | Snapdom (current) | Snapdom v1.8.0 | html2canvas | html-to-image |
363
- | ------------------------ | ----------------- | -------------- | ----------- | ------------- |
364
- | Small (200×100) | **1.1 ms** | 3.2 ms | 76.0 ms | 15.3 ms |
365
- | Modal (400×300) | **4.5 ms** | 14.0 ms | 133.2 ms | 55.4 ms |
366
- | Page View (1200×800) | **32.9 ms** | 113.6 ms | 303.4 ms | 369.1 ms |
367
- | Large Scroll (2000×1500) | **133.9 ms** | 387.4 ms | 594.4 ms | 1,163.0 ms |
368
- | Very Large (4000×2000) | **364.0 ms** | 1,200.4 ms | 1,380.8 ms | 3,023.9 ms |
369
-
370
-
371
-
372
- ### Summary
373
-
374
- * Snapdom (current) is **2×–6× faster** than `v1.8.0`
375
- * Up to **150× faster** than `html2canvas`
376
- * Up to **8× faster** than `html-to-image` in large scenarios
377
-
378
- <sub>Benchmarks run in Chromium using Vitest.<br>
379
- Hardware: MacBook Air 2018.<br>
380
- ⚠️ Performance may vary depending on device.</sub>
381
-
397
+ | Scenario | SnapDOM v1.9.10 | SnapDOM v1.9.9 | html2canvas | html-to-image |
398
+ | ------------------------ | --------------- | -------------- | ----------- | ------------- |
399
+ | Small (200×100) | **1.6 ms** | 3.3 ms | 68.0 ms | 14.3 ms |
400
+ | Modal (400×300) | **2.9 ms** | 6.8 ms | 87.5 ms | 34.8 ms |
401
+ | Page View (1200×800) | **17.5 ms** | 50.2 ms | 178.0 ms | 429.0 ms |
402
+ | Large Scroll (2000×1500) | **54.0 ms** | 201.8 ms | 735.2 ms | 984.2 ms |
403
+ | Very Large (4000×2000) | **171.4 ms** | 453.7 ms | 1,800.4 ms | 2,611.9 ms |
382
404
 
383
405
 
384
406
  ### Run the benchmarks
@@ -404,10 +426,10 @@ Planned improvements for future versions of SnapDOM:
404
426
  * [X] **Decouple internal logic from global options**
405
427
  Functions will be redesigned to avoid relying directly on `options`. A centralized capture context will improve clarity, autonomy, and testability. See [`next` branch](https://github.com/zumerlab/snapdom/tree/main)
406
428
 
407
- * [ ] **Expose cache control**
429
+ * [X] **Expose cache control**
408
430
  Users will be able to manually clear image and font caches or configure their own caching strategies.
409
431
 
410
- * [ ] **Auto font preloading**
432
+ * [X] **Auto font preloading**
411
433
  Required fonts will be automatically detected and preloaded before capture, reducing the need for manual `preCache()` calls.
412
434
 
413
435
  * [ ] **Document plugin development**
@@ -453,11 +475,12 @@ The main entry point is in `src/`, and output bundles are generated in the `dist
453
475
  For detailed contribution guidelines, please see [CONTRIBUTING](https://github.com/zumerlab/snapdom/blob/main/CONTRIBUTING.md).
454
476
 
455
477
 
456
- ## Contributors 🙌
478
+ ## Contributors
457
479
 
458
480
  <!-- CONTRIBUTORS:START -->
459
481
  <p>
460
482
  <a href="https://github.com/tinchox5" title="tinchox5"><img src="https://avatars.githubusercontent.com/u/11557901?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tinchox5"/></a>
483
+ <a href="https://github.com/Jarvis2018" title="Jarvis2018"><img src="https://avatars.githubusercontent.com/u/36788851?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="Jarvis2018"/></a>
461
484
  <a href="https://github.com/tarwin" title="tarwin"><img src="https://avatars.githubusercontent.com/u/646149?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="tarwin"/></a>
462
485
  <a href="https://github.com/17biubiu" title="17biubiu"><img src="https://avatars.githubusercontent.com/u/13295895?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="17biubiu"/></a>
463
486
  <a href="https://github.com/av01d" title="av01d"><img src="https://avatars.githubusercontent.com/u/6247646?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="av01d"/></a>
@@ -470,6 +493,7 @@ For detailed contribution guidelines, please see [CONTRIBUTING](https://github.c
470
493
  <a href="https://github.com/simon1uo" title="simon1uo"><img src="https://avatars.githubusercontent.com/u/60037549?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="simon1uo"/></a>
471
494
  <a href="https://github.com/titoBouzout" title="titoBouzout"><img src="https://avatars.githubusercontent.com/u/64156?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="titoBouzout"/></a>
472
495
  <a href="https://github.com/jhbae200" title="jhbae200"><img src="https://avatars.githubusercontent.com/u/20170610?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jhbae200"/></a>
496
+ <a href="https://github.com/xiaobai-web715" title="xiaobai-web715"><img src="https://avatars.githubusercontent.com/u/81091224?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="xiaobai-web715"/></a>
473
497
  <a href="https://github.com/miusuncle" title="miusuncle"><img src="https://avatars.githubusercontent.com/u/7549857?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="miusuncle"/></a>
474
498
  <a href="https://github.com/rbbydotdev" title="rbbydotdev"><img src="https://avatars.githubusercontent.com/u/101137670?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="rbbydotdev"/></a>
475
499
  <a href="https://github.com/zhanghaotian2018" title="zhanghaotian2018"><img src="https://avatars.githubusercontent.com/u/169218899?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="zhanghaotian2018"/></a>
@@ -478,7 +502,7 @@ For detailed contribution guidelines, please see [CONTRIBUTING](https://github.c
478
502
  </p>
479
503
  <!-- CONTRIBUTORS:END -->
480
504
 
481
- ## 💖 Sponsors
505
+ ## Sponsors
482
506
 
483
507
  Special thanks to [@megaphonecolin](https://github.com/megaphonecolin) for supporting this project!
484
508
 
package/dist/snapdom.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.9.10-dev.2
4
+ * v.1.9.10
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -721,22 +721,14 @@
721
721
  return Promise.all(childList.map((child) => {
722
722
  return new Promise((resolve) => {
723
723
  function deal() {
724
- idle(
725
- (deadline) => {
726
- if (deadline instanceof IdleDeadline) {
727
- if (deadline.timeRemaining()) {
728
- callback(child, resolve);
729
- } else {
730
- deal();
731
- }
732
- } else {
733
- callback(child, resolve);
734
- }
735
- },
736
- {
737
- fast
724
+ idle((deadline) => {
725
+ const hasIdleBudget = deadline && typeof deadline.timeRemaining === "function" ? deadline.timeRemaining() > 0 : true;
726
+ if (hasIdleBudget) {
727
+ callback(child, resolve);
728
+ } else {
729
+ deal();
738
730
  }
739
- );
731
+ }, { fast });
740
732
  }
741
733
  deal();
742
734
  });
@@ -1843,7 +1835,7 @@
1843
1835
  }
1844
1836
  return used;
1845
1837
  }
1846
- function ensureFontsReady(families, warmupRepetitions = 1) {
1838
+ function ensureFontsReady(families, warmupRepetitions = 2) {
1847
1839
  return __async(this, null, function* () {
1848
1840
  try {
1849
1841
  yield document.fonts.ready;
@@ -2375,17 +2367,40 @@
2375
2367
  function inlineBackgroundImages(_0, _1, _2) {
2376
2368
  return __async(this, arguments, function* (source, clone, styleCache, options = {}) {
2377
2369
  const queue = [[source, clone]];
2378
- const imageProps = [
2370
+ const URL_PROPS = [
2379
2371
  "background-image",
2372
+ // Mask shorthands & images (both standard and WebKit)
2380
2373
  "mask",
2381
2374
  "mask-image",
2375
+ "-webkit-mask",
2382
2376
  "-webkit-mask-image",
2377
+ // Mask sources (rare, but keep)
2383
2378
  "mask-source",
2384
2379
  "mask-box-image-source",
2385
2380
  "mask-border-source",
2386
2381
  "-webkit-mask-box-image-source",
2382
+ // Border image
2387
2383
  "border-image",
2388
- "border-image-source",
2384
+ "border-image-source"
2385
+ ];
2386
+ const MASK_LAYOUT_PROPS = [
2387
+ "mask-position",
2388
+ "mask-size",
2389
+ "mask-repeat",
2390
+ // WebKit variants
2391
+ "-webkit-mask-position",
2392
+ "-webkit-mask-size",
2393
+ "-webkit-mask-repeat",
2394
+ // Extra (optional but helpful across engines)
2395
+ "mask-origin",
2396
+ "mask-clip",
2397
+ "-webkit-mask-origin",
2398
+ "-webkit-mask-clip",
2399
+ // Some engines expose X/Y position separately:
2400
+ "-webkit-mask-position-x",
2401
+ "-webkit-mask-position-y"
2402
+ ];
2403
+ const BORDER_AUX_PROPS = [
2389
2404
  "border-image-slice",
2390
2405
  "border-image-width",
2391
2406
  "border-image-outset",
@@ -2400,10 +2415,7 @@
2400
2415
  const bis = style.getPropertyValue("border-image-source");
2401
2416
  return bi && bi !== "none" || bis && bis !== "none";
2402
2417
  })();
2403
- for (const prop of imageProps) {
2404
- if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
2405
- continue;
2406
- }
2418
+ for (const prop of URL_PROPS) {
2407
2419
  const val = style.getPropertyValue(prop);
2408
2420
  if (!val || val === "none") continue;
2409
2421
  const splits = splitBackgroundImage(val);
@@ -2414,6 +2426,18 @@
2414
2426
  cloneNode.style.setProperty(prop, inlined.join(", "));
2415
2427
  }
2416
2428
  }
2429
+ for (const prop of MASK_LAYOUT_PROPS) {
2430
+ const val = style.getPropertyValue(prop);
2431
+ if (!val || val === "initial") continue;
2432
+ cloneNode.style.setProperty(prop, val);
2433
+ }
2434
+ if (hasBorderImage) {
2435
+ for (const prop of BORDER_AUX_PROPS) {
2436
+ const val = style.getPropertyValue(prop);
2437
+ if (!val || val === "initial") continue;
2438
+ cloneNode.style.setProperty(prop, val);
2439
+ }
2440
+ }
2417
2441
  const sChildren = Array.from(srcNode.children);
2418
2442
  const cChildren = Array.from(cloneNode.children);
2419
2443
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
@@ -2456,7 +2480,7 @@
2456
2480
  const families = new Set(
2457
2481
  Array.from(required).map((k) => String(k).split("__")[0]).filter(Boolean)
2458
2482
  );
2459
- yield ensureFontsReady(families, 2);
2483
+ yield ensureFontsReady(families, 5);
2460
2484
  }
2461
2485
  fontsCSS = yield embedCustomFonts({
2462
2486
  required,
@@ -2715,8 +2739,6 @@
2715
2739
 
2716
2740
  // src/core/context.js
2717
2741
  function normalizeCachePolicy(v) {
2718
- if (v === true) return "soft";
2719
- if (v === false) return "disabled";
2720
2742
  if (typeof v === "string") {
2721
2743
  const s = v.toLowerCase().trim();
2722
2744
  if (s === "disabled" || s === "full" || s === "auto" || s === "soft") return (
@@ -2886,9 +2908,7 @@
2886
2908
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2887
2909
  if (!context.snap) {
2888
2910
  context.snap = {
2889
- /** Delegates to this module’s PNG path (tu build devuelve <img>). */
2890
2911
  toPng: (el, opts) => snapdom.toPng(el, opts),
2891
- /** Útil si en algún flujo necesitás vector o <img> directo. */
2892
2912
  toImg: (el, opts) => snapdom.toImg(el, opts)
2893
2913
  };
2894
2914
  }
@@ -2896,12 +2916,18 @@
2896
2916
  });
2897
2917
  }
2898
2918
  snapdom.capture = (el, context, _token) => __async(null, null, function* () {
2899
- if (_token !== INTERNAL_TOKEN) {
2900
- throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
2901
- }
2919
+ if (_token !== INTERNAL_TOKEN) throw new Error("[snapdom.capture] is internal. Use snapdom(...) instead.");
2902
2920
  const url = yield captureDOM(el, context);
2903
2921
  const ensureContext = (opts) => __spreadValues(__spreadValues({}, context), opts || {});
2904
- const withFormat = (format) => (opts) => rasterize(url, ensureContext(__spreadProps(__spreadValues({}, opts || {}), { format })));
2922
+ const withFormat = (format) => (opts) => {
2923
+ const next = ensureContext(__spreadProps(__spreadValues({}, opts || {}), { format }));
2924
+ const wantsJpeg = format === "jpeg" || format === "jpg";
2925
+ const noBg = next.backgroundColor == null || next.backgroundColor === "transparent";
2926
+ if (wantsJpeg && noBg) {
2927
+ next.backgroundColor = "#ffffff";
2928
+ }
2929
+ return rasterize(url, next);
2930
+ };
2905
2931
  return {
2906
2932
  url,
2907
2933
  toRaw: () => url,