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
  */
@@ -7913,19 +7913,19 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
7913
7913
  }`;
7914
7914
  /**
7915
7915
  * Inject the stylesheet that hides pseudo-element surrogate containers.
7916
- * Called on the cloned <body> so the surrogates themselves never render.
7916
+ * Added to the cloned <head> so the surrogates themselves never render.
7917
7917
  */
7918
- const createPseudoHideStyles = (body, cspNonce) => {
7919
- createStyles(body, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
7918
+ const createPseudoHideStyles = (parent, cspNonce) => {
7919
+ createStyles(parent, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
7920
7920
  .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
7921
7921
  };
7922
- const createStyles = (body, styles, cspNonce) => {
7923
- const document = body.ownerDocument;
7922
+ const createStyles = (parent, styles, cspNonce) => {
7923
+ const document = parent.ownerDocument;
7924
7924
  if (document) {
7925
7925
  const style = document.createElement("style");
7926
7926
  style.textContent = styles;
7927
7927
  if (cspNonce) style.nonce = cspNonce;
7928
- body.appendChild(style);
7928
+ parent.appendChild(style);
7929
7929
  }
7930
7930
  };
7931
7931
  /**
@@ -8045,9 +8045,20 @@ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
8045
8045
  cloneIframeContainer.style.position = "fixed";
8046
8046
  cloneIframeContainer.style.left = "-10000px";
8047
8047
  cloneIframeContainer.style.top = "0px";
8048
- cloneIframeContainer.style.border = "0";
8049
8048
  cloneIframeContainer.width = bounds.width.toString();
8050
8049
  cloneIframeContainer.height = bounds.height.toString();
8050
+ const contentBox = [
8051
+ ["width", `${bounds.width}px`],
8052
+ ["height", `${bounds.height}px`],
8053
+ ["min-width", "0px"],
8054
+ ["max-width", "none"],
8055
+ ["min-height", "0px"],
8056
+ ["max-height", "none"],
8057
+ ["box-sizing", "content-box"],
8058
+ ["padding", "0px"],
8059
+ ["border", "0"]
8060
+ ];
8061
+ for (const [property, value] of contentBox) cloneIframeContainer.style.setProperty(property, value, "important");
8051
8062
  cloneIframeContainer.scrolling = "no";
8052
8063
  cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, "true");
8053
8064
  (customContainer || ownerDocument.body).appendChild(cloneIframeContainer);
@@ -8396,6 +8407,7 @@ var DocumentCloner = class {
8396
8407
  }
8397
8408
  this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
8398
8409
  this.appendDocumentAdoptedStyles(element.ownerDocument);
8410
+ this.appendPseudoHideStyles();
8399
8411
  }
8400
8412
  /**
8401
8413
  * Re-materialise the source document's constructable stylesheets
@@ -8415,6 +8427,16 @@ var DocumentCloner = class {
8415
8427
  const style = createAdoptedStylesElement(this.documentElement.ownerDocument ?? ownerDocument, sheets, this.options.cspNonce);
8416
8428
  if (style) head.appendChild(style);
8417
8429
  }
8430
+ /**
8431
+ * Add the rules that hide the source ::before / ::after (the clone draws
8432
+ * them as surrogate elements) to the clone's <head>. Inside <body> the
8433
+ * <style> would be one of its children, and `body > :first-child`,
8434
+ * `:nth-child()` and sibling selectors would match the wrong elements.
8435
+ */
8436
+ appendPseudoHideStyles() {
8437
+ const head = this.documentElement.getElementsByTagName("head").item(0);
8438
+ createPseudoHideStyles(head ?? this.documentElement, this.options.cspNonce);
8439
+ }
8418
8440
  toIFrame(ownerDocument, windowSize) {
8419
8441
  return mountCloneInIFrame(this.context, ownerDocument, windowSize, {
8420
8442
  documentElement: this.documentElement,
@@ -8550,7 +8572,6 @@ var DocumentCloner = class {
8550
8572
  const style = window.getComputedStyle(node);
8551
8573
  const checkPseudoElements = canHavePseudoElements(node);
8552
8574
  if (this.referenceElement === node && isHTMLElementNode(clone)) this.clonedReferenceElement = clone;
8553
- if (isBodyElement(clone)) createPseudoHideStyles(clone, this.options.cspNonce);
8554
8575
  const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
8555
8576
  if (checkPseudoElements) {
8556
8577
  const styleBefore = window.getComputedStyle(node, ":before");
@@ -8674,6 +8695,115 @@ var DOMNormalizer = class {
8674
8695
  }
8675
8696
  };
8676
8697
  //#endregion
8698
+ //#region src/dom/filter-reference.ts
8699
+ const SVG_NS$1 = "http://www.w3.org/2000/svg";
8700
+ const XLINK_NS = "http://www.w3.org/1999/xlink";
8701
+ const REGION_DEFAULTS = {
8702
+ x: "-10%",
8703
+ y: "-10%",
8704
+ width: "120%",
8705
+ height: "120%"
8706
+ };
8707
+ const PRESENTATION_PROPERTIES = [
8708
+ "color-interpolation-filters",
8709
+ "flood-color",
8710
+ "flood-opacity",
8711
+ "lighting-color"
8712
+ ];
8713
+ const REFERENCE_RE = /^(["']?)(#[^"'\s()]+)\1$/;
8714
+ const URL_RE = /url\(\s*([^)]*?)\s*\)/g;
8715
+ /** The `#id` of a same-document url() body (`"#id"`, `'#id'` or `#id`), or null. */
8716
+ const parseFilterReference = (body) => {
8717
+ const match = REFERENCE_RE.exec(body.trim());
8718
+ return match ? match[2] : null;
8719
+ };
8720
+ /**
8721
+ * Resolve every same-document url() reference of a computed filter value.
8722
+ * References that cannot be expressed in the serialized SVG are left out, so
8723
+ * the compositor rejects the chain and the subtree keeps the legacy renderer.
8724
+ */
8725
+ function resolveFilterReferences(element, filter, box) {
8726
+ if (!filter || filter.indexOf("url(") === -1) return void 0;
8727
+ const references = /* @__PURE__ */ new Map();
8728
+ const seen = /* @__PURE__ */ new Set();
8729
+ for (const match of filter.matchAll(URL_RE)) {
8730
+ const ref = parseFilterReference(match[1]);
8731
+ if (!ref || seen.has(ref)) continue;
8732
+ seen.add(ref);
8733
+ const reference = resolveFilterReference(element.ownerDocument, ref, box);
8734
+ if (reference) references.set(ref, reference);
8735
+ }
8736
+ return references;
8737
+ }
8738
+ function resolveFilterReference(document, ref, box) {
8739
+ const source = document.getElementById(ref.slice(1));
8740
+ if (!source || source.namespaceURI !== SVG_NS$1 || source.localName !== "filter") return null;
8741
+ if (!isSelfContained(source)) return null;
8742
+ const region = filterRegion(source, box);
8743
+ if (!region) return null;
8744
+ const copy = source.cloneNode(true);
8745
+ inlinePresentation(source, copy);
8746
+ copy.removeAttribute("id");
8747
+ copy.setAttribute("filterUnits", "userSpaceOnUse");
8748
+ Object.keys(region).forEach((name) => copy.setAttribute(name, String(region[name])));
8749
+ const outset = Math.max(0, -region.x, -region.y, region.x + region.width - box.width, region.y + region.height - box.height);
8750
+ return {
8751
+ element: copy,
8752
+ region,
8753
+ outset: Math.ceil(outset)
8754
+ };
8755
+ }
8756
+ const hasHref = (node) => node.hasAttribute("href") || node.hasAttributeNS(XLINK_NS, "href");
8757
+ function isSelfContained(source) {
8758
+ if (source.getAttribute("primitiveUnits") === "objectBoundingBox" || hasHref(source)) return false;
8759
+ for (const primitive of Array.from(source.querySelectorAll("*"))) {
8760
+ if (primitive.localName === "feImage" || hasHref(primitive)) return false;
8761
+ for (const name of [
8762
+ "x",
8763
+ "y",
8764
+ "width",
8765
+ "height"
8766
+ ]) if ((primitive.getAttribute(name) ?? "").indexOf("%") !== -1) return false;
8767
+ }
8768
+ return true;
8769
+ }
8770
+ function filterRegion(source, box) {
8771
+ const boundingBox = source.getAttribute("filterUnits") !== "userSpaceOnUse";
8772
+ const read = (name, size) => {
8773
+ const raw = (source.getAttribute(name) ?? (boundingBox ? REGION_DEFAULTS[name] : "")).trim();
8774
+ if (raw === "") return null;
8775
+ const percentage = raw.endsWith("%");
8776
+ const value = Number(percentage ? raw.slice(0, -1) : raw.replace(/px$/, ""));
8777
+ if (!Number.isFinite(value)) return null;
8778
+ if (boundingBox) return (percentage ? value / 100 : value) * size;
8779
+ return percentage ? null : value;
8780
+ };
8781
+ const x = read("x", box.width);
8782
+ const y = read("y", box.height);
8783
+ const width = read("width", box.width);
8784
+ const height = read("height", box.height);
8785
+ if (x === null || y === null || width === null || height === null || width <= 0 || height <= 0) return null;
8786
+ return {
8787
+ x,
8788
+ y,
8789
+ width,
8790
+ height
8791
+ };
8792
+ }
8793
+ function inlinePresentation(source, copy) {
8794
+ const view = source.ownerDocument.defaultView;
8795
+ if (!view) return;
8796
+ const sources = [source, ...Array.from(source.querySelectorAll("*"))];
8797
+ const copies = [copy, ...Array.from(copy.querySelectorAll("*"))];
8798
+ sources.forEach((node, index) => {
8799
+ const style = view.getComputedStyle(node);
8800
+ for (const property of PRESENTATION_PROPERTIES) {
8801
+ const value = style.getPropertyValue(property);
8802
+ if (value) copies[index]?.setAttribute(property, value);
8803
+ }
8804
+ });
8805
+ }
8806
+ //#endregion
8677
8807
  //#region src/dom/element-container.ts
