html2canvas-pro 2.5.0 → 2.5.1

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.5.0 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.5.1 <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
  */
@@ -4747,6 +4747,17 @@ const conicGradient = (context, tokens) => {
4747
4747
  stops
4748
4748
  };
4749
4749
  };
4750
+ /**
4751
+ * repeating-conic-gradient shares the conic-gradient grammar exactly; only
4752
+ * the image type differs, which switches the renderer to periodic stops
4753
+ * stacked across the full sweep.
4754
+ */
4755
+ const repeatingConicGradient = (context, tokens) => {
4756
+ return {
4757
+ ...conicGradient(context, tokens),
4758
+ type: 6
4759
+ };
4760
+ };
4750
4761
  //#endregion
4751
4762
  //#region src/css/types/functions/image-set.ts
4752
4763
  const parseResolution = (token) => {
@@ -4799,6 +4810,9 @@ const isRepeatingRadialGradient = (background) => {
4799
4810
  const isConicGradient = (background) => {
4800
4811
  return background.type === 5;
4801
4812
  };
4813
+ const isRepeatingConicGradient = (background) => {
4814
+ return background.type === 6;
4815
+ };
4802
4816
  const image = {
4803
4817
  name: "image",
4804
4818
  parse: (context, value) => {
@@ -4844,6 +4858,9 @@ const SUPPORTED_IMAGE_FUNCTIONS = {
4844
4858
  "conic-gradient": conicGradient,
4845
4859
  "-webkit-conic-gradient": conicGradient,
4846
4860
  "-moz-conic-gradient": conicGradient,
4861
+ "repeating-conic-gradient": repeatingConicGradient,
4862
+ "-webkit-repeating-conic-gradient": repeatingConicGradient,
4863
+ "-moz-repeating-conic-gradient": repeatingConicGradient,
4847
4864
  "image-set": imageSet,
4848
4865
  "-webkit-image-set": imageSet
4849
4866
  };
@@ -5332,6 +5349,7 @@ const listStyleImage = {
5332
5349
  skipCache: true,
5333
5350
  parse: (context, token) => {
5334
5351
  if (token.type === 20 && token.value === "none") return null;
5352
+ if (!isSupportedImage(token)) return null;
5335
5353
  return image.parse(context, token);
5336
5354
  }
5337
5355
  };
@@ -7776,9 +7794,12 @@ var PseudoContentResolver = class {
7776
7794
  case "open-quote":
7777
7795
  anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth++, true)));
7778
7796
  break;
7779
- case "close-quote":
7780
- anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, --this.quoteDepth, false)));
7797
+ case "close-quote": {
7798
+ const hasOpenQuote = this.quoteDepth > 0;
7799
+ this.quoteDepth = Math.max(0, this.quoteDepth - 1);
7800
+ if (hasOpenQuote) anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth, false)));
7781
7801
  break;
7802
+ }
7782
7803
  default: anonymousReplacedElement.appendChild(document.createTextNode(token.value));
7783
7804
  }
7784
7805
  });
@@ -8120,12 +8141,14 @@ var DocumentCloner = class {
8120
8141
  const styleBefore = window.getComputedStyle(node, ":before");
8121
8142
  const before = this.pseudoContents.resolvePseudoContent(node, clone, styleBefore, 0);
8122
8143
  if (before) clone.insertBefore(before, clone.firstChild);
8144
+ }
8145
+ if (isCustomElement(node)) copyStyles = true;
8146
+ if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
8147
+ if (checkPseudoElements) {
8123
8148
  const styleAfter = window.getComputedStyle(node, ":after");
8124
8149
  const after = this.pseudoContents.resolvePseudoContent(node, clone, styleAfter, 1);
8125
8150
  if (after) clone.appendChild(after);
8126
8151
  }
8127
- if (isCustomElement(node)) copyStyles = true;
8128
- if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
8129
8152
  this.counters.pop(counters);
8130
8153
  if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node) || copyStyles) copyCSSStyles(style, clone);
8131
8154
  if (node.scrollTop !== 0 || node.scrollLeft !== 0) this.scrolledElements.push([
@@ -9021,10 +9044,10 @@ const LIST_OWNERS = [
9021
9044
  "UL",
9022
9045
  "MENU"
9023
9046
  ];
9024
- const parseNodeTree = (context, node, parent, root) => {
9047
+ const parseNodeTree = (context, node, parent, root, zoomScale = 1) => {
9025
9048
  for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
9026
9049
  nextNode = childNode.nextSibling;
9027
- parseChildNode(context, childNode, parent, root);
9050
+ parseChildNode(context, childNode, parent, root, zoomScale);
9028
9051
  }
9029
9052
  };
9030
9053
  /**
@@ -9035,19 +9058,29 @@ const parseNodeTree = (context, node, parent, root) => {
9035
9058
  * this, slotted shadow-DOM content such as web component labels was silently
9036
9059
  * dropped because `parseNodeTree` only iterates `node.firstChild` (issue #226).
9037
9060
  */
9038
- const parseChildNode = (context, childNode, parent, root) => {
9039
- if (isTextNode(childNode) && childNode.data.length > 0) parent.textNodes.push(new TextContainer(context, childNode, parent.styles));
9040
- else if (isElementNode(childNode)) {
9041
- if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root));
9061
+ const parseChildNode = (context, childNode, parent, root, zoomScale = 1) => {
9062
+ if (isTextNode(childNode) && childNode.data.length > 0) {
9063
+ const textContainer = new TextContainer(context, childNode, parent.styles);
9064
+ if (zoomScale !== 1) {
9065
+ const originLeft = parent.bounds.left;
9066
+ const originTop = parent.bounds.top;
9067
+ textContainer.textBounds = textContainer.textBounds.map((tb) => new TextBounds(tb.text, new Bounds(originLeft + (tb.bounds.left - originLeft) / zoomScale, originTop + (tb.bounds.top - originTop) / zoomScale, tb.bounds.width / zoomScale, tb.bounds.height / zoomScale)));
9068
+ }
9069
+ parent.textNodes.push(textContainer);
9070
+ } else if (isElementNode(childNode)) {
9071
+ if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root, zoomScale));
9042
9072
  else {
9043
9073
  const container = createContainer(context, childNode);
9074
+ const ownZoom = container.styles.zoom || 1;
9075
+ if (ownZoom !== 1) container.bounds = new Bounds(container.bounds.left, container.bounds.top, container.bounds.width / ownZoom, container.bounds.height / ownZoom);
9044
9076
  if (container.styles.isVisible()) {
9045
9077
  if (createsRealStackingContext(childNode, container, root)) container.createsRealStackingContext = true;
9046
9078
  else if (createsStackingContext(container.styles)) container.createsStackingContext = true;
9047
9079
  if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) container.isListOwner = true;
9048
9080
  parent.elements.push(container);
9049
- if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root);
9050
- else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root);
9081
+ const childZoom = zoomScale * (container.styles.zoom || 1);
9082
+ if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root, childZoom);
9083
+ else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root, childZoom);
9051
9084
  }
