html2canvas-pro 2.3.2 → 2.3.3

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.3.2 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.3.3 <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
  */
@@ -3959,6 +3959,7 @@ const backgroundClip = {
3959
3959
  if (isIdentToken(token)) switch (token.value) {
3960
3960
  case "padding-box": return 1;
3961
3961
  case "content-box": return 2;
3962
+ case "text": return 3;
3962
3963
  }
3963
3964
  return 0;
3964
3965
  });
@@ -10525,6 +10526,7 @@ var CanvasRenderer = class CanvasRenderer {
10525
10526
  this.effectsRenderer.applyEffects(paint.getEffects(2));
10526
10527
  const styles = paint.container.styles;
10527
10528
  const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
10529
+ const hasTextClip = hasTextBackgroundClip(styles);
10528
10530
  const borders = [
10529
10531
  {
10530
10532
  style: styles.borderTopStyle,
@@ -10547,17 +10549,21 @@ var CanvasRenderer = class CanvasRenderer {
10547
10549
  width: styles.borderLeftWidth
10548
10550
  }
10549
10551
  ];
10550
- const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
10552
+ const backgroundClipValue = getBackgroundValueForIndex(styles.backgroundClip, 0);
10553
+ const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(backgroundClipValue, paint.curves);
10551
10554
  if (hasBackground || styles.boxShadow.length) {
10552
- this.ctx.save();
10553
- this.path(backgroundPaintingArea);
10554
- this.ctx.clip();
10555
- if (!isTransparent(styles.backgroundColor)) {
10556
- this.ctx.fillStyle = asString(styles.backgroundColor);
10557
- this.ctx.fill();
10555
+ if (hasTextClip && paint.container.textNodes.length > 0) await this.renderTextClippedBackground(paint);
10556
+ else {
10557
+ this.ctx.save();
10558
+ this.path(backgroundPaintingArea);
10559
+ this.ctx.clip();
10560
+ if (!isTransparent(styles.backgroundColor)) {
10561
+ this.ctx.fillStyle = asString(styles.backgroundColor);
10562
+ this.ctx.fill();
10563
+ }
10564
+ await this.backgroundRenderer.renderBackgroundImage(paint.container);
10565
+ this.ctx.restore();
10558
10566
  }
10559
- await this.backgroundRenderer.renderBackgroundImage(paint.container);
10560
- this.ctx.restore();
10561
10567
  styles.boxShadow.slice(0).reverse().forEach((shadow) => {
10562
10568
  this.ctx.save();
10563
10569
  const borderBoxArea = calculateBorderBoxPath(paint.curves);
@@ -10616,6 +10622,82 @@ var CanvasRenderer = class CanvasRenderer {
10616
10622
  this.effectsRenderer.applyEffects([]);
10617
10623
  return this.canvas;
10618
10624
  }
10625
+ /**
10626
+ * Render background clipped to text shape using offscreen canvas with destination-in compositing.
10627
+ * This implements background-clip: text support.
10628
+ *
10629
+ * Algorithm:
10630
+ * 1. Draw background (color + images) onto an offscreen canvas
10631
+ * 2. Use destination-in compositing to clip the background to the text glyph shapes
10632
+ * 3. Composite the result back onto the main canvas
10633
+ *
10634
+ * The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
10635
+ * background renderer handles scaling internally via BackgroundRenderer options.
10636
+ */
10637
+ async renderTextClippedBackground(paint) {
10638
+ const container = paint.container;
10639
+ const styles = container.styles;
10640
+ const bounds = container.bounds;
10641
+ if (bounds.width <= 0 || bounds.height <= 0) return;
10642
+ const offscreen = (this.canvas.ownerDocument ?? document).createElement("canvas");
10643
+ const width = Math.ceil(bounds.width);
10644
+ const height = Math.ceil(bounds.height);
10645
+ offscreen.width = width;
10646
+ offscreen.height = height;
10647
+ const offCtx = offscreen.getContext("2d");
10648
+ if (!offCtx) return;
10649
+ const [fontString] = this.textRenderer.createFontStyle(styles);
10650
+ offCtx.font = fontString;
10651
+ offCtx.textBaseline = "alphabetic";
10652
+ offCtx.textAlign = "left";
10653
+ offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
10654
+ const baseline = measureBaseline(offCtx, styles.fontSize.number);
10655
+ const bgRenderer = new BackgroundRenderer({
10656
+ ctx: offCtx,
10657
+ context: this.context,
10658
+ canvas: offscreen,
10659
+ options: {
10660
+ width,
10661
+ height,
10662
+ scale: 1
10663
+ }
10664
+ });
10665
+ if (!isTransparent(styles.backgroundColor)) {
10666
+ offCtx.fillStyle = asString(styles.backgroundColor);
10667
+ offCtx.fillRect(0, 0, width, height);
10668
+ }
10669
+ offCtx.save();
10670
+ offCtx.translate(-bounds.left, -bounds.top);
10671
+ await bgRenderer.renderBackgroundImage(container);
10672
+ offCtx.restore();
10673
+ offCtx.globalCompositeOperation = "destination-in";
10674
+ offCtx.fillStyle = "#000";
10675
+ const writingMode = styles.writingMode;
10676
+ const letterSpacing = styles.letterSpacing;
10677
+ for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
10678
+ const localLeft = textBound.bounds.left - bounds.left;
10679
+ const localTop = textBound.bounds.top - bounds.top + baseline;
10680
+ if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(offCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
10681
+ else offCtx.fillText(textBound.text, localLeft, localTop);
10682
+ }
10683
+ this.ctx.drawImage(offscreen, bounds.left, bounds.top);
10684
+ }
10685
+ /**
10686
+ * Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
10687
+ * This ensures the mask matches how the text renderer lays out the characters.
10688
+ */
10689
+ renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
10690
+ const letters = segmentGraphemes(text.text);
10691
+ let offset = x;
10692
+ for (const letter of letters) {
10693
+ if (isVerticalWritingMode(writingMode)) {
10694
+ ctx.fillText(text.text, x, y);
10695
+ return;
10696
+ }
10697
+ ctx.fillText(letter, offset, y);
10698
+ offset += ctx.measureText(letter).width + letterSpacing;
10699
+ }
10700
+ }
10619
10701
  };
10620
10702
  const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
10621
10703
  switch (clip) {
@@ -10624,6 +10706,12 @@ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
10624
10706
  default: return calculatePaddingBoxPath(curves);
10625
10707
  }
10626
10708
  };
10709
+ /**
10710
+ * Check if any background layer uses background-clip: text
10711
+ */
10712
+ const hasTextBackgroundClip = (styles) => {
10713
+ return styles.backgroundClip.some((clip) => clip === 3);
10714
+ };
10627
10715
  //#endregion
10628
10716
  //#region src/render/canvas/foreignobject-renderer.ts
10629
10717
  var ForeignObjectRenderer = class {