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.
- package/dist/html2canvas-pro.esm.js +365 -43
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +365 -42
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +4 -4
- package/dist/lib/css/index.js +8 -0
- package/dist/lib/css/index.js.map +1 -1
- package/dist/lib/css/property-descriptors/text-decoration-style.js +25 -0
- package/dist/lib/css/property-descriptors/text-decoration-style.js.map +1 -0
- package/dist/lib/css/property-descriptors/text-decoration-thickness.js +27 -0
- package/dist/lib/css/property-descriptors/text-decoration-thickness.js.map +1 -0
- package/dist/lib/css/property-descriptors/text-overflow.js +19 -0
- package/dist/lib/css/property-descriptors/text-overflow.js.map +1 -0
- package/dist/lib/css/property-descriptors/text-underline-offset.js +24 -0
- package/dist/lib/css/property-descriptors/text-underline-offset.js.map +1 -0
- package/dist/lib/dom/document-cloner.js +50 -22
- package/dist/lib/dom/document-cloner.js.map +1 -1
- package/dist/lib/index.js +10 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/render/canvas/canvas-renderer.js +220 -18
- package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
- package/dist/types/css/index.d.ts +8 -0
- package/dist/types/css/property-descriptors/text-decoration-style.d.ts +9 -0
- package/dist/types/css/property-descriptors/text-decoration-thickness.d.ts +3 -0
- package/dist/types/css/property-descriptors/text-overflow.d.ts +6 -0
- package/dist/types/css/property-descriptors/text-underline-offset.d.ts +3 -0
- package/dist/types/dom/document-cloner.d.ts +2 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/render/canvas/canvas-renderer.d.ts +5 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 1.6.
|
|
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
|
-
|
|
6128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|