html2canvas-pro 2.4.2 → 2.4.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.4.2 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.4.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
  */
@@ -5331,10 +5331,15 @@
5331
5331
  parse: (_context, tokens) => {
5332
5332
  const accumulator = [];
5333
5333
  const results = [];
5334
+ const quoted = [];
5335
+ let isQuoted = false;
5334
5336
  tokens.forEach((token) => {
5335
5337
  switch (token.type) {
5336
5338
  case 20:
5339
+ accumulator.push(token.value);
5340
+ break;
5337
5341
  case 0:
5342
+ isQuoted = true;
5338
5343
  accumulator.push(token.value);
5339
5344
  break;
5340
5345
  case 17:
@@ -5342,12 +5347,17 @@
5342
5347
  break;
5343
5348
  case 4:
5344
5349
  results.push(accumulator.join(" "));
5350
+ quoted.push(isQuoted);
5351
+ isQuoted = false;
5345
5352
  accumulator.length = 0;
5346
5353
  break;
5347
5354
  }
5348
5355
  });
5349
- if (accumulator.length) results.push(accumulator.join(" "));
5350
- return results.map((result) => result.indexOf(" ") === -1 ? result : `'${result}'`);
5356
+ if (accumulator.length) {
5357
+ results.push(accumulator.join(" "));
5358
+ quoted.push(isQuoted);
5359
+ }
5360
+ return results.map((result, index) => quoted[index] || result.indexOf(" ") !== -1 ? `'${result.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ").replace(/\f/g, "\\c ")}'` : result);
5351
5361
  }
5352
5362
  };
5353
5363
  //#endregion
@@ -5710,7 +5720,7 @@
5710
5720
  const renderedArgs = renderFilterArgs(fn.values);
5711
5721
  switch (fn.name) {
5712
5722
  case "blur":
5713
- parts.push(`blur(${renderedArgs}px)`);
5723
+ parts.push(`blur(${renderedArgs})`);
5714
5724
  break;
5715
5725
  case "brightness":
5716
5726
  case "contrast":
@@ -5724,7 +5734,7 @@
5724
5734
  parts.push(`grayscale(${renderedArgs})`);
5725
5735
  break;
5726
5736
  case "hue-rotate":
5727
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5737
+ parts.push(`hue-rotate(${renderedArgs})`);
5728
5738
  break;
5729
5739
  case "drop-shadow":
5730
5740
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5742,14 +5752,20 @@
5742
5752
  const renderFilterArgs = (values) => {
5743
5753
  const parts = [];
5744
5754
  for (const v of values) {
5745
- if (v.type === 31) continue;
5746
- if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5755
+ if (v.type === 31) {
5756
+ parts.push(" ");
5757
+ continue;
5758
+ }
5759
+ if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
5760
+ else if (v.type === 4) parts.push(",");
5761
+ else if (v.type === 6) parts.push(v.value);
5762
+ else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5747
5763
  else if (v.type === 17) parts.push(`${v.number}`);
5748
5764
  else if (v.type === 16) parts.push(`${v.number}%`);
5749
5765
  else if (v.type === 20) parts.push(v.value);
5750
5766
  else if (v.type === 5) parts.push(`#${v.value}`);
5751
5767
  }
5752
- return parts.join(" ");
5768
+ return parts.join("").trim();
5753
5769
  };
5754
5770
  //#endregion
5755
5771
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8299,7 +8315,7 @@
8299
8315
  return container;
8300
8316
  };
8301
8317
  const createsRealStackingContext = (node, container, root) => {
8302
- return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8318
+ return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8303
8319
  };
8304
8320
  const createsStackingContext = (styles) => {
8305
8321
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8736,12 +8752,13 @@
8736
8752
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8737
8753
  }
8738
8754
  }
