html2canvas-pro 2.6.0 → 2.6.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
@@ -12,7 +12,7 @@ Next generation JavaScript screenshot tool.
12
12
  <a href="https://npm.im/html2canvas-pro"><img src="https://badgen.net/npm/v/html2canvas-pro" alt="npm version"></a>
13
13
  <a href="http://npm.im/html2canvas-pro"><img src="https://badgen.net/npm/dm/html2canvas-pro" alt="npm downloads"></a>
14
14
  <a href="https://www.jsdelivr.com/package/npm/html2canvas-pro"><img src="https://data.jsdelivr.com/v1/package/npm/html2canvas-pro/badge" /></a>
15
- <a href="https://deepwiki.com/yorickshan/html2canvas-pro"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
15
+ <a href="https://deepwiki.com/yorickshan/html2canvas-pro"><img src=".github/assets/deepwiki-badge.svg" alt="Ask DeepWiki"></a>
16
16
  </p>
17
17
  <p align="center">
18
18
  <a href="https://yorickshan.github.io/html2canvas-pro/getting-started.html">Getting Started</a> ·
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 2.6.0 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.6.1 <https://yorickshan.github.io/html2canvas-pro/>
3
3
  * Copyright (c) 2024-present yorickshan and html2canvas-pro contributors
4
4
  * Released under MIT License
5
5
  */
