@zumer/snapdom 1.9.7 → 1.9.8

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
@@ -1,8 +1,6 @@
1
- # snapDOM
2
-
3
1
  <p align="center">
4
2
  <a href="http://zumerlab.github.io/snapdom">
5
- <img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/hero.png" width="80%">
3
+ <img src="https://raw.githubusercontent.com/zumerlab/snapdom/main/docs/assets/newhero.png" width="70%">
6
4
  </a>
7
5
  </p>
8
6
 
@@ -28,13 +26,15 @@
28
26
  </a>
29
27
  </p>
30
28
 
29
+ # snapDOM
30
+
31
31
  **snapDOM** is a fast and accurate DOM-to-image capture tool built for **Zumly**, a zoom-based view transition framework.
32
32
 
33
33
  It captures any HTML element as a scalable SVG image, preserving styles, fonts, background images, pseudo-elements, and even shadow DOM. It also supports export to raster image formats and canvas.
34
34
 
35
35
  * 📸 Full DOM capture
36
36
  * 🎨 Embedded styles, pseudo-elements, and fonts
37
- * 🖼️ Export to SVG, PNG, JPG, WebP, or `canvas`
37
+ * 🖼️ Export to SVG, PNG, JPG, WebP, `canvas`, or Blob
38
38
  * ⚡ Ultra fast, no dependencies
39
39
  * 📦 100% based on standard Web APIs
40
40
 
@@ -54,6 +54,12 @@ npm i @zumer/snapdom
54
54
  yarn add @zumer/snapdom
55
55
  ```
56
56
 
57
+ Then import it in your code:
58
+
59
+ ```js
60
+ import { snapdom } from '@zumer/snapdom';
61
+ ```
62
+
57
63
  ### CDN
58
64
 
59
65
  ```html
@@ -146,6 +152,7 @@ All capture methods accept an `options` object:
146
152
  | `fast` | boolean | `true` | Skips idle delay for faster results |
147
153
  | `embedFonts` | boolean | `false` | Inlines fonts (icon fonts always embedded) |
148
154
  | `scale` | number | `1` | Output scale multiplier |
155
+ | `dpr` | number | `devicePixelRatio` | Device pixel ratio |
149
156
  | `width` | number | - | Output specific width size |
150
157
  | `height` | number | - | Output specific height size |
151
158
  | `backgroundColor` | string | `"#fff"` | Fallback color for JPG/WebP |
