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
  */
@@ -4743,6 +4743,17 @@ const conicGradient = (context, tokens) => {
4743
4743
  stops
4744
4744
  };
4745
4745
  };
4746
+ /**
4747
+ * repeating-conic-gradient shares the conic-gradient grammar exactly; only
4748
+ * the image type differs, which switches the renderer to periodic stops
4749
+ * stacked across the full sweep.
4750
+ */
4751
+ const repeatingConicGradient = (context, tokens) => {
4752
+ return {
4753
+ ...conicGradient(context, tokens),
4754
+ type: 6
4755
+ };
4756
+ };
4746
4757
  //#endregion
4747
4758
  //#region src/css/types/functions/image-set.ts
4748
4759
  const parseResolution = (token) => {
@@ -4795,6 +4806,9 @@ const isRepeatingRadialGradient = (background) => {
4795
4806
  const isConicGradient = (background) => {
4796
4807
  return background.type === 5;
4797
4808
  };
4809
+ const isRepeatingConicGradient = (background) => {
4810
+ return background.type === 6;
4811
+ };
4798
4812
  const image = {
4799
4813
  name: "image",
4800
4814
  parse: (context, value) => {
@@ -4840,6 +4854,9 @@ const SUPPORTED_IMAGE_FUNCTIONS = {
4840
4854
  "conic-gradient": conicGradient,
4841
4855
  "-webkit-conic-gradient": conicGradient,
4842
4856
  "-moz-conic-gradient": conicGradient,
4857
+ "repeating-conic-gradient": repeatingConicGradient,
4858
+ "-webkit-repeating-conic-gradient": repeatingConicGradient,
4859
+ "-moz-repeating-conic-gradient": repeatingConicGradient,
4843
4860
  "image-set": imageSet,
4844
4861
  "-webkit-image-set": imageSet
4845
4862
  };
@@ -5328,6 +5345,7 @@ const listStyleImage = {
5328
5345
  skipCache: true,
5329
5346
  parse: (context, token) => {
5330
5347
  if (token.type === 20 && token.value === "none") return null;
5348
+ if (!isSupportedImage(token)) return null;
5331
5349
  return image.parse(context, token);
5332
5350
  }
5333
5351
  };
@@ -7772,9 +7790,12 @@ var PseudoContentResolver = class {
7772
7790
  case "open-quote":
7773
7791
  anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth++, true)));
7774
7792
  break;
7775
- case "close-quote":
7776
- anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, --this.quoteDepth, false)));
7793
+ case "close-quote": {
7794
+ const hasOpenQuote = this.quoteDepth > 0;
7795
+ this.quoteDepth = Math.max(0, this.quoteDepth - 1);
7796
+ if (hasOpenQuote) anonymousReplacedElement.appendChild(document.createTextNode(getQuote(declaration.quotes, this.quoteDepth, false)));
7777
7797
  break;
7798
+ }
7778
7799
  default: anonymousReplacedElement.appendChild(document.createTextNode(token.value));
7779
7800
  }
7780
7801
  });
@@ -8116,12 +8137,14 @@ var DocumentCloner = class {
8116
8137
  const styleBefore = window.getComputedStyle(node, ":before");
8117
8138
  const before = this.pseudoContents.resolvePseudoContent(node, clone, styleBefore, 0);
8118
8139
  if (before) clone.insertBefore(before, clone.firstChild);
8140
+ }
8141
+ if (isCustomElement(node)) copyStyles = true;
8142
+ if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
8143
+ if (checkPseudoElements) {
8119
8144
  const styleAfter = window.getComputedStyle(node, ":after");
8120
8145
  const after = this.pseudoContents.resolvePseudoContent(node, clone, styleAfter, 1);
8121
8146
  if (after) clone.appendChild(after);
8122
8147
  }
8123
- if (isCustomElement(node)) copyStyles = true;
8124
- if (!isVideoElement(node)) this.cloneChildNodes(node, clone, copyStyles);
8125
8148
  this.counters.pop(counters);
8126
8149
  if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node) || copyStyles) copyCSSStyles(style, clone);
8127
8150
  if (node.scrollTop !== 0 || node.scrollLeft !== 0) this.scrolledElements.push([
@@ -9017,10 +9040,10 @@ const LIST_OWNERS = [
9017
9040
  "UL",
9018
9041
  "MENU"
9019
9042
  ];
9020
- const parseNodeTree = (context, node, parent, root) => {
9043
+ const parseNodeTree = (context, node, parent, root, zoomScale = 1) => {
9021
9044
  for (let childNode = node.firstChild, nextNode; childNode; childNode = nextNode) {
9022
9045
  nextNode = childNode.nextSibling;
9023
- parseChildNode(context, childNode, parent, root);
9046
+ parseChildNode(context, childNode, parent, root, zoomScale);
9024
9047
  }
9025
9048
  };
9026
9049
  /**
@@ -9031,19 +9054,29 @@ const parseNodeTree = (context, node, parent, root) => {
9031
9054
  * this, slotted shadow-DOM content such as web component labels was silently
9032
9055
  * dropped because `parseNodeTree` only iterates `node.firstChild` (issue #226).
9033
9056
  */
9034
- const parseChildNode = (context, childNode, parent, root) => {
9035
- if (isTextNode(childNode) && childNode.data.length > 0) parent.textNodes.push(new TextContainer(context, childNode, parent.styles));
9036
- else if (isElementNode(childNode)) {
9037
- if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root));
9057
+ const parseChildNode = (context, childNode, parent, root, zoomScale = 1) => {
9058
+ if (isTextNode(childNode) && childNode.data.length > 0) {
9059
+ const textContainer = new TextContainer(context, childNode, parent.styles);
9060
+ if (zoomScale !== 1) {
9061
+ const originLeft = parent.bounds.left;
9062
+ const originTop = parent.bounds.top;
9063
+ 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)));
9064
+ }
9065
+ parent.textNodes.push(textContainer);
9066
+ } else if (isElementNode(childNode)) {
9067
+ if (isSlotElement(childNode) && childNode.assignedNodes) childNode.assignedNodes().forEach((assignedNode) => parseChildNode(context, assignedNode, parent, root, zoomScale));
9038
9068
  else {
9039
9069
  const container = createContainer(context, childNode);
9070
+ const ownZoom = container.styles.zoom || 1;
9071
+ if (ownZoom !== 1) container.bounds = new Bounds(container.bounds.left, container.bounds.top, container.bounds.width / ownZoom, container.bounds.height / ownZoom);
9040
9072
  if (container.styles.isVisible()) {
9041
9073
  if (createsRealStackingContext(childNode, container, root)) container.createsRealStackingContext = true;
9042
9074
  else if (createsStackingContext(container.styles)) container.createsStackingContext = true;
9043
9075
  if (LIST_OWNERS.indexOf(childNode.tagName) !== -1) container.isListOwner = true;
9044
9076
  parent.elements.push(container);
9045
- if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root);
9046
- else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root);
9077
+ const childZoom = zoomScale * (container.styles.zoom || 1);
9078
+ if (childNode.shadowRoot) parseNodeTree(context, childNode.shadowRoot, container, root, childZoom);
9079
+ else if (!isTextareaElement(childNode) && !isSVGElement(childNode) && !isSelectElement(childNode)) parseNodeTree(context, childNode, container, root, childZoom);
9047
9080
  }
9048
9081
  }