9052
9085
  }
9053
9086
  }
@@ -9075,6 +9108,7 @@ const createsRealStackingContext = (node, container, root) => {
9075
9108
  };
9076
9109
  const createsStackingContext = (styles) => {
9077
9110
  if (styles.isPositioned() || styles.isFloating()) return true;
9111
+ if (styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) return true;
9078
9112
  return contains(styles.display, 268435456) || contains(styles.display, 33554432) || contains(styles.display, 536870912) || contains(styles.display, 134217728);
9079
9113
  };
9080
9114
  //#endregion
@@ -9494,9 +9528,8 @@ var ElementPaint = class {
9494
9528
  if (this.container.styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) this.effects.push(new BlendEffect(this.container.styles.mixBlendMode));
9495
9529
  if (this.container.styles.filter !== null) this.effects.push(new FilterEffect(this.container.styles.filter));
9496
9530
  if (this.container.styles.zoom !== 1) {
9497
- const origin = this.container.styles.transformOrigin;
9498
- const offsetX = this.container.bounds.left + getAbsoluteValue(origin[0], this.container.bounds.width);
9499
- const offsetY = this.container.bounds.top + getAbsoluteValue(origin[1], this.container.bounds.height);
9531
+ const offsetX = this.container.bounds.left;
9532
+ const offsetY = this.container.bounds.top;
9500
9533
  const z = this.container.styles.zoom;
9501
9534
  const zoomMatrix = [
9502
9535
  z,
@@ -9528,7 +9561,7 @@ var ElementPaint = class {
9528
9561
  computeEffects(target, surfaceRoot) {
9529
9562
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
9530
9563
  let parent = this === surfaceRoot ? null : this.parent;
9531
- const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
9564
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect));
9532
9565
  const effects = sourceEffects(this);
9533
9566
  while (parent) {
9534
9567
  const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
@@ -10421,7 +10454,7 @@ var TextRenderer = class {
10421
10454
  this.ctx.shadowColor = asString(textShadow.color);
10422
10455
  this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
10423
10456
  this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
10424
- this.ctx.shadowBlur = textShadow.blur.number;
10457
+ this.ctx.shadowBlur = textShadow.blur.number * this.options.scale;
10425
10458
  this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
10426
10459
  });
10427
10460
  this.ctx.shadowColor = "";
@@ -10571,7 +10604,7 @@ var TextRenderer = class {
10571
10604
  this.ctx.textBaseline = "alphabetic";
10572
10605
  const paintOrder = styles.paintOrder;
10573
10606
  const baseline = this.measureBaselineCached(fontString ?? "", styles.fontSize.number);
10574
- if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
10607
+ if (styles.webkitLineClamp > 0 && (styles.display & 34) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
10575
10608
  if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
10576
10609
  }
10577
10610
  if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds)) return;
@@ -10770,6 +10803,7 @@ var BackgroundRenderer = class {
10770
10803
  else if (isRadialGradient(backgroundImage)) this.renderRadialGradient(container, backgroundImage, index);
10771
10804
  else if (isRepeatingRadialGradient(backgroundImage)) this.renderRepeatingRadialGradient(container, backgroundImage, index);
10772
10805
  else if (isConicGradient(backgroundImage)) this.renderConicGradient(container, backgroundImage, index);
10806
+ else if (isRepeatingConicGradient(backgroundImage)) this.renderRepeatingConicGradient(container, backgroundImage, index);
10773
10807
  if (layerCount > 0) {
10774
10808
  if ((blendModes[layerCount] ?? blendModes[0] ?? "normal") !== "normal") this.ctx.restore();
10775
10809
  }
@@ -10820,45 +10854,43 @@ var BackgroundRenderer = class {
10820
10854
  null,
10821
10855
  null
10822
10856
  ]);
10823
- const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
10857
+ const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(backgroundImage.angle, width, height);
10858
+ const gx0 = x + lx0;
10859
+ const gx1 = x + lx1;
10860
+ const gy0 = y + ly0;
10861
+ const gy1 = y + ly1;
10824
10862
  const processedStops = processColorStops(backgroundImage.stops, lineLength || 1);
10825
10863
  const lastStop = at(processedStops, processedStops.length - 1);
10826
10864
  const firstStop = at(processedStops, 0);
10827
- const stopSpan = lastStop.stop - firstStop.stop;
10828
- const patternLength = stopSpan * (lineLength || 1);
10829
- if (patternLength <= 0) {
10830
- this.renderLinearGradient(container, backgroundImage, index);
10865
+ const period = Math.max(lastStop.stop - firstStop.stop, .01);
10866
+ if (lastStop.stop - firstStop.stop < 1e-6) {
10867
+ const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
10868
+ gradient.addColorStop(0, asString(lastStop.color));
10869
+ gradient.addColorStop(1, asString(lastStop.color));
10870
+ this.path(path);
10871
+ this.ctx.fillStyle = gradient;
10872
+ this.ctx.fill();
10831
10873
  return;
10832
10874
  }
10833
- const dirX = x1 - x0;
10834
- const dirY = y1 - y0;
10835
- const scale = patternLength / (Math.sqrt(dirX * dirX + dirY * dirY) || 1);
10836
- const pX0 = x0;
10837
- const pY0 = y0;
10838
- const pX1 = x0 + dirX * scale;
10839
- const pY1 = y0 + dirY * scale;
10840
- const ownerDocument = this.canvas.ownerDocument ?? document;
10841
- const cacheKey = `rlg|${backgroundImage.angle}|${Math.round(patternLength)}|${JSON.stringify(backgroundImage.stops)}`;
10842
- let pattern = this.patternCache.get(cacheKey);
10843
- if (!pattern) {
10844
- const canvas = ownerDocument.createElement("canvas");
10845
- const canvasSize = Math.max(1, Math.ceil(patternLength));
10846
- canvas.width = canvasSize;
10847
- canvas.height = canvasSize;
10848
- const ctx = canvas.getContext("2d");
10849
- if (!ctx) return;
10850
- const gradient = ctx.createLinearGradient(pX0 - x, pY0 - y, pX1 - x, pY1 - y);
10851
- processedStops.forEach((colorStop) => {
10852
- gradient.addColorStop((colorStop.stop - firstStop.stop) / stopSpan, asString(colorStop.color));
10875
+ const stops = [];
10876
+ const cycles = Math.ceil(1 / period);
10877
+ for (let k = 0; k < cycles; k++) for (const s of processedStops) {
10878
+ const stop = k * period + (s.stop - firstStop.stop);
10879
+ if (stop > 1) break;
10880
+ stops.push({
10881
+ stop,
10882
+ color: asString(s.color)
10853
10883
  });
10854
- ctx.fillStyle = gradient;
10855
- ctx.fillRect(0, 0, canvasSize, canvasSize);
10856
- if (canvasSize > 0) {
10857
- pattern = this.ctx.createPattern(canvas, "repeat");
10858
- this.patternCache.set(cacheKey, pattern);
10859
- }
10860
10884
  }
10861
- if (pattern) this.renderRepeat(path, pattern, x, y);
10885
+ stops.push({
10886
+ stop: 1,
10887
+ color: asString(lastStop.color)
10888
+ });
10889
+ const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
10890
+ stops.forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color));
10891
+ this.path(path);
10892
+ this.ctx.fillStyle = gradient;
10893
+ this.ctx.fill();
10862
10894
  }
10863
10895
  /**
10864
10896
  * Render a linear gradient background
@@ -10908,7 +10940,7 @@ var BackgroundRenderer = class {
10908
10940
  }
10909
10941
  if (rx > 0 && ry > 0) {
10910
10942
  const cacheKey = `rg|${Math.round(x)}x${Math.round(y)}|${Math.round(rx)}x${Math.round(ry)}|${JSON.stringify(backgroundImage.stops)}`;
10911
- const stops = processColorStops(backgroundImage.stops, rx * 2);
10943
+ const stops = processColorStops(backgroundImage.stops, rx);
10912
10944
  const lastStop = stops[stops.length - 1];
10913
10945
  let pattern = this.patternCache.get(cacheKey);
10914
10946
  if (!pattern) {
@@ -10965,14 +10997,14 @@ var BackgroundRenderer = class {
10965
10997
  ry = Math.max(ry, .01);
10966
10998
  }
10967
10999
  if (!(rx > 0 && ry > 0)) return;
10968
- const processed = processColorStops(backgroundImage.stops, rx * 2);
11000
+ const processed = processColorStops(backgroundImage.stops, rx);
10969
11001
  const firstStop = at(processed, 0);
10970
11002
  const lastStop = at(processed, processed.length - 1);
10971
11003
  const period = Math.max(lastStop.stop - firstStop.stop, .01);
10972
11004
  const stops = [];
10973
- const rings = Math.ceil(.5 / period);
11005
+ const rings = Math.ceil(1 / period);
10974
11006
  for (let k = 0; k < rings; k++) for (const s of processed) {
10975
- const stop = (k * period + (s.stop - firstStop.stop)) / 2;
11007
+ const stop = k * period + (s.stop - firstStop.stop);
10976
11008
  if (stop > 1) break;
10977
11009
  stops.push({
10978
11010
  stop,
@@ -11039,6 +11071,65 @@ var BackgroundRenderer = class {
11039
11071
  this.ctx.restore();
11040
11072
  }
11041
11073
  /**
11074
+ * Render a repeating conic gradient background.
11075
+ *
11076
+ * Canvas conic gradient stops span the full 2π sweep as [0, 1], so the
11077
+ * one-cycle stop list is stacked k·period + s for k = 0..cycles-1 over the
11078
+ * whole sweep — the same periodic-stop stacking the repeating linear and
11079
+ * radial renderers use along their gradient lines.
11080
+ */
11081
+ renderRepeatingConicGradient(container, backgroundImage, index) {
11082
+ const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
11083
+ null,
11084
+ null,
11085
+ null
11086
+ ]);
11087
+ const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
11088
+ const cx = left + getAbsoluteValue(at(position, 0), width);
11089
+ const cy = top + getAbsoluteValue(at(position, position.length - 1), height);
11090
+ const processedStops = processColorStops(backgroundImage.stops, 1);
11091
+ const firstStop = at(processedStops, 0);
11092
+ const lastStop = at(processedStops, processedStops.length - 1);
11093
+ const period = Math.max(lastStop.stop - firstStop.stop, .01);
11094
+ if (lastStop.stop - firstStop.stop < 1e-6) {
11095
+ const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
11096
+ gradient.addColorStop(0, asString(lastStop.color));
11097
+ gradient.addColorStop(1, asString(lastStop.color));
11098
+ this.path(path);
11099
+ this.ctx.save();
11100
+ this.ctx.clip();
11101
+ this.ctx.fillStyle = gradient;
11102
+ this.ctx.fill();
11103
+ this.ctx.restore();
11104
+ return;
11105
+ }
11106
+ const stops = [];
11107
+ const cycles = Math.ceil(1 / period);
11108
+ for (let k = 0; k < cycles; k++) for (const s of processedStops) {
11109
+ const stop = k * period + (s.stop - firstStop.stop);
11110
+ if (stop > 1) break;
11111
+ stops.push({
11112
+ stop,
11113
+ color: asString(s.color)
11114
+ });
11115
+ }
11116
+ const lastIncluded = at(stops, stops.length - 1);
11117
+ if (lastIncluded && lastIncluded.stop < 1) stops.push({
11118
+ stop: 1,
11119
+ color: lastIncluded.color
11120
+ });
11121
+ const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
11122
+ stops.forEach((s) => {
11123
+ gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
11124
+ });
11125
+ this.path(path);
11126
+ this.ctx.save();
11127
+ this.ctx.clip();
11128
+ this.ctx.fillStyle = gradient;
11129
+ this.ctx.fill();
11130
+ this.ctx.restore();
11131
+ }
11132
+ /**
11042
11133
  * Render a repeating pattern with offset
11043
11134
  *
11044
11135
  * @param path - Path to fill
@@ -11463,6 +11554,13 @@ var BorderImageRenderer = class {
11463
11554
  * - Filter effects (CSS filter functions)
11464
11555
  */
11465
11556
  /**
11557
+ * Scale `blur(<n>px)` radii in a ctx.filter string by the device-pixel
11558
+ * ratio. Other filter functions (brightness, saturate, …) take
11559
+ * unitless values and are unaffected.
11560
+ */
11561
+ const BLUR_PX_RE = /blur\((\d+(?:\.\d+)?)px\)/gi;
11562
+ const scaleFilterBlur = (filter, scale) => scale === 1 ? filter : filter.replace(BLUR_PX_RE, (_match, value) => `blur(${parseFloat(value) * scale}px)`);
11563
+ /**
11466
11564
  * Effects Renderer
11467
11565
  *
11468
11566
  * Manages rendering effects stack including opacity, transforms, and clipping.
@@ -11500,6 +11598,7 @@ var EffectsRenderer = class {
11500
11598
  this.saveAtDepth = -1;
11501
11599
  this.ctx = deps.ctx;
11502
11600
  this.pathCallback = pathCallback;
11601
+ this.scale = deps.scale ?? 1;
11503
11602
  }
11504
11603
  /**
11505
11604
  * Apply multiple effects.
@@ -11538,11 +11637,11 @@ var EffectsRenderer = class {
11538
11637
  else if (isOpacityEffect(effect)) this.ctx.globalAlpha *= effect.opacity;
11539
11638
  else if (isBlendEffect(effect)) this.ctx.globalCompositeOperation = effect.compositeOperation;
11540
11639
  else if (isFilterEffect(effect)) {
11541
- this.ctx.filter = effect.safeFilterString || "none";
11640
+ this.ctx.filter = scaleFilterBlur(effect.safeFilterString, this.scale) || "none";
11542
11641
  if (effect.shadow) {
11543
- this.ctx.shadowOffsetX = effect.shadow.offsetX;
11544
- this.ctx.shadowOffsetY = effect.shadow.offsetY;
11545
- this.ctx.shadowBlur = effect.shadow.blur;
11642
+ this.ctx.shadowOffsetX = effect.shadow.offsetX * this.scale;
11643
+ this.ctx.shadowOffsetY = effect.shadow.offsetY * this.scale;
11644
+ this.ctx.shadowBlur = effect.shadow.blur * this.scale;
11546
11645
  this.ctx.shadowColor = effect.shadow.color;
11547
11646
  }
11548
11647
  }
@@ -11687,8 +11786,8 @@ const paintGradientLayer = (ctx, layer, area) => {
11687
11786
  let gradient = null;
11688
11787
  if (layer.type === 1 || layer.type === 3) {
11689
11788
  const linear = layer;
11690
- const [lineLength, gx0, gy0, gx1, gy1] = calculateGradientDirection(linear.angle || 0, width, height);
11691
- gradient = ctx.createLinearGradient(gx0, gy0, gx1, gy1);
11789
+ const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(linear.angle || 0, width, height);
11790
+ gradient = ctx.createLinearGradient(area.left + lx0, area.top + ly0, area.left + lx1, area.top + ly1);
11692
11791
  processColorStops(linear.stops, lineLength || 1).forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), asString(s.color)));
11693
11792
  } else if (isConicGradient(layer)) {
11694
11793
  const conic = layer;
@@ -11849,14 +11948,14 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
11849
11948
  new Vector(container.bounds.left + size * .84, container.bounds.top + size * .34085),
11850
11949
  new Vector(container.bounds.left + size * .39363, container.bounds.top + size * .79)
11851
11950
  ]);
11852
- ctx.fillStyle = asString(container.styles.accentColor ?? 707406591);
11951
+ ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
11853
11952
  ctx.fill();
11854
11953
  ctx.restore();
11855
11954
  } else if (container.type === "radio" && container.checked) {
11856
11955
  ctx.save();
11857
11956
  ctx.beginPath();
11858
11957
  ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
11859
- ctx.fillStyle = asString(container.styles.accentColor ?? 707406591);
11958
+ ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
11860
11959
  ctx.fill();
11861
11960
  ctx.restore();
11862
11961
  }
@@ -11955,22 +12054,51 @@ async function renderListMarker(ctx, context, textRenderer, paint, container, st
11955
12054
  const url = img.url;
11956
12055
  try {
11957
12056
  const image = await context.cache.match(url);
11958
- ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
12057
+ const fontSize = getAbsoluteValue(styles.fontSize, 0);
12058
+ const naturalWidth = image?.naturalWidth || image.width;
12059
+ const naturalHeight = image?.naturalHeight || image.height;
12060
+ if (naturalWidth <= 0 || naturalHeight <= 0) return;
12061
+ const markerScale = Math.min(fontSize / naturalWidth, fontSize / naturalHeight);
12062
+ const markerWidth = naturalWidth * markerScale;
12063
+ const markerHeight = naturalHeight * markerScale;
12064
+ const markerCenterY = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
12065
+ ctx.drawImage(image, container.bounds.left - (markerWidth + 10), markerCenterY - markerHeight / 2, markerWidth, markerHeight);
11959
12066
  } catch (e) {
11960
12067
  context.logger.error(`Error loading list-style-image ${url}`);
11961
12068
  context.onError?.(e instanceof Error ? e : new Error(String(e)));
11962
12069
  }
11963
12070
  }
11964
12071
  } else if (paint.listValue && container.styles.listStyleType !== -1) {
11965
- const [font] = textRenderer.createFontStyle(styles);
11966
- ctx.font = font ?? "";
11967
- ctx.fillStyle = asString(styles.color);
11968
- ctx.textBaseline = "middle";
11969
- ctx.textAlign = "right";
11970
- const bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
11971
- textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
11972
- ctx.textBaseline = "bottom";
11973
- ctx.textAlign = "left";
12072
+ const type = container.styles.listStyleType;
12073
+ if (type === 0 || type === 1 || type === 2) {
12074
+ const size = styles.fontSize.number * .45;
12075
+ const cx = container.bounds.left - size * .9;
12076
+ const cy = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
12077
+ ctx.save();
12078
+ ctx.fillStyle = asString(styles.color);
12079
+ ctx.beginPath();
12080
+ if (type === 2) ctx.rect(cx - size / 2, cy - size / 2, size, size);
12081
+ else {
12082
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
12083
+ if (type === 1) {
12084
+ ctx.strokeStyle = asString(styles.color);
12085
+ ctx.lineWidth = size * .18;
12086
+ }
12087
+ }
12088
+ if (type === 1) ctx.stroke();
12089
+ else ctx.fill();
12090
+ ctx.restore();
12091
+ } else {
12092
+ const [font] = textRenderer.createFontStyle(styles);
12093
+ ctx.font = font ?? "";
12094
+ ctx.fillStyle = asString(styles.color);
12095
+ ctx.textBaseline = "middle";
12096
+ ctx.textAlign = "right";
12097
+ const bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
12098
+ textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
12099
+ ctx.textBaseline = "bottom";
12100
+ ctx.textAlign = "left";
12101
+ }
11974
12102
  }
11975
12103
  }
11976
12104
  /**
@@ -12213,31 +12341,33 @@ var TextClipRenderer = class {
12213
12341
  constructor(deps) {
12214
12342
  this.ctx = deps.ctx;
12215
12343
  this.context = deps.context;
12344
+ this.scale = deps.scale;
12216
12345
  this.createFontStyle = deps.createFontStyle;
12217
12346
  }
12218
12347
  async render(paint) {
12219
12348
  const container = paint.container;
12220
12349
  const styles = container.styles;
12221
12350
  const bounds = container.bounds;
12351
+ const scale = this.scale;
12222
12352
  if (bounds.width <= 0 || bounds.height <= 0) return;
12223
12353
  const ownerDocument = this.ctx.canvas.ownerDocument ?? document;
12224
- const offscreen = ownerDocument.createElement("canvas");
12225
12354
  const width = Math.ceil(bounds.width);
12226
12355
  const height = Math.ceil(bounds.height);
12227
- offscreen.width = width;
12228
- offscreen.height = height;
12229
- const offCtx = offscreen.getContext("2d");
12230
- if (!offCtx) return;
12356
+ const deviceWidth = Math.ceil(bounds.width * scale);
12357
+ const deviceHeight = Math.ceil(bounds.height * scale);
12358
+ const offscreen = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
12359
+ if (!offscreen) return;
12360
+ offscreen.ctx.scale(scale, scale);
12231
12361
  const [fontString] = this.createFontStyle(styles);
12232
- offCtx.font = fontString ?? "";
12233
- offCtx.textBaseline = "alphabetic";
12234
- offCtx.textAlign = "left";
12235
- offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
12236
- const baseline = measureBaseline(offCtx, styles.fontSize.number);
12362
+ offscreen.ctx.font = fontString ?? "";
12363
+ offscreen.ctx.textBaseline = "alphabetic";
12364
+ offscreen.ctx.textAlign = "left";
12365
+ offscreen.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
12366
+ const baseline = measureBaseline(offscreen.ctx, styles.fontSize.number);
12237
12367
  const bgRenderer = new BackgroundRenderer({
12238
- ctx: offCtx,
12368
+ ctx: offscreen.ctx,
12239
12369
  context: this.context,
12240
- canvas: offscreen,
12370
+ canvas: offscreen.canvas,
12241
12371
  options: {
12242
12372
  width,
12243
12373
  height,
@@ -12245,51 +12375,115 @@ var TextClipRenderer = class {
12245
12375
  }
12246
12376
  });
12247
12377
  if (!isTransparent(styles.backgroundColor)) {
12248
- offCtx.fillStyle = asString(styles.backgroundColor);
12249
- offCtx.fillRect(0, 0, width, height);
12378
+ offscreen.ctx.fillStyle = asString(styles.backgroundColor);
12379
+ offscreen.ctx.fillRect(0, 0, width, height);
12250
12380
  }
12251
- offCtx.save();
12252
- offCtx.translate(-bounds.left, -bounds.top);
12381
+ offscreen.ctx.save();
12382
+ offscreen.ctx.translate(-bounds.left, -bounds.top);
12253
12383
  await bgRenderer.renderBackgroundImage(container);
12254
- offCtx.restore();
12255
- const mask = ownerDocument.createElement("canvas");
12256
- mask.width = width;
12257
- mask.height = height;
12258
- const maskCtx = mask.getContext("2d");
12259
- if (!maskCtx) return;
12260
- maskCtx.font = offCtx.font;
12261
- maskCtx.textBaseline = offCtx.textBaseline;
12262
- maskCtx.textAlign = offCtx.textAlign;
12263
- maskCtx.direction = offCtx.direction;
12264
- maskCtx.fillStyle = "#000";
12265
- const writingMode = styles.writingMode;
12266
- const letterSpacing = styles.letterSpacing;
12267
- for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
12268
- const localLeft = textBound.bounds.left - bounds.left;
12269
- const localTop = textBound.bounds.top - bounds.top + baseline;
12270
- if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(maskCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
12271
- else maskCtx.fillText(textBound.text, localLeft, localTop);
12272
- }
12273
- offCtx.globalCompositeOperation = "destination-in";
12274
- offCtx.drawImage(mask, 0, 0);
12275
- this.ctx.drawImage(offscreen, bounds.left, bounds.top);
12384
+ offscreen.ctx.restore();
12385
+ const drawOptions = {
12386
+ bounds,
12387
+ baseline,
12388
+ letterSpacing: styles.letterSpacing,
12389
+ writingMode: styles.writingMode
12390
+ };
12391
+ const mask = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, offscreen.ctx);
12392
+ if (!mask) return;
12393
+ const hasStroke = !!styles.webkitTextStrokeWidth;
12394
+ if (hasStroke) {
12395
+ mask.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
12396
+ mask.ctx.lineWidth = styles.webkitTextStrokeWidth;
12397
+ mask.ctx.lineJoin = this.getTextStrokeLineJoin();
12398
+ }
12399
+ for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(mask, textBound, drawOptions, (ctx, text, x, y) => {
12400
+ ctx.fillText(text, x, y);
12401
+ if (hasStroke) ctx.strokeText(text, x, y);
12402
+ });
12403
+ offscreen.ctx.globalCompositeOperation = "destination-in";
12404
+ offscreen.ctx.drawImage(mask.canvas, 0, 0, width, height);
12405
+ const shadowCanvases = this.renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, offscreen.ctx, styles, drawOptions);
12406
+ if (shadowCanvases) {
12407
+ const composite = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
12408
+ if (!composite) return;
12409
+ for (const shadowCanvas of shadowCanvases) composite.ctx.drawImage(shadowCanvas, 0, 0);
12410
+ composite.ctx.drawImage(offscreen.canvas, 0, 0);
12411
+ this.ctx.drawImage(composite.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
12412
+ } else this.ctx.drawImage(offscreen.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
12413
+ }
12414
+ createLayer(ownerDocument, width, height) {
12415
+ const canvas = ownerDocument.createElement("canvas");
12416
+ canvas.width = width;
12417
+ canvas.height = height;
12418
+ const ctx = canvas.getContext("2d");
12419
+ if (!ctx) return null;
12420
+ return {
12421
+ canvas,
12422
+ ctx
12423
+ };
12424
+ }
12425
+ createGlyphLayer(ownerDocument, width, height, fontSource) {
12426
+ const layer = this.createLayer(ownerDocument, width, height);
12427
+ if (!layer) return null;
12428
+ layer.ctx.scale(this.scale, this.scale);
12429
+ layer.ctx.font = fontSource.font;
12430
+ layer.ctx.textBaseline = fontSource.textBaseline;
12431
+ layer.ctx.textAlign = fontSource.textAlign;
12432
+ layer.ctx.direction = fontSource.direction;
12433
+ layer.ctx.fillStyle = "#000";
12434
+ return layer;
12276
12435
  }
12277
12436
  /**
12278
- * Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
12279
- * This ensures the mask matches how the text renderer lays out the characters.
12437
+ * Draw one text bound onto a glyph layer, splitting into per-glyph draws
12438
+ * when letter-spacing is in effect so the layout matches the main text
12439
+ * renderer. Vertical writing modes fall back to a single draw call.
12280
12440
  */
12281
- renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
12282
- const letters = segmentGraphemes(text.text);
12441
+ drawGlyphs(layer, textBound, drawOptions, draw) {
12442
+ const x = textBound.bounds.left - drawOptions.bounds.left;
12443
+ const y = textBound.bounds.top - drawOptions.bounds.top + drawOptions.baseline;
12444
+ const { letterSpacing, writingMode } = drawOptions;
12445
+ if (letterSpacing <= 0) {
12446
+ draw(layer.ctx, textBound.text, x, y);
12447
+ return;
12448
+ }
12449
+ if (isVerticalWritingMode(writingMode)) {
12450
+ draw(layer.ctx, textBound.text, x, y);
12451
+ return;
12452
+ }
12283
12453
  let offset = x;
12284
- for (const letter of letters) {
12285
- if (isVerticalWritingMode(writingMode)) {
12286
- ctx.fillText(text.text, x, y);
12287
- return;
12288
- }
12289
- ctx.fillText(letter, offset, y);
12290
- offset += ctx.measureText(letter).width + letterSpacing;
12454
+ for (const letter of segmentGraphemes(textBound.text)) {
12455
+ draw(layer.ctx, letter, offset, y);
12456
+ offset += layer.ctx.measureText(letter).width + letterSpacing;
12291
12457
  }
12292
12458
  }
12459
+ /**
12460
+ * Paint the text-shadow silhouettes for every glyph, one canvas per
12461
+ * shadow, returned front-to-back (the last shadow sits deepest). Returns
12462
+ * null when the element has no text shadows.
12463
+ */
12464
+ renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, fontSource, styles, drawOptions) {
12465
+ const textShadows = styles.textShadow;
12466
+ if (!textShadows.length) return null;
12467
+ const canvases = [];
12468
+ for (let i = textShadows.length - 1; i >= 0; i--) {
12469
+ const textShadow = at(textShadows, i);
12470
+ const layer = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, fontSource);
12471
+ if (!layer) continue;
12472
+ layer.ctx.shadowColor = asString(textShadow.color);
12473
+ layer.ctx.shadowOffsetX = textShadow.offsetX.number * this.scale;
12474
+ layer.ctx.shadowOffsetY = textShadow.offsetY.number * this.scale;
12475
+ layer.ctx.shadowBlur = textShadow.blur.number * this.scale;
12476
+ layer.ctx.fillStyle = asString(textShadow.color);
12477
+ for (const textNode of paint.container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(layer, textBound, drawOptions, (ctx, text, x, y) => {
12478
+ ctx.fillText(text, x, y);
12479
+ });
12480
+ canvases.push(layer.canvas);
12481
+ }
12482
+ return canvases.length ? canvases : null;
12483
+ }
12484
+ getTextStrokeLineJoin() {
12485
+ return (typeof window !== "undefined" ? window : void 0)?.chrome ? "miter" : "round";
12486
+ }
12293
12487
  };