@@ -217,7 +224,7 @@ const result = await snapdom(element, {
217
224
 
218
225
  ### `preCache()` – Optional helper
219
226
 
220
- The `preCache()` function can be used to load external resources (like images and fonts) in advance. It is specially useful when the element to capure is big and complex.
227
+ The `preCache()` function can be used to load external resources (like images and fonts) in advance. It is specially useful when the element to capture is big and complex.
221
228
 
222
229
  ```js
223
230
  import { preCache } from '@zumer/snapdom';
@@ -250,7 +257,7 @@ import { snapdom, preCache } from './snapdom.mjs';
250
257
 
251
258
  ## Limitations
252
259
 
253
- * External images shloud be CORS-accessible (use `useProxy` option for handling CORS denied)
260
+ * External images should be CORS-accessible (use `useProxy` option for handling CORS denied)
254
261
  * Iframes are not supported
255
262
  * When WebP format is used on Safari, it will fallback to PNG rendering.
256
263
  * `@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)
@@ -311,6 +318,36 @@ npm install
311
318
  npm run test:benchmark
312
319
  ```
313
320
 
321
+
322
+ ## Roadmap
323
+
324
+ Planned improvements for future versions of SnapDOM:
325
+
326
+ * [ ] **Implement plugin system**
327
+ SnapDOM will support external plugins to extend or override internal behavior (e.g. custom node transformers, exporters, or filters).
328
+
329
+ * [ ] **Refactor to modular architecture**
330
+ Internal logic will be split into smaller, focused modules to improve maintainability and code reuse.
331
+
332
+ * [X] **Decouple internal logic from global options**
333
+ 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)
334
+
335
+ * [ ] **Expose cache control**
336
+ Users will be able to manually clear image and font caches or configure their own caching strategies.
337
+
338
+ * [ ] **Auto font preloading**
339
+ Required fonts will be automatically detected and preloaded before capture, reducing the need for manual `preCache()` calls.
340
+
341
+ * [ ] **Document plugin development**
342
+ A full guide will be provided for creating and registering custom SnapDOM plugins.
343
+
344
+ * [ ] **Make export utilities tree-shakeable**
345
+ Export functions like `toPng`, `toJpg`, `toBlob`, etc. will be restructured into independent modules to support tree shaking and minimal builds.
346
+
347
+ Have ideas or feature requests?
348
+ Feel free to share suggestions or feedback in [GitHub Discussions](https://github.com/zumerlab/snapdom/discussions).
349
+
350
+
314
351
  ## Development
315
352
 
316
353
  To contribute or build snapDOM locally:
@@ -351,16 +388,21 @@ For detailed contribution guidelines, please see [CONTRIBUTING](https://github.c
351
388
  <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>
352
389
  <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>
353
390
  <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>
391
+ <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>
392
+ <a href="https://github.com/CHOYSEN" title="CHOYSEN"><img src="https://avatars.githubusercontent.com/u/25995358?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="CHOYSEN"/></a>
354
393
  <a href="https://github.com/pedrocateexte" title="pedrocateexte"><img src="https://avatars.githubusercontent.com/u/207524750?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="pedrocateexte"/></a>
355
394
  <a href="https://github.com/domialex" title="domialex"><img src="https://avatars.githubusercontent.com/u/4694217?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="domialex"/></a>
356
395
  <a href="https://github.com/elliots" title="elliots"><img src="https://avatars.githubusercontent.com/u/622455?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="elliots"/></a>
357
396
  <a href="https://github.com/jswhisperer" title="jswhisperer"><img src="https://avatars.githubusercontent.com/u/1177690?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="jswhisperer"/></a>
397
+ <a href="https://github.com/sharuzzaman" title="sharuzzaman"><img src="https://avatars.githubusercontent.com/u/7421941?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="sharuzzaman"/></a>
358
398
  <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>
359
399
  <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>
360
400
  <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>
361
401
  <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>
362
402
  <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>
403
+ <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>
363
404
  <a href="https://github.com/kohaiy" title="kohaiy"><img src="https://avatars.githubusercontent.com/u/15622127?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="kohaiy"/></a>
405
+ <a href="https://github.com/fu050409" title="fu050409"><img src="https://avatars.githubusercontent.com/u/46275354?v=4&s=100" style="border-radius:10px; width:60px; height:60px; object-fit:cover; margin:5px;" alt="fu050409"/></a>
364
406
  </p>
365
407
  <!-- CONTRIBUTORS:END -->
366
408
 
@@ -370,6 +412,10 @@ Special thanks to [@megaphonecolin](https://github.com/megaphonecolin) for suppo
370
412
 
371
413
  If you'd like to support this project too, you can [become a sponsor](https://github.com/sponsors/tinchox5).
372
414
 
415
+ ## Star History
416
+
417
+ [![Star History Chart](https://api.star-history.com/svg?repos=zumerlab/snapdom&type=Date)](https://www.star-history.com/#zumerlab/snapdom&Date)
418
+
373
419
  ## License
374
420
 
375
421
  MIT © Zumerlab
package/dist/snapdom.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.9.7
4
+ * v.1.9.8
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -130,7 +130,7 @@
130
130
  if (cache.defaultStyle.has(tagName)) {
131
131
  return cache.defaultStyle.get(tagName);
132
132
  }
133
- const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
133
+ const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
134
134
  if (skipTags.has(tagName)) {
135
135
  const empty = {};
136
136
  cache.defaultStyle.set(tagName, empty);
@@ -160,10 +160,14 @@
160
160
  cache.defaultStyle.set(tagName, defaults);
161
161
  return defaults;
162
162
  }
163
+ var IGNORED_PROPS = /* @__PURE__ */ new Set([
164
+ "-webkit-locale"
165
+ ]);
163
166
  function getStyleKey(snapshot, tagName, compress = false) {
164
167
  const entries = [];
165
168
  const defaultStyles = getDefaultStyleForTag(tagName);
166
169
  for (let [prop, value] of Object.entries(snapshot)) {
170
+ if (IGNORED_PROPS.has(prop)) continue;
167
171
  if (!compress) {
168
172
  if (value) {
169
173
  entries.push(`${prop}:${value}`);
@@ -213,6 +217,7 @@
213
217
  const classMap = /* @__PURE__ */ new Map();
214
218
  let counter = 1;
215
219
  for (const key of keySet) {
220
+ if (!key.trim()) continue;
216
221
  classMap.set(key, `c${counter++}`);
217
222
  }
218
223
  return classMap;
@@ -448,6 +453,7 @@
448
453
  // src/modules/styles.js
449
454
  function snapshotComputedStyleFull(style) {
450
455
  const result = {};
456
+ const computedVisibility = style.getPropertyValue("visibility");
451
457
  for (let i = 0; i < style.length; i++) {
452
458
  const prop = style[i];
453
459
  let val = style.getPropertyValue(prop);
@@ -456,6 +462,9 @@
456
462
  }
457
463
  result[prop] = val;
458
464
  }
465
+ if (computedVisibility === "hidden") {
466
+ result.opacity = "0";
467
+ }
459
468
  return result;
460
469
  }
461
470
  function inlineAllStyles(source, clone, compress) {
@@ -678,25 +687,38 @@
678
687
  return __async(this, null, function* () {
679
688
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
680
689
  const dpr = window.devicePixelRatio || 1;
681
- const tempCanvas = document.createElement("canvas");
682
- const tempCtx = tempCanvas.getContext("2d");
683
- tempCtx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
684
- const metrics = tempCtx.measureText(unicodeChar);
685
- const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
686
- const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
687
- const height = ascent + descent;
688
- const width = metrics.width;
690
+ yield document.fonts.ready;
691
+ const span = document.createElement("span");
692
+ span.textContent = unicodeChar;
693
+ span.style.position = "absolute";
694
+ span.style.visibility = "hidden";
695
+ span.style.fontFamily = `"${fontFamily}"`;
696
+ span.style.fontWeight = fontWeight || "normal";
697
+ span.style.fontSize = `${fontSize}px`;
698
+ span.style.lineHeight = "1";
699
+ span.style.whiteSpace = "nowrap";
700
+ span.style.padding = "0";
701
+ span.style.margin = "0";
702
+ document.body.appendChild(span);
703
+ const rect = span.getBoundingClientRect();
704
+ const width = Math.ceil(rect.width);
705
+ const height = Math.ceil(rect.height);
706
+ document.body.removeChild(span);
689
707
  const canvas = document.createElement("canvas");
690
- canvas.width = Math.ceil(width * dpr);
691
- canvas.height = Math.ceil(height * dpr);
708
+ canvas.width = width * dpr;
709
+ canvas.height = height * dpr;
692
710
  const ctx = canvas.getContext("2d");
693
711
  ctx.scale(dpr, dpr);
694
- ctx.font = tempCtx.font;
712
+ ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
695
713
  ctx.textAlign = "left";
696
- ctx.textBaseline = "alphabetic";
714
+ ctx.textBaseline = "top";
697
715
  ctx.fillStyle = color;
698
- ctx.fillText(unicodeChar, 0, ascent);
699
- return canvas.toDataURL();
716
+ ctx.fillText(unicodeChar, 0, 0);
717
+ return {
718
+ dataUrl: canvas.toDataURL(),
719
+ width,
720
+ height
721
+ };
700
722
  });
701
723
  }
702
724
  function isStylesheetLoaded(href) {
@@ -884,19 +906,25 @@
884
906
  }
885
907
 
886
908
  // src/modules/pseudo.js
887
- function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
909
+ function inlinePseudoElements(source, clone, options) {
888
910
  return __async(this, null, function* () {
911
+ var _a;
889
912
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
890
913
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
891
914
  try {
892
915
  const style = getStyle(source, pseudo);
893
916
  if (!style || typeof style[Symbol.iterator] !== "function") continue;
917
+ const isEmptyPseudo = style.content === "none" && style.backgroundImage === "none" && style.backgroundColor === "transparent" && (style.borderStyle === "none" || parseFloat(style.borderWidth) === 0) && (!style.transform || style.transform === "none") && style.display === "inline";
918
+ if (isEmptyPseudo) continue;
894
919
  if (pseudo === "::first-letter") {
895
920
  const normal = getComputedStyle(source);
896
921
  const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
897
922
  if (!isMeaningful) continue;
898
923
  const textNode = Array.from(clone.childNodes).find(
899
- (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
924
+ (n) => {
925
+ var _a2;
926
+ return n.nodeType === Node.TEXT_NODE && ((_a2 = n.textContent) == null ? void 0 : _a2.trim().length) > 0;
927
+ }
900
928
  );
901
929
  if (!textNode) continue;
902
930
  const text = textNode.textContent;
@@ -908,57 +936,55 @@
908
936
  span.textContent = first;
909
937
  span.dataset.snapdomPseudo = "::first-letter";
910
938
  const snapshot2 = snapshotComputedStyle(style);
911
- const key2 = getStyleKey(snapshot2, "span", compress);
939
+ const key2 = getStyleKey(snapshot2, "span", options);
912
940
  cache.preStyleMap.set(span, key2);
913
941
  const restNode = document.createTextNode(rest);
914
942
  clone.replaceChild(restNode, textNode);
915
943
  clone.insertBefore(span, restNode);
916
944
  continue;
917
945
  }
918
- const content = style.getPropertyValue("content");
919
- const bg = style.getPropertyValue("background-image");
920
- const bgColor = style.getPropertyValue("background-color");
921
- const fontFamily = style.getPropertyValue("font-family");
922
- const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
923
- const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
924
- const color = style.getPropertyValue("color") || "#000";
925
- const display = style.getPropertyValue("display");
926
- const width = parseFloat(style.getPropertyValue("width"));
927
- const height = parseFloat(style.getPropertyValue("height"));
928
- const borderStyle = style.getPropertyValue("border-style");
929
- const transform = style.getPropertyValue("transform");
946
+ const content = style.content;
947
+ const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
948
+ const bg = style.backgroundImage;
949
+ const bgColor = style.backgroundColor;
950
+ const fontFamily = style.fontFamily;
951
+ const fontSize = parseInt(style.fontSize) || 32;
952
+ const fontWeight = parseInt(style.fontWeight) || false;
953
+ const color = style.color || "#000";
954
+ const display = style.display;
955
+ const width = parseFloat(style.width);
956
+ const height = parseFloat(style.height);
957
+ const borderStyle = style.borderStyle;
958
+ const borderWidth = parseFloat(style.borderWidth);
959
+ const transform = style.transform;
930
960
  const isIconFont2 = isIconFont(fontFamily);
931
- let cleanContent;
932
- if (/counter\s*\(|counters\s*\(/.test(content)) {
933
- cleanContent = "- ";
934
- } else {
935
- cleanContent = parseContent(content);
936
- }
937
- const hasContent = content !== "none";
938
- const hasExplicitContent = hasContent && cleanContent !== "";
961
+ const hasExplicitContent = content !== "none" && cleanContent !== "";
939
962
  const hasBg = bg && bg !== "none";
940
963
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
941
964
  const hasBox = display !== "inline" && (width > 0 || height > 0);
942
- const hasBorder = borderStyle && borderStyle !== "none";
965
+ const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
943
966
  const hasTransform = transform && transform !== "none";
944
967
  const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
945
968
  if (!shouldRender) continue;
946
969
  const pseudoEl = document.createElement("span");
947
970
  pseudoEl.dataset.snapdomPseudo = pseudo;
971
+ pseudoEl.style.verticalAlign = "middle";
948
972
  const snapshot = snapshotComputedStyle(style);
949
- const key = getStyleKey(snapshot, "span", compress);
973
+ const key = getStyleKey(snapshot, "span", options);
950
974
  cache.preStyleMap.set(pseudoEl, key);
951
975
  if (isIconFont2 && cleanContent.length === 1) {
976
+ const { dataUrl, width: width2, height: height2 } = yield iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
952
977
  const imgEl = document.createElement("img");
953
- imgEl.src = yield iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
954
- imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
978
+ imgEl.src = dataUrl;
979
+ imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
955
980
  pseudoEl.appendChild(imgEl);
981
+ clone.dataset.snapdomHasIcon = "true";
956
982
  } else if (cleanContent.startsWith("url(")) {
957
983
  const rawUrl = extractURL(cleanContent);
958
- if (rawUrl && rawUrl.trim() !== "") {
984
+ if (rawUrl == null ? void 0 : rawUrl.trim()) {
959
985
  try {
960
986
  const imgEl = document.createElement("img");
961
- const dataUrl = yield fetchImage(safeEncodeURI(rawUrl, { useProxy }));
987
+ const dataUrl = yield fetchImage(safeEncodeURI(rawUrl), options);
962
988
  imgEl.src = dataUrl;
963
989
  imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
964
990
  pseudoEl.appendChild(imgEl);
@@ -972,19 +998,21 @@
972
998
  if (hasBg) {
973
999
  try {
974
1000
  const bgSplits = splitBackgroundImage(bg);
975
- const newBgParts = yield Promise.all(
976
- bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
977
- );
1001
+ const newBgParts = yield Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
978
1002
  pseudoEl.style.backgroundImage = newBgParts.join(", ");
979
1003
  } catch (e) {
980
1004
  console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
981
1005
  }
982
1006
  }
983
1007
  if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
984
- const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
1008
+ const hasContent2 = pseudoEl.childNodes.length > 0 || ((_a = pseudoEl.textContent) == null ? void 0 : _a.trim()) !== "";
985
1009
  const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
986
1010
  if (!hasVisibleBox) continue;
987
- pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
1011
+ if (pseudo === "::before") {
1012
+ clone.insertBefore(pseudoEl, clone.firstChild);
1013
+ } else {
1014
+ clone.appendChild(pseudoEl);
1015
+ }
988
1016
  } catch (e) {
989
1017
  console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
990
1018
  }
@@ -992,53 +1020,73 @@
992
1020
  const sChildren = Array.from(source.children);
993
1021
  const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
994
1022
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
995
- yield inlinePseudoElements(
996
- sChildren[i],
997
- cChildren[i],
998
- compress,
999
- embedFonts,
1000
- useProxy
1001
- );
1023
+ yield inlinePseudoElements(sChildren[i], cChildren[i], options);
1002
1024
  }
1003
1025
  });
1004
1026
  }
1005
1027
 
1006
1028
  // src/modules/svgDefs.js
1007
- function inlineExternalDef(root) {
1008
- if (!root) return;
1009
- const defsSources = document.querySelectorAll("svg > defs");
1010
- if (!defsSources.length) return;
1011
- root.querySelectorAll("svg").forEach((svg) => {
1012
- const uses = svg.querySelectorAll("use");
1013
- if (!uses.length) return;
1014
- const usedIds = /* @__PURE__ */ new Set();
1015
- uses.forEach((use) => {
1016
- const href = use.getAttribute("xlink:href") || use.getAttribute("href");
1017
- if (href && href.startsWith("#")) usedIds.add(href.slice(1));
1018
- });
1019
- if (!usedIds.size) return;
1020
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1021
- for (const id of usedIds) {
1022
- for (const source of defsSources) {
1023
- const def = source.querySelector(`#${CSS.escape(id)}`);
1024
- if (def) {
1025
- defs.appendChild(def.cloneNode(true));
1026
- break;
1029
+ function inlineExternalDefsAndSymbols(rootElement) {
1030
+ if (!rootElement) return;
1031
+ const usedIds = /* @__PURE__ */ new Set();
1032
+ rootElement.querySelectorAll("use").forEach((use) => {
1033
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
1034
+ if (href && href.startsWith("#")) {
1035
+ usedIds.add(href.slice(1));
1036
+ }
1037
+ });
1038
+ if (!usedIds.size) return;
1039
+ const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
1040
+ const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
1041
+ const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
1042
+ let container = rootElement.querySelector("svg.inline-defs-container");
1043
+ if (!container) {
1044
+ container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1045
+ container.setAttribute("aria-hidden", "true");
1046
+ container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
1047
+ container.classList.add("inline-defs-container");
1048
+ rootElement.insertBefore(container, rootElement.firstChild);
1049
+ }
1050
+ const existingIds = /* @__PURE__ */ new Set();
1051
+ rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
1052
+ existingIds.add(el.id);
1053
+ });
1054
+ usedIds.forEach((id) => {
1055
+ if (existingIds.has(id)) return;
1056
+ const symbol = globalSymbols.find((sym) => sym.id === id);
1057
+ if (symbol) {
1058
+ container.appendChild(symbol.cloneNode(true));
1059
+ existingIds.add(id);
1060
+ return;
1061
+ }
1062
+ for (const defs of globalDefs) {
1063
+ const defEl = defs.querySelector(`#${CSS.escape(id)}`);
1064
+ if (defEl) {
1065
+ let defsContainer = container.querySelector("defs");
1066
+ if (!defsContainer) {
1067
+ defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1068
+ container.appendChild(defsContainer);
1027
1069
  }
1070
+ defsContainer.appendChild(defEl.cloneNode(true));
1071
+ existingIds.add(id);
1072
+ break;
1028
1073
  }
1029
1074
  }
1030
- if (defs.childNodes.length) {
1031
- svg.insertBefore(defs, svg.firstChild);
1032
- }
1033
1075
  });
1034
1076
  }