9049
9082
  }
@@ -9071,6 +9104,7 @@ const createsRealStackingContext = (node, container, root) => {
9071
9104
  };
9072
9105
  const createsStackingContext = (styles) => {
9073
9106
  if (styles.isPositioned() || styles.isFloating()) return true;
9107
+ if (styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) return true;
9074
9108
  return contains(styles.display, 268435456) || contains(styles.display, 33554432) || contains(styles.display, 536870912) || contains(styles.display, 134217728);
9075
9109
  };
9076
9110
  //#endregion
@@ -9490,9 +9524,8 @@ var ElementPaint = class {
9490
9524
  if (this.container.styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL) this.effects.push(new BlendEffect(this.container.styles.mixBlendMode));
9491
9525
  if (this.container.styles.filter !== null) this.effects.push(new FilterEffect(this.container.styles.filter));
9492
9526
  if (this.container.styles.zoom !== 1) {
9493
- const origin = this.container.styles.transformOrigin;
9494
- const offsetX = this.container.bounds.left + getAbsoluteValue(origin[0], this.container.bounds.width);
9495
- const offsetY = this.container.bounds.top + getAbsoluteValue(origin[1], this.container.bounds.height);
9527
+ const offsetX = this.container.bounds.left;
9528
+ const offsetY = this.container.bounds.top;
9496
9529
  const z = this.container.styles.zoom;
9497
9530
  const zoomMatrix = [
9498
9531
  z,
@@ -9524,7 +9557,7 @@ var ElementPaint = class {
9524
9557
  computeEffects(target, surfaceRoot) {
9525
9558
  let inFlow = [2, 3].indexOf(this.container.styles.position) === -1;
9526
9559
  let parent = this === surfaceRoot ? null : this.parent;
9527
- const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect));
9560
+ const sourceEffects = (paint) => paint.effects.filter((effect) => paint !== surfaceRoot || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect));
9528
9561
  const effects = sourceEffects(this);
9529
9562
  while (parent) {
9530
9563
  const croplessEffects = sourceEffects(parent).filter((effect) => !isClipEffect(effect));
@@ -10417,7 +10450,7 @@ var TextRenderer = class {
10417
10450
  this.ctx.shadowColor = asString(textShadow.color);
10418
10451
  this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
10419
10452
  this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
10420
- this.ctx.shadowBlur = textShadow.blur.number;
10453
+ this.ctx.shadowBlur = textShadow.blur.number * this.options.scale;
10421
10454
  this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
10422
10455
  });
10423
10456
  this.ctx.shadowColor = "";
@@ -10567,7 +10600,7 @@ var TextRenderer = class {
10567
10600
  this.ctx.textBaseline = "alphabetic";
10568
10601
  const paintOrder = styles.paintOrder;
10569
10602
  const baseline = this.measureBaselineCached(fontString ?? "", styles.fontSize.number);
10570
- if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
10603
+ if (styles.webkitLineClamp > 0 && (styles.display & 34) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
10571
10604
  if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
10572
10605
  }
10573
10606
  if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds)) return;
@@ -10766,6 +10799,7 @@ var BackgroundRenderer = class {
10766
10799
  else if (isRadialGradient(backgroundImage)) this.renderRadialGradient(container, backgroundImage, index);
10767
10800
  else if (isRepeatingRadialGradient(backgroundImage)) this.renderRepeatingRadialGradient(container, backgroundImage, index);
10768
10801
  else if (isConicGradient(backgroundImage)) this.renderConicGradient(container, backgroundImage, index);
10802
+ else if (isRepeatingConicGradient(backgroundImage)) this.renderRepeatingConicGradient(container, backgroundImage, index);
10769
10803
  if (layerCount > 0) {
10770
10804
  if ((blendModes[layerCount] ?? blendModes[0] ?? "normal") !== "normal") this.ctx.restore();
10771
10805
  }
@@ -10816,45 +10850,43 @@ var BackgroundRenderer = class {
10816
10850
  null,
10817
10851
  null
10818
10852
  ]);
10819
- const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
10853
+ const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(backgroundImage.angle, width, height);
10854
+ const gx0 = x + lx0;
10855
+ const gx1 = x + lx1;
10856
+ const gy0 = y + ly0;
10857
+ const gy1 = y + ly1;
10820
10858
  const processedStops = processColorStops(backgroundImage.stops, lineLength || 1);
10821
10859
  const lastStop = at(processedStops, processedStops.length - 1);
10822
10860
  const firstStop = at(processedStops, 0);
10823
- const stopSpan = lastStop.stop - firstStop.stop;
10824
- const patternLength = stopSpan * (lineLength || 1);
10825
- if (patternLength <= 0) {
10826
- this.renderLinearGradient(container, backgroundImage, index);
10861
+ const period = Math.max(lastStop.stop - firstStop.stop, .01);
10862
+ if (lastStop.stop - firstStop.stop < 1e-6) {
10863
+ const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
10864
+ gradient.addColorStop(0, asString(lastStop.color));
10865
+ gradient.addColorStop(1, asString(lastStop.color));
10866
+ this.path(path);
10867
+ this.ctx.fillStyle = gradient;
10868
+ this.ctx.fill();
10827
10869
  return;
10828
10870
  }
10829
- const dirX = x1 - x0;
10830
- const dirY = y1 - y0;
10831
- const scale = patternLength / (Math.sqrt(dirX * dirX + dirY * dirY) || 1);
10832
- const pX0 = x0;
10833
- const pY0 = y0;
10834
- const pX1 = x0 + dirX * scale;
10835
- const pY1 = y0 + dirY * scale;
10836
- const ownerDocument = this.canvas.ownerDocument ?? document;
10837
- const cacheKey = `rlg|${backgroundImage.angle}|${Math.round(patternLength)}|${JSON.stringify(backgroundImage.stops)}`;
10838
- let pattern = this.patternCache.get(cacheKey);
10839
- if (!pattern) {
10840
- const canvas = ownerDocument.createElement("canvas");
10841
- const canvasSize = Math.max(1, Math.ceil(patternLength));
10842
- canvas.width = canvasSize;
10843
- canvas.height = canvasSize;
10844
- const ctx = canvas.getContext("2d");
10845
- if (!ctx) return;
10846
- const gradient = ctx.createLinearGradient(pX0 - x, pY0 - y, pX1 - x, pY1 - y);
10847
- processedStops.forEach((colorStop) => {
10848
- gradient.addColorStop((colorStop.stop - firstStop.stop) / stopSpan, asString(colorStop.color));
10871
+ const stops = [];
10872
+ const cycles = Math.ceil(1 / period);
10873
+ for (let k = 0; k < cycles; k++) for (const s of processedStops) {
10874
+ const stop = k * period + (s.stop - firstStop.stop);
10875
+ if (stop > 1) break;
10876
+ stops.push({
10877
+ stop,
10878
+ color: asString(s.color)
10849
10879
  });
10850
- ctx.fillStyle = gradient;
10851
- ctx.fillRect(0, 0, canvasSize, canvasSize);
10852
- if (canvasSize > 0) {
10853
- pattern = this.ctx.createPattern(canvas, "repeat");
10854
- this.patternCache.set(cacheKey, pattern);
10855
- }
10856
10880
  }
10857
- if (pattern) this.renderRepeat(path, pattern, x, y);
10881
+ stops.push({
10882
+ stop: 1,
10883
+ color: asString(lastStop.color)
10884
+ });
10885
+ const gradient = this.ctx.createLinearGradient(gx0, gy0, gx1, gy1);
10886
+ stops.forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color));
10887
+ this.path(path);
10888
+ this.ctx.fillStyle = gradient;
10889
+ this.ctx.fill();
10858
10890
  }
10859
10891
  /**
10860
10892
  * Render a linear gradient background
@@ -10904,7 +10936,7 @@ var BackgroundRenderer = class {
10904
10936
  }
10905
10937
  if (rx > 0 && ry > 0) {
10906
10938
  const cacheKey = `rg|${Math.round(x)}x${Math.round(y)}|${Math.round(rx)}x${Math.round(ry)}|${JSON.stringify(backgroundImage.stops)}`;
10907
- const stops = processColorStops(backgroundImage.stops, rx * 2);
10939
+ const stops = processColorStops(backgroundImage.stops, rx);
10908
10940
  const lastStop = stops[stops.length - 1];
10909
10941
  let pattern = this.patternCache.get(cacheKey);
10910
10942
  if (!pattern) {
@@ -10961,14 +10993,14 @@ var BackgroundRenderer = class {
10961
10993
  ry = Math.max(ry, .01);
10962
10994
  }
10963
10995
  if (!(rx > 0 && ry > 0)) return;
10964
- const processed = processColorStops(backgroundImage.stops, rx * 2);
10996
+ const processed = processColorStops(backgroundImage.stops, rx);
10965
10997
  const firstStop = at(processed, 0);
10966
10998
  const lastStop = at(processed, processed.length - 1);
10967
10999
  const period = Math.max(lastStop.stop - firstStop.stop, .01);
10968
11000
  const stops = [];
10969
- const rings = Math.ceil(.5 / period);
11001
+ const rings = Math.ceil(1 / period);
10970
11002
  for (let k = 0; k < rings; k++) for (const s of processed) {
10971
- const stop = (k * period + (s.stop - firstStop.stop)) / 2;
11003
+ const stop = k * period + (s.stop - firstStop.stop);
10972
11004
  if (stop > 1) break;
10973
11005
  stops.push({
10974
11006
  stop,
@@ -11035,6 +11067,65 @@ var BackgroundRenderer = class {
11035
11067
  this.ctx.restore();
11036
11068
  }
11037
11069
  /**
11070
+ * Render a repeating conic gradient background.
11071
+ *
11072
+ * Canvas conic gradient stops span the full 2π sweep as [0, 1], so the
11073
+ * one-cycle stop list is stacked k·period + s for k = 0..cycles-1 over the
11074
+ * whole sweep — the same periodic-stop stacking the repeating linear and
11075
+ * radial renderers use along their gradient lines.
11076
+ */
11077
+ renderRepeatingConicGradient(container, backgroundImage, index) {
11078
+ const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
11079
+ null,
11080
+ null,
11081
+ null
11082
+ ]);
11083
+ const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
11084
+ const cx = left + getAbsoluteValue(at(position, 0), width);
11085
+ const cy = top + getAbsoluteValue(at(position, position.length - 1), height);
11086
+ const processedStops = processColorStops(backgroundImage.stops, 1);
11087
+ const firstStop = at(processedStops, 0);
11088
+ const lastStop = at(processedStops, processedStops.length - 1);
11089
+ const period = Math.max(lastStop.stop - firstStop.stop, .01);
11090
+ if (lastStop.stop - firstStop.stop < 1e-6) {
11091
+ const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
11092
+ gradient.addColorStop(0, asString(lastStop.color));
11093
+ gradient.addColorStop(1, asString(lastStop.color));
11094
+ this.path(path);
11095
+ this.ctx.save();
11096
+ this.ctx.clip();
11097
+ this.ctx.fillStyle = gradient;
11098
+ this.ctx.fill();
11099
+ this.ctx.restore();
11100
+ return;
11101
+ }
11102
+ const stops = [];
11103
+ const cycles = Math.ceil(1 / period);
11104
+ for (let k = 0; k < cycles; k++) for (const s of processedStops) {
11105
+ const stop = k * period + (s.stop - firstStop.stop);
11106
+ if (stop > 1) break;
11107
+ stops.push({
11108
+ stop,
11109
+ color: asString(s.color)
11110
+ });
11111
+ }
11112
+ const lastIncluded = at(stops, stops.length - 1);
11113
+ if (lastIncluded && lastIncluded.stop < 1) stops.push({
11114
+ stop: 1,
11115
+ color: lastIncluded.color
11116
+ });
11117
+ const gradient = this.ctx.createConicGradient(backgroundImage.angle - Math.PI / 2, cx, cy);
11118
+ stops.forEach((s) => {
11119
+ gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
11120
+ });
11121
+ this.path(path);
11122
+ this.ctx.save();
11123
+ this.ctx.clip();
11124
+ this.ctx.fillStyle = gradient;
11125
+ this.ctx.fill();
11126
+ this.ctx.restore();
11127
+ }
11128
+ /**
11038
11129
  * Render a repeating pattern with offset
11039
11130
  *
11040
11131
  * @param path - Path to fill
@@ -11459,6 +11550,13 @@ var BorderImageRenderer = class {
11459
11550
  * - Filter effects (CSS filter functions)
11460
11551
  */