@@ -7917,19 +7917,19 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
7917
7917
  }`;
7918
7918
  /**
7919
7919
  * Inject the stylesheet that hides pseudo-element surrogate containers.
7920
- * Called on the cloned <body> so the surrogates themselves never render.
7920
+ * Added to the cloned <head> so the surrogates themselves never render.
7921
7921
  */
7922
- const createPseudoHideStyles = (body, cspNonce) => {
7923
- createStyles(body, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
7922
+ const createPseudoHideStyles = (parent, cspNonce) => {
7923
+ createStyles(parent, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
7924
7924
  .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
7925
7925
  };
7926
- const createStyles = (body, styles, cspNonce) => {
7927
- const document = body.ownerDocument;
7926
+ const createStyles = (parent, styles, cspNonce) => {
7927
+ const document = parent.ownerDocument;
7928
7928
  if (document) {
7929
7929
  const style = document.createElement("style");
7930
7930
  style.textContent = styles;
7931
7931
  if (cspNonce) style.nonce = cspNonce;
7932
- body.appendChild(style);
7932
+ parent.appendChild(style);
7933
7933
  }
7934
7934
  };
7935
7935
  /**
@@ -8049,9 +8049,20 @@ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
8049
8049
  cloneIframeContainer.style.position = "fixed";
8050
8050
  cloneIframeContainer.style.left = "-10000px";
8051
8051
  cloneIframeContainer.style.top = "0px";
8052
- cloneIframeContainer.style.border = "0";
8053
8052
  cloneIframeContainer.width = bounds.width.toString();
8054
8053
  cloneIframeContainer.height = bounds.height.toString();
8054
+ const contentBox = [
8055
+ ["width", `${bounds.width}px`],
8056
+ ["height", `${bounds.height}px`],
8057
+ ["min-width", "0px"],
8058
+ ["max-width", "none"],
8059
+ ["min-height", "0px"],
8060
+ ["max-height", "none"],
8061
+ ["box-sizing", "content-box"],
8062
+ ["padding", "0px"],
8063
+ ["border", "0"]
8064
+ ];
8065
+ for (const [property, value] of contentBox) cloneIframeContainer.style.setProperty(property, value, "important");
8055
8066
  cloneIframeContainer.scrolling = "no";
8056
8067
  cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, "true");
8057
8068
  (customContainer || ownerDocument.body).appendChild(cloneIframeContainer);
@@ -8400,6 +8411,7 @@ var DocumentCloner = class {
8400
8411
  }
8401
8412
  this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
8402
8413
  this.appendDocumentAdoptedStyles(element.ownerDocument);
8414
+ this.appendPseudoHideStyles();
8403
8415
  }
8404
8416
  /**
8405
8417
  * Re-materialise the source document's constructable stylesheets
@@ -8419,6 +8431,16 @@ var DocumentCloner = class {
8419
8431
  const style = createAdoptedStylesElement(this.documentElement.ownerDocument ?? ownerDocument, sheets, this.options.cspNonce);
8420
8432
  if (style) head.appendChild(style);
8421
8433
  }
8434
+ /**
8435
+ * Add the rules that hide the source ::before / ::after (the clone draws
8436
+ * them as surrogate elements) to the clone's <head>. Inside <body> the
8437
+ * <style> would be one of its children, and `body > :first-child`,
8438
+ * `:nth-child()` and sibling selectors would match the wrong elements.
8439
+ */
8440
+ appendPseudoHideStyles() {
8441
+ const head = this.documentElement.getElementsByTagName("head").item(0);
8442
+ createPseudoHideStyles(head ?? this.documentElement, this.options.cspNonce);
8443
+ }
8422
8444
  toIFrame(ownerDocument, windowSize) {
8423
8445
  return mountCloneInIFrame(this.context, ownerDocument, windowSize, {
8424
8446
  documentElement: this.documentElement,
@@ -8554,7 +8576,6 @@ var DocumentCloner = class {
8554
8576
  const style = window.getComputedStyle(node);
8555
8577
  const checkPseudoElements = canHavePseudoElements(node);
8556
8578
  if (this.referenceElement === node && isHTMLElementNode(clone)) this.clonedReferenceElement = clone;
8557
- if (isBodyElement(clone)) createPseudoHideStyles(clone, this.options.cspNonce);
8558
8579
  const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
8559
8580
  if (checkPseudoElements) {
8560
8581
  const styleBefore = window.getComputedStyle(node, ":before");
@@ -8678,6 +8699,115 @@ var DOMNormalizer = class {
8678
8699
  }
8679
8700
  };
8680
8701
  //#endregion
8702
+ //#region src/dom/filter-reference.ts
8703
+ const SVG_NS$1 = "http://www.w3.org/2000/svg";
8704
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
8705
+ const REGION_DEFAULTS = {
8706
+ x: "-10%",
8707
+ y: "-10%",
8708
+ width: "120%",
8709
+ height: "120%"
8710
+ };
8711
+ const PRESENTATION_PROPERTIES = [
8712
+ "color-interpolation-filters",
8713
+ "flood-color",
8714
+ "flood-opacity",
8715
+ "lighting-color"
8716
+ ];
8717
+ const REFERENCE_RE = /^(["']?)(#[^"'\s()]+)\1$/;
8718
+ const URL_RE = /url\(\s*([^)]*?)\s*\)/g;
8719
+ /** The `#id` of a same-document url() body (`"#id"`, `'#id'` or `#id`), or null. */
8720
+ const parseFilterReference = (body) => {
8721
+ const match = REFERENCE_RE.exec(body.trim());
8722
+ return match ? match[2] : null;
8723
+ };
8724
+ /**
8725
+ * Resolve every same-document url() reference of a computed filter value.
8726
+ * References that cannot be expressed in the serialized SVG are left out, so
8727
+ * the compositor rejects the chain and the subtree keeps the legacy renderer.
8728
+ */
8729
+ function resolveFilterReferences(element, filter, box) {
8730
+ if (!filter || filter.indexOf("url(") === -1) return void 0;
8731
+ const references = /* @__PURE__ */ new Map();
8732
+ const seen = /* @__PURE__ */ new Set();
8733
+ for (const match of filter.matchAll(URL_RE)) {
8734
+ const ref = parseFilterReference(match[1]);
8735
+ if (!ref || seen.has(ref)) continue;
8736
+ seen.add(ref);
8737
+ const reference = resolveFilterReference(element.ownerDocument, ref, box);
8738
+ if (reference) references.set(ref, reference);
8739
+ }
8740
+ return references;
8741
+ }
8742
+ function resolveFilterReference(document, ref, box) {
8743
+ const source = document.getElementById(ref.slice(1));
8744
+ if (!source || source.namespaceURI !== SVG_NS$1 || source.localName !== "filter") return null;
8745
+ if (!isSelfContained(source)) return null;
8746
+ const region = filterRegion(source, box);
8747
+ if (!region) return null;
8748
+ const copy = source.cloneNode(true);
8749
+ inlinePresentation(source, copy);
8750
+ copy.removeAttribute("id");
8751
+ copy.setAttribute("filterUnits", "userSpaceOnUse");
8752
+ Object.keys(region).forEach((name) => copy.setAttribute(name, String(region[name])));
8753
+ const outset = Math.max(0, -region.x, -region.y, region.x + region.width - box.width, region.y + region.height - box.height);
8754
+ return {
8755
+ element: copy,
8756
+ region,
8757
+ outset: Math.ceil(outset)
8758
+ };
8759
+ }
8760
+ const hasHref = (node) => node.hasAttribute("href") || node.hasAttributeNS(XLINK_NS, "href");
8761
+ function isSelfContained(source) {
8762
+ if (source.getAttribute("primitiveUnits") === "objectBoundingBox" || hasHref(source)) return false;
8763
+ for (const primitive of Array.from(source.querySelectorAll("*"))) {
8764
+ if (primitive.localName === "feImage" || hasHref(primitive)) return false;
8765
+ for (const name of [
8766
+ "x",
8767
+ "y",
8768
+ "width",
8769
+ "height"
8770
+ ]) if ((primitive.getAttribute(name) ?? "").indexOf("%") !== -1) return false;
8771
+ }
8772
+ return true;
8773
+ }
8774
+ function filterRegion(source, box) {
8775
+ const boundingBox = source.getAttribute("filterUnits") !== "userSpaceOnUse";
8776
+ const read = (name, size) => {
8777
+ const raw = (source.getAttribute(name) ?? (boundingBox ? REGION_DEFAULTS[name] : "")).trim();
8778
+ if (raw === "") return null;
8779
+ const percentage = raw.endsWith("%");
8780
+ const value = Number(percentage ? raw.slice(0, -1) : raw.replace(/px$/, ""));
8781
+ if (!Number.isFinite(value)) return null;
8782
+ if (boundingBox) return (percentage ? value / 100 : value) * size;
8783
+ return percentage ? null : value;
8784
+ };
8785
+ const x = read("x", box.width);
8786
+ const y = read("y", box.height);
8787
+ const width = read("width", box.width);
8788
+ const height = read("height", box.height);
8789
+ if (x === null || y === null || width === null || height === null || width <= 0 || height <= 0) return null;
8790
+ return {
8791
+ x,
8792
+ y,
8793
+ width,
8794
+ height
8795
+ };
8796
+ }
8797
+ function inlinePresentation(source, copy) {
8798
+ const view = source.ownerDocument.defaultView;
8799
+ if (!view) return;
8800
+ const sources = [source, ...Array.from(source.querySelectorAll("*"))];
8801
+ const copies = [copy, ...Array.from(copy.querySelectorAll("*"))];
8802
+ sources.forEach((node, index) => {
8803
+ const style = view.getComputedStyle(node);
8804
+ for (const property of PRESENTATION_PROPERTIES) {
8805
+ const value = style.getPropertyValue(property);
8806
+ if (value) copies[index]?.setAttribute(property, value);
8807
+ }
8808
+ });
8809
+ }
8810
+ //#endregion
8681
8811
  //#region src/dom/element-container.ts
