@zumer/snapdom 1.9.6 → 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/dist/snapdom.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.9.6
4
+ * v.1.9.8
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -81,7 +81,7 @@ function getDefaultStyleForTag(tagName) {
81
81
  if (cache.defaultStyle.has(tagName)) {
82
82
  return cache.defaultStyle.get(tagName);
83
83
  }
84
- const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template"]);
84
+ const skipTags = /* @__PURE__ */ new Set(["script", "style", "meta", "link", "noscript", "template", "defs", "symbol", "title", "metadata", "desc"]);
85
85
  if (skipTags.has(tagName)) {
86
86
  const empty = {};
87
87
  cache.defaultStyle.set(tagName, empty);
@@ -111,10 +111,14 @@ function getDefaultStyleForTag(tagName) {
111
111
  cache.defaultStyle.set(tagName, defaults);
112
112
  return defaults;
113
113
  }
114
+ var IGNORED_PROPS = /* @__PURE__ */ new Set([
115
+ "-webkit-locale"
116
+ ]);
114
117
  function getStyleKey(snapshot, tagName, compress = false) {
115
118
  const entries = [];
116
119
  const defaultStyles = getDefaultStyleForTag(tagName);
117
120
  for (let [prop, value] of Object.entries(snapshot)) {
121
+ if (IGNORED_PROPS.has(prop)) continue;
118
122
  if (!compress) {
119
123
  if (value) {
120
124
  entries.push(`${prop}:${value}`);
@@ -164,6 +168,7 @@ function generateCSSClasses() {
164
168
  const classMap = /* @__PURE__ */ new Map();
165
169
  let counter = 1;
166
170
  for (const key of keySet) {
171
+ if (!key.trim()) continue;
167
172
  classMap.set(key, `c${counter++}`);
168
173
  }
169
174
  return classMap;
@@ -395,6 +400,7 @@ function splitBackgroundImage(bg) {
395
400
  // src/modules/styles.js
396
401
  function snapshotComputedStyleFull(style) {
397
402
  const result = {};
403
+ const computedVisibility = style.getPropertyValue("visibility");
398
404
  for (let i = 0; i < style.length; i++) {
399
405
  const prop = style[i];
400
406
  let val = style.getPropertyValue(prop);
@@ -403,6 +409,9 @@ function snapshotComputedStyleFull(style) {
403
409
  }
404
410
  result[prop] = val;
405
411
  }
412
+ if (computedVisibility === "hidden") {
413
+ result.opacity = "0";
414
+ }
406
415
  return result;
407
416
  }
408
417
  function inlineAllStyles(source, clone, compress) {
@@ -519,7 +528,6 @@ function deepClone(node, compress, options = {}, originalRoot) {
519
528
  if (node.checked) clone.setAttribute("checked", "");
520
529
  if (node.indeterminate) clone.indeterminate = node.indeterminate;
521
530
  }
522
- return clone;
523
531
  }
524
532
  if (node instanceof HTMLSelectElement) {
525
533
  pendingSelectValue = node.value;
@@ -623,25 +631,38 @@ function isIconFont(input) {
623
631
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
624
632
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
625
633
  const dpr = window.devicePixelRatio || 1;
626
- const tempCanvas = document.createElement("canvas");
627
- const tempCtx = tempCanvas.getContext("2d");
628
- tempCtx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
629
- const metrics = tempCtx.measureText(unicodeChar);
630
- const ascent = metrics.actualBoundingBoxAscent || fontSize * 0.8;
631
- const descent = metrics.actualBoundingBoxDescent || fontSize * 0.2;
632
- const height = ascent + descent;
633
- const width = metrics.width;
634
+ await document.fonts.ready;
635
+ const span = document.createElement("span");
636
+ span.textContent = unicodeChar;
637
+ span.style.position = "absolute";
638
+ span.style.visibility = "hidden";
639
+ span.style.fontFamily = `"${fontFamily}"`;
640
+ span.style.fontWeight = fontWeight || "normal";
641
+ span.style.fontSize = `${fontSize}px`;
642
+ span.style.lineHeight = "1";
643
+ span.style.whiteSpace = "nowrap";
644
+ span.style.padding = "0";
645
+ span.style.margin = "0";
646
+ document.body.appendChild(span);
647
+ const rect = span.getBoundingClientRect();
648
+ const width = Math.ceil(rect.width);
649
+ const height = Math.ceil(rect.height);
650
+ document.body.removeChild(span);
634
651
  const canvas = document.createElement("canvas");
635
- canvas.width = Math.ceil(width * dpr);
636
- canvas.height = Math.ceil(height * dpr);
652
+ canvas.width = width * dpr;
653
+ canvas.height = height * dpr;
637
654
  const ctx = canvas.getContext("2d");
638
655
  ctx.scale(dpr, dpr);
639
- ctx.font = tempCtx.font;
656
+ ctx.font = fontWeight ? `${fontWeight} ${fontSize}px "${fontFamily}"` : `${fontSize}px "${fontFamily}"`;
640
657
  ctx.textAlign = "left";
641
- ctx.textBaseline = "alphabetic";
658
+ ctx.textBaseline = "top";
642
659
  ctx.fillStyle = color;
643
- ctx.fillText(unicodeChar, 0, ascent);
644
- return canvas.toDataURL();
660
+ ctx.fillText(unicodeChar, 0, 0);
661
+ return {
662
+ dataUrl: canvas.toDataURL(),
663
+ width,
664
+ height
665
+ };
645
666
  }
646
667
  function isStylesheetLoaded(href) {
647
668
  return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
@@ -740,8 +761,8 @@ async function embedCustomFonts({ preCached = false } = {}) {
740
761
  if (!src || isIconFont(family)) continue;
741
762
  const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
742
763
  const localRegex = /local\((["']?)[^)]+?\1\)/g;
743
- const hasURL = urlRegex.test(src);
744
- const hasLocal = localRegex.test(src);
764
+ const hasURL = !!src.match(urlRegex);
765
+ const hasLocal = !!src.match(localRegex);
745
766
  if (!hasURL && hasLocal) {
746
767
  finalCSS += `@font-face{font-family:${family};src:${src};font-style:${rule.style.getPropertyValue("font-style") || "normal"};font-weight:${rule.style.getPropertyValue("font-weight") || "normal"};}`;
747
768
  continue;
@@ -826,18 +847,20 @@ async function embedCustomFonts({ preCached = false } = {}) {
826
847
  }
827
848
 
828
849
  // src/modules/pseudo.js
829
- async function inlinePseudoElements(source, clone, compress, embedFonts = false, useProxy) {
850
+ async function inlinePseudoElements(source, clone, options) {
830
851
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
831
852
  for (const pseudo of ["::before", "::after", "::first-letter"]) {
832
853
  try {
833
854
  const style = getStyle(source, pseudo);
834
855
  if (!style || typeof style[Symbol.iterator] !== "function") continue;
856
+ 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";
857
+ if (isEmptyPseudo) continue;
835
858
  if (pseudo === "::first-letter") {
836
859
  const normal = getComputedStyle(source);
837
860
  const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
838
861
  if (!isMeaningful) continue;
839
862
  const textNode = Array.from(clone.childNodes).find(
840
- (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
863
+ (n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim().length > 0
841
864
  );
842
865
  if (!textNode) continue;
843
866
  const text = textNode.textContent;
@@ -848,74 +871,83 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
848
871
  const span = document.createElement("span");
849
872
  span.textContent = first;
850
873
  span.dataset.snapdomPseudo = "::first-letter";
851
- const snapshot = snapshotComputedStyle(style);
852
- const key = getStyleKey(snapshot, "span", compress);
853
- cache.preStyleMap.set(span, key);
874
+ const snapshot2 = snapshotComputedStyle(style);
875
+ const key2 = getStyleKey(snapshot2, "span", options);
876
+ cache.preStyleMap.set(span, key2);
854
877
  const restNode = document.createTextNode(rest);
855
878
  clone.replaceChild(restNode, textNode);
856
879
  clone.insertBefore(span, restNode);
857
880
  continue;
858
881
  }
859
- const content = style.getPropertyValue("content");
860
- const bg = style.getPropertyValue("background-image");
861
- const bgColor = style.getPropertyValue("background-color");
862
- const hasContent = content !== "none";
882
+ const content = style.content;
883
+ const cleanContent = /counter\s*\(|counters\s*\(/.test(content) ? "- " : parseContent(content);
884
+ const bg = style.backgroundImage;
885
+ const bgColor = style.backgroundColor;
886
+ const fontFamily = style.fontFamily;
887
+ const fontSize = parseInt(style.fontSize) || 32;
888
+ const fontWeight = parseInt(style.fontWeight) || false;
889
+ const color = style.color || "#000";
890
+ const display = style.display;
891
+ const width = parseFloat(style.width);
892
+ const height = parseFloat(style.height);
893
+ const borderStyle = style.borderStyle;
894
+ const borderWidth = parseFloat(style.borderWidth);
895
+ const transform = style.transform;
896
+ const isIconFont2 = isIconFont(fontFamily);
897
+ const hasExplicitContent = content !== "none" && cleanContent !== "";
863
898
  const hasBg = bg && bg !== "none";
864
899
  const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
865
- if (hasContent || hasBg || hasBgColor) {
866
- const fontFamily = style.getPropertyValue("font-family");
867
- const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
868
- const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
869
- const color = style.getPropertyValue("color") || "#000";
870
- const pseudoEl = document.createElement("span");
871
- pseudoEl.dataset.snapdomPseudo = pseudo;
872
- const snapshot = snapshotComputedStyle(style);
873
- const key = getStyleKey(snapshot, "span", compress);
874
- cache.preStyleMap.set(pseudoEl, key);
875
- const isIconFont2 = isIconFont(fontFamily);
876
- let cleanContent;
877
- if (/counter\s*\(|counters\s*\(/.test(content)) {
878
- cleanContent = "- ";
879
- } else {
880
- cleanContent = parseContent(content);
881
- }
882
- if (isIconFont2 && cleanContent.length === 1) {
883
- const imgEl = document.createElement("img");
884
- imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
885
- imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
886
- pseudoEl.appendChild(imgEl);
887
- } else if (cleanContent.startsWith("url(")) {
888
- const rawUrl = extractURL(cleanContent);
889
- if (rawUrl && rawUrl.trim() !== "") {
890
- try {
891
- const imgEl = document.createElement("img");
892
- const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
893
- imgEl.src = dataUrl;
894
- imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
895
- pseudoEl.appendChild(imgEl);
896
- } catch (e) {
897
- console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
898
- }
899
- }
900
- } else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
901
- pseudoEl.textContent = cleanContent;
902
- }
903
- if (hasBg) {
900
+ const hasBox = display !== "inline" && (width > 0 || height > 0);
901
+ const hasBorder = borderStyle && borderStyle !== "none" && borderWidth > 0;
902
+ const hasTransform = transform && transform !== "none";
903
+ const shouldRender = hasExplicitContent || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
904
+ if (!shouldRender) continue;
905
+ const pseudoEl = document.createElement("span");
906
+ pseudoEl.dataset.snapdomPseudo = pseudo;
907
+ pseudoEl.style.verticalAlign = "middle";
908
+ const snapshot = snapshotComputedStyle(style);
909
+ const key = getStyleKey(snapshot, "span", options);
910
+ cache.preStyleMap.set(pseudoEl, key);
911
+ if (isIconFont2 && cleanContent.length === 1) {
912
+ const { dataUrl, width: width2, height: height2 } = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
913
+ const imgEl = document.createElement("img");
914
+ imgEl.src = dataUrl;
915
+ imgEl.style = `height:${fontSize}px;width:${width2 / height2 * fontSize}px;object-fit:contain;`;
916
+ pseudoEl.appendChild(imgEl);
917
+ clone.dataset.snapdomHasIcon = "true";
918
+ } else if (cleanContent.startsWith("url(")) {
919
+ const rawUrl = extractURL(cleanContent);
920
+ if (rawUrl?.trim()) {
904
921
  try {
905
- const bgSplits = splitBackgroundImage(bg);
906
- const newBgParts = await Promise.all(
907
- bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
908
- );
909
- pseudoEl.style.backgroundImage = newBgParts.join(", ");
922
+ const imgEl = document.createElement("img");
923
+ const dataUrl = await fetchImage(safeEncodeURI(rawUrl), options);
924
+ imgEl.src = dataUrl;
925
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
926
+ pseudoEl.appendChild(imgEl);
910
927
  } catch (e) {
911
- console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
928
+ console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
912
929
  }
913
930
  }
914
- if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
915
- const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
916
- const hasVisibleBox = hasContent2 || hasBg || hasBgColor;
917
- if (!hasVisibleBox) continue;
918
- pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
931
+ } else if (!isIconFont2 && hasExplicitContent) {
932
+ pseudoEl.textContent = cleanContent;
933
+ }
934
+ if (hasBg) {
935
+ try {
936
+ const bgSplits = splitBackgroundImage(bg);
937
+ const newBgParts = await Promise.all(bgSplits.map(inlineSingleBackgroundEntry));
938
+ pseudoEl.style.backgroundImage = newBgParts.join(", ");
939
+ } catch (e) {
940
+ console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
941
+ }
942
+ }
943
+ if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
944
+ const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent?.trim() !== "";
945
+ const hasVisibleBox = hasContent2 || hasBg || hasBgColor || hasBox || hasBorder || hasTransform;
946
+ if (!hasVisibleBox) continue;
947
+ if (pseudo === "::before") {
948
+ clone.insertBefore(pseudoEl, clone.firstChild);
949
+ } else {
950
+ clone.appendChild(pseudoEl);
919
951
  }
920
952
  } catch (e) {
921
953
  console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
@@ -924,43 +956,57 @@ async function inlinePseudoElements(source, clone, compress, embedFonts = false,
924
956
  const sChildren = Array.from(source.children);
925
957
  const cChildren = Array.from(clone.children).filter((child) => !child.dataset.snapdomPseudo);
926
958
  for (let i = 0; i < Math.min(sChildren.length, cChildren.length); i++) {
927
- await inlinePseudoElements(
928
- sChildren[i],
929
- cChildren[i],
930
- compress,
931
- embedFonts,
932
- useProxy
933
- );
959
+ await inlinePseudoElements(sChildren[i], cChildren[i], options);
934
960
  }
935
961
  }
936
962
 
937
963
  // src/modules/svgDefs.js
938
- function inlineExternalDef(root) {
939
- if (!root) return;
940
- const defsSources = document.querySelectorAll("svg > defs");
941
- if (!defsSources.length) return;
942
- root.querySelectorAll("svg").forEach((svg) => {
943
- const uses = svg.querySelectorAll("use");
944
- if (!uses.length) return;
945
- const usedIds = /* @__PURE__ */ new Set();
946
- uses.forEach((use) => {
947
- const href = use.getAttribute("xlink:href") || use.getAttribute("href");
948
- if (href && href.startsWith("#")) usedIds.add(href.slice(1));
949
- });
950
- if (!usedIds.size) return;
951
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
952
- for (const id of usedIds) {
953
- for (const source of defsSources) {
954
- const def = source.querySelector(`#${CSS.escape(id)}`);
955
- if (def) {
956
- defs.appendChild(def.cloneNode(true));
957
- break;
964
+ function inlineExternalDefsAndSymbols(rootElement) {
965
+ if (!rootElement) return;
966
+ const usedIds = /* @__PURE__ */ new Set();
967
+ rootElement.querySelectorAll("use").forEach((use) => {
968
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
969
+ if (href && href.startsWith("#")) {
970
+ usedIds.add(href.slice(1));
971
+ }
972
+ });
973
+ if (!usedIds.size) return;
974
+ const allGlobal = Array.from(document.querySelectorAll("svg > symbol, svg > defs"));
975
+ const globalSymbols = allGlobal.filter((el) => el.tagName.toLowerCase() === "symbol");
976
+ const globalDefs = allGlobal.filter((el) => el.tagName.toLowerCase() === "defs");
977
+ let container = rootElement.querySelector("svg.inline-defs-container");
978
+ if (!container) {
979
+ container = document.createElementNS("http://www.w3.org/2000/svg", "svg");
980
+ container.setAttribute("aria-hidden", "true");
981
+ container.setAttribute("style", "position: absolute; width: 0; height: 0; overflow: hidden;");
982
+ container.classList.add("inline-defs-container");
983
+ rootElement.insertBefore(container, rootElement.firstChild);
984
+ }
985
+ const existingIds = /* @__PURE__ */ new Set();
986
+ rootElement.querySelectorAll("symbol[id], defs > *[id]").forEach((el) => {
987
+ existingIds.add(el.id);
988
+ });
989
+ usedIds.forEach((id) => {
990
+ if (existingIds.has(id)) return;
991
+ const symbol = globalSymbols.find((sym) => sym.id === id);
992
+ if (symbol) {
993
+ container.appendChild(symbol.cloneNode(true));
994
+ existingIds.add(id);
995
+ return;
996
+ }
997
+ for (const defs of globalDefs) {
998
+ const defEl = defs.querySelector(`#${CSS.escape(id)}`);
999
+ if (defEl) {
1000
+ let defsContainer = container.querySelector("defs");
1001
+ if (!defsContainer) {
1002
+ defsContainer = document.createElementNS("http://www.w3.org/2000/svg", "defs");
1003
+ container.appendChild(defsContainer);
958
1004
  }
1005
+ defsContainer.appendChild(defEl.cloneNode(true));
1006
+ existingIds.add(id);
1007
+ break;
959
1008
  }
960
1009
  }
961
- if (defs.childNodes.length) {
962
- svg.insertBefore(defs, svg.firstChild);
963
- }
964
1010
  });
965
1011
  }
966
1012
 
@@ -968,6 +1014,12 @@ function inlineExternalDef(root) {
968
1014
  async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
969
1015
  let clone;
970
1016
  let classCSS = "";
1017
+ stabilizeLayout(element);
1018
+ try {
1019
+ inlineExternalDefsAndSymbols(element);
1020
+ } catch (e) {
1021
+ console.warn("inlineExternal defs or symbol failed:", e);
1022
+ }
971
1023
  try {
972
1024
  clone = deepClone(element, compress, options, element);
973
1025
  } catch (e) {
@@ -979,21 +1031,24 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
979
1031
  } catch (e) {
980
1032
  console.warn("inlinePseudoElements failed:", e);
981
1033
  }
982
- try {
983
- inlineExternalDef(clone);
984
- } catch (e) {
985
- console.warn("inlineExternalDef failed:", e);
986
- }
987
1034
  if (compress) {
988
1035
  const keyToClass = generateCSSClasses();
989
1036
  classCSS = Array.from(keyToClass.entries()).map(([key, className]) => `.${className}{${key}}`).join("");
990
1037
  for (const [node, key] of cache.preStyleMap.entries()) {
991
1038
  if (node.tagName === "STYLE") continue;
1039
+ if (node.getRootNode && node.getRootNode() instanceof ShadowRoot) {
1040
+ node.setAttribute("style", key.replace(/;/g, "; "));
1041
+ continue;
1042
+ }
992
1043
  const className = keyToClass.get(key);
993
1044
  if (className) node.classList.add(className);
994
1045
  const bgImage = node.style?.backgroundImage;
995
- node.removeAttribute("style");
1046
+ const hasIcon = node.dataset?.snapdomHasIcon;
996
1047
  if (bgImage && bgImage !== "none") node.style.backgroundImage = bgImage;
1048
+ if (hasIcon) {
1049
+ node.style.verticalAlign = "middle";
1050
+ node.style.display = "inline";
1051
+ }
997
1052
  }
998
1053
  } else {
999
1054
  for (const [node, key] of cache.preStyleMap.entries()) {
@@ -1043,6 +1098,18 @@ async function prepareClone(element, compress = false, embedFonts = false, optio
1043
1098
  }
1044
1099
  return { clone, classCSS };
1045
1100
  }
1101
+ function stabilizeLayout(element) {
1102
+ const style = getComputedStyle(element);
1103
+ const outlineStyle = style.outlineStyle;
1104
+ const outlineWidth = style.outlineWidth;
1105
+ const borderStyle = style.borderStyle;
1106
+ const borderWidth = style.borderWidth;
1107
+ const outlineVisible = outlineStyle !== "none" && parseFloat(outlineWidth) > 0;
1108
+ const borderAbsent = borderStyle === "none" || parseFloat(borderWidth) === 0;
1109
+ if (outlineVisible && borderAbsent) {
1110
+ element.style.border = `${outlineWidth} solid transparent`;
1111
+ }
1112
+ }
1046
1113
 
1047
1114
  // src/modules/images.js
1048
1115
  async function inlineImages(clone, options = {}) {
@@ -1078,13 +1145,27 @@ async function inlineBackgroundImages(source, clone, options = {}) {
1078
1145
  "mask-source",
1079
1146
  "mask-box-image-source",
1080
1147
  "mask-border-source",
1081
- "-webkit-mask-box-image-source"
1148
+ "-webkit-mask-box-image-source",
1149
+ "border-image",
1150
+ "border-image-source",
1151
+ "border-image-slice",
1152
+ "border-image-width",
1153
+ "border-image-outset",
1154
+ "border-image-repeat"
1082
1155
  ];
1083
1156
  while (queue.length) {
1084
1157
  const [srcNode, cloneNode] = queue.shift();
1085
1158
  const style = cache.preStyle.get(srcNode) || getStyle(srcNode);
1086
1159
  if (!cache.preStyle.has(srcNode)) cache.preStyle.set(srcNode, style);
1160
+ const hasBorderImage = (() => {
1161
+ const bi = style.getPropertyValue("border-image");
1162
+ const bis = style.getPropertyValue("border-image-source");
1163
+ return bi && bi !== "none" || bis && bis !== "none";
1164
+ })();
1087
1165
  for (const prop of imageProps) {
1166
+ if (["border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"].includes(prop) && !hasBorderImage) {
1167
+ continue;
1168
+ }
1088
1169
  const val = style.getPropertyValue(prop);
1089
1170
  if (!val || val === "none") continue;
1090
1171
  const splits = splitBackgroundImage(val);
@@ -1186,8 +1267,6 @@ async function captureDOM(element, options = {}) {
1186
1267
  const existingTransform = clone.style.transform || "";
1187
1268
  const scaleTransform = `scale(${scaleX}, ${scaleY})`;
1188
1269
  clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
1189
- } else if (hasScale && isSafari()) {
1190
- clone.style.scale = `${scale}`;
1191
1270
  }
1192
1271
  const svgNS = "http://www.w3.org/2000/svg";
1193
1272
  const fo = document.createElementNS(svgNS, "foreignObject");
@@ -1212,33 +1291,47 @@ async function captureDOM(element, options = {}) {
1212
1291
  }
1213
1292
 
1214
1293
  // src/api/snapdom.js
1215
- async function toImg(url, { dpr = 1, scale = 1 }) {
1294
+ async function toImg(url, { scale = 1 } = {}) {
1216
1295
  const img = new Image();
1217
1296
  img.src = url;
1218
1297
  await img.decode();
1219
- if (isSafari) {
1220
- img.width = img.width * scale;
1221
- img.height = img.height * scale;
1222
- } else {
1223
- img.width = img.width / scale;
1224
- img.height = img.height / scale;
1298
+ if (scale !== 1) {
1299
+ img.style.width = `${img.naturalWidth * scale}px`;
1300
+ img.style.height = `${img.naturalHeight * scale}px`;
1225
1301
  }
1226
1302
  return img;
1227
1303
  }
1228
1304
  async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
1229
1305
  const img = new Image();
1230
1306
  img.src = url;
1307
+ img.crossOrigin = "anonymous";
1308
+ img.loading = "eager";
1309
+ img.decoding = "sync";
1310
+ const isSafariBrowser = isSafari();
1311
+ let appended = false;
1312
+ if (isSafariBrowser) {
1313
+ document.body.appendChild(img);
1314
+ appended = true;
1315
+ }
1231
1316
  await img.decode();
1317
+ if (isSafariBrowser) {
1318
+ await new Promise((resolve) => setTimeout(resolve, 100));
1319
+ }
1320
+ if (img.width === 0 || img.height === 0) {
1321
+ if (appended) img.remove();
1322
+ throw new Error("Image failed to load or has no dimensions");
1323
+ }
1324
+ const width = img.naturalWidth * scale;
1325
+ const height = img.naturalHeight * scale;
1232
1326
  const canvas = document.createElement("canvas");
1233
- const width = img.width * scale;
1234
- const height = img.height * scale;
1235
1327
  canvas.width = Math.ceil(width * dpr);
1236
1328
  canvas.height = Math.ceil(height * dpr);
1329
+ canvas.style.width = `${width}px`;
1330
+ canvas.style.height = `${height}px`;
1237
1331
  const ctx = canvas.getContext("2d");
1238
1332
  ctx.scale(dpr, dpr);
1239
1333
  ctx.drawImage(img, 0, 0, width, height);
1240
- canvas.style.width = `${width}px`;
1241
- canvas.style.height = `${height}px`;
1334
+ if (appended) img.remove();
1242
1335
  return canvas;
1243
1336
  }
1244
1337
  async function toBlob(url, {
@@ -1285,7 +1378,7 @@ async function toRasterImg(url, { dpr = 1, scale = 1, backgroundColor, quality }
1285
1378
  img.style.height = `${canvas.height / dpr}px`;
1286
1379
  return img;
1287
1380
  }
1288
- async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", format = "png", filename = "capture" } = {}) {
1381
+ async function download(url, { dpr = 1, scale = 1, backgroundColor, format = "png", filename = "snapDOM" } = {}) {
1289
1382
  if (format === "svg") {
1290
1383
  const blob = await toBlob(url);
1291
1384
  const objectURL = URL.createObjectURL(blob);
@@ -1321,19 +1414,19 @@ async function snapdom(element, options = {}) {
1321
1414
  }
1322
1415
  snapdom.capture = async (el, options = {}) => {
1323
1416
  const url = await captureDOM(el, options);
1324
- const dpr = window.devicePixelRatio || 1;
1417
+ const dpr = options.dpr ?? (window.devicePixelRatio || 1);
1325
1418
  const scale = options.scale || 1;
1326
1419
  return {
1327
1420
  url,
1328
1421
  options,
1329
1422
  toRaw: () => url,
1330
- toImg: () => toImg(url, { dpr, scale }),
1331
- toCanvas: () => toCanvas(url, { dpr, scale }),
1332
- toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
1333
- toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
1334
- toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
1335
- toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
1336
- download: ({ format = "png", filename = "capture", backgroundColor } = {}) => download(url, { dpr, scale, backgroundColor, format, filename })
1423
+ toImg: (opts = {}) => toImg(url, { dpr, scale, ...opts }),
1424
+ toCanvas: (opts = {}) => toCanvas(url, { dpr, scale, ...opts }),
1425
+ toBlob: (opts = {}) => toBlob(url, { dpr, scale, ...opts }),
1426
+ toPng: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "png"),
1427
+ toJpg: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "jpeg"),
1428
+ toWebp: (opts = {}) => toRasterImg(url, { dpr, scale, ...opts }, "webp"),
1429
+ download: ({ format = "png", filename = "snapDOM", backgroundColor, ...opts } = {}) => download(url, { dpr, scale, format, filename, backgroundColor, ...opts })
1337
1430
  };
1338
1431
  };
1339
1432
  snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
4
4
  "description": "snapDOM captures HTML elements to images with exceptional speed and accuracy.",
5
5
  "main": "dist/snapdom.js",
6
6
  "module": "dist/snapdom.mjs",
@@ -58,6 +58,7 @@ declare module "@zumer/snapdom" {
58
58
  options?: {
59
59
  embedFonts?: boolean;
60
60
  useProxy?: string;
61
+ reset?: boolean;
61
62
  }
62
63
  ): Promise<void>;
63
64
  }