12294
12488
  //#endregion
12295
12489
  //#region src/render/canvas/canvas-renderer.ts
@@ -12334,7 +12528,10 @@ var CanvasRenderer = class CanvasRenderer {
12334
12528
  formatPath: (paths) => this.formatPath(paths)
12335
12529
  });
12336
12530
  this.borderImageRenderer = new BorderImageRenderer(this.ctx);
12337
- this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
12531
+ this.effectsRenderer = new EffectsRenderer({
12532
+ ctx: this.ctx,
12533
+ scale: options.scale
12534
+ }, { path: (paths) => this.path(paths) });
12338
12535
  this.textRenderer = new TextRenderer({
12339
12536
  ctx: this.ctx,
12340
12537
  options: { scale: options.scale }
@@ -12342,6 +12539,7 @@ var CanvasRenderer = class CanvasRenderer {
12342
12539
  this.textClipRenderer = new TextClipRenderer({
12343
12540
  ctx: this.ctx,
12344
12541
  context: this.context,
12542
+ scale: options.scale,
12345
12543
  createFontStyle: (styles) => this.textRenderer.createFontStyle(styles)
12346
12544
  });
12347
12545
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
@@ -12352,8 +12550,9 @@ var CanvasRenderer = class CanvasRenderer {
12352
12550
  const filter = parseSimpleFilter(styles.filter);
12353
12551
  const hasMask = styles.maskImage.length > 0;
12354
12552
  const reflect = styles.webkitBoxReflect;
12355
- if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
12356
- if (!await this.renderCompositedStack(stack, filter, hasMask, reflect)) {
12553
+ const hasBlend = styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL;
12554
+ if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || hasBlend || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
12555
+ if (!await this.renderCompositedStack(stack, filter, hasMask, reflect, hasBlend)) {
12357
12556
  this.legacySubtree = true;
12358
12557
  try {
12359
12558
  await this.renderStackContent(stack);
@@ -12369,7 +12568,7 @@ var CanvasRenderer = class CanvasRenderer {
12369
12568
  canComposite(paint) {
12370
12569
  const supported = (container) => {
12371
12570
  const styles = container.styles;
12372
- 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;
12571
+ return !styles.isTransformed() && styles.zoom === 1 && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
12373
12572
  };
12374
12573
  const subtree = (container) => supported(container) && container.elements.every(subtree);
12375
12574
  let ancestor = paint.parent;
@@ -12379,7 +12578,7 @@ var CanvasRenderer = class CanvasRenderer {
12379
12578
  }
12380
12579
  return subtree(paint.container);
12381
12580
  }
12382
- async renderCompositedStack(stack, filter, hasMask, reflect) {
12581
+ async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
12383
12582
  const signal = this.options.signal;
12384
12583
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
12385
12584
  const bounds = cropSurface(surfaceBounds(stack.element.container), new Bounds(this.options.x, this.options.y, this.options.width, this.options.height), filter ? filterOutset(filter) : 0, this.options.scale);
@@ -12409,9 +12608,14 @@ var CanvasRenderer = class CanvasRenderer {
12409
12608
  if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
12410
12609
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
12411
12610
  const own = stack.element.effects;
12412
- const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
12611
+ const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect) && !isClipEffect(effect));
12413
12612
  this.effectsRenderer.applyEffects(effects);
12414
- this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12613
+ if (hasBlend) {
12614
+ this.ctx.save();
12615
+ this.ctx.globalCompositeOperation = stack.element.container.styles.mixBlendMode;
12616
+ this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12617
+ this.ctx.restore();
12618
+ } else this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12415
12619
  if (reflect) await this.drawReflection(composited, stack.element.container.bounds, bounds, reflect);
12416
12620
  return true;
12417
12621
  } catch (error) {
@@ -12501,6 +12705,7 @@ var CanvasRenderer = class CanvasRenderer {
12501
12705
  maskCanvas.height = h;
12502
12706
  const mctx = maskCanvas.getContext("2d");
12503
12707
  if (mctx) {
12708
+ mctx.scale(scale, scale);
12504
12709
  await paintMaskLayers(mctx, [reflect.mask], {
12505
12710
  maskPosition: [],
12506
12711
  maskRepeat: [],
@@ -12521,7 +12726,7 @@ var CanvasRenderer = class CanvasRenderer {
12521
12726
  ctx.scale(1, -1);
12522
12727
  }
12523
12728
  ctx.globalCompositeOperation = "destination-in";
12524
- ctx.drawImage(maskCanvas, 0, 0);
12729
+ ctx.drawImage(maskCanvas, 0, 0, w, h);
12525
12730
  } finally {
12526
12731
  ctx.restore();
12527
12732
  }
@@ -12733,26 +12938,27 @@ var CanvasRenderer = class CanvasRenderer {
12733
12938
  }
12734
12939
  if (styles.borderImageSource) {
12735
12940
  const source = styles.borderImageSource;
12941
+ const bounds = paint.container.bounds;
12942
+ const borderWidths = [
12943
+ Math.max(0, styles.borderTopWidth),
12944
+ Math.max(0, styles.borderRightWidth),
12945
+ Math.max(0, styles.borderBottomWidth),
12946
+ Math.max(0, styles.borderLeftWidth)
12947
+ ];
12948
+ let image = null;
12736
12949
  if (source.type === 0) {
12737
12950
  const url = source.url;
12738
12951
  try {
12739
- const image = await this.context.cache.match(url);
12740
- if (image) {
12741
- const bounds = paint.container.bounds;
12742
- const borderWidths = [
12743
- Math.max(0, styles.borderTopWidth),
12744
- Math.max(0, styles.borderRightWidth),
12745
- Math.max(0, styles.borderBottomWidth),
12746
- Math.max(0, styles.borderLeftWidth)
12747
- ];
12748
- const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
12749
- const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
12750
- const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
12751
- this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
12752
- }
12952
+ image = await this.context.cache.match(url) ?? null;
12753
12953
  } catch (e) {
12754
12954
  this.context.logger.error(`Error loading border-image ${url}`);
12755
12955
  }
12956
+ } else image = this.rasterizeGradientSource(source, bounds);
12957
+ if (image) {
12958
+ const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
12959
+ const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
12960
+ const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
12961
+ this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
12756
12962
  }
12757
12963
  return;
12758
12964
  }
@@ -12820,6 +13026,91 @@ var CanvasRenderer = class CanvasRenderer {
12820
13026
  }
12821
13027
  }
12822
13028
  /**
13029
+ * Rasterise a gradient border-image-source across the border image area.
13030
+ * CSS allows any <image> as the border-image source; gradients are painted
13031
+ * over the whole border-image box and then sliced by the regular 9-slice
13032
+ * pipeline, matching browser behaviour for `border-image: linear-gradient()`.
13033
+ * Returns null when the gradient could not be painted.
13034
+ */
13035
+ rasterizeGradientSource(source, bounds) {
13036
+ const width = Math.max(1, Math.ceil(bounds.width));
13037
+ const height = Math.max(1, Math.ceil(bounds.height));
13038
+ const canvas = (this.ctx.canvas.ownerDocument ?? document).createElement("canvas");
13039
+ canvas.width = width;
13040
+ canvas.height = height;
13041
+ const ctx = canvas.getContext("2d");
13042
+ if (!ctx) return null;
13043
+ if (source.type === 1 || source.type === 3) {
13044
+ const gradientImage = source;
13045
+ const [lineLength, x0, y0, x1, y1] = calculateGradientDirection(gradientImage.angle, width, height);
13046
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
13047
+ processColorStops(gradientImage.stops, lineLength || width).forEach((colorStop) => {
13048
+ gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
13049
+ });
13050
+ ctx.fillStyle = gradient;
13051
+ ctx.fillRect(0, 0, width, height);
13052
+ return canvas;
13053
+ }
13054
+ if (source.type === 2 || source.type === 4) {
13055
+ const gradientImage = source;
13056
+ const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
13057
+ const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
13058
+ const [rx, ry] = calculateRadius(gradientImage, cx, cy, width, height);
13059
+ if (rx <= 0 || ry <= 0) return null;
13060
+ ctx.save();
13061
+ ctx.translate(cx, cy);
13062
+ ctx.scale(1, ry / rx);
13063
+ ctx.translate(-cx, -cy);
13064
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
13065
+ processColorStops(gradientImage.stops, rx).forEach((colorStop) => {
13066
+ gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
13067
+ });
13068
+ ctx.fillStyle = gradient;
13069
+ ctx.fillRect(0, 0, width, height);
13070
+ ctx.restore();
13071
+ return canvas;
13072
+ }
13073
+ if (source.type === 5 || source.type === 6) {
13074
+ const gradientImage = source;
13075
+ const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
13076
+ const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
13077
+ const gradient = ctx.createConicGradient(gradientImage.angle - Math.PI / 2, cx, cy);
13078
+ let stops;
13079
+ if (source.type === 6) {
13080
+ const processed = processColorStops(gradientImage.stops, 1);
13081
+ const first = at(processed, 0);
13082
+ const last = at(processed, processed.length - 1);
13083
+ const period = Math.max(last.stop - first.stop, .01);
13084
+ stops = [];
13085
+ const cycles = Math.ceil(1 / period);
13086
+ for (let k = 0; k < cycles; k++) for (const s of processed) {
13087
+ const stop = k * period + (s.stop - first.stop);
13088
+ if (stop > 1) break;
13089
+ stops.push({
13090
+ stop,
13091
+ color: asString(s.color)
13092
+ });
13093
+ }
13094
+ const lastIncluded = at(stops, stops.length - 1);
13095
+ if (lastIncluded && lastIncluded.stop < 1) stops.push({
13096
+ stop: 1,
13097
+ color: lastIncluded.color
13098
+ });
13099
+ } else stops = processColorStops(gradientImage.stops, Math.max(width, height)).map((colorStop) => ({
13100
+ stop: colorStop.stop,
13101
+ color: asString(colorStop.color)
13102
+ }));
13103
+ stops.forEach((s) => {
13104
+ gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
13105
+ });
13106
+ ctx.fillStyle = gradient;
13107
+ ctx.fillRect(0, 0, width, height);
13108
+ return canvas;
13109
+ }
13110
+ this.context.logger.warn("Unsupported border-image source; only URL and gradient images are supported");
13111
+ return null;
13112
+ }
13113
+ /**
12823
13114
  * Filter the already-painted content beneath the element's border box and
12824
13115
  * paint the result back, approximating CSS backdrop-filter for the
12825
13116
  * blur/drop-shadow subset supported by parseSimpleFilter.
@@ -12923,9 +13214,6 @@ var ForeignObjectRenderer = class {
12923
13214
  //#endregion
12924
13215
  //#region src/core/logger.ts
12925
13216
  var Logger = class {
12926
- static {
12927
- this.instances = {};
12928
- }
12929
13217
  constructor({ id, enabled }) {
12930
13218
  this.id = id;
12931
13219
  this.enabled = enabled;
@@ -12961,6 +13249,9 @@ var Logger = class {
12961
13249
  //#endregion
12962
13250
  //#region src/core/cache-storage.ts
12963
13251
  var Cache = class {
13252
+ get _evictionEnabled() {
13253
+ return this._evictionSuspendCount === 0;
13254
+ }
12964
13255
  constructor(context, _options) {
12965
13256
  this.context = context;
12966
13257
  this._options = _options;
@@ -12968,6 +13259,7 @@ var Cache = class {
12968
13259
  this._pendingOperations = /* @__PURE__ */ new Map();
12969
13260
  this._deferMode = false;
12970
13261
  this._collectedUrls = /* @__PURE__ */ new Set();
13262
+ this._evictionSuspendCount = 0;
12971
13263
  this.maxSize = _options.maxCacheSize ?? 100;
12972
13264
  if (this.maxSize < 1) throw new Error("Cache maxSize must be at least 1");
12973
13265
  if (this.maxSize > 1e4) this.context.logger.warn(`Cache maxSize ${this.maxSize} is very large and may cause memory issues. Consider using a smaller value (recommended: 100-1000).`);
@@ -12978,6 +13270,7 @@ var Cache = class {
12978
13270
  */
12979
13271
  startDefer() {
12980
13272
  this._deferMode = true;
13273
+ this._evictionSuspendCount++;
12981
13274
  }
12982
13275
  /**
12983
13276
  * Exit defer mode and load all collected URLs in parallel, respecting
@@ -12995,17 +13288,21 @@ var Cache = class {
12995
13288
  this._deferMode = false;
12996
13289
  const urls = Array.from(this._collectedUrls);
12997
13290
  this._collectedUrls.clear();
12998
- if (urls.length === 0) return;
12999
- const limit = Math.max(1, Math.min(concurrency, 100));
13000
- this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
13001
- let settled = 0;
13002
- onProgress?.(0, urls.length);
13003
- for (let i = 0; i < urls.length; i += limit) {
13004
- const batch = urls.slice(i, i + limit);
13005
- const operations = batch.map((url) => this._addImageWithPending(url));
13006
- await Promise.allSettled(operations);
13007
- settled += batch.length;
13008
- onProgress?.(settled, urls.length);
13291
+ try {
13292
+ if (urls.length === 0) return;
13293
+ const limit = Math.max(1, Math.min(concurrency, 100));
13294
+ this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
13295
+ let settled = 0;
13296
+ onProgress?.(0, urls.length);
13297
+ for (let i = 0; i < urls.length; i += limit) {
13298
+ const batch = urls.slice(i, i + limit);
13299
+ const operations = batch.map((url) => this._addImageWithPending(url));
13300
+ await Promise.allSettled(operations);
13301
+ settled += batch.length;
13302
+ onProgress?.(settled, urls.length);
13303
+ }
13304
+ } finally {
13305
+ this._evictionSuspendCount = Math.max(0, this._evictionSuspendCount - 1);
13009
13306
  }
13010
13307
  }
13011
13308
  addImage(src) {
@@ -13072,7 +13369,7 @@ var Cache = class {
13072
13369
  */
13073
13370
  set(key, value) {
13074
13371
  if (this._cache.has(key)) this._cache.delete(key);
13075
- else if (this._cache.size >= this.maxSize) {
13372
+ else if (this._evictionEnabled && this._cache.size >= this.maxSize) {
13076
13373
  const lruKey = this._cache.keys().next().value;
13077
13374
  if (lruKey !== void 0) {
13078
13375
  this._cache.delete(lruKey);