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
  */
@@ -5713,7 +5713,7 @@ const filter = {
5713
5713
  const renderedArgs = renderFilterArgs(fn.values);
5714
5714
  switch (fn.name) {
5715
5715
  case "blur":
5716
- parts.push(`blur(${renderedArgs}px)`);
5716
+ parts.push(`blur(${renderedArgs})`);
5717
5717
  break;
5718
5718
  case "brightness":
5719
5719
  case "contrast":
@@ -5727,7 +5727,7 @@ const filter = {
5727
5727
  parts.push(`grayscale(${renderedArgs})`);
5728
5728
  break;
5729
5729
  case "hue-rotate":
5730
- parts.push(`hue-rotate(${renderedArgs}deg)`);
5730
+ parts.push(`hue-rotate(${renderedArgs})`);
5731
5731
  break;
5732
5732
  case "drop-shadow":
5733
5733
  parts.push(`drop-shadow(${renderedArgs})`);
@@ -5745,14 +5745,20 @@ const filter = {
5745
5745
  const renderFilterArgs = (values) => {
5746
5746
  const parts = [];
5747
5747
  for (const v of values) {
5748
- if (v.type === 31) continue;
5749
- 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}`);
5750
5756
  else if (v.type === 17) parts.push(`${v.number}`);
5751
5757
  else if (v.type === 16) parts.push(`${v.number}%`);
5752
5758
  else if (v.type === 20) parts.push(v.value);
5753
5759
  else if (v.type === 5) parts.push(`#${v.value}`);
5754
5760
  }
5755
- return parts.join(" ");
5761
+ return parts.join("").trim();
5756
5762
  };
5757
5763
  //#endregion
5758
5764
  //#region src/css/property-descriptors/font-variant-ligatures.ts
@@ -8302,7 +8308,7 @@ const parseTree = (context, element) => {
8302
8308
  return container;
8303
8309
  };
8304
8310
  const createsRealStackingContext = (node, container, root) => {
8305
- 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();
8306
8312
  };
8307
8313
  const createsStackingContext = (styles) => {
8308
8314
  if (styles.isPositioned() || styles.isFloating()) return true;
@@ -8739,12 +8745,13 @@ var ElementPaint = class {
8739
8745
  this.effects.push(new TransformEffect(offsetX, offsetY, zoomMatrix));
8740
8746
  }
8741
8747
  }
8742
- getEffects(target) {
8748
+ getEffects(target, surfaceRoot) {
8743
8749
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
8744
- let parent = this.parent;
8745
- 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);
8746
8753
  while (parent) {
8747
- const croplessEffects = parent.effects.filter((effect) => !isClipEffect(effect));
8754
+ const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
8748
8755
  if (inFlow || parent.container.styles.position !== 0 || !parent.parent) {
8749
8756
  inFlow = [2, 3].indexOf(parent.container.styles.position) === -1;
8750
8757
  if (hasOverflowClip(parent.container.styles)) {
@@ -8754,7 +8761,7 @@ var ElementPaint = class {
8754
8761
  }
8755
8762
  effects.unshift(...croplessEffects);
8756
8763
  } else effects.unshift(...croplessEffects);
8757
- parent = parent.parent;
8764
+ parent = parent === surfaceRoot ? null : parent.parent;
8758
8765
  }
8759
8766
  return effects.filter((effect) => contains(effect.target, target));
8760
8767
  }
@@ -9035,6 +9042,268 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
9035
9042
  }
9036
9043
  };
9037
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
9038
9307
  //#region src/render/canvas/text/text-decoration-renderer.ts
9039
9308
  var TextDecorationRenderer = class {
9040
9309
  constructor(ctx) {
@@ -9164,8 +9433,8 @@ const iOSBrokenFonts = ["-apple-system", "system-ui"];
9164
9433
  const TEXT_OVERFLOW_EPSILON = 1;
9165
9434
  /**
9166
9435
  * Detect CJK (Chinese, Japanese, Korean) characters in a string.
9167
- * CJK characters use the ideographic baseline in browsers, which differs
9168
- * 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.
9169
9438
  *
9170
9439
  * Covers:
9171
9440
  * U+2E80–U+2FFF CJK Radicals Supplement, Kangxi Radicals
@@ -9235,15 +9504,17 @@ var TextRenderer = class {
9235
9504
  return w;
9236
9505
  }
9237
9506
  /**
9238
- * Iterate grapheme clusters one-by-one, applying correct letter-spacing and
9239
- * per-script baseline for each character.
9507
+ * Iterate grapheme clusters one-by-one, applying correct letter-spacing.
9240
9508
  *
9241
9509
  * Issue #73: When letter-spacing is non-zero, text must be rendered character by
9242
- * character. This helper centralises two fixes applied during that iteration:
9243
- * 1. Add `letterSpacing` to each character's advance width (was previously
9244
- * omitted, causing characters to render without any spacing).
9245
- * 2. Switch to the ideographic baseline for CJK glyphs so their vertical
9246
- * 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.
9247
9518
  *
9248
9519
  * The `renderFn` callback receives (letter, x, y) and performs the actual draw
9249
9520
  * call (fillText or strokeText), allowing fill and stroke paths to share one
@@ -9259,12 +9530,7 @@ var TextRenderer = class {
9259
9530
  const fontString = this.ctx.font;
9260
9531
  let left = text.bounds.left;
9261
9532
  for (const letter of letters) {
9262
- if (hasCJKCharacters(letter)) {
9263
- const savedBaseline = this.ctx.textBaseline;
9264
- this.ctx.textBaseline = "ideographic";
9265
- renderFn(letter, left, y);
9266
- this.ctx.textBaseline = savedBaseline;
9267
- } else renderFn(letter, left, y);
9533
+ renderFn(letter, left, y);
9268
9534
  left += this.cachedMeasureText(letter, fontString) + letterSpacing;
9269
9535
  }
9270
9536
  }
@@ -9306,7 +9572,7 @@ var TextRenderer = class {
9306
9572
  /**
9307
9573
  * Render text with letter-spacing applied (fill pass).
9308
9574
  * When letterSpacing is 0 the whole string is drawn in one call; otherwise each
9309
- * grapheme is drawn individually so spacing and CJK baseline are applied correctly.
9575
+ * grapheme is drawn individually so spacing is applied correctly.
9310
9576
  */
9311
9577
  renderTextWithLetterSpacing(text, letterSpacing, baseline, writingMode = 0) {
9312
9578
  this.renderFillText(text, letterSpacing, baseline, writingMode);
@@ -10541,12 +10807,19 @@ const canvasTextAlign = (textAlign) => {
10541
10807
  //#endregion
10542
10808
  //#region src/render/canvas/canvas-renderer.ts
10543
10809
  var CanvasRenderer = class CanvasRenderer {
10544
- constructor(context, options) {
10810
+ constructor(context, options, surfaceRoot, surfaceBudget = { pixels: 0 }) {
10811
+ this.surfaceRoot = surfaceRoot;
10812
+ this.surfaceBudget = surfaceBudget;
10813
+ this.legacySubtree = false;
10545
10814
  this.context = context;
10546
10815
  this.options = options;
10547
10816
  this.canvas = options.canvas ? options.canvas : document.createElement("canvas");
10548
10817
  const ctx = this.canvas.getContext("2d");
10549
- 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
+ }
10550
10823
  this.ctx = ctx;
10551
10824
  if (!options.canvas) {
10552
10825
  this.canvas.width = Math.floor(options.width * options.scale);
@@ -10582,7 +10855,77 @@ var CanvasRenderer = class CanvasRenderer {
10582
10855
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
10583
10856
  }
10584
10857
  async renderStack(stack) {
10585
- 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
+ }
10586
10929
  }
10587
10930
  async renderNode(paint) {
10588
10931
  if (paint.container.debugRender) debugger;
@@ -10610,7 +10953,7 @@ var CanvasRenderer = class CanvasRenderer {
10610
10953
  }
10611
10954
  }
10612
10955
  async renderNodeContent(paint) {
10613
- this.effectsRenderer.applyEffects(paint.getEffects(4));
10956
+ this.effectsRenderer.applyEffects(paint.getEffects(4, this.surfaceRoot));
10614
10957
  const container = paint.container;
10615
10958
  const curves = paint.curves;
10616
10959
  const styles = container.styles;
@@ -10674,8 +11017,51 @@ var CanvasRenderer = class CanvasRenderer {
10674
11017
  formatPath(paths) {
10675
11018
  formatCanvasPath(this.ctx, paths);
10676
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
+ }
10677
11063
  async renderNodeBackgroundAndBorders(paint) {
10678
- this.effectsRenderer.applyEffects(paint.getEffects(2));
11064
+ this.effectsRenderer.applyEffects(paint.getEffects(2, this.surfaceRoot));
10679
11065
  const styles = paint.container.styles;
10680
11066
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10681
11067
  const hasTextClip = hasTextBackgroundClip(styles);
@@ -10716,7 +11102,11 @@ var CanvasRenderer = class CanvasRenderer {
10716
11102
  await this.backgroundRenderer.renderBackgroundImage(paint.container);
10717
11103
  this.ctx.restore();
10718
11104
  }
10719
- 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
+ }
10720
11110
  this.ctx.save();
10721
11111
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
10722
11112
  const maskOffset = shadow.inset ? 0 : SHADOW_MASK_OFFSET;
@@ -10730,14 +11120,15 @@ var CanvasRenderer = class CanvasRenderer {
10730
11120
  this.ctx.clip();
10731
11121
  this.path(shadowPaintingArea);
10732
11122
  }
10733
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
10734
- 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;
10735
11126
  this.ctx.shadowColor = asString(shadow.color);
10736
- this.ctx.shadowBlur = shadow.blur.number;
11127
+ this.ctx.shadowBlur = shadow.blur.number * shadowScale;
10737
11128
  this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : "rgba(0,0,0,1)";
10738
11129
  this.ctx.fill();
10739
11130
  this.ctx.restore();
10740
- });
11131
+ }
10741
11132
  }
10742
11133
  if (styles.borderImageSource) {
10743
11134
  const source = styles.borderImageSource;
@@ -10793,14 +11184,19 @@ var CanvasRenderer = class CanvasRenderer {
10793
11184
  }
10794
11185
  }
10795
11186
  async render(element) {
10796
- if (this.options.backgroundColor) {
10797
- this.ctx.fillStyle = asString(this.options.backgroundColor);
10798
- 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;
10799
11199
  }
10800
- const stack = parseStackingContexts(element);
10801
- await this.renderStack(stack);
10802
- this.effectsRenderer.applyEffects([]);
10803
- return this.canvas;
10804
11200
  }
10805
11201
  /**
10806
11202
  * Render background clipped to text shape using offscreen canvas with destination-in compositing.
@@ -11216,22 +11612,6 @@ var Context = class Context {
11216
11612
  }
11217
11613
  };
11218
11614
  //#endregion
11219
- //#region src/core/abort-helper.ts
11220
- /**
11221
- * AbortSignal helper utilities.
11222
- */
11223
- /**
11224
- * Throw a DOMException if the given AbortSignal is already aborted.
11225
- * Used at checkpoints throughout the rendering pipeline to support
11226
- * cancellable renders.
11227
- *
11228
- * @param signal - The AbortSignal to check. If undefined or not aborted, this is a no-op.
11229
- * @throws {DOMException} With name 'AbortError' if the signal has been aborted.
11230
- */
11231
- const throwIfAborted = (signal) => {
11232
- if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
11233
- };
11234
- //#endregion
11235
11615
  //#region src/core/config-assembler.ts
11236
11616
  /**
11237
11617
  * Coerce known numeric options from string (or other) values to actual numbers.
@@ -11400,18 +11780,19 @@ const renderElement = async (element, opts, config) => {
11400
11780
  canvas = await new CanvasRenderer(context, renderOptions).render(root);
11401
11781
  perfMonitor.end("render");
11402
11782
  }
11783
+ } finally {
11403
11784
  perfMonitor.start("cleanup");
11404
- if (opts.removeContainer ?? true) {
11405
- 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");
11406
11790
  }
11407
- perfMonitor.end("cleanup");
11408
- perfMonitor.end("total");
11409
- context.logger.debug(`Finished rendering`);
11410
- if (performanceMonitoring) perfMonitor.logSummary();
11411
- return canvas;
11412
- } finally {
11413
- if (root) root.restoreTree();
11414
11791
  }
11792
+ perfMonitor.end("total");
11793
+ context.logger.debug(`Finished rendering`);
11794
+ if (performanceMonitoring) perfMonitor.logSummary();
11795
+ return canvas;
11415
11796
  };
11416
11797
  //#endregion
11417
11798
  //#region src/index.ts