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
  */
@@ -6521,6 +6521,15 @@ const SHADOW_MASK_OFFSET = 1e4;
6521
6521
  */
6522
6522
  const CLONE_READY_TIMEOUT_MS = 3e4;
6523
6523
  /**
6524
+ * Total time (ms) to wait for the clone's external stylesheets (link and
6525
+ * @import chains) to apply. Shorter than the document readiness window: a
6526
+ * stylesheet that needs longer usually means a hung CDN, and rendering with
6527
+ * a 10s-old layout beats blocking the capture for 30s. A missed deadline
6528
+ * resolves with a warning (the capture continues with a possibly unstyled
6529
+ * clone).
6530
+ */
6531
+ const STYLESHEET_READY_TIMEOUT_MS = 1e4;
6532
+ /**
6524
6533
  * Hard ceiling (ms) for the cloned iframe's load event itself. If the event
6525
6534
  * never fires, the mount is rejected instead of hanging forever.
6526
6535
  */
@@ -8034,7 +8043,49 @@ const imagesReady = (document) => {
8034
8043
  return Promise.all([].slice.call(document.images, 0).map(imageReady));
8035
8044
  };
8036
8045
  /**
8037
- * Wait until every external stylesheet link of the clone has been applied or
8046
+ * Whether every CSS rule of the sheet (and of the @import chains it pulls
8047
+ * in) has been applied. A sheet is "attached" as soon as its rules are
8048
+ * readable; an external @import inside it may still be loading, which shows
8049
+ * up as an inaccessible CSSImportRule.sheet. `cssRules` can throw for
8050
+ * cross-origin sheets without a CORS-approved response — those rules cannot
8051
+ * be inspected, so the sheet counts as ready rather than blocking the render
8052
+ * forever.
8053
+ */
8054
+ const isSheetFullyApplied = (sheet, depth = 0) => {
8055
+ if (!sheet || depth > MAX_IMPORT_DEPTH) return true;
8056
+ let rules;
8057
+ try {
8058
+ rules = sheet.cssRules;
8059
+ } catch (_e) {
8060
+ return true;
8061
+ }
8062
+ if (!rules) return true;
8063
+ for (let i = 0; i < rules.length; i++) {
8064
+ const rule = rules[i];
8065
+ if (rule && typeof CSSImportRule !== "undefined" && rule instanceof CSSImportRule) {
8066
+ if (!rule.styleSheet || !isSheetFullyApplied(rule.styleSheet, depth + 1)) return false;
8067
+ }
8068
+ }
8069
+ return true;
8070
+ };
8071
+ const MAX_IMPORT_DEPTH = 32;
8072
+ /** Collect every <link> in the document, including links inside shadow roots. */
8073
+ const collectLinks = (root, out) => {
8074
+ const links = root.querySelectorAll("link");
8075
+ for (let i = 0; i < links.length; i++) {
8076
+ const link = links[i];
8077
+ if (link) out.push(link);
8078
+ }
8079
+ const walker = (root.ownerDocument ?? root).createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
8080
+ let current = walker.nextNode();
8081
+ while (current) {
8082
+ const shadowRoot = current.shadowRoot;
8083
+ if (shadowRoot) collectLinks(shadowRoot, out);
8084
+ current = walker.nextNode();
8085
+ }
8086
+ };
8087
+ /**
8088
+ * Wait until every external stylesheet of the clone has been applied or
8038
8089
  * definitively failed, or the readiness budget expires.
8039
8090
  *
8040
8091
  * The clone is mounted by adoptNode() into an already-closed document
@@ -8047,35 +8098,68 @@ const imagesReady = (document) => {
8047
8098
  * an unstyled layout (issue #245). Inline <style> clones apply
8048
8099
  * synchronously and document-level adopted stylesheets are serialised at
8049
8100
  * clone time, so neither needs this wait.
8101
+ *
8102
+ * Readiness is polled rather than purely event-driven because a <style>
8103
+ * clone serialised from `createStyleClone` re-fetches its external @import
8104
+ * rules without firing link load/error events, and shadow-root links are
8105
+ * equally event-hostile in some engines. The poll covers both; link events
8106
+ * remain as the settle path for plain <link> fetches.
8050
8107
  * @internal – exported for testing only.
8051
8108
  */
