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
  */
@@ -5324,10 +5324,15 @@ const fontFamily = {
5324
5324
  parse: (_context, tokens) => {
5325
5325
  const accumulator = [];
5326
5326
  const results = [];
5327
+ const quoted = [];
5328
+ let isQuoted = false;
5327
5329
  tokens.forEach((token) => {
5328
5330
  switch (token.type) {
5329
5331
  case 20:
5332
+ accumulator.push(token.value);
5333
+ break;
5330
5334
  case 0:
5335
+ isQuoted = true;
5331
5336
  accumulator.push(token.value);
5332
5337
  break;
5333
5338
  case 17:
@@ -5335,12 +5340,17 @@ const fontFamily = {
5335
5340
  break;
5336
5341
  case 4:
5337
5342
  results.push(accumulator.join(" "));
5343
+ quoted.push(isQuoted);
5344
+ isQuoted = false;
5338
5345
  accumulator.length = 0;
5339
5346
  break;
5340
5347
  }
5341
5348
  });
5342
- if (accumulator.length) results.push(accumulator.join(" "));
5343
- return results.map((result) => result.indexOf(" ") === -1 ? result : `'${result}'`);
5349
+ if (accumulator.length) {
5350
+ results.push(accumulator.join(" "));
5351
+ quoted.push(isQuoted);
5352
+ }
5353
+ 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);
5344
5354
  }
5345
5355
  };
5346
5356
  //#endregion
