@zumer/snapdom 1.7.1 → 1.9.3-next-dev.0

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