html2canvas-pro 2.4.3 → 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.3 <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
  */
@@ -5720,7 +5720,7 @@
5720
5720
  const renderedArgs = renderFilterArgs(fn.values);
5721
5721
  switch (fn.name) {
5722
5722
  case "blur":
5723
- parts.push(`blur(${renderedArgs}px)`);
5723
+ parts.push(`blur(${renderedArgs})`);
5724
5724
  break;
5725
5725
  case "brightness":
5726
5726
  case "contrast":
@@ -5734,7 +5734,7 @@
5734
5734
  parts.push(`grayscale(${renderedArgs})`);
5735
5735
  break;
5736
5736
  case "hue-rotate":
5737
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5737
+ parts.push(`hue-rotate(${renderedArgs})`);
5738
5738
  break;
5739
5739
  case "drop-shadow":
5740
5740
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5752,14 +5752,20 @@
5752
5752
  const renderFilterArgs = (values) => {
5753
5753
  const parts = [];
5754
5754
  for (const v of values) {
5755
- if (v.type === 31) continue;
5756
- 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}`);
5757
5763
  else if (v.type === 17) parts.push(`${v.number}`);
5758
5764
  else if (v.type === 16) parts.push(`${v.number}%`);
5759
5765
  else if (v.type === 20) parts.push(v.value);
5760
5766
  else if (v.type === 5) parts.push(`#${v.value}`);
5761
5767
  }
5762
- return parts.join(" ");
5768
+ return parts.join("").trim();
5763
5769
  };
5764
5770
  //#endregion
5765
5771
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8309,7 +8315,7 @@
8309
8315
  return container;
8310
8316
  };
8311
8317
  const createsRealStackingContext = (node, container, root) => {
8312
- 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();
8313
8319
  };
8314
8320
  const createsStackingContext = (styles) => {
8315
8321
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8746,12 +8752,13 @@
8746
8752
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8747
8753
  }
8748
8754
  }
8749
- getEffects(target) {
8755
+ getEffects(target, surfaceRoot) {
8750
8756
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8751
- let parent = this.parent;
8752
- 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);
8753
8760
  while (parent) {
8754
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8761
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8755
8762
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8756
8763
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8757
8764
  if (hasOverflowClip(parent.container.styles)) {
@@ -8761,7 +8768,7 @@
8761
8768
  }
8762
8769
  effects.unshift(...croplessEffects);
8763
8770
  } else effects.unshift(...croplessEffects);
8764
- parent = parent.parent;
8771
+ parent = parent === surfaceRoot ? null : parent.parent;
8765
8772
  }
8766
8773
  return effects.filter((effect) => contains(effect.target, target));
8767
8774
  }
@@ -9042,6 +9049,268 @@
9042
9049
  }
9043
9050
  };
9044
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
9045
9314
  //#region src/render/canvas/text/text-decoration-renderer.ts