8052
8109
  const stylesheetsReady = (documentClone, logger, signal) => {
8053
8110
  if (signal?.aborted) return Promise.resolve();
8054
8111
  const pending = [];
8055
- const links = documentClone.getElementsByTagName("link");
8056
- for (let i = 0; i < links.length; i++) {
8057
- const link = links[i];
8058
- if (!link || link.sheet) continue;
8112
+ const links = [];
8113
+ collectLinks(documentClone, links);
8114
+ for (const link of links) {
8115
+ if (link.sheet) continue;
8059
8116
  const rel = (link.getAttribute("rel") ?? "").trim().toLowerCase().split(/\s+/);
8060
8117
  if (!rel.includes("stylesheet") || rel.includes("alternate") || link.disabled || !link.getAttribute("href")) continue;
8061
8118
  pending.push(new Promise((resolve) => {
8062
- let timer;
8119
+ const href = link.href;
8063
8120
  const settle = () => {
8064
8121
  link.removeEventListener("load", settle);
8065
8122
  link.removeEventListener("error", settle);
8066
8123
  signal?.removeEventListener("abort", onAbort);
8067
- clearTimeout(timer);
8124
+ clearInterval(timer);
8068
8125
  resolve();
8069
8126
  };
8070
8127
  const onAbort = () => settle();
8071
- timer = setTimeout(() => {
8072
- logger?.warn(`Stylesheet ${link.href} did not finish loading within ${CLONE_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8073
- settle();
8074
- }, CLONE_READY_TIMEOUT_MS);
8128
+ if (link.sheet) {
8129
+ resolve();
8130
+ return;
8131
+ }
8075
8132
  link.addEventListener("load", settle);
8076
8133
  link.addEventListener("error", settle);
8077
8134
  signal?.addEventListener("abort", onAbort, { once: true });
8078
- if (link.sheet) settle();
8135
+ let elapsed = 0;
8136
+ const timer = setInterval(() => {
8137
+ elapsed += 50;
8138
+ if (link.sheet) settle();
8139
+ else if (elapsed >= 1e4) {
8140
+ logger?.warn(`Stylesheet ${href} did not finish loading within ${STYLESHEET_READY_TIMEOUT_MS / 1e3}s; continuing with a possibly unstyled clone`);
8141
+ settle();
8142
+ }
8143
+ }, 50);
8144
+ }));
8145
+ }
8146
+ const styleElements = documentClone.getElementsByTagName("style");
8147
+ for (let i = 0; i < styleElements.length; i++) {
8148
+ const style = styleElements[i];
8149
+ const sheet = style?.sheet;
8150
+ if (!sheet || !style?.textContent?.includes("@import")) continue;
8151
+ if (isSheetFullyApplied(sheet)) continue;
8152
+ pending.push(new Promise((resolve) => {
8153
+ let elapsed = 0;
8154
+ const timer = setInterval(() => {
8155
+ elapsed += 50;
8156
+ const currentSheet = style.sheet;
8157
+ if (currentSheet && isSheetFullyApplied(currentSheet) || elapsed >= 1e4) {
8158
+ 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`);
8159
+ clearInterval(timer);
8160
+ resolve();
8161
+ }
8162
+ }, 50);
8079
8163
  }));
8080
8164
  }
8081
8165
  if (pending.length === 0) return Promise.resolve();
@@ -8233,7 +8317,10 @@ const mountCloneInIFrame = (context, ownerDocument, windowSize, mount) => {
8233
8317
  addBase(adoptedNode, baseUri);
8234
8318
  documentClone.replaceChild(adoptedNode, documentClone.documentElement);
8235
8319
  const stylesheetWait = stylesheetsReady(documentClone, context.logger, mount.signal);
8236
- return iframeLoad;
8320
+ return iframeLoad.catch((error) => {
8321
+ iframe.parentNode?.removeChild(iframe);
8322
+ throw error;
8323
+ });
8237
8324
  };
8238
8325
  //#endregion
8239
8326
  //#region src/dom/document-cloner.ts
@@ -13541,14 +13628,19 @@ var ForeignObjectRenderer = class {
13541
13628
  const svg = createForeignObjectSVG(this.options.width, this.options.height, 0, 0, element);
13542
13629
  const imagePromise = loadSerializedSVG(svg);
13543
13630
  restoreRootOffsets();
13544
- const img = await imagePromise;
13545
- if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13546
- if (this.options.backgroundColor) {
13547
- this.ctx.fillStyle = asString(this.options.backgroundColor);
13548
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
13631
+ try {
13632
+ const img = await imagePromise;
13633
+ if (this.options.signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
13634
+ if (this.options.backgroundColor) {
13635
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
13636
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
13637
+ }
13638
+ this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
13639
+ return this.canvas;
13640
+ } catch (error) {
13641
+ if (!this.options.canvas) releaseSurface(this.canvas);
13642
+ throw error;
13549
13643
  }
13550
- this.ctx.drawImage(img, this.options.x, this.options.y, this.options.width, this.options.height);
13551
- return this.canvas;
13552
13644
  }
13553
13645
  };
13554
13646
  /**
@@ -14104,19 +14196,17 @@ const renderElement = async (element, opts, config) => {
14104
14196
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
14105
14197
  const clonedElement = documentCloner.clonedReferenceElement;
14106
14198
  if (!clonedElement) throw new Error("Unable to find element in cloned iframe");
14107
- const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
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);
14199
+ let container;
14117
14200
  let canvas;
14118
14201
  let root;
14119
14202
  try {
14203
+ container = await documentCloner.toIFrame(ownerDocument, windowBounds);
14204
+ perfMonitor.end("clone");
14205
+ emitProgress(opts.onProgress, context, "clone", 30);
14206
+ throwIfAborted(signal);
14207
+ const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement) ? parseDocumentSize(clonedElement.ownerDocument) : parseBounds(context, clonedElement);
14208
+ const backgroundColor = parseBackgroundColor(context, clonedElement, opts.backgroundColor);
14209
+ const renderOptions = assembleRenderOptions(opts, backgroundColor, left, top, width, height, defaultView.devicePixelRatio ?? 1);
14120
14210
  if (foreignObjectRendering) {
14121
14211
  context.logger.debug(`Document cloned, using foreign object rendering`);
14122
14212
  perfMonitor.start("render-foreignobject");
@@ -14150,7 +14240,7 @@ const renderElement = async (element, opts, config) => {
14150
14240
  try {
14151
14241
  if (root) root.restoreTree();
14152
14242
  } finally {
14153
- if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
14243
+ if (container && (opts.removeContainer ?? true)) DocumentCloner.destroy(container);
14154
14244
  perfMonitor.end("cleanup");
14155
14245
  }
14156
14246
  }