8678
8808
  var ElementContainer = class {
8679
8809
  context;
@@ -8687,6 +8817,11 @@ var ElementContainer = class {
8687
8817
  * bounds to leave a matching gap (issue #227).
8688
8818
  */
8689
8819
  legendBounds;
8820
+ /**
8821
+ * Same-document `filter: url(#id)` references, resolved while the cloned
8822
+ * DOM is available. The surface compositor renders them in an SVG pass.
8823
+ */
8824
+ filterReferences;
8690
8825
  createsStackingContext = false;
8691
8826
  createsRealStackingContext = false;
8692
8827
  isListOwner = false;
@@ -8711,6 +8846,7 @@ var ElementContainer = class {
8711
8846
  this.bounds = new Bounds(this.bounds.left, borderBoxTop, this.bounds.width, this.bounds.height - (borderBoxTop - this.bounds.top));
8712
8847
  }
8713
8848
  }
8849
+ this.filterReferences = resolveFilterReferences(element, this.styles.filter, this.bounds);
8714
8850
  if (isDebugging(element, 4)) this.debugRender = true;
8715
8851
  }
8716
8852
  /**
@@ -10226,6 +10362,13 @@ const parseFilterFunction = (name, body) => {
10226
10362
  value: Number.parseFloat(numeric) * (percentage ? 1 / 100 : 1)
10227
10363
  };
10228
10364
  }
10365
+ if (name === "url") {
10366
+ const ref = parseFilterReference(body);
10367
+ return ref ? {
10368
+ kind: "url",
10369
+ ref
10370
+ } : null;
10371
+ }
10229
10372
  return null;
10230
10373
  };
10231
10374
  /** Split on whitespace outside parentheses so rgba(...) colours stay one token. */
@@ -10281,22 +10424,25 @@ const nativeFilterString = (filter, scale) => filter.functions.map((fn) => {
10281
10424
  case "blur": return `blur(${fn.value * scale}px)`;
10282
10425
  case "drop-shadow": return `drop-shadow(${fn.x * scale}px ${fn.y * scale}px ${fn.blur * scale}px ${fn.color})`;
10283
10426
  case "unitless": return fn.name === "hue-rotate" ? `hue-rotate(${fn.value}deg)` : `${fn.name}(${fn.value})`;
10427
+ case "url": return `url("${fn.ref}")`;
10284
10428
  }
10285
10429
  }).join(" ");