8682
8812
  var ElementContainer = class {
8683
8813
  context;
@@ -8691,6 +8821,11 @@ var ElementContainer = class {
8691
8821
  * bounds to leave a matching gap (issue #227).
8692
8822
  */
8693
8823
  legendBounds;
8824
+ /**
8825
+ * Same-document `filter: url(#id)` references, resolved while the cloned
8826
+ * DOM is available. The surface compositor renders them in an SVG pass.
8827
+ */
8828
+ filterReferences;
8694
8829
  createsStackingContext = false;
8695
8830
  createsRealStackingContext = false;
8696
8831
  isListOwner = false;
@@ -8715,6 +8850,7 @@ var ElementContainer = class {
8715
8850
  this.bounds = new Bounds(this.bounds.left, borderBoxTop, this.bounds.width, this.bounds.height - (borderBoxTop - this.bounds.top));
8716
8851
  }
8717
8852
  }
8853
+ this.filterReferences = resolveFilterReferences(element, this.styles.filter, this.bounds);
8718
8854
  if (isDebugging(element, 4)) this.debugRender = true;
8719
8855
  }
8720
8856
  /**
@@ -10230,6 +10366,13 @@ const parseFilterFunction = (name, body) => {
10230
10366
  value: Number.parseFloat(numeric) * (percentage ? 1 / 100 : 1)
10231
10367
  };
10232
10368
  }
10369
+ if (name === "url") {
10370
+ const ref = parseFilterReference(body);
10371
+ return ref ? {
10372
+ kind: "url",
10373
+ ref
10374
+ } : null;
10375
+ }
10233
10376
  return null;
10234
10377
  };
10235
10378
  /** Split on whitespace outside parentheses so rgba(...) colours stay one token. */
@@ -10285,22 +10428,25 @@ const nativeFilterString = (filter, scale) => filter.functions.map((fn) => {
10285
10428
  case "blur": return `blur(${fn.value * scale}px)`;
10286
10429
  case "drop-shadow": return `drop-shadow(${fn.x * scale}px ${fn.y * scale}px ${fn.blur * scale}px ${fn.color})`;
10287
10430
  case "unitless": return fn.name === "hue-rotate" ? `hue-rotate(${fn.value}deg)` : `${fn.name}(${fn.value})`;
10431
+ case "url": return `url("${fn.ref}")`;
10288
10432
  }
10289
10433
  }).join(" ");