9046
9315
  var TextDecorationRenderer = class {
9047
9316
  constructor(ctx) {
@@ -9171,8 +9440,8 @@
9171
9440
  const TEXT_OVERFLOW_EPSILON = 1;
9172
9441
  /**
9173
9442
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9174
- * CJK characters use the ideographic baseline in browsers, which differs
9175
- * 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.
9176
9445
  *
9177
9446
  * Covers:
9178
9447
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9242,15 +9511,17 @@
9242
9511
  return w;
9243
9512
  }
9244
9513
  /**
9245
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9246
- * per-script baseline for each character.
9514
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9247
9515
  *
9248
9516
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9249
- * character. This helper centralises two fixes applied during that iteration:
9250
- * 1. Add `letterSpacing` to each character's advance width (was previously
9251
- * omitted, causing characters to render without any spacing).
9252
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9253
- * 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.
9254
9525
  *
9255
9526
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9256
9527
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9266,12 +9537,7 @@
9266
9537
  const fontString = this.ctx.font;
9267
9538
  let left = text.bounds.left;
9268
9539
  for (const letter of letters) {
9269
- if (hasCJKCharacters(letter)) {
9270
- const savedBaseline = this.ctx.textBaseline;
9271
- this.ctx.textBaseline = "ideographic";
9272
- renderFn(letter, left, y);
9273
- this.ctx.textBaseline = savedBaseline;
9274
- } else renderFn(letter, left, y);
9540
+ renderFn(letter, left, y);
9275
9541
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9276
9542
  }
9277
9543
  }
@@ -9313,7 +9579,7 @@
9313
9579
  /**
9314
9580
  * Render text with letter-spacing applied (fill pass).
9315
9581
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9316
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9582
+ * grapheme is drawn individually so spacing is applied correctly.
9317
9583
  */
9318
9584
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9319
9585
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10548,12 +10814,19 @@
10548
10814
  //#endregion
10549
10815
  //#region src/render/canvas/canvas-renderer.ts
10550
10816
  var CanvasRenderer = class CanvasRenderer {
10551
- constructor(context, options) {
10817
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10818
+ this.surfaceRoot = surfaceRoot;
10819
+ this.surfaceBudget = surfaceBudget;
10820
+ this.legacySubtree = false;
10552
10821
  this.context = context;
10553
10822
  this.options = options;
10554
10823
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10555
10824
  const ctx = this.canvas.getContext("2d");
10556
- 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
+ }
10557
10830
  this.ctx = ctx;
10558
10831
  if (!options.canvas) {
10559
10832
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10589,7 +10862,77 @@
10589
10862
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10590
10863
  }
10591
10864
  async renderStack(stack) {
10592
- 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
+ }
10593
10936
  }
10594
10937
  async renderNode(paint) {
10595
10938
  if (paint.container.debugRender) debugger;
@@ -10617,7 +10960,7 @@
10617
10960
  }
10618
10961
  }
10619
10962
  async renderNodeContent(paint) {
10620
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10963
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10621
10964
  const container = paint.container;
10622
10965
  const curves = paint.curves;
10623
10966
  const styles = container.styles;
@@ -10681,8 +11024,51 @@
10681
11024
  formatPath(paths) {
10682
11025
  formatCanvasPath(this.ctx, paths);
10683
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
+ }
10684
11070
  async renderNodeBackgroundAndBorders(paint) {
10685
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11071
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10686
11072
  const styles = paint.container.styles;
10687
11073
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10688
11074
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10723,7 +11109,11 @@
10723
11109
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10724
11110
  this.ctx.restore();
10725
11111
  }
10726
- 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
+ }
10727
11117
  this.ctx.save();
10728
11118
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10729
11119
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10737,14 +11127,15 @@
10737
11127
  this.ctx.clip();
10738
11128
  this.path(shadowPaintingArea);
10739
11129
  }
10740
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10741
- 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;
10742
11133
  this.ctx.shadowColor = asString(shadow.color);
10743
- this.ctx.shadowBlur = shadow.blur.number;
11134
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10744
11135
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10745
11136
  this.ctx.fill();
10746
11137
  this.ctx.restore();
10747
- });
11138
+ }
10748
11139
  }
10749
11140
  if (styles.borderImageSource) {
10750
11141
  const source = styles.borderImageSource;
@@ -10800,14 +11191,19 @@
10800
11191
  }
10801
11192
  }
10802
11193
  async render(element) {
10803
- if (this.options.backgroundColor) {
10804
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10805
- 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;
10806
11206
  }
10807
- const stack = parseStackingContexts(element);
10808
- await this.renderStack(stack);
10809
- this.effectsRenderer.applyEffects([]);
10810
- return this.canvas;
10811
11207
  }
10812
11208
  /**
10813
11209
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11223,22 +11619,6 @@
11223
11619
  }
11224
11620
  };
11225
11621
  //#endregion
11226
- //#region src/core/abort-helper.ts
11227
- /**
11228
- * AbortSignal helper utilities.
11229
- */
11230
- /**
11231
- * Throw a DOMException if the given AbortSignal is already aborted.
11232
- * Used at checkpoints throughout the rendering pipeline to support
11233
- * cancellable renders.
11234
- *
11235
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11236
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11237
- */
11238
- const throwIfAborted = (signal) => {
11239
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11240
- };
11241
- //#endregion
11242
11622
  //#region src/core/config-assembler.ts
11243
11623
  /**
11244
11624
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11407,18 +11787,19 @@
11407
11787
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11408
11788
  perfMonitor.end("render");
11409
11789
  }
11790
+ } finally {
11410
11791
  perfMonitor.start("cleanup");
11411
- if (opts.removeContainer ?? true) {
11412
- 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");
11413
11797
  }
11414
- perfMonitor.end("cleanup");
11415
- perfMonitor.end("total");
11416
- context.logger.debug(`Finished rendering`);
11417
- if (performanceMonitoring) perfMonitor.logSummary();
11418
- return canvas;
11419
- } finally {
11420
- if (root) root.restoreTree();
11421
11798
  }
11799
+ perfMonitor.end("total");
11800
+ context.logger.debug(`Finished rendering`);
11801
+ if (performanceMonitoring) perfMonitor.logSummary();
11802
+ return canvas;
11422
11803
  };
11423
11804
  //#endregion
11424
11805
  //#region src/index.ts