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.
- package/README.md +1 -1
- package/dist/html2canvas-pro.cjs +350 -77
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +350 -77
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +350 -77
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +6 -6
- package/dist/types/core/validator.d.ts +9 -1
- 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/iframe-mount.d.ts +1 -1
- package/dist/types/dom/pseudo-content.d.ts +2 -2
- package/dist/types/render/canvas/filter-surface.d.ts +21 -6
- package/package.json +10 -9
package/dist/html2canvas-pro.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.
|
|
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
|
*/
|
|
@@ -249,6 +249,30 @@
|
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
251
|
/**
|
|
252
|
+
* Validate the backgroundColor option
|
|
253
|
+
*
|
|
254
|
+
* @param backgroundColor - CSS color string or null (transparent)
|
|
255
|
+
* @returns Validation result
|
|
256
|
+
*/
|
|
257
|
+
validateBackgroundColor(backgroundColor) {
|
|
258
|
+
if (backgroundColor === null) return {
|
|
259
|
+
valid: true,
|
|
260
|
+
sanitized: null
|
|
261
|
+
};
|
|
262
|
+
if (typeof backgroundColor !== "string" || backgroundColor.length === 0) return {
|
|
263
|
+
valid: false,
|
|
264
|
+
error: "backgroundColor must be a CSS color string or null"
|
|
265
|
+
};
|
|
266
|
+
if (/[^a-zA-Z0-9%(),.#\s/-]/.test(backgroundColor)) return {
|
|
267
|
+
valid: false,
|
|
268
|
+
error: `backgroundColor contains characters that cannot appear in a CSS color: ${backgroundColor}`
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
valid: true,
|
|
272
|
+
sanitized: backgroundColor
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
252
276
|
* Validate image timeout
|
|
253
277
|
*
|
|
254
278
|
* @param timeout - Timeout in milliseconds
|
|
@@ -393,6 +417,10 @@
|
|
|
393
417
|
const nonceResult = this.validateCspNonce(options.cspNonce);
|
|
394
418
|
if (!nonceResult.valid) errors.push(`CSP nonce: ${nonceResult.error}`);
|
|
395
419
|
}
|
|
420
|
+
if (options.backgroundColor !== void 0) {
|
|
421
|
+
const colorResult = this.validateBackgroundColor(options.backgroundColor);
|
|
422
|
+
if (!colorResult.valid) errors.push(`backgroundColor: ${colorResult.error}`);
|
|
423
|
+
}
|
|
396
424
|
if (this.config.customValidator) {
|
|
397
425
|
const customResult = this.config.customValidator(options, "options");
|
|
398
426
|
if (!customResult.valid) errors.push(`Custom validation: ${customResult.error}`);
|
|
@@ -7892,19 +7920,19 @@
|
|
|
7892
7920
|
}`;
|
|
7893
7921
|
/**
|
|
7894
7922
|
* Inject the stylesheet that hides pseudo-element surrogate containers.
|
|
7895
|
-
*
|
|
7923
|
+
* Added to the cloned <head> so the surrogates themselves never render.
|
|
7896
7924
|
*/
|
|
7897
|
-
const createPseudoHideStyles = (
|
|
7898
|
-
createStyles(
|
|
7925
|
+
const createPseudoHideStyles = (parent, cspNonce) => {
|
|
7926
|
+
createStyles(parent, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
|
7899
7927
|
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
|
|
7900
7928
|
};
|
|
7901
|
-
const createStyles = (
|
|
7902
|
-
const document =
|
|
7929
|
+
const createStyles = (parent, styles, cspNonce) => {
|
|
7930
|
+
const document = parent.ownerDocument;
|
|
7903
7931
|
if (document) {
|
|
7904
7932
|
const style = document.createElement("style");
|
|
7905
7933
|
style.textContent = styles;
|
|
7906
7934
|
if (cspNonce) style.nonce = cspNonce;
|
|
7907
|
-
|
|
7935
|
+
parent.appendChild(style);
|
|
7908
7936
|
}
|
|
7909
7937
|
};
|
|
7910
7938
|
/**
|
|
@@ -8024,9 +8052,20 @@
|
|
|
8024
8052
|
cloneIframeContainer.style.position = "fixed";
|
|
8025
8053
|
cloneIframeContainer.style.left = "-10000px";
|
|
8026
8054
|
cloneIframeContainer.style.top = "0px";
|
|
8027
|
-
cloneIframeContainer.style.border = "0";
|
|
8028
8055
|
cloneIframeContainer.width = bounds.width.toString();
|
|
8029
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");
|
|
8030
8069
|
cloneIframeContainer.scrolling = "no";
|
|
8031
8070
|
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, "true");
|
|
8032
8071
|
(customContainer || ownerDocument.body).appendChild(cloneIframeContainer);
|
|
@@ -8069,9 +8108,9 @@
|
|
|
8069
8108
|
if (!rules) return true;
|
|
8070
8109
|
for (let i = 0; i < rules.length; i++) {
|
|
8071
8110
|
const rule = rules[i];
|
|
8072
|
-
if (rule
|
|
8073
|
-
|
|
8074
|
-
|
|
8111
|
+
if (!rule || !("styleSheet" in rule)) continue;
|
|
8112
|
+
const inner = rule.styleSheet;
|
|
8113
|
+
if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
|
|
8075
8114
|
}
|
|
8076
8115
|
return true;
|
|
8077
8116
|
};
|
|
@@ -8113,7 +8152,7 @@
|
|
|
8113
8152
|
* remain as the settle path for plain <link> fetches.
|
|
8114
8153
|
* @internal – exported for testing only.
|
|
8115
8154
|
*/
|
|
8116
|
-
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8155
|
+
const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
|
|
8117
8156
|
if (signal?.aborted) return Promise.resolve();
|
|
8118
8157
|
const pending = [];
|
|
8119
8158
|
const links = [];
|
|
@@ -8143,8 +8182,8 @@
|
|
|
8143
8182
|
const timer = setInterval(() => {
|
|
8144
8183
|
elapsed += 50;
|
|
8145
8184
|
if (link.sheet) settle();
|
|
8146
|
-
else if (elapsed >=
|
|
8147
|
-
logger?.warn(`Stylesheet ${href} did not finish loading within ${
|
|
8185
|
+
else if (elapsed >= timeoutMs) {
|
|
8186
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8148
8187
|
settle();
|
|
8149
8188
|
}
|
|
8150
8189
|
}, 50);
|
|
@@ -8161,8 +8200,8 @@
|
|
|
8161
8200
|
const timer = setInterval(() => {
|
|
8162
8201
|
elapsed += 50;
|
|
8163
8202
|
const currentSheet = style.sheet;
|
|
8164
|
-
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >=
|
|
8165
|
-
if (elapsed >=
|
|
8203
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
|
|
8204
|
+
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`);
|
|
8166
8205
|
clearInterval(timer);
|
|
8167
8206
|
resolve();
|
|
8168
8207
|
}
|
|
@@ -8375,6 +8414,7 @@
|
|
|
8375
8414
|
}
|
|
8376
8415
|
this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
|
|
8377
8416
|
this.appendDocumentAdoptedStyles(element.ownerDocument);
|
|
8417
|
+
this.appendPseudoHideStyles();
|
|
8378
8418
|
}
|
|
8379
8419
|
/**
|
|
8380
8420
|
* Re-materialise the source document's constructable stylesheets
|
|
@@ -8394,6 +8434,16 @@
|
|
|
8394
8434
|
const style = createAdoptedStylesElement(this.documentElement.ownerDocument ?? ownerDocument, sheets, this.options.cspNonce);
|
|
8395
8435
|
if (style) head.appendChild(style);
|
|
8396
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
|
+
}
|
|
8397
8447
|
toIFrame(ownerDocument, windowSize) {
|
|
8398
8448
|
return mountCloneInIFrame(this.context, ownerDocument, windowSize, {
|
|
8399
8449
|
documentElement: this.documentElement,
|
|
@@ -8529,7 +8579,6 @@
|
|
|
8529
8579
|
const style = window.getComputedStyle(node);
|
|
8530
8580
|
const checkPseudoElements = canHavePseudoElements(node);
|
|
8531
8581
|
if (this.referenceElement === node && isHTMLElementNode(clone)) this.clonedReferenceElement = clone;
|
|
8532
|
-
if (isBodyElement(clone)) createPseudoHideStyles(clone, this.options.cspNonce);
|
|
8533
8582
|
const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
|
8534
8583
|
if (checkPseudoElements) {
|
|
8535
8584
|
const styleBefore = window.getComputedStyle(node, ":before");
|
|
@@ -8653,6 +8702,115 @@
|
|
|
8653
8702
|
}
|
|
8654
8703
|
};
|
|
8655
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
|
|
8656
8814
|
//#region src/dom/element-container.ts
|
|
8657
8815
|
var ElementContainer = class {
|
|
8658
8816
|
context;
|
|
@@ -8666,6 +8824,11 @@
|
|
|
8666
8824
|
* bounds to leave a matching gap (issue #227).
|
|
8667
8825
|
*/
|
|
8668
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;
|
|
8669
8832
|
createsStackingContext = false;
|
|
8670
8833
|
createsRealStackingContext = false;
|
|
8671
8834
|
isListOwner = false;
|
|
@@ -8690,6 +8853,7 @@
|
|
|
8690
8853
|
this.bounds = new Bounds(this.bounds.left, borderBoxTop, this.bounds.width, this.bounds.height - (borderBoxTop - this.bounds.top));
|
|
8691
8854
|
}
|
|
8692
8855
|
}
|
|
8856
|
+
this.filterReferences = resolveFilterReferences(element, this.styles.filter, this.bounds);
|
|
8693
8857
|
if (isDebugging(element, 4)) this.debugRender = true;
|
|
8694
8858
|
}
|
|
8695
8859
|
/**
|
|
@@ -9850,7 +10014,7 @@
|
|
|
9850
10014
|
if (treatAsRealStackingContext || createsStackingContext) {
|
|
9851
10015
|
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
|
9852
10016
|
const stack = new StackingContext(paintContainer);
|
|
9853
|
-
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
10017
|
+
if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9854
10018
|
const order = child.styles.zIndex.order;
|
|
9855
10019
|
if (order < 0) {
|
|
9856
10020
|
let index = 0;
|
|
@@ -10205,6 +10369,13 @@
|
|
|
10205
10369
|
value: Number.parseFloat(numeric) * (percentage ? 1 / 100 : 1)
|
|
10206
10370
|
};
|
|
10207
10371
|
}
|
|
10372
|
+
if (name === "url") {
|
|
10373
|
+
const ref = parseFilterReference(body);
|
|
10374
|
+
return ref ? {
|
|
10375
|
+
kind: "url",
|
|
10376
|
+
ref
|
|
10377
|
+
} : null;
|
|
10378
|
+
}
|
|
10208
10379
|
return null;
|
|
10209
10380
|
};
|
|
10210
10381
|
/** Split on whitespace outside parentheses so rgba(...) colours stay one token. */
|
|
@@ -10260,22 +10431,25 @@
|
|
|
10260
10431
|
case "blur": return `blur(${fn.value * scale}px)`;
|
|
10261
10432
|
case "drop-shadow": return `drop-shadow(${fn.x * scale}px ${fn.y * scale}px ${fn.blur * scale}px ${fn.color})`;
|
|
10262
10433
|
case "unitless": return fn.name === "hue-rotate" ? `hue-rotate(${fn.value}deg)` : `${fn.name}(${fn.value})`;
|
|
10434
|
+
case "url": return `url("${fn.ref}")`;
|
|
10263
10435
|
}
|
|
10264
10436
|
}).join(" ");
|
|
10265
10437
|
/**
|
|
10266
10438
|
* Outset (in CSS pixels) a surface must grow so blur/shadow spillover is not
|
|
10267
10439
|
* cropped. Unitless functions never spread beyond the bounds. Consecutive
|
|
10268
|
-
* 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.
|
|
10269
10442
|
*/
|
|
10270
|
-
function filterOutset(filter) {
|
|
10443
|
+
function filterOutset(filter, references) {
|
|
10271
10444
|
let blurSum = 0;
|
|
10272
10445
|
let maxShadowOffset = 0;
|
|
10446
|
+
let referenceOutset = 0;
|
|
10273
10447
|
for (const fn of filter.functions) if (fn.kind === "blur") blurSum += fn.value;
|
|
10274
10448
|
else if (fn.kind === "drop-shadow") {
|
|
10275
10449
|
blurSum += fn.blur;
|
|
10276
10450
|
maxShadowOffset = Math.max(maxShadowOffset, Math.abs(fn.x), Math.abs(fn.y));
|
|
10277
|
-
}
|
|
10278
|
-
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);
|
|
10279
10453
|
}
|
|
10280
10454
|
function canvasLike(source) {
|
|
10281
10455
|
const canvas = (source.ownerDocument ?? document).createElement("canvas");
|
|
@@ -10371,8 +10545,9 @@
|
|
|
10371
10545
|
if (blurs > 0 && shadows > 0) return filter.functions.findIndex((fn) => fn.kind === "blur") < filter.functions.findIndex((fn) => fn.kind === "drop-shadow");
|
|
10372
10546
|
return true;
|
|
10373
10547
|
};
|
|
10374
|
-
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
10548
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal, references) {
|
|
10375
10549
|
throwIfAborted(signal);
|
|
10550
|
+
if (filter.functions.some((fn) => fn.kind === "url")) return renderReferencedFilterSurface(source, filter, opacity, scale, signal, references);
|
|
10376
10551
|
if (filter.functions.length > 0 && supportsNativeFilters(source.ownerDocument ?? document)) {
|
|
10377
10552
|
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
10378
10553
|
if (output) return output;
|
|
@@ -10380,6 +10555,136 @@
|
|
|
10380
10555
|
if (!isSvgExpressible(filter)) throw new FilterSurfaceError("Filter chain requires native canvas filters");
|
|
10381
10556
|
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
10382
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
|
+
}
|
|
10383
10688
|
async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
|
|
10384
10689
|
throwIfAborted(signal);
|
|
10385
10690
|
const blur = filter.functions.find((fn) => fn.kind === "blur");
|
|
@@ -10391,29 +10696,9 @@
|
|
|
10391
10696
|
blur: shadow.blur,
|
|
10392
10697
|
color: shadow.color
|
|
10393
10698
|
} : void 0;
|
|
10394
|
-
if (!blurValue && !shadowValue)
|
|
10395
|
-
const output = canvasLike(source);
|
|
10396
|
-
const context = output.getContext("2d");
|
|
10397
|
-
if (!context) {
|
|
10398
|
-
releaseSurface(output);
|
|
10399
|
-
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10400
|
-
}
|
|
10401
|
-
context.globalAlpha = opacity;
|
|
10402
|
-
try {
|
|
10403
|
-
context.drawImage(source, 0, 0);
|
|
10404
|
-
return output;
|
|
10405
|
-
} catch {
|
|
10406
|
-
releaseSurface(output);
|
|
10407
|
-
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10408
|
-
}
|
|
10409
|
-
}
|
|
10410
|
-
const namespace = "http://www.w3.org/2000/svg";
|
|
10699
|
+
if (!blurValue && !shadowValue) return drawWithOpacity(source, opacity);
|
|
10411
10700
|
const svgDocument = source.ownerDocument ?? document;
|
|
10412
|
-
const element = (tag, attributes = {}) =>
|
|
10413
|
-
const node = svgDocument.createElementNS(namespace, tag);
|
|
10414
|
-
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
10415
|
-
return node;
|
|
10416
|
-
};
|
|
10701
|
+
const element = (tag, attributes = {}) => createSvgElement(svgDocument, tag, attributes);
|
|
10417
10702
|
const svg = element("svg", {
|
|
10418
10703
|
width: source.width,
|
|
10419
10704
|
height: source.height,
|
|
@@ -10455,39 +10740,13 @@
|
|
|
10455
10740
|
}
|
|
10456
10741
|
defs.appendChild(filterElement);
|
|
10457
10742
|
svg.appendChild(defs);
|
|
10458
|
-
let sourceUrl;
|
|
10459
|
-
try {
|
|
10460
|
-
sourceUrl = source.toDataURL("image/png");
|
|
10461
|
-
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
10462
|
-
} catch {
|
|
10463
|
-
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
10464
|
-
}
|
|
10465
10743
|
svg.appendChild(element("image", {
|
|
10466
10744
|
width: source.width,
|
|
10467
10745
|
height: source.height,
|
|
10468
|
-
href:
|
|
10746
|
+
href: serializeSurface(source),
|
|
10469
10747
|
filter: "url(#composited-filter)"
|
|
10470
10748
|
}));
|
|
10471
|
-
|
|
10472
|
-
const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
|
|
10473
|
-
let output;
|
|
10474
|
-
try {
|
|
10475
|
-
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
10476
|
-
await decodeSurface(image, signal);
|
|
10477
|
-
throwIfAborted(signal);
|
|
10478
|
-
output = canvasLike(source);
|
|
10479
|
-
const context = output.getContext("2d");
|
|
10480
|
-
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10481
|
-
context.globalAlpha = opacity;
|
|
10482
|
-
context.drawImage(image, 0, 0);
|
|
10483
|
-
return output;
|
|
10484
|
-
} catch (error) {
|
|
10485
|
-
if (output) releaseSurface(output);
|
|
10486
|
-
if (signal?.aborted) throwIfAborted(signal);
|
|
10487
|
-
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10488
|
-
} finally {
|
|
10489
|
-
image.removeAttribute("src");
|
|
10490
|
-
}
|
|
10749
|
+
return drawSvgSurface(svg, source, opacity, signal);
|
|
10491
10750
|
}
|
|
10492
10751
|
function reserveSurface(budget, width, height) {
|
|
10493
10752
|
const pixels = width * height * 4;
|
|
@@ -10511,7 +10770,7 @@
|
|
|
10511
10770
|
}
|
|
10512
10771
|
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
10513
10772
|
const filter = parseFilterChain(styles.filter);
|
|
10514
|
-
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
10773
|
+
return expand(bounds, filter ? filterOutset(filter, container.filterReferences) : 0);
|
|
10515
10774
|
}
|
|
10516
10775
|
function cropSurface(bounds, viewport, margin, scale) {
|
|
10517
10776
|
const area = expand(viewport, margin);
|
|
@@ -12148,6 +12407,7 @@
|
|
|
12148
12407
|
ctx.fillStyle = rasterized.lastColor;
|
|
12149
12408
|
ctx.fillRect(area.left, area.top, area.width, area.height);
|
|
12150
12409
|
ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
|
|
12410
|
+
releaseSurface(rasterized.canvas);
|
|
12151
12411
|
};
|
|
12152
12412
|
/** Draw one gradient layer filling the whole mask area. */
|
|
12153
12413
|
const paintGradientLayer = (ctx, layer, area) => {
|
|
@@ -13002,7 +13262,8 @@
|
|
|
13002
13262
|
async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
|
|
13003
13263
|
const signal = this.options.signal;
|
|
13004
13264
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
13005
|
-
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);
|
|
13006
13267
|
if (!bounds.width || !bounds.height) return true;
|
|
13007
13268
|
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
13008
13269
|
if (!reserved) {
|
|
@@ -13024,7 +13285,13 @@
|
|
|
13024
13285
|
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
13025
13286
|
await source.renderStackContent(stack);
|
|
13026
13287
|
source.effectsRenderer.applyEffects([]);
|
|
13027
|
-
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
|
+
});
|
|
13028
13295
|
const composited = filtered ?? source.canvas;
|
|
13029
13296
|
if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
|
|
13030
13297
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -13061,7 +13328,10 @@
|
|
|
13061
13328
|
maskCanvas.width = surface.width;
|
|
13062
13329
|
maskCanvas.height = surface.height;
|
|
13063
13330
|
const maskCtx = maskCanvas.getContext("2d");
|
|
13064
|
-
if (!maskCtx)
|
|
13331
|
+
if (!maskCtx) {
|
|
13332
|
+
releaseSurface(maskCanvas);
|
|
13333
|
+
return;
|
|
13334
|
+
}
|
|
13065
13335
|
maskCtx.scale(this.options.scale, this.options.scale);
|
|
13066
13336
|
maskCtx.translate(-bounds.left, -bounds.top);
|
|
13067
13337
|
try {
|
|
@@ -13073,6 +13343,7 @@
|
|
|
13073
13343
|
}, async (url) => await this.context.cache.match(url));
|
|
13074
13344
|
} catch (error) {
|
|
13075
13345
|
this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
|
|
13346
|
+
releaseSurface(maskCanvas);
|
|
13076
13347
|
return;
|
|
13077
13348
|
}
|
|
13078
13349
|
ctx.save();
|
|
@@ -13082,6 +13353,7 @@
|
|
|
13082
13353
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
13083
13354
|
} finally {
|
|
13084
13355
|
ctx.restore();
|
|
13356
|
+
releaseSurface(maskCanvas);
|
|
13085
13357
|
}
|
|
13086
13358
|
}
|
|
13087
13359
|
/**
|
|
@@ -13486,6 +13758,7 @@
|
|
|
13486
13758
|
ctx.fillStyle = rasterized.lastColor;
|
|
13487
13759
|
ctx.fillRect(0, 0, width, height);
|
|
13488
13760
|
ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
|
|
13761
|
+
releaseSurface(rasterized.canvas);
|
|
13489
13762
|
return canvas;
|
|
13490
13763
|
}
|
|
13491
13764
|
if (source.type === 5 || source.type === 6) {
|