10290
10434
  /**
10291
10435
  * Outset (in CSS pixels) a surface must grow so blur/shadow spillover is not
10292
10436
  * cropped. Unitless functions never spread beyond the bounds. Consecutive
10293
- * blurs compound, so their radii sum.
10437
+ * blurs compound, so their radii sum. A url() reference can paint up to its
10438
+ * filter region, which may extend beyond the border box.
10294
10439
  */
10295
- function filterOutset(filter) {
10440
+ function filterOutset(filter, references) {
10296
10441
  let blurSum = 0;
10297
10442
  let maxShadowOffset = 0;
10443
+ let referenceOutset = 0;
10298
10444
  for (const fn of filter.functions) if (fn.kind === "blur") blurSum += fn.value;
10299
10445
  else if (fn.kind === "drop-shadow") {
10300
10446
  blurSum += fn.blur;
10301
10447
  maxShadowOffset = Math.max(maxShadowOffset, Math.abs(fn.x), Math.abs(fn.y));
10302
- }
10303
- return Math.ceil(3 * blurSum + maxShadowOffset);
10448
+ } else if (fn.kind === "url") referenceOutset += references?.get(fn.ref)?.outset ?? 0;
10449
+ return Math.ceil(3 * blurSum + maxShadowOffset + referenceOutset);
10304
10450
  }
