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
|
@@ -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
|
*/
|
|
@@ -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}`);
|
|
@@ -6521,6 +6549,15 @@ const SHADOW_MASK_OFFSET = 1e4;
|
|
|
6521
6549
|
*/
|
|
6522
6550
|
const CLONE_READY_TIMEOUT_MS = 3e4;
|
|
6523
6551
|
/**
|
|
6552
|
+
* Total time (ms) to wait for the clone's external stylesheets (link and
|
|
6553
|
+
* @import chains) to apply. Shorter than the document readiness window: a
|
|
6554
|
+
* stylesheet that needs longer usually means a hung CDN, and rendering with
|
|
6555
|
+
* a 10s-old layout beats blocking the capture for 30s. A missed deadline
|
|
6556
|
+
* resolves with a warning (the capture continues with a possibly unstyled
|
|
6557
|
+
* clone).
|
|
6558
|
+
*/
|
|
6559
|
+
const STYLESHEET_READY_TIMEOUT_MS = 1e4;
|
|
6560
|
+
/**
|
|
6524
6561
|
* Hard ceiling (ms) for the cloned iframe's load event itself. If the event
|
|
6525
6562
|
* never fires, the mount is rejected instead of hanging forever.
|
|
6526
6563
|
*/
|
|
@@ -8034,7 +8071,49 @@ const imagesReady = (document) => {
|
|
|
8034
8071
|
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
|
8035
8072
|
};
|
|
8036
8073
|
/**
|
|
8037
|
-
*
|
|
8074
|
+
* Whether every CSS rule of the sheet (and of the @import chains it pulls
|
|
8075
|
+
* in) has been applied. A sheet is "attached" as soon as its rules are
|
|
8076
|
+
* readable; an external @import inside it may still be loading, which shows
|
|
8077
|
+
* up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
|
|
8078
|
+
* cross-origin sheets without a CORS-approved response — those rules cannot
|
|
8079
|
+
* be inspected, so the sheet counts as ready rather than blocking the render
|
|
8080
|
+
* forever.
|
|
8081
|
+
*/
|
|
8082
|
+
const isSheetFullyApplied = (sheet, depth = 0) => {
|
|
8083
|
+
if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
|
|
8084
|
+
let rules;
|
|
8085
|
+
try {
|
|
8086
|
+
rules = sheet.cssRules;
|
|
8087
|
+
} catch (_e) {
|
|
8088
|
+
return true;
|
|
8089
|
+
}
|
|
8090
|
+
if (!rules) return true;
|
|
8091
|
+
for (let i = 0; i < rules.length; i++) {
|
|
8092
|
+
const rule = rules[i];
|
|
8093
|
+
if (!rule || !("styleSheet" in rule)) continue;
|
|
8094
|
+
const inner = rule.styleSheet;
|
|
8095
|
+
if (!inner || !isSheetFullyApplied(inner, depth + 1)) return false;
|
|
8096
|
+
}
|
|
8097
|
+
return true;
|
|
8098
|
+
};
|
|
8099
|
+
const MAX_IMPORT_DEPTH = 32;
|
|
8100
|
+
/** Collect every <link> in the document, including links inside shadow roots. */
|
|
8101
|
+
const collectLinks = (root, out) => {
|
|
8102
|
+
const links = root.querySelectorAll("link");
|
|
8103
|
+
for (let i = 0; i < links.length; i++) {
|
|
8104
|
+
const link = links[i];
|
|
8105
|
+
if (link) out.push(link);
|
|
8106
|
+
}
|
|
8107
|
+
const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
8108
|
+
let current = walker.nextNode();
|
|
8109
|
+
while (current) {
|
|
8110
|
+
const shadowRoot = current.shadowRoot;
|
|
8111
|
+
if (shadowRoot) collectLinks(shadowRoot, out);
|
|
8112
|
+
current = walker.nextNode();
|
|
8113
|
+
}
|
|
8114
|
+
};
|
|
8115
|
+
/**
|
|
8116
|
+
* Wait until every external stylesheet of the clone has been applied or
|
|
8038
8117
|
* definitively failed, or the readiness budget expires.
|
|
8039
8118
|
*
|
|
8040
8119
|
* The clone is mounted by adoptNode() into an already-closed document
|
|
@@ -8047,35 +8126,68 @@ const imagesReady = (document) => {
|
|
|
8047
8126
|
* an unstyled layout (issue #245). Inline <style> clones apply
|
|
8048
8127
|
* synchronously and document-level adopted stylesheets are serialised at
|
|
8049
8128
|
* clone time, so neither needs this wait.
|
|
8129
|
+
*
|
|
8130
|
+
* Readiness is polled rather than purely event-driven because a <style>
|
|
8131
|
+
* clone serialised from `createStyleClone` re-fetches its external @import
|
|
8132
|
+
* rules without firing link load/error events, and shadow-root links are
|
|
8133
|
+
* equally event-hostile in some engines. The poll covers both; link events
|
|
8134
|
+
* remain as the settle path for plain <link> fetches.
|
|
8050
8135
|
* @internal – exported for testing only.
|
|
8051
8136
|
*/
|
|
8052
|
-
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8137
|
+
const stylesheetsReady = (documentClone, logger, signal, timeoutMs = STYLESHEET_READY_TIMEOUT_MS) => {
|
|
8053
8138
|
if (signal?.aborted) return Promise.resolve();
|
|
8054
8139
|
const pending = [];
|
|
8055
|
-
const links =
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
if (
|
|
8140
|
+
const links = [];
|
|
8141
|
+
collectLinks(documentClone, links);
|
|
8142
|
+
for (const link of links) {
|
|
8143
|
+
if (link.sheet) continue;
|
|
8059
8144
|
const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
|
|
8060
8145
|
if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
|
|
8061
8146
|
pending.push(new Promise((resolve) => {
|
|
8062
|
-
|
|
8147
|
+
const href = link.href;
|
|
8063
8148
|
const settle = () => {
|
|
8064
8149
|
link.removeEventListener("load", settle);
|
|
8065
8150
|
link.removeEventListener("error", settle);
|
|
8066
8151
|
signal?.removeEventListener("abort", onAbort);
|
|
8067
|
-
|
|
8152
|
+
clearInterval(timer);
|
|
8068
8153
|
resolve();
|
|
8069
8154
|
};
|
|
8070
8155
|
const onAbort = () => settle();
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
}
|
|
8156
|
+
if (link.sheet) {
|
|
8157
|
+
resolve();
|
|
8158
|
+
return;
|
|
8159
|
+
}
|
|
8075
8160
|
link.addEventListener("load", settle);
|
|
8076
8161
|
link.addEventListener("error", settle);
|
|
8077
8162
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8078
|
-
|
|
8163
|
+
let elapsed = 0;
|
|
8164
|
+
const timer = setInterval(() => {
|
|
8165
|
+
elapsed += 50;
|
|
8166
|
+
if (link.sheet) settle();
|
|
8167
|
+
else if (elapsed >= timeoutMs) {
|
|
8168
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${timeoutMs / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8169
|
+
settle();
|
|
8170
|
+
}
|
|
8171
|
+
}, 50);
|
|
8172
|
+
}));
|
|
8173
|
+
}
|
|
8174
|
+
const styleElements = documentClone.getElementsByTagName("style");
|
|
8175
|
+
for (let i = 0; i < styleElements.length; i++) {
|
|
8176
|
+
const style = styleElements[i];
|
|
8177
|
+
const sheet = style?.sheet;
|
|
8178
|
+
if (!sheet || !style?.textContent?.includes("@import")) continue;
|
|
8179
|
+
if (isSheetFullyApplied(sheet)) continue;
|
|
8180
|
+
pending.push(new Promise((resolve) => {
|
|
8181
|
+
let elapsed = 0;
|
|
8182
|
+
const timer = setInterval(() => {
|
|
8183
|
+
elapsed += 50;
|
|
8184
|
+
const currentSheet = style.sheet;
|
|
8185
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= timeoutMs) {
|
|
8186
|
+
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`);
|
|
8187
|
+
clearInterval(timer);
|
|
8188
|
+
resolve();
|
|
8189
|
+
}
|
|
8190
|
+
}, 50);
|
|
8079
8191
|
}));
|
|
8080
8192
|
}
|
|
8081
8193
|
if (pending.length === 0) return Promise.resolve();
|
|
@@ -8233,7 +8345,10 @@ const mountCloneInIFrame = (context, ownerDocument, windowSize, mount) => {
|
|
|
8233
8345
|
addBase(adoptedNode, baseUri);
|
|
8234
8346
|
documentClone.replaceChild(adoptedNode, documentClone.documentElement);
|
|
8235
8347
|
const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
|
|
8236
|
-
return iframeLoad
|
|
8348
|
+
return iframeLoad.catch((error) => {
|
|
8349
|
+
iframe.parentNode?.removeChild(iframe);
|
|
8350
|
+
throw error;
|
|
8351
|
+
});
|
|
8237
8352
|
};
|
|
8238
8353
|
//#endregion
|
|
8239
8354
|
//#region src/dom/document-cloner.ts
|
|
@@ -9756,7 +9871,7 @@ const parseStackTree = (parent, stackingContext, realStackingContext, listItems)
|
|
|
9756
9871
|
if (treatAsRealStackingContext || createsStackingContext) {
|
|
9757
9872
|
const parentStack = treatAsRealStackingContext || child.styles.isPositioned() ? realStackingContext : stackingContext;
|
|
9758
9873
|
const stack = new StackingContext(paintContainer);
|
|
9759
|
-
if (child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9874
|
+
if (treatAsRealStackingContext || child.styles.isPositioned() || child.styles.opacity < 1 || child.styles.isTransformed()) {
|
|
9760
9875
|
const order = child.styles.zIndex.order;
|
|
9761
9876
|
if (order < 0) {
|
|
9762
9877
|
let index = 0;
|
|
@@ -12054,6 +12169,7 @@ const paintRadialLayer = (ctx, layer, area) => {
|
|
|
12054
12169
|
ctx.fillStyle = rasterized.lastColor;
|
|
12055
12170
|
ctx.fillRect(area.left, area.top, area.width, area.height);
|
|
12056
12171
|
ctx.drawImage(rasterized.canvas, area.left + cx - rx, area.top + cy - ry, rx * 2, ry * 2);
|
|
12172
|
+
releaseSurface(rasterized.canvas);
|
|
12057
12173
|
};
|
|
12058
12174
|
/** Draw one gradient layer filling the whole mask area. */
|
|
12059
12175
|
const paintGradientLayer = (ctx, layer, area) => {
|
|
@@ -12967,7 +13083,10 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12967
13083
|
maskCanvas.width = surface.width;
|
|
12968
13084
|
maskCanvas.height = surface.height;
|
|
12969
13085
|
const maskCtx = maskCanvas.getContext("2d");
|
|
12970
|
-
if (!maskCtx)
|
|
13086
|
+
if (!maskCtx) {
|
|
13087
|
+
releaseSurface(maskCanvas);
|
|
13088
|
+
return;
|
|
13089
|
+
}
|
|
12971
13090
|
maskCtx.scale(this.options.scale, this.options.scale);
|
|
12972
13091
|
maskCtx.translate(-bounds.left, -bounds.top);
|
|
12973
13092
|
try {
|
|
@@ -12979,6 +13098,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12979
13098
|
}, async (url) => await this.context.cache.match(url));
|
|
12980
13099
|
} catch (error) {
|
|
12981
13100
|
this.context.logger.error(`Error rendering mask-image: ${error instanceof Error ? error.message : error}`);
|
|
13101
|
+
releaseSurface(maskCanvas);
|
|
12982
13102
|
return;
|
|
12983
13103
|
}
|
|
12984
13104
|
ctx.save();
|
|
@@ -12988,6 +13108,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
12988
13108
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
12989
13109
|
} finally {
|
|
12990
13110
|
ctx.restore();
|
|
13111
|
+
releaseSurface(maskCanvas);
|
|
12991
13112
|
}
|
|
12992
13113
|
}
|
|
12993
13114
|
/**
|
|
@@ -13392,6 +13513,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
13392
13513
|
ctx.fillStyle = rasterized.lastColor;
|
|
13393
13514
|
ctx.fillRect(0, 0, width, height);
|
|
13394
13515
|
ctx.drawImage(rasterized.canvas, cx - rx, cy - ry, rx * 2, ry * 2);
|
|
13516
|
+
releaseSurface(rasterized.canvas);
|
|
13395
13517
|
return canvas;
|
|
13396
13518
|
}
|
|
13397
13519
|
if (source.type === 5 || source.type === 6) {
|
|
@@ -13541,14 +13663,19 @@ var ForeignObjectRenderer = class {
|
|
|
13541
13663
|
const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
|
|
13542
13664
|
const imagePromise = loadSerializedSVG(svg);
|
|
13543
13665
|
restoreRootOffsets();
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13666
|
+
try {
|
|
13667
|
+
const img = await imagePromise;
|
|
13668
|
+
if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
13669
|
+
if (this.options.backgroundColor) {
|
|
13670
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
13671
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13672
|
+
}
|
|
13673
|
+
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13674
|
+
return this.canvas;
|
|
13675
|
+
} catch (error) {
|
|
13676
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
13677
|
+
throw error;
|
|
13549
13678
|
}
|
|
13550
|
-
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13551
|
-
return this.canvas;
|
|
13552
13679
|
}
|
|
13553
13680
|
};
|
|
13554
13681
|
/**
|
|
@@ -14104,19 +14231,17 @@ const renderElement = async (element, opts, config) => {
|
|
|
14104
14231
|
const documentCloner = new DocumentCloner(context, element, cloneOptions);
|
|
14105
14232
|
const clonedElement = documentCloner.clonedReferenceElement;
|
|
14106
14233
|
if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
|
|
14107
|
-
|
|
14108
|
-
perfMonitor.end("clone");
|
|
14109
|
-
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14110
|
-
if (signal?.aborted) {
|
|
14111
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14112
|
-
throwIfAborted(signal);
|
|
14113
|
-
}
|
|
14114
|
-
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14115
|
-
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14116
|
-
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14234
|
+
let container;
|
|
14117
14235
|
let canvas;
|
|
14118
14236
|
let root;
|
|
14119
14237
|
try {
|
|
14238
|
+
container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
|
14239
|
+
perfMonitor.end("clone");
|
|
14240
|
+
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14241
|
+
throwIfAborted(signal);
|
|
14242
|
+
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14243
|
+
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14244
|
+
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14120
14245
|
if (foreignObjectRendering) {
|
|
14121
14246
|
context.logger.debug(`Document cloned, using foreign object rendering`);
|
|
14122
14247
|
perfMonitor.start("render-foreignobject");
|
|
@@ -14150,7 +14275,7 @@ const renderElement = async (element, opts, config) => {
|
|
|
14150
14275
|
try {
|
|
14151
14276
|
if (root) root.restoreTree();
|
|
14152
14277
|
} finally {
|
|
14153
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14278
|
+
if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
|
|
14154
14279
|
perfMonitor.end("cleanup");
|
|
14155
14280
|
}
|
|
14156
14281
|
}
|