11461
11552
  /**
11553
+ * Scale `blur(<n>px)` radii in a ctx.filter string by the device-pixel
11554
+ * ratio. Other filter functions (brightness, saturate, …) take
11555
+ * unitless values and are unaffected.
11556
+ */
11557
+ const BLUR_PX_RE = /blur\((\d+(?:\.\d+)?)px\)/gi;
11558
+ const scaleFilterBlur = (filter, scale) => scale === 1 ? filter : filter.replace(BLUR_PX_RE, (_match, value) => `blur(${parseFloat(value) * scale}px)`);
11559
+ /**
11462
11560
  * Effects Renderer
11463
11561
  *
11464
11562
  * Manages rendering effects stack including opacity, transforms, and clipping.
@@ -11496,6 +11594,7 @@ var EffectsRenderer = class {
11496
11594
  this.saveAtDepth = -1;
11497
11595
  this.ctx = deps.ctx;
11498
11596
  this.pathCallback = pathCallback;
11597
+ this.scale = deps.scale ?? 1;
11499
11598
  }
11500
11599
  /**
11501
11600
  * Apply multiple effects.
@@ -11534,11 +11633,11 @@ var EffectsRenderer = class {
11534
11633
  else if (isOpacityEffect(effect)) this.ctx.globalAlpha *= effect.opacity;
11535
11634
  else if (isBlendEffect(effect)) this.ctx.globalCompositeOperation = effect.compositeOperation;
11536
11635
  else if (isFilterEffect(effect)) {
11537
- this.ctx.filter = effect.safeFilterString || "none";
11636
+ this.ctx.filter = scaleFilterBlur(effect.safeFilterString, this.scale) || "none";
11538
11637
  if (effect.shadow) {
11539
- this.ctx.shadowOffsetX = effect.shadow.offsetX;
11540
- this.ctx.shadowOffsetY = effect.shadow.offsetY;
11541
- this.ctx.shadowBlur = effect.shadow.blur;
11638
+ this.ctx.shadowOffsetX = effect.shadow.offsetX * this.scale;
11639
+ this.ctx.shadowOffsetY = effect.shadow.offsetY * this.scale;
11640
+ this.ctx.shadowBlur = effect.shadow.blur * this.scale;
11542
11641
  this.ctx.shadowColor = effect.shadow.color;
11543
11642
  }
11544
11643
  }
@@ -11683,8 +11782,8 @@ const paintGradientLayer = (ctx, layer, area) => {
11683
11782
  let gradient = null;
11684
11783
  if (layer.type === 1 || layer.type === 3) {
11685
11784
  const linear = layer;
11686
- const [lineLength, gx0, gy0, gx1, gy1] = calculateGradientDirection(linear.angle || 0, width, height);
11687
- gradient = ctx.createLinearGradient(gx0, gy0, gx1, gy1);
11785
+ const [lineLength, lx0, lx1, ly0, ly1] = calculateGradientDirection(linear.angle || 0, width, height);
11786
+ gradient = ctx.createLinearGradient(area.left + lx0, area.top + ly0, area.left + lx1, area.top + ly1);
11688
11787
  processColorStops(linear.stops, lineLength || 1).forEach((s) => gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), asString(s.color)));
11689
11788
  } else if (isConicGradient(layer)) {
11690
11789
  const conic = layer;
@@ -11845,14 +11944,14 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
11845
11944
  new Vector(container.bounds.left + size * .84, container.bounds.top + size * .34085),
11846
11945
  new Vector(container.bounds.left + size * .39363, container.bounds.top + size * .79)
11847
11946
  ]);
11848
- ctx.fillStyle = asString(container.styles.accentColor ?? 707406591);
11947
+ ctx.fillStyle = asString(container.styles.accentColor ?? 7733247);
11849
11948
  ctx.fill();
11850
11949
  ctx.restore();
11851
11950
  } else if (container.type === "radio" && container.checked) {
11852
11951
  ctx.save();
11853
11952
  ctx.beginPath();
11854
11953
  ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
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
  }
@@ -11951,22 +12050,51 @@ async function renderListMarker(ctx, context, textRenderer, paint, container, st
11951
12050
  const url = img.url;
11952
12051
  try {
11953
12052
  const image = await context.cache.match(url);
11954
- ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
12053
+ const fontSize = getAbsoluteValue(styles.fontSize, 0);
12054
+ const naturalWidth = image?.naturalWidth || image.width;
12055
+ const naturalHeight = image?.naturalHeight || image.height;
12056
+ if (naturalWidth <= 0 || naturalHeight <= 0) return;
12057
+ const markerScale = Math.min(fontSize / naturalWidth, fontSize / naturalHeight);
12058
+ const markerWidth = naturalWidth * markerScale;
12059
+ const markerHeight = naturalHeight * markerScale;
12060
+ const markerCenterY = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
12061
+ ctx.drawImage(image, container.bounds.left - (markerWidth + 10), markerCenterY - markerHeight / 2, markerWidth, markerHeight);
11955
12062
  } catch (e) {
11956
12063
  context.logger.error(`Error loading list-style-image ${url}`);
11957
12064
  context.onError?.(e instanceof Error ? e : new Error(String(e)));
11958
12065
  }
11959
12066
  }
11960
12067
  } else if (paint.listValue && container.styles.listStyleType !== -1) {
11961
- const [font] = textRenderer.createFontStyle(styles);
11962
- ctx.font = font ?? "";
11963
- ctx.fillStyle = asString(styles.color);
11964
- ctx.textBaseline = "middle";
11965
- ctx.textAlign = "right";
11966
- 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);
11967
- textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
11968
- ctx.textBaseline = "bottom";
11969
- ctx.textAlign = "left";
12068
+ const type = container.styles.listStyleType;
12069
+ if (type === 0 || type === 1 || type === 2) {
12070
+ const size = styles.fontSize.number * .45;
12071
+ const cx = container.bounds.left - size * .9;
12072
+ const cy = container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width) + computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2;
12073
+ ctx.save();
12074
+ ctx.fillStyle = asString(styles.color);
12075
+ ctx.beginPath();
12076
+ if (type === 2) ctx.rect(cx - size / 2, cy - size / 2, size, size);
12077
+ else {
12078
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
12079
+ if (type === 1) {
12080
+ ctx.strokeStyle = asString(styles.color);
12081
+ ctx.lineWidth = size * .18;
12082
+ }
12083
+ }
12084
+ if (type === 1) ctx.stroke();
12085
+ else ctx.fill();
12086
+ ctx.restore();
12087
+ } else {
12088
+ const [font] = textRenderer.createFontStyle(styles);
12089
+ ctx.font = font ?? "";
12090
+ ctx.fillStyle = asString(styles.color);
12091
+ ctx.textBaseline = "middle";
12092
+ ctx.textAlign = "right";
12093
+ 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);
12094
+ textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
12095
+ ctx.textBaseline = "bottom";
12096
+ ctx.textAlign = "left";
12097
+ }
11970
12098
  }
11971
12099
  }
11972
12100
  /**
@@ -12209,31 +12337,33 @@ var TextClipRenderer = class {
12209
12337
  constructor(deps) {
12210
12338
  this.ctx = deps.ctx;
12211
12339
  this.context = deps.context;
12340
+ this.scale = deps.scale;
12212
12341
  this.createFontStyle = deps.createFontStyle;
12213
12342
  }
12214
12343
  async render(paint) {
12215
12344
  const container = paint.container;
12216
12345
  const styles = container.styles;
12217
12346
  const bounds = container.bounds;
12347
+ const scale = this.scale;
12218
12348
  if (bounds.width <= 0 || bounds.height <= 0) return;
12219
12349
  const ownerDocument = this.ctx.canvas.ownerDocument ?? document;
12220
- const offscreen = ownerDocument.createElement("canvas");
12221
12350
  const width = Math.ceil(bounds.width);
12222
12351
  const height = Math.ceil(bounds.height);
12223
- offscreen.width = width;
12224
- offscreen.height = height;
12225
- const offCtx = offscreen.getContext("2d");
12226
- if (!offCtx) return;
12352
+ const deviceWidth = Math.ceil(bounds.width * scale);
12353
+ const deviceHeight = Math.ceil(bounds.height * scale);
12354
+ const offscreen = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
12355
+ if (!offscreen) return;
12356
+ offscreen.ctx.scale(scale, scale);
12227
12357
  const [fontString] = this.createFontStyle(styles);
12228
- offCtx.font = fontString ?? "";
12229
- offCtx.textBaseline = "alphabetic";
12230
- offCtx.textAlign = "left";
12231
- offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
12232
- const baseline = measureBaseline(offCtx, styles.fontSize.number);
12358
+ offscreen.ctx.font = fontString ?? "";
12359
+ offscreen.ctx.textBaseline = "alphabetic";
12360
+ offscreen.ctx.textAlign = "left";
12361
+ offscreen.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
12362
+ const baseline = measureBaseline(offscreen.ctx, styles.fontSize.number);
12233
12363
  const bgRenderer = new BackgroundRenderer({
12234
- ctx: offCtx,
12364
+ ctx: offscreen.ctx,
12235
12365
  context: this.context,
12236
- canvas: offscreen,
12366
+ canvas: offscreen.canvas,
12237
12367
  options: {
12238
12368
  width,
12239
12369
  height,
@@ -12241,51 +12371,115 @@ var TextClipRenderer = class {
12241
12371
  }
12242
12372
  });
12243
12373
  if (!isTransparent(styles.backgroundColor)) {
12244
- offCtx.fillStyle = asString(styles.backgroundColor);
12245
- offCtx.fillRect(0, 0, width, height);
12374
+ offscreen.ctx.fillStyle = asString(styles.backgroundColor);
12375
+ offscreen.ctx.fillRect(0, 0, width, height);
12246
12376
  }
12247
- offCtx.save();
12248
- offCtx.translate(-bounds.left, -bounds.top);
12377
+ offscreen.ctx.save();
12378
+ offscreen.ctx.translate(-bounds.left, -bounds.top);
12249
12379
  await bgRenderer.renderBackgroundImage(container);
12250
- offCtx.restore();
12251
- const mask = ownerDocument.createElement("canvas");
12252
- mask.width = width;
12253
- mask.height = height;
12254
- const maskCtx = mask.getContext("2d");
12255
- if (!maskCtx) return;
12256
- maskCtx.font = offCtx.font;
12257
- maskCtx.textBaseline = offCtx.textBaseline;
12258
- maskCtx.textAlign = offCtx.textAlign;
12259
- maskCtx.direction = offCtx.direction;
12260
- maskCtx.fillStyle = "#000";
12261
- const writingMode = styles.writingMode;
12262
- const letterSpacing = styles.letterSpacing;
12263
- for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
12264
- const localLeft = textBound.bounds.left - bounds.left;
12265
- const localTop = textBound.bounds.top - bounds.top + baseline;
12266
- if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(maskCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
12267
- else maskCtx.fillText(textBound.text, localLeft, localTop);
12268
- }
12269
- offCtx.globalCompositeOperation = "destination-in";
12270
- offCtx.drawImage(mask, 0, 0);
12271
- this.ctx.drawImage(offscreen, bounds.left, bounds.top);
12380
+ offscreen.ctx.restore();
12381
+ const drawOptions = {
12382
+ bounds,
12383
+ baseline,
12384
+ letterSpacing: styles.letterSpacing,
12385
+ writingMode: styles.writingMode
12386
+ };
12387
+ const mask = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, offscreen.ctx);
12388
+ if (!mask) return;
12389
+ const hasStroke = !!styles.webkitTextStrokeWidth;
12390
+ if (hasStroke) {
12391
+ mask.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
12392
+ mask.ctx.lineWidth = styles.webkitTextStrokeWidth;
12393
+ mask.ctx.lineJoin = this.getTextStrokeLineJoin();
12394
+ }
12395
+ for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(mask, textBound, drawOptions, (ctx, text, x, y) => {
12396
+ ctx.fillText(text, x, y);
12397
+ if (hasStroke) ctx.strokeText(text, x, y);
12398
+ });
12399
+ offscreen.ctx.globalCompositeOperation = "destination-in";
12400
+ offscreen.ctx.drawImage(mask.canvas, 0, 0, width, height);
12401
+ const shadowCanvases = this.renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, offscreen.ctx, styles, drawOptions);
12402
+ if (shadowCanvases) {
12403
+ const composite = this.createLayer(ownerDocument, deviceWidth, deviceHeight);
12404
+ if (!composite) return;
12405
+ for (const shadowCanvas of shadowCanvases) composite.ctx.drawImage(shadowCanvas, 0, 0);
12406
+ composite.ctx.drawImage(offscreen.canvas, 0, 0);
12407
+ this.ctx.drawImage(composite.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
12408
+ } else this.ctx.drawImage(offscreen.canvas, bounds.left, bounds.top, bounds.width, bounds.height);
12409
+ }
12410
+ createLayer(ownerDocument, width, height) {
12411
+ const canvas = ownerDocument.createElement("canvas");
12412
+ canvas.width = width;
12413
+ canvas.height = height;
12414
+ const ctx = canvas.getContext("2d");
12415
+ if (!ctx) return null;
12416
+ return {
12417
+ canvas,
12418
+ ctx
12419
+ };
12420
+ }
12421
+ createGlyphLayer(ownerDocument, width, height, fontSource) {
12422
+ const layer = this.createLayer(ownerDocument, width, height);
12423
+ if (!layer) return null;
12424
+ layer.ctx.scale(this.scale, this.scale);
12425
+ layer.ctx.font = fontSource.font;
12426
+ layer.ctx.textBaseline = fontSource.textBaseline;
12427
+ layer.ctx.textAlign = fontSource.textAlign;
12428
+ layer.ctx.direction = fontSource.direction;
12429
+ layer.ctx.fillStyle = "#000";
12430
+ return layer;
12272
12431
  }
12273
12432
  /**
12274
- * Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
12275
- * This ensures the mask matches how the text renderer lays out the characters.
12433
+ * Draw one text bound onto a glyph layer, splitting into per-glyph draws
12434
+ * when letter-spacing is in effect so the layout matches the main text
12435
+ * renderer. Vertical writing modes fall back to a single draw call.
12276
12436
  */
