html2canvas-pro 2.5.3 → 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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 2.5.3 <https://yorickshan.github.io/html2canvas-pro/>
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
  */
@@ -8041,7 +8050,49 @@
8041
8050
  return Promise.all([].slice.call(document.images, 0).map(imageReady));
8042
8051
  };
8043
8052
  /**
8044
- * Wait until every external stylesheet link of the clone has been applied or
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
8045
8096
  * definitively failed, or the readiness budget expires.
8046
8097
  *
8047
8098
  * The clone is mounted by adoptNode() into an already-closed document
@@ -8054,35 +8105,68 @@
8054
8105
  * an unstyled layout (issue #245). Inline <style> clones apply
8055
8106
  * synchronously and document-level adopted stylesheets are serialised at
8056
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.
8057
8114
  * @internal – exported for testing only.
8058
8115
  */
8059
8116
  const stylesheetsReady = (documentClone, logger, signal) => {
8060
8117
  if (signal?.aborted) return Promise.resolve();
8061
8118
  const pending = [];
8062
- const links = documentClone.getElementsByTagName("link");
8063
- for (let i = 0; i < links.length; i++) {
8064
- const link = links[i];
8065
- if (!link || link.sheet) continue;
8119
+ const links = [];
8120
+ collectLinks(documentClone, links);
8121
+ for (const link of links) {
8122
+ if (link.sheet) continue;
8066
8123
  const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
8067
8124
  if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
8068
8125
  pending.push(new Promise((resolve) => {
8069
- let timer;
8126
+ const href = link.href;
8070
8127
  const settle = () => {
8071
8128
  link.removeEventListener("load", settle);
8072
8129
  link.removeEventListener("error", settle);
8073
8130
  signal?.removeEventListener("abort", onAbort);
8074
- clearTimeout(timer);
8131
+ clearInterval(timer);
8075
8132
  resolve();
8076
8133
  };
8077
8134
  const onAbort = () => settle();
8078
- timer = setTimeout(() => {
8079
- logger?.warn(`Stylesheet ${link.href} did not finish loading within ${CLONE_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8080
- settle();
8081
- }, CLONE_READY_TIMEOUT_MS);
8135
+ if (link.sheet) {
8136
+ resolve();
8137
+ return;
8138
+ }
8082
8139
  link.addEventListener("load", settle);
8083
8140
  link.addEventListener("error", settle);
8084
8141
  signal?.addEventListener("abort", onAbort, { once: true });
8085
- if (link.sheet) settle();
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);
8086
8170
  }));
8087
8171
  }
8088
8172
  if (pending.length === 0) return Promise.resolve();
@@ -8240,7 +8324,10 @@
8240
8324
  addBase(adoptedNode, baseUri);
8241
8325
  documentClone.replaceChild(adoptedNode, documentClone.documentElement);
8242
8326
  const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
8243
- return iframeLoad;
8327
+ return iframeLoad.catch((error) => {
8328
+ iframe.parentNode?.removeChild(iframe);
8329
+ throw error;
8330
+ });
8244
8331
  };
8245
8332
  //#endregion
8246
8333
  //#region src/dom/document-cloner.ts
@@ -13548,14 +13635,19 @@
13548
13635
  const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
13549
13636
  const imagePromise = loadSerializedSVG(svg);
13550
13637
  restoreRootOffsets();
13551
- const img = await imagePromise;
13552
- if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13553
- if (this.options.backgroundColor) {
13554
- this.ctx.fillStyle = asString(this.options.backgroundColor);
13555
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
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;
13556
13650
  }
13557
- this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
13558
- return this.canvas;
13559
13651
  }
13560
13652
  };
13561
13653
  /**
@@ -14111,19 +14203,17 @@
14111
14203
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
14112
14204
  const clonedElement = documentCloner.clonedReferenceElement;
14113
14205
  if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
14114
- const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
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);
14206
+ let container;
14124
14207
  let canvas;
14125
14208
  let root;
14126
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);
14127
14217
  if (foreignObjectRendering) {
14128
14218
  context.logger.debug(`Document cloned, using foreign object rendering`);
14129
14219
  perfMonitor.start("render-foreignobject");
@@ -14157,7 +14247,7 @@
14157
14247
  try {
14158
14248
  if (root) root.restoreTree();
14159
14249
  } finally {
14160
- if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
14250
+ if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
14161
14251
  perfMonitor.end("cleanup");
14162
14252
  }
14163
14253
  }