10286
10430
  /**
10287
10431
  * Outset (in CSS pixels) a surface must grow so blur/shadow spillover is not
10288
10432
  * cropped. Unitless functions never spread beyond the bounds. Consecutive
10289
- * blurs compound, so their radii sum.
10433
+ * blurs compound, so their radii sum. A url() reference can paint up to its
10434
+ * filter region, which may extend beyond the border box.
10290
10435
  */
10291
- function filterOutset(filter) {
10436
+ function filterOutset(filter, references) {
10292
10437
  let blurSum = 0;
10293
10438
  let maxShadowOffset = 0;
10439
+ let referenceOutset = 0;
10294
10440
  for (const fn of filter.functions) if (fn.kind === "blur") blurSum += fn.value;
10295
10441
  else if (fn.kind === "drop-shadow") {
10296
10442
  blurSum += fn.blur;
10297
10443
  maxShadowOffset = Math.max(maxShadowOffset, Math.abs(fn.x), Math.abs(fn.y));
10298
- }
10299
- return Math.ceil(3 * blurSum + maxShadowOffset);
10444
+ } else if (fn.kind === "url") referenceOutset += references?.get(fn.ref)?.outset ?? 0;
10445
+ return Math.ceil(3 * blurSum + maxShadowOffset + referenceOutset);
10300
10446
  }
