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
|
@@ -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
|
*/
|
|
@@ -242,6 +242,30 @@ var Validator = class {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
+
* Validate the backgroundColor option
|
|
246
|
+
*
|
|
247
|
+
* @param backgroundColor - CSS color string or null (transparent)
|
|
248
|
+
* @returns Validation result
|
|
249
|
+
*/
|
|
250
|
+
validateBackgroundColor(backgroundColor) {
|
|
251
|
+
if (backgroundColor === null) return {
|
|
252
|
+
valid: true,
|
|
253
|
+
sanitized: null
|
|
254
|
+
};
|
|
255
|
+
if (typeof backgroundColor !== "string" || backgroundColor.length === 0) return {
|
|
256
|
+
valid: false,
|
|
257
|
+
error: "backgroundColor must be a CSS color string or null"
|
|
258
|
+
};
|
|
259
|
+
if (/[^a-zA-Z0-9%(),.#\s/-]/.test(backgroundColor)) return {
|
|
260
|
+
valid: false,
|
|
261
|
+
error: `backgroundColor contains characters that cannot appear in a CSS color: ${backgroundColor}`
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
valid: true,
|
|
265
|
+
sanitized: backgroundColor
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
245
269
|
* Validate image timeout
|
|
246
270
|
*
|
|
247
271
|
* @param timeout - Timeout in milliseconds
|
|
@@ -386,6 +410,10 @@ var Validator = class {
|
|
|
386
410
|
const nonceResult = this.validateCspNonce(options.cspNonce);
|
|
387
411
|
if (!nonceResult.valid) errors.push(`CSP nonce: ${nonceResult.error}`);
|
|
388
412
|
}
|
|
413
|
+
if (options.backgroundColor !== void 0) {
|
|
414
|
+
const colorResult = this.validateBackgroundColor(options.backgroundColor);
|
|
415
|
+
if (!colorResult.valid) errors.push(`backgroundColor: ${colorResult.error}`);
|
|
416
|
+
}
|
|
389
417
|
if (this.config.customValidator) {
|
|
390
418
|
const customResult = this.config.customValidator(options, "options");
|
|
391
419
|
if (!customResult.valid) errors.push(`Custom validation: ${customResult.error}`);
|
|
@@ -7885,19 +7913,19 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
|
|
|
7885
7913
|
}`;
|
|
7886
7914
|
/**
|
|
7887
7915
|
* Inject the stylesheet that hides pseudo-element surrogate containers.
|
|
7888
|
-
*
|
|
7916
|
+
* Added to the cloned <head> so the surrogates themselves never render.
|
|
7889
7917
|
*/
|
|
7890
|
-
const createPseudoHideStyles = (
|
|
7891
|
-
createStyles(
|
|
7918
|
+
const createPseudoHideStyles = (parent, cspNonce) => {
|
|
7919
|
+
createStyles(parent, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
|
|
7892
7920
|
.${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
|
|
7893
7921
|
};
|
|
7894
|
-
const createStyles = (
|
|
7895
|
-
const document =
|
|
7922
|
+
const createStyles = (parent, styles, cspNonce) => {
|
|
7923
|
+
const document = parent.ownerDocument;
|
|
7896
7924
|
if (document) {
|
|
7897
7925
|
const style = document.createElement("style");
|
|
7898
7926
|
style.textContent = styles;
|
|
7899
7927
|
if (cspNonce) style.nonce = cspNonce;
|
|
7900
|
-
|
|
7928
|
+
parent.appendChild(style);
|
|
7901
7929
|
}
|
|
7902
7930
|
};
|
|
7903
7931
|
/**
|
|
@@ -8017,9 +8045,20 @@ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
|
|
|
8017
8045
|
cloneIframeContainer.style.position = "fixed";
|
|
8018
8046
|
cloneIframeContainer.style.left = "-10000px";
|
|
8019
8047
|
cloneIframeContainer.style.top = "0px";
|
|
8020
|
-
cloneIframeContainer.style.border = "0";
|
|
8021
8048
|
cloneIframeContainer.width = bounds.width.toString();
|
|
8022
8049
|
cloneIframeContainer.height = bounds.height.toString();
|
|
8050
|
+
const contentBox = [
|
|
8051
|
+
["width", `${bounds.width}px`],
|
|
8052
|
+
["height", `${bounds.height}px`],
|
|
8053
|
+
["min-width", "0px"],
|
|
8054
|
+
["max-width", "none"],
|
|
8055
|
+
["min-height", "0px"],
|
|
8056
|
+
["max-height", "none"],
|
|
8057
|
+
["box-sizing", "content-box"],
|
|
8058
|
+
["padding", "0px"],
|
|
8059
|
+
["border", "0"]
|
|
8060
|
+
];
|
|
8061
|
+
for (const [property, value] of contentBox) cloneIframeContainer.style.setProperty(property, value, "important");
|
|
8023
8062
|
cloneIframeContainer.scrolling = "no";
|
|
8024
8063
|
cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, "true");
|
|
8025
8064
|
(customContainer || ownerDocument.body).appendChild(cloneIframeContainer);
|
|
@@ -8062,9 +8101,9 @@ const isSheetFullyApplied = (sheet, depth = 0) => {
|
|
|
8062
8101
|
if (!rules) return true;
|
|
8063
8102
|
for (let i = 0; i < rules.length; i++) {
|
|
8064
8103
|
const rule = rules[i];
|
|
8065
|
-
if (rule
|
|
8066
|
-
|
|
8067
|
-
|
|
8104
|
+
if (!rule || !("styleSheet" in rule)) continue;
|
|
8105
|
+
const inner = rule.styleSheet;
|
|
8106
|
+
if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
|
|
8068
8107
|
}
|
|
8069
8108
|
return true;
|
|
8070
8109
|
};
|
|
@@ -8106,7 +8145,7 @@ const collectLinks = (root, out) => {
|
|
|
8106
8145
|
* remain as the settle path for plain <link> fetches.
|
|
8107
8146
|
* @internal – exported for testing only.
|
|
8108
8147
|
*/
|
|
8109
|
-
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8148
|
+
const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
|
|
8110
8149
|
if (signal?.aborted) return Promise.resolve();
|
|
8111
8150
|
const pending = [];
|
|
8112
8151
|
const links = [];
|
|
@@ -8136,8 +8175,8 @@ const stylesheetsReady = (documentClone, logger, signal) => {
|
|
|
8136
8175
|
const timer = setInterval(() => {
|
|
8137
8176
|
elapsed += 50;
|
|
8138
8177
|
if (link.sheet) settle();
|
|
8139
|
-
else if (elapsed >=
|
|
8140
|
-
logger?.warn(`Stylesheet ${href} did not finish loading within ${
|
|
8178
|
+
else if (elapsed >= timeoutMs) {
|
|
8179
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8141
8180
|
settle();
|
|
8142
8181
|
}
|
|
8143
8182
|
}, 50);
|
|
@@ -8154,8 +8193,8 @@ const stylesheetsReady = (documentClone, logger, signal) => {
|
|
|
8154
8193
|
const timer = setInterval(() => {
|
|
8155
8194
|
elapsed += 50;
|
|
8156
8195
|
const currentSheet = style.sheet;
|
|
8157
|
-
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >=
|
|
8158
|
-
if (elapsed >=
|
|
8196
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
|
|
8197
|
+
if (elapsed >= timeoutMs && !isSheetFullyApplied(currentSheet ?? null)) logger?.warn(`@import chain in an inline <style> did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8159
8198
|
clearInterval(timer);
|
|
8160
8199
|
resolve();
|
|
8161
8200
|
}
|
|
@@ -8368,6 +8407,7 @@ var DocumentCloner = class {
|
|
|
8368
8407
|
}
|
|
8369
8408
|
this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
|
|
8370
8409
|
this.appendDocumentAdoptedStyles(element.ownerDocument);
|
|
8410
|
+
this.appendPseudoHideStyles();
|
|
8371
8411
|
}
|
|
8372
8412
|
/**
|
|
8373
8413
|
* Re-materialise the source document's constructable stylesheets
|
|
@@ -8387,6 +8427,16 @@ var DocumentCloner = class {
|
|
|
8387
8427
|
const style = createAdoptedStylesElement(this.documentElement.ownerDocument ?? ownerDocument, sheets, this.options.cspNonce);
|
|
8388
8428
|
if (style) head.appendChild(style);
|
|
8389
8429
|
}
|
|
8430
|
+
/**
|
|
8431
|
+
* Add the rules that hide the source ::before / ::after (the clone draws
|
|
8432
|
+
* them as surrogate elements) to the clone's <head>. Inside <body> the
|
|
8433
|
+
* <style> would be one of its children, and `body > :first-child`,
|
|
8434
|
+
* `:nth-child()` and sibling selectors would match the wrong elements.
|
|
8435
|
+
*/
|
|
8436
|
+
appendPseudoHideStyles() {
|
|
8437
|
+
const head = this.documentElement.getElementsByTagName("head").item(0);
|
|
8438
|
+
createPseudoHideStyles(head ?? this.documentElement, this.options.cspNonce);
|
|
8439
|
+
}
|
|
8390
8440
|
toIFrame(ownerDocument, windowSize) {
|
|
8391
8441
|
return mountCloneInIFrame(this.context, ownerDocument, windowSize, {
|
|
8392
8442
|
documentElement: this.documentElement,
|
|
@@ -8522,7 +8572,6 @@ var DocumentCloner = class {
|
|
|
8522
8572
|
const style = window.getComputedStyle(node);
|
|
8523
8573
|
const checkPseudoElements = canHavePseudoElements(node);
|
|
8524
8574
|
if (this.referenceElement === node && isHTMLElementNode(clone)) this.clonedReferenceElement = clone;
|
|
8525
|
-
if (isBodyElement(clone)) createPseudoHideStyles(clone, this.options.cspNonce);
|
|
8526
8575
|
const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
|
|
8527
8576
|
if (checkPseudoElements) {
|
|
8528
8577
|
const styleBefore = window.getComputedStyle(node, ":before");
|
|
@@ -8646,6 +8695,115 @@ var DOMNormalizer = class {
|
|
|
8646
8695
|
}
|
|
8647
8696
|
};
|
|
8648
8697
|
//#endregion
|
|
8698
|
+
//#region src/dom/filter-reference.ts
|
|
8699
|
+
const SVG_NS$1 = "http://www.w3.org/2000/svg";
|
|
8700
|
+
const XLINK_NS = "http://www.w3.org/1999/xlink";
|
|
8701
|
+
const REGION_DEFAULTS = {
|
|
8702
|
+
x: "-10%",
|
|
8703
|
+
y: "-10%",
|
|
8704
|
+
width: "120%",
|
|
8705
|
+
height: "120%"
|
|
8706
|
+
};
|
|
8707
|
+
const PRESENTATION_PROPERTIES = [
|
|
8708
|
+
"color-interpolation-filters",
|
|
8709
|
+
"flood-color",
|
|
8710
|
+
"flood-opacity",
|
|
8711
|
+
"lighting-color"
|
|
8712
|
+
];
|
|
8713
|
+
const REFERENCE_RE = /^(["']?)(#[^"'\s()]+)\1$/;
|
|
8714
|
+
const URL_RE = /url\(\s*([^)]*?)\s*\)/g;
|
|
8715
|
+
/** The `#id` of a same-document url() body (`"#id"`, `'#id'` or `#id`), or null. */
|
|
8716
|
+
const parseFilterReference = (body) => {
|
|
8717
|
+
const match = REFERENCE_RE.exec(body.trim());
|
|
8718
|
+
return match ? match[2] : null;
|
|
8719
|
+
};
|
|
8720
|
+
/**
|
|
8721
|
+
* Resolve every same-document url() reference of a computed filter value.
|
|
8722
|
+
* References that cannot be expressed in the serialized SVG are left out, so
|
|
8723
|
+
* the compositor rejects the chain and the subtree keeps the legacy renderer.
|
|
8724
|
+
*/
|
|
8725
|
+
function resolveFilterReferences(element, filter, box) {
|
|
8726
|
+
if (!filter || filter.indexOf("url(") === -1) return void 0;
|
|
8727
|
+
const references = /* @__PURE__ */ new Map();
|
|
8728
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8729
|
+
for (const match of filter.matchAll(URL_RE)) {
|
|
8730
|
+
const ref = parseFilterReference(match[1]);
|
|
8731
|
+
if (!ref || seen.has(ref)) continue;
|
|
8732
|
+
seen.add(ref);
|
|
8733
|
+
const reference = resolveFilterReference(element.ownerDocument, ref, box);
|
|
8734
|
+
if (reference) references.set(ref, reference);
|
|
8735
|
+
}
|
|
8736
|
+
return references;
|
|
8737
|
+
}
|
|
8738
|
+
function resolveFilterReference(document, ref, box) {
|
|
8739
|
+
const source = document.getElementById(ref.slice(1));
|
|
8740
|
+
if (!source || source.namespaceURI !== SVG_NS$1 || source.localName !== "filter") return null;
|
|
8741
|
+
if (!isSelfContained(source)) return null;
|
|
8742
|
+
const region = filterRegion(source, box);
|
|
8743
|
+
if (!region) return null;
|
|
8744
|
+
const copy = source.cloneNode(true);
|
|
8745
|
+
inlinePresentation(source, copy);
|
|
8746
|
+
copy.removeAttribute("id");
|
|
8747
|
+
copy.setAttribute("filterUnits", "userSpaceOnUse");
|
|
8748
|
+
Object.keys(region).forEach((name) => copy.setAttribute(name, String(region[name])));
|
|
8749
|
+
const outset = Math.max(0, -region.x, -region.y, region.x + region.width - box.width, region.y + region.height - box.height);
|
|
8750
|
+
return {
|
|
8751
|
+
element: copy,
|
|
8752
|
+
region,
|
|
8753
|
+
outset: Math.ceil(outset)
|
|
8754
|
+
};
|
|
8755
|
+
}
|
|
8756
|
+
const hasHref = (node) => node.hasAttribute("href") || node.hasAttributeNS(XLINK_NS, "href");
|
|
8757
|
+
function isSelfContained(source) {
|
|
8758
|
+
if (source.getAttribute("primitiveUnits") === "objectBoundingBox" || hasHref(source)) return false;
|
|
8759
|
+
for (const primitive of Array.from(source.querySelectorAll("*"))) {
|
|
8760
|
+
if (primitive.localName === "feImage" || hasHref(primitive)) return false;
|
|
8761
|
+
for (const name of [
|
|
8762
|
+
"x",
|
|
8763
|
+
"y",
|
|
8764
|
+
"width",
|
|
8765
|
+
"height"
|
|
8766
|
+
]) if ((primitive.getAttribute(name) ?? "").indexOf("%") !== -1) return false;
|
|
8767
|
+
}
|
|
8768
|
+
return true;
|
|
8769
|
+
}
|
|
8770
|
+
function filterRegion(source, box) {
|
|
8771
|
+
const boundingBox = source.getAttribute("filterUnits") !== "userSpaceOnUse";
|
|
8772
|
+
const read = (name, size) => {
|
|
8773
|
+
const raw = (source.getAttribute(name) ?? (boundingBox ? REGION_DEFAULTS[name] : "")).trim();
|
|
8774
|
+
if (raw === "") return null;
|
|
8775
|
+
const percentage = raw.endsWith("%");
|
|
8776
|
+
const value = Number(percentage ? raw.slice(0, -1) : raw.replace(/px$/, ""));
|
|
8777
|
+
if (!Number.isFinite(value)) return null;
|
|
8778
|
+
if (boundingBox) return (percentage ? value / 100 : value) * size;
|
|
8779
|
+
return percentage ? null : value;
|
|
8780
|
+
};
|
|
8781
|
+
const x = read("x", box.width);
|
|
8782
|
+
const y = read("y", box.height);
|
|
8783
|
+
const width = read("width", box.width);
|
|
8784
|
+
const height = read("height", box.height);
|
|
8785
|
+
if (x === null || y === null || width === null || height === null || width <= 0 || height <= 0) return null;
|
|
8786
|
+
return {
|
|
8787
|
+
x,
|
|
8788
|
+
y,
|
|
8789
|
+
width,
|
|
8790
|
+
height
|
|
8791
|
+
};
|
|
8792
|
+
}
|
|
8793
|
+
function inlinePresentation(source, copy) {
|
|
8794
|
+
const view = source.ownerDocument.defaultView;
|
|
8795
|
+
if (!view) return;
|
|
8796
|
+
const sources = [source, ...Array.from(source.querySelectorAll("*"))];
|
|
8797
|
+
const copies = [copy, ...Array.from(copy.querySelectorAll("*"))];
|
|
8798
|
+
sources.forEach((node, index) => {
|
|
8799
|
+
const style = view.getComputedStyle(node);
|
|
8800
|
+
for (const property of PRESENTATION_PROPERTIES) {
|
|
8801
|
+
const value = style.getPropertyValue(property);
|
|
8802
|
+
if (value) copies[index]?.setAttribute(property, value);
|
|
8803
|
+
}
|
|
8804
|
+
});
|
|
8805
|
+
}
|
|
8806
|
+
//#endregion
|
|
8649
8807
|
//#region src/dom/element-container.ts
|
|
8650
8808
|
var ElementContainer = class {
|
|
8651
8809
|
context;
|
|
@@ -8659,6 +8817,11 @@ var ElementContainer = class {
|
|
|
8659
8817
|
* bounds to leave a matching gap (issue #227).
|
|
8660
8818
|
*/
|
|
8661
8819
|
legendBounds;
|
|
8820
|
+
/**
|
|
8821
|
+
* Same-document `filter: url(#id)` references, resolved while the cloned
|
|
8822
|
+
* DOM is available. The surface compositor renders them in an SVG pass.
|
|
8823
|
+
*/
|
|
8824
|
+
filterReferences;
|
|
8662
8825
|
createsStackingContext = false;
|
|
8663
8826
|
createsRealStackingContext = false;
|
|
8664
8827
|
isListOwner = false;
|
|
@@ -8683,6 +8846,7 @@ var ElementContainer = class {
|
|
|
8683
8846
|
this.bounds = new Bounds(this.bounds.left, borderBoxTop, this.bounds.width, this.bounds.height - (borderBoxTop - this.bounds.top));
|
|
8684
8847
|
}
|
|
8685
8848
|
}
|
|
8849
|
+
this.filterReferences = resolveFilterReferences(element, this.styles.filter, this.bounds);
|
|
8686
8850
|
if (isDebugging(element, 4)) this.debugRender = true;
|
|
8687
8851
|
}
|
|
8688
8852
|
/**
|
|
@@ -9843,7 +10007,7 @@ const parseStackTree = (parent, stackingContext, realStackingContext, listItems)
|
|
|
9843
10007
|
if (treatAsRealStackingContext || createsStackingContext) {
|
|
9844
10008
|
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
|
9845
10009
|
const stack = new StackingContext(paintContainer);
|
|
9846
|
-
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
10010
|
+
if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9847
10011
|
const order = child.styles.zIndex.order;
|
|
9848
10012
|
if (order < 0) {
|
|
9849
10013
|
let index = 0;
|
|
@@ -10198,6 +10362,13 @@ const parseFilterFunction = (name, body) => {
|
|
|
10198
10362
|
value: Number.parseFloat(numeric) * (percentage ? 1 / 100 : 1)
|
|
10199
10363
|
};
|
|
10200
10364
|
}
|
|
10365
|
+
if (name === "url") {
|
|
10366
|
+
const ref = parseFilterReference(body);
|
|
10367
|
+
return ref ? {
|
|
10368
|
+
kind: "url",
|
|
10369
|
+
ref
|
|
10370
|
+
} : null;
|
|
10371
|
+
}
|
|
10201
10372
|
return null;
|
|
10202
10373
|
};
|
|
10203
10374
|
/** Split on whitespace outside parentheses so rgba(...) colours stay one token. */
|
|
@@ -10253,22 +10424,25 @@ const nativeFilterString = (filter, scale) => filter.functions.map((fn) => {
|
|
|
10253
10424
|
case "blur": return `blur(${fn.value * scale}px)`;
|
|
10254
10425
|
case "drop-shadow": return `drop-shadow(${fn.x * scale}px ${fn.y * scale}px ${fn.blur * scale}px ${fn.color})`;
|
|
10255
10426
|
case "unitless": return fn.name === "hue-rotate" ? `hue-rotate(${fn.value}deg)` : `${fn.name}(${fn.value})`;
|
|
10427
|
+
case "url": return `url("${fn.ref}")`;
|
|
10256
10428
|
}
|
|
10257
10429
|
}).join(" ");
|
|
10258
10430
|
/**
|
|
10259
10431
|
* Outset (in CSS pixels) a surface must grow so blur/shadow spillover is not
|
|
10260
10432
|
* cropped. Unitless functions never spread beyond the bounds. Consecutive
|
|
10261
|
-
* blurs compound, so their radii sum.
|
|
10433
|
+
* blurs compound, so their radii sum. A url() reference can paint up to its
|
|
10434
|
+
* filter region, which may extend beyond the border box.
|
|
10262
10435
|
*/
|
|
10263
|
-
function filterOutset(filter) {
|
|
10436
|
+
function filterOutset(filter, references) {
|
|
10264
10437
|
let blurSum = 0;
|
|
10265
10438
|
let maxShadowOffset = 0;
|
|
10439
|
+
let referenceOutset = 0;
|
|
10266
10440
|
for (const fn of filter.functions) if (fn.kind === "blur") blurSum += fn.value;
|
|
10267
10441
|
else if (fn.kind === "drop-shadow") {
|
|
10268
10442
|
blurSum += fn.blur;
|
|
10269
10443
|
maxShadowOffset = Math.max(maxShadowOffset, Math.abs(fn.x), Math.abs(fn.y));
|
|
10270
|
-
}
|
|
10271
|
-
return Math.ceil(3 * blurSum + maxShadowOffset);
|
|
10444
|
+
} else if (fn.kind === "url") referenceOutset += references?.get(fn.ref)?.outset ?? 0;
|
|
10445
|
+
return Math.ceil(3 * blurSum + maxShadowOffset + referenceOutset);
|
|
10272
10446
|
}
|
|
10273
10447
|
function canvasLike(source) {
|
|
10274
10448
|
const canvas = (source.ownerDocument ?? document).createElement("canvas");
|
|
@@ -10364,8 +10538,9 @@ const isSvgExpressible = (filter) => {
|
|
|
10364
10538
|
if (blurs > 0 && shadows > 0) return filter.functions.findIndex((fn) => fn.kind === "blur") < filter.functions.findIndex((fn) => fn.kind === "drop-shadow");
|
|
10365
10539
|
return true;
|
|
10366
10540
|
};
|
|
10367
|
-
async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
10541
|
+
async function renderFilterSurface(source, filter, opacity, scale, signal, references) {
|
|
10368
10542
|
throwIfAborted(signal);
|
|
10543
|
+
if (filter.functions.some((fn) => fn.kind === "url")) return renderReferencedFilterSurface(source, filter, opacity, scale, signal, references);
|
|
10369
10544
|
if (filter.functions.length > 0 && supportsNativeFilters(source.ownerDocument ?? document)) {
|
|
10370
10545
|
const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
|
|
10371
10546
|
if (output) return output;
|
|
@@ -10373,6 +10548,136 @@ async function renderFilterSurface(source, filter, opacity, scale, signal) {
|
|
|
10373
10548
|
if (!isSvgExpressible(filter)) throw new FilterSurfaceError("Filter chain requires native canvas filters");
|
|
10374
10549
|
return renderSvgFilterSurface(source, filter, opacity, scale, signal);
|
|
10375
10550
|
}
|
|
10551
|
+
/**
|
|
10552
|
+
* Split a chain at its url() references: each reference is one SVG pass, and
|
|
10553
|
+
* the standard functions between them run through the usual backends. Every
|
|
10554
|
+
* step filters the previous step's complete output at alpha 1, then CSS
|
|
10555
|
+
* opacity is applied once, as for any other chain.
|
|
10556
|
+
*/
|
|
10557
|
+
async function renderReferencedFilterSurface(source, filter, opacity, scale, signal, context) {
|
|
10558
|
+
const steps = [];
|
|
10559
|
+
for (const fn of filter.functions) {
|
|
10560
|
+
if (fn.kind === "url") {
|
|
10561
|
+
const reference = context?.references?.get(fn.ref);
|
|
10562
|
+
if (!reference) throw new FilterSurfaceError(`Filter reference ${fn.ref} cannot be resolved`);
|
|
10563
|
+
steps.push(reference);
|
|
10564
|
+
continue;
|
|
10565
|
+
}
|
|
10566
|
+
const last = steps[steps.length - 1];
|
|
10567
|
+
if (Array.isArray(last)) last.push(fn);
|
|
10568
|
+
else steps.push([fn]);
|
|
10569
|
+
}
|
|
10570
|
+
const origin = context?.origin ?? {
|
|
10571
|
+
x: 0,
|
|
10572
|
+
y: 0
|
|
10573
|
+
};
|
|
10574
|
+
const last = steps[steps.length - 1];
|
|
10575
|
+
const opacityApplied = Array.isArray(last);
|
|
10576
|
+
let current = source;
|
|
10577
|
+
try {
|
|
10578
|
+
for (const step of steps) {
|
|
10579
|
+
const next = Array.isArray(step) ? await renderFilterSurface(current, { functions: step }, step === last ? opacity : 1, scale, signal) : await renderReferenceSurface(current, step, origin, scale, signal);
|
|
10580
|
+
if (current !== source) releaseSurface(current);
|
|
10581
|
+
current = next;
|
|
10582
|
+
}
|
|
10583
|
+
if (opacityApplied || opacity === 1) {
|
|
10584
|
+
const output = current;
|
|
10585
|
+
current = source;
|
|
10586
|
+
return output;
|
|
10587
|
+
}
|
|
10588
|
+
return drawWithOpacity(current, opacity);
|
|
10589
|
+
} finally {
|
|
10590
|
+
if (current !== source) releaseSurface(current);
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
function drawWithOpacity(source, opacity) {
|
|
10594
|
+
const output = canvasLike(source);
|
|
10595
|
+
const context = output.getContext("2d");
|
|
10596
|
+
if (!context) {
|
|
10597
|
+
releaseSurface(output);
|
|
10598
|
+
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10599
|
+
}
|
|
10600
|
+
context.globalAlpha = opacity;
|
|
10601
|
+
try {
|
|
10602
|
+
context.drawImage(source, 0, 0);
|
|
10603
|
+
return output;
|
|
10604
|
+
} catch {
|
|
10605
|
+
releaseSurface(output);
|
|
10606
|
+
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10607
|
+
}
|
|
10608
|
+
}
|
|
10609
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
10610
|
+
function createSvgElement(owner, tag, attributes = {}) {
|
|
10611
|
+
const node = owner.createElementNS(SVG_NS, tag);
|
|
10612
|
+
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
10613
|
+
return node;
|
|
10614
|
+
}
|
|
10615
|
+
function serializeSurface(source) {
|
|
10616
|
+
try {
|
|
10617
|
+
const url = source.toDataURL("image/png");
|
|
10618
|
+
if (url === "data:,") throw new Error("Canvas size is unsupported");
|
|
10619
|
+
return url;
|
|
10620
|
+
} catch {
|
|
10621
|
+
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10624
|
+
/** Decode a filtered SVG image and draw it onto a surface-sized canvas. */
|
|
10625
|
+
async function drawSvgSurface(svg, source, opacity, signal) {
|
|
10626
|
+
const imageWindow = (source.ownerDocument ?? document).defaultView;
|
|
10627
|
+
const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
|
|
10628
|
+
let output;
|
|
10629
|
+
try {
|
|
10630
|
+
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
10631
|
+
await decodeSurface(image, signal);
|
|
10632
|
+
throwIfAborted(signal);
|
|
10633
|
+
output = canvasLike(source);
|
|
10634
|
+
const context = output.getContext("2d");
|
|
10635
|
+
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10636
|
+
context.globalAlpha = opacity;
|
|
10637
|
+
context.drawImage(image, 0, 0);
|
|
10638
|
+
return output;
|
|
10639
|
+
} catch (error) {
|
|
10640
|
+
if (output) releaseSurface(output);
|
|
10641
|
+
if (signal?.aborted) throwIfAborted(signal);
|
|
10642
|
+
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10643
|
+
} finally {
|
|
10644
|
+
image.removeAttribute("src");
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
/**
|
|
10648
|
+
* Apply a referenced SVG filter to the complete surface. The viewBox is in CSS
|
|
10649
|
+
* pixels, so the filter's user units scale with the capture, and the image is
|
|
10650
|
+
* placed so user space starts at the element's border box, as it does for the
|
|
10651
|
+
* live element. Canvas filters cannot express either: their lengths are in
|
|
10652
|
+
* device pixels and their bounding box is the whole canvas.
|
|
10653
|
+
*/
|
|
10654
|
+
async function renderReferenceSurface(source, reference, origin, scale, signal) {
|
|
10655
|
+
throwIfAborted(signal);
|
|
10656
|
+
const owner = source.ownerDocument ?? document;
|
|
10657
|
+
const width = source.width / scale;
|
|
10658
|
+
const height = source.height / scale;
|
|
10659
|
+
const svg = createSvgElement(owner, "svg", {
|
|
10660
|
+
width: source.width,
|
|
10661
|
+
height: source.height,
|
|
10662
|
+
viewBox: `0 0 ${width} ${height}`
|
|
10663
|
+
});
|
|
10664
|
+
const defs = createSvgElement(owner, "defs");
|
|
10665
|
+
const filterElement = owner.importNode(reference.element, true);
|
|
10666
|
+
filterElement.setAttribute("id", "composited-reference");
|
|
10667
|
+
defs.appendChild(filterElement);
|
|
10668
|
+
svg.appendChild(defs);
|
|
10669
|
+
const group = createSvgElement(owner, "g", { transform: `translate(${origin.x} ${origin.y})` });
|
|
10670
|
+
group.appendChild(createSvgElement(owner, "image", {
|
|
10671
|
+
x: -origin.x,
|
|
10672
|
+
y: -origin.y,
|
|
10673
|
+
width,
|
|
10674
|
+
height,
|
|
10675
|
+
href: serializeSurface(source),
|
|
10676
|
+
filter: "url(#composited-reference)"
|
|
10677
|
+
}));
|
|
10678
|
+
svg.appendChild(group);
|
|
10679
|
+
return drawSvgSurface(svg, source, 1, signal);
|
|
10680
|
+
}
|
|
10376
10681
|
async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
|
|
10377
10682
|
throwIfAborted(signal);
|
|
10378
10683
|
const blur = filter.functions.find((fn) => fn.kind === "blur");
|
|
@@ -10384,29 +10689,9 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
|
|
|
10384
10689
|
blur: shadow.blur,
|
|
10385
10690
|
color: shadow.color
|
|
10386
10691
|
} : void 0;
|
|
10387
|
-
if (!blurValue && !shadowValue)
|
|
10388
|
-
const output = canvasLike(source);
|
|
10389
|
-
const context = output.getContext("2d");
|
|
10390
|
-
if (!context) {
|
|
10391
|
-
releaseSurface(output);
|
|
10392
|
-
throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10393
|
-
}
|
|
10394
|
-
context.globalAlpha = opacity;
|
|
10395
|
-
try {
|
|
10396
|
-
context.drawImage(source, 0, 0);
|
|
10397
|
-
return output;
|
|
10398
|
-
} catch {
|
|
10399
|
-
releaseSurface(output);
|
|
10400
|
-
throw new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10401
|
-
}
|
|
10402
|
-
}
|
|
10403
|
-
const namespace = "http://www.w3.org/2000/svg";
|
|
10692
|
+
if (!blurValue && !shadowValue) return drawWithOpacity(source, opacity);
|
|
10404
10693
|
const svgDocument = source.ownerDocument ?? document;
|
|
10405
|
-
const element = (tag, attributes = {}) =>
|
|
10406
|
-
const node = svgDocument.createElementNS(namespace, tag);
|
|
10407
|
-
Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
|
|
10408
|
-
return node;
|
|
10409
|
-
};
|
|
10694
|
+
const element = (tag, attributes = {}) => createSvgElement(svgDocument, tag, attributes);
|
|
10410
10695
|
const svg = element("svg", {
|
|
10411
10696
|
width: source.width,
|
|
10412
10697
|
height: source.height,
|
|
@@ -10448,39 +10733,13 @@ async function renderSvgFilterSurface(source, filter, opacity, scale, signal) {
|
|
|
10448
10733
|
}
|
|
10449
10734
|
defs.appendChild(filterElement);
|
|
10450
10735
|
svg.appendChild(defs);
|
|
10451
|
-
let sourceUrl;
|
|
10452
|
-
try {
|
|
10453
|
-
sourceUrl = source.toDataURL("image/png");
|
|
10454
|
-
if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
|
|
10455
|
-
} catch {
|
|
10456
|
-
throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
|
|
10457
|
-
}
|
|
10458
10736
|
svg.appendChild(element("image", {
|
|
10459
10737
|
width: source.width,
|
|
10460
10738
|
height: source.height,
|
|
10461
|
-
href:
|
|
10739
|
+
href: serializeSurface(source),
|
|
10462
10740
|
filter: "url(#composited-filter)"
|
|
10463
10741
|
}));
|
|
10464
|
-
|
|
10465
|
-
const image = imageWindow?.Image ? new imageWindow.Image() : new Image();
|
|
10466
|
-
let output;
|
|
10467
|
-
try {
|
|
10468
|
-
image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
|
|
10469
|
-
await decodeSurface(image, signal);
|
|
10470
|
-
throwIfAborted(signal);
|
|
10471
|
-
output = canvasLike(source);
|
|
10472
|
-
const context = output.getContext("2d");
|
|
10473
|
-
if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
|
|
10474
|
-
context.globalAlpha = opacity;
|
|
10475
|
-
context.drawImage(image, 0, 0);
|
|
10476
|
-
return output;
|
|
10477
|
-
} catch (error) {
|
|
10478
|
-
if (output) releaseSurface(output);
|
|
10479
|
-
if (signal?.aborted) throwIfAborted(signal);
|
|
10480
|
-
throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
|
|
10481
|
-
} finally {
|
|
10482
|
-
image.removeAttribute("src");
|
|
10483
|
-
}
|
|
10742
|
+
return drawSvgSurface(svg, source, opacity, signal);
|
|
10484
10743
|
}
|
|
10485
10744
|
function reserveSurface(budget, width, height) {
|
|
10486
10745
|
const pixels = width * height * 4;
|
|
@@ -10504,7 +10763,7 @@ function surfaceBounds(container) {
|
|
|
10504
10763
|
}
|
|
10505
10764
|
for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
|
|
10506
10765
|
const filter = parseFilterChain(styles.filter);
|
|
10507
|
-
return expand(bounds, filter ? filterOutset(filter) : 0);
|
|
10766
|
+
return expand(bounds, filter ? filterOutset(filter, container.filterReferences) : 0);
|
|
10508
10767
|
}
|
|
10509
10768
|
function cropSurface(bounds, viewport, margin, scale) {
|
|
10510
10769
|
const area = expand(viewport, margin);
|
|
@@ -12141,6 +12400,7 @@ const paintRadialLayer = (ctx, layer, area) => {
|
|
|
12141
12400
|
ctx.fillStyle = rasterized.lastColor;
|
|
12142
12401
|
ctx.fillRect(area.left, area.top, area.width, area.height);
|
|
12143
12402
|
ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
|
|
12403
|
+
releaseSurface(rasterized.canvas);
|
|
12144
12404
|
};
|
|
12145
12405
|
/** Draw one gradient layer filling the whole mask area. */
|
|
12146
12406
|
const paintGradientLayer = (ctx, layer, area) => {
|
|
@@ -12995,7 +13255,8 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12995
13255
|
async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
|
|
12996
13256
|
const signal = this.options.signal;
|
|
12997
13257
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
12998
|
-
const
|
|
13258
|
+
const container = stack.element.container;
|
|
13259
|
+
const bounds = cropSurface(surfaceBounds(container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filter ? filterOutset(filter, container.filterReferences) : 0, this.options.scale);
|
|
12999
13260
|
if (!bounds.width || !bounds.height) return true;
|
|
13000
13261
|
const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
|
|
13001
13262
|
if (!reserved) {
|
|
@@ -13017,7 +13278,13 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13017
13278
|
source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
|
|
13018
13279
|
await source.renderStackContent(stack);
|
|
13019
13280
|
source.effectsRenderer.applyEffects([]);
|
|
13020
|
-
if (filter) filtered = await renderFilterSurface(source.canvas, filter,
|
|
13281
|
+
if (filter) filtered = await renderFilterSurface(source.canvas, filter, container.styles.opacity, options.scale, signal, {
|
|
13282
|
+
references: container.filterReferences,
|
|
13283
|
+
origin: {
|
|
13284
|
+
x: container.bounds.left - bounds.left,
|
|
13285
|
+
y: container.bounds.top - bounds.top
|
|
13286
|
+
}
|
|
13287
|
+
});
|
|
13021
13288
|
const composited = filtered ?? source.canvas;
|
|
13022
13289
|
if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
|
|
13023
13290
|
if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
@@ -13054,7 +13321,10 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13054
13321
|
maskCanvas.width = surface.width;
|
|
13055
13322
|
maskCanvas.height = surface.height;
|
|
13056
13323
|
const maskCtx = maskCanvas.getContext("2d");
|
|
13057
|
-
if (!maskCtx)
|
|
13324
|
+
if (!maskCtx) {
|
|
13325
|
+
releaseSurface(maskCanvas);
|
|
13326
|
+
return;
|
|
13327
|
+
}
|
|
13058
13328
|
maskCtx.scale(this.options.scale, this.options.scale);
|
|
13059
13329
|
maskCtx.translate(-bounds.left, -bounds.top);
|
|
13060
13330
|
try {
|
|
@@ -13066,6 +13336,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13066
13336
|
}, async (url) => await this.context.cache.match(url));
|
|
13067
13337
|
} catch (error) {
|
|
13068
13338
|
this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
|
|
13339
|
+
releaseSurface(maskCanvas);
|
|
13069
13340
|
return;
|
|
13070
13341
|
}
|
|
13071
13342
|
ctx.save();
|
|
@@ -13075,6 +13346,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13075
13346
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
13076
13347
|
} finally {
|
|
13077
13348
|
ctx.restore();
|
|
13349
|
+
releaseSurface(maskCanvas);
|
|
13078
13350
|
}
|
|
13079
13351
|
}
|
|
13080
13352
|
/**
|
|
@@ -13479,6 +13751,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13479
13751
|
ctx.fillStyle = rasterized.lastColor;
|
|
13480
13752
|
ctx.fillRect(0, 0, width, height);
|
|
13481
13753
|
ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
|
|
13754
|
+
releaseSurface(rasterized.canvas);
|
|
13482
13755
|
return canvas;
|
|
13483
13756
|
}
|
|
13484
13757
|
if (source.type === 5 || source.type === 6) {
|