8739
- getEffects(target) {
8755
+ getEffects(target, surfaceRoot) {
8740
8756
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8741
- let parent = this.parent;
8742
- const effects = this.effects.slice(0);
8757
+ let parent = this === surfaceRoot ? null : this.parent;
8758
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
8759
+ const effects = sourceEffects(this);
8743
8760
  while (parent) {
8744
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8761
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8745
8762
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8746
8763
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8747
8764
  if (hasOverflowClip(parent.container.styles)) {
@@ -8751,7 +8768,7 @@
8751
8768
  }
8752
8769
  effects.unshift(...croplessEffects);
8753
8770
  } else effects.unshift(...croplessEffects);
8754
- parent = parent.parent;
8771
+ parent = parent === surfaceRoot ? null : parent.parent;
8755
8772
  }
8756
8773
  return effects.filter((effect) => contains(effect.target, target));
8757
8774
  }
@@ -9032,6 +9049,268 @@
9032
9049
  }
9033
9050
  };
9034
9051
  //#endregion
9052
+ //#region src/core/abort-helper.ts
9053
+ /**
9054
+ * AbortSignal helper utilities.
9055
+ */
9056
+ /**
9057
+ * Throw a DOMException if the given AbortSignal is already aborted.
9058
+ * Used at checkpoints throughout the rendering pipeline to support
9059
+ * cancellable renders.
9060
+ *
9061
+ * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
9062
+ * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
9063
+ */
9064
+ const throwIfAborted = (signal) => {
9065
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
9066
+ };
9067
+ //#endregion
9068
+ //#region src/render/canvas/filter-surface.ts
9069
+ var FilterSurfaceError = class extends Error {};
9070
+ const releaseSurface = (canvas) => {
9071
+ canvas.width = canvas.height = 0;
9072
+ };
9073
+ function parseSimpleFilter(value) {
9074
+ if (!value || value === "none") return { blur: 0 };
9075
+ const effect = new FilterEffect(value);
9076
+ const shadows = value.match(/drop-shadow\(/g) || [];
9077
+ if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
9078
+ const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
9079
+ if (effect.safeFilterString && !blur) return null;
9080
+ if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
9081
+ return {
9082
+ blur: blur ? Number(blur[1]) : 0,
9083
+ shadow: effect.shadow ? {
9084
+ x: effect.shadow.offsetX,
9085
+ y: effect.shadow.offsetY,
9086
+ blur: effect.shadow.blur,
9087
+ color: effect.shadow.color
9088
+ } : void 0
9089
+ };
9090
+ }
9091
+ function filterOutset(filter) {
9092
+ return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
9093
+ }
9094
+ function canvasLike(source) {
9095
+ const canvas = document.createElement("canvas");
9096
+ canvas.width = source.width;
9097
+ canvas.height = source.height;
9098
+ return canvas;
9099
+ }
9100
+ const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
9101
+ const finish = (error) => {
9102
+ clearTimeout(timer);
9103
+ signal?.removeEventListener("abort", abort);
9104
+ if (error) reject(error);
9105
+ else resolve();
9106
+ };
9107
+ const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
9108
+ const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
9109
+ signal?.addEventListener("abort", abort, { once: true });
9110
+ if (signal?.aborted) abort();
9111
+ else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
9112
+ });
9113
+ const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
9114
+ function supportsNativeFilters(owner = document) {
9115
+ const cached = canvasFilterSupport.get(owner);
9116
+ if (cached !== void 0) return cached;
9117
+ const source = owner.createElement("canvas");
9118
+ const target = owner.createElement("canvas");
9119
+ let supported = false;
9120
+ try {
9121
+ source.width = source.height = target.width = target.height = 32;
9122
+ const input = source.getContext("2d");
9123
+ const context = target.getContext("2d");
9124
+ if (input && context && "filter" in context) {
9125
+ input.fillStyle = "#fff";
9126
+ input.fillRect(8, 8, 8, 8);
9127
+ context.filter = "blur(2px)";
9128
+ context.drawImage(source, 0, 0);
9129
+ const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
9130
+ context.clearRect(0, 0, 32, 32);
9131
+ context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
9132
+ context.drawImage(source, 0, 0);
9133
+ const shadow = context.getImageData(24, 12, 1, 1).data;
9134
+ supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
9135
+ }
9136
+ } catch {} finally {
9137
+ releaseSurface(source);
9138
+ releaseSurface(target);
9139
+ }
9140
+ canvasFilterSupport.set(owner, supported);
9141
+ return supported;
9142
+ }
9143
+ function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9144
+ let filtered;
9145
+ let output;
9146
+ let complete = false;
9147
+ try {
9148
+ throwIfAborted(signal);
9149
+ filtered = canvasLike(source);
9150
+ const context = filtered.getContext("2d");
9151
+ if (!context || !("filter" in context)) return null;
9152
+ const functions = [];
9153
+ if (blur) functions.push(`blur(${blur * scale}px)`);
9154
+ if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
9155
+ context.filter = functions.join(" ");
9156
+ if (context.filter === "none") return null;
9157
+ context.drawImage(source, 0, 0);
9158
+ throwIfAborted(signal);
9159
+ output = canvasLike(source);
9160
+ const composite = output.getContext("2d");
9161
+ if (!composite) return null;
9162
+ composite.globalAlpha = opacity;
9163
+ composite.drawImage(filtered, 0, 0);
9164
+ throwIfAborted(signal);
9165
+ complete = true;
9166
+ return output;
9167
+ } catch {
9168
+ throwIfAborted(signal);
9169
+ return null;
9170
+ } finally {
9171
+ if (filtered) releaseSurface(filtered);
9172
+ if (output && !complete) releaseSurface(output);
9173
+ }
9174
+ }
9175
+ async function renderFilterSurface(source, filter, opacity, scale, signal) {
9176
+ throwIfAborted(signal);
9177
+ if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
9178
+ const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
9179
+ if (output) return output;
9180
+ }
9181
+ return renderSvgFilterSurface(source, filter, opacity, scale, signal);
9182
+ }
9183
+ async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9184
+ throwIfAborted(signal);
9185
+ if (!blur && !shadow) {
9186
+ const output = canvasLike(source);
9187
+ const context = output.getContext("2d");
9188
+ if (!context) {
9189
+ releaseSurface(output);
9190
+ throw new FilterSurfaceError("Filter surface canvas is unavailable");
9191
+ }
9192
+ context.globalAlpha = opacity;
9193
+ try {
9194
+ context.drawImage(source, 0, 0);
9195
+ return output;
9196
+ } catch {
9197
+ releaseSurface(output);
9198
+ throw new FilterSurfaceError("Filter surface image cannot be drawn");
9199
+ }
9200
+ }
9201
+ const namespace = "http://www.w3.org/2000/svg";
9202
+ const element = (tag, attributes = {}) => {
9203
+ const node = document.createElementNS(namespace, tag);
9204
+ Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
9205
+ return node;
9206
+ };
9207
+ const svg = element("svg", {
9208
+ width: source.width,
9209
+ height: source.height,
9210
+ viewBox: `0 0 ${source.width} ${source.height}`
9211
+ });
9212
+ const defs = element("defs");
9213
+ const filter = element("filter", {
9214
+ id: "composited-filter",
9215
+ filterUnits: "userSpaceOnUse",
9216
+ x: 0,
9217
+ y: 0,
9218
+ width: source.width,
9219
+ height: source.height,
9220
+ "color-interpolation-filters": "sRGB"
9221
+ });
9222
+ if (blur) filter.appendChild(element("feGaussianBlur", {
9223
+ stdDeviation: blur * scale,
9224
+ result: "blurred-source"
9225
+ }));
9226
+ if (shadow) {
9227
+ filter.appendChild(element("feGaussianBlur", {
9228
+ in: blur ? "blurred-source" : "SourceAlpha",
9229
+ stdDeviation: shadow.blur * scale
9230
+ }));
9231
+ filter.appendChild(element("feOffset", {
9232
+ dx: shadow.x * scale,
9233
+ dy: shadow.y * scale,
9234
+ result: "offset-shadow"
9235
+ }));
9236
+ filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
9237
+ filter.appendChild(element("feComposite", {
9238
+ in2: "offset-shadow",
9239
+ operator: "in"
9240
+ }));
9241
+ const merge = element("feMerge");
9242
+ merge.appendChild(element("feMergeNode"));
9243
+ merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
9244
+ filter.appendChild(merge);
9245
+ }
9246
+ defs.appendChild(filter);
9247
+ svg.appendChild(defs);
9248
+ let sourceUrl;
9249
+ try {
9250
+ sourceUrl = source.toDataURL("image/png");
9251
+ if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
9252
+ } catch {
9253
+ throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
9254
+ }
9255
+ svg.appendChild(element("image", {
9256
+ width: source.width,
9257
+ height: source.height,
9258
+ href: sourceUrl,
9259
+ filter: "url(#composited-filter)"
9260
+ }));
9261
+ const image = new Image();
9262
+ let output;
9263
+ try {
9264
+ image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
9265
+ await decodeSurface(image, signal);
9266
+ throwIfAborted(signal);
9267
+ output = canvasLike(source);
9268
+ const context = output.getContext("2d");
9269
+ if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
9270
+ context.globalAlpha = opacity;
9271
+ context.drawImage(image, 0, 0);
9272
+ return output;
9273
+ } catch (error) {
9274
+ if (output) releaseSurface(output);
9275
+ if (signal?.aborted) throwIfAborted(signal);
9276
+ throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
9277
+ } finally {
9278
+ image.removeAttribute("src");
9279
+ }
9280
+ }
9281
+ function reserveSurface(budget, width, height) {
9282
+ const pixels = width * height * 4;
9283
+ if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
9284
+ budget.pixels += pixels;
9285
+ return pixels;
9286
+ }
9287
+ const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
9288
+ const union = (a, b) => new Bounds(Math.min(a.left, b.left), Math.min(a.top, b.top), Math.max(a.left + a.width, b.left + b.width) - Math.min(a.left, b.left), Math.max(a.top + a.height, b.top + b.height) - Math.min(a.top, b.top));
9289
+ function surfaceBounds(container) {
9290
+ const styles = container.styles;
9291
+ let bounds = container.bounds;
9292
+ for (const shadow of styles.boxShadow) if (!shadow.inset) {
9293
+ const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
9294
+ bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
9295
+ }
9296
+ for (const text of container.textNodes) for (const part of text.textBounds) {
9297
+ const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
9298
+ bounds = union(bounds, ink);
9299
+ for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
9300
+ }
9301
+ for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
9302
+ const filter = parseSimpleFilter(styles.filter);
9303
+ return expand(bounds, filter ? filterOutset(filter) : 0);
9304
+ }
9305
+ function cropSurface(bounds, viewport, margin, scale) {
9306
+ const area = expand(viewport, margin);
9307
+ const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
9308
+ const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
9309
+ const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
9310
+ const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
9311
+ return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
9312
+ }
9313
+ //#endregion
9035
9314
  //#region src/render/canvas/text/text-decoration-renderer.ts