1035
1077
 
1036
1078
  // src/core/prepare.js
1037
1079
  function prepareClone(_0) {
1038
1080
  return __async(this, arguments, function* (element, compress = false, embedFonts = false, options = {}) {
1039
- var _a;
1081
+ var _a, _b;
1040
1082
  let clone;
1041
1083
  let classCSS = "";
1084
+ stabilizeLayout(element);
1085
+ try {
1086
+ inlineExternalDefsAndSymbols(element);
1087
+ } catch (e) {
1088
+ console.warn("inlineExternal defs or symbol failed:", e);
1089
+ }
1042
1090
  try {
1043
1091
  clone = deepClone(element, compress, options, element);
1044
1092
  } catch (e) {
@@ -1050,21 +1098,24 @@
1050
1098
  } catch (e) {
1051
1099
  console.warn("inlinePseudoElements failed:", e);
1052
1100
  }
1053
- try {
1054
- inlineExternalDef(clone);
1055
- } catch (e) {
1056
- console.warn("inlineExternalDef failed:", e);
1057
- }
1058
1101
  if (compress) {
1059
1102
  const keyToClass = generateCSSClasses();
1060
1103
  classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
1061
1104
  for (const [node, key] of cache.preStyleMap.entries()) {
1062
1105
  if (node.tagName === "STYLE") continue;
1106
+ if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
1107
+ node.setAttribute("style", key.replace(/;/g, "; "));
1108
+ continue;
1109
+ }
1063
1110
  const className = keyToClass.get(key);
1064
1111
  if (className) node.classList.add(className);
1065
1112
  const bgImage = (_a = node.style) == null ? void 0 : _a.backgroundImage;
1066
- node.removeAttribute("style");
1113
+ const hasIcon = (_b = node.dataset) == null ? void 0 : _b.snapdomHasIcon;
1067
1114
  if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
1115
+ if (hasIcon) {
1116
+ node.style.verticalAlign = "middle";
1117
+ node.style.display = "inline";
1118
+ }
1068
1119
  }
1069
1120
  } else {
1070
1121
  for (const [node, key] of cache.preStyleMap.entries()) {
@@ -1115,6 +1166,18 @@
1115
1166
  return { clone, classCSS };
1116
1167
  });
1117
1168
  }
1169
+ function stabilizeLayout(element) {
1170
+ const style = getComputedStyle(element);
1171
+ const outlineStyle = style.outlineStyle;
1172
+ const outlineWidth = style.outlineWidth;
1173
+ const borderStyle = style.borderStyle;
1174
+ const borderWidth = style.borderWidth;
1175
+ const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
1176
+ const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
1177
+ if (outlineVisible && borderAbsent) {
1178
+ element.style.border = `${outlineWidth} solid transparent`;
1179
+ }
1180
+ }
1118
1181
 
1119
1182
  // src/modules/images.js
1120
1183
  function inlineImages(_0) {
@@ -1153,13 +1216,27 @@
1153
1216
  "mask-source",
1154
1217
  "mask-box-image-source",
1155
1218
  "mask-border-source",
1156
- "-webkit-mask-box-image-source"
1219
+ "-webkit-mask-box-image-source",
1220
+ "border-image",
1221
+ "border-image-source",
1222
+ "border-image-slice",
1223
+ "border-image-width",
1224
+ "border-image-outset",
1225
+ "border-image-repeat"
1157
1226
  ];
1158
1227
  while (queue.length) {
1159
1228
  const [srcNode, cloneNode] = queue.shift();
1160
1229
  const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
1161
1230
  if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
1231
+ const hasBorderImage = (() => {
1232
+ const bi = style.getPropertyValue("border-image");
1233
+ const bis = style.getPropertyValue("border-image-source");
1234
+ return bi && bi !== "none" || bis && bis !== "none";
1235
+ })();
1162
1236
  for (const prop of imageProps) {
1237
+ if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
1238
+ continue;
1239
+ }
1163
1240
  const val = style.getPropertyValue(prop);
1164
1241
  if (!val || val === "none") continue;
1165
1242
  const splits = splitBackgroundImage(val);
@@ -1288,13 +1365,15 @@
1288
1365
  }
1289
1366
 
1290
1367
  // src/api/snapdom.js
1291
- function toImg(_0, _1) {
1292
- return __async(this, arguments, function* (url, { dpr = 1, scale = 1 }) {
1368
+ function toImg(_0) {
1369
+ return __async(this, arguments, function* (url, { scale = 1 } = {}) {
1293
1370
  const img = new Image();
1294
1371
  img.src = url;
1295
1372
  yield img.decode();
1296
- img.width = img.width * scale;
1297
- img.height = img.height * scale;
1373
+ if (scale !== 1) {
1374
+ img.style.width = `${img.naturalWidth * scale}px`;
1375
+ img.style.height = `${img.naturalHeight * scale}px`;
1376
+ }
1298
1377
  return img;
1299
1378
  });
1300
1379
  }
@@ -1319,8 +1398,8 @@
1319
1398
  if (appended) img.remove();
1320
1399
  throw new Error("Image failed to load or has no dimensions");
1321
1400
  }
1322
- const width = img.width * scale;
1323
- const height = img.height * scale;
1401
+ const width = img.naturalWidth * scale;
1402
+ const height = img.naturalHeight * scale;
1324
1403
  const canvas = document.createElement("canvas");
1325
1404
  canvas.width = Math.ceil(width * dpr);
1326
1405
  canvas.height = Math.ceil(height * dpr);
@@ -1422,20 +1501,24 @@
1422
1501
  });
1423
1502
  }
