html2canvas-pro 1.6.4 → 1.6.5

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.
Files changed (30) hide show
  1. package/dist/html2canvas-pro.esm.js +365 -43
  2. package/dist/html2canvas-pro.esm.js.map +1 -1
  3. package/dist/html2canvas-pro.js +365 -42
  4. package/dist/html2canvas-pro.js.map +1 -1
  5. package/dist/html2canvas-pro.min.js +4 -4
  6. package/dist/lib/css/index.js +8 -0
  7. package/dist/lib/css/index.js.map +1 -1
  8. package/dist/lib/css/property-descriptors/text-decoration-style.js +25 -0
  9. package/dist/lib/css/property-descriptors/text-decoration-style.js.map +1 -0
  10. package/dist/lib/css/property-descriptors/text-decoration-thickness.js +27 -0
  11. package/dist/lib/css/property-descriptors/text-decoration-thickness.js.map +1 -0
  12. package/dist/lib/css/property-descriptors/text-overflow.js +19 -0
  13. package/dist/lib/css/property-descriptors/text-overflow.js.map +1 -0
  14. package/dist/lib/css/property-descriptors/text-underline-offset.js +24 -0
  15. package/dist/lib/css/property-descriptors/text-underline-offset.js.map +1 -0
  16. package/dist/lib/dom/document-cloner.js +50 -22
  17. package/dist/lib/dom/document-cloner.js.map +1 -1
  18. package/dist/lib/index.js +10 -2
  19. package/dist/lib/index.js.map +1 -1
  20. package/dist/lib/render/canvas/canvas-renderer.js +220 -18
  21. package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
  22. package/dist/types/css/index.d.ts +8 -0
  23. package/dist/types/css/property-descriptors/text-decoration-style.d.ts +9 -0
  24. package/dist/types/css/property-descriptors/text-decoration-thickness.d.ts +3 -0
  25. package/dist/types/css/property-descriptors/text-overflow.d.ts +6 -0
  26. package/dist/types/css/property-descriptors/text-underline-offset.d.ts +3 -0
  27. package/dist/types/dom/document-cloner.d.ts +2 -0
  28. package/dist/types/index.d.ts +6 -2
  29. package/dist/types/render/canvas/canvas-renderer.d.ts +5 -1
  30. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 1.6.4 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 1.6.5 <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
  */
@@ -4120,6 +4120,71 @@ const textDecorationLine = {
4120
4120
  }
4121
4121
  };
4122
4122
 