12277
- renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
12278
- const letters = segmentGraphemes(text.text);
12437
+ drawGlyphs(layer, textBound, drawOptions, draw) {
12438
+ const x = textBound.bounds.left - drawOptions.bounds.left;
12439
+ const y = textBound.bounds.top - drawOptions.bounds.top + drawOptions.baseline;
12440
+ const { letterSpacing, writingMode } = drawOptions;
12441
+ if (letterSpacing <= 0) {
12442
+ draw(layer.ctx, textBound.text, x, y);
12443
+ return;
12444
+ }
12445
+ if (isVerticalWritingMode(writingMode)) {
12446
+ draw(layer.ctx, textBound.text, x, y);
12447
+ return;
12448
+ }
12279
12449
  let offset = x;
12280
- for (const letter of letters) {
12281
- if (isVerticalWritingMode(writingMode)) {
12282
- ctx.fillText(text.text, x, y);
12283
- return;
12284
- }
12285
- ctx.fillText(letter, offset, y);
12286
- offset += ctx.measureText(letter).width + letterSpacing;
12450
+ for (const letter of segmentGraphemes(textBound.text)) {
12451
+ draw(layer.ctx, letter, offset, y);
12452
+ offset += layer.ctx.measureText(letter).width + letterSpacing;
12287
12453
  }
12288
12454
  }
12455
+ /**
12456
+ * Paint the text-shadow silhouettes for every glyph, one canvas per
12457
+ * shadow, returned front-to-back (the last shadow sits deepest). Returns
12458
+ * null when the element has no text shadows.
12459
+ */
12460
+ renderShadowSilhouettes(paint, ownerDocument, deviceWidth, deviceHeight, fontSource, styles, drawOptions) {
12461
+ const textShadows = styles.textShadow;
12462
+ if (!textShadows.length) return null;
12463
+ const canvases = [];
12464
+ for (let i = textShadows.length - 1; i >= 0; i--) {
12465
+ const textShadow = at(textShadows, i);
12466
+ const layer = this.createGlyphLayer(ownerDocument, deviceWidth, deviceHeight, fontSource);
12467
+ if (!layer) continue;
12468
+ layer.ctx.shadowColor = asString(textShadow.color);
12469
+ layer.ctx.shadowOffsetX = textShadow.offsetX.number * this.scale;
12470
+ layer.ctx.shadowOffsetY = textShadow.offsetY.number * this.scale;
12471
+ layer.ctx.shadowBlur = textShadow.blur.number * this.scale;
12472
+ layer.ctx.fillStyle = asString(textShadow.color);
12473
+ for (const textNode of paint.container.textNodes) for (const textBound of textNode.textBounds) this.drawGlyphs(layer, textBound, drawOptions, (ctx, text, x, y) => {
12474
+ ctx.fillText(text, x, y);
12475
+ });
12476
+ canvases.push(layer.canvas);
12477
+ }
12478
+ return canvases.length ? canvases : null;
12479
+ }
12480
+ getTextStrokeLineJoin() {
12481
+ return (typeof window !== "undefined" ? window : void 0)?.chrome ? "miter" : "round";
12482
+ }
12289
12483
  };
