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
  */
@@ -5328,10 +5328,15 @@ const fontFamily = {
5328
5328
  parse: (_context, tokens) => {
5329
5329
  const accumulator = [];
5330
5330
  const results = [];
5331
+ const quoted = [];
5332
+ let isQuoted = false;
5331
5333
  tokens.forEach((token) => {
5332
5334
  switch (token.type) {
5333
5335
  case 20:
5336
+ accumulator.push(token.value);
5337
+ break;
5334
5338
  case 0:
5339
+ isQuoted = true;
5335
5340
  accumulator.push(token.value);
5336
5341
  break;
5337
5342
  case 17:
@@ -5339,12 +5344,17 @@ const fontFamily = {
5339
5344
  break;
5340
5345
  case 4:
5341
5346
  results.push(accumulator.join(" "));
5347
+ quoted.push(isQuoted);
5348
+ isQuoted = false;
5342
5349
  accumulator.length = 0;
5343
5350
  break;
5344
5351
  }
5345
5352
  });
5346
- if (accumulator.length) results.push(accumulator.join(" "));
5347
- return results.map((result) => result.indexOf(" ") === -1 ? result : `'${result}'`);
5353
+ if (accumulator.length) {
5354
+ results.push(accumulator.join(" "));
5355
+ quoted.push(isQuoted);
5356
+ }
5357
+ 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);
5348
5358
  }
5349
5359
  };
5350
5360
  //#endregion
