html2canvas-pro 2.5.2 → 2.5.4
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 +2 -2
- package/dist/html2canvas-pro.cjs +175 -25
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +175 -25
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +175 -25
- 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/dom/iframe-mount.d.ts +25 -0
- package/package.json +18 -19
package/dist/html2canvas-pro.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.5.
|
|
2
|
+
* html2canvas-pro 2.5.4 <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
|
*/
|
|
@@ -6528,6 +6528,15 @@
|
|
|
6528
6528
|
*/
|
|
6529
6529
|
const CLONE_READY_TIMEOUT_MS = 3e4;
|
|
6530
6530
|
/**
|
|
6531
|
+
* Total time (ms) to wait for the clone's external stylesheets (link and
|
|
6532
|
+
* @import chains) to apply. Shorter than the document readiness window: a
|
|
6533
|
+
* stylesheet that needs longer usually means a hung CDN, and rendering with
|
|
6534
|
+
* a 10s-old layout beats blocking the capture for 30s. A missed deadline
|
|
6535
|
+
* resolves with a warning (the capture continues with a possibly unstyled
|
|
6536
|
+
* clone).
|
|
6537
|
+
*/
|
|
6538
|
+
const STYLESHEET_READY_TIMEOUT_MS = 1e4;
|
|
6539
|
+
/**
|
|
6531
6540
|
* Hard ceiling (ms) for the cloned iframe's load event itself. If the event
|
|
6532
6541
|
* never fires, the mount is rejected instead of hanging forever.
|
|
6533
6542
|
*/
|
|
@@ -8040,6 +8049,129 @@
|
|
|
8040
8049
|
const imagesReady = (document) => {
|
|
8041
8050
|
return Promise.all([].slice.call(document.images, 0).map(imageReady));
|
|
8042
8051
|
};
|
|
8052
|
+
/**
|
|
8053
|
+
* Whether every CSS rule of the sheet (and of the @import chains it pulls
|
|
8054
|
+
* in) has been applied. A sheet is "attached" as soon as its rules are
|
|
8055
|
+
* readable; an external @import inside it may still be loading, which shows
|
|
8056
|
+
* up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
|
|
8057
|
+
* cross-origin sheets without a CORS-approved response — those rules cannot
|
|
8058
|
+
* be inspected, so the sheet counts as ready rather than blocking the render
|
|
8059
|
+
* forever.
|
|
8060
|
+
*/
|
|
8061
|
+
const isSheetFullyApplied = (sheet, depth = 0) => {
|
|
8062
|
+
if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
|
|
8063
|
+
let rules;
|
|
8064
|
+
try {
|
|
8065
|
+
rules = sheet.cssRules;
|
|
8066
|
+
} catch (_e) {
|
|
8067
|
+
return true;
|
|
8068
|
+
}
|
|
8069
|
+
if (!rules) return true;
|
|
8070
|
+
for (let i = 0; i < rules.length; i++) {
|
|
8071
|
+
const rule = rules[i];
|
|
8072
|
+
if (rule && typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
|
|
8073
|
+
if (!rule.styleSheet || !isSheetFullyApplied(rule.styleSheet, depth + 1)) return false;
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
return true;
|
|
8077
|
+
};
|
|
8078
|
+
const MAX_IMPORT_DEPTH = 32;
|
|
8079
|
+
/** Collect every <link> in the document, including links inside shadow roots. */
|
|
8080
|
+
const collectLinks = (root, out) => {
|
|
8081
|
+
const links = root.querySelectorAll("link");
|
|
8082
|
+
for (let i = 0; i < links.length; i++) {
|
|
8083
|
+
const link = links[i];
|
|
8084
|
+
if (link) out.push(link);
|
|
8085
|
+
}
|
|
8086
|
+
const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
8087
|
+
let current = walker.nextNode();
|
|
8088
|
+
while (current) {
|
|
8089
|
+
const shadowRoot = current.shadowRoot;
|
|
8090
|
+
if (shadowRoot) collectLinks(shadowRoot, out);
|
|
8091
|
+
current = walker.nextNode();
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
8094
|
+
/**
|
|
8095
|
+
* Wait until every external stylesheet of the clone has been applied or
|
|
8096
|
+
* definitively failed, or the readiness budget expires.
|
|
8097
|
+
*
|
|
8098
|
+
* The clone is mounted by adoptNode() into an already-closed document
|
|
8099
|
+
* (close() must precede adoptNode for Chrome uppercase class-name matching,
|
|
8100
|
+
* see the mount sequence below), so the cloned <link rel="stylesheet">
|
|
8101
|
+
* elements are inserted after the document finished loading: they neither
|
|
8102
|
+
* delay the iframe load event nor change readyState, and the iframeLoader
|
|
8103
|
+
* readiness poll cannot observe them. On a cold cache the first capture
|
|
8104
|
+
* could then measure the clone before its external CSS arrived and render
|
|
8105
|
+
* an unstyled layout (issue #245). Inline <style> clones apply
|
|
8106
|
+
* synchronously and document-level adopted stylesheets are serialised at
|
|
8107
|
+
* clone time, so neither needs this wait.
|
|
8108
|
+
*
|
|
8109
|
+
* Readiness is polled rather than purely event-driven because a <style>
|
|
8110
|
+
* clone serialised from `createStyleClone` re-fetches its external @import
|
|
8111
|
+
* rules without firing link load/error events, and shadow-root links are
|
|
8112
|
+
* equally event-hostile in some engines. The poll covers both; link events
|
|
8113
|
+
* remain as the settle path for plain <link> fetches.
|
|
8114
|
+
* @internal – exported for testing only.
|
|
8115
|
+
*/
|
|
8116
|
+
const stylesheetsReady = (documentClone, logger, signal) => {
|
|
8117
|
+
if (signal?.aborted) return Promise.resolve();
|
|
8118
|
+
const pending = [];
|
|
8119
|
+
const links = [];
|
|
8120
|
+
collectLinks(documentClone, links);
|
|
8121
|
+
for (const link of links) {
|
|
8122
|
+
if (link.sheet) continue;
|
|
8123
|
+
const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
|
|
8124
|
+
if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
|
|
8125
|
+
pending.push(new Promise((resolve) => {
|
|
8126
|
+
const href = link.href;
|
|
8127
|
+
const settle = () => {
|
|
8128
|
+
link.removeEventListener("load", settle);
|
|
8129
|
+
link.removeEventListener("error", settle);
|
|
8130
|
+
signal?.removeEventListener("abort", onAbort);
|
|
8131
|
+
clearInterval(timer);
|
|
8132
|
+
resolve();
|
|
8133
|
+
};
|
|
8134
|
+
const onAbort = () => settle();
|
|
8135
|
+
if (link.sheet) {
|
|
8136
|
+
resolve();
|
|
8137
|
+
return;
|
|
8138
|
+
}
|
|
8139
|
+
link.addEventListener("load", settle);
|
|
8140
|
+
link.addEventListener("error", settle);
|
|
8141
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
8142
|
+
let elapsed = 0;
|
|
8143
|
+
const timer = setInterval(() => {
|
|
8144
|
+
elapsed += 50;
|
|
8145
|
+
if (link.sheet) settle();
|
|
8146
|
+
else if (elapsed >= 1e4) {
|
|
8147
|
+
logger?.warn(`Stylesheet ${href} did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8148
|
+
settle();
|
|
8149
|
+
}
|
|
8150
|
+
}, 50);
|
|
8151
|
+
}));
|
|
8152
|
+
}
|
|
8153
|
+
const styleElements = documentClone.getElementsByTagName("style");
|
|
8154
|
+
for (let i = 0; i < styleElements.length; i++) {
|
|
8155
|
+
const style = styleElements[i];
|
|
8156
|
+
const sheet = style?.sheet;
|
|
8157
|
+
if (!sheet || !style?.textContent?.includes("@import")) continue;
|
|
8158
|
+
if (isSheetFullyApplied(sheet)) continue;
|
|
8159
|
+
pending.push(new Promise((resolve) => {
|
|
8160
|
+
let elapsed = 0;
|
|
8161
|
+
const timer = setInterval(() => {
|
|
8162
|
+
elapsed += 50;
|
|
8163
|
+
const currentSheet = style.sheet;
|
|
8164
|
+
if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= 1e4) {
|
|
8165
|
+
if (elapsed >= 1e4 && !isSheetFullyApplied(currentSheet ?? null)) logger?.warn(`@import chain in an inline <style> did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
|
|
8166
|
+
clearInterval(timer);
|
|
8167
|
+
resolve();
|
|
8168
|
+
}
|
|
8169
|
+
}, 50);
|
|
8170
|
+
}));
|
|
8171
|
+
}
|
|
8172
|
+
if (pending.length === 0) return Promise.resolve();
|
|
8173
|
+
return Promise.all(pending).then(() => void 0);
|
|
8174
|
+
};
|
|
8043
8175
|
const iframeLoader = (iframe, logger, signal) => {
|
|
8044
8176
|
return new Promise((resolve, reject) => {
|
|
8045
8177
|
const cloneWindow = iframe.contentWindow;
|
|
@@ -8082,6 +8214,15 @@
|
|
|
8082
8214
|
});
|
|
8083
8215
|
};
|
|
8084
8216
|
/**
|
|
8217
|
+
* Reduce a doctype component to characters that are inert inside a
|
|
8218
|
+
* <!DOCTYPE ...> declaration. Doctype content is parsed as a declaration
|
|
8219
|
+
* token, never as HTML markup, but the fields originate from the captured
|
|
8220
|
+
* document — stripping markup delimiters and control characters here makes
|
|
8221
|
+
* that property locally verifiable (and keeps the sanitizer guarantee even
|
|
8222
|
+
* if a future serializer ever embeds the result elsewhere).
|
|
8223
|
+
*/
|
|
8224
|
+
const sanitizeDoctypePart = (value) => value.replace(/[<>&"'`\\]|[\u0000-\u001f\u007f]/g, "");
|
|
8225
|
+
/**
|
|
8085
8226
|
* Serialise a document type declaration to an HTML string.
|
|
8086
8227
|
* @internal – exported for testing only, not part of the public API.
|
|
8087
8228
|
*/
|
|
@@ -8089,12 +8230,12 @@
|
|
|
8089
8230
|
let str = "";
|
|
8090
8231
|
if (doctype) {
|
|
8091
8232
|
str += "<!DOCTYPE ";
|
|
8092
|
-
if (doctype.name) str += doctype.name;
|
|
8093
|
-
if (doctype.internalSubset) str += " " + doctype.internalSubset
|
|
8233
|
+
if (doctype.name) str += sanitizeDoctypePart(doctype.name);
|
|
8234
|
+
if (doctype.internalSubset) str += " " + sanitizeDoctypePart(doctype.internalSubset);
|
|
8094
8235
|
if (doctype.publicId) {
|
|
8095
|
-
str += " PUBLIC \"" + doctype.publicId
|
|
8096
|
-
if (doctype.systemId) str += " \"" + doctype.systemId
|
|
8097
|
-
} else if (doctype.systemId) str += " SYSTEM \"" + doctype.systemId
|
|
8236
|
+
str += " PUBLIC \"" + sanitizeDoctypePart(doctype.publicId) + "\"";
|
|
8237
|
+
if (doctype.systemId) str += " \"" + sanitizeDoctypePart(doctype.systemId) + "\"";
|
|
8238
|
+
} else if (doctype.systemId) str += " SYSTEM \"" + sanitizeDoctypePart(doctype.systemId) + "\"";
|
|
8098
8239
|
str += ">";
|
|
8099
8240
|
}
|
|
8100
8241
|
return str;
|
|
@@ -8130,6 +8271,8 @@
|
|
|
8130
8271
|
const referenceElement = mount.clonedReferenceElement;
|
|
8131
8272
|
if (typeof referenceElement === "undefined") throw new Error(`Error finding the ${mount.referenceElementName} in the cloned document`);
|
|
8132
8273
|
throwIfAborted(mount.signal);
|
|
8274
|
+
await stylesheetWait;
|
|
8275
|
+
throwIfAborted(mount.signal);
|
|
8133
8276
|
if (documentClone.fonts && documentClone.fonts.ready) await documentClone.fonts.ready;
|
|
8134
8277
|
throwIfAborted(mount.signal);
|
|
8135
8278
|
if (/(AppleWebKit)/g.test(navigator.userAgent)) {
|
|
@@ -8180,7 +8323,11 @@
|
|
|
8180
8323
|
const adoptedNode = documentClone.adoptNode(mount.documentElement);
|
|
8181
8324
|
addBase(adoptedNode, baseUri);
|
|
8182
8325
|
documentClone.replaceChild(adoptedNode, documentClone.documentElement);
|
|
8183
|
-
|
|
8326
|
+
const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
|
|
8327
|
+
return iframeLoad.catch((error) => {
|
|
8328
|
+
iframe.parentNode?.removeChild(iframe);
|
|
8329
|
+
throw error;
|
|
8330
|
+
});
|
|
8184
8331
|
};
|
|
8185
8332
|
//#endregion
|
|
8186
8333
|
//#region src/dom/document-cloner.ts
|
|
@@ -13488,14 +13635,19 @@
|
|
|
13488
13635
|
const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
|
|
13489
13636
|
const imagePromise = loadSerializedSVG(svg);
|
|
13490
13637
|
restoreRootOffsets();
|
|
13491
|
-
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13638
|
+
try {
|
|
13639
|
+
const img = await imagePromise;
|
|
13640
|
+
if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
|
|
13641
|
+
if (this.options.backgroundColor) {
|
|
13642
|
+
this.ctx.fillStyle = asString(this.options.backgroundColor);
|
|
13643
|
+
this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13644
|
+
}
|
|
13645
|
+
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13646
|
+
return this.canvas;
|
|
13647
|
+
} catch (error) {
|
|
13648
|
+
if (!this.options.canvas) releaseSurface(this.canvas);
|
|
13649
|
+
throw error;
|
|
13496
13650
|
}
|
|
13497
|
-
this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
|
|
13498
|
-
return this.canvas;
|
|
13499
13651
|
}
|
|
13500
13652
|
};
|
|
13501
13653
|
/**
|
|
@@ -14051,19 +14203,17 @@
|
|
|
14051
14203
|
const documentCloner = new DocumentCloner(context, element, cloneOptions);
|
|
14052
14204
|
const clonedElement = documentCloner.clonedReferenceElement;
|
|
14053
14205
|
if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
|
|
14054
|
-
|
|
14055
|
-
perfMonitor.end("clone");
|
|
14056
|
-
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14057
|
-
if (signal?.aborted) {
|
|
14058
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14059
|
-
throwIfAborted(signal);
|
|
14060
|
-
}
|
|
14061
|
-
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14062
|
-
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14063
|
-
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14206
|
+
let container;
|
|
14064
14207
|
let canvas;
|
|
14065
14208
|
let root;
|
|
14066
14209
|
try {
|
|
14210
|
+
container = await documentCloner.toIFrame(ownerDocument, windowBounds);
|
|
14211
|
+
perfMonitor.end("clone");
|
|
14212
|
+
emitProgress(opts.onProgress, context, "clone", 30);
|
|
14213
|
+
throwIfAborted(signal);
|
|
14214
|
+
const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
|
|
14215
|
+
const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
|
|
14216
|
+
const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
|
|
14067
14217
|
if (foreignObjectRendering) {
|
|
14068
14218
|
context.logger.debug(`Document cloned, using foreign object rendering`);
|
|
14069
14219
|
perfMonitor.start("render-foreignobject");
|
|
@@ -14097,7 +14247,7 @@
|
|
|
14097
14247
|
try {
|
|
14098
14248
|
if (root) root.restoreTree();
|
|
14099
14249
|
} finally {
|
|
14100
|
-
if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
|
|
14250
|
+
if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
|
|
14101
14251
|
perfMonitor.end("cleanup");
|
|
14102
14252
|
}
|
|
14103
14253
|
}
|