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 +1 -1
- package/dist/html2canvas-pro.cjs +305 -67
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +305 -67
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +305 -67
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +4 -4
- package/dist/types/dom/document-cloner.d.ts +7 -0
- package/dist/types/dom/element-container.d.ts +6 -0
- package/dist/types/dom/filter-reference.d.ts +35 -0
- package/dist/types/dom/pseudo-content.d.ts +2 -2
- package/dist/types/render/canvas/filter-surface.d.ts +21 -6
- package/package.json +6 -6
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="
|
|
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> ·
|
package/dist/html2canvas-pro.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.6.
|
|
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
|
-
*
|
|
7920
|
+
* Added to the cloned <head> so the surrogates themselves never render.
|
|
7921
7921
|
*/
|
|
7922
|
-
const createPseudoHideStyles = (
|
|
7923
|
-
createStyles(
|
|
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 = (
|
|
7927
|
-
const document =
|
|
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
|
-
|
|
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:
|
|
10743
|
+
href: serializeSurface(source),
|
|
10494
10744
|
filter: "url(#composited-filter)"
|
|
10495
10745
|
}));
|
|
10496
|
-
|
|
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
|
|
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,
|
|
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");
|