9036
9315
  var TextDecorationRenderer = class {
9037
9316
  constructor(ctx) {
@@ -9161,8 +9440,8 @@
9161
9440
  const TEXT_OVERFLOW_EPSILON = 1;
9162
9441
  /**
9163
9442
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9164
- * CJK characters use the ideographic baseline in browsers, which differs
9165
- * from the alphabetic baseline used for Latin script.
9443
+ * Vertical writing modes use it to keep CJK glyphs upright and on the
9444
+ * ideographic baseline while other scripts are rotated.
9166
9445
  *
9167
9446
  * Covers:
9168
9447
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9232,15 +9511,17 @@
9232
9511
  return w;
9233
9512
  }
9234
9513
  /**
9235
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9236
- * per-script baseline for each character.
9514
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9237
9515
  *
9238
9516
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9239
- * character. This helper centralises two fixes applied during that iteration:
9240
- * 1. Add `letterSpacing` to each character's advance width (was previously
9241
- * omitted, causing characters to render without any spacing).
9242
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9243
- * position matches how browsers lay them out in the DOM.
9517
+ * character, adding `letterSpacing` to each character's advance width.
9518
+ *
9519
+ * In horizontal text every glyph keeps the alphabetic baseline, CJK included:
9520
+ * inline layout aligns a whole line on its alphabetic baseline regardless of
9521
+ * script, and `baseline` was measured for that. Drawing CJK graphemes at the
9522
+ * same y with `textBaseline = 'ideographic'` lifts them above the Latin text
9523
+ * next to them, and away from where the `letterSpacing === 0` whole-string
9524
+ * path draws them.
9244
9525
  *
9245
9526
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9246
9527
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9256,12 +9537,7 @@
9256
9537
  const fontString = this.ctx.font;
9257
9538
  let left = text.bounds.left;
9258
9539
  for (const letter of letters) {
9259
- if (hasCJKCharacters(letter)) {
9260
- const savedBaseline = this.ctx.textBaseline;
9261
- this.ctx.textBaseline = "ideographic";
9262
- renderFn(letter, left, y);
9263
- this.ctx.textBaseline = savedBaseline;
9264
- } else renderFn(letter, left, y);
9540
+ renderFn(letter, left, y);
9265
9541
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9266
9542
  }
9267
9543
  }
@@ -9303,7 +9579,7 @@
9303
9579
  /**
9304
9580
  * Render text with letter-spacing applied (fill pass).
9305
9581
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9306
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9582
+ * grapheme is drawn individually so spacing is applied correctly.
9307
9583
  */
9308
9584
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9309
9585
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10538,12 +10814,19 @@
10538
10814
  //#endregion
10539
10815
  //#region src/render/canvas/canvas-renderer.ts
10540
10816
  var CanvasRenderer = class CanvasRenderer {
10541
- constructor(context, options) {
10817
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10818
+ this.surfaceRoot = surfaceRoot;
10819
+ this.surfaceBudget = surfaceBudget;
10820
+ this.legacySubtree = false;
10542
10821
  this.context = context;
10543
10822
  this.options = options;
10544
10823
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10545
10824
  const ctx = this.canvas.getContext("2d");
10546
- if (!ctx) throw new Error("Failed to get 2D rendering context from canvas");
10825
+ if (!ctx) {
10826
+ if (!options.canvas) releaseSurface(this.canvas);
10827
+ if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10828
+ throw new Error("Failed to get 2D rendering context from canvas");
10829
+ }
10547
10830
  this.ctx = ctx;
10548
10831
  if (!options.canvas) {
10549
10832
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10579,7 +10862,77 @@
10579
10862
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10580
10863
  }
10581
10864
  async renderStack(stack) {
10582
- if (stack.element.container.styles.isVisible()) await this.renderStackContent(stack);
10865
+ const styles = stack.element.container.styles;
10866
+ if (styles.isVisible()) {
10867
+ const filter = parseSimpleFilter(styles.filter);
10868
+ if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
10869
+ if (!await this.renderCompositedStack(stack, filter)) {
10870
+ this.legacySubtree = true;
10871
+ try {
10872
+ await this.renderStackContent(stack);
10873
+ } finally {
10874
+ this.legacySubtree = false;
10875
+ }
10876
+ }
10877
+ return;
10878
+ }
10879
+ await this.renderStackContent(stack);
10880
+ }
10881
+ }
10882
+ canComposite(paint) {
10883
+ const supported = (container) => {
10884
+ const styles = container.styles;
10885
+ return !styles.isTransformed() && styles.zoom === 1 && styles.mixBlendMode === MIX_BLEND_MODE$1.NORMAL && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
10886
+ };
10887
+ const subtree = (container) => supported(container) && container.elements.every(subtree);
10888
+ let ancestor = paint.parent;
10889
+ while (ancestor && ancestor !== this.surfaceRoot) {
10890
+ if (!supported(ancestor.container)) return false;
10891
+ ancestor = ancestor.parent;
10892
+ }
10893
+ return subtree(paint.container);
10894
+ }
10895
+ async renderCompositedStack(stack, filter) {
10896
+ const signal = this.options.signal;
10897
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10898
+ const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filterOutset(filter), this.options.scale);
10899
+ if (!bounds.width || !bounds.height) return true;
10900
+ const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
10901
+ if (!reserved) {
10902
+ this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
10903
+ return false;
10904
+ }
10905
+ const options = {
10906
+ ...this.options,
10907
+ canvas: void 0,
10908
+ backgroundColor: null,
10909
+ x: bounds.left,
10910
+ y: bounds.top,
10911
+ width: bounds.width,
10912
+ height: bounds.height
10913
+ };
10914
+ let source;
10915
+ let filtered;
10916
+ try {
10917
+ source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
10918
+ await source.renderStackContent(stack);
10919
+ source.effectsRenderer.applyEffects([]);
10920
+ filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
10921
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10922
+ const own = stack.element.effects;
10923
+ const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
10924
+ this.effectsRenderer.applyEffects(effects);
10925
+ this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
10926
+ return true;
10927
+ } catch (error) {
10928
+ if (!(error instanceof FilterSurfaceError)) throw error;
10929
+ this.context.logger.info(`${error.message}; using the existing renderer`);
10930
+ return false;
10931
+ } finally {
10932
+ if (source) releaseSurface(source.canvas);
10933
+ if (filtered) releaseSurface(filtered);
10934
+ this.surfaceBudget.pixels -= reserved;
10935
+ }
10583
10936
  }
10584
10937
  async renderNode(paint) {
10585
10938
  if (paint.container.debugRender) debugger;
@@ -10607,7 +10960,7 @@
10607
10960
  }
10608
10961
  }
10609
10962
  async renderNodeContent(paint) {
10610
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10963
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10611
10964
  const container = paint.container;
10612
10965
  const curves = paint.curves;
10613
10966
  const styles = container.styles;
@@ -10671,8 +11024,51 @@
10671
11024
  formatPath(paths) {
10672
11025
  formatCanvasPath(this.ctx, paths);
10673
11026
  }
11027
+ async renderSurfaceBoxShadow(paint, shadow) {
11028
+ const spread = shadow.spread.number;
11029
+ const blur = shadow.blur.number / 2;
11030
+ const margin = Math.ceil(3 * blur) + 1;
11031
+ const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
11032
+ if (box.width <= 0 || box.height <= 0) return;
11033
+ const scale = this.options.scale;
11034
+ const bounds = cropSurface(box.add(-margin, -margin, 2 * margin, 2 * margin), new Bounds(this.options.x - shadow.offsetX.number, this.options.y - shadow.offsetY.number, this.options.width, this.options.height), margin, scale);
11035
+ if (!bounds.width || !bounds.height) return;
11036
+ const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
11037
+ const reserved = reserveSurface(this.surfaceBudget, width, height);
11038
+ if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
11039
+ let source;
11040
+ let filtered;
11041
+ try {
11042
+ source = document.createElement("canvas");
11043
+ source.width = width;
11044
+ source.height = height;
11045
+ const ctx = source.getContext("2d");
11046
+ if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
11047
+ ctx.scale(scale, scale);
11048
+ ctx.translate(-bounds.left, -bounds.top);
11049
+ createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
11050
+ ctx.fillStyle = asString(shadow.color);
11051
+ ctx.fill();
11052
+ filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
11053
+ this.ctx.save();
11054
+ try {
11055
+ this.ctx.beginPath();
11056
+ this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
11057
+ formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
11058
+ this.ctx.closePath();
11059
+ this.ctx.clip("evenodd");
11060
+ this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
11061
+ } finally {
11062
+ this.ctx.restore();
11063
+ }
11064
+ } finally {
11065
+ if (source) releaseSurface(source);
11066
+ if (filtered) releaseSurface(filtered);
11067
+ this.surfaceBudget.pixels -= reserved;
11068
+ }
11069
+ }
10674
11070
  async renderNodeBackgroundAndBorders(paint) {
10675
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11071
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10676
11072
  const styles = paint.container.styles;
10677
11073
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10678
11074
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10713,7 +11109,11 @@
10713
11109
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10714
11110
  this.ctx.restore();
10715
11111
  }
10716
- styles.boxShadow.slice(0).reverse().forEach((shadow) => {
11112
+ for (const shadow of styles.boxShadow.slice(0).reverse()) {
11113
+ if (this.surfaceRoot && !shadow.inset) {
11114
+ await this.renderSurfaceBoxShadow(paint, shadow);
11115
+ continue;
11116
+ }
10717
11117
  this.ctx.save();
10718
11118
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10719
11119
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10727,14 +11127,15 @@
10727
11127
  this.ctx.clip();
10728
11128
  this.path(shadowPaintingArea);
10729
11129
  }
10730
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10731
- this.ctx.shadowOffsetY = shadow.offsetY.number;
11130
+ const shadowScale = this.surfaceRoot ? this.options.scale : 1;
11131
+ this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
11132
+ this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
10732
11133
  this.ctx.shadowColor = asString(shadow.color);
10733
- this.ctx.shadowBlur = shadow.blur.number;
11134
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10734
11135
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10735
11136
  this.ctx.fill();
10736
11137
  this.ctx.restore();
10737
- });
11138
+ }
10738
11139
  }
