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
|
@@ -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
|
*/
|
|
@@ -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
|
-
*
|
|
7916
|
+
* Added to the cloned <head> so the surrogates themselves never render.
|
|
7917
7917
|
*/
|
|
7918
|
-
const createPseudoHideStyles = (
|
|
7919
|
-
createStyles(
|
|
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 = (
|
|
7923
|
-
const document =
|
|
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
|
-
|
|
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:
|
|
10739
|
+
href: serializeSurface(source),
|
|
10490
10740
|
filter: "url(#composited-filter)"
|
|
10491
10741
|
}));
|
|
10492
|
-
|
|
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
|
|
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,
|
|
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");
|