10305
10451
  function canvasLike(source) {
10306
10452
  const canvas = (source.ownerDocument ?? document).createElement("canvas");
@@ -10396,8 +10542,9 @@ const isSvgExpressible = (filter) => {
10396
10542
  if (blurs > 0 && shadows > 0) return filter.functions.findIndex((fn) => fn.kind === "blur") < filter.functions.findIndex((fn) => fn.kind === "drop-shadow");
10397
10543
  return true;
10398
10544
  };
10399
- async function renderFilterSurface(source, filter, opacity, scale, signal) {
10545
+ async function renderFilterSurface(source, filter, opacity, scale, signal, references) {
10400
10546
  throwIfAborted(signal);
10547
+ if (filter.functions.some((fn) => fn.kind === "url")) return renderReferencedFilterSurface(source, filter, opacity, scale, signal, references);
10401
10548
  if (filter.functions.length > 0 && supportsNativeFilters(source.ownerDocument ?? document)) {
10402
10549
  const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
10403
10550
  if (output) return output;
@@ -10405,6 +10552,136 @@ async function renderFilterSurface(source, filter, opacity, scale, signal) {
10405
10552
  if (!isSvgExpressible(filter)) throw new FilterSurfaceError("Filter chain requires native canvas filters");
10406
10553
  return renderSvgFilterSurface(source, filter, opacity, scale, signal);
10407
10554
  }
10555
+ /**
10556
+ * Split a chain at its url() references: each reference is one SVG pass, and
10557
+ * the standard functions between them run through the usual backends. Every
10558
+ * step filters the previous step's complete output at alpha 1, then CSS
10559
+ * opacity is applied once, as for any other chain.
10560
+ */
10561
+ async function renderReferencedFilterSurface(source, filter, opacity, scale, signal, context) {
10562
+ const steps = [];
10563
+ for (const fn of filter.functions) {
10564
+ if (fn.kind === "url") {
10565
+ const reference = context?.references?.get(fn.ref);
10566
+ if (!reference) throw new FilterSurfaceError(`Filter reference ${fn.ref} cannot be resolved`);
10567
+ steps.push(reference);
10568
+ continue;
10569
+ }
10570
+ const last = steps[steps.length - 1];
10571
+ if (Array.isArray(last)) last.push(fn);
10572
+ else steps.push([fn]);
10573
+ }
10574
+ const origin = context?.origin ?? {
10575
+ x: 0,
10576
+ y: 0
10577
+ };
10578
+ const last = steps[steps.length - 1];
10579
+ const opacityApplied = Array.isArray(last);
10580
+ let current = source;
10581
+ try {
10582
+ for (const step of steps) {
10583
+ const next = Array.isArray(step) ? await renderFilterSurface(current, { functions: step }, step === last ? opacity : 1, scale, signal) : await renderReferenceSurface(current, step, origin, scale, signal);
10584
+ if (current !== source) releaseSurface(current);
10585
+ current = next;
10586
+ }
10587
+ if (opacityApplied || opacity === 1) {
10588
+ const output = current;
10589
+ current = source;
10590
+ return output;
10591
+ }
10592
+ return drawWithOpacity(current, opacity);
10593
+ } finally {
10594
+ if (current !== source) releaseSurface(current);
10595
+ }
10596
+ }
10597
+ function drawWithOpacity(source, opacity) {
10598
+ const output = canvasLike(source);
10599
+ const context = output.getContext("2d");
10600
+ if (!context) {
10601
+ releaseSurface(output);
10602
+ throw new FilterSurfaceError("Filter surface canvas is unavailable");
10603
+ }
10604
+ context.globalAlpha = opacity;
10605
+ try {
10606
+ context.drawImage(source, 0, 0);
10607
+ return output;
10608
+ } catch {
10609
+ releaseSurface(output);
10610
+ throw new FilterSurfaceError("Filter surface image cannot be drawn");
10611
+ }
10612
+ }
10613
+ const SVG_NS = "http://www.w3.org/2000/svg";
10614
+ function createSvgElement(owner, tag, attributes = {}) {
10615
+ const node = owner.createElementNS(SVG_NS, tag);
10616
+ Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
10617
+ return node;
10618
+ }
10619
+ function serializeSurface(source) {
10620
+ try {
10621
+ const url = source.toDataURL("image/png");
10622
+ if (url === "data:,") throw new Error("Canvas size is unsupported");
10623
+ return url;
10624
+ } catch {
10625
+ throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
10626
+ }
10627
+ }
10628
+ /** Decode a filtered SVG image and draw it onto a surface-sized canvas. */
10629
+ async function drawSvgSurface(svg, source, opacity, signal) {
10630
+ const imageWindow = (source.ownerDocument ?? document).defaultView;
10631
+ const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
10632
+ let output;
10633
+ try {
10634
+ image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
10635
+ await decodeSurface(image, signal);
10636
+ throwIfAborted(signal);
10637
+ output = canvasLike(source);
10638
+ const context = output.getContext("2d");
10639
+ if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10640
+ context.globalAlpha = opacity;
10641
+ context.drawImage(image, 0, 0);
10642
+ return output;
10643
+ } catch (error) {
10644
+ if (output) releaseSurface(output);
10645
+ if (signal?.aborted) throwIfAborted(signal);
10646
+ throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
10647
+ } finally {
10648
+ image.removeAttribute("src");
10649
+ }
10650
+ }
10651
+ /**
10652
+ * Apply a referenced SVG filter to the complete surface. The viewBox is in CSS
10653
+ * pixels, so the filter's user units scale with the capture, and the image is
10654
+ * placed so user space starts at the element's border box, as it does for the
10655
+ * live element. Canvas filters cannot express either: their lengths are in
10656
+ * device pixels and their bounding box is the whole canvas.
10657
+ */
10658
+ async function renderReferenceSurface(source, reference, origin, scale, signal) {
10659
+ throwIfAborted(signal);
10660
+ const owner = source.ownerDocument ?? document;
10661
+ const width = source.width / scale;
10662
+ const height = source.height / scale;
10663
+ const svg = createSvgElement(owner, "svg", {
10664
+ width: source.width,
10665
+ height: source.height,
10666
+ viewBox: `0 0 ${width} ${height}`
10667
+ });
10668
+ const defs = createSvgElement(owner, "defs");
10669
+ const filterElement = owner.importNode(reference.element, true);
10670
+ filterElement.setAttribute("id", "composited-reference");
10671
+ defs.appendChild(filterElement);
10672
+ svg.appendChild(defs);
10673
+ const group = createSvgElement(owner, "g", { transform: `translate(${origin.x} ${origin.y})` });
10674
+ group.appendChild(createSvgElement(owner, "image", {
10675
+ x: -origin.x,
10676
+ y: -origin.y,
10677
+ width,
10678
+ height,
10679
+ href: serializeSurface(source),
10680
+ filter: "url(#composited-reference)"
10681
+ }));
10682
+ svg.appendChild(group);
10683
+ return drawSvgSurface(svg, source, 1, signal);
10684
+ }
10408
10685
  async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10409
10686
  throwIfAborted(signal);
10410
10687
  const blur = filter.functions.find((fn) => fn.kind === "blur");
@@ -10416,29 +10693,9 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10416
10693
  blur: shadow.blur,
10417
10694
  color: shadow.color
10418
10695
  } : void 0;
10419
- if (!blurValue && !shadowValue) {
10420
- const output = canvasLike(source);
10421
- const context = output.getContext("2d");
10422
- if (!context) {
10423
- releaseSurface(output);
10424
- throw new FilterSurfaceError("Filter surface canvas is unavailable");
10425
- }
10426
- context.globalAlpha = opacity;
10427
- try {
10428
- context.drawImage(source, 0, 0);
10429
- return output;
10430
- } catch {
10431
- releaseSurface(output);
10432
- throw new FilterSurfaceError("Filter surface image cannot be drawn");
10433
- }
10434
- }
10435
- const namespace = "http://www.w3.org/2000/svg";
10696
+ if (!blurValue && !shadowValue) return drawWithOpacity(source, opacity);
10436
10697
  const svgDocument = source.ownerDocument ?? document;
10437
- const element = (tag, attributes = {}) => {
10438
- const node = svgDocument.createElementNS(namespace, tag);
10439
- Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
10440
- return node;
10441
- };
10698
+ const element = (tag, attributes = {}) => createSvgElement(svgDocument, tag, attributes);
10442
10699
  const svg = element("svg", {
10443
10700
  width: source.width,
10444
10701
  height: source.height,
@@ -10480,39 +10737,13 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10480
10737
  }
10481
10738
  defs.appendChild(filterElement);
10482
10739
  svg.appendChild(defs);
10483
- let sourceUrl;
10484
- try {
10485
- sourceUrl = source.toDataURL("image/png");
10486
- if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
10487
- } catch {
10488
- throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
10489
- }
10490
10740
  svg.appendChild(element("image", {
10491
10741
  width: source.width,
10492
10742
  height: source.height,
10493
- href: sourceUrl,
10743
+ href: serializeSurface(source),
10494
10744
  filter: "url(#composited-filter)"
10495
10745
  }));
10496
- const imageWindow = svgDocument.defaultView;
10497
- const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
10498
- let output;
10499
- try {
10500
- image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
10501
- await decodeSurface(image, signal);
10502
- throwIfAborted(signal);
10503
- output = canvasLike(source);
10504
- const context = output.getContext("2d");
10505
- if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10506
- context.globalAlpha = opacity;
10507
- context.drawImage(image, 0, 0);
10508
- return output;
10509
- } catch (error) {
10510
- if (output) releaseSurface(output);
10511
- if (signal?.aborted) throwIfAborted(signal);
10512
- throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
10513
- } finally {
10514
- image.removeAttribute("src");
10515
- }
10746
+ return drawSvgSurface(svg, source, opacity, signal);
10516
10747
  }
10517
10748
  function reserveSurface(budget, width, height) {
10518
10749
  const pixels = width * height * 4;
@@ -10536,7 +10767,7 @@ function surfaceBounds(container) {
10536
10767
  }
10537
10768
  for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
10538
10769
  const filter = parseFilterChain(styles.filter);
10539
- return expand(bounds, filter ? filterOutset(filter) : 0);
10770
+ return expand(bounds, filter ? filterOutset(filter, container.filterReferences) : 0);
10540
10771
  }
10541
10772
  function cropSurface(bounds, viewport, margin, scale) {
10542
10773
  const area = expand(viewport, margin);
@@ -13028,7 +13259,8 @@ var CanvasRenderer = class CanvasRenderer {
13028
13259
  async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
13029
13260
  const signal = this.options.signal;
13030
13261
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13031
- const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filter ? filterOutset(filter) : 0, this.options.scale);
13262
+ const container = stack.element.container;
13263
+ const bounds = cropSurface(surfaceBounds(container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filter ? filterOutset(filter, container.filterReferences) : 0, this.options.scale);
13032
13264
  if (!bounds.width || !bounds.height) return true;
13033
13265
  const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
13034
13266
  if (!reserved) {
@@ -13050,7 +13282,13 @@ var CanvasRenderer = class CanvasRenderer {
13050
13282
  source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
13051
13283
  await source.renderStackContent(stack);
13052
13284
  source.effectsRenderer.applyEffects([]);
13053
- if (filter) filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
13285
+ if (filter) filtered = await renderFilterSurface(source.canvas, filter, container.styles.opacity, options.scale, signal, {
13286
+ references: container.filterReferences,
13287
+ origin: {
13288
+ x: container.bounds.left - bounds.left,
13289
+ y: container.bounds.top - bounds.top
13290
+ }
13291
+ });
13054
13292
  const composited = filtered ?? source.canvas;
13055
13293
  if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
13056
13294
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");