@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 +64 -40
- package/dist/snapdom.js +58 -32
- package/dist/snapdom.min.js +3 -3
- package/dist/snapdom.min.mjs +4 -4
- package/dist/snapdom.mjs +58 -32
- package/package.json +1 -1
- package/types/snapdom.d.ts +100 -56
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
|
-
##
|
|
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 |
|
|
351
|
-
| ------------------------ |
|
|
352
|
-
| Small (200×100) | **0.
|
|
353
|
-
| Modal (400×300) | **0.
|
|
354
|
-
| Page View (1200×800) | **0.
|
|
355
|
-
| Large Scroll (2000×1500) | **0.
|
|
356
|
-
| Very Large (4000×2000) | **0.
|
|
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 |
|
|
363
|
-
| ------------------------ |
|
|
364
|
-
| Small (200×100) | **1.
|
|
365
|
-
| Modal (400×300) | **
|
|
366
|
-
| Page View (1200×800) | **
|
|
367
|
-
| Large Scroll (2000×1500) | **
|
|
368
|
-
| Very Large (4000×2000) | **
|
|
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
|
-
* [
|
|
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
|
-
* [
|
|
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
|
-
##
|
|
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
|
|
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
|
-
(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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) =>
|
|
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,
|