10739
11140
  if (styles.borderImageSource) {
10740
11141
  const source = styles.borderImageSource;
@@ -10790,14 +11191,19 @@
10790
11191
  }
10791
11192
  }
10792
11193
  async render(element) {
10793
- if (this.options.backgroundColor) {
10794
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10795
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11194
+ try {
11195
+ if (this.options.backgroundColor) {
11196
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
11197
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11198
+ }
11199
+ const stack = parseStackingContexts(element);
11200
+ await this.renderStack(stack);
11201
+ this.effectsRenderer.applyEffects([]);
11202
+ return this.canvas;
11203
+ } catch (error) {
11204
+ if (!this.options.canvas) releaseSurface(this.canvas);
11205
+ throw error;
10796
11206
  }
10797
- const stack = parseStackingContexts(element);
10798
- await this.renderStack(stack);
10799
- this.effectsRenderer.applyEffects([]);
10800
- return this.canvas;
10801
11207
  }
10802
11208
  /**
10803
11209
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11213,22 +11619,6 @@
11213
11619
  }
11214
11620
  };
11215
11621
  //#endregion
11216
- //#region src/core/abort-helper.ts
11217
- /**
11218
- * AbortSignal helper utilities.
11219
- */
11220
- /**
11221
- * Throw a DOMException if the given AbortSignal is already aborted.
11222
- * Used at checkpoints throughout the rendering pipeline to support
11223
- * cancellable renders.
11224
- *
11225
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11226
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11227
- */
11228
- const throwIfAborted = (signal) => {
11229
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11230
- };
11231
- //#endregion
11232
11622
  //#region src/core/config-assembler.ts
11233
11623
  /**
11234
11624
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11397,18 +11787,19 @@
11397
11787
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11398
11788
  perfMonitor.end("render");
11399
11789
  }
11790
+ } finally {
11400
11791
  perfMonitor.start("cleanup");
11401
- if (opts.removeContainer ?? true) {
11402
- if (!DocumentCloner.destroy(container)) context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
11792
+ try {
11793
+ if (root) root.restoreTree();
11794
+ } finally {
11795
+ if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
11796
+ perfMonitor.end("cleanup");
11403
11797
  }
11404
- perfMonitor.end("cleanup");
11405
- perfMonitor.end("total");
11406
- context.logger.debug(`Finished rendering`);
11407
- if (performanceMonitoring) perfMonitor.logSummary();
11408
- return canvas;
11409
- } finally {
11410
- if (root) root.restoreTree();
11411
11798
  }
11799
+ perfMonitor.end("total");
11800
+ context.logger.debug(`Finished rendering`);
11801
+ if (performanceMonitoring) perfMonitor.logSummary();
11802
+ return canvas;
11412
11803
  };
11413
11804
  //#endregion
11414
11805
  //#region src/index.ts