4123
+ const textDecorationStyle = {
4124
+ name: 'text-decoration-style',
4125
+ initialValue: 'solid',
4126
+ prefix: false,
4127
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4128
+ parse: (_context, style) => {
4129
+ switch (style) {
4130
+ case 'double':
4131
+ return 1 /* TEXT_DECORATION_STYLE.DOUBLE */;
4132
+ case 'dotted':
4133
+ return 2 /* TEXT_DECORATION_STYLE.DOTTED */;
4134
+ case 'dashed':
4135
+ return 3 /* TEXT_DECORATION_STYLE.DASHED */;
4136
+ case 'wavy':
4137
+ return 4 /* TEXT_DECORATION_STYLE.WAVY */;
4138
+ case 'solid':
4139
+ default:
4140
+ return 0 /* TEXT_DECORATION_STYLE.SOLID */;
4141
+ }
4142
+ }
4143
+ };
4144
+
4145
+ const textDecorationThickness = {
4146
+ name: 'text-decoration-thickness',
4147
+ initialValue: 'auto',
4148
+ prefix: false,
4149
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4150
+ parse: (_context, token) => {
4151
+ if (isIdentToken(token)) {
4152
+ switch (token.value) {
4153
+ case 'auto':
4154
+ return 'auto';
4155
+ case 'from-font':
4156
+ return 'from-font';
4157
+ }
4158
+ }
4159
+ if (isDimensionToken(token)) {
4160
+ // Convert to pixels
4161
+ return token.number;
4162
+ }
4163
+ // Default to auto
4164
+ return 'auto';
4165
+ }
4166
+ };
4167
+
4168
+ const textUnderlineOffset = {
4169
+ name: 'text-underline-offset',
4170
+ initialValue: 'auto',
4171
+ prefix: false,
4172
+ type: 0 /* PropertyDescriptorParsingType.VALUE */,
4173
+ parse: (_context, token) => {
4174
+ if (isIdentToken(token)) {
4175
+ if (token.value === 'auto') {
4176
+ return 'auto';
4177
+ }
4178
+ }
4179
+ if (isDimensionToken(token)) {
4180
+ // Return pixel value
4181
+ return token.number;
4182
+ }
4183
+ // Default to auto
4184
+ return 'auto';
4185
+ }
4186
+ };
4187
+
4123
4188
  const fontFamily = {
4124
4189
  name: `font-family`,
4125
4190
  initialValue: '',
@@ -4448,6 +4513,22 @@ const parseDisplayValue = (display) => {
4448
4513
  return 0 /* OBJECT_FIT.FILL */;
4449
4514
  };
4450
4515
 
4516
+ const textOverflow = {
4517
+ name: 'text-overflow',
4518
+ initialValue: 'clip',
4519
+ prefix: false,
4520
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4521
+ parse: (_context, textOverflow) => {
4522
+ switch (textOverflow) {
4523
+ case 'ellipsis':
4524
+ return 1 /* TEXT_OVERFLOW.ELLIPSIS */;
4525
+ case 'clip':
4526
+ default:
4527
+ return 0 /* TEXT_OVERFLOW.CLIP */;
4528
+ }
4529
+ }
4530
+ };
4531
+
4451
4532
  class CSSParsedDeclaration {
4452
4533
  constructor(context, declaration) {
4453
4534
  this.animationDuration = parse(context, duration, declaration.animationDuration);
@@ -4508,8 +4589,12 @@ class CSSParsedDeclaration {
4508
4589
  this.textAlign = parse(context, textAlign, declaration.textAlign);
4509
4590
  this.textDecorationColor = parse(context, textDecorationColor, declaration.textDecorationColor ?? declaration.color);
4510
4591
  this.textDecorationLine = parse(context, textDecorationLine, declaration.textDecorationLine ?? declaration.textDecoration);
4592
+ this.textDecorationStyle = parse(context, textDecorationStyle, declaration.textDecorationStyle);
4593
+ this.textDecorationThickness = parse(context, textDecorationThickness, declaration.textDecorationThickness);
4594
+ this.textUnderlineOffset = parse(context, textUnderlineOffset, declaration.textUnderlineOffset);
4511
4595
  this.textShadow = parse(context, textShadow, declaration.textShadow);
4512
4596
  this.textTransform = parse(context, textTransform, declaration.textTransform);
4597
+ this.textOverflow = parse(context, textOverflow, declaration.textOverflow);
4513
4598
  this.transform = parse(context, transform$1, declaration.transform);
4514
4599
  this.transformOrigin = parse(context, transformOrigin, declaration.transformOrigin);
4515
4600
  this.rotate = parse(context, rotate, declaration.rotate);
@@ -6004,6 +6089,27 @@ const createCounterText = (value, type, appendSuffix) => {
6004
6089
  };
6005
6090
 
6006
6091
  const IGNORE_ATTRIBUTE = 'data-html2canvas-ignore';
6092
+ /**
6093
+ * Find the parent ShadowRoot of an element, if any
6094
+ * @param element - The element to check
6095
+ * @returns The parent ShadowRoot or null
6096
+ */
6097
+ const findParentShadowRoot = (element) => {
6098
+ let current = element;
6099
+ while (current) {
6100
+ // Check if we've reached a shadow root boundary
6101
+ if (current.parentNode && current.parentNode.host) {
6102
+ return current.parentNode;
6103
+ }
6104
+ // Use getRootNode to check if we're in a shadow root
6105
+ const root = current.getRootNode();
6106
+ if (root && root !== current.ownerDocument && root.host) {
6107
+ return root;
6108
+ }
6109
+ current = current.parentNode;
6110
+ }
6111
+ return null;
6112
+ };
6007
6113
  class DocumentCloner {
6008
6114
  constructor(context, element, options) {
6009
6115
  this.context = context;
@@ -6015,10 +6121,17 @@ class DocumentCloner {
6015
6121
  if (!element.ownerDocument) {
6016
6122
  throw new Error('Cloned element does not have an owner document');
6017
6123
  }
6124
+ // Auto-detect Shadow Root if not explicitly provided
6125
+ if (!this.options.iframeContainer) {
6126
+ const shadowRoot = findParentShadowRoot(element);
6127
+ if (shadowRoot) {
6128
+ this.options.iframeContainer = shadowRoot;
6129
+ }
6130
+ }
6018
6131
  this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false);
6019
6132
  }
6020
6133
  toIFrame(ownerDocument, windowSize) {
6021
- const iframe = createIFrameContainer(ownerDocument, windowSize);
6134
+ const iframe = createIFrameContainer(ownerDocument, windowSize, this.options.iframeContainer);
6022
6135
  if (!iframe.contentWindow) {
6023
6136
  return Promise.reject(`Unable to find iframe window`);
6024
6137
  }
@@ -6124,20 +6237,8 @@ class DocumentCloner {
6124
6237
  return clone;
6125
6238
  }
6126
6239
  createCustomElementClone(node) {
6127
- // Ensure html2canvascustomelement is defined
6128
- if (typeof window !== 'undefined' && !customElements.get('html2canvascustomelement')) {
6129
- try {
6130
- customElements.define('html2canvascustomelement', class extends HTMLElement {
6131
- constructor() {
6132
- super();
6133
- }
6134
- });
6135
- }
6136
- catch (e) {
6137
- // Already defined or cannot define
6138
- }
6139
- }
6140
- const clone = document.createElement('html2canvascustomelement');
6240
+ const clone = document.createElement('div');
6241
+ clone.className = node.className;
6141
6242
  copyCSSStyles(node.style, clone);
6142
6243
  // Clone shadow DOM if it exists
6143
6244
  // Fix for Issue #108: This is critical for Web Components with slots to work correctly
@@ -6166,6 +6267,9 @@ class DocumentCloner {
6166
6267
  }, '');
6167
6268
  const style = node.cloneNode(false);
6168
6269
  style.textContent = css;
6270
+ if (this.options.cspNonce) {
6271
+ style.nonce = this.options.cspNonce;
6272
+ }
6169
6273
  return style;
6170
6274
  }
6171
6275
  }
@@ -6176,7 +6280,11 @@ class DocumentCloner {
6176
6280
  throw e;
6177
6281
  }
6178
6282
  }
6179
- return node.cloneNode(false);
6283
+ const cloned = node.cloneNode(false);
6284
+ if (this.options.cspNonce) {
6285
+ cloned.nonce = this.options.cspNonce;
6286
+ }
6287
+ return cloned;
6180
6288
  }
6181
6289
  createCanvasClone(canvas) {
6182
6290
  if (this.options.inlineImages && canvas.ownerDocument) {
@@ -6281,7 +6389,7 @@ class DocumentCloner {
6281
6389
  this.clonedReferenceElement = clone;
6282
6390
  }
6283
6391
  if (isBodyElement(clone)) {
6284
- createPseudoHideStyles(clone);
6392
+ createPseudoHideStyles(clone, this.options.cspNonce);
6285
6393
  }
6286
6394
  const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style));
6287
6395
  const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE);
@@ -6409,7 +6517,7 @@ var PseudoElementType;
6409
6517
  PseudoElementType[PseudoElementType["BEFORE"] = 0] = "BEFORE";
6410
6518
  PseudoElementType[PseudoElementType["AFTER"] = 1] = "AFTER";
6411
6519
  })(PseudoElementType || (PseudoElementType = {}));