@@ -5707,7 +5717,7 @@ const filter = {
5707
5717
  const renderedArgs = renderFilterArgs(fn.values);
5708
5718
  switch (fn.name) {
5709
5719
  case "blur":
5710
- parts.push(`blur(${renderedArgs}px)`);
5720
+ parts.push(`blur(${renderedArgs})`);
5711
5721
  break;
5712
5722
  case "brightness":
5713
5723
  case "contrast":
@@ -5721,7 +5731,7 @@ const filter = {
5721
5731
  parts.push(`grayscale(${renderedArgs})`);
5722
5732
  break;
5723
5733
  case "hue-rotate":
5724
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5734
+ parts.push(`hue-rotate(${renderedArgs})`);
5725
5735
  break;
5726
5736
  case "drop-shadow":
5727
5737
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5739,14 +5749,20 @@ const filter = {
5739
5749
  const renderFilterArgs = (values) => {
5740
5750
  const parts = [];
5741
5751
  for (const v of values) {
5742
- if (v.type === 31) continue;
5743
- 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}`);
5744
5760
  else if (v.type === 17) parts.push(`${v.number}`);
5745
5761
  else if (v.type === 16) parts.push(`${v.number}%`);
5746
5762
  else if (v.type === 20) parts.push(v.value);
5747
5763
  else if (v.type === 5) parts.push(`#${v.value}`);
5748
5764
  }
5749
- return parts.join(" ");
5765
+ return parts.join("").trim();
5750
5766
  };
5751
5767
  //#endregion
5752
5768
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8296,7 +8312,7 @@ const parseTree = (context, element) => {
8296
8312
  return container;
8297
8313
  };
8298
8314
  const createsRealStackingContext = (node, container, root) => {
8299
- 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();
8300
8316
  };
8301
8317
  const createsStackingContext = (styles) => {
8302
8318
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8733,12 +8749,13 @@ var ElementPaint = class {
8733
8749
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8734
8750
  }
8735
8751
  }
8736
- getEffects(target) {
8752
+ getEffects(target, surfaceRoot) {
8737
8753
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8738
- let parent = this.parent;
8739
- 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);
8740
8757
  while (parent) {
8741
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8758
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8742
8759
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8743
8760
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8744
8761
  if (hasOverflowClip(parent.container.styles)) {
@@ -8748,7 +8765,7 @@ var ElementPaint = class {
8748
8765
  }
8749
8766
  effects.unshift(...croplessEffects);
8750
8767
  } else effects.unshift(...croplessEffects);
8751
- parent = parent.parent;
8768
+ parent = parent === surfaceRoot ? null : parent.parent;
8752
8769
  }
8753
8770
  return effects.filter((effect) => contains(effect.target, target));
8754
8771
  }
@@ -9029,6 +9046,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
9029
9046
  }
9030
9047
  };
9031
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
9032
9311
  //#region src/render/canvas/text/text-decoration-renderer.ts
9033
9312
  var TextDecorationRenderer = class {
9034
9313
  constructor(ctx) {
@@ -9158,8 +9437,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
9158
9437
  const TEXT_OVERFLOW_EPSILON = 1;
9159
9438
  /**
9160
9439
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9161
- * CJK characters use the ideographic baseline in browsers, which differs
9162
- * 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.
9163
9442
  *
9164
9443
  * Covers:
9165
9444
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9229,15 +9508,17 @@ var TextRenderer = class {
9229
9508
  return w;
9230
9509
  }
9231
9510
  /**
9232
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9233
- * per-script baseline for each character.
9511
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9234
9512
  *
9235
9513
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9236
- * character. This helper centralises two fixes applied during that iteration:
9237
- * 1. Add `letterSpacing` to each character's advance width (was previously
9238
- * omitted, causing characters to render without any spacing).
9239
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9240
- * 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.
9241
9522
  *
9242
9523
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9243
9524
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9253,12 +9534,7 @@ var TextRenderer = class {
9253
9534
  const fontString = this.ctx.font;
9254
9535
  let left = text.bounds.left;
9255
9536
  for (const letter of letters) {
9256
- if (hasCJKCharacters(letter)) {
9257
- const savedBaseline = this.ctx.textBaseline;
9258
- this.ctx.textBaseline = "ideographic";
9259
- renderFn(letter, left, y);
9260
- this.ctx.textBaseline = savedBaseline;
9261
- } else renderFn(letter, left, y);
9537
+ renderFn(letter, left, y);
9262
9538
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9263
9539
  }
9264
9540
  }
@@ -9300,7 +9576,7 @@ var TextRenderer = class {
9300
9576
  /**
9301
9577
  * Render text with letter-spacing applied (fill pass).
9302
9578
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9303
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9579
+ * grapheme is drawn individually so spacing is applied correctly.
9304
9580
  */
9305
9581
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9306
9582
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10535,12 +10811,19 @@ const canvasTextAlign = (textAlign) => {
10535
10811
  //#endregion
10536
10812
  //#region src/render/canvas/canvas-renderer.ts
10537
10813
  var CanvasRenderer = class CanvasRenderer {
10538
- constructor(context, options) {
10814
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10815
+ this.surfaceRoot = surfaceRoot;
10816
+ this.surfaceBudget = surfaceBudget;
10817
+ this.legacySubtree = false;
10539
10818
  this.context = context;
10540
10819
  this.options = options;
10541
10820
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10542
10821
  const ctx = this.canvas.getContext("2d");
10543
- 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
+ }
10544
10827
  this.ctx = ctx;
10545
10828
  if (!options.canvas) {
10546
10829
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10576,7 +10859,77 @@ var CanvasRenderer = class CanvasRenderer {
10576
10859
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10577
10860
  }
10578
10861
  async renderStack(stack) {
10579
- 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
+ }
10580
10933
  }
10581
10934
  async renderNode(paint) {
10582
10935
  if (paint.container.debugRender) debugger;
@@ -10604,7 +10957,7 @@ var CanvasRenderer = class CanvasRenderer {
10604
10957
  }
10605
10958
  }
10606
10959
  async renderNodeContent(paint) {
10607
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10960
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10608
10961
  const container = paint.container;
10609
10962
  const curves = paint.curves;
10610
10963
  const styles = container.styles;
@@ -10668,8 +11021,51 @@ var CanvasRenderer = class CanvasRenderer {
10668
11021
  formatPath(paths) {
10669
11022
  formatCanvasPath(this.ctx, paths);
10670
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
+ }
10671
11067
  async renderNodeBackgroundAndBorders(paint) {
10672
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11068
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10673
11069
  const styles = paint.container.styles;
10674
11070
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10675
11071
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10710,7 +11106,11 @@ var CanvasRenderer = class CanvasRenderer {
10710
11106
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10711
11107
  this.ctx.restore();
10712
11108
  }
10713
- 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
+ }
10714
11114
  this.ctx.save();
10715
11115
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10716
11116
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10724,14 +11124,15 @@ var CanvasRenderer = class CanvasRenderer {
10724
11124
  this.ctx.clip();
10725
11125
  this.path(shadowPaintingArea);
10726
11126
  }
10727
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10728
- 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;
10729
11130
  this.ctx.shadowColor = asString(shadow.color);
10730
- this.ctx.shadowBlur = shadow.blur.number;
11131
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10731
11132
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10732
11133
  this.ctx.fill();
10733
11134
  this.ctx.restore();
10734
- });
11135
+ }
10735
11136
  }
10736
11137
  if (styles.borderImageSource) {
10737
11138
  const source = styles.borderImageSource;
@@ -10787,14 +11188,19 @@ var CanvasRenderer = class CanvasRenderer {
10787
11188
  }
10788
11189
  }
10789
11190
  async render(element) {
10790
- if (this.options.backgroundColor) {
10791
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10792
- 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;
10793
11203
  }
10794
- const stack = parseStackingContexts(element);
10795
- await this.renderStack(stack);
10796
- this.effectsRenderer.applyEffects([]);
10797
- return this.canvas;
10798
11204
  }
10799
11205
  /**
10800
11206
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11210,22 +11616,6 @@ var Context = class Context {
11210
11616
  }
11211
11617
  };
11212
11618
  //#endregion
11213
- //#region src/core/abort-helper.ts
11214
- /**
11215
- * AbortSignal helper utilities.
11216
- */
11217
- /**
11218
- * Throw a DOMException if the given AbortSignal is already aborted.
11219
- * Used at checkpoints throughout the rendering pipeline to support
11220
- * cancellable renders.
11221
- *
11222
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11223
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11224
- */
11225
- const throwIfAborted = (signal) => {
11226
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11227
- };
11228
- //#endregion
11229
11619
  //#region src/core/config-assembler.ts
11230
11620
  /**
11231
11621
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11394,18 +11784,19 @@ const renderElement = async (element, opts, config) => {
11394
11784
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11395
11785
  perfMonitor.end("render");
11396
11786
  }
11787
+ } finally {
11397
11788
  perfMonitor.start("cleanup");
11398
- if (opts.removeContainer ?? true) {
11399
- 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");
11400
11794
  }
11401
- perfMonitor.end("cleanup");
11402
- perfMonitor.end("total");
11403
- context.logger.debug(`Finished rendering`);
11404
- if (performanceMonitoring) perfMonitor.logSummary();
11405
- return canvas;
11406
- } finally {
11407
- if (root) root.restoreTree();
11408
11795
  }
11796
+ perfMonitor.end("total");
11797
+ context.logger.debug(`Finished rendering`);
11798
+ if (performanceMonitoring) perfMonitor.logSummary();
11799
+ return canvas;
11409
11800
  };
11410
11801
  //#endregion
11411
11802
  //#region src/index.ts