html2canvas-pro 2.5.4 → 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.5.4 <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
  */
@@ -242,6 +242,30 @@ var Validator = class {
242
242
  };
243
243
  }
244
244
  /**
245
+ * Validate the backgroundColor option
246
+ *
247
+ * @param backgroundColor - CSS color string or null (transparent)
248
+ * @returns Validation result
249
+ */
250
+ validateBackgroundColor(backgroundColor) {
251
+ if (backgroundColor === null) return {
252
+ valid: true,
253
+ sanitized: null
254
+ };
255
+ if (typeof backgroundColor !== "string" || backgroundColor.length === 0) return {
256
+ valid: false,
257
+ error: "backgroundColor must be a CSS color string or null"
258
+ };
259
+ if (/[^a-zA-Z0-9%(),.#\s/-]/.test(backgroundColor)) return {
260
+ valid: false,
261
+ error: `backgroundColor contains characters that cannot appear in a CSS color: ${backgroundColor}`
262
+ };
263
+ return {
264
+ valid: true,
265
+ sanitized: backgroundColor
266
+ };
267
+ }
268
+ /**
245
269
  * Validate image timeout
246
270
  *
247
271
  * @param timeout - Timeout in milliseconds
@@ -386,6 +410,10 @@ var Validator = class {
386
410
  const nonceResult = this.validateCspNonce(options.cspNonce);
387
411
  if (!nonceResult.valid) errors.push(`CSP nonce: ${nonceResult.error}`);
388
412
  }
413
+ if (options.backgroundColor !== void 0) {
414
+ const colorResult = this.validateBackgroundColor(options.backgroundColor);
415
+ if (!colorResult.valid) errors.push(`backgroundColor: ${colorResult.error}`);
416
+ }
389
417
  if (this.config.customValidator) {
390
418
  const customResult = this.config.customValidator(options, "options");
391
419
  if (!customResult.valid) errors.push(`Custom validation: ${customResult.error}`);
@@ -7885,19 +7913,19 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
7885
7913
  }`;
7886
7914
  /**
7887
7915
  * Inject the stylesheet that hides pseudo-element surrogate containers.
7888
- * Called on the cloned <body> so the surrogates themselves never render.
7916
+ * Added to the cloned <head> so the surrogates themselves never render.
7889
7917
  */
7890
- const createPseudoHideStyles = (body, cspNonce) => {
7891
- 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}
7892
7920
  .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
7893
7921
  };
