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