html2canvas-pro 2.5.3 → 2.6.0
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/dist/html2canvas-pro.cjs +160 -35
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +160 -35
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +160 -35
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +6 -6
- package/dist/types/core/constants.d.ts +11 -0
- package/dist/types/core/validator.d.ts +9 -1
- package/dist/types/dom/iframe-mount.d.ts +8 -2
- package/package.json +5 -4
package/dist/html2canvas-pro.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.
|
|
2
|
+
* html2canvas-pro 2.6.0 <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
|
*/
|
|
@@ -246,6 +246,30 @@ var Validator = class {
|
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
/**
|
|
249
|
+
* Validate the backgroundColor option
|
|
250
|
+
*
|
|
251
|
+
* @param backgroundColor - CSS color string or null (transparent)
|
|
252
|
+
* @returns Validation result
|
|
253
|
+
*/
|
|
254
|
+
validateBackgroundColor(backgroundColor) {
|
|
255
|
+
if (backgroundColor === null) return {
|
|
256
|
+
valid: true,
|
|
257
|
+
sanitized: null
|
|
258
|
+
};
|
|
259
|
+
if (typeof backgroundColor !== "string" || backgroundColor.length === 0) return {
|
|
260
|
+
valid: false,
|
|
261
|
+
error: "backgroundColor must be a CSS color string or null"
|
|
262
|
+
};
|
|
263
|
+
if (/[^a-zA-Z0-9%(),.#\s/-]/.test(backgroundColor)) return {
|
|
264
|
+
valid: false,
|
|
265
|
+
error: `backgroundColor contains characters that cannot appear in a CSS color: ${backgroundColor}`
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
valid: true,
|
|
269
|
+
sanitized: backgroundColor
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
249
273
|
* Validate image timeout
|
|
250
274
|
*
|
|
251
275
|
* @param timeout - Timeout in milliseconds
|
|
@@ -390,6 +414,10 @@ var Validator = class {
|
|
|
390
414
|
const nonceResult = this.validateCspNonce(options.cspNonce);
|
|
391
415
|
if (!nonceResult.valid) errors.push(`CSP nonce: ${nonceResult.error}`);
|
|
392
416
|
}
|
|
417
|
+
if (options.backgroundColor !== void 0) {
|
|
418
|
+
const colorResult = this.validateBackgroundColor(options.backgroundColor);
|
|
419
|
+
if (!colorResult.valid) errors.push(`backgroundColor: ${colorResult.error}`);
|
|
420
|
+
}
|
|
393
421
|
if (this.config.customValidator) {
|
|
394
422
|
const customResult = this.config.customValidator(options, "options");
|
|
395
423
|
if (!customResult.valid) errors.push(`Custom validation: ${customResult.error}`);
|
|
@@ -6525,6 +6553,15 @@ const SHADOW_MASK_OFFSET = 1e4;
|
|
|
6525
6553
|
*/
|
|
6526
6554
|
const CLONE_READY_TIMEOUT_MS = 3e4;
|
|
6527
6555
|
/**
|
|
6556
|
+
* Total time (ms) to wait for the clone's external stylesheets (link and
|
|
6557
|
+
* @import chains) to apply. Shorter than the document readiness window: a
|
|
6558
|
+
* stylesheet that needs longer usually means a hung CDN, and rendering with
|
|
6559
|
+
* a 10s-old layout beats blocking the capture for 30s. A missed deadline
|
|
6560
|
+
* resolves with a warning (the capture continues with a possibly unstyled
|
|
6561
|
+
* clone).
|
|
6562
|
+
*/
|
|
6563
|
+
const STYLESHEET_READY_TIMEOUT_MS = 1e4;
|
|
6564
|
+
/**
|
|
6528
6565
|
* Hard ceiling (ms) for the cloned iframe's load event itself. If the event
|
|
6529
6566
|
* never fires, the mount is rejected instead of hanging forever.
|
|
6530
6567
|
*/
|
|
@@ -8038,7 +8075,49 @@ const imagesReady = (document) => {
|
|
|
8038
8075
|
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
|
8039
8076
|
};
|
|
8040
8077
|
/**
|
|
8041
|
-
*
|
|
8078
|
+
* Whether every CSS rule of the sheet (and of the @import chains it pulls
|
|
8079
|
+
* in) has been applied. A sheet is "attached" as soon as its rules are
|
|
8080
|
+
* readable; an external @import inside it may still be loading, which shows
|
|
8081
|
+
* up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
|
|
8082
|
+
* cross-origin sheets without a CORS-approved response — those rules cannot
|
|
8083
|
+
* be inspected, so the sheet counts as ready rather than blocking the render
|
|
8084
|
+
* forever.
|
|
8085
|
+
*/
|
|
8086
|
+
const isSheetFullyApplied = (sheet, depth = 0) => {
|
|
8087
|
+
if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
|
|
8088
|
+
let rules;
|
|
8089
|
+
try {
|
|
8090
|
+
rules = sheet.cssRules;
|
|
8091
|
+
} catch (_e) {
|
|
8092
|
+
return true;
|
|
8093
|
+
}
|
|
8094
|
+
if (!rules) return true;
|
|
8095
|
+
for (let i = 0; i < rules.length; i++) {
|
|
8096
|
+
const rule = rules[i];
|
|
8097
|
+
if (!rule || !("styleSheet" in rule)) continue;
|
|
8098
|
+
const inner = rule.styleSheet;
|
|
8099
|
+
if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
|
|
8100
|
+
}
|
|
8101
|
+
return true;
|
|
8102
|
+
};
|
|
8103
|
+
const MAX_IMPORT_DEPTH = 32;
|
|
8104
|
+
/** Collect every <link> in the document, including links inside shadow roots. */
|
|
8105
|
+
const collectLinks = (root, out) => {
|
|
8106
|
+
const links = root.querySelectorAll("link");
|
|
8107
|
+
for (let i = 0; i < links.length; i++) {
|
|
8108
|
+
const link = links[i];
|
|
8109
|
+
if (link) out.push(link);
|
|
8110
|
+
}
|
|
8111
|
+
const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
8112
|
+
let current = walker.nextNode();
|
|
8113
|
+
while (current) {
|
|
8114
|
+
const shadowRoot = current.shadowRoot;
|
|
8115
|
+
if (shadowRoot) collectLinks(shadowRoot, out);
|
|
8116
|
+
current = walker.nextNode();
|
|
8117
|
+
}
|
|
8118
|
+
};
|
|
8119
|
+
/**
|
|
8120
|
+
* Wait until every external stylesheet of the clone has been applied or
|
|
8042
8121
|
* definitively failed, or the readiness budget expires.
|
|
8043
8122
|
*
|
|
8044
8123
|
* The clone is mounted by adoptNode() into an already-closed document
|
|
@@ -8051,35 +8130,68 @@ const imagesReady = (document) => {
|
|
|
8051
8130
|
* an unstyled layout (issue #245). Inline <style> clones apply
|
|
8052
8131
|
* synchronously and document-level adopted stylesheets are serialised at
|
|
8053
8132
|
* clone time, so neither needs this wait.
|
|
8133
|
+
*
|
|
8134
|
+
* Readiness is polled rather than purely event-driven because a <style>
|
|
8135
|
+
* clone serialised from `createStyleClone` re-fetches its external @import
|
|
8136
|
+
* rules without firing link load/error events, and shadow-root links are
|
|
8137
|
+
* equally event-hostile in some engines. The poll covers both; link events
|
|
8138
|
+
* remain as the settle path for plain <link> fetches.
|
|
8054
8139
|
* @internal – exported for testing only.
|
|
8055
8140
|
*/
|
|
8056
|
-
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8141
|
+
const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
|
|
8057
8142
|
if (signal?.aborted) return Promise.resolve();
|
|
8058
8143
|
const pending = [];
|
|
8059
|
-
const links =
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
if (
|
|
8144
|
+
const links = [];
|
|
8145
|
+
collectLinks(documentClone, links);
|
|
8146
|
+
for (const link of links) {
|
|
8147
|
+
if (link.sheet) continue;
|
|
8063
8148
|
const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
|
|
8064
8149
|
if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
|
|
8065
8150
|
pending.push(new Promise((resolve) => {
|
|
8066
|
-
|
|
8151
|
+
const href = link.href;
|
|
8067
8152
|
const settle = () => {
|
|
8068
8153
|
link.removeEventListener("load", settle);
|
|
8069
8154
|
link.removeEventListener("error", settle);
|
|
8070
8155
|
signal?.removeEventListener("abort", onAbort);
|
|
8071
|
-
|
|
8156
|
+
clearInterval(timer);
|
|
8072
8157
|
resolve();
|
|
8073
8158
|
};
|
|
8074
8159
|
const onAbort = () => settle();
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
}
|
|
8160
|
+
if (link.sheet) {
|
|
8161
|
+
resolve();
|
|
8162
|
+
return;
|
|
8163
|
+
}
|
|
8079
8164
|
link.addEventListener("load", settle);
|
|
8080
8165
|
link.addEventListener("error", settle);
|
|
8081
8166
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8082
|
-
|
|
8167
|
+
let elapsed = 0;
|
|
8168
|
+
const timer = setInterval(() => {
|
|
8169
|
+
elapsed += 50;
|
|
8170
|
+
if (link.sheet) settle();
|
|
8171
|
+
else if (elapsed >= timeoutMs) {
|
|
8172
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8173
|
+
settle();
|
|
8174
|
+
}
|
|
8175
|
+
}, 50);
|
|
8176
|
+
}));
|
|
8177
|
+
}
|
|
8178
|
+
const styleElements = documentClone.getElementsByTagName("style");
|
|
8179
|
+
for (let i = 0; i < styleElements.length; i++) {
|
|
8180
|
+
const style = styleElements[i];
|
|
8181
|
+
const sheet = style?.sheet;
|
|
8182
|
+
if (!sheet || !style?.textContent?.includes("@import")) continue;
|
|
8183
|
+
if (isSheetFullyApplied(sheet)) continue;
|
|
8184
|
+
pending.push(new Promise((resolve) => {
|
|
8185
|
+
let elapsed = 0;
|
|
8186
|
+
const timer = setInterval(() => {
|
|
8187
|
+
elapsed += 50;
|
|
8188
|
+
const currentSheet = style.sheet;
|
|
8189
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
|
|
8190
|
+
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`);
|
|
8191
|
+
clearInterval(timer);
|
|
8192
|
+
resolve();
|
|
8193
|
+
}
|
|
8194
|
+
}, 50);
|
|
8083
8195
|
}));
|
|
8084
8196
|
}
|
|
8085
8197
|
if (pending.length === 0) return Promise.resolve();
|
|
@@ -8237,7 +8349,10 @@ const mountCloneInIFrame = (context, ownerDocument, windowSize, mount) => {
|
|
|
8237
8349
|
addBase(adoptedNode, baseUri);
|
|
8238
8350
|
documentClone.replaceChild(adoptedNode, documentClone.documentElement);
|
|
8239
8351
|
const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
|
|
8240
|
-
return iframeLoad
|
|
8352
|
+
return iframeLoad.catch((error) => {
|
|
8353
|
+
iframe.parentNode?.removeChild(iframe);
|
|
8354
|
+
throw error;
|
|
8355
|
+
});
|
|
8241
8356
|
};
|
|
8242
8357
|
//#endregion
|
|
8243
8358
|
//#region src/dom/document-cloner.ts
|
|
@@ -9760,7 +9875,7 @@ const parseStackTree = (parent, stackingContext, realStackingContext, listItems)
|
|
|
9760
9875
|
if (treatAsRealStackingContext || createsStackingContext) {
|
|
9761
9876
|
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
|
9762
9877
|
const stack = new StackingContext(paintContainer);
|
|
9763
|
-
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9878
|
+
if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9764
9879
|
const order = child.styles.zIndex.order;
|
|
9765
9880
|
if (order < 0) {
|
|
9766
9881
|
let index = 0;
|
|
@@ -12058,6 +12173,7 @@ const paintRadialLayer = (ctx, layer, area) => {
|
|
|
12058
12173
|
ctx.fillStyle = rasterized.lastColor;
|
|
12059
12174
|
ctx.fillRect(area.left, area.top, area.width, area.height);
|
|
12060
12175
|
ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
|
|
12176
|
+
releaseSurface(rasterized.canvas);
|
|
12061
12177
|
};
|
|
12062
12178
|
/** Draw one gradient layer filling the whole mask area. */
|
|
12063
12179
|
const paintGradientLayer = (ctx, layer, area) => {
|
|
@@ -12971,7 +13087,10 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12971
13087
|
maskCanvas.width = surface.width;
|
|
12972
13088
|
maskCanvas.height = surface.height;
|
|
12973
13089
|
const maskCtx = maskCanvas.getContext("2d");
|
|
12974
|
-
if (!maskCtx)
|
|
13090
|
+
if (!maskCtx) {
|
|
13091
|
+
releaseSurface(maskCanvas);
|
|
13092
|
+
return;
|
|
13093
|
+
}
|
|
12975
13094
|
maskCtx.scale(this.options.scale, this.options.scale);
|
|
12976
13095
|
maskCtx.translate(-bounds.left, -bounds.top);
|
|
12977
13096
|
try {
|
|
@@ -12983,6 +13102,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12983
13102
|
}, async (url) => await this.context.cache.match(url));
|
|
12984
13103
|
} catch (error) {
|
|
12985
13104
|
this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
|
|
13105
|
+
releaseSurface(maskCanvas);
|
|
12986
13106
|
return;
|
|
12987
13107
|
}
|
|
12988
13108
|
ctx.save();
|
|
@@ -12992,6 +13112,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12992
13112
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
12993
13113
|
} finally {
|
|
12994
13114
|
ctx.restore();
|
|
13115
|
+
releaseSurface(maskCanvas);
|
|
12995
13116
|
}
|
|
12996
13117
|
}
|
|
12997
13118
|
/**
|
|
@@ -13396,6 +13517,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13396
13517
|
ctx.fillStyle = rasterized.lastColor;
|
|
13397
13518
|
ctx.fillRect(0, 0, width, height);
|
|
13398
13519
|
ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
|
|
13520
|
+
releaseSurface(rasterized.canvas);
|
|
13399
13521
|
return canvas;
|
|
13400
13522
|
}
|
|
13401
13523
|
if (source.type === 5 || source.type === 6) {
|
|
@@ -13545,14 +13667,19 @@ var ForeignObjectRenderer = class {
|
|
|
13545
13667
|
const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
|
|
13546
13668
|
const imagePromise = loadSerializedSVG(svg);
|
|
13547
13669
|
restoreRootOffsets();
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13670
|
+
try {
|
|
13671
|
+
const img = await imagePromise;
|
|
13672
|
+
if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
13673
|
+
if (this.options.backgroundColor) {
|
|
13674
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
13675
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13676
|
+
}
|
|
13677
|
+
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13678
|
+
return this.canvas;
|
|
13679
|
+
} catch (error) {
|
|
13680
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
13681
|
+
throw error;
|
|
13553
13682
|
}
|
|
13554
|
-
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13555
|
-
return this.canvas;
|
|
13556
13683
|
}
|
|
13557
13684
|
};
|
|
13558
13685
|
/**
|
|
@@ -14108,19 +14235,17 @@ const renderElement = async (element, opts, config) => {
|
|
|
14108
14235
|
const documentCloner = new DocumentCloner(context, element, cloneOptions);
|
|
14109
14236
|
const clonedElement = documentCloner.clonedReferenceElement;
|
|
14110
14237
|
if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
|
|
14111
|
-
|
|
14112
|
-
perfMonitor.end("clone");
|
|
14113
|
-
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14114
|
-
if (signal?.aborted) {
|
|
14115
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14116
|
-
throwIfAborted(signal);
|
|
14117
|
-
}
|
|
14118
|
-
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14119
|
-
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14120
|
-
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14238
|
+
let container;
|
|
14121
14239
|
let canvas;
|
|
14122
14240
|
let root;
|
|
14123
14241
|
try {
|
|
14242
|
+
container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
|
14243
|
+
perfMonitor.end("clone");
|
|
14244
|
+
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14245
|
+
throwIfAborted(signal);
|
|
14246
|
+
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14247
|
+
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14248
|
+
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14124
14249
|
if (foreignObjectRendering) {
|
|
14125
14250
|
context.logger.debug(`Document cloned, using foreign object rendering`);
|
|
14126
14251
|
perfMonitor.start("render-foreignobject");
|
|
@@ -14154,7 +14279,7 @@ const renderElement = async (element, opts, config) => {
|
|
|
14154
14279
|
try {
|
|
14155
14280
|
if (root) root.restoreTree();
|
|
14156
14281
|
} finally {
|
|
14157
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14282
|
+
if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
|
|
14158
14283
|
perfMonitor.end("cleanup");
|
|
14159
14284
|
}
|
|
14160
14285
|
}
|