7894
- const createStyles = (body, styles, cspNonce) => {
7895
- const document = body.ownerDocument;
7922
+ const createStyles = (parent, styles, cspNonce) => {
7923
+ const document = parent.ownerDocument;
7896
7924
  if (document) {
7897
7925
  const style = document.createElement("style");
7898
7926
  style.textContent = styles;
7899
7927
  if (cspNonce) style.nonce = cspNonce;
7900
- body.appendChild(style);
7928
+ parent.appendChild(style);
7901
7929
  }
7902
7930
  };
7903
7931
  /**
@@ -8017,9 +8045,20 @@ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
8017
8045
  cloneIframeContainer.style.position = "fixed";
8018
8046
  cloneIframeContainer.style.left = "-10000px";
8019
8047
  cloneIframeContainer.style.top = "0px";
8020
- cloneIframeContainer.style.border = "0";
8021
8048
  cloneIframeContainer.width = bounds.width.toString();
8022
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");
8023
8062
  cloneIframeContainer.scrolling = "no";
8024
8063
  cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, "true");
8025
8064
  (customContainer || ownerDocument.body).appendChild(cloneIframeContainer);
@@ -8062,9 +8101,9 @@ const isSheetFullyApplied = (sheet, depth = 0) => {
8062
8101
  if (!rules) return true;
8063
8102
  for (let i = 0; i < rules.length; i++) {
8064
8103
  const rule = rules[i];
8065
- if (rule && typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
8066
- if (!rule.styleSheet || !isSheetFullyApplied(rule.styleSheet, depth + 1)) return false;
8067
- }
8104
+ if (!rule || !("styleSheet" in rule)) continue;
8105
+ const inner = rule.styleSheet;
8106
+ if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
8068
8107
  }
8069
8108
  return true;
8070
8109
  };
@@ -8106,7 +8145,7 @@ const collectLinks = (root, out) => {
8106
8145
  * remain as the settle path for plain <link> fetches.
8107
8146
  * @internal – exported for testing only.
8108
8147
  */
8109
- const stylesheetsReady = (documentClone, logger, signal) => {
8148
+ const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
8110
8149
  if (signal?.aborted) return Promise.resolve();
8111
8150
  const pending = [];
8112
8151
  const links = [];
@@ -8136,8 +8175,8 @@ const stylesheetsReady = (documentClone, logger, signal) => {
8136
8175
  const timer = setInterval(() => {
8137
8176
  elapsed += 50;
8138
8177
  if (link.sheet) settle();
8139
- else if (elapsed >= 1e4) {
8140
- logger?.warn(`Stylesheet ${href} did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8178
+ else if (elapsed >= timeoutMs) {
8179
+ logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
8141
8180
  settle();
8142
8181
  }
8143
8182
  }, 50);
@@ -8154,8 +8193,8 @@ const stylesheetsReady = (documentClone, logger, signal) => {
8154
8193
  const timer = setInterval(() => {
8155
8194
  elapsed += 50;
8156
8195
  const currentSheet = style.sheet;
8157
- if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= 1e4) {
8158
- if (elapsed >= 1e4 && !isSheetFullyApplied(currentSheet ?? null)) logger?.warn(`@import chain in an inline <style> did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8196
+ if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
8197
+ if (elapsed >= timeoutMs && !isSheetFullyApplied(currentSheet ?? null)) logger?.warn(`@import chain in an inline <style> did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
8159
8198
  clearInterval(timer);
8160
8199
  resolve();
8161
8200
  }
@@ -8368,6 +8407,7 @@ var DocumentCloner = class {
8368
8407
  }
8369
8408
  this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
8370
8409
  this.appendDocumentAdoptedStyles(element.ownerDocument);
8410
+ this.appendPseudoHideStyles();
8371
8411
  }
8372
8412
  /**
8373
8413
  * Re-materialise the source document's constructable stylesheets
@@ -8387,6 +8427,16 @@ var DocumentCloner = class {
8387
8427
  const style = createAdoptedStylesElement(this.documentElement.ownerDocument ?? ownerDocument, sheets, this.options.cspNonce);
8388
8428
  if (style) head.appendChild(style);
8389
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
+ }
8390
8440
  toIFrame(ownerDocument, windowSize) {
8391
8441
  return mountCloneInIFrame(this.context, ownerDocument, windowSize, {
8392
8442
  documentElement: this.documentElement,
@@ -8522,7 +8572,6 @@ var DocumentCloner = class {
8522
8572
  const style = window.getComputedStyle(node);
8523
8573
  const checkPseudoElements = canHavePseudoElements(node);
8524
8574
  if (this.referenceElement === node && isHTMLElementNode(clone)) this.clonedReferenceElement = clone;
8525
- if (isBodyElement(clone)) createPseudoHideStyles(clone, this.options.cspNonce);
8526
8575
  const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
8527
8576
  if (checkPseudoElements) {
8528
8577
  const styleBefore = window.getComputedStyle(node, ":before");
@@ -8646,6 +8695,115 @@ var DOMNormalizer = class {
8646
8695
  }
8647
8696
  };
8648
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
8649
8807
  //#region src/dom/element-container.ts
8650
8808
  var ElementContainer = class {
8651
8809
  context;
@@ -8659,6 +8817,11 @@ var ElementContainer = class {
8659
8817
  * bounds to leave a matching gap (issue #227).
8660
8818
  */
8661
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;
8662
8825
  createsStackingContext = false;
8663
8826
  createsRealStackingContext = false;
8664
8827
  isListOwner = false;
@@ -8683,6 +8846,7 @@ var ElementContainer = class {
8683
8846
  this.bounds = new Bounds(this.bounds.left, borderBoxTop, this.bounds.width, this.bounds.height - (borderBoxTop - this.bounds.top));
8684
8847
  }
8685
8848
  }
8849
+ this.filterReferences = resolveFilterReferences(element, this.styles.filter, this.bounds);
8686
8850
  if (isDebugging(element, 4)) this.debugRender = true;
8687
8851
  }
8688
8852
  /**
@@ -9843,7 +10007,7 @@ const parseStackTree = (parent, stackingContext, realStackingContext, listItems)
9843
10007
  if (treatAsRealStackingContext || createsStackingContext) {
9844
10008
  const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
9845
10009
  const stack = new StackingContext(paintContainer);
9846
- if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
10010
+ if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
9847
10011
  const order = child.styles.zIndex.order;
9848
10012
  if (order < 0) {
9849
10013
  let index = 0;
@@ -10198,6 +10362,13 @@ const parseFilterFunction = (name, body) => {
10198
10362
  value: Number.parseFloat(numeric) * (percentage ? 1 / 100 : 1)
10199
10363
  };
10200
10364
  }
10365
+ if (name === "url") {
10366
+ const ref = parseFilterReference(body);
10367
+ return ref ? {
10368
+ kind: "url",
10369
+ ref
10370
+ } : null;
10371
+ }
10201
10372
  return null;
10202
10373
  };
10203
10374
  /** Split on whitespace outside parentheses so rgba(...) colours stay one token. */
@@ -10253,22 +10424,25 @@ const nativeFilterString = (filter, scale) => filter.functions.map((fn) => {
10253
10424
  case "blur": return `blur(${fn.value * scale}px)`;
10254
10425
  case "drop-shadow": return `drop-shadow(${fn.x * scale}px ${fn.y * scale}px ${fn.blur * scale}px ${fn.color})`;
10255
10426
  case "unitless": return fn.name === "hue-rotate" ? `hue-rotate(${fn.value}deg)` : `${fn.name}(${fn.value})`;
10427
+ case "url": return `url("${fn.ref}")`;
10256
10428
  }
10257
10429
  }).join(" ");
10258
10430
  /**
10259
10431
  * Outset (in CSS pixels) a surface must grow so blur/shadow spillover is not
10260
10432
  * cropped. Unitless functions never spread beyond the bounds. Consecutive
10261
- * 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.
10262
10435
  */
10263
- function filterOutset(filter) {
10436
+ function filterOutset(filter, references) {
10264
10437
  let blurSum = 0;
10265
10438
  let maxShadowOffset = 0;
10439
+ let referenceOutset = 0;
10266
10440
  for (const fn of filter.functions) if (fn.kind === "blur") blurSum += fn.value;
10267
10441
  else if (fn.kind === "drop-shadow") {
10268
10442
  blurSum += fn.blur;
10269
10443
  maxShadowOffset = Math.max(maxShadowOffset, Math.abs(fn.x), Math.abs(fn.y));
10270
- }
10271
- 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);
10272
10446
  }
10273
10447
  function canvasLike(source) {
10274
10448
  const canvas = (source.ownerDocument ?? document).createElement("canvas");
@@ -10364,8 +10538,9 @@ const isSvgExpressible = (filter) => {
10364
10538
  if (blurs > 0 && shadows > 0) return filter.functions.findIndex((fn) => fn.kind === "blur") < filter.functions.findIndex((fn) => fn.kind === "drop-shadow");
10365
10539
  return true;
10366
10540
  };
10367
- async function renderFilterSurface(source, filter, opacity, scale, signal) {
10541
+ async function renderFilterSurface(source, filter, opacity, scale, signal, references) {
10368
10542
  throwIfAborted(signal);
10543
+ if (filter.functions.some((fn) => fn.kind === "url")) return renderReferencedFilterSurface(source, filter, opacity, scale, signal, references);
10369
10544
  if (filter.functions.length > 0 && supportsNativeFilters(source.ownerDocument ?? document)) {
10370
10545
  const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
10371
10546
  if (output) return output;
@@ -10373,6 +10548,136 @@ async function renderFilterSurface(source, filter, opacity, scale, signal) {
10373
10548
  if (!isSvgExpressible(filter)) throw new FilterSurfaceError("Filter chain requires native canvas filters");
10374
10549
  return renderSvgFilterSurface(source, filter, opacity, scale, signal);
10375
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
+ }
10376
10681
  async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10377
10682
  throwIfAborted(signal);
10378
10683
  const blur = filter.functions.find((fn) => fn.kind === "blur");
@@ -10384,29 +10689,9 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10384
10689
  blur: shadow.blur,
10385
10690
  color: shadow.color
10386
10691
  } : void 0;
10387
- if (!blurValue && !shadowValue) {
10388
- const output = canvasLike(source);
10389
- const context = output.getContext("2d");
10390
- if (!context) {
10391
- releaseSurface(output);
10392
- throw new FilterSurfaceError("Filter surface canvas is unavailable");
10393
- }
10394
- context.globalAlpha = opacity;
10395
- try {
10396
- context.drawImage(source, 0, 0);
10397
- return output;
10398
- } catch {
10399
- releaseSurface(output);
10400
- throw new FilterSurfaceError("Filter surface image cannot be drawn");
10401
- }
10402
- }
10403
- const namespace = "http://www.w3.org/2000/svg";
10692
+ if (!blurValue && !shadowValue) return drawWithOpacity(source, opacity);
10404
10693
  const svgDocument = source.ownerDocument ?? document;
10405
- const element = (tag, attributes = {}) => {
10406
- const node = svgDocument.createElementNS(namespace, tag);
10407
- Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
10408
- return node;
10409
- };
10694
+ const element = (tag, attributes = {}) => createSvgElement(svgDocument, tag, attributes);
10410
10695
  const svg = element("svg", {
10411
10696
  width: source.width,
10412
10697
  height: source.height,
@@ -10448,39 +10733,13 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
10448
10733
  }
10449
10734
  defs.appendChild(filterElement);
10450
10735
  svg.appendChild(defs);
10451
- let sourceUrl;
10452
- try {
10453
- sourceUrl = source.toDataURL("image/png");
10454
- if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
10455
- } catch {
10456
- throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
10457
- }
10458
10736
  svg.appendChild(element("image", {
10459
10737
  width: source.width,
10460
10738
  height: source.height,
10461
- href: sourceUrl,
10739
+ href: serializeSurface(source),
10462
10740
  filter: "url(#composited-filter)"
10463
10741
  }));
10464
- const imageWindow = svgDocument.defaultView;
10465
- const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
10466
- let output;
10467
- try {
10468
- image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
10469
- await decodeSurface(image, signal);
10470
- throwIfAborted(signal);
10471
- output = canvasLike(source);
10472
- const context = output.getContext("2d");
10473
- if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10474
- context.globalAlpha = opacity;
10475
- context.drawImage(image, 0, 0);
10476
- return output;
10477
- } catch (error) {
10478
- if (output) releaseSurface(output);
10479
- if (signal?.aborted) throwIfAborted(signal);
10480
- throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
10481
- } finally {
10482
- image.removeAttribute("src");
10483
- }
10742
+ return drawSvgSurface(svg, source, opacity, signal);
10484
10743
  }
10485
10744
  function reserveSurface(budget, width, height) {
10486
10745
  const pixels = width * height * 4;
@@ -10504,7 +10763,7 @@ function surfaceBounds(container) {
10504
10763
  }
10505
10764
  for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
10506
10765
  const filter = parseFilterChain(styles.filter);
10507
- return expand(bounds, filter ? filterOutset(filter) : 0);
10766
+ return expand(bounds, filter ? filterOutset(filter, container.filterReferences) : 0);
10508
10767
  }
10509
10768
  function cropSurface(bounds, viewport, margin, scale) {
10510
10769
  const area = expand(viewport, margin);
@@ -12141,6 +12400,7 @@ const paintRadialLayer = (ctx, layer, area) => {
12141
12400
  ctx.fillStyle = rasterized.lastColor;
12142
12401
  ctx.fillRect(area.left, area.top, area.width, area.height);
12143
12402
  ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
12403
+ releaseSurface(rasterized.canvas);
12144
12404
  };
12145
12405
  /** Draw one gradient layer filling the whole mask area. */
12146
12406
  const paintGradientLayer = (ctx, layer, area) => {
@@ -12995,7 +13255,8 @@ var CanvasRenderer = class CanvasRenderer {
12995
13255
  async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
12996
13256
  const signal = this.options.signal;
12997
13257
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
12998
- 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);
12999
13260
  if (!bounds.width || !bounds.height) return true;
13000
13261
  const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
13001
13262
  if (!reserved) {
@@ -13017,7 +13278,13 @@ var CanvasRenderer = class CanvasRenderer {
13017
13278
  source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
13018
13279
  await source.renderStackContent(stack);
13019
13280
  source.effectsRenderer.applyEffects([]);
13020
- 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
+ });
13021
13288
  const composited = filtered ?? source.canvas;
13022
13289
  if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
13023
13290
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
@@ -13054,7 +13321,10 @@ var CanvasRenderer = class CanvasRenderer {
13054
13321
  maskCanvas.width = surface.width;
13055
13322
  maskCanvas.height = surface.height;
13056
13323
  const maskCtx = maskCanvas.getContext("2d");
13057
- if (!maskCtx) return;
13324
+ if (!maskCtx) {
13325
+ releaseSurface(maskCanvas);
13326
+ return;
13327
+ }
13058
13328
  maskCtx.scale(this.options.scale, this.options.scale);
13059
13329
  maskCtx.translate(-bounds.left, -bounds.top);
13060
13330
  try {
@@ -13066,6 +13336,7 @@ var CanvasRenderer = class CanvasRenderer {
13066
13336
  }, async (url) => await this.context.cache.match(url));
13067
13337
  } catch (error) {
13068
13338
  this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
13339
+ releaseSurface(maskCanvas);
13069
13340
  return;
13070
13341
  }
13071
13342
  ctx.save();
@@ -13075,6 +13346,7 @@ var CanvasRenderer = class CanvasRenderer {
13075
13346
  ctx.drawImage(maskCanvas, 0, 0);
13076
13347
  } finally {
13077
13348
  ctx.restore();
13349
+ releaseSurface(maskCanvas);
13078
13350
  }
13079
13351
  }
13080
13352
  /**
@@ -13479,6 +13751,7 @@ var CanvasRenderer = class CanvasRenderer {
13479
13751
  ctx.fillStyle = rasterized.lastColor;
13480
13752
  ctx.fillRect(0, 0, width, height);
13481
13753
  ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
13754
+ releaseSurface(rasterized.canvas);
13482
13755
  return canvas;
13483
13756
  }
13484
13757
  if (source.type === 5 || source.type === 6) {