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