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.js
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
|
*/
|
|
@@ -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}`);
|
|
@@ -6528,6 +6556,15 @@
|
|
|
6528
6556
|
*/
|
|
6529
6557
|
const CLONE_READY_TIMEOUT_MS = 3e4;
|
|
6530
6558
|
/**
|
|
6559
|
+
* Total time (ms) to wait for the clone's external stylesheets (link and
|
|
6560
|
+
* @import chains) to apply. Shorter than the document readiness window: a
|
|
6561
|
+
* stylesheet that needs longer usually means a hung CDN, and rendering with
|
|
6562
|
+
* a 10s-old layout beats blocking the capture for 30s. A missed deadline
|
|
6563
|
+
* resolves with a warning (the capture continues with a possibly unstyled
|
|
6564
|
+
* clone).
|
|
6565
|
+
*/
|
|
6566
|
+
const STYLESHEET_READY_TIMEOUT_MS = 1e4;
|
|
6567
|
+
/**
|
|
6531
6568
|
* Hard ceiling (ms) for the cloned iframe's load event itself. If the event
|
|
6532
6569
|
* never fires, the mount is rejected instead of hanging forever.
|
|
6533
6570
|
*/
|
|
@@ -8041,7 +8078,49 @@
|
|
|
8041
8078
|
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
|
8042
8079
|
};
|
|
8043
8080
|
/**
|
|
8044
|
-
*
|
|
8081
|
+
* Whether every CSS rule of the sheet (and of the @import chains it pulls
|
|
8082
|
+
* in) has been applied. A sheet is "attached" as soon as its rules are
|
|
8083
|
+
* readable; an external @import inside it may still be loading, which shows
|
|
8084
|
+
* up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
|
|
8085
|
+
* cross-origin sheets without a CORS-approved response — those rules cannot
|
|
8086
|
+
* be inspected, so the sheet counts as ready rather than blocking the render
|
|
8087
|
+
* forever.
|
|
8088
|
+
*/
|
|
8089
|
+
const isSheetFullyApplied = (sheet, depth = 0) => {
|
|
8090
|
+
if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
|
|
8091
|
+
let rules;
|
|
8092
|
+
try {
|
|
8093
|
+
rules = sheet.cssRules;
|
|
8094
|
+
} catch (_e) {
|
|
8095
|
+
return true;
|
|
8096
|
+
}
|
|
8097
|
+
if (!rules) return true;
|
|
8098
|
+
for (let i = 0; i < rules.length; i++) {
|
|
8099
|
+
const rule = rules[i];
|
|
8100
|
+
if (!rule || !("styleSheet" in rule)) continue;
|
|
8101
|
+
const inner = rule.styleSheet;
|
|
8102
|
+
if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
|
|
8103
|
+
}
|
|
8104
|
+
return true;
|
|
8105
|
+
};
|
|
8106
|
+
const MAX_IMPORT_DEPTH = 32;
|
|
8107
|
+
/** Collect every <link> in the document, including links inside shadow roots. */
|
|
8108
|
+
const collectLinks = (root, out) => {
|
|
8109
|
+
const links = root.querySelectorAll("link");
|
|
8110
|
+
for (let i = 0; i < links.length; i++) {
|
|
8111
|
+
const link = links[i];
|
|
8112
|
+
if (link) out.push(link);
|
|
8113
|
+
}
|
|
8114
|
+
const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
8115
|
+
let current = walker.nextNode();
|
|
8116
|
+
while (current) {
|
|
8117
|
+
const shadowRoot = current.shadowRoot;
|
|
8118
|
+
if (shadowRoot) collectLinks(shadowRoot, out);
|
|
8119
|
+
current = walker.nextNode();
|
|
8120
|
+
}
|
|
8121
|
+
};
|
|
8122
|
+
/**
|
|
8123
|
+
* Wait until every external stylesheet of the clone has been applied or
|
|
8045
8124
|
* definitively failed, or the readiness budget expires.
|
|
8046
8125
|
*
|
|
8047
8126
|
* The clone is mounted by adoptNode() into an already-closed document
|
|
@@ -8054,35 +8133,68 @@
|
|
|
8054
8133
|
* an unstyled layout (issue #245). Inline <style> clones apply
|
|
8055
8134
|
* synchronously and document-level adopted stylesheets are serialised at
|
|
8056
8135
|
* clone time, so neither needs this wait.
|
|
8136
|
+
*
|
|
8137
|
+
* Readiness is polled rather than purely event-driven because a <style>
|
|
8138
|
+
* clone serialised from `createStyleClone` re-fetches its external @import
|
|
8139
|
+
* rules without firing link load/error events, and shadow-root links are
|
|
8140
|
+
* equally event-hostile in some engines. The poll covers both; link events
|
|
8141
|
+
* remain as the settle path for plain <link> fetches.
|
|
8057
8142
|
* @internal – exported for testing only.
|
|
8058
8143
|
*/
|
|
8059
|
-
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8144
|
+
const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
|
|
8060
8145
|
if (signal?.aborted) return Promise.resolve();
|
|
8061
8146
|
const pending = [];
|
|
8062
|
-
const links =
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
if (
|
|
8147
|
+
const links = [];
|
|
8148
|
+
collectLinks(documentClone, links);
|
|
8149
|
+
for (const link of links) {
|
|
8150
|
+
if (link.sheet) continue;
|
|
8066
8151
|
const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
|
|
8067
8152
|
if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
|
|
8068
8153
|
pending.push(new Promise((resolve) => {
|
|
8069
|
-
|
|
8154
|
+
const href = link.href;
|
|
8070
8155
|
const settle = () => {
|
|
8071
8156
|
link.removeEventListener("load", settle);
|
|
8072
8157
|
link.removeEventListener("error", settle);
|
|
8073
8158
|
signal?.removeEventListener("abort", onAbort);
|
|
8074
|
-
|
|
8159
|
+
clearInterval(timer);
|
|
8075
8160
|
resolve();
|
|
8076
8161
|
};
|
|
8077
8162
|
const onAbort = () => settle();
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
}
|
|
8163
|
+
if (link.sheet) {
|
|
8164
|
+
resolve();
|
|
8165
|
+
return;
|
|
8166
|
+
}
|
|
8082
8167
|
link.addEventListener("load", settle);
|
|
8083
8168
|
link.addEventListener("error", settle);
|
|
8084
8169
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8085
|
-
|
|
8170
|
+
let elapsed = 0;
|
|
8171
|
+
const timer = setInterval(() => {
|
|
8172
|
+
elapsed += 50;
|
|
8173
|
+
if (link.sheet) settle();
|
|
8174
|
+
else if (elapsed >= timeoutMs) {
|
|
8175
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8176
|
+
settle();
|
|
8177
|
+
}
|
|
8178
|
+
}, 50);
|
|
8179
|
+
}));
|
|
8180
|
+
}
|
|
8181
|
+
const styleElements = documentClone.getElementsByTagName("style");
|
|
8182
|
+
for (let i = 0; i < styleElements.length; i++) {
|
|
8183
|
+
const style = styleElements[i];
|
|
8184
|
+
const sheet = style?.sheet;
|
|
8185
|
+
if (!sheet || !style?.textContent?.includes("@import")) continue;
|
|
8186
|
+
if (isSheetFullyApplied(sheet)) continue;
|
|
8187
|
+
pending.push(new Promise((resolve) => {
|
|
8188
|
+
let elapsed = 0;
|
|
8189
|
+
const timer = setInterval(() => {
|
|
8190
|
+
elapsed += 50;
|
|
8191
|
+
const currentSheet = style.sheet;
|
|
8192
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
|
|
8193
|
+
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`);
|
|
8194
|
+
clearInterval(timer);
|
|
8195
|
+
resolve();
|
|
8196
|
+
}
|
|
8197
|
+
}, 50);
|
|
8086
8198
|
}));
|
|
8087
8199
|
}
|
|
8088
8200
|
if (pending.length === 0) return Promise.resolve();
|
|
@@ -8240,7 +8352,10 @@
|
|
|
8240
8352
|
addBase(adoptedNode, baseUri);
|
|
8241
8353
|
documentClone.replaceChild(adoptedNode, documentClone.documentElement);
|
|
8242
8354
|
const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
|
|
8243
|
-
return iframeLoad
|
|
8355
|
+
return iframeLoad.catch((error) => {
|
|
8356
|
+
iframe.parentNode?.removeChild(iframe);
|
|
8357
|
+
throw error;
|
|
8358
|
+
});
|
|
8244
8359
|
};
|
|
8245
8360
|
//#endregion
|
|
8246
8361
|
//#region src/dom/document-cloner.ts
|
|
@@ -9763,7 +9878,7 @@
|
|
|
9763
9878
|
if (treatAsRealStackingContext || createsStackingContext) {
|
|
9764
9879
|
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
|
9765
9880
|
const stack = new StackingContext(paintContainer);
|
|
9766
|
-
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9881
|
+
if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9767
9882
|
const order = child.styles.zIndex.order;
|
|
9768
9883
|
if (order < 0) {
|
|
9769
9884
|
let index = 0;
|
|
@@ -12061,6 +12176,7 @@
|
|
|
12061
12176
|
ctx.fillStyle = rasterized.lastColor;
|
|
12062
12177
|
ctx.fillRect(area.left, area.top, area.width, area.height);
|
|
12063
12178
|
ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
|
|
12179
|
+
releaseSurface(rasterized.canvas);
|
|
12064
12180
|
};
|
|
12065
12181
|
/** Draw one gradient layer filling the whole mask area. */
|
|
12066
12182
|
const paintGradientLayer = (ctx, layer, area) => {
|
|
@@ -12974,7 +13090,10 @@
|
|
|
12974
13090
|
maskCanvas.width = surface.width;
|
|
12975
13091
|
maskCanvas.height = surface.height;
|
|
12976
13092
|
const maskCtx = maskCanvas.getContext("2d");
|
|
12977
|
-
if (!maskCtx)
|
|
13093
|
+
if (!maskCtx) {
|
|
13094
|
+
releaseSurface(maskCanvas);
|
|
13095
|
+
return;
|
|
13096
|
+
}
|
|
12978
13097
|
maskCtx.scale(this.options.scale, this.options.scale);
|
|
12979
13098
|
maskCtx.translate(-bounds.left, -bounds.top);
|
|
12980
13099
|
try {
|
|
@@ -12986,6 +13105,7 @@
|
|
|
12986
13105
|
}, async (url) => await this.context.cache.match(url));
|
|
12987
13106
|
} catch (error) {
|
|
12988
13107
|
this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
|
|
13108
|
+
releaseSurface(maskCanvas);
|
|
12989
13109
|
return;
|
|
12990
13110
|
}
|
|
12991
13111
|
ctx.save();
|
|
@@ -12995,6 +13115,7 @@
|
|
|
12995
13115
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
12996
13116
|
} finally {
|
|
12997
13117
|
ctx.restore();
|
|
13118
|
+
releaseSurface(maskCanvas);
|
|
12998
13119
|
}
|
|
12999
13120
|
}
|
|
13000
13121
|
/**
|
|
@@ -13399,6 +13520,7 @@
|
|
|
13399
13520
|
ctx.fillStyle = rasterized.lastColor;
|
|
13400
13521
|
ctx.fillRect(0, 0, width, height);
|
|
13401
13522
|
ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
|
|
13523
|
+
releaseSurface(rasterized.canvas);
|
|
13402
13524
|
return canvas;
|
|
13403
13525
|
}
|
|
13404
13526
|
if (source.type === 5 || source.type === 6) {
|
|
@@ -13548,14 +13670,19 @@
|
|
|
13548
13670
|
const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
|
|
13549
13671
|
const imagePromise = loadSerializedSVG(svg);
|
|
13550
13672
|
restoreRootOffsets();
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13673
|
+
try {
|
|
13674
|
+
const img = await imagePromise;
|
|
13675
|
+
if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
13676
|
+
if (this.options.backgroundColor) {
|
|
13677
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
13678
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13679
|
+
}
|
|
13680
|
+
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13681
|
+
return this.canvas;
|
|
13682
|
+
} catch (error) {
|
|
13683
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
13684
|
+
throw error;
|
|
13556
13685
|
}
|
|
13557
|
-
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13558
|
-
return this.canvas;
|
|
13559
13686
|
}
|
|
13560
13687
|
};
|
|
13561
13688
|
/**
|
|
@@ -14111,19 +14238,17 @@
|
|
|
14111
14238
|
const documentCloner = new DocumentCloner(context, element, cloneOptions);
|
|
14112
14239
|
const clonedElement = documentCloner.clonedReferenceElement;
|
|
14113
14240
|
if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
|
|
14114
|
-
|
|
14115
|
-
perfMonitor.end("clone");
|
|
14116
|
-
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14117
|
-
if (signal?.aborted) {
|
|
14118
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14119
|
-
throwIfAborted(signal);
|
|
14120
|
-
}
|
|
14121
|
-
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14122
|
-
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14123
|
-
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14241
|
+
let container;
|
|
14124
14242
|
let canvas;
|
|
14125
14243
|
let root;
|
|
14126
14244
|
try {
|
|
14245
|
+
container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
|
14246
|
+
perfMonitor.end("clone");
|
|
14247
|
+
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14248
|
+
throwIfAborted(signal);
|
|
14249
|
+
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14250
|
+
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14251
|
+
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14127
14252
|
if (foreignObjectRendering) {
|
|
14128
14253
|
context.logger.debug(`Document cloned, using foreign object rendering`);
|
|
14129
14254
|
perfMonitor.start("render-foreignobject");
|
|
@@ -14157,7 +14282,7 @@
|
|
|
14157
14282
|
try {
|
|
14158
14283
|
if (root) root.restoreTree();
|
|
14159
14284
|
} finally {
|
|
14160
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14285
|
+
if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
|
|
14161
14286
|
perfMonitor.end("cleanup");
|
|
14162
14287
|
}
|
|
14163
14288
|
}
|