12290
12484
  //#endregion
12291
12485
  //#region src/render/canvas/canvas-renderer.ts
@@ -12330,7 +12524,10 @@ var CanvasRenderer = class CanvasRenderer {
12330
12524
  formatPath: (paths) => this.formatPath(paths)
12331
12525
  });
12332
12526
  this.borderImageRenderer = new BorderImageRenderer(this.ctx);
12333
- this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
12527
+ this.effectsRenderer = new EffectsRenderer({
12528
+ ctx: this.ctx,
12529
+ scale: options.scale
12530
+ }, { path: (paths) => this.path(paths) });
12334
12531
  this.textRenderer = new TextRenderer({
12335
12532
  ctx: this.ctx,
12336
12533
  options: { scale: options.scale }
@@ -12338,6 +12535,7 @@ var CanvasRenderer = class CanvasRenderer {
12338
12535
  this.textClipRenderer = new TextClipRenderer({
12339
12536
  ctx: this.ctx,
12340
12537
  context: this.context,
12538
+ scale: options.scale,
12341
12539
  createFontStyle: (styles) => this.textRenderer.createFontStyle(styles)
12342
12540
  });
12343
12541
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
@@ -12348,8 +12546,9 @@ var CanvasRenderer = class CanvasRenderer {
12348
12546
  const filter = parseSimpleFilter(styles.filter);
12349
12547
  const hasMask = styles.maskImage.length > 0;
12350
12548
  const reflect = styles.webkitBoxReflect;
12351
- if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
12352
- if (!await this.renderCompositedStack(stack, filter, hasMask, reflect)) {
12549
+ const hasBlend = styles.mixBlendMode !== MIX_BLEND_MODE$1.NORMAL;
12550
+ if (stack.element !== this.surfaceRoot && !this.legacySubtree && (hasMask || reflect || hasBlend || filter && (styles.filter || styles.opacity < 1)) && this.canComposite(stack.element)) {
12551
+ if (!await this.renderCompositedStack(stack, filter, hasMask, reflect, hasBlend)) {
12353
12552
  this.legacySubtree = true;
12354
12553
  try {
12355
12554
  await this.renderStackContent(stack);
@@ -12365,7 +12564,7 @@ var CanvasRenderer = class CanvasRenderer {
12365
12564
  canComposite(paint) {
12366
12565
  const supported = (container) => {
12367
12566
  const styles = container.styles;
12368
- 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;
12567
+ return !styles.isTransformed() && styles.zoom === 1 && styles.clipPath.type === 0 && !contains(styles.display, 2048) && parseSimpleFilter(styles.filter) !== null;
12369
12568
  };
12370
12569
  const subtree = (container) => supported(container) && container.elements.every(subtree);
12371
12570
  let ancestor = paint.parent;
@@ -12375,7 +12574,7 @@ var CanvasRenderer = class CanvasRenderer {
12375
12574
  }
12376
12575
  return subtree(paint.container);
12377
12576
  }
12378
- async renderCompositedStack(stack, filter, hasMask, reflect) {
12577
+ async renderCompositedStack(stack, filter, hasMask, reflect, hasBlend = false) {
12379
12578
  const signal = this.options.signal;
12380
12579
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
12381
12580
  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);
@@ -12405,9 +12604,14 @@ var CanvasRenderer = class CanvasRenderer {
12405
12604
  if (hasMask) await this.applyMask(composited, stack.element.container.styles, bounds);
12406
12605
  if (signal?.aborted) throw new DOMException("The operation was aborted.", "AbortError");
12407
12606
  const own = stack.element.effects;
12408
- const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isClipEffect(effect));
12607
+ const effects = stack.element.getEffects(4, this.surfaceRoot).filter((effect) => !own.includes(effect) || !isFilterEffect(effect) && !isOpacityEffect(effect) && !isBlendEffect(effect) && !isClipEffect(effect));
12409
12608
  this.effectsRenderer.applyEffects(effects);
12410
- this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12609
+ if (hasBlend) {
12610
+ this.ctx.save();
12611
+ this.ctx.globalCompositeOperation = stack.element.container.styles.mixBlendMode;
12612
+ this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12613
+ this.ctx.restore();
12614
+ } else this.ctx.drawImage(composited, options.x, options.y, composited.width / options.scale, composited.height / options.scale);
12411
12615
  if (reflect) await this.drawReflection(composited, stack.element.container.bounds, bounds, reflect);
12412
12616
  return true;
12413
12617
  } catch (error) {
@@ -12497,6 +12701,7 @@ var CanvasRenderer = class CanvasRenderer {
12497
12701
  maskCanvas.height = h;
12498
12702
  const mctx = maskCanvas.getContext("2d");
12499
12703
  if (mctx) {
12704
+ mctx.scale(scale, scale);
12500
12705
  await paintMaskLayers(mctx, [reflect.mask], {
12501
12706
  maskPosition: [],
12502
12707
  maskRepeat: [],
@@ -12517,7 +12722,7 @@ var CanvasRenderer = class CanvasRenderer {
12517
12722
  ctx.scale(1, -1);
12518
12723
  }
12519
12724
  ctx.globalCompositeOperation = "destination-in";
12520
- ctx.drawImage(maskCanvas, 0, 0);
12725
+ ctx.drawImage(maskCanvas, 0, 0, w, h);
12521
12726
  } finally {
12522
12727
  ctx.restore();
12523
12728
  }
@@ -12729,26 +12934,27 @@ var CanvasRenderer = class CanvasRenderer {
12729
12934
  }
12730
12935
  if (styles.borderImageSource) {
12731
12936
  const source = styles.borderImageSource;
12937
+ const bounds = paint.container.bounds;
12938
+ const borderWidths = [
12939
+ Math.max(0, styles.borderTopWidth),
12940
+ Math.max(0, styles.borderRightWidth),
12941
+ Math.max(0, styles.borderBottomWidth),
12942
+ Math.max(0, styles.borderLeftWidth)
12943
+ ];
12944
+ let image = null;
12732
12945
  if (source.type === 0) {
12733
12946
  const url = source.url;
12734
12947
  try {
12735
- const image = await this.context.cache.match(url);
12736
- if (image) {
12737
- const bounds = paint.container.bounds;
12738
- const borderWidths = [
12739
- Math.max(0, styles.borderTopWidth),
12740
- Math.max(0, styles.borderRightWidth),
12741
- Math.max(0, styles.borderBottomWidth),
12742
- Math.max(0, styles.borderLeftWidth)
12743
- ];
12744
- const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
12745
- const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
12746
- const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
12747
- this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
12748
- }
12948
+ image = await this.context.cache.match(url) ?? null;
12749
12949
  } catch (e) {
12750
12950
  this.context.logger.error(`Error loading border-image ${url}`);
12751
12951
  }
12952
+ } else image = this.rasterizeGradientSource(source, bounds);
12953
+ if (image) {
12954
+ const outset = resolveBorderImageOutset(styles.borderImageOutset, borderWidths);
12955
+ const area = bounds.add(-outset.left, -outset.top, outset.left + outset.right, outset.top + outset.bottom);
12956
+ const widths = resolveBorderImageWidths(styles.borderImageWidth, borderWidths, area);
12957
+ this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, widths[0], widths[1], widths[2], widths[3], outset);
12752
12958
  }
12753
12959
  return;
12754
12960
  }
@@ -12816,6 +13022,91 @@ var CanvasRenderer = class CanvasRenderer {
12816
13022
  }
12817
13023
  }
12818
13024
  /**
13025
+ * Rasterise a gradient border-image-source across the border image area.
13026
+ * CSS allows any <image> as the border-image source; gradients are painted
13027
+ * over the whole border-image box and then sliced by the regular 9-slice
13028
+ * pipeline, matching browser behaviour for `border-image: linear-gradient()`.
13029
+ * Returns null when the gradient could not be painted.
13030
+ */
13031
+ rasterizeGradientSource(source, bounds) {
13032
+ const width = Math.max(1, Math.ceil(bounds.width));
13033
+ const height = Math.max(1, Math.ceil(bounds.height));
13034
+ const canvas = (this.ctx.canvas.ownerDocument ?? document).createElement("canvas");
13035
+ canvas.width = width;
13036
+ canvas.height = height;
13037
+ const ctx = canvas.getContext("2d");
13038
+ if (!ctx) return null;
13039
+ if (source.type === 1 || source.type === 3) {
13040
+ const gradientImage = source;
13041
+ const [lineLength, x0, y0, x1, y1] = calculateGradientDirection(gradientImage.angle, width, height);
13042
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
13043
+ processColorStops(gradientImage.stops, lineLength || width).forEach((colorStop) => {
13044
+ gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
13045
+ });
13046
+ ctx.fillStyle = gradient;
13047
+ ctx.fillRect(0, 0, width, height);
13048
+ return canvas;
13049
+ }
13050
+ if (source.type === 2 || source.type === 4) {
13051
+ const gradientImage = source;
13052
+ const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
13053
+ const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
13054
+ const [rx, ry] = calculateRadius(gradientImage, cx, cy, width, height);
13055
+ if (rx <= 0 || ry <= 0) return null;
13056
+ ctx.save();
13057
+ ctx.translate(cx, cy);
13058
+ ctx.scale(1, ry / rx);
13059
+ ctx.translate(-cx, -cy);
13060
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, rx);
13061
+ processColorStops(gradientImage.stops, rx).forEach((colorStop) => {
13062
+ gradient.addColorStop(Math.min(1, Math.max(0, colorStop.stop)), asString(colorStop.color));
13063
+ });
13064
+ ctx.fillStyle = gradient;
13065
+ ctx.fillRect(0, 0, width, height);
13066
+ ctx.restore();
13067
+ return canvas;
13068
+ }
13069
+ if (source.type === 5 || source.type === 6) {
13070
+ const gradientImage = source;
13071
+ const cx = getAbsoluteValue(at(gradientImage.position, 0), width);
13072
+ const cy = getAbsoluteValue(at(gradientImage.position, 1), height);
13073
+ const gradient = ctx.createConicGradient(gradientImage.angle - Math.PI / 2, cx, cy);
13074
+ let stops;
13075
+ if (source.type === 6) {
13076
+ const processed = processColorStops(gradientImage.stops, 1);
13077
+ const first = at(processed, 0);
13078
+ const last = at(processed, processed.length - 1);
13079
+ const period = Math.max(last.stop - first.stop, .01);
13080
+ stops = [];
13081
+ const cycles = Math.ceil(1 / period);
13082
+ for (let k = 0; k < cycles; k++) for (const s of processed) {
13083
+ const stop = k * period + (s.stop - first.stop);
13084
+ if (stop > 1) break;
13085
+ stops.push({
13086
+ stop,
13087
+ color: asString(s.color)
13088
+ });
13089
+ }
13090
+ const lastIncluded = at(stops, stops.length - 1);
13091
+ if (lastIncluded && lastIncluded.stop < 1) stops.push({
13092
+ stop: 1,
13093
+ color: lastIncluded.color
13094
+ });
13095
+ } else stops = processColorStops(gradientImage.stops, Math.max(width, height)).map((colorStop) => ({
13096
+ stop: colorStop.stop,
13097
+ color: asString(colorStop.color)
13098
+ }));
13099
+ stops.forEach((s) => {
13100
+ gradient.addColorStop(Math.min(1, Math.max(0, s.stop)), s.color);
13101
+ });
13102
+ ctx.fillStyle = gradient;
13103
+ ctx.fillRect(0, 0, width, height);
13104
+ return canvas;
13105
+ }
13106
+ this.context.logger.warn("Unsupported border-image source; only URL and gradient images are supported");
13107
+ return null;
13108
+ }
13109
+ /**
12819
13110
  * Filter the already-painted content beneath the element's border box and
12820
13111
  * paint the result back, approximating CSS backdrop-filter for the
12821
13112
  * blur/drop-shadow subset supported by parseSimpleFilter.
@@ -12919,9 +13210,6 @@ var ForeignObjectRenderer = class {
12919
13210
  //#endregion
12920
13211
  //#region src/core/logger.ts
12921
13212
  var Logger = class {
12922
- static {
12923
- this.instances = {};
12924
- }
12925
13213
  constructor({ id, enabled }) {
12926
13214
  this.id = id;
12927
13215
  this.enabled = enabled;
@@ -12957,6 +13245,9 @@ var Logger = class {
12957
13245
  //#endregion
12958
13246
  //#region src/core/cache-storage.ts
12959
13247
  var Cache = class {
13248
+ get _evictionEnabled() {
13249
+ return this._evictionSuspendCount === 0;
13250
+ }
12960
13251
  constructor(context, _options) {
12961
13252
  this.context = context;
12962
13253
  this._options = _options;
@@ -12964,6 +13255,7 @@ var Cache = class {
12964
13255
  this._pendingOperations = /* @__PURE__ */ new Map();
12965
13256
  this._deferMode = false;
12966
13257
  this._collectedUrls = /* @__PURE__ */ new Set();
13258
+ this._evictionSuspendCount = 0;
12967
13259
  this.maxSize = _options.maxCacheSize ?? 100;
12968
13260
  if (this.maxSize < 1) throw new Error("Cache maxSize must be at least 1");
12969
13261
  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).`);
@@ -12974,6 +13266,7 @@ var Cache = class {
12974
13266
  */
12975
13267
  startDefer() {
12976
13268
  this._deferMode = true;
13269
+ this._evictionSuspendCount++;
12977
13270
  }
12978
13271
  /**
12979
13272
  * Exit defer mode and load all collected URLs in parallel, respecting
@@ -12991,17 +13284,21 @@ var Cache = class {
12991
13284
  this._deferMode = false;
12992
13285
  const urls = Array.from(this._collectedUrls);
12993
13286
  this._collectedUrls.clear();
12994
- if (urls.length === 0) return;
12995
- const limit = Math.max(1, Math.min(concurrency, 100));
12996
- this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
12997
- let settled = 0;
12998
- onProgress?.(0, urls.length);
12999
- for (let i = 0; i < urls.length; i += limit) {
13000
- const batch = urls.slice(i, i + limit);
13001
- const operations = batch.map((url) => this._addImageWithPending(url));
13002
- await Promise.allSettled(operations);
13003
- settled += batch.length;
13004
- onProgress?.(settled, urls.length);
13287
+ try {
13288
+ if (urls.length === 0) return;
13289
+ const limit = Math.max(1, Math.min(concurrency, 100));
13290
+ this.context.logger.debug(`Preloading ${urls.length} image(s) with concurrency ${limit}`);
13291
+ let settled = 0;
13292
+ onProgress?.(0, urls.length);
13293
+ for (let i = 0; i < urls.length; i += limit) {
13294
+ const batch = urls.slice(i, i + limit);
13295
+ const operations = batch.map((url) => this._addImageWithPending(url));
13296
+ await Promise.allSettled(operations);
13297
+ settled += batch.length;
13298
+ onProgress?.(settled, urls.length);
13299
+ }
13300
+ } finally {
13301
+ this._evictionSuspendCount = Math.max(0, this._evictionSuspendCount - 1);
13005
13302
  }
13006
13303
  }
13007
13304
  addImage(src) {
@@ -13068,7 +13365,7 @@ var Cache = class {
13068
13365
  */
13069
13366
  set(key, value) {
13070
13367
  if (this._cache.has(key)) this._cache.delete(key);
13071
- else if (this._cache.size >= this.maxSize) {
13368
+ else if (this._evictionEnabled && this._cache.size >= this.maxSize) {
13072
13369
  const lruKey = this._cache.keys().next().value;
13073
13370
  if (lruKey !== void 0) {
13074
13371
  this._cache.delete(lruKey);