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