10301
10447
  function canvasLike(source) {
10302
10448
  const canvas = (source.ownerDocument ?? document).createElement("canvas");
@@ -10392,8 +10538,9 @@ const isSvgExpressible = (filter) => {
10392
10538
  if (blurs > 0 && shadows > 0) return filter.functions.findIndex((fn) => fn.kind === "blur") < filter.functions.findIndex((fn) => fn.kind === "drop-shadow");
10393
10539
  return true;
10394
10540
  };
10395
- async function renderFilterSurface(source, filter, opacity, scale, signal) {
10541
+ async function renderFilterSurface(source, filter, opacity, scale, signal, references) {
10396
10542
  throwIfAborted(signal);
10543
+ if (filter.functions.some((fn) => fn.kind === "url")) return renderReferencedFilterSurface(source, filter, opacity, scale, signal, references);
10397
10544
  if (filter.functions.length > 0 && supportsNativeFilters(source.ownerDocument ?? document)) {
10398
10545
  const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
10399
10546
  if (output) return output;
@@ -10401,6 +10548,136 @@ async function renderFilterSurface(source, filter, opacity, scale, signal) {
10401
10548
  if (!isSvgExpressible(filter)) throw new FilterSurfaceError("Filter chain requires native canvas filters");
10402
10549
  return renderSvgFilterSurface(source, filter, opacity, scale, signal);
10403
10550
  }
10551
+ /**
10552
+ * Split a chain at its url() references: each reference is one SVG pass, and
10553
+ * the standard functions between them run through the usual backends. Every
10554
+ * step filters the previous step's complete output at alpha 1, then CSS
10555
+ * opacity is applied once, as for any other chain.
10556
+ */
10557
+ async function renderReferencedFilterSurface(source, filter, opacity, scale, signal, context) {
10558
+ const steps = [];
10559
+ for (const fn of filter.functions) {
10560
+ if (fn.kind === "url") {
10561
+ const reference = context?.references?.get(fn.ref);
10562
+ if (!reference) throw new FilterSurfaceError(`Filter reference ${fn.ref} cannot be resolved`);
10563
+ steps.push(reference);
10564
+ continue;
10565
+ }
10566
+ const last = steps[steps.length - 1];
10567
+ if (Array.isArray(last)) last.push(fn);
10568
+ else steps.push([fn]);
10569
+ }
10570
+ const origin = context?.origin ?? {
10571
+ x: 0,
10572
+ y: 0
10573
+ };
10574
+ const last = steps[steps.length - 1];
10575
+ const opacityApplied = Array.isArray(last);
10576
+ let current = source;
10577
+ try {
10578
+ for (const step of steps) {
10579
+ const next = Array.isArray(step) ? await renderFilterSurface(current, { functions: step }, step === last ? opacity : 1, scale, signal) : await renderReferenceSurface(current, step, origin, scale, signal);
10580
+ if (current !== source) releaseSurface(current);
10581
+ current = next;
10582
+ }
10583
+ if (opacityApplied || opacity === 1) {
10584
+ const output = current;
10585
+ current = source;
10586
+ return output;
10587
+ }
10588
+ return drawWithOpacity(current, opacity);
10589
+ } finally {
10590
+ if (current !== source) releaseSurface(current);
10591
+ }
10592
+ }
10593
+ function drawWithOpacity(source, opacity) {
10594
+ const output = canvasLike(source);
10595
+ const context = output.getContext("2d");
10596
+ if (!context) {
10597
+ releaseSurface(output);
10598
+ throw new FilterSurfaceError("Filter surface canvas is unavailable");
10599
+ }
10600
+ context.globalAlpha = opacity;
10601
+ try {
10602
+ context.drawImage(source, 0, 0);
10603
+ return output;
10604
+ } catch {
10605
+ releaseSurface(output);
10606
+ throw new FilterSurfaceError("Filter surface image cannot be drawn");
10607
+ }
10608
+ }
10609
+ const SVG_NS = "http://www.w3.org/2000/svg";
10610
+ function createSvgElement(owner, tag, attributes = {}) {
10611
+ const node = owner.createElementNS(SVG_NS, tag);
10612
+ Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
10613
+ return node;
10614
+ }
10615
+ function serializeSurface(source) {
10616
+ try {
10617
+ const url = source.toDataURL("image/png");
10618
+ if (url === "data:,") throw new Error("Canvas size is unsupported");
10619
+ return url;
10620
+ } catch {
10621
+ throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
10622
+ }
10623
+ }
10624
+ /** Decode a filtered SVG image and draw it onto a surface-sized canvas. */
10625
+ async function drawSvgSurface(svg, source, opacity, signal) {
10626
+ const imageWindow = (source.ownerDocument ?? document).defaultView;
10627
+ const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
10628
+ let output;
10629
+ try {
10630
+ image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
10631
+ await decodeSurface(image, signal);
10632
+ throwIfAborted(signal);
10633
+ output = canvasLike(source);
10634
+ const context = output.getContext("2d");
10635
+ if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10636
+ context.globalAlpha = opacity;
10637
+ context.drawImage(image, 0, 0);
10638
+ return output;
10639
+ } catch (error) {
10640
+ if (output) releaseSurface(output);
10641
+ if (signal?.aborted) throwIfAborted(signal);
10642
+ throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
10643
+ } finally {
10644
+ image.removeAttribute("src");
10645
+ }
10646
+ }
10647
+ /**
10648
+ * Apply a referenced SVG filter to the complete surface. The viewBox is in CSS
10649
+ * pixels, so the filter's user units scale with the capture, and the image is
10650
+ * placed so user space starts at the element's border box, as it does for the
10651
+ * live element. Canvas filters cannot express either: their lengths are in
10652
+ * device pixels and their bounding box is the whole canvas.
10653
+ */
10654
+ async function renderReferenceSurface(source, reference, origin, scale, signal) {
10655
+ throwIfAborted(signal);
10656
+ const owner = source.ownerDocument ?? document;
10657
+ const width = source.width / scale;
10658
+ const height = source.height / scale;
10659
+ const svg = createSvgElement(owner, "svg", {
10660
+ width: source.width,
10661
+ height: source.height,
10662
+ viewBox: `0 0 ${width} ${height}`
10663
+ });
10664
+ const defs = createSvgElement(owner, "defs");
10665
+ const filterElement = owner.importNode(reference.element, true);
10666
+ filterElement.setAttribute("id", "composited-reference");
10667
+ defs.appendChild(filterElement);
10668
+ svg.appendChild(defs);
10669
+ const group = createSvgElement(owner, "g", { transform: `translate(${origin.x} ${origin.y})` });
10670
+ group.appendChild(createSvgElement(owner, "image", {
10671
+ x: -origin.x,
10672
+ y: -origin.y,
10673
+ width,
10674
+ height,
10675
+ href: serializeSurface(source),
10676
+ filter: "url(#composited-reference)"
10677
+ }));
10678
+ svg.appendChild(group);
10679
+ return drawSvgSurface(svg, source, 1, signal);
10680
+ }
10404
10681
  async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10405
10682
  throwIfAborted(signal);
10406
10683
  const blur = filter.functions.find((fn) => fn.kind === "blur");
@@ -10412,29 +10689,9 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10412
10689
  blur: shadow.blur,
10413
10690
  color: shadow.color
10414
10691
  } : void 0;
10415
- if (!blurValue && !shadowValue) {
10416
- const output = canvasLike(source);
10417
- const context = output.getContext("2d");
10418
- if (!context) {
10419
- releaseSurface(output);
10420
- throw new FilterSurfaceError("Filter surface canvas is unavailable");
10421
- }
10422
- context.globalAlpha = opacity;
10423
- try {
10424
- context.drawImage(source, 0, 0);
10425
- return output;
10426
- } catch {
10427
- releaseSurface(output);
10428
- throw new FilterSurfaceError("Filter surface image cannot be drawn");
10429
- }
10430
- }
10431
- const namespace = "http://www.w3.org/2000/svg";
10692
+ if (!blurValue && !shadowValue) return drawWithOpacity(source, opacity);
10432
10693
  const svgDocument = source.ownerDocument ?? document;
10433
- const element = (tag, attributes = {}) => {
10434
- const node = svgDocument.createElementNS(namespace, tag);
10435
- Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
10436
- return node;
10437
- };
10694
+ const element = (tag, attributes = {}) => createSvgElement(svgDocument, tag, attributes);
10438
10695
  const svg = element("svg", {
10439
10696
  width: source.width,
10440
10697
  height: source.height,
@@ -10476,39 +10733,13 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10476
10733
  }
10477
10734
  defs.appendChild(filterElement);
10478
10735
  svg.appendChild(defs);
10479
- let sourceUrl;
10480
- try {
10481
- sourceUrl = source.toDataURL("image/png");
10482
- if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
10483
- } catch {
10484
- throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
10485
- }
10486
10736
  svg.appendChild(element("image", {
10487
10737
  width: source.width,
10488
10738
  height: source.height,
10489
- href: sourceUrl,
10739
+ href: serializeSurface(source),
10490
10740
  filter: "url(#composited-filter)"
10491
10741
  }));
10492
- const imageWindow = svgDocument.defaultView;
10493
- const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
10494
- let output;
10495
- try {
10496
- image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
10497
- await decodeSurface(image, signal);
10498
- throwIfAborted(signal);
10499
- output = canvasLike(source);
10500
- const context = output.getContext("2d");
10501
- if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10502
- context.globalAlpha = opacity;
10503
- context.drawImage(image, 0, 0);
10504
- return output;
10505
- } catch (error) {
10506
- if (output) releaseSurface(output);
10507
- if (signal?.aborted) throwIfAborted(signal);
10508
- throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
10509
- } finally {
10510
- image.removeAttribute("src");
10511
- }
10742
+ return drawSvgSurface(svg, source, opacity, signal);
10512
10743
  }
10513
10744
  function reserveSurface(budget, width, height) {
10514
10745
  const pixels = width * height * 4;
@@ -10532,7 +10763,7 @@ function surfaceBounds(container) {
10532
10763
  }
10533
10764
  for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
10534
10765
  const filter = parseFilterChain(styles.filter);
10535
- return expand(bounds, filter ? filterOutset(filter) : 0);
10766
+ return expand(bounds, filter ? filterOutset(filter, container.filterReferences) : 0);
10536
10767
  }
10537
10768
  function cropSurface(bounds, viewport, margin, scale) {
10538
10769
  const area = expand(viewport, margin);
@@ -13024,7 +13255,8 @@ var CanvasRenderer = class CanvasRenderer {
13024
13255
  async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
13025
13256
  const signal = this.options.signal;
13026
13257
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13027
- 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);
13258
+ const container = stack.element.container;
13259
+ 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);
13028
13260
  if (!bounds.width || !bounds.height) return true;
13029
13261
  const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
13030
13262
  if (!reserved) {
@@ -13046,7 +13278,13 @@ var CanvasRenderer = class CanvasRenderer {
13046
13278
  source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
13047
13279
  await source.renderStackContent(stack);
13048
13280
  source.effectsRenderer.applyEffects([]);
13049
- if (filter) filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
13281
+ if (filter) filtered = await renderFilterSurface(source.canvas, filter, container.styles.opacity, options.scale, signal, {
13282
+ references: container.filterReferences,
13283
+ origin: {
13284
+ x: container.bounds.left - bounds.left,
13285
+ y: container.bounds.top - bounds.top
13286
+ }
13287
+ });
13050
13288
  const composited = filtered ?? source.canvas;
13051
13289
  if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
13052
13290
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");