@zumer/snapdom 1.7.1 → 1.9.3

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,11 +1,12 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.1.7.1
4
+ * v.1.9.3
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
8
8
 
9
+
9
10
  // src/core/cache.js
10
11
  var imageCache = /* @__PURE__ */ new Map();
11
12
  var bgCache = /* @__PURE__ */ new Map();
@@ -13,6 +14,7 @@ var resourceCache = /* @__PURE__ */ new Map();
13
14
  var defaultStylesCache = /* @__PURE__ */ new Map();
14
15
  var baseCSSCache = /* @__PURE__ */ new Map();
15
16
  var computedStyleCache = /* @__PURE__ */ new WeakMap();
17
+ var processedFontURLs = /* @__PURE__ */ new Set();
16
18
 
17
19
  // src/utils/cssTools.js
18
20
  var commonTags = [
@@ -152,6 +154,26 @@ function generateCSSClasses(styleMap) {
152
154
  }
153
155
 
154
156
  // src/utils/helpers.js
157
+ async function inlineSingleBackgroundEntry(entry, options = {}) {
158
+ const isUrl = entry.startsWith("url(");
159
+ const isGradient = /^((repeating-)?(linear|radial|conic)-gradient)\(/i.test(entry);
160
+ if (isUrl) {
161
+ const rawUrl = extractURL(entry);
162
+ if (!rawUrl) return entry;
163
+ const encodedUrl = safeEncodeURI(rawUrl);
164
+ if (bgCache.has(encodedUrl)) {
165
+ return options.skipInline ? void 0 : `url(${bgCache.get(encodedUrl)})`;
166
+ } else {
167
+ const dataUrl = await fetchImage(encodedUrl, { useProxy: options.useProxy });
168
+ bgCache.set(encodedUrl, dataUrl);
169
+ return options.skipInline ? void 0 : `url("${dataUrl}")`;
170
+ }
171
+ }
172
+ if (isGradient || entry === "none") {
173
+ return entry;
174
+ }
175
+ return entry;
176
+ }
155
177
  function idle(fn, { fast = false } = {}) {
156
178
  if (fast) return fn();
157
179
  if ("requestIdleCallback" in window) {
@@ -196,29 +218,84 @@ function extractURL(value) {
196
218
  }
197
219
  return url;
198
220
  }
199
- function isIconFont(familyOrUrl) {
200
- const iconFontPatterns = [
201
- /font\s*awesome/i,
202
- /material\s*icons/i,
203
- /ionicons/i,
204
- /glyphicons/i,
205
- /feather/i,
206
- /bootstrap\s*icons/i,
207
- /remix\s*icons/i,
208
- /heroicons/i
209
- ];
210
- return iconFontPatterns.some((rx) => rx.test(familyOrUrl));
211
- }
212
- function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
221
+ function fetchImage(src, { timeout = 3e3, useProxy = "" } = {}) {
222
+ function getCrossOriginMode(url) {
223
+ try {
224
+ const parsed = new URL(url, window.location.href);
225
+ return parsed.origin === window.location.origin ? "use-credentials" : "anonymous";
226
+ } catch {
227
+ return "anonymous";
228
+ }
229
+ }
230
+ async function fetchWithFallback(url) {
231
+ const fetchBlobAsDataURL = (fetchUrl) => fetch(fetchUrl, {
232
+ mode: "cors",
233
+ credentials: getCrossOriginMode(fetchUrl) === "use-credentials" ? "include" : "omit"
234
+ }).then((r) => r.blob()).then((blob) => new Promise((resolve, reject) => {
235
+ const reader = new FileReader();
236
+ reader.onloadend = () => {
237
+ const base64 = reader.result;
238
+ if (typeof base64 !== "string" || !base64.startsWith("data:image/")) {
239
+ reject(new Error("Invalid image data URL"));
240
+ return;
241
+ }
242
+ resolve(base64);
243
+ };
244
+ reader.onerror = () => reject(new Error("FileReader error"));
245
+ reader.readAsDataURL(blob);
246
+ }));
247
+ try {
248
+ return await fetchBlobAsDataURL(url);
249
+ } catch (e) {
250
+ if (useProxy && typeof useProxy === "string") {
251
+ const proxied = useProxy.replace(/\/$/, "") + safeEncodeURI(url);
252
+ try {
253
+ return await fetchBlobAsDataURL(proxied);
254
+ } catch {
255
+ console.error(`[SnapDOM - fetchImage] Proxy fallback failed for: ${url}`);
256
+ throw new Error("CORS restrictions prevented image capture (even via proxy)");
257
+ }
258
+ } else {
259
+ console.error(`[SnapDOM - fetchImage] No valid proxy URL provided for fallback: ${url}`);
260
+ throw new Error("Fetch fallback failed and no proxy provided");
261
+ }
262
+ }
263
+ }
264
+ const crossOriginValue = getCrossOriginMode(src);
265
+ console.log(`[SnapDOM - fetchImage] Start loading image: ${src} with crossOrigin=${crossOriginValue}`);
213
266
  if (imageCache.has(src)) {
267
+ console.log(`[SnapDOM - fetchImage] Cache hit for: ${src}`);
214
268
  return Promise.resolve(imageCache.get(src));
215
269
  }
270
+ const isDataURI = src.startsWith("data:image/");
271
+ if (isDataURI) {
272
+ imageCache.set(src, src);
273
+ return Promise.resolve(src);
274
+ }
275
+ const isSVG = /\.svg(\?.*)?$/i.test(src);
276
+ if (isSVG) {
277
+ return (async () => {
278
+ try {
279
+ const response = await fetch(src, {
280
+ mode: "cors",
281
+ credentials: crossOriginValue === "use-credentials" ? "include" : "omit"
282
+ });
283
+ const svgText = await response.text();
284
+ const encoded = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgText)}`;
285
+ imageCache.set(src, encoded);
286
+ return encoded;
287
+ } catch {
288
+ return fetchWithFallback(src);
289
+ }
290
+ })();
291
+ }
216
292
  return new Promise((resolve, reject) => {
217
293
  const timeoutId = setTimeout(() => {
294
+ console.log(`[SnapDOM - fetchImage] Timeout after ${timeout}ms for image: ${src}`);
218
295
  reject(new Error("Image load timed out"));
219
296
  }, timeout);
220
297
  const image = new Image();
221
- image.crossOrigin = crossOrigin;
298
+ image.crossOrigin = crossOriginValue;
222
299
  image.onload = async () => {
223
300
  clearTimeout(timeoutId);
224
301
  try {
@@ -228,20 +305,29 @@ function fetchImage(src, timeout = 3e3, crossOrigin = "anonymous") {
228
305
  canvas.height = image.height;
229
306
  const ctx = canvas.getContext("2d");
230
307
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
308
+ const dataURL = canvas.toDataURL("image/png");
309
+ imageCache.set(src, dataURL);
310
+ resolve(dataURL);
311
+ } catch {
231
312
  try {
232
- const dataURL = canvas.toDataURL("image/png");
233
- imageCache.set(src, dataURL);
234
- resolve(dataURL);
313
+ const fallbackDataURL = await fetchWithFallback(src);
314
+ imageCache.set(src, fallbackDataURL);
315
+ resolve(fallbackDataURL);
235
316
  } catch (e) {
236
- reject(new Error("CORS restrictions prevented image capture"));
317
+ reject(e);
237
318
  }
238
- } catch (e) {
239
- reject(e);
240
319
  }
241
320
  };
242
- image.onerror = (e) => {
321
+ image.onerror = async () => {
243
322
  clearTimeout(timeoutId);
244
- reject(new Error("Failed to load image: " + (e.message || "Unknown error")));
323
+ console.error(`[SnapDOM - fetchImage] Image failed to load: ${src}`);
324
+ try {
325
+ const fallbackDataURL = await fetchWithFallback(src);
326
+ imageCache.set(src, fallbackDataURL);
327
+ resolve(fallbackDataURL);
328
+ } catch (e) {
329
+ reject(e);
330
+ }
245
331
  };
246
332
  image.src = src;
247
333
  });
@@ -256,35 +342,136 @@ function snapshotComputedStyle(style) {
256
342
  function isSafari() {
257
343
  return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
258
344
  }
345
+ function stripTranslate(transform) {
346
+ if (!transform || transform === "none") return "";
347
+ let cleaned = transform.replace(/translate[XY]?\([^)]*\)/g, "");
348
+ cleaned = cleaned.replace(/matrix\(([^)]+)\)/g, (_, values) => {
349
+ const parts = values.split(",").map((s) => s.trim());
350
+ if (parts.length !== 6) return `matrix(${values})`;
351
+ parts[4] = "0";
352
+ parts[5] = "0";
353
+ return `matrix(${parts.join(", ")})`;
354
+ });
355
+ cleaned = cleaned.replace(/matrix3d\(([^)]+)\)/g, (_, values) => {
356
+ const parts = values.split(",").map((s) => s.trim());
357
+ if (parts.length !== 16) return `matrix3d(${values})`;
358
+ parts[12] = "0";
359
+ parts[13] = "0";
360
+ return `matrix3d(${parts.join(", ")})`;
361
+ });
362
+ return cleaned.trim().replace(/\s{2,}/g, " ");
363
+ }
364
+ function safeEncodeURI(uri) {
365
+ if (/%[0-9A-Fa-f]{2}/.test(uri)) return uri;
366
+ try {
367
+ return encodeURI(uri);
368
+ } catch {
369
+ return uri;
370
+ }
371
+ }
372
+ function splitBackgroundImage(bg) {
373
+ const parts = [];
374
+ let depth = 0;
375
+ let lastIndex = 0;
376
+ for (let i = 0; i < bg.length; i++) {
377
+ const char = bg[i];
378
+ if (char === "(") depth++;
379
+ if (char === ")") depth--;
380
+ if (char === "," && depth === 0) {
381
+ parts.push(bg.slice(lastIndex, i).trim());
382
+ lastIndex = i + 1;
383
+ }
384
+ }
385
+ parts.push(bg.slice(lastIndex).trim());
386
+ return parts;
387
+ }
259
388
 
260
389
  // src/modules/styles.js
390
+ var snapshotCache = /* @__PURE__ */ new WeakMap();
391
+ var snapshotKeyCache = /* @__PURE__ */ new Map();
392
+ function snapshotComputedStyleFull(style) {
393
+ const result = {};
394
+ for (let i = 0; i < style.length; i++) {
395
+ const prop = style[i];
396
+ let val = style.getPropertyValue(prop);
397
+ if ((prop === "background-image" || prop === "content") && val.includes("url(") && !val.includes("data:")) {
398
+ val = "none";
399
+ }
400
+ result[prop] = val;
401
+ }
402
+ return result;
403
+ }
261
404
  function inlineAllStyles(source, clone, styleMap, cache, compress) {
262
405
  if (source.tagName === "STYLE") return;
263
406
  if (!cache.has(source)) {
264
407
  cache.set(source, getStyle(source));
265
408
  }
266
409
  const style = cache.get(source);
267
- const snapshot = snapshotComputedStyle(style);
410
+ if (!snapshotCache.has(source)) {
411
+ const snapshot2 = snapshotComputedStyleFull(style);
412
+ snapshotCache.set(source, snapshot2);
413
+ }
414
+ const snapshot = snapshotCache.get(source);
415
+ const hash = Object.entries(snapshot).sort(([a], [b]) => a.localeCompare(b)).map(([prop, val]) => `${prop}:${val}`).join(";");
416
+ if (snapshotKeyCache.has(hash)) {
417
+ styleMap.set(clone, snapshotKeyCache.get(hash));
418
+ return;
419
+ }
268
420
  const tagName = source.tagName?.toLowerCase() || "div";
269
421
  const key = getStyleKey(snapshot, tagName, compress);
422
+ snapshotKeyCache.set(hash, key);
270
423
  styleMap.set(clone, key);
271
424
  }
272
425
 
273
426
  // src/core/clone.js
274
- function deepClone(node, styleMap, styleCache, nodeMap, compress) {
275
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "exclude") {
427
+ function deepClone(node, styleMap, styleCache, nodeMap, compress, options = {}, originalRoot) {
428
+ if (node.nodeType === Node.TEXT_NODE) {
429
+ if (node.parentElement?.shadowRoot) {
430
+ const tag = node.parentElement.tagName.toLowerCase();
431
+ if (customElements.get(tag)) return null;
432
+ }
433
+ return node.cloneNode(true);
434
+ }
435
+ if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
436
+ if (node.getAttribute("data-capture") === "exclude") {
276
437
  const spacer = document.createElement("div");
277
438
  const rect = node.getBoundingClientRect();
278
439
  spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
279
440
  return spacer;
280
441
  }
442
+ if (options.exclude && Array.isArray(options.exclude) && options.exclude.length > 0) {
443
+ for (const selector of options.exclude) {
444
+ try {
445
+ if (node.matches && node.matches(selector)) {
446
+ const spacer = document.createElement("div");
447
+ const rect = node.getBoundingClientRect();
448
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
449
+ return spacer;
450
+ }
451
+ } catch (err) {
452
+ console.warn(`Invalid selector in exclude option: ${selector}`, err);
453
+ }
454
+ }
455
+ }
456
+ if (typeof options.filter === "function") {
457
+ try {
458
+ if (!options.filter(node, originalRoot || node)) {
459
+ const spacer = document.createElement("div");
460
+ const rect = node.getBoundingClientRect();
461
+ spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
462
+ return spacer;
463
+ }
464
+ } catch (err) {
465
+ console.warn("Error in filter function:", err);
466
+ }
467
+ }
281
468
  if (node.tagName === "IFRAME") {
282
469
  const fallback = document.createElement("div");
283
470
  fallback.textContent = "";
284
- fallback.style.cssText = `width: ${node.offsetWidth}px; height: ${node.offsetHeight}px; background: 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;`;
471
+ 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;`;
285
472
  return fallback;
286
473
  }
287
- if (node.nodeType === Node.ELEMENT_NODE && node.getAttribute("data-capture") === "placeholder") {
474
+ if (node.getAttribute("data-capture") === "placeholder") {
288
475
  const clone2 = node.cloneNode(false);
289
476
  nodeMap.set(clone2, node);
290
477
  inlineAllStyles(node, clone2, styleMap, styleCache, compress);
@@ -301,18 +488,10 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
301
488
  img.width = node.width;
302
489
  img.height = node.height;
303
490
  img.style.display = "inline-block";
304
- img.style.width = `${node.width}px`;
305
- img.style.height = `${node.height}px`;
491
+ img.style.width = node.style.width || `${node.width}px`;
492
+ img.style.height = node.style.height || `${node.height}px`;
306
493
  return img;
307
494
  }
308
- if (node.nodeType === Node.TEXT_NODE) {
309
- if (node.parentElement?.shadowRoot) {
310
- const tag = node.parentElement.tagName.toLowerCase();
311
- if (customElements.get(tag)) return null;
312
- }
313
- return node.cloneNode(true);
314
- }
315
- if (node.nodeType !== Node.ELEMENT_NODE) return node.cloneNode(true);
316
495
  const clone = node.cloneNode(false);
317
496
  nodeMap.set(clone, node);
318
497
  if (node instanceof HTMLInputElement) {
@@ -337,13 +516,14 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
337
516
  }
338
517
  inlineAllStyles(node, clone, styleMap, styleCache, compress);
339
518
  const frag = document.createDocumentFragment();
519
+ const rootElement = originalRoot || node;
340
520
  node.childNodes.forEach((child) => {
341
- const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress);
521
+ const clonedChild = deepClone(child, styleMap, styleCache, nodeMap, compress, options, rootElement);
342
522
  if (clonedChild) frag.appendChild(clonedChild);
343
523
  });
344
524
  clone.appendChild(frag);
345
525
  if (node.shadowRoot) {
346
- const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap)).filter(Boolean);
526
+ const shadowContent = Array.from(node.shadowRoot.children).filter((el) => el.tagName !== "STYLE").map((el) => deepClone(el, styleMap, styleCache, nodeMap, compress, options, rootElement)).filter(Boolean);
347
527
  const shadowFrag = document.createDocumentFragment();
348
528
  shadowContent.forEach((child) => shadowFrag.appendChild(child));
349
529
  clone.appendChild(shadowFrag);
@@ -351,6 +531,43 @@ function deepClone(node, styleMap, styleCache, nodeMap, compress) {
351
531
  return clone;
352
532
  }
353
533
 
534
+ // src/modules/iconFonts.js
535
+ var defaultIconFonts = [
536
+ // /uicons/i,
537
+ /font\s*awesome/i,
538
+ /material\s*icons/i,
539
+ /ionicons/i,
540
+ /glyphicons/i,
541
+ /feather/i,
542
+ /bootstrap\s*icons/i,
543
+ /remix\s*icons/i,
544
+ /heroicons/i,
545
+ /layui/i,
546
+ /lucide/i
547
+ ];
548
+ var userIconFonts = [];
549
+ function extendIconFonts(fonts) {
550
+ const list = Array.isArray(fonts) ? fonts : [fonts];
551
+ for (const f of list) {
552
+ if (f instanceof RegExp) {
553
+ userIconFonts.push(f);
554
+ } else if (typeof f === "string") {
555
+ userIconFonts.push(new RegExp(f, "i"));
556
+ } else {
557
+ console.warn("[snapdom] Ignored invalid iconFont value:", f);
558
+ }
559
+ }
560
+ }
561
+ function isIconFont(input) {
562
+ const text = typeof input === "string" ? input : "";
563
+ const candidates = [...defaultIconFonts, ...userIconFonts];
564
+ for (const rx of candidates) {
565
+ if (rx instanceof RegExp && rx.test(text)) return true;
566
+ }
567
+ if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
568
+ return false;
569
+ }
570
+
354
571
  // src/modules/fonts.js
355
572
  async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
356
573
  fontFamily = fontFamily.replace(/^['"]+|['"]+$/g, "");
@@ -375,36 +592,67 @@ async function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, c
375
592
  ctx.fillText(unicodeChar, 0, ascent);
376
593
  return canvas.toDataURL();
377
594
  }
378
- async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } = {}) {
595
+ function isStylesheetLoaded(href) {
596
+ return Array.from(document.styleSheets).some((sheet) => sheet.href === href);
597
+ }
598
+ function injectLinkIfMissing(href) {
599
+ return new Promise((resolve) => {
600
+ if (isStylesheetLoaded(href)) return resolve(null);
601
+ const link = document.createElement("link");
602
+ link.rel = "stylesheet";
603
+ link.href = href;
604
+ link.setAttribute("data-snapdom", "injected-import");
605
+ link.onload = () => resolve(link);
606
+ link.onerror = () => resolve(null);
607
+ document.head.appendChild(link);
608
+ });
609
+ }
610
+ async function embedCustomFonts({ preCached = false } = {}) {
611
+ if (resourceCache.has("fonts-embed-css")) {
612
+ if (preCached) {
613
+ const style = document.createElement("style");
614
+ style.setAttribute("data-snapdom", "embedFonts");
615
+ style.textContent = resourceCache.get("fonts-embed-css");
616
+ document.head.appendChild(style);
617
+ }
618
+ return resourceCache.get("fonts-embed-css");
619
+ }
620
+ const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)/g;
621
+ const styleImports = [];
622
+ for (const styleTag of document.querySelectorAll("style")) {
623
+ const cssText = styleTag.textContent || "";
624
+ const matches = Array.from(cssText.matchAll(importRegex));
625
+ for (const match of matches) {
626
+ const importUrl = match[1];
627
+ if (isIconFont(importUrl)) continue;
628
+ if (!isStylesheetLoaded(importUrl)) {
629
+ styleImports.push(importUrl);
630
+ }
631
+ }
632
+ }
633
+ await Promise.all(styleImports.map(injectLinkIfMissing));
379
634
  const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).filter((link) => link.href);
380
635
  let finalCSS = "";
381
636
  for (const link of links) {
382
637
  try {
383
638
  const res = await fetch(link.href);
384
639
  const cssText = await res.text();
385
- if (ignoreIconFonts && (isIconFont(link.href) || isIconFont(cssText))) {
386
- continue;
387
- }
388
- const urlRegex = /url\(([^)]+)\)/g;
640
+ if (isIconFont(link.href) || isIconFont(cssText)) continue;
641
+ const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
389
642
  const inlinedCSS = await Promise.all(
390
643
  Array.from(cssText.matchAll(urlRegex)).map(async (match) => {
391
644
  let rawUrl = extractURL(match[0]);
392
645
  if (!rawUrl) return null;
393
646
  let url = rawUrl;
394
- if (!url.startsWith("http")) {
647
+ if (!url.startsWith("http") && !url.startsWith("data:")) {
395
648
  url = new URL(url, link.href).href;
396
649
  }
397
- try {
398
- url = encodeURI(url);
399
- } catch (e) {
400
- console.warn("[snapdom] Failed to encode font URL:", url);
401
- }
402
- if (ignoreIconFonts && isIconFont(url)) {
403
- return null;
404
- }
650
+ if (isIconFont(url)) return null;
405
651
  if (resourceCache.has(url)) {
652
+ processedFontURLs.add(url);
406
653
  return { original: match[0], inlined: `url(${resourceCache.get(url)})` };
407
654
  }
655
+ if (processedFontURLs.has(url)) return null;
408
656
  try {
409
657
  const fontRes = await fetch(url);
410
658
  const blob = await fontRes.blob();
@@ -414,18 +662,17 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
414
662
  reader.readAsDataURL(blob);
415
663
  });
416
664
  resourceCache.set(url, b64);
665
+ processedFontURLs.add(url);
417
666
  return { original: match[0], inlined: `url(${b64})` };
418
- } catch (err) {
419
- console.warn("[snapdom] Failed to fetch font:", url);
667
+ } catch (e) {
668
+ console.warn("[snapdom] Failed to fetch font resource:", url);
420
669
  return null;
421
670
  }
422
671
  })
423
672
  );
424
673
  let cssFinal = cssText;
425
674
  for (const r of inlinedCSS) {
426
- if (r) {
427
- cssFinal = cssFinal.replace(r.original, r.inlined);
428
- }
675
+ if (r) cssFinal = cssFinal.replace(r.original, r.inlined);
429
676
  }
430
677
  finalCSS += cssFinal + "\n";
431
678
  } catch (e) {
@@ -438,24 +685,25 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
438
685
  for (const rule of sheet.cssRules) {
439
686
  if (rule.type === CSSRule.FONT_FACE_RULE) {
440
687
  const src = rule.style.getPropertyValue("src");
441
- if (!src) continue;
442
- const urlRegex = /url\(([^)]+)\)/g;
688
+ const family = rule.style.getPropertyValue("font-family");
689
+ if (!src || isIconFont(family)) continue;
690
+ const urlRegex = /url\((["']?)([^"')]+)\1\)/g;
443
691
  let inlinedSrc = src;
444
692
  const matches = Array.from(src.matchAll(urlRegex));
445
693
  for (const match of matches) {
446
- let rawUrl = match[1].trim().replace(/^["']|["']$/g, "");
694
+ let rawUrl = match[2].trim();
447
695
  if (!rawUrl) continue;
448
696
  let url = rawUrl;
449
697
  if (!url.startsWith("http") && !url.startsWith("data:")) {
450
698
  url = new URL(url, sheet.href || location.href).href;
451
699
  }
452
- if (ignoreIconFonts && isIconFont(url)) {
453
- continue;
454
- }
700
+ if (isIconFont(url)) continue;
455
701
  if (resourceCache.has(url)) {
702
+ processedFontURLs.add(url);
456
703
  inlinedSrc = inlinedSrc.replace(match[0], `url(${resourceCache.get(url)})`);
457
704
  continue;
458
705
  }
706
+ if (processedFontURLs.has(url)) continue;
459
707
  try {
460
708
  const res = await fetch(url);
461
709
  const blob = await res.blob();
@@ -465,13 +713,14 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
465
713
  reader.readAsDataURL(blob);
466
714
  });
467
715
  resourceCache.set(url, b64);
716
+ processedFontURLs.add(url);
468
717
  inlinedSrc = inlinedSrc.replace(match[0], `url(${b64})`);
469
718
  } catch (e) {
470
719
  console.warn("[snapdom] Failed to fetch font URL:", url);
471
720
  }
472
721
  }
473
722
  finalCSS += `@font-face {
474
- font-family: ${rule.style.getPropertyValue("font-family")};
723
+ font-family: ${family};
475
724
  src: ${inlinedSrc};
476
725
  font-style: ${rule.style.getPropertyValue("font-style") || "normal"};
477
726
  font-weight: ${rule.style.getPropertyValue("font-weight") || "normal"};
@@ -486,11 +735,13 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
486
735
  }
487
736
  for (const font of document.fonts) {
488
737
  if (font.family && font.status === "loaded" && font._snapdomSrc) {
738
+ if (isIconFont(font.family)) continue;
489
739
  let b64 = font._snapdomSrc;
490
740
  if (!b64.startsWith("data:")) {
491
741
  if (resourceCache.has(font._snapdomSrc)) {
492
742
  b64 = resourceCache.get(font._snapdomSrc);
493
- } else {
743
+ processedFontURLs.add(font._snapdomSrc);
744
+ } else if (!processedFontURLs.has(font._snapdomSrc)) {
494
745
  try {
495
746
  const res = await fetch(font._snapdomSrc);
496
747
  const blob = await res.blob();
@@ -500,6 +751,7 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
500
751
  reader.readAsDataURL(blob);
501
752
  });
502
753
  resourceCache.set(font._snapdomSrc, b64);
754
+ processedFontURLs.add(font._snapdomSrc);
503
755
  } catch (e) {
504
756
  console.warn("[snapdom] Failed to fetch dynamic font src:", font._snapdomSrc);
505
757
  continue;
@@ -515,27 +767,56 @@ async function embedCustomFonts({ ignoreIconFonts = true, preCached = false } =
515
767
  `;
516
768
  }
517
769
  }
518
- if (finalCSS && preCached) {
519
- const style = document.createElement("style");
520
- style.setAttribute("data-snapdom", "embedFonts");
521
- style.textContent = finalCSS;
522
- document.head.appendChild(style);
770
+ if (finalCSS) {
771
+ resourceCache.set("fonts-embed-css", finalCSS);
772
+ if (preCached) {
773
+ const style = document.createElement("style");
774
+ style.setAttribute("data-snapdom", "embedFonts");
775
+ style.textContent = finalCSS;
776
+ document.head.appendChild(style);
777
+ }
523
778
  }
524
779
  return finalCSS;
525
780
  }
526
781
 
527
782
  // src/modules/pseudo.js
528
- async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false) {
783
+ async function inlinePseudoElements(source, clone, styleMap, styleCache, compress, embedFonts = false, useProxy) {
529
784
  if (!(source instanceof Element) || !(clone instanceof Element)) return;
530
- for (const pseudo of ["::before", "::after"]) {
785
+ for (const pseudo of ["::before", "::after", "::first-letter"]) {
531
786
  try {
532
787
  const style = getStyle(source, pseudo);
533
- if (!style) continue;
788
+ if (!style || typeof style[Symbol.iterator] !== "function") continue;
789
+ if (pseudo === "::first-letter") {
790
+ const normal = getComputedStyle(source);
791
+ const isMeaningful = style.color !== normal.color || style.fontSize !== normal.fontSize || style.fontWeight !== normal.fontWeight;
792
+ if (!isMeaningful) continue;
793
+ const textNode = Array.from(clone.childNodes).find(
794
+ (n) => n.nodeType === Node.TEXT_NODE && n.textContent && n.textContent.trim().length > 0
795
+ );
796
+ if (!textNode) continue;
797
+ const text = textNode.textContent;
798
+ const match = text.match(/^([^\p{L}\p{N}\s]*[\p{L}\p{N}](?:['’])?)/u);
799
+ const first = match?.[0];
800
+ const rest = text.slice(first?.length || 0);
801
+ if (!first || /[\uD800-\uDFFF]/.test(first)) continue;
802
+ const span = document.createElement("span");
803
+ span.textContent = first;
804
+ span.dataset.snapdomPseudo = "::first-letter";
805
+ const snapshot = snapshotComputedStyle(style);
806
+ const key = getStyleKey(snapshot, "span", compress);
807
+ styleMap.set(span, key);
808
+ const restNode = document.createTextNode(rest);
809
+ clone.replaceChild(restNode, textNode);
810
+ clone.insertBefore(span, restNode);
811
+ continue;
812
+ }
534
813
  const content = style.getPropertyValue("content");
535
814
  const bg = style.getPropertyValue("background-image");
536
- const hasContent = content && content !== "none" && content !== '""' && content !== "''";
537
- const hasBg = bg && bg.startsWith("url(");
538
- if (hasContent || hasBg) {
815
+ const bgColor = style.getPropertyValue("background-color");
816
+ const hasContent = content !== "none";
817
+ const hasBg = bg && bg !== "none";
818
+ const hasBgColor = bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)";
819
+ if (hasContent || hasBg || hasBgColor) {
539
820
  const fontFamily = style.getPropertyValue("font-family");
540
821
  const fontSize = parseInt(style.getPropertyValue("font-size")) || 32;
541
822
  const fontWeight = parseInt(style.getPropertyValue("font-weight")) || false;
@@ -545,29 +826,21 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
545
826
  const snapshot = snapshotComputedStyle(style);
546
827
  const key = getStyleKey(snapshot, "span", compress);
547
828
  styleMap.set(pseudoEl, key);
548
- const isIconFont2 = fontFamily && /font.*awesome|material|bootstrap|glyphicons|ionicons|remixicon|simple-line-icons|octicons|feather|typicons|weathericons/i.test(
549
- fontFamily
550
- );
551
- let cleanContent = parseContent(content);
552
- if (!embedFonts && isIconFont2 && cleanContent.length === 1) {
829
+ const isIconFont2 = isIconFont(fontFamily);
830
+ const cleanContent = parseContent(content);
831
+ if (isIconFont2 && cleanContent.length === 1) {
553
832
  const imgEl = document.createElement("img");
554
- imgEl.src = await iconToImage(
555
- cleanContent,
556
- fontFamily,
557
- fontWeight,
558
- fontSize,
559
- color
560
- );
561
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
833
+ imgEl.src = await iconToImage(cleanContent, fontFamily, fontWeight, fontSize, color);
834
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
562
835
  pseudoEl.appendChild(imgEl);
563
836
  } else if (cleanContent.startsWith("url(")) {
564
837
  const rawUrl = extractURL(cleanContent);
565
- if (rawUrl) {
838
+ if (rawUrl && rawUrl.trim() !== "") {
566
839
  try {
567
840
  const imgEl = document.createElement("img");
568
- const dataUrl = await fetchImage(encodeURI(rawUrl));
841
+ const dataUrl = await fetchImage(safeEncodeURI(rawUrl, { useProxy }));
569
842
  imgEl.src = dataUrl;
570
- imgEl.style = "display:block;width:100%;height:100%;object-fit:contain;";
843
+ imgEl.style = `width:${fontSize}px;height:auto;object-fit:contain;`;
571
844
  pseudoEl.appendChild(imgEl);
572
845
  } catch (e) {
573
846
  console.error(`[snapdom] Error in pseudo ${pseudo} for`, source, e);
@@ -576,24 +849,22 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
576
849
  } else if (!isIconFont2 && cleanContent && cleanContent !== "none") {
577
850
  pseudoEl.textContent = cleanContent;
578
851
  }
579
- if (hasBg && bg) {
580
- const rawUrl = extractURL(bg);
581
- if (rawUrl) {
582
- try {
583
- const dataUrl = await fetchImage(encodeURI(rawUrl));
584
- pseudoEl.style.backgroundImage = `url(${dataUrl})`;
585
- } catch (e) {
586
- console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
587
- }
852
+ if (hasBg) {
853
+ try {
854
+ const bgSplits = splitBackgroundImage(bg);
855
+ const newBgParts = await Promise.all(
856
+ bgSplits.map((entry) => inlineSingleBackgroundEntry(entry))
857
+ );
858
+ pseudoEl.style.backgroundImage = newBgParts.join(", ");
859
+ } catch (e) {
860
+ console.warn(`[snapdom] Failed to inline background-image for ${pseudo}`, e);
588
861
  }
589
862
  }
863
+ if (hasBgColor) pseudoEl.style.backgroundColor = bgColor;
590
864
  const hasContent2 = pseudoEl.childNodes.length > 0 || pseudoEl.textContent && pseudoEl.textContent.trim() !== "";
591
- if (!hasContent2 && !hasBg) continue;
592
- if (pseudo === "::before") {
593
- clone.insertBefore(pseudoEl, clone.firstChild);
594
- } else {
595
- clone.appendChild(pseudoEl);
596
- }
865
+ const hasVisibleBox = hasContent2 || hasBg || hasBgColor;
866
+ if (!hasVisibleBox) continue;
867
+ pseudo === "::before" ? clone.insertBefore(pseudoEl, clone.firstChild) : clone.appendChild(pseudoEl);
597
868
  }
598
869
  } catch (e) {
599
870
  console.warn(`[snapdom] Failed to capture ${pseudo} for`, source, e);
@@ -608,28 +879,64 @@ async function inlinePseudoElements(source, clone, styleMap, styleCache, compres
608
879
  styleMap,
609
880
  styleCache,
610
881
  compress,
611
- embedFonts
882
+ embedFonts,
883
+ useProxy
612
884
  );
613
885
  }
614
886
  }
615
887
 
888
+ // src/modules/svgDefs.js
889
+ function inlineExternalDef(root) {
890
+ if (!root) return;
891
+ const defsSources = document.querySelectorAll("svg > defs");
892
+ if (!defsSources.length) return;
893
+ root.querySelectorAll("svg").forEach((svg) => {
894
+ const uses = svg.querySelectorAll("use");
895
+ if (!uses.length) return;
896
+ const usedIds = /* @__PURE__ */ new Set();
897
+ uses.forEach((use) => {
898
+ const href = use.getAttribute("xlink:href") || use.getAttribute("href");
899
+ if (href && href.startsWith("#")) usedIds.add(href.slice(1));
900
+ });
901
+ if (!usedIds.size) return;
902
+ const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
903
+ for (const id of usedIds) {
904
+ for (const source of defsSources) {
905
+ const def = source.querySelector(`#${CSS.escape(id)}`);
906
+ if (def) {
907
+ defs.appendChild(def.cloneNode(true));
908
+ break;
909
+ }
910
+ }
911
+ }
912
+ if (defs.childNodes.length) {
913
+ svg.insertBefore(defs, svg.firstChild);
914
+ }
915
+ });
916
+ }
917
+
616
918
  // src/core/prepare.js
617
- async function prepareClone(element, compress = false, embedFonts = false) {
919
+ async function prepareClone(element, compress = false, embedFonts = false, options = {}) {
618
920
  const styleMap = /* @__PURE__ */ new Map();
619
921
  const styleCache = /* @__PURE__ */ new WeakMap();
620
922
  const nodeMap = /* @__PURE__ */ new Map();
621
923
  let clone;
622
924
  try {
623
- clone = deepClone(element, styleMap, styleCache, nodeMap, compress);
925
+ clone = deepClone(element, styleMap, styleCache, nodeMap, compress, options, element);
624
926
  } catch (e) {
625
927
  console.warn("deepClone failed:", e);
626
928
  throw e;
627
929
  }
628
930
  try {
629
- await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts);
931
+ await inlinePseudoElements(element, clone, styleMap, styleCache, compress, embedFonts, options.useProxy);
630
932
  } catch (e) {
631
933
  console.warn("inlinePseudoElements failed:", e);
632
934
  }
935
+ try {
936
+ inlineExternalDef(clone);
937
+ } catch (e) {
938
+ console.warn("inlineExternalDef failed:", e);
939
+ }
633
940
  let classCSS = "";
634
941
  if (compress) {
635
942
  const keyToClass = generateCSSClasses(styleMap);
@@ -667,6 +974,27 @@ async function prepareClone(element, compress = false, embedFonts = false) {
667
974
  cloneNode.appendChild(inner);
668
975
  }
669
976
  }
977
+ if (element === nodeMap.get(clone)) {
978
+ const computed = styleCache.get(element) || window.getComputedStyle(element);
979
+ styleCache.set(element, computed);
980
+ const transform = stripTranslate(computed.transform);
981
+ clone.style.margin = "0";
982
+ clone.style.position = "static";
983
+ clone.style.top = "auto";
984
+ clone.style.left = "auto";
985
+ clone.style.right = "auto";
986
+ clone.style.bottom = "auto";
987
+ clone.style.zIndex = "auto";
988
+ clone.style.float = "none";
989
+ clone.style.clear = "none";
990
+ clone.style.transform = transform || "";
991
+ }
992
+ for (const [cloneNode, originalNode] of nodeMap.entries()) {
993
+ if (originalNode.tagName === "PRE") {
994
+ cloneNode.style.marginTop = "0";
995
+ cloneNode.style.marginBlockStart = "0";
996
+ }
997
+ }
670
998
  return { clone, classCSS, styleCache };
671
999
  }
672
1000
 
@@ -676,8 +1004,7 @@ async function inlineImages(clone, options = {}) {
676
1004
  const processImg = async (img) => {
677
1005
  const src = img.src;
678
1006
  try {
679
- const crossOrigin = options.crossOrigin ? options.crossOrigin(src) : "anonymous";
680
- const dataUrl = await fetchImage(src, 3e3, crossOrigin);
1007
+ const dataUrl = await fetchImage(src, { useProxy: options.useProxy });
681
1008
  img.src = dataUrl;
682
1009
  if (!img.width) img.width = img.naturalWidth || 100;
683
1010
  if (!img.height) img.height = img.naturalHeight || 100;
@@ -702,28 +1029,24 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
702
1029
  const style = styleCache.get(srcNode) || getStyle(srcNode);
703
1030
  if (!styleCache.has(srcNode)) styleCache.set(srcNode, style);
704
1031
  const bg = style.getPropertyValue("background-image");
705
- const bgSplits = bg.split(",");
706
- for (let i = 0; i < bgSplits.length; i++) {
707
- const rawUrl = extractURL(bgSplits[i]);
708
- if (rawUrl) {
709
- try {
710
- let bgUrl = encodeURI(rawUrl);
711
- let dataUrl;
712
- if (bgCache.has(bgUrl)) {
713
- dataUrl = bgCache.get(bgUrl);
714
- } else {
715
- const crossOrigin = options.crossOrigin ? options.crossOrigin(bgUrl) : "anonymous";
716
- dataUrl = await fetchImage(bgUrl, 3e3, crossOrigin);
717
- bgCache.set(bgUrl, dataUrl);
718
- }
719
- bgSplits[i] = `url(${dataUrl})`;
720
- } catch {
721
- bgSplits[i] = "none";
722
- }
1032
+ const bgColor = style.getPropertyValue("background-color");
1033
+ if (!bg || bg === "none") {
1034
+ const sChildren2 = Array.from(srcNode.children);
1035
+ const cChildren2 = Array.from(cloneNode.children);
1036
+ for (let i = 0; i < Math.min(sChildren2.length, cChildren2.length); i++) {
1037
+ queue.push([sChildren2[i], cChildren2[i]]);
723
1038
  }
1039
+ continue;
724
1040
  }
725
- if (bgSplits.some((s) => s.includes("url("))) {
726
- cloneNode.style.backgroundImage = bgSplits.join(",");
1041
+ const bgSplits = splitBackgroundImage(bg);
1042
+ const newBgParts = await Promise.all(
1043
+ bgSplits.map((entry) => inlineSingleBackgroundEntry(entry, options))
1044
+ );
1045
+ if (newBgParts.some((p) => p && p !== "none" && !/^url\(undefined\)/.test(p))) {
1046
+ cloneNode.style.backgroundImage = newBgParts.join(", ");
1047
+ }
1048
+ if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") {
1049
+ cloneNode.style.backgroundColor = bgColor;
727
1050
  }
728
1051
  const sChildren = Array.from(srcNode.children);
729
1052
  const cChildren = Array.from(cloneNode.children);
@@ -736,13 +1059,13 @@ async function inlineBackgroundImages(source, clone, styleCache, options = {}) {
736
1059
  // src/core/capture.js
737
1060
  async function captureDOM(element, options = {}) {
738
1061
  if (!element) throw new Error("Element cannot be null or undefined");
739
- const { compress = true, embedFonts = false, fast = true, scale = 1 } = options;
1062
+ const { compress = true, embedFonts = false, fast = true, scale = 1, useProxy = "" } = options;
740
1063
  let clone, classCSS, styleCache;
741
1064
  let fontsCSS = "";
742
1065
  let baseCSS = "";
743
1066
  let dataURL;
744
1067
  let svgString;
745
- ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts));
1068
+ ({ clone, classCSS, styleCache } = await prepareClone(element, compress, embedFonts, options));
746
1069
  await new Promise((resolve) => {
747
1070
  idle(async () => {
748
1071
  await inlineImages(clone, options);
@@ -758,7 +1081,7 @@ async function captureDOM(element, options = {}) {
758
1081
  if (embedFonts) {
759
1082
  await new Promise((resolve) => {
760
1083
  idle(async () => {
761
- fontsCSS = await embedCustomFonts({ ignoreIconFonts: !embedFonts });
1084
+ fontsCSS = await embedCustomFonts();
762
1085
  resolve();
763
1086
  }, { fast });
764
1087
  });
@@ -781,11 +1104,37 @@ async function captureDOM(element, options = {}) {
781
1104
  await new Promise((resolve) => {
782
1105
  idle(() => {
783
1106
  const rect = element.getBoundingClientRect();
784
- const w = Math.ceil(rect.width);
785
- const h = Math.ceil(rect.height);
1107
+ let w = rect.width;
1108
+ let h = rect.height;
1109
+ const hasW = Number.isFinite(options.width);
1110
+ const hasH = Number.isFinite(options.height);
1111
+ const hasScale = typeof scale === "number" && scale !== 1;
1112
+ if (!hasScale) {
1113
+ const aspect = rect.width / rect.height;
1114
+ if (hasW && hasH) {
1115
+ w = options.width;
1116
+ h = options.height;
1117
+ } else if (hasW) {
1118
+ w = options.width;
1119
+ h = w / aspect;
1120
+ } else if (hasH) {
1121
+ h = options.height;
1122
+ w = h * aspect;
1123
+ }
1124
+ }
1125
+ w = Math.ceil(w);
1126
+ h = Math.ceil(h);
786
1127
  clone.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
787
1128
  clone.style.transformOrigin = "top left";
788
- if (scale !== 1 && isSafari()) {
1129
+ if (!hasScale && (hasW || hasH)) {
1130
+ const originalW = rect.width;
1131
+ const originalH = rect.height;
1132
+ const scaleX = w / originalW;
1133
+ const scaleY = h / originalH;
1134
+ const existingTransform = clone.style.transform || "";
1135
+ const scaleTransform = `scale(${scaleX}, ${scaleY})`;
1136
+ clone.style.transform = `${scaleTransform} ${existingTransform}`.trim();
1137
+ } else if (hasScale && isSafari()) {
789
1138
  clone.style.scale = `${scale}`;
790
1139
  }
791
1140
  const svgNS = "http://www.w3.org/2000/svg";
@@ -840,9 +1189,26 @@ async function toCanvas(url, { dpr = 1, scale = 1 } = {}) {
840
1189
  canvas.style.height = `${height}px`;
841
1190
  return canvas;
842
1191
  }
843
- async function toBlob(url) {
844
- const svgText = decodeURIComponent(url.split(",")[1]);
845
- return new Blob([svgText], { type: "image/svg+xml" });
1192
+ async function toBlob(url, {
1193
+ type = "svg",
1194
+ scale = 1,
1195
+ backgroundColor = "#fff",
1196
+ quality
1197
+ } = {}) {
1198
+ const mime = {
1199
+ jpg: "image/jpeg",
1200
+ jpeg: "image/jpeg",
1201
+ png: "image/png",
1202
+ webp: "image/webp"
1203
+ }[type] || "image/png";
1204
+ if (type === "svg") {
1205
+ const svgText = decodeURIComponent(url.split(",")[1]);
1206
+ return new Blob([svgText], { type: "image/svg+xml" });
1207
+ }
1208
+ const canvas = await createBackground(url, { dpr: 1, scale }, backgroundColor);
1209
+ return new Promise((resolve) => {
1210
+ canvas.toBlob((blob) => resolve(blob), `${mime}`, quality);
1211
+ });
846
1212
  }
847
1213
  async function createBackground(url, { dpr = 1, scale = 1 }, backgroundColor) {
848
1214
  const baseCanvas = await toCanvas(url, { dpr, scale });
@@ -894,6 +1260,9 @@ async function download(url, { dpr = 1, scale = 1, backgroundColor = "#fff", for
894
1260
  async function snapdom(element, options = {}) {
895
1261
  options = { scale: 1, ...options };
896
1262
  if (!element) throw new Error("Element cannot be null or undefined");
1263
+ if (options.iconFonts) {
1264
+ extendIconFonts(options.iconFonts);
1265
+ }
897
1266
  return await snapdom.capture(element, options);
898
1267
  }
899
1268
  snapdom.capture = async (el, options = {}) => {
@@ -906,7 +1275,7 @@ snapdom.capture = async (el, options = {}) => {
906
1275
  toRaw: () => url,
907
1276
  toImg: () => toImg(url, { dpr, scale }),
908
1277
  toCanvas: () => toCanvas(url, { dpr, scale }),
909
- toBlob: () => toBlob(url),
1278
+ toBlob: (options2) => toBlob(url, { dpr, scale, ...options2 }),
910
1279
  toPng: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "png"),
911
1280
  toJpg: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "jpeg"),
912
1281
  toWebp: (options2) => toRasterImg(url, { dpr, scale, ...options2 }, "webp"),
@@ -916,7 +1285,7 @@ snapdom.capture = async (el, options = {}) => {
916
1285
  snapdom.toRaw = async (el, options) => (await snapdom.capture(el, options)).toRaw();
917
1286
  snapdom.toImg = async (el, options) => (await snapdom.capture(el, options)).toImg();
918
1287
  snapdom.toCanvas = async (el, options) => (await snapdom.capture(el, options)).toCanvas();
919
- snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob();
1288
+ snapdom.toBlob = async (el, options) => (await snapdom.capture(el, options)).toBlob(options);
920
1289
  snapdom.toPng = async (el, options) => (await snapdom.capture(el, options)).toPng(options);
921
1290
  snapdom.toJpg = async (el, options) => (await snapdom.capture(el, options)).toJpg(options);
922
1291
  snapdom.toWebp = async (el, options) => (await snapdom.capture(el, options)).toWebp(options);
@@ -933,13 +1302,12 @@ snapdom.download = async (el, options = {}) => {
933
1302
 
934
1303
  // src/api/preCache.js
935
1304
  async function preCache(root = document, options = {}) {
936
- const { embedFonts = true, reset = false, crossOrigin: crossOriginFn } = options;
1305
+ const { embedFonts = true, reset = false } = options;
937
1306
  if (reset) {
938
1307
  imageCache.clear();
939
1308
  bgCache.clear();
940
1309
  resourceCache.clear();
941
1310
  baseCSSCache.clear();
942
- computedStyleCache.clear();
943
1311
  return;
944
1312
  }
945
1313
  await document.fonts.ready;
@@ -953,26 +1321,29 @@ async function preCache(root = document, options = {}) {
953
1321
  for (const img of imgEls) {
954
1322
  const src = img.src;
955
1323
  if (!imageCache.has(src)) {
956
- const crossOrigin = crossOriginFn ? crossOriginFn(src) : "anonymous";
957
1324
  promises.push(
958
- fetchImage(src, 3e3, crossOrigin).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
1325
+ fetchImage(src, { useProxy: options.useProxy }).then((dataURL) => imageCache.set(src, dataURL)).catch(() => {
959
1326
  })
960
1327
  );
961
1328
  }
962
1329
  }
963
1330
  for (const el of allEls) {
964
- const bg = getComputedStyle(el).backgroundImage;
965
- const url = extractURL(bg);
966
- if (url && !bgCache.has(url)) {
967
- const crossOrigin = crossOriginFn ? crossOriginFn(url) : "anonymous";
968
- promises.push(
969
- fetchImage(url, 3e3, crossOrigin).then((dataURL) => bgCache.set(url, dataURL)).catch(() => {
970
- })
971
- );
1331
+ const bg = getStyle(el).backgroundImage;
1332
+ if (bg && bg !== "none") {
1333
+ const bgSplits = splitBackgroundImage(bg);
1334
+ for (const entry of bgSplits) {
1335
+ const isUrl = entry.startsWith("url(");
1336
+ if (isUrl) {
1337
+ promises.push(
1338
+ inlineSingleBackgroundEntry(entry, options).catch(() => {
1339
+ })
1340
+ );
1341
+ }
1342
+ }
972
1343
  }
973
1344
  }
974
1345
  if (embedFonts) {
975
- await embedCustomFonts({ ignoreIconFonts: !embedFonts, preCached: true });
1346
+ await embedCustomFonts({ preCached: true });
976
1347
  }
977
1348
  await Promise.all(promises);
978
1349
  }