1424
1503
  snapdom.capture = (_0, ..._1) => __async(null, [_0, ..._1], function* (el, options = {}) {
1504
+ var _a;
1425
1505
  const url = yield captureDOM(el, options);
1426
- const dpr = window.devicePixelRatio || 1;
1506
+ const dpr = (_a = options.dpr) != null ? _a : window.devicePixelRatio || 1;
1427
1507
  const scale = options.scale || 1;
1428
1508
  return {
1429
1509
  url,
1430
1510
  options,
1431
1511
  toRaw: () => url,
1432
- toImg: () => toImg(url, { dpr, scale }),
1433
- toCanvas: () => toCanvas(url, { dpr, scale }),
1434
- toBlob: (options2) => toBlob(url, __spreadValues({ dpr, scale }, options2)),
1435
- toPng: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "png"),
1436
- toJpg: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "jpeg"),
1437
- toWebp: (options2) => toRasterImg(url, __spreadValues({ dpr, scale }, options2), "webp"),
1438
- download: ({ format = "png", filename = "capture", backgroundColor } = {}) => download(url, { dpr, scale, backgroundColor, format, filename })
1512
+ toImg: (opts = {}) => toImg(url, __spreadValues({ dpr, scale }, opts)),
1513
+ toCanvas: (opts = {}) => toCanvas(url, __spreadValues({ dpr, scale }, opts)),
1514
+ toBlob: (opts = {}) => toBlob(url, __spreadValues({ dpr, scale }, opts)),
1515
+ toPng: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "png"),
1516
+ toJpg: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "jpeg"),
1517
+ toWebp: (opts = {}) => toRasterImg(url, __spreadValues({ dpr, scale }, opts), "webp"),
1518
+ download: (_b = {}) => {
1519
+ var _c = _b, { format = "png", filename = "snapDOM", backgroundColor } = _c, opts = __objRest(_c, ["format", "filename", "backgroundColor"]);
1520
+ return download(url, __spreadValues({ dpr, scale, format, filename, backgroundColor }, opts));
1521
+ }
1439
1522
  };
1440
1523
  });
1441
1524
  snapdom.toRaw = (el, options) => __async(null, null, function* () {