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
  */
@@ -5717,7 +5717,7 @@ const filter = {
5717
5717
  const renderedArgs = renderFilterArgs(fn.values);
5718
5718
  switch (fn.name) {
5719
5719
  case "blur":
5720
- parts.push(`blur(${renderedArgs}px)`);
5720
+ parts.push(`blur(${renderedArgs})`);
5721
5721
  break;
5722
5722
  case "brightness":
5723
5723
  case "contrast":
@@ -5731,7 +5731,7 @@ const filter = {
5731
5731
  parts.push(`grayscale(${renderedArgs})`);
5732
5732
  break;
5733
5733
  case "hue-rotate":
5734
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5734
+ parts.push(`hue-rotate(${renderedArgs})`);
5735
5735
  break;
5736
5736
  case "drop-shadow":
5737
5737
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5749,14 +5749,20 @@ const filter = {
5749
5749
  const renderFilterArgs = (values) => {
5750
5750
  const parts = [];
5751
5751
  for (const v of values) {
5752
- if (v.type === 31) continue;
5753
- if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5752
+ if (v.type === 31) {
5753
+ parts.push(" ");
5754
+ continue;
5755
+ }
5756
+ if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
5757
+ else if (v.type === 4) parts.push(",");
5758
+ else if (v.type === 6) parts.push(v.value);
5759
+ else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5754
5760
  else if (v.type === 17) parts.push(`${v.number}`);
5755
5761
  else if (v.type === 16) parts.push(`${v.number}%`);
5756
5762
  else if (v.type === 20) parts.push(v.value);
5757
5763
  else if (v.type === 5) parts.push(`#${v.value}`);
5758
5764
  }
5759
- return parts.join(" ");
5765
+ return parts.join("").trim();
5760
5766
  };
5761
5767
  //#endregion
5762
5768
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8306,7 +8312,7 @@ const parseTree = (context, element) => {
8306
8312
  return container;
8307
8313
  };
8308
8314
  const createsRealStackingContext = (node, container, root) => {
8309
- return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8315
+ return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8310
8316
  };
8311
8317
  const createsStackingContext = (styles) => {
8312
8318
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8743,12 +8749,13 @@ var ElementPaint = class {
8743
8749
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8744
8750
  }
8745
8751
  }
8746
- getEffects(target) {
8752
+ getEffects(target, surfaceRoot) {
8747
8753
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8748
- let parent = this.parent;
8749
- const effects = this.effects.slice(0);
8754
+ let parent = this === surfaceRoot ? null : this.parent;
8755
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
8756
+ const effects = sourceEffects(this);
8750
8757
  while (parent) {
8751
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8758
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8752
8759
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8753
8760
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8754
8761
  if (hasOverflowClip(parent.container.styles)) {
@@ -8758,7 +8765,7 @@ var ElementPaint = class {
8758
8765
  }
8759
8766
  effects.unshift(...croplessEffects);
8760
8767
  } else effects.unshift(...croplessEffects);
8761
- parent = parent.parent;
8768
+ parent = parent === surfaceRoot ? null : parent.parent;
8762
8769
  }
8763
8770
  return effects.filter((effect) => contains(effect.target, target));
8764
8771
  }
@@ -9039,6 +9046,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
9039
9046
  }
9040
9047
  };
9041
9048
  //#endregion
9049
+ //#region src/core/abort-helper.ts
9050
+ /**
9051
+ * AbortSignal helper utilities.
9052
+ */
9053
+ /**
9054
+ * Throw a DOMException if the given AbortSignal is already aborted.
9055
+ * Used at checkpoints throughout the rendering pipeline to support
9056
+ * cancellable renders.
9057
+ *
9058
+ * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
9059
+ * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
9060
+ */
9061
+ const throwIfAborted = (signal) => {
9062
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
9063
+ };
9064
+ //#endregion
9065
+ //#region src/render/canvas/filter-surface.ts
9066
+ var FilterSurfaceError = class extends Error {};
9067
+ const releaseSurface = (canvas) => {
9068
+ canvas.width = canvas.height = 0;
9069
+ };
9070
+ function parseSimpleFilter(value) {
9071
+ if (!value || value === "none") return { blur: 0 };
9072
+ const effect = new FilterEffect(value);
9073
+ const shadows = value.match(/drop-shadow\(/g) || [];
9074
+ if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
9075
+ const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
9076
+ if (effect.safeFilterString && !blur) return null;
9077
+ if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
9078
+ return {
9079
+ blur: blur ? Number(blur[1]) : 0,
9080
+ shadow: effect.shadow ? {
9081
+ x: effect.shadow.offsetX,
9082
+ y: effect.shadow.offsetY,
9083
+ blur: effect.shadow.blur,
9084
+ color: effect.shadow.color
9085
+ } : void 0
9086
+ };
9087
+ }
9088
+ function filterOutset(filter) {
9089
+ return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
9090
+ }
9091
+ function canvasLike(source) {
9092
+ const canvas = document.createElement("canvas");
9093
+ canvas.width = source.width;
9094
+ canvas.height = source.height;
9095
+ return canvas;
9096
+ }
9097
+ const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
9098
+ const finish = (error) => {
9099
+ clearTimeout(timer);
9100
+ signal?.removeEventListener("abort", abort);
9101
+ if (error) reject(error);
9102
+ else resolve();
9103
+ };
9104
+ const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
9105
+ const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
9106
+ signal?.addEventListener("abort", abort, { once: true });
9107
+ if (signal?.aborted) abort();
9108
+ else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
9109
+ });
9110
+ const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
9111
+ function supportsNativeFilters(owner = document) {
9112
+ const cached = canvasFilterSupport.get(owner);
9113
+ if (cached !== void 0) return cached;
9114
+ const source = owner.createElement("canvas");
9115
+ const target = owner.createElement("canvas");
9116
+ let supported = false;
9117
+ try {
9118
+ source.width = source.height = target.width = target.height = 32;
9119
+ const input = source.getContext("2d");
9120
+ const context = target.getContext("2d");
9121
+ if (input && context && "filter" in context) {
9122
+ input.fillStyle = "#fff";
9123
+ input.fillRect(8, 8, 8, 8);
9124
+ context.filter = "blur(2px)";
9125
+ context.drawImage(source, 0, 0);
9126
+ const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
9127
+ context.clearRect(0, 0, 32, 32);
9128
+ context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
9129
+ context.drawImage(source, 0, 0);
9130
+ const shadow = context.getImageData(24, 12, 1, 1).data;
9131
+ supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
9132
+ }
9133
+ } catch {} finally {
9134
+ releaseSurface(source);
9135
+ releaseSurface(target);
9136
+ }
9137
+ canvasFilterSupport.set(owner, supported);
9138
+ return supported;
9139
+ }
9140
+ function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9141
+ let filtered;
9142
+ let output;
9143
+ let complete = false;
9144
+ try {
9145
+ throwIfAborted(signal);
9146
+ filtered = canvasLike(source);
9147
+ const context = filtered.getContext("2d");
9148
+ if (!context || !("filter" in context)) return null;
9149
+ const functions = [];
9150
+ if (blur) functions.push(`blur(${blur * scale}px)`);
9151
+ if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
9152
+ context.filter = functions.join(" ");
9153
+ if (context.filter === "none") return null;
9154
+ context.drawImage(source, 0, 0);
9155
+ throwIfAborted(signal);
9156
+ output = canvasLike(source);
9157
+ const composite = output.getContext("2d");
9158
+ if (!composite) return null;
9159
+ composite.globalAlpha = opacity;
9160
+ composite.drawImage(filtered, 0, 0);
9161
+ throwIfAborted(signal);
9162
+ complete = true;
9163
+ return output;
9164
+ } catch {
9165
+ throwIfAborted(signal);
9166
+ return null;
9167
+ } finally {
9168
+ if (filtered) releaseSurface(filtered);
9169
+ if (output && !complete) releaseSurface(output);
9170
+ }
9171
+ }
9172
+ async function renderFilterSurface(source, filter, opacity, scale, signal) {
9173
+ throwIfAborted(signal);
9174
+ if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
9175
+ const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
9176
+ if (output) return output;
9177
+ }
9178
+ return renderSvgFilterSurface(source, filter, opacity, scale, signal);
9179
+ }
9180
+ async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9181
+ throwIfAborted(signal);
9182
+ if (!blur && !shadow) {
9183
+ const output = canvasLike(source);
9184
+ const context = output.getContext("2d");
9185
+ if (!context) {
9186
+ releaseSurface(output);
9187
+ throw new FilterSurfaceError("Filter surface canvas is unavailable");
9188
+ }
9189
+ context.globalAlpha = opacity;
9190
+ try {
9191
+ context.drawImage(source, 0, 0);
9192
+ return output;
9193
+ } catch {
9194
+ releaseSurface(output);
9195
+ throw new FilterSurfaceError("Filter surface image cannot be drawn");
9196
+ }
9197
+ }
9198
+ const namespace = "http://www.w3.org/2000/svg";
9199
+ const element = (tag, attributes = {}) => {
9200
+ const node = document.createElementNS(namespace, tag);
9201
+ Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
9202
+ return node;
9203
+ };
9204
+ const svg = element("svg", {
9205
+ width: source.width,
9206
+ height: source.height,
9207
+ viewBox: `0 0 ${source.width} ${source.height}`
9208
+ });
9209
+ const defs = element("defs");
9210
+ const filter = element("filter", {
9211
+ id: "composited-filter",
9212
+ filterUnits: "userSpaceOnUse",
9213
+ x: 0,
9214
+ y: 0,
9215
+ width: source.width,
9216
+ height: source.height,
9217
+ "color-interpolation-filters": "sRGB"
9218
+ });
9219
+ if (blur) filter.appendChild(element("feGaussianBlur", {
9220
+ stdDeviation: blur * scale,
9221
+ result: "blurred-source"
9222
+ }));
9223
+ if (shadow) {
9224
+ filter.appendChild(element("feGaussianBlur", {
9225
+ in: blur ? "blurred-source" : "SourceAlpha",
9226
+ stdDeviation: shadow.blur * scale
9227
+ }));
9228
+ filter.appendChild(element("feOffset", {
9229
+ dx: shadow.x * scale,
9230
+ dy: shadow.y * scale,
9231
+ result: "offset-shadow"
9232
+ }));
9233
+ filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
9234
+ filter.appendChild(element("feComposite", {
9235
+ in2: "offset-shadow",
9236
+ operator: "in"
9237
+ }));
9238
+ const merge = element("feMerge");
9239
+ merge.appendChild(element("feMergeNode"));
9240
+ merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
9241
+ filter.appendChild(merge);
9242
+ }
9243
+ defs.appendChild(filter);
9244
+ svg.appendChild(defs);
9245
+ let sourceUrl;
9246
+ try {
9247
+ sourceUrl = source.toDataURL("image/png");
9248
+ if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
9249
+ } catch {
9250
+ throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
9251
+ }
9252
+ svg.appendChild(element("image", {
9253
+ width: source.width,
9254
+ height: source.height,
9255
+ href: sourceUrl,
9256
+ filter: "url(#composited-filter)"
9257
+ }));
9258
+ const image = new Image();
9259
+ let output;
9260
+ try {
9261
+ image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
9262
+ await decodeSurface(image, signal);
9263
+ throwIfAborted(signal);
9264
+ output = canvasLike(source);
9265
+ const context = output.getContext("2d");
9266
+ if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
9267
+ context.globalAlpha = opacity;
9268
+ context.drawImage(image, 0, 0);
9269
+ return output;
9270
+ } catch (error) {
9271
+ if (output) releaseSurface(output);
9272
+ if (signal?.aborted) throwIfAborted(signal);
9273
+ throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
9274
+ } finally {
9275
+ image.removeAttribute("src");
9276
+ }
9277
+ }
9278
+ function reserveSurface(budget, width, height) {
9279
+ const pixels = width * height * 4;
9280
+ if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
9281
+ budget.pixels += pixels;
9282
+ return pixels;
9283
+ }
9284
+ const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
9285
+ 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));
9286
+ function surfaceBounds(container) {
9287
+ const styles = container.styles;
9288
+ let bounds = container.bounds;
9289
+ for (const shadow of styles.boxShadow) if (!shadow.inset) {
9290
+ const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
9291
+ bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
9292
+ }
9293
+ for (const text of container.textNodes) for (const part of text.textBounds) {
9294
+ const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
9295
+ bounds = union(bounds, ink);
9296
+ for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
9297
+ }
9298
+ for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
9299
+ const filter = parseSimpleFilter(styles.filter);
9300
+ return expand(bounds, filter ? filterOutset(filter) : 0);
9301
+ }
9302
+ function cropSurface(bounds, viewport, margin, scale) {
9303
+ const area = expand(viewport, margin);
9304
+ const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
9305
+ const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
9306
+ const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
9307
+ const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
9308
+ return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
9309
+ }
9310
+ //#endregion
9042
9311
  //#region src/render/canvas/text/text-decoration-renderer.ts
9043
9312
  var TextDecorationRenderer = class {
9044
9313
  constructor(ctx) {
@@ -9168,8 +9437,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
9168
9437
  const TEXT_OVERFLOW_EPSILON = 1;
9169
9438
  /**
9170
9439
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9171
- * CJK characters use the ideographic baseline in browsers, which differs
9172
- * from the alphabetic baseline used for Latin script.
9440
+ * Vertical writing modes use it to keep CJK glyphs upright and on the
9441
+ * ideographic baseline while other scripts are rotated.
9173
9442
  *
9174
9443
  * Covers:
9175
9444
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9239,15 +9508,17 @@ var TextRenderer = class {
9239
9508
  return w;
9240
9509
  }
9241
9510
  /**
9242
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9243
- * per-script baseline for each character.
9511
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9244
9512
  *
9245
9513
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9246
- * character. This helper centralises two fixes applied during that iteration:
9247
- * 1. Add `letterSpacing` to each character's advance width (was previously
9248
- * omitted, causing characters to render without any spacing).
9249
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9250
- * position matches how browsers lay them out in the DOM.
9514
+ * character, adding `letterSpacing` to each character's advance width.
9515
+ *
9516
+ * In horizontal text every glyph keeps the alphabetic baseline, CJK included:
9517
+ * inline layout aligns a whole line on its alphabetic baseline regardless of
9518
+ * script, and `baseline` was measured for that. Drawing CJK graphemes at the
9519
+ * same y with `textBaseline = 'ideographic'` lifts them above the Latin text
9520
+ * next to them, and away from where the `letterSpacing === 0` whole-string
9521
+ * path draws them.
9251
9522
  *
9252
9523
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9253
9524
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9263,12 +9534,7 @@ var TextRenderer = class {
9263
9534
  const fontString = this.ctx.font;
9264
9535
  let left = text.bounds.left;
9265
9536
  for (const letter of letters) {
9266
- if (hasCJKCharacters(letter)) {
9267
- const savedBaseline = this.ctx.textBaseline;
9268
- this.ctx.textBaseline = "ideographic";
9269
- renderFn(letter, left, y);
9270
- this.ctx.textBaseline = savedBaseline;
9271
- } else renderFn(letter, left, y);
9537
+ renderFn(letter, left, y);
9272
9538
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9273
9539
  }
9274
9540
  }
@@ -9310,7 +9576,7 @@ var TextRenderer = class {
9310
9576
  /**
9311
9577
  * Render text with letter-spacing applied (fill pass).
9312
9578
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9313
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9579
+ * grapheme is drawn individually so spacing is applied correctly.
9314
9580
  */
9315
9581
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9316
9582
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10545,12 +10811,19 @@ const canvasTextAlign = (textAlign) => {
10545
10811
  //#endregion
10546
10812
  //#region src/render/canvas/canvas-renderer.ts
10547
10813
  var CanvasRenderer = class CanvasRenderer {
10548
- constructor(context, options) {
10814
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10815
+ this.surfaceRoot = surfaceRoot;
10816
+ this.surfaceBudget = surfaceBudget;
10817
+ this.legacySubtree = false;
10549
10818
  this.context = context;
10550
10819
  this.options = options;
10551
10820
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10552
10821
  const ctx = this.canvas.getContext("2d");
10553
- if (!ctx) throw new Error("Failed to get 2D rendering context from canvas");
10822
+ if (!ctx) {
10823
+ if (!options.canvas) releaseSurface(this.canvas);
10824
+ if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10825
+ throw new Error("Failed to get 2D rendering context from canvas");
10826
+ }
10554
10827
  this.ctx = ctx;
10555
10828
  if (!options.canvas) {
10556
10829
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10586,7 +10859,77 @@ var CanvasRenderer = class CanvasRenderer {
10586
10859
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10587
10860
  }
10588
10861
  async renderStack(stack) {
10589
- if (stack.element.container.styles.isVisible()) await this.renderStackContent(stack);
10862
+ const styles = stack.element.container.styles;
10863
+ if (styles.isVisible()) {
10864
+ const filter = parseSimpleFilter(styles.filter);
10865
+ if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
10866
+ if (!await this.renderCompositedStack(stack, filter)) {
10867
+ this.legacySubtree = true;
10868
+ try {
10869
+ await this.renderStackContent(stack);
10870
+ } finally {
10871
+ this.legacySubtree = false;
10872
+ }
10873
+ }
10874
+ return;
10875
+ }
10876
+ await this.renderStackContent(stack);
10877
+ }
10878
+ }
10879
+ canComposite(paint) {
10880
+ const supported = (container) => {
10881
+ const styles = container.styles;
10882
+ 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;
10883
+ };
10884
+ const subtree = (container) => supported(container) && container.elements.every(subtree);
10885
+ let ancestor = paint.parent;
10886
+ while (ancestor && ancestor !== this.surfaceRoot) {
10887
+ if (!supported(ancestor.container)) return false;
10888
+ ancestor = ancestor.parent;
10889
+ }
10890
+ return subtree(paint.container);
10891
+ }
10892
+ async renderCompositedStack(stack, filter) {
10893
+ const signal = this.options.signal;
10894
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10895
+ 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);
10896
+ if (!bounds.width || !bounds.height) return true;
10897
+ const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
10898
+ if (!reserved) {
10899
+ this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
10900
+ return false;
10901
+ }
10902
+ const options = {
10903
+ ...this.options,
10904
+ canvas: void 0,
10905
+ backgroundColor: null,
10906
+ x: bounds.left,
10907
+ y: bounds.top,
10908
+ width: bounds.width,
10909
+ height: bounds.height
10910
+ };
10911
+ let source;
10912
+ let filtered;
10913
+ try {
10914
+ source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
10915
+ await source.renderStackContent(stack);
10916
+ source.effectsRenderer.applyEffects([]);
10917
+ filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
10918
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10919
+ const own = stack.element.effects;
10920
+ const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
10921
+ this.effectsRenderer.applyEffects(effects);
10922
+ this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
10923
+ return true;
10924
+ } catch (error) {
10925
+ if (!(error instanceof FilterSurfaceError)) throw error;
10926
+ this.context.logger.info(`${error.message}; using the existing renderer`);
10927
+ return false;
10928
+ } finally {
10929
+ if (source) releaseSurface(source.canvas);
10930
+ if (filtered) releaseSurface(filtered);
10931
+ this.surfaceBudget.pixels -= reserved;
10932
+ }
10590
10933
  }
10591
10934
  async renderNode(paint) {
10592
10935
  if (paint.container.debugRender) debugger;
@@ -10614,7 +10957,7 @@ var CanvasRenderer = class CanvasRenderer {
10614
10957
  }
10615
10958
  }
10616
10959
  async renderNodeContent(paint) {
10617
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10960
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10618
10961
  const container = paint.container;
10619
10962
  const curves = paint.curves;
10620
10963
  const styles = container.styles;
@@ -10678,8 +11021,51 @@ var CanvasRenderer = class CanvasRenderer {
10678
11021
  formatPath(paths) {
10679
11022
  formatCanvasPath(this.ctx, paths);
10680
11023
  }
11024
+ async renderSurfaceBoxShadow(paint, shadow) {
11025
+ const spread = shadow.spread.number;
11026
+ const blur = shadow.blur.number / 2;
11027
+ const margin = Math.ceil(3 * blur) + 1;
11028
+ const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
11029
+ if (box.width <= 0 || box.height <= 0) return;
11030
+ const scale = this.options.scale;
11031
+ 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);
11032
+ if (!bounds.width || !bounds.height) return;
11033
+ const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
11034
+ const reserved = reserveSurface(this.surfaceBudget, width, height);
11035
+ if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
11036
+ let source;
11037
+ let filtered;
11038
+ try {
11039
+ source = document.createElement("canvas");
11040
+ source.width = width;
11041
+ source.height = height;
11042
+ const ctx = source.getContext("2d");
11043
+ if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
11044
+ ctx.scale(scale, scale);
11045
+ ctx.translate(-bounds.left, -bounds.top);
11046
+ createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
11047
+ ctx.fillStyle = asString(shadow.color);
11048
+ ctx.fill();
11049
+ filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
11050
+ this.ctx.save();
11051
+ try {
11052
+ this.ctx.beginPath();
11053
+ this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
11054
+ formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
11055
+ this.ctx.closePath();
11056
+ this.ctx.clip("evenodd");
11057
+ this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
11058
+ } finally {
11059
+ this.ctx.restore();
11060
+ }
11061
+ } finally {
11062
+ if (source) releaseSurface(source);
11063
+ if (filtered) releaseSurface(filtered);
11064
+ this.surfaceBudget.pixels -= reserved;
11065
+ }
11066
+ }
10681
11067
  async renderNodeBackgroundAndBorders(paint) {
10682
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11068
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10683
11069
  const styles = paint.container.styles;
10684
11070
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10685
11071
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10720,7 +11106,11 @@ var CanvasRenderer = class CanvasRenderer {
10720
11106
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10721
11107
  this.ctx.restore();
10722
11108
  }
10723
- styles.boxShadow.slice(0).reverse().forEach((shadow) => {
11109
+ for (const shadow of styles.boxShadow.slice(0).reverse()) {
11110
+ if (this.surfaceRoot && !shadow.inset) {
11111
+ await this.renderSurfaceBoxShadow(paint, shadow);
11112
+ continue;
11113
+ }
10724
11114
  this.ctx.save();
10725
11115
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10726
11116
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10734,14 +11124,15 @@ var CanvasRenderer = class CanvasRenderer {
10734
11124
  this.ctx.clip();
10735
11125
  this.path(shadowPaintingArea);
10736
11126
  }
10737
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10738
- this.ctx.shadowOffsetY = shadow.offsetY.number;
11127
+ const shadowScale = this.surfaceRoot ? this.options.scale : 1;
11128
+ this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
11129
+ this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
10739
11130
  this.ctx.shadowColor = asString(shadow.color);
10740
- this.ctx.shadowBlur = shadow.blur.number;
11131
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10741
11132
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10742
11133
  this.ctx.fill();
10743
11134
  this.ctx.restore();
10744
- });
11135
+ }
10745
11136
  }
10746
11137
  if (styles.borderImageSource) {
10747
11138
  const source = styles.borderImageSource;
@@ -10797,14 +11188,19 @@ var CanvasRenderer = class CanvasRenderer {
10797
11188
  }
10798
11189
  }
10799
11190
  async render(element) {
10800
- if (this.options.backgroundColor) {
10801
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10802
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11191
+ try {
11192
+ if (this.options.backgroundColor) {
11193
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
11194
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11195
+ }
11196
+ const stack = parseStackingContexts(element);
11197
+ await this.renderStack(stack);
11198
+ this.effectsRenderer.applyEffects([]);
11199
+ return this.canvas;
11200
+ } catch (error) {
11201
+ if (!this.options.canvas) releaseSurface(this.canvas);
11202
+ throw error;
10803
11203
  }
10804
- const stack = parseStackingContexts(element);
10805
- await this.renderStack(stack);
10806
- this.effectsRenderer.applyEffects([]);
10807
- return this.canvas;
10808
11204
  }
10809
11205
  /**
10810
11206
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11220,22 +11616,6 @@ var Context = class Context {
11220
11616
  }
11221
11617
  };
11222
11618
  //#endregion
11223
- //#region src/core/abort-helper.ts
11224
- /**
11225
- * AbortSignal helper utilities.
11226
- */
11227
- /**
11228
- * Throw a DOMException if the given AbortSignal is already aborted.
11229
- * Used at checkpoints throughout the rendering pipeline to support
11230
- * cancellable renders.
11231
- *
11232
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11233
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11234
- */
11235
- const throwIfAborted = (signal) => {
11236
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11237
- };
11238
- //#endregion
11239
11619
  //#region src/core/config-assembler.ts
11240
11620
  /**
11241
11621
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11404,18 +11784,19 @@ const renderElement = async (element, opts, config) => {
11404
11784
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11405
11785
  perfMonitor.end("render");
11406
11786
  }
11787
+ } finally {
11407
11788
  perfMonitor.start("cleanup");
11408
- if (opts.removeContainer ?? true) {
11409
- if (!DocumentCloner.destroy(container)) context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
11789
+ try {
11790
+ if (root) root.restoreTree();
11791
+ } finally {
11792
+ if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
11793
+ perfMonitor.end("cleanup");
11410
11794
  }
11411
- perfMonitor.end("cleanup");
11412
- perfMonitor.end("total");
11413
- context.logger.debug(`Finished rendering`);
11414
- if (performanceMonitoring) perfMonitor.logSummary();
11415
- return canvas;
11416
- } finally {
11417
- if (root) root.restoreTree();
11418
11795
  }
11796
+ perfMonitor.end("total");
11797
+ context.logger.debug(`Finished rendering`);
11798
+ if (performanceMonitoring) perfMonitor.logSummary();
11799
+ return canvas;
11419
11800
  };
11420
11801
  //#endregion
11421
11802
  //#region src/index.ts