@@ -5703,7 +5713,7 @@ const filter = {
5703
5713
  const renderedArgs = renderFilterArgs(fn.values);
5704
5714
  switch (fn.name) {
5705
5715
  case "blur":
5706
- parts.push(`blur(${renderedArgs}px)`);
5716
+ parts.push(`blur(${renderedArgs})`);
5707
5717
  break;
5708
5718
  case "brightness":
5709
5719
  case "contrast":
@@ -5717,7 +5727,7 @@ const filter = {
5717
5727
  parts.push(`grayscale(${renderedArgs})`);
5718
5728
  break;
5719
5729
  case "hue-rotate":
5720
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5730
+ parts.push(`hue-rotate(${renderedArgs})`);
5721
5731
  break;
5722
5732
  case "drop-shadow":
5723
5733
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5735,14 +5745,20 @@ const filter = {
5735
5745
  const renderFilterArgs = (values) => {
5736
5746
  const parts = [];
5737
5747
  for (const v of values) {
5738
- if (v.type === 31) continue;
5739
- if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5748
+ if (v.type === 31) {
5749
+ parts.push(" ");
5750
+ continue;
5751
+ }
5752
+ if (v.type === 18) parts.push(`${v.name}(${renderFilterArgs(v.values)})`);
5753
+ else if (v.type === 4) parts.push(",");
5754
+ else if (v.type === 6) parts.push(v.value);
5755
+ else if (v.type === 15) parts.push(`${v.number}${v.unit}`);
5740
5756
  else if (v.type === 17) parts.push(`${v.number}`);
5741
5757
  else if (v.type === 16) parts.push(`${v.number}%`);
5742
5758
  else if (v.type === 20) parts.push(v.value);
5743
5759
  else if (v.type === 5) parts.push(`#${v.value}`);
5744
5760
  }
5745
- return parts.join(" ");
5761
+ return parts.join("").trim();
5746
5762
  };
5747
5763
  //#endregion
5748
5764
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8292,7 +8308,7 @@ const parseTree = (context, element) => {
8292
8308
  return container;
8293
8309
  };
8294
8310
  const createsRealStackingContext = (node, container, root) => {
8295
- return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8311
+ return container.styles.isPositionedWithZIndex() || container.styles.opacity < 1 || Boolean(container.styles.filter) || container.styles.isTransformed() || isBodyElement(node) && root.styles.isTransparent();
8296
8312
  };
8297
8313
  const createsStackingContext = (styles) => {
8298
8314
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8729,12 +8745,13 @@ var ElementPaint = class {
8729
8745
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8730
8746
  }
8731
8747
  }
8732
- getEffects(target) {
8748
+ getEffects(target, surfaceRoot) {
8733
8749
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8734
- let parent = this.parent;
8735
- const effects = this.effects.slice(0);
8750
+ let parent = this === surfaceRoot ? null : this.parent;
8751
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
8752
+ const effects = sourceEffects(this);
8736
8753
  while (parent) {
8737
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8754
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8738
8755
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8739
8756
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8740
8757
  if (hasOverflowClip(parent.container.styles)) {
@@ -8744,7 +8761,7 @@ var ElementPaint = class {
8744
8761
  }
8745
8762
  effects.unshift(...croplessEffects);
8746
8763
  } else effects.unshift(...croplessEffects);
8747
- parent = parent.parent;
8764
+ parent = parent === surfaceRoot ? null : parent.parent;
8748
8765
  }
8749
8766
  return effects.filter((effect) => contains(effect.target, target));
8750
8767
  }
@@ -9025,6 +9042,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
9025
9042
  }
9026
9043
  };
9027
9044
  //#endregion
9045
+ //#region src/core/abort-helper.ts
9046
+ /**
9047
+ * AbortSignal helper utilities.
9048
+ */
9049
+ /**
9050
+ * Throw a DOMException if the given AbortSignal is already aborted.
9051
+ * Used at checkpoints throughout the rendering pipeline to support
9052
+ * cancellable renders.
9053
+ *
9054
+ * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
9055
+ * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
9056
+ */
9057
+ const throwIfAborted = (signal) => {
9058
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
9059
+ };
9060
+ //#endregion
9061
+ //#region src/render/canvas/filter-surface.ts
9062
+ var FilterSurfaceError = class extends Error {};
9063
+ const releaseSurface = (canvas) => {
9064
+ canvas.width = canvas.height = 0;
9065
+ };
9066
+ function parseSimpleFilter(value) {
9067
+ if (!value || value === "none") return { blur: 0 };
9068
+ const effect = new FilterEffect(value);
9069
+ const shadows = value.match(/drop-shadow\(/g) || [];
9070
+ if (shadows.length > 1 || shadows.length === 1 && !effect.shadow) return null;
9071
+ const blur = effect.safeFilterString.match(/^blur\((\d+(?:\.\d+)?)px\)$/);
9072
+ if (effect.safeFilterString && !blur) return null;
9073
+ if (blur && shadows.length && value.indexOf("blur(") > value.indexOf("drop-shadow(")) return null;
9074
+ return {
9075
+ blur: blur ? Number(blur[1]) : 0,
9076
+ shadow: effect.shadow ? {
9077
+ x: effect.shadow.offsetX,
9078
+ y: effect.shadow.offsetY,
9079
+ blur: effect.shadow.blur,
9080
+ color: effect.shadow.color
9081
+ } : void 0
9082
+ };
9083
+ }
9084
+ function filterOutset(filter) {
9085
+ return Math.ceil(3 * (filter.blur + (filter.shadow?.blur ?? 0)) + Math.max(Math.abs(filter.shadow?.x ?? 0), Math.abs(filter.shadow?.y ?? 0)));
9086
+ }
9087
+ function canvasLike(source) {
9088
+ const canvas = document.createElement("canvas");
9089
+ canvas.width = source.width;
9090
+ canvas.height = source.height;
9091
+ return canvas;
9092
+ }
9093
+ const decodeSurface = (image, signal) => new Promise((resolve, reject) => {
9094
+ const finish = (error) => {
9095
+ clearTimeout(timer);
9096
+ signal?.removeEventListener("abort", abort);
9097
+ if (error) reject(error);
9098
+ else resolve();
9099
+ };
9100
+ const abort = () => finish(new DOMException("The operation was aborted.", "AbortError"));
9101
+ const timer = setTimeout(() => finish(new FilterSurfaceError("Filter surface image decode timed out")), 1e4);
9102
+ signal?.addEventListener("abort", abort, { once: true });
9103
+ if (signal?.aborted) abort();
9104
+ else image.decode().then(() => finish(), () => finish(new FilterSurfaceError("Filter surface image decode failed")));
9105
+ });
9106
+ const canvasFilterSupport = /* @__PURE__ */ new WeakMap();
9107
+ function supportsNativeFilters(owner = document) {
9108
+ const cached = canvasFilterSupport.get(owner);
9109
+ if (cached !== void 0) return cached;
9110
+ const source = owner.createElement("canvas");
9111
+ const target = owner.createElement("canvas");
9112
+ let supported = false;
9113
+ try {
9114
+ source.width = source.height = target.width = target.height = 32;
9115
+ const input = source.getContext("2d");
9116
+ const context = target.getContext("2d");
9117
+ if (input && context && "filter" in context) {
9118
+ input.fillStyle = "#fff";
9119
+ input.fillRect(8, 8, 8, 8);
9120
+ context.filter = "blur(2px)";
9121
+ context.drawImage(source, 0, 0);
9122
+ const blurred = context.getImageData(6, 12, 1, 1).data[3] > 0;
9123
+ context.clearRect(0, 0, 32, 32);
9124
+ context.filter = "drop-shadow(12px 0px 0px rgba(255, 0, 0, 0.5))";
9125
+ context.drawImage(source, 0, 0);
9126
+ const shadow = context.getImageData(24, 12, 1, 1).data;
9127
+ supported = blurred && shadow[0] > 240 && shadow[1] < 10 && shadow[3] >= 126 && shadow[3] <= 129;
9128
+ }
9129
+ } catch {} finally {
9130
+ releaseSurface(source);
9131
+ releaseSurface(target);
9132
+ }
9133
+ canvasFilterSupport.set(owner, supported);
9134
+ return supported;
9135
+ }
9136
+ function tryNativeFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9137
+ let filtered;
9138
+ let output;
9139
+ let complete = false;
9140
+ try {
9141
+ throwIfAborted(signal);
9142
+ filtered = canvasLike(source);
9143
+ const context = filtered.getContext("2d");
9144
+ if (!context || !("filter" in context)) return null;
9145
+ const functions = [];
9146
+ if (blur) functions.push(`blur(${blur * scale}px)`);
9147
+ if (shadow) functions.push(`drop-shadow(${shadow.x * scale}px ${shadow.y * scale}px ${shadow.blur * scale}px ${shadow.color})`);
9148
+ context.filter = functions.join(" ");
9149
+ if (context.filter === "none") return null;
9150
+ context.drawImage(source, 0, 0);
9151
+ throwIfAborted(signal);
9152
+ output = canvasLike(source);
9153
+ const composite = output.getContext("2d");
9154
+ if (!composite) return null;
9155
+ composite.globalAlpha = opacity;
9156
+ composite.drawImage(filtered, 0, 0);
9157
+ throwIfAborted(signal);
9158
+ complete = true;
9159
+ return output;
9160
+ } catch {
9161
+ throwIfAborted(signal);
9162
+ return null;
9163
+ } finally {
9164
+ if (filtered) releaseSurface(filtered);
9165
+ if (output && !complete) releaseSurface(output);
9166
+ }
9167
+ }
9168
+ async function renderFilterSurface(source, filter, opacity, scale, signal) {
9169
+ throwIfAborted(signal);
9170
+ if ((filter.blur || filter.shadow) && supportsNativeFilters()) {
9171
+ const output = tryNativeFilterSurface(source, filter, opacity, scale, signal);
9172
+ if (output) return output;
9173
+ }
9174
+ return renderSvgFilterSurface(source, filter, opacity, scale, signal);
9175
+ }
9176
+ async function renderSvgFilterSurface(source, { blur, shadow }, opacity, scale, signal) {
9177
+ throwIfAborted(signal);
9178
+ if (!blur && !shadow) {
9179
+ const output = canvasLike(source);
9180
+ const context = output.getContext("2d");
9181
+ if (!context) {
9182
+ releaseSurface(output);
9183
+ throw new FilterSurfaceError("Filter surface canvas is unavailable");
9184
+ }
9185
+ context.globalAlpha = opacity;
9186
+ try {
9187
+ context.drawImage(source, 0, 0);
9188
+ return output;
9189
+ } catch {
9190
+ releaseSurface(output);
9191
+ throw new FilterSurfaceError("Filter surface image cannot be drawn");
9192
+ }
9193
+ }
9194
+ const namespace = "http://www.w3.org/2000/svg";
9195
+ const element = (tag, attributes = {}) => {
9196
+ const node = document.createElementNS(namespace, tag);
9197
+ Object.entries(attributes).forEach(([key, value]) => node.setAttribute(key, String(value)));
9198
+ return node;
9199
+ };
9200
+ const svg = element("svg", {
9201
+ width: source.width,
9202
+ height: source.height,
9203
+ viewBox: `0 0 ${source.width} ${source.height}`
9204
+ });
9205
+ const defs = element("defs");
9206
+ const filter = element("filter", {
9207
+ id: "composited-filter",
9208
+ filterUnits: "userSpaceOnUse",
9209
+ x: 0,
9210
+ y: 0,
9211
+ width: source.width,
9212
+ height: source.height,
9213
+ "color-interpolation-filters": "sRGB"
9214
+ });
9215
+ if (blur) filter.appendChild(element("feGaussianBlur", {
9216
+ stdDeviation: blur * scale,
9217
+ result: "blurred-source"
9218
+ }));
9219
+ if (shadow) {
9220
+ filter.appendChild(element("feGaussianBlur", {
9221
+ in: blur ? "blurred-source" : "SourceAlpha",
9222
+ stdDeviation: shadow.blur * scale
9223
+ }));
9224
+ filter.appendChild(element("feOffset", {
9225
+ dx: shadow.x * scale,
9226
+ dy: shadow.y * scale,
9227
+ result: "offset-shadow"
9228
+ }));
9229
+ filter.appendChild(element("feFlood", { "flood-color": shadow.color }));
9230
+ filter.appendChild(element("feComposite", {
9231
+ in2: "offset-shadow",
9232
+ operator: "in"
9233
+ }));
9234
+ const merge = element("feMerge");
9235
+ merge.appendChild(element("feMergeNode"));
9236
+ merge.appendChild(element("feMergeNode", { in: blur ? "blurred-source" : "SourceGraphic" }));
9237
+ filter.appendChild(merge);
9238
+ }
9239
+ defs.appendChild(filter);
9240
+ svg.appendChild(defs);
9241
+ let sourceUrl;
9242
+ try {
9243
+ sourceUrl = source.toDataURL("image/png");
9244
+ if (sourceUrl === "data:,") throw new Error("Canvas size is unsupported");
9245
+ } catch {
9246
+ throw new FilterSurfaceError("Filter surface pixels cannot be serialized");
9247
+ }
9248
+ svg.appendChild(element("image", {
9249
+ width: source.width,
9250
+ height: source.height,
9251
+ href: sourceUrl,
9252
+ filter: "url(#composited-filter)"
9253
+ }));
9254
+ const image = new Image();
9255
+ let output;
9256
+ try {
9257
+ image.src = `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
9258
+ await decodeSurface(image, signal);
9259
+ throwIfAborted(signal);
9260
+ output = canvasLike(source);
9261
+ const context = output.getContext("2d");
9262
+ if (!context) throw new FilterSurfaceError("Filter surface canvas is unavailable");
9263
+ context.globalAlpha = opacity;
9264
+ context.drawImage(image, 0, 0);
9265
+ return output;
9266
+ } catch (error) {
9267
+ if (output) releaseSurface(output);
9268
+ if (signal?.aborted) throwIfAborted(signal);
9269
+ throw error instanceof FilterSurfaceError ? error : new FilterSurfaceError("Filter surface image cannot be drawn");
9270
+ } finally {
9271
+ image.removeAttribute("src");
9272
+ }
9273
+ }
9274
+ function reserveSurface(budget, width, height) {
9275
+ const pixels = width * height * 4;
9276
+ if (!Number.isFinite(pixels) || width <= 0 || height <= 0 || width > 8192 || height > 8192 || budget.pixels + pixels > 16777216) return 0;
9277
+ budget.pixels += pixels;
9278
+ return pixels;
9279
+ }
9280
+ const expand = (b, margin) => b.add(-margin, -margin, margin * 2, margin * 2);
9281
+ 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));
9282
+ function surfaceBounds(container) {
9283
+ const styles = container.styles;
9284
+ let bounds = container.bounds;
9285
+ for (const shadow of styles.boxShadow) if (!shadow.inset) {
9286
+ const spread = Math.max(0, shadow.spread.number) + 3 * shadow.blur.number;
9287
+ bounds = union(bounds, expand(container.bounds.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), spread));
9288
+ }
9289
+ for (const text of container.textNodes) for (const part of text.textBounds) {
9290
+ const ink = expand(part.bounds, 2 * getAbsoluteValue(styles.fontSize, container.bounds.height) + styles.webkitTextStrokeWidth);
9291
+ bounds = union(bounds, ink);
9292
+ for (const shadow of styles.textShadow) bounds = union(bounds, expand(ink.add(shadow.offsetX.number, shadow.offsetY.number, 0, 0), 3 * shadow.blur.number));
9293
+ }
9294
+ for (const child of container.elements) if (child.styles.isVisible()) bounds = union(bounds, surfaceBounds(child));
9295
+ const filter = parseSimpleFilter(styles.filter);
9296
+ return expand(bounds, filter ? filterOutset(filter) : 0);
9297
+ }
9298
+ function cropSurface(bounds, viewport, margin, scale) {
9299
+ const area = expand(viewport, margin);
9300
+ const left = Math.floor(Math.max(bounds.left, area.left) * scale) / scale;
9301
+ const top = Math.floor(Math.max(bounds.top, area.top) * scale) / scale;
9302
+ const right = Math.ceil(Math.min(bounds.left + bounds.width, area.left + area.width) * scale) / scale;
9303
+ const bottom = Math.ceil(Math.min(bounds.top + bounds.height, area.top + area.height) * scale) / scale;
9304
+ return new Bounds(left, top, Math.max(0, right - left), Math.max(0, bottom - top));
9305
+ }
9306
+ //#endregion
9028
9307
  //#region src/render/canvas/text/text-decoration-renderer.ts
9029
9308
  var TextDecorationRenderer = class {
9030
9309
  constructor(ctx) {
@@ -9154,8 +9433,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
9154
9433
  const TEXT_OVERFLOW_EPSILON = 1;
9155
9434
  /**
9156
9435
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9157
- * CJK characters use the ideographic baseline in browsers, which differs
9158
- * from the alphabetic baseline used for Latin script.
9436
+ * Vertical writing modes use it to keep CJK glyphs upright and on the
9437
+ * ideographic baseline while other scripts are rotated.
9159
9438
  *
9160
9439
  * Covers:
9161
9440
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9225,15 +9504,17 @@ var TextRenderer = class {
9225
9504
  return w;
9226
9505
  }
9227
9506
  /**
9228
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9229
- * per-script baseline for each character.
9507
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9230
9508
  *
9231
9509
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9232
- * character. This helper centralises two fixes applied during that iteration:
9233
- * 1. Add `letterSpacing` to each character's advance width (was previously
9234
- * omitted, causing characters to render without any spacing).
9235
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9236
- * position matches how browsers lay them out in the DOM.
9510
+ * character, adding `letterSpacing` to each character's advance width.
9511
+ *
9512
+ * In horizontal text every glyph keeps the alphabetic baseline, CJK included:
9513
+ * inline layout aligns a whole line on its alphabetic baseline regardless of
9514
+ * script, and `baseline` was measured for that. Drawing CJK graphemes at the
9515
+ * same y with `textBaseline = 'ideographic'` lifts them above the Latin text
9516
+ * next to them, and away from where the `letterSpacing === 0` whole-string
9517
+ * path draws them.
9237
9518
  *
9238
9519
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9239
9520
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9249,12 +9530,7 @@ var TextRenderer = class {
9249
9530
  const fontString = this.ctx.font;
9250
9531
  let left = text.bounds.left;
9251
9532
  for (const letter of letters) {
9252
- if (hasCJKCharacters(letter)) {
9253
- const savedBaseline = this.ctx.textBaseline;
9254
- this.ctx.textBaseline = "ideographic";
9255
- renderFn(letter, left, y);
9256
- this.ctx.textBaseline = savedBaseline;
9257
- } else renderFn(letter, left, y);
9533
+ renderFn(letter, left, y);
9258
9534
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9259
9535
  }
9260
9536
  }
@@ -9296,7 +9572,7 @@ var TextRenderer = class {
9296
9572
  /**
9297
9573
  * Render text with letter-spacing applied (fill pass).
9298
9574
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9299
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9575
+ * grapheme is drawn individually so spacing is applied correctly.
9300
9576
  */
9301
9577
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9302
9578
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10531,12 +10807,19 @@ const canvasTextAlign = (textAlign) => {
10531
10807
  //#endregion
10532
10808
  //#region src/render/canvas/canvas-renderer.ts
10533
10809
  var CanvasRenderer = class CanvasRenderer {
10534
- constructor(context, options) {
10810
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10811
+ this.surfaceRoot = surfaceRoot;
10812
+ this.surfaceBudget = surfaceBudget;
10813
+ this.legacySubtree = false;
10535
10814
  this.context = context;
10536
10815
  this.options = options;
10537
10816
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10538
10817
  const ctx = this.canvas.getContext("2d");
10539
- if (!ctx) throw new Error("Failed to get 2D rendering context from canvas");
10818
+ if (!ctx) {
10819
+ if (!options.canvas) releaseSurface(this.canvas);
10820
+ if (surfaceRoot) throw new FilterSurfaceError("Filter surface canvas is unavailable");
10821
+ throw new Error("Failed to get 2D rendering context from canvas");
10822
+ }
10540
10823
  this.ctx = ctx;
10541
10824
  if (!options.canvas) {
10542
10825
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10572,7 +10855,77 @@ var CanvasRenderer = class CanvasRenderer {
10572
10855
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10573
10856
  }
10574
10857
  async renderStack(stack) {
10575
- if (stack.element.container.styles.isVisible()) await this.renderStackContent(stack);
10858
+ const styles = stack.element.container.styles;
10859
+ if (styles.isVisible()) {
10860
+ const filter = parseSimpleFilter(styles.filter);
10861
+ if (stack.element !== this.surfaceRoot && !this.legacySubtree && filter && (styles.filter || styles.opacity < 1) && this.canComposite(stack.element)) {
10862
+ if (!await this.renderCompositedStack(stack, filter)) {
10863
+ this.legacySubtree = true;
10864
+ try {
10865
+ await this.renderStackContent(stack);
10866
+ } finally {
10867
+ this.legacySubtree = false;
10868
+ }
10869
+ }
10870
+ return;
10871
+ }
10872
+ await this.renderStackContent(stack);
10873
+ }
10874
+ }
10875
+ canComposite(paint) {
10876
+ const supported = (container) => {
10877
+ const styles = container.styles;
10878
+ 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;
10879
+ };
10880
+ const subtree = (container) => supported(container) && container.elements.every(subtree);
10881
+ let ancestor = paint.parent;
10882
+ while (ancestor && ancestor !== this.surfaceRoot) {
10883
+ if (!supported(ancestor.container)) return false;
10884
+ ancestor = ancestor.parent;
10885
+ }
10886
+ return subtree(paint.container);
10887
+ }
10888
+ async renderCompositedStack(stack, filter) {
10889
+ const signal = this.options.signal;
10890
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10891
+ 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);
10892
+ if (!bounds.width || !bounds.height) return true;
10893
+ const reserved = reserveSurface(this.surfaceBudget, Math.ceil(bounds.width * this.options.scale), Math.ceil(bounds.height * this.options.scale));
10894
+ if (!reserved) {
10895
+ this.context.logger.info("Filter surface budget exceeded; using the existing renderer");
10896
+ return false;
10897
+ }
10898
+ const options = {
10899
+ ...this.options,
10900
+ canvas: void 0,
10901
+ backgroundColor: null,
10902
+ x: bounds.left,
10903
+ y: bounds.top,
10904
+ width: bounds.width,
10905
+ height: bounds.height
10906
+ };
10907
+ let source;
10908
+ let filtered;
10909
+ try {
10910
+ source = new CanvasRenderer(this.context, options, stack.element, this.surfaceBudget);
10911
+ await source.renderStackContent(stack);
10912
+ source.effectsRenderer.applyEffects([]);
10913
+ filtered = await renderFilterSurface(source.canvas, filter, stack.element.container.styles.opacity, options.scale, signal);
10914
+ if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
10915
+ const own = stack.element.effects;
10916
+ const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
10917
+ this.effectsRenderer.applyEffects(effects);
10918
+ this.ctx.drawImage(filtered, options.x, options.y, filtered.width / options.scale, filtered.height / options.scale);
10919
+ return true;
10920
+ } catch (error) {
10921
+ if (!(error instanceof FilterSurfaceError)) throw error;
10922
+ this.context.logger.info(`${error.message}; using the existing renderer`);
10923
+ return false;
10924
+ } finally {
10925
+ if (source) releaseSurface(source.canvas);
10926
+ if (filtered) releaseSurface(filtered);
10927
+ this.surfaceBudget.pixels -= reserved;
10928
+ }
10576
10929
  }
10577
10930
  async renderNode(paint) {
10578
10931
  if (paint.container.debugRender) debugger;
@@ -10600,7 +10953,7 @@ var CanvasRenderer = class CanvasRenderer {
10600
10953
  }
10601
10954
  }
10602
10955
  async renderNodeContent(paint) {
10603
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10956
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10604
10957
  const container = paint.container;
10605
10958
  const curves = paint.curves;
10606
10959
  const styles = container.styles;
@@ -10664,8 +11017,51 @@ var CanvasRenderer = class CanvasRenderer {
10664
11017
  formatPath(paths) {
10665
11018
  formatCanvasPath(this.ctx, paths);
10666
11019
  }
11020
+ async renderSurfaceBoxShadow(paint, shadow) {
11021
+ const spread = shadow.spread.number;
11022
+ const blur = shadow.blur.number / 2;
11023
+ const margin = Math.ceil(3 * blur) + 1;
11024
+ const box = paint.container.bounds.add(-spread, -spread, 2 * spread, 2 * spread);
11025
+ if (box.width <= 0 || box.height <= 0) return;
11026
+ const scale = this.options.scale;
11027
+ 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);
11028
+ if (!bounds.width || !bounds.height) return;
11029
+ const width = Math.ceil(bounds.width * scale), height = Math.ceil(bounds.height * scale);
11030
+ const reserved = reserveSurface(this.surfaceBudget, width, height);
11031
+ if (!reserved) throw new FilterSurfaceError("Box shadow surface budget exceeded");
11032
+ let source;
11033
+ let filtered;
11034
+ try {
11035
+ source = document.createElement("canvas");
11036
+ source.width = width;
11037
+ source.height = height;
11038
+ const ctx = source.getContext("2d");
11039
+ if (!ctx) throw new FilterSurfaceError("Box shadow surface canvas is unavailable");
11040
+ ctx.scale(scale, scale);
11041
+ ctx.translate(-bounds.left, -bounds.top);
11042
+ createCanvasPath(ctx, transformPath(calculateBorderBoxPath(paint.curves), -spread, -spread, 2 * spread, 2 * spread));
11043
+ ctx.fillStyle = asString(shadow.color);
11044
+ ctx.fill();
11045
+ filtered = await renderFilterSurface(source, { blur }, 1, scale, this.options.signal);
11046
+ this.ctx.save();
11047
+ try {
11048
+ this.ctx.beginPath();
11049
+ this.ctx.rect(this.options.x, this.options.y, this.options.width, this.options.height);
11050
+ formatCanvasPath(this.ctx, calculateBorderBoxPath(paint.curves));
11051
+ this.ctx.closePath();
11052
+ this.ctx.clip("evenodd");
11053
+ this.ctx.drawImage(filtered, bounds.left + shadow.offsetX.number, bounds.top + shadow.offsetY.number, filtered.width / scale, filtered.height / scale);
11054
+ } finally {
11055
+ this.ctx.restore();
11056
+ }
11057
+ } finally {
11058
+ if (source) releaseSurface(source);
11059
+ if (filtered) releaseSurface(filtered);
11060
+ this.surfaceBudget.pixels -= reserved;
11061
+ }
11062
+ }
10667
11063
  async renderNodeBackgroundAndBorders(paint) {
10668
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11064
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10669
11065
  const styles = paint.container.styles;
10670
11066
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10671
11067
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10706,7 +11102,11 @@ var CanvasRenderer = class CanvasRenderer {
10706
11102
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10707
11103
  this.ctx.restore();
10708
11104
  }
10709
- styles.boxShadow.slice(0).reverse().forEach((shadow) => {
11105
+ for (const shadow of styles.boxShadow.slice(0).reverse()) {
11106
+ if (this.surfaceRoot && !shadow.inset) {
11107
+ await this.renderSurfaceBoxShadow(paint, shadow);
11108
+ continue;
11109
+ }
10710
11110
  this.ctx.save();
10711
11111
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10712
11112
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10720,14 +11120,15 @@ var CanvasRenderer = class CanvasRenderer {
10720
11120
  this.ctx.clip();
10721
11121
  this.path(shadowPaintingArea);
10722
11122
  }
10723
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10724
- this.ctx.shadowOffsetY = shadow.offsetY.number;
11123
+ const shadowScale = this.surfaceRoot ? this.options.scale : 1;
11124
+ this.ctx.shadowOffsetX = (shadow.offsetX.number + maskOffset) * shadowScale;
11125
+ this.ctx.shadowOffsetY = shadow.offsetY.number * shadowScale;
10725
11126
  this.ctx.shadowColor = asString(shadow.color);
10726
- this.ctx.shadowBlur = shadow.blur.number;
11127
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10727
11128
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10728
11129
  this.ctx.fill();
10729
11130
  this.ctx.restore();
10730
- });
11131
+ }
10731
11132
  }
10732
11133
  if (styles.borderImageSource) {
10733
11134
  const source = styles.borderImageSource;
@@ -10783,14 +11184,19 @@ var CanvasRenderer = class CanvasRenderer {
10783
11184
  }
10784
11185
  }
10785
11186
  async render(element) {
10786
- if (this.options.backgroundColor) {
10787
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10788
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11187
+ try {
11188
+ if (this.options.backgroundColor) {
11189
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
11190
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
11191
+ }
11192
+ const stack = parseStackingContexts(element);
11193
+ await this.renderStack(stack);
11194
+ this.effectsRenderer.applyEffects([]);
11195
+ return this.canvas;
11196
+ } catch (error) {
11197
+ if (!this.options.canvas) releaseSurface(this.canvas);
11198
+ throw error;
10789
11199
  }
10790
- const stack = parseStackingContexts(element);
10791
- await this.renderStack(stack);
10792
- this.effectsRenderer.applyEffects([]);
10793
- return this.canvas;
10794
11200
  }
10795
11201
  /**
10796
11202
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11206,22 +11612,6 @@ var Context = class Context {
11206
11612
  }
11207
11613
  };
11208
11614
  //#endregion
11209
- //#region src/core/abort-helper.ts
11210
- /**
11211
- * AbortSignal helper utilities.
11212
- */
11213
- /**
11214
- * Throw a DOMException if the given AbortSignal is already aborted.
11215
- * Used at checkpoints throughout the rendering pipeline to support
11216
- * cancellable renders.
11217
- *
11218
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11219
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11220
- */
11221
- const throwIfAborted = (signal) => {
11222
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11223
- };
11224
- //#endregion
11225
11615
  //#region src/core/config-assembler.ts
11226
11616
  /**
11227
11617
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11390,18 +11780,19 @@ const renderElement = async (element, opts, config) => {
11390
11780
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11391
11781
  perfMonitor.end("render");
11392
11782
  }
11783
+ } finally {
11393
11784
  perfMonitor.start("cleanup");
11394
- if (opts.removeContainer ?? true) {
11395
- if (!DocumentCloner.destroy(container)) context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
11785
+ try {
11786
+ if (root) root.restoreTree();
11787
+ } finally {
11788
+ if (opts.removeContainer ?? true) DocumentCloner.destroy(container);
11789
+ perfMonitor.end("cleanup");
11396
11790
  }
11397
- perfMonitor.end("cleanup");
11398
- perfMonitor.end("total");
11399
- context.logger.debug(`Finished rendering`);
11400
- if (performanceMonitoring) perfMonitor.logSummary();
11401
- return canvas;
11402
- } finally {
11403
- if (root) root.restoreTree();
11404
11791
  }
11792
+ perfMonitor.end("total");
11793
+ context.logger.debug(`Finished rendering`);
11794
+ if (performanceMonitoring) perfMonitor.logSummary();
11795
+ return canvas;
11405
11796
  };
11406
11797
  //#endregion
11407
11798
  //#region src/index.ts