6412
- const createIFrameContainer = (ownerDocument, bounds) => {
6520
+ const createIFrameContainer = (ownerDocument, bounds, customContainer) => {
6413
6521
  const cloneIframeContainer = ownerDocument.createElement('iframe');
6414
6522
  cloneIframeContainer.className = 'html2canvas-container';
6415
6523
  cloneIframeContainer.style.visibility = 'hidden';
@@ -6421,7 +6529,9 @@ const createIFrameContainer = (ownerDocument, bounds) => {
6421
6529
  cloneIframeContainer.height = bounds.height.toString();
6422
6530
  cloneIframeContainer.scrolling = 'no'; // ios won't scroll without it
6423
6531
  cloneIframeContainer.setAttribute(IGNORE_ATTRIBUTE, 'true');
6424
- ownerDocument.body.appendChild(cloneIframeContainer);
6532
+ // Use custom container if provided, otherwise use body
6533
+ const container = customContainer || ownerDocument.body;
6534
+ container.appendChild(cloneIframeContainer);
6425
6535
  return cloneIframeContainer;
6426
6536
  };
6427
6537
  const imageReady = (img) => {
@@ -6514,15 +6624,18 @@ const PSEUDO_HIDE_ELEMENT_STYLE = `{
6514
6624
  content: "" !important;
6515
6625
  display: none !important;
6516
6626
  }`;
6517
- const createPseudoHideStyles = (body) => {
6627
+ const createPseudoHideStyles = (body, cspNonce) => {
6518
6628
  createStyles(body, `.${PSEUDO_HIDE_ELEMENT_CLASS_BEFORE}${PSEUDO_BEFORE}${PSEUDO_HIDE_ELEMENT_STYLE}
6519
- .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`);
6629
+ .${PSEUDO_HIDE_ELEMENT_CLASS_AFTER}${PSEUDO_AFTER}${PSEUDO_HIDE_ELEMENT_STYLE}`, cspNonce);
6520
6630
  };
6521
- const createStyles = (body, styles) => {
6631
+ const createStyles = (body, styles, cspNonce) => {
6522
6632
  const document = body.ownerDocument;
6523
6633
  if (document) {
6524
6634
  const style = document.createElement('style');
6525
6635
  style.textContent = styles;
6636
+ if (cspNonce) {
6637
+ style.nonce = cspNonce;
6638
+ }
6526
6639
  body.appendChild(style);
6527
6640
  }
6528
6641
  };
@@ -7525,6 +7638,134 @@ class CanvasRenderer extends Renderer {
7525
7638
  }, text.bounds.left);
7526
7639
  }
7527
7640
  }
7641
+ renderTextDecoration(bounds, styles) {
7642
+ this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7643
+ // Calculate decoration line thickness
7644
+ let thickness = 1; // default
7645
+ if (typeof styles.textDecorationThickness === 'number') {
7646
+ thickness = styles.textDecorationThickness;
7647
+ }
7648
+ else if (styles.textDecorationThickness === 'from-font') {
7649
+ // Use a reasonable default based on font size
7650
+ thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
7651
+ }
7652
+ // 'auto' uses default thickness of 1
7653
+ // Calculate underline offset
7654
+ let underlineOffset = 0;
7655
+ if (typeof styles.textUnderlineOffset === 'number') {
7656
+ // It's a pixel value
7657
+ underlineOffset = styles.textUnderlineOffset;
7658
+ }
7659
+ // 'auto' uses default offset of 0
7660
+ const decorationStyle = styles.textDecorationStyle;
7661
+ styles.textDecorationLine.forEach((textDecorationLine) => {
7662
+ let y = 0;
7663
+ switch (textDecorationLine) {
7664
+ case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7665
+ y = bounds.top + bounds.height - thickness + underlineOffset;
7666
+ break;
7667
+ case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7668
+ y = bounds.top;
7669
+ break;
7670
+ case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7671
+ y = bounds.top + (bounds.height / 2 - thickness / 2);
7672
+ break;
7673
+ default:
7674
+ return;
7675
+ }
7676
+ this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
7677
+ });
7678
+ }
7679
+ drawDecorationLine(x, y, width, thickness, style) {
7680
+ switch (style) {
7681
+ case 0 /* TEXT_DECORATION_STYLE.SOLID */:
7682
+ // Solid line (default)
7683
+ this.ctx.fillRect(x, y, width, thickness);
7684
+ break;
7685
+ case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
7686
+ // Double line
7687
+ const gap = Math.max(1, thickness);
7688
+ this.ctx.fillRect(x, y, width, thickness);
7689
+ this.ctx.fillRect(x, y + thickness + gap, width, thickness);
7690
+ break;
7691
+ case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
7692
+ // Dotted line
7693
+ this.ctx.save();
7694
+ this.ctx.beginPath();
7695
+ this.ctx.setLineDash([thickness, thickness * 2]);
7696
+ this.ctx.lineWidth = thickness;
7697
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7698
+ this.ctx.moveTo(x, y + thickness / 2);
7699
+ this.ctx.lineTo(x + width, y + thickness / 2);
7700
+ this.ctx.stroke();
7701
+ this.ctx.restore();
7702
+ break;
7703
+ case 3 /* TEXT_DECORATION_STYLE.DASHED */:
7704
+ // Dashed line
7705
+ this.ctx.save();
7706
+ this.ctx.beginPath();
7707
+ this.ctx.setLineDash([thickness * 3, thickness * 2]);
7708
+ this.ctx.lineWidth = thickness;
7709
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7710
+ this.ctx.moveTo(x, y + thickness / 2);
7711
+ this.ctx.lineTo(x + width, y + thickness / 2);
7712
+ this.ctx.stroke();
7713
+ this.ctx.restore();
7714
+ break;
7715
+ case 4 /* TEXT_DECORATION_STYLE.WAVY */:
7716
+ // Wavy line (approximation using quadratic curves)
7717
+ this.ctx.save();
7718
+ this.ctx.beginPath();
7719
+ this.ctx.lineWidth = thickness;
7720
+ this.ctx.strokeStyle = this.ctx.fillStyle;
7721
+ const amplitude = thickness * 2;
7722
+ const wavelength = thickness * 4;
7723
+ let currentX = x;
7724
+ this.ctx.moveTo(currentX, y + thickness / 2);
7725
+ while (currentX < x + width) {
7726
+ const nextX = Math.min(currentX + wavelength / 2, x + width);
7727
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
7728
+ currentX = nextX;
7729
+ if (currentX < x + width) {
7730
+ const nextX2 = Math.min(currentX + wavelength / 2, x + width);
7731
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
7732
+ currentX = nextX2;
7733
+ }
7734
+ }
7735
+ this.ctx.stroke();
7736
+ this.ctx.restore();
7737
+ break;
7738
+ default:
7739
+ // Fallback to solid
7740
+ this.ctx.fillRect(x, y, width, thickness);
7741
+ }
7742
+ }
7743
+ // Helper method to truncate text and add ellipsis if needed
7744
+ truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
7745
+ const ellipsis = '...';
7746
+ const ellipsisWidth = this.ctx.measureText(ellipsis).width;
7747
+ if (letterSpacing === 0) {
7748
+ let truncated = text;
7749
+ while (this.ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
7750
+ truncated = truncated.slice(0, -1);
7751
+ }
7752
+ return truncated + ellipsis;
7753
+ }
7754
+ else {
7755
+ const letters = segmentGraphemes(text);
7756
+ let width = ellipsisWidth;
7757
+ let result = [];
7758
+ for (const letter of letters) {
7759
+ const letterWidth = this.ctx.measureText(letter).width + letterSpacing;
7760
+ if (width + letterWidth > maxWidth) {
7761
+ break;
7762
+ }
7763
+ result.push(letter);
7764
+ width += letterWidth;
7765
+ }
7766
+ return result.join('') + ellipsis;
7767
+ }
7768
+ }
7528
7769
  createFontStyle(styles) {
7529
7770
  const fontVariant = styles.fontVariant
7530
7771
  .filter((variant) => variant === 'normal' || variant === 'small-caps')
@@ -7539,13 +7780,99 @@ class CanvasRenderer extends Renderer {
7539
7780
  fontSize
7540
7781
  ];
7541
7782
  }
7542
- async renderTextNode(text, styles) {
7783
+ async renderTextNode(text, styles, containerBounds) {
7543
7784
  const [font] = this.createFontStyle(styles);
7544
7785
  this.ctx.font = font;
7545
7786
  this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
7546
7787
  this.ctx.textAlign = 'left';
7547
7788
  this.ctx.textBaseline = 'alphabetic';
7548
7789
  const paintOrder = styles.paintOrder;
7790
+ // Check if we need to apply text-overflow: ellipsis
7791
+ const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ && containerBounds && styles.overflowX === 1 /* OVERFLOW.HIDDEN */;
7792
+ // Calculate total text width if ellipsis might be needed
7793
+ let needsEllipsis = false;
7794
+ let truncatedText = '';
7795
+ if (shouldApplyEllipsis && text.textBounds.length > 0) {
7796
+ // Measure the full text content
7797
+ // Note: text.textBounds may contain whitespace characters from HTML formatting
7798
+ // We need to collapse them like the browser does for white-space: nowrap
7799
+ let fullText = text.textBounds.map((tb) => tb.text).join('');
7800
+ // Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
7801
+ // and trim leading/trailing whitespace
7802
+ fullText = fullText.replace(/\s+/g, ' ').trim();
7803
+ const fullTextWidth = this.ctx.measureText(fullText).width;
7804
+ const availableWidth = containerBounds.width;
7805
+ if (fullTextWidth > availableWidth) {
7806
+ needsEllipsis = true;
7807
+ truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
7808
+ }
7809
+ }
7810
+ // If ellipsis is needed, render the truncated text once
7811
+ if (needsEllipsis) {
7812
+ const firstBound = text.textBounds[0];
7813
+ paintOrder.forEach((paintOrderLayer) => {
7814
+ switch (paintOrderLayer) {
7815
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
7816
+ this.ctx.fillStyle = asString(styles.color);
7817
+ if (styles.letterSpacing === 0) {
7818
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7819
+ }
7820
+ else {
7821
+ const letters = segmentGraphemes(truncatedText);
7822
+ letters.reduce((left, letter) => {
7823
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7824
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7825
+ }, firstBound.bounds.left);
7826
+ }
7827
+ const textShadows = styles.textShadow;
7828
+ if (textShadows.length && truncatedText.trim().length) {
7829
+ textShadows
7830
+ .slice(0)
7831
+ .reverse()
7832
+ .forEach((textShadow) => {
7833
+ this.ctx.shadowColor = asString(textShadow.color);
7834
+ this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
7835
+ this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
7836
+ this.ctx.shadowBlur = textShadow.blur.number;
7837
+ if (styles.letterSpacing === 0) {
7838
+ this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7839
+ }
7840
+ else {
7841
+ const letters = segmentGraphemes(truncatedText);
7842
+ letters.reduce((left, letter) => {
7843
+ this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7844
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7845
+ }, firstBound.bounds.left);
7846
+ }
7847
+ });
7848
+ this.ctx.shadowColor = '';
7849
+ this.ctx.shadowOffsetX = 0;
7850
+ this.ctx.shadowOffsetY = 0;
7851
+ this.ctx.shadowBlur = 0;
7852
+ }
7853
+ break;
7854
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
7855
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
7856
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7857
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7858
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7859
+ if (styles.letterSpacing === 0) {
7860
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
7861
+ }
7862
+ else {
7863
+ const letters = segmentGraphemes(truncatedText);
7864
+ letters.reduce((left, letter) => {
7865
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
7866
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
7867
+ }, firstBound.bounds.left);
7868
+ }
7869
+ }
7870
+ break;
7871
+ }
7872
+ });
7873
+ return;
7874
+ }
7875
+ // Normal rendering (no ellipsis needed)
7549
7876
  text.textBounds.forEach((text) => {
7550
7877
  paintOrder.forEach((paintOrderLayer) => {
7551
7878
  switch (paintOrderLayer) {
@@ -7570,22 +7897,7 @@ class CanvasRenderer extends Renderer {
7570
7897
  this.ctx.shadowBlur = 0;
7571
7898
  }
7572
7899
  if (styles.textDecorationLine.length) {
7573
- this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7574
- const decorationLineHeight = 1;
7575
- styles.textDecorationLine.forEach((textDecorationLine) => {
7576
- // Fix the issue where textDecorationLine exhibits x-axis positioning errors on high-resolution devices due to varying devicePixelRatio, corrected by using relative values of element heights.
7577
- switch (textDecorationLine) {
7578
- case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7579
- this.ctx.fillRect(text.bounds.left, text.bounds.top + text.bounds.height - decorationLineHeight, text.bounds.width, decorationLineHeight);
7580
- break;
7581
- case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7582
- this.ctx.fillRect(text.bounds.left, text.bounds.top, text.bounds.width, decorationLineHeight);
7583
- break;
7584
- case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7585
- this.ctx.fillRect(text.bounds.left, text.bounds.top + (text.bounds.height / 2 - decorationLineHeight / 2), text.bounds.width, decorationLineHeight);
7586
- break;
7587
- }
7588
- });
7900
+ this.renderTextDecoration(text.bounds, styles);
7589
7901
  }
7590
7902
  break;
7591
7903
  case 1 /* PAINT_ORDER_LAYER.STROKE */:
@@ -7707,8 +8019,11 @@ class CanvasRenderer extends Renderer {
7707
8019
  const container = paint.container;
7708
8020
  const curves = paint.curves;
7709
8021
  const styles = container.styles;
8022
+ // Use content box for text overflow calculation (excludes padding and border)
8023
+ // This matches browser behavior where text-overflow uses the content width
8024
+ const textBounds = contentBox(container);
7710
8025
  for (const child of container.textNodes) {
7711
- await this.renderTextNode(child, styles);
8026
+ await this.renderTextNode(child, styles, textBounds);
7712
8027
  }
7713
8028
  if (container instanceof ImageElementContainer) {
7714
8029
  try {
@@ -8363,9 +8678,14 @@ class Context {
8363
8678
  }
8364
8679
  Context.instanceCount = 1;
8365
8680
 
8681
+ let cspNonce;
8682
+ const setCspNonce = (nonce) => {
8683
+ cspNonce = nonce;
8684
+ };
8366
8685
  const html2canvas = (element, options = {}) => {
8367
8686
  return renderElement(element, options);
8368
8687
  };
8688
+ html2canvas.setCspNonce = setCspNonce;
8369
8689
  if (typeof window !== 'undefined') {
8370
8690
  CacheStorage.setContext(window);
8371
8691
  }
@@ -8406,8 +8726,10 @@ const renderElement = async (element, opts) => {
8406
8726
  allowTaint: opts.allowTaint ?? false,
8407
8727
  onclone: opts.onclone,
8408
8728
  ignoreElements: opts.ignoreElements,
8729
+ iframeContainer: opts.iframeContainer,
8409
8730
  inlineImages: foreignObjectRendering,
8410
- copyStyles: foreignObjectRendering
8731
+ copyStyles: foreignObjectRendering,
8732
+ cspNonce
8411
8733
  };
8412
8734
  context.logger.debug(`Starting document clone with size ${windowBounds.width}x${windowBounds.height} scrolled to ${-windowBounds.left},${-windowBounds.top}`);
8413
8735
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
@@ -8477,5 +8799,5 @@ const parseBackgroundColor = (context, element, backgroundColorOverride) => {
8477
8799
  : defaultBackgroundColor;
8478
8800
  };
8479
8801
 
8480
- export { html2canvas as default, html2canvas };
8802
+ export { html2canvas as default, html2canvas, setCspNonce };
8481
8803
  //# sourceMappingURL=html2canvas-pro.esm.js.map