@zumer/snapdom 1.9.10-dev.1 → 1.9.10-dev.2

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.10-dev.1
4
+ * v.1.9.10-dev.2
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -42,10 +42,13 @@ function applyCachePolicy(policy = "soft") {
42
42
  cache.session.styleMap = /* @__PURE__ */ new Map();
43
43
  cache.session.nodeMap = /* @__PURE__ */ new Map();
44
44
  cache.session.styleCache = /* @__PURE__ */ new WeakMap();
45
- cache.image.clear();
46
- cache.background.clear();
47
- cache.resource.clear();
48
- cache.font.clear();
45
+ cache.computedStyle = /* @__PURE__ */ new WeakMap();
46
+ cache.baseStyle = /* @__PURE__ */ new Map();
47
+ cache.defaultStyle = /* @__PURE__ */ new Map();
48
+ cache.image = /* @__PURE__ */ new Map();
49
+ cache.background = /* @__PURE__ */ new Map();
50
+ cache.resource = /* @__PURE__ */ new Map();
51
+ cache.font = /* @__PURE__ */ new Set();
49
52
  return;
50
53
  }
51
54
  default: {
@@ -637,6 +640,11 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
637
640
  const ctx = _resolveCtx(sessionOrCtx, opts);
638
641
  const resetMode = ctx.options && ctx.options.cache || "auto";
639
642
  if (resetMode !== "disabled") setupInvalidationOnce(document.documentElement);
643
+ if (resetMode === "disabled" && !ctx.session.__bumpedForDisabled) {
644
+ bumpEpoch();
645
+ snapshotKeyCache.clear();
646
+ ctx.session.__bumpedForDisabled = true;
647
+ }
640
648
  if (NO_DEFAULTS_TAGS.has(source.tagName?.toLowerCase())) {
641
649
  const author = source.getAttribute?.("style");
642
650
  if (author) clone.setAttribute("style", author);
@@ -658,6 +666,31 @@ async function inlineAllStyles(source, clone, sessionOrCtx, opts) {
658
666
  }
659
667
 
660
668
  // src/core/clone.js
669
+ function idleCallback(childList, callback, fast) {
670
+ return Promise.all(childList.map((child) => {
671
+ return new Promise((resolve) => {
672
+ function deal() {
673
+ idle(
674
+ (deadline) => {
675
+ if (deadline instanceof IdleDeadline) {
676
+ if (deadline.timeRemaining()) {
677
+ callback(child, resolve);
678
+ } else {
679
+ deal();
680
+ }
681
+ } else {
682
+ callback(child, resolve);
683
+ }
684
+ },
685
+ {
686
+ fast
687
+ }
688
+ );
689
+ }
690
+ deal();
691
+ });
692
+ }));
693
+ }
661
694
  function addNotSlottedRightmost(sel) {
662
695
  sel = sel.trim();
663
696
  if (!sel) return sel;
@@ -779,10 +812,84 @@ function markSlottedSubtree(root) {
779
812
  root.querySelectorAll("*").forEach((el) => el.setAttribute("data-sd-slotted", ""));
780
813
  }
781
814
  }
782
- function deepClone(node, sessionCache, options) {
815
+ async function getAccessibleIframeDocument(iframe, attempts = 3) {
816
+ const probe = () => {
817
+ try {
818
+ return iframe.contentDocument || iframe.contentWindow?.document || null;
819
+ } catch {
820
+ return null;
821
+ }
822
+ };
823
+ let doc = probe();
824
+ let i = 0;
825
+ while (i < attempts && (!doc || !doc.body && !doc.documentElement)) {
826
+ await new Promise((r) => setTimeout(r, 0));
827
+ doc = probe();
828
+ i++;
829
+ }
830
+ return doc && (doc.body || doc.documentElement) ? doc : null;
831
+ }
832
+ function measureContentBox(el) {
833
+ const rect = el.getBoundingClientRect();
834
+ let bl = 0, br = 0, bt = 0, bb = 0;
835
+ try {
836
+ const cs = getComputedStyle(el);
837
+ bl = parseFloat(cs.borderLeftWidth) || 0;
838
+ br = parseFloat(cs.borderRightWidth) || 0;
839
+ bt = parseFloat(cs.borderTopWidth) || 0;
840
+ bb = parseFloat(cs.borderBottomWidth) || 0;
841
+ } catch {
842
+ }
843
+ const contentWidth = Math.max(0, Math.round(rect.width - (bl + br)));
844
+ const contentHeight = Math.max(0, Math.round(rect.height - (bt + bb)));
845
+ return { contentWidth, contentHeight, rect };
846
+ }
847
+ function pinIframeViewport(doc, w, h) {
848
+ const style = doc.createElement("style");
849
+ style.setAttribute("data-sd-iframe-pin", "");
850
+ style.textContent = `html, body {margin: 0 !important;padding: 0 !important;width: ${w}px !important;height: ${h}px !important;min-width: ${w}px !important;min-height: ${h}px !important;box-sizing: border-box !important;overflow: hidden !important;background-clip: border-box !important;}`;
851
+ (doc.head || doc.documentElement).appendChild(style);
852
+ return () => {
853
+ try {
854
+ style.remove();
855
+ } catch {
856
+ }
857
+ };
858
+ }
859
+ async function rasterizeIframe(iframe, sessionCache, options) {
860
+ const doc = await getAccessibleIframeDocument(iframe, 3);
861
+ if (!doc) throw new Error("iframe document not accessible/ready");
862
+ const { contentWidth, contentHeight, rect } = measureContentBox(iframe);
863
+ const snap = options?.snap;
864
+ if (!snap || typeof snap.toPng !== "function") {
865
+ throw new Error("snapdom.toPng not available in iframe or window");
866
+ }
867
+ const nested = { ...options, scale: 1 };
868
+ const unpin = pinIframeViewport(doc, contentWidth, contentHeight);
869
+ let imgEl;
870
+ try {
871
+ imgEl = await snap.toPng(doc.documentElement, nested);
872
+ } finally {
873
+ unpin();
874
+ }
875
+ imgEl.style.display = "block";
876
+ imgEl.style.width = `${contentWidth}px`;
877
+ imgEl.style.height = `${contentHeight}px`;
878
+ const wrapper = document.createElement("div");
879
+ sessionCache.nodeMap.set(wrapper, iframe);
880
+ inlineAllStyles(iframe, wrapper, sessionCache, options);
881
+ wrapper.style.overflow = "hidden";
882
+ wrapper.style.display = "block";
883
+ if (!wrapper.style.width) wrapper.style.width = `${Math.round(rect.width)}px`;
884
+ if (!wrapper.style.height) wrapper.style.height = `${Math.round(rect.height)}px`;
885
+ wrapper.appendChild(imgEl);
886
+ return wrapper;
887
+ }
888
+ async function deepClone(node, sessionCache, options) {
783
889
  if (!node) throw new Error("Invalid node");
784
890
  const clonedAssignedNodes = /* @__PURE__ */ new Set();
785
891
  let pendingSelectValue = null;
892
+ let pendingTextAreaValue = null;
786
893
  if (node.nodeType === Node.ELEMENT_NODE) {
787
894
  const tag = (node.localName || node.tagName || "").toLowerCase();
788
895
  if (node.id === "snapdom-sandbox" || node.hasAttribute("data-snapdom-sandbox")) {
@@ -831,6 +938,20 @@ function deepClone(node, sessionCache, options) {
831
938
  }
832
939
  }
833
940
  if (node.tagName === "IFRAME") {
941
+ let sameOrigin = false;
942
+ try {
943
+ sameOrigin = !!(node.contentDocument || node.contentWindow?.document);
944
+ } catch {
945
+ sameOrigin = false;
946
+ }
947
+ if (sameOrigin) {
948
+ try {
949
+ const wrapper = await rasterizeIframe(node, sessionCache, options);
950
+ return wrapper;
951
+ } catch (err) {
952
+ console.warn("[SnapDOM] iframe rasterization failed, fallback:", err);
953
+ }
954
+ }
834
955
  if (options.placeholders) {
835
956
  const fallback = document.createElement("div");
836
957
  fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
@@ -870,14 +991,31 @@ function deepClone(node, sessionCache, options) {
870
991
  sessionCache.nodeMap.set(clone, node);
871
992
  if (node.tagName === "IMG") {
872
993
  freezeImgSrcset(node, clone);
994
+ try {
995
+ const rect = node.getBoundingClientRect();
996
+ let w = Math.round(rect.width || 0);
997
+ let h = Math.round(rect.height || 0);
998
+ if (!w || !h) {
999
+ const computed = window.getComputedStyle(node);
1000
+ const cssW = parseFloat(computed.width) || 0;
1001
+ const cssH = parseFloat(computed.height) || 0;
1002
+ const attrW = parseInt(node.getAttribute("width") || "", 10) || 0;
1003
+ const attrH = parseInt(node.getAttribute("height") || "", 10) || 0;
1004
+ const propW = node.width || node.naturalWidth || 0;
1005
+ const propH = node.height || node.naturalHeight || 0;
1006
+ w = Math.round(w || cssW || attrW || propW || 0);
1007
+ h = Math.round(h || cssH || attrH || propH || 0);
1008
+ }
1009
+ if (w) clone.dataset.snapdomWidth = String(w);
1010
+ if (h) clone.dataset.snapdomHeight = String(h);
1011
+ } catch {
1012
+ }
873
1013
  }
874
1014
  } catch (err) {
875
1015
  console.error("[Snapdom] Failed to clone node:", node, err);
876
1016
  throw err;
877
1017
  }
878
1018
  if (node instanceof HTMLTextAreaElement) {
879
- clone.textContent = node.value;
880
- clone.value = node.value;
881
1019
  const rect = node.getBoundingClientRect();
882
1020
  clone.style.width = `${rect.width}px`;
883
1021
  clone.style.height = `${rect.height}px`;
@@ -894,8 +1032,22 @@ function deepClone(node, sessionCache, options) {
894
1032
  if (node instanceof HTMLSelectElement) {
895
1033
  pendingSelectValue = node.value;
896
1034
  }
1035
+ if (node instanceof HTMLTextAreaElement) {
1036
+ pendingTextAreaValue = node.value;
1037
+ }
897
1038
  inlineAllStyles(node, clone, sessionCache, options);
898
1039
  if (node.shadowRoot) {
1040
+ let callback2 = function(child, resolve) {
1041
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
1042
+ return resolve(null);
1043
+ } else {
1044
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1045
+ resolve(clonedChild || null);
1046
+ }).catch((e) => {
1047
+ resolve(null);
1048
+ });
1049
+ }
1050
+ };
899
1051
  try {
900
1052
  const slots = node.shadowRoot.querySelectorAll("slot");
901
1053
  for (const s of slots) {
@@ -921,31 +1073,38 @@ function deepClone(node, sessionCache, options) {
921
1073
  const seed = buildSeedCustomPropsRule(node, neededVars, scopeSelector);
922
1074
  injectScopedStyle(clone, seed + rewritten, scopeId);
923
1075
  const shadowFrag = document.createDocumentFragment();
924
- for (const child of node.shadowRoot.childNodes) {
925
- if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") continue;
926
- const clonedChild = deepClone(child, sessionCache, options);
927
- if (clonedChild) shadowFrag.appendChild(clonedChild);
928
- }
1076
+ const cloneList2 = await idleCallback(Array.from(node.shadowRoot.childNodes), callback2, options.fast);
1077
+ shadowFrag.append(...cloneList2.filter((clonedChild) => !!clonedChild));
929
1078
  clone.appendChild(shadowFrag);
930
1079
  }
931
1080
  if (node.tagName === "SLOT") {
1081
+ let callback2 = function(child, resolve) {
1082
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1083
+ if (clonedChild) {
1084
+ markSlottedSubtree(clonedChild);
1085
+ }
1086
+ resolve(clonedChild || null);
1087
+ }).catch((e) => {
1088
+ resolve(null);
1089
+ });
1090
+ };
932
1091
  const assigned = node.assignedNodes?.({ flatten: true }) || [];
933
1092
  const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
934
1093
  const fragment = document.createDocumentFragment();
935
- for (const child of nodesToClone) {
936
- const clonedChild = deepClone(child, sessionCache, options);
937
- if (clonedChild) {
938
- markSlottedSubtree(clonedChild);
939
- fragment.appendChild(clonedChild);
940
- }
941
- }
1094
+ const cloneList2 = await idleCallback(Array.from(nodesToClone), callback2, options.fast);
1095
+ fragment.append(...cloneList2.filter((clonedChild) => !!clonedChild));
942
1096
  return fragment;
943
1097
  }
944
- for (const child of node.childNodes) {
945
- if (clonedAssignedNodes.has(child)) continue;
946
- const clonedChild = deepClone(child, sessionCache, options);
947
- if (clonedChild) clone.appendChild(clonedChild);
1098
+ function callback(child, resolve) {
1099
+ if (clonedAssignedNodes.has(child)) return resolve(null);
1100
+ deepClone(child, sessionCache, options).then((clonedChild) => {
1101
+ resolve(clonedChild || null);
1102
+ }).catch((e) => {
1103
+ resolve(null);
1104
+ });
948
1105
  }
1106
+ const cloneList = await idleCallback(Array.from(node.childNodes), callback, options.fast);
1107
+ clone.append(...cloneList.filter((clonedChild) => !!clonedChild));
949
1108
  if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
950
1109
  clone.value = pendingSelectValue;
951
1110
  for (const opt of clone.options) {
@@ -956,6 +1115,9 @@ function deepClone(node, sessionCache, options) {
956
1115
  }
957
1116
  }
958
1117
  }
1118
+ if (pendingTextAreaValue !== null && clone instanceof HTMLTextAreaElement) {
1119
+ clone.textContent = pendingTextAreaValue;
1120
+ }
959
1121
  return clone;
960
1122
  }
961
1123
 
@@ -1826,7 +1988,7 @@ async function prepareClone(element, options = {}) {
1826
1988
  console.warn("inlineExternal defs or symbol failed:", e);
1827
1989
  }
1828
1990
  try {
1829
- clone = deepClone(element, sessionCache, options, element);
1991
+ clone = await deepClone(element, sessionCache, options, element);
1830
1992
  } catch (e) {
1831
1993
  console.warn("deepClone failed:", e);
1832
1994
  throw e;
@@ -2064,6 +2226,30 @@ async function inlineImages(clone, options = {}) {
2064
2226
  if (!img.height) img.height = img.naturalHeight || 100;
2065
2227
  return;
2066
2228
  }
2229
+ const { defaultImageUrl } = options || {};
2230
+ if (defaultImageUrl) {
2231
+ try {
2232
+ const dsW = parseInt(img.dataset?.snapdomWidth || "", 10) || 0;
2233
+ const dsH = parseInt(img.dataset?.snapdomHeight || "", 10) || 0;
2234
+ const attrW = parseInt(img.getAttribute("width") || "", 10) || 0;
2235
+ const attrH = parseInt(img.getAttribute("height") || "", 10) || 0;
2236
+ const styleW = parseFloat(img.style?.width || "") || 0;
2237
+ const styleH = parseFloat(img.style?.height || "") || 0;
2238
+ const width = dsW || styleW || attrW || img.width || void 0;
2239
+ const height = dsH || styleH || attrH || img.height || void 0;
2240
+ const fallbackUrl = typeof defaultImageUrl === "function" ? await defaultImageUrl({ width, height, src, element: img }) : defaultImageUrl;
2241
+ if (fallbackUrl) {
2242
+ const fallbackData = await snapFetch(fallbackUrl, { as: "dataURL", useProxy: options.useProxy });
2243
+ img.src = fallbackData.data;
2244
+ if (!img.width && width) img.width = width;
2245
+ if (!img.height && height) img.height = height;
2246
+ if (!img.width) img.width = img.naturalWidth || 100;
2247
+ if (!img.height) img.height = img.naturalHeight || 100;
2248
+ return;
2249
+ }
2250
+ } catch {
2251
+ }
2252
+ }
2067
2253
  const w = img.width || img.naturalWidth || 100;
2068
2254
  const h = img.height || img.naturalHeight || 100;
2069
2255
  if (options.placeholders !== false) {
@@ -2177,7 +2363,13 @@ async function captureDOM(element, options) {
2177
2363
  );
2178
2364
  await ensureFontsReady(families, 2);
2179
2365
  }
2180
- fontsCSS = await embedCustomFonts({ required, usedCodepoints, preCached: false, exclude: options.excludeFonts, useProxy: options.useProxy });
2366
+ fontsCSS = await embedCustomFonts({
2367
+ required,
2368
+ usedCodepoints,
2369
+ preCached: false,
2370
+ exclude: options.excludeFonts,
2371
+ useProxy: options.useProxy
2372
+ });
2181
2373
  resolve();
2182
2374
  }, { fast });
2183
2375
  });
@@ -2197,12 +2389,13 @@ async function captureDOM(element, options) {
2197
2389
  }
2198
2390
  await new Promise((resolve) => {
2199
2391
  idle(() => {
2200
- const rect = element.getBoundingClientRect();
2392
+ const rect = getNaturalBorderBoxSize(element);
2201
2393
  let w = rect.width;
2202
2394
  let h = rect.height;
2203
2395
  const hasW = Number.isFinite(options.width);
2204
2396
  const hasH = Number.isFinite(options.height);
2205
- const hasScale = typeof scale === "number" && scale !== 1;
2397
+ const scaleOpt = options.scale ?? 1;
2398
+ const hasScale = typeof scaleOpt === "number" && scaleOpt !== 1;
2206
2399
  if (!hasScale) {
2207
2400
  const aspect = rect.width / rect.height;
2208
2401
  if (hasW && hasH) {
@@ -2216,30 +2409,79 @@ async function captureDOM(element, options) {
2216
2409
  w = h * aspect;
2217
2410
  }
2218
2411
  }
2219
- w = Math.ceil(w);
2220
- h = Math.ceil(h);
2221
- clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2222
- clone.style.transformOrigin = "top left";
2223
- if (!hasScale && (hasW || hasH)) {
2224
- const originalW = rect.width;
2225
- const originalH = rect.height;
2226
- const scaleX = w / originalW;
2227
- const scaleY = h / originalH;
2228
- const existingTransform = clone.style.transform || "";
2229
- const scaleTransform = `scale(${scaleX}, ${scaleY})`;
2230
- clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
2412
+ w = Math.max(1, Math.ceil(w));
2413
+ h = Math.max(1, Math.ceil(h));
2414
+ const csEl = getComputedStyle(element);
2415
+ const baseTransform = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
2416
+ const ind = readIndividualTransforms(element);
2417
+ let outW, outH, tx, ty, cancelTranslateX = 0, cancelTranslateY = 0;
2418
+ if (!hasBBoxAffectingTransform(element)) {
2419
+ const r = element.getBoundingClientRect();
2420
+ const vp = inflateRect(r, { top: 0, right: 0, bottom: 0, left: 0 });
2421
+ outW = Math.max(1, vp.width);
2422
+ outH = Math.max(1, vp.height);
2423
+ tx = -(vp.x - Math.floor(r.left));
2424
+ ty = -(vp.y - Math.floor(r.top));
2425
+ } else {
2426
+ const TOTAL = readTotalTransformMatrix({
2427
+ baseTransform,
2428
+ rotate: ind.rotate || "0deg",
2429
+ scale: ind.scale,
2430
+ translate: ind.translate
2431
+ });
2432
+ const e = TOTAL.e ?? TOTAL.m41 ?? 0;
2433
+ const f = TOTAL.f ?? TOTAL.m42 ?? 0;
2434
+ cancelTranslateX = -e;
2435
+ cancelTranslateY = -f;
2436
+ const M2D = TOTAL.is2D ? matrix2DNoTranslate(TOTAL) : matrix2DNoTranslate(new DOMMatrix(TOTAL.toString()));
2437
+ const bb = bboxFromMatrix(w, h, M2D);
2438
+ outW = Math.max(1, Math.ceil(bb.width));
2439
+ outH = Math.max(1, Math.ceil(bb.height));
2440
+ tx = -bb.minX;
2441
+ ty = -bb.minY;
2231
2442
  }
2232
2443
  const svgNS = "http://www.w3.org/2000/svg";
2233
2444
  const fo = document.createElementNS(svgNS, "foreignObject");
2234
2445
  fo.setAttribute("width", "100%");
2235
2446
  fo.setAttribute("height", "100%");
2447
+ fo.setAttribute("x", String(tx));
2448
+ fo.setAttribute("y", String(ty));
2449
+ fo.style.overflow = "visible";
2236
2450
  const styleTag = document.createElement("style");
2237
- styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;}" + classCSS;
2451
+ styleTag.textContent = baseCSS + fontsCSS + "svg{overflow:visible;} foreignObject{overflow:visible;}" + classCSS;
2238
2452
  fo.appendChild(styleTag);
2239
- fo.appendChild(clone);
2453
+ const container = document.createElement("div");
2454
+ container.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2455
+ container.style.width = `${w}px`;
2456
+ container.style.height = `${h}px`;
2457
+ container.style.overflow = "visible";
2458
+ container.style.transformOrigin = "0 0";
2459
+ const cancels = [];
2460
+ if (cancelTranslateX || cancelTranslateY) {
2461
+ cancels.push(`translate(${cancelTranslateX}px, ${cancelTranslateY}px)`);
2462
+ }
2463
+ if (!hasScale && (hasW || hasH)) {
2464
+ const sxWH = rect.width ? w / rect.width : 1;
2465
+ const syWH = rect.height ? h / rect.height : 1;
2466
+ cancels.push(`scale(${sxWH}, ${syWH})`);
2467
+ } else if (hasScale) {
2468
+ cancels.push(`scale(${scaleOpt})`);
2469
+ }
2470
+ if (cancels.length) container.style.transform = cancels.join(" ");
2471
+ clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2472
+ clone.style.transformOrigin = "0 0";
2473
+ clone.style.position = "static";
2474
+ clone.style.left = "0";
2475
+ clone.style.top = "0";
2476
+ if (baseTransform) clone.style.transform = baseTransform;
2477
+ clone.style.rotate = ind.rotate || "0deg";
2478
+ clone.style.scale = ind.scale || "1";
2479
+ clone.style.translate = ind.translate || "0 0";
2480
+ container.appendChild(clone);
2481
+ fo.appendChild(container);
2240
2482
  const serializer = new XMLSerializer();
2241
2483
  const foString = serializer.serializeToString(fo);
2242
- const svgHeader = `<svg xmlns="${svgNS}" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`;
2484
+ const svgHeader = `<svg xmlns="${svgNS}" width="${outW}" height="${outH}" viewBox="0 0 ${outW} ${outH}">`;
2243
2485
  const svgFooter = "</svg>";
2244
2486
  svgString = svgHeader + foString + svgFooter;
2245
2487
  dataURL = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`;
@@ -2250,6 +2492,128 @@ async function captureDOM(element, options) {
2250
2492
  if (sandbox && sandbox.style.position === "absolute") sandbox.remove();
2251
2493
  return dataURL;
2252
2494
  }
2495
+ function getNaturalBorderBoxSize(el) {
2496
+ const cs = getComputedStyle(el);
2497
+ const px = (v) => parseFloat(v) || 0;
2498
+ let w = px(cs.width);
2499
+ let h = px(cs.height);
2500
+ if (cs.boxSizing === "content-box") {
2501
+ w += px(cs.paddingLeft) + px(cs.paddingRight) + px(cs.borderLeftWidth) + px(cs.borderRightWidth);
2502
+ h += px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth);
2503
+ }
2504
+ return { width: w, height: h };
2505
+ }
2506
+ function matrixFromComputed(el) {
2507
+ const tr = getComputedStyle(el).transform;
2508
+ if (!tr || tr === "none") return new DOMMatrix();
2509
+ try {
2510
+ return new DOMMatrix(tr);
2511
+ } catch {
2512
+ return new WebKitCSSMatrix(tr);
2513
+ }
2514
+ }
2515
+ function matrix2DNoTranslate(M) {
2516
+ return new DOMMatrix([M.a, M.b, M.c, M.d, 0, 0]);
2517
+ }
2518
+ function bboxFromMatrix(w, h, M2D) {
2519
+ const pts = [
2520
+ { x: 0, y: 0 },
2521
+ { x: w, y: 0 },
2522
+ { x: 0, y: h },
2523
+ { x: w, y: h }
2524
+ ];
2525
+ const t = pts.map((p) => ({
2526
+ x: M2D.a * p.x + M2D.c * p.y,
2527
+ y: M2D.b * p.x + M2D.d * p.y
2528
+ }));
2529
+ const minX = Math.min(...t.map((p) => p.x));
2530
+ const maxX = Math.max(...t.map((p) => p.x));
2531
+ const minY = Math.min(...t.map((p) => p.y));
2532
+ const maxY = Math.max(...t.map((p) => p.y));
2533
+ return { minX, minY, width: maxX - minX, height: maxY - minY };
2534
+ }
2535
+ function readIndividualTransforms(el) {
2536
+ const out = { rotate: "0deg", scale: null, translate: null };
2537
+ const map = el.computedStyleMap?.();
2538
+ if (map) {
2539
+ const rot = map.get("rotate");
2540
+ if (rot) {
2541
+ const ang = rot.angle ?? rot;
2542
+ if (ang && "unit" in ang) {
2543
+ out.rotate = ang.unit === "rad" ? ang.value * 180 / Math.PI + "deg" : ang.value + ang.unit;
2544
+ } else {
2545
+ out.rotate = String(rot);
2546
+ }
2547
+ }
2548
+ const sc = map.get("scale");
2549
+ if (sc && sc.length) {
2550
+ const sx = sc[0]?.value ?? 1;
2551
+ const sy = sc[1]?.value ?? sx;
2552
+ out.scale = `${sx} ${sy}`;
2553
+ }
2554
+ const tr = map.get("translate");
2555
+ if (tr && tr.length) {
2556
+ const tx = tr[0]?.value ?? 0;
2557
+ const ty = tr[1]?.value ?? 0;
2558
+ const ux = tr[0]?.unit ?? "px";
2559
+ const uy = tr[1]?.unit ?? "px";
2560
+ out.translate = `${tx}${ux} ${ty}${uy}`;
2561
+ }
2562
+ return out;
2563
+ }
2564
+ const cs = getComputedStyle(el);
2565
+ out.rotate = cs.rotate && cs.rotate !== "none" ? cs.rotate : "0deg";
2566
+ out.scale = cs.scale && cs.scale !== "none" ? cs.scale : null;
2567
+ out.translate = cs.translate && cs.translate !== "none" ? cs.translate : null;
2568
+ return out;
2569
+ }
2570
+ function hasBBoxAffectingTransform(el) {
2571
+ const t = getComputedStyle(el).transform || "none";
2572
+ if (t === "none" || t === "matrix(1, 0, 0, 1, 0, 0)") return false;
2573
+ if (/^matrix\(\s*1\s*,\s*0\s*,\s*0\s*,\s*1\s*,/i.test(t)) return false;
2574
+ return true;
2575
+ }
2576
+ function inflateRect(r, p) {
2577
+ const x = Math.floor(r.left - p.left);
2578
+ const y = Math.floor(r.top - p.top);
2579
+ const w = Math.ceil(r.width + p.left + p.right);
2580
+ const h = Math.ceil(r.height + p.top + p.bottom);
2581
+ return { x, y, width: w, height: h };
2582
+ }
2583
+ var __measureHost = null;
2584
+ function getMeasureHost() {
2585
+ if (__measureHost) return __measureHost;
2586
+ const n = document.createElement("div");
2587
+ n.id = "snapdom-measure-slot";
2588
+ n.setAttribute("aria-hidden", "true");
2589
+ Object.assign(n.style, {
2590
+ position: "absolute",
2591
+ left: "-99999px",
2592
+ top: "0px",
2593
+ width: "0px",
2594
+ height: "0px",
2595
+ overflow: "hidden",
2596
+ opacity: "0",
2597
+ pointerEvents: "none",
2598
+ contain: "size layout style"
2599
+ });
2600
+ document.documentElement.appendChild(n);
2601
+ __measureHost = n;
2602
+ return n;
2603
+ }
2604
+ function readTotalTransformMatrix(t) {
2605
+ const host = getMeasureHost();
2606
+ const tmp = document.createElement("div");
2607
+ tmp.style.transformOrigin = "0 0";
2608
+ if (t.baseTransform) tmp.style.transform = t.baseTransform;
2609
+ if (t.rotate) tmp.style.rotate = t.rotate;
2610
+ if (t.scale) tmp.style.scale = t.scale;
2611
+ if (t.translate) tmp.style.translate = t.translate;
2612
+ host.appendChild(tmp);
2613
+ const M = matrixFromComputed(tmp);
2614
+ host.removeChild(tmp);
2615
+ return M;
2616
+ }
2253
2617
 
2254
2618
  // src/core/context.js
2255
2619
  function normalizeCachePolicy(v) {
@@ -2282,6 +2646,7 @@ function createContext(options = {}) {
2282
2646
  iconFonts: Array.isArray(options.iconFonts) ? options.iconFonts : options.iconFonts ? [options.iconFonts] : [],
2283
2647
  localFonts: Array.isArray(options.localFonts) ? options.localFonts : [],
2284
2648
  excludeFonts: options.excludeFonts ?? void 0,
2649
+ defaultImageUrl: options.defaultImageUrl ?? void 0,
2285
2650
  /** Cache policy (disabled|full|auto|soft). Default: "soft" */
2286
2651
  /** @type {CachePolicy} */
2287
2652
  cache: cachePolicy,
@@ -2315,7 +2680,7 @@ async function toImg(url, options) {
2315
2680
 
2316
2681
  // src/exporters/toCanvas.js
2317
2682
  async function toCanvas(url, options) {
2318
- const { scale: scale2, dpr } = options;
2683
+ const { scale, dpr } = options;
2319
2684
  const safari = isSafari();
2320
2685
  const img = new Image();
2321
2686
  img.crossOrigin = "anonymous";
@@ -2334,8 +2699,8 @@ async function toCanvas(url, options) {
2334
2699
  if (safari) {
2335
2700
  await new Promise((r) => setTimeout(r, 100));
2336
2701
  }
2337
- const width = img.naturalWidth * scale2;
2338
- const height = img.naturalHeight * scale2;
2702
+ const width = img.naturalWidth * scale;
2703
+ const height = img.naturalHeight * scale;
2339
2704
  const canvas = document.createElement("canvas");
2340
2705
  canvas.width = Math.ceil(width * dpr);
2341
2706
  canvas.height = Math.ceil(height * dpr);
@@ -2409,6 +2774,14 @@ async function snapdom(element, userOptions) {
2409
2774
  if (!element) throw new Error("Element cannot be null or undefined");
2410
2775
  const context = createContext(userOptions);
2411
2776
  if (context.iconFonts && context.iconFonts.length > 0) extendIconFonts(context.iconFonts);
2777
+ if (!context.snap) {
2778
+ context.snap = {
2779
+ /** Delegates to this module’s PNG path (tu build devuelve <img>). */
2780
+ toPng: (el, opts) => snapdom.toPng(el, opts),
2781
+ /** Útil si en algún flujo necesitás vector o <img> directo. */
2782
+ toImg: (el, opts) => snapdom.toImg(el, opts)
2783
+ };
2784
+ }
2412
2785
  return snapdom.capture(element, context, INTERNAL_TOKEN);
2413
2786
  }
2414
2787
  snapdom.capture = async (el, context, _token) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom",
3
- "version": "1.9.10-dev.1",
3
+ "version": "1.9.10-dev.2",
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",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * snapDOM – ultra-fast DOM-to-image capture
3
3
  * TypeScript definitions
4
- *
4
+ *
5
5
  * Notes:
6
6
  * - Style compression is always enabled internally (no user option).
7
7
  * - Icon fonts are always embedded; `embedFonts` controls non-icon fonts only.
@@ -101,6 +101,20 @@ export interface CaptureOptions {
101
101
  * - "none": do not reset anything (fastest, may reuse stale resources).
102
102
  */
103
103
  reset?: "soft" | "hard" | "none";
104
+
105
+ /**
106
+ * Fallback image source when an <img> fails to load.
107
+ * - String: use as-is.
108
+ * - Callback: receives measured width/height and original src, returns a URL string.
109
+ */
110
+ defaultImageUrl?:
111
+ | string
112
+ | ((args: {
113
+ width?: number;
114
+ height?: number;
115
+ src?: string;
116
+ element: HTMLImageElement;
117
+ }) => string | Promise<string>);
104
118
  }
105
119
 
106
120
  export interface BlobOptions {