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