@zumer/snapdom 2.0.0-dev.0 → 2.0.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -72,8 +72,8 @@ It captures any HTML element as a scalable SVG image, preserving styles, fonts,
72
72
  - [iconFonts](#iconfonts)
73
73
  - [excludeFonts](#excludefonts)
74
74
  - [Filtering nodes: `exclude` vs `filter`](#filtering-nodes-exclude-vs-filter)
75
- - [straighten](#straighten)
76
- - [noShadows](#no-shadows)
75
+ - [outerTransforms](#outerTransforms)
76
+ - [outerShadows](#no-shadows)
77
77
  - [Cache control](#cache-control)
78
78
  - [preCache](#precache--optional-helper)
79
79
  - [Plugins (BETA)](#plugins-beta)
@@ -228,8 +228,8 @@ All capture methods accept an `options` object:
228
228
  | `cache` | string | `"soft"` | `disabled` \| `soft` \| `auto` \| `full` |
229
229
  | `placeholders` | boolean | `true` | Show placeholders for images/CORS iframes |
230
230
  | `fallbackURL` | string \| function | - | Fallback image for `<img>` load failure |
231
- | `straighten` | boolean | `false` | Straightens the root: removes `translate/rotate` but preserves `scale/skew`, producing a flat, reusable capture |
232
- | `noShadows` | boolean | `false` | Do not expand the root’s bounding box for shadows/blur/outline, and strip those visual effects from the cloned root |
231
+ | `outerTransforms` | boolean | `true` | When `false` removes `translate/rotate` but preserves `scale/skew`, producing a flat, reusable capture |
232
+ | `outerShadows` | boolean | `false` | Do not expand the root’s bounding box for shadows/blur/outline, and strip those visual effects from the cloned root |
233
233
 
234
234
  ### Fallback image on `<img>` load failure
235
235
 
@@ -354,26 +354,25 @@ await snapdom.toPng(el, {
354
354
  });
355
355
  ```
356
356
 
357
- ### Straighten
357
+ ### outerTransforms
358
358
 
359
- When capturing rotated or translated elements, you may want to **straighten** the root so the snapshot can be reused in another layout without inheriting those transforms.
359
+ When capturing rotated or translated elements, you may want use **outerTransforms: false** option if you want to eliminate those external transforms. So, the output is **flat, upright, and ready** to use elsewhere.
360
360
 
361
- - **`straighten: true`**
362
- Straightens the cloned root: **removes `translate` and `rotate`** but **keeps `scale/skew`** to preserve proportions.
363
- The output is **flat, upright, and ready** to embed elsewhere.
361
+ - **`outerTransforms: true (default)`**
362
+ **Keeps the original `transforms` and `rotate`**.
363
+
364
364
 
365
-
366
- ### noShadows
367
- - **`noShadows: true`**
365
+ ### outerShadows
366
+ - **`outerShadows: false (default)`**
368
367
  Prevents expanding the bounding box for shadows, blur, or outline on the root, and also strips `box-shadow`, `text-shadow`, `filter: blur()/drop-shadow()`, and `outline` from the cloned root.
369
368
 
370
- > 💡 **Tip:** Using both (`straighten` + `noShadows`) produces a strict, minimal bounding box with no visual bleed.
369
+ > 💡 **Tip:** Using both (`outerTransforms: false` + `outerShadows: false`) produces a strict, minimal bounding box with no visual bleed.
371
370
 
372
371
  **Example**
373
372
 
374
373
  ```js
375
- // Straighten and remove shadow bleed
376
- await snapdom.toSvg(el, { straighten: true, noShadows: true });
374
+ // outerTransforms and remove shadow bleed
375
+ await snapdom.toSvg(el, { outerTransforms: true, outerShadows: true });
377
376
  ```
378
377
 
379
378
  ## Cache control
@@ -474,7 +473,7 @@ const out = await snapdom(element, {
474
473
  Every hook receives a single `context` object that contains normalized capture state:
475
474
 
476
475
  * **Input & options:**
477
- `element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `straighten`, `noShadows`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
476
+ `element`, `debug`, `fast`, `scale`, `dpr`, `width`, `height`, `backgroundColor`, `quality`, `useProxy`, `cache`, `outerTransforms`, `outerShadows`, `embedFonts`, `localFonts`, `iconFonts`, `excludeFonts`, `exclude`, `excludeMode`, `filter`, `filterMode`, `fallbackURL`.
478
477
 
479
478
  * **Intermediate values (depending on stage):**
480
479
  `clone`, `classCSS`, `styleCache`, `fontsCSS`, `baseCSS`, `svgString`, `dataURL`.
package/dist/snapdom.js CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  /*
3
3
  * snapdom
4
- * v.2.0.0-dev.0
4
+ * v.2.0.0-dev.1
5
5
  * Author Juan Martin Muda
6
6
  * License MIT
7
7
  **/
@@ -1504,6 +1504,130 @@
1504
1504
  if (/icon/i.test(text) || /glyph/i.test(text) || /symbols/i.test(text) || /feather/i.test(text) || /fontawesome/i.test(text)) return true;
1505
1505
  return false;
1506
1506
  }
1507
+ function isMaterialFamily(family = "") {
1508
+ const s = String(family).toLowerCase();
1509
+ return /\bmaterial\s*icons\b/.test(s) || /\bmaterial\s*symbols\b/.test(s);
1510
+ }
1511
+ function ensureMaterialFontsReady(family = "Material Icons", px = 24) {
1512
+ return __async(this, null, function* () {
1513
+ try {
1514
+ yield Promise.all([
1515
+ document.fonts.load(`400 ${px}px "${family.replace(/["']/g, "")}"`),
1516
+ document.fonts.ready
1517
+ ]);
1518
+ } catch (e) {
1519
+ }
1520
+ });
1521
+ }
1522
+ function resolvePaintColor(cs) {
1523
+ var _a, _b;
1524
+ let fill = ((_a = cs.getPropertyValue("-webkit-text-fill-color")) == null ? void 0 : _a.trim()) || "";
1525
+ const isTransparent = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)/i.test(fill);
1526
+ if (fill && !isTransparent && fill.toLowerCase() !== "currentcolor") {
1527
+ return fill;
1528
+ }
1529
+ const c = (_b = cs.color) == null ? void 0 : _b.trim();
1530
+ return c && c !== "inherit" ? c : "#000";
1531
+ }
1532
+ function materialIconToImage(_0) {
1533
+ return __async(this, arguments, function* (ligatureText, {
1534
+ family = "Material Icons",
1535
+ weight = "normal",
1536
+ fontSize = 32,
1537
+ color = "#000",
1538
+ variation = ""
1539
+ } = {}) {
1540
+ const fam = String(family || "").replace(/^['"]+|['"]+$/g, "");
1541
+ const dpr = window.devicePixelRatio || 1;
1542
+ yield ensureMaterialFontsReady(fam, fontSize);
1543
+ const span = document.createElement("span");
1544
+ span.textContent = ligatureText;
1545
+ span.style.position = "absolute";
1546
+ span.style.visibility = "hidden";
1547
+ span.style.left = "-99999px";
1548
+ span.style.whiteSpace = "nowrap";
1549
+ span.style.fontFamily = `"${fam}"`;
1550
+ span.style.fontWeight = String(weight || "normal");
1551
+ span.style.fontSize = `${fontSize}px`;
1552
+ span.style.lineHeight = "1";
1553
+ span.style.margin = "0";
1554
+ span.style.padding = "0";
1555
+ span.style.fontFeatureSettings = "'liga' 1";
1556
+ span.style.fontVariantLigatures = "normal";
1557
+ if (variation) span.style.fontVariationSettings = variation;
1558
+ span.style.color = color;
1559
+ document.body.appendChild(span);
1560
+ const rect = span.getBoundingClientRect();
1561
+ const width = Math.max(1, Math.ceil(rect.width));
1562
+ const height = Math.max(1, Math.ceil(rect.height));
1563
+ document.body.removeChild(span);
1564
+ const canvas = document.createElement("canvas");
1565
+ canvas.width = width * dpr;
1566
+ canvas.height = height * dpr;
1567
+ const ctx = canvas.getContext("2d");
1568
+ ctx.scale(dpr, dpr);
1569
+ ctx.font = `${weight ? `${weight} ` : ""}${fontSize}px "${fam}"`;
1570
+ ctx.textAlign = "left";
1571
+ ctx.textBaseline = "top";
1572
+ ctx.fillStyle = color;
1573
+ try {
1574
+ ctx.fontKerning = "normal";
1575
+ } catch (e) {
1576
+ }
1577
+ ctx.fillText(ligatureText, 0, 0);
1578
+ return {
1579
+ dataUrl: canvas.toDataURL(),
1580
+ width,
1581
+ height
1582
+ };
1583
+ });
1584
+ }
1585
+ function ligatureIconToImage(cloneRoot, sourceRoot) {
1586
+ return __async(this, null, function* () {
1587
+ if (!(cloneRoot instanceof Element)) return 0;
1588
+ const selector = '.material-icons, [class*="material-symbols"]';
1589
+ const cloneNodes = Array.from(
1590
+ cloneRoot.querySelectorAll(selector)
1591
+ ).filter((n) => n && n.textContent && n.textContent.trim());
1592
+ if (cloneNodes.length === 0) return 0;
1593
+ const sourceNodes = sourceRoot instanceof Element ? Array.from(sourceRoot.querySelectorAll(selector)).filter((n) => n && n.textContent && n.textContent.trim()) : [];
1594
+ let replaced = 0;
1595
+ for (let i = 0; i < cloneNodes.length; i++) {
1596
+ const el = cloneNodes[i];
1597
+ const src = sourceNodes[i] || null;
1598
+ try {
1599
+ const csSrc = src ? getComputedStyle(src) : getComputedStyle(el);
1600
+ const family = csSrc.fontFamily || "Material Icons";
1601
+ if (!isMaterialFamily(family)) continue;
1602
+ const text = (src || el).textContent.trim();
1603
+ if (!text) continue;
1604
+ const size = parseInt(csSrc.fontSize, 10) || 24;
1605
+ const weight = csSrc.fontWeight && csSrc.fontWeight !== "normal" ? csSrc.fontWeight : "normal";
1606
+ const color = resolvePaintColor(csSrc);
1607
+ const variation = csSrc.fontVariationSettings && csSrc.fontVariationSettings !== "normal" ? csSrc.fontVariationSettings : "";
1608
+ const { dataUrl, width, height } = yield materialIconToImage(text, {
1609
+ family,
1610
+ weight,
1611
+ fontSize: size,
1612
+ color,
1613
+ variation
1614
+ });
1615
+ el.textContent = "";
1616
+ const img = el.ownerDocument.createElement("img");
1617
+ img.src = dataUrl;
1618
+ img.alt = text;
1619
+ img.style.height = `${size}px`;
1620
+ img.style.width = `${Math.max(1, Math.round(width / height * size))}px`;
1621
+ img.style.objectFit = "contain";
1622
+ img.style.verticalAlign = getComputedStyle(el).verticalAlign || "baseline";
1623
+ el.appendChild(img);
1624
+ replaced++;
1625
+ } catch (e) {
1626
+ }
1627
+ }
1628
+ return replaced;
1629
+ });
1630
+ }
1507
1631
 
1508
1632
  // src/modules/fonts.js
1509
1633
  function iconToImage(unicodeChar, fontFamily, fontWeight, fontSize = 32, color = "#000") {
@@ -3563,8 +3687,8 @@ ${imported}
3563
3687
  if (!element) throw new Error("Element cannot be null or undefined");
3564
3688
  applyCachePolicy(options.cache);
3565
3689
  const fast = options.fast;
3566
- const straighten = !!options.straighten;
3567
- const noShadows = !!options.noShadows;
3690
+ const outerTransforms = options.outerTransforms !== false;
3691
+ const outerShadows = !!options.outerShadows;
3568
3692
  let state = { element, options, plugins: options.plugins };
3569
3693
  let clone, classCSS, styleCache;
3570
3694
  let fontsCSS = "";
@@ -3577,17 +3701,21 @@ ${imported}
3577
3701
  const undoClamp = lineClamp(state.element);
3578
3702
  try {
3579
3703
  ({ clone, classCSS, styleCache } = yield prepareClone(state.element, state.options));
3580
- if (straighten && clone) {
3581
- rootTransform2D = normalizeRootTransforms(state.element, state.clone);
3704
+ if (!outerTransforms && clone) {
3705
+ rootTransform2D = normalizeRootTransforms(state.element, clone);
3582
3706
  }
3583
- if (noShadows && clone) {
3584
- stripRootShadows(state.element, state.clone);
3707
+ if (!outerShadows && clone) {
3708
+ stripRootShadows(state.element, clone);
3585
3709
  }
3586
3710
  } finally {
3587
3711
  undoClamp();
3588
3712
  }
3589
3713
  state = __spreadValues({ clone, classCSS, styleCache }, state);
3590
3714
  yield runHook("afterClone", state);
3715
+ try {
3716
+ yield ligatureIconToImage(state.clone, state.element);
3717
+ } catch (e) {
3718
+ }
3591
3719
  yield new Promise((resolve) => {
3592
3720
  idle(() => __async(null, null, function* () {
3593
3721
  yield inlineImages(state.clone, state.options);
@@ -3667,7 +3795,7 @@ ${imported}
3667
3795
  h = h0;
3668
3796
  }
3669
3797
  let minX = 0, minY = 0, maxX = w0, maxY = h0;
3670
- if (straighten && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3798
+ if (!outerTransforms && rootTransform2D && Number.isFinite(rootTransform2D.a)) {
3671
3799
  const M2 = {
3672
3800
  a: rootTransform2D.a,
3673
3801
  b: rootTransform2D.b || 0,
@@ -3682,7 +3810,7 @@ ${imported}
3682
3810
  maxX = limitDecimals(bb2.maxX);
3683
3811
  maxY = limitDecimals(bb2.maxY);
3684
3812
  } else {
3685
- const useTFBBox = !straighten && hasTFBBox(state.element);
3813
+ const useTFBBox = outerTransforms && hasTFBBox(state.element);
3686
3814
  if (useTFBBox) {
3687
3815
  const baseTransform2 = csEl.transform && csEl.transform !== "none" ? csEl.transform : "";
3688
3816
  const ind2 = readIndividualTransforms(state.element);
@@ -3705,7 +3833,7 @@ ${imported}
3705
3833
  const bleedBlur = parseFilterBlur(csEl);
3706
3834
  const bleedOutline = parseOutline(csEl);
3707
3835
  const drop = parseFilterDropShadows(csEl);
3708
- const bleed = noShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3836
+ const bleed = !outerShadows ? { top: 0, right: 0, bottom: 0, left: 0 } : {
3709
3837
  top: limitDecimals(bleedShadow.top + bleedBlur.top + bleedOutline.top + drop.bleed.top),
3710
3838
  right: limitDecimals(bleedShadow.right + bleedBlur.right + bleedOutline.right + drop.bleed.right),
3711
3839
  bottom: limitDecimals(bleedShadow.bottom + bleedBlur.bottom + bleedOutline.bottom + drop.bleed.bottom),
@@ -3723,7 +3851,7 @@ ${imported}
3723
3851
  const outH = Math.max(1, limitDecimals(vbH0 * scaleH));
3724
3852
  const svgNS = "http://www.w3.org/2000/svg";
3725
3853
  const basePad = isSafari() ? 1 : 0;
3726
- const extraPad = straighten ? 1 : 0;
3854
+ const extraPad = !outerTransforms ? 1 : 0;
3727
3855
  const pad = limitDecimals(basePad + extraPad);
3728
3856
  const fo = document.createElementNS(svgNS, "foreignObject");
3729
3857
  const vbMinX = limitDecimals(minX);
@@ -4085,8 +4213,8 @@ ${imported}
4085
4213
  backgroundColor: (_q = options.backgroundColor) != null ? _q : ["jpeg", "webp"].includes(resolvedFormat) ? "#ffffff" : null,
4086
4214
  filename: (_r = options.filename) != null ? _r : "snapDOM",
4087
4215
  // NEW flags (user-friendly)
4088
- straighten: (_s = options.straighten) != null ? _s : false,
4089
- noShadows: (_t = options.noShadows) != null ? _t : false
4216
+ outerTransforms: (_s = options.outerTransforms) != null ? _s : true,
4217
+ outerShadows: (_t = options.outerShadows) != null ? _t : false
4090
4218
  // Plugins (reservado)
4091
4219
  // plugins: normalizePlugins(...),
4092
4220
  };