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