html2canvas-pro 2.3.8 → 2.4.0
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.cjs +46 -30
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +46 -30
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +46 -30
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +4 -4
- package/dist/lib/dom/document-cloner.js +10 -8
- package/dist/lib/render/canvas/canvas-renderer.js +11 -8
- package/dist/lib/render/canvas/content-renderer.js +22 -9
- package/dist/lib/render/canvas/font-utils.js +24 -7
- package/dist/types/render/canvas/content-renderer.d.ts +9 -0
- package/dist/types/render/canvas/font-utils.d.ts +10 -2
- package/package.json +1 -1
|
@@ -67,11 +67,6 @@ class DocumentCloner {
|
|
|
67
67
|
this.scrolledElements.forEach(restoreNodeScroll);
|
|
68
68
|
if (cloneWindow) {
|
|
69
69
|
cloneWindow.scrollTo(windowSize.left, windowSize.top);
|
|
70
|
-
if (/AppleWebKit/g.test(navigator.userAgent) &&
|
|
71
|
-
(cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)) {
|
|
72
|
-
this.context.logger.warn('Unable to restore scroll position for cloned document');
|
|
73
|
-
this.context.windowBounds = this.context.windowBounds.add(cloneWindow.scrollX - windowSize.left, cloneWindow.scrollY - windowSize.top, 0, 0);
|
|
74
|
-
}
|
|
75
70
|
}
|
|
76
71
|
const onclone = this.options.onclone;
|
|
77
72
|
const referenceElement = this.clonedReferenceElement;
|
|
@@ -85,9 +80,16 @@ class DocumentCloner {
|
|
|
85
80
|
await imagesReady(documentClone);
|
|
86
81
|
}
|
|
87
82
|
if (typeof onclone === 'function') {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
await Promise.resolve().then(() => onclone(documentClone, referenceElement));
|
|
84
|
+
}
|
|
85
|
+
// onclone can change layout above the viewport and trigger browser scroll
|
|
86
|
+
// anchoring. Reapply the requested offset after the callback so element
|
|
87
|
+
// bounds stay aligned with the window bounds used by the renderer.
|
|
88
|
+
cloneWindow.scrollTo(windowSize.left, windowSize.top);
|
|
89
|
+
if (/AppleWebKit/g.test(navigator.userAgent) &&
|
|
90
|
+
(cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)) {
|
|
91
|
+
this.context.logger.warn('Unable to restore scroll position for cloned document');
|
|
92
|
+
this.context.windowBounds = this.context.windowBounds.add(cloneWindow.scrollX - windowSize.left, cloneWindow.scrollY - windowSize.top, 0, 0);
|
|
91
93
|
}
|
|
92
94
|
return iframe;
|
|
93
95
|
});
|
|
@@ -187,14 +187,17 @@ class CanvasRenderer {
|
|
|
187
187
|
}
|
|
188
188
|
mask(paths) {
|
|
189
189
|
this.ctx.beginPath();
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
this.ctx.
|
|
197
|
-
this.ctx.lineTo(
|
|
190
|
+
// Anchor the mask rectangle to (options.x, options.y) instead of (0, 0)
|
|
191
|
+
// because ctx.translate(-options.x, -options.y) shifts the coordinate system.
|
|
192
|
+
// Fix for Issue #230: inset box-shadow rendering was inverted for elements
|
|
193
|
+
// not at document origin due to the mask covering the wrong region.
|
|
194
|
+
const x = this.options.x;
|
|
195
|
+
const y = this.options.y;
|
|
196
|
+
this.ctx.moveTo(x, y);
|
|
197
|
+
this.ctx.lineTo(x + this.options.width, y);
|
|
198
|
+
this.ctx.lineTo(x + this.options.width, y + this.options.height);
|
|
199
|
+
this.ctx.lineTo(x, y + this.options.height);
|
|
200
|
+
this.ctx.lineTo(x, y);
|
|
198
201
|
this.formatPath(paths.slice(0).reverse());
|
|
199
202
|
this.ctx.closePath();
|
|
200
203
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.wrapTextToWidth = void 0;
|
|
3
|
+
exports.wrapTextToWidth = exports.inputTextClipBounds = void 0;
|
|
4
4
|
exports.renderReplacedElements = renderReplacedElements;
|
|
5
5
|
exports.renderFormElements = renderFormElements;
|
|
6
6
|
exports.renderListMarker = renderListMarker;
|
|
@@ -107,12 +107,16 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
107
107
|
const [font] = textRenderer.createFontStyle(styles);
|
|
108
108
|
// Use Canvas API to measure baseline from the actual rendered font
|
|
109
109
|
ctx.font = font;
|
|
110
|
-
const
|
|
110
|
+
const fontSizeValue = (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0);
|
|
111
|
+
const { baseline, height: fontHeight } = (0, font_utils_1.measureFontMetrics)(ctx, fontSizeValue);
|
|
111
112
|
const isPlaceholder = container instanceof input_element_container_1.InputElementContainer && container.isPlaceholder;
|
|
112
113
|
ctx.fillStyle = isPlaceholder ? (0, color_utilities_1.asString)(input_element_container_1.PLACEHOLDER_COLOR) : (0, color_utilities_1.asString)(styles.color);
|
|
113
114
|
ctx.textBaseline = 'alphabetic';
|
|
114
115
|
ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
115
116
|
const bounds = (0, box_sizing_1.contentBox)(container);
|
|
117
|
+
const clipBounds = container instanceof input_element_container_1.InputElementContainer
|
|
118
|
+
? (0, exports.inputTextClipBounds)(bounds, (0, box_sizing_1.paddingBox)(container), fontHeight)
|
|
119
|
+
: bounds;
|
|
116
120
|
let x = 0;
|
|
117
121
|
switch (container.styles.textAlign) {
|
|
118
122
|
case 1 /* TEXT_ALIGN.CENTER */:
|
|
@@ -124,16 +128,15 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
124
128
|
}
|
|
125
129
|
let verticalOffset = 0;
|
|
126
130
|
if (container instanceof input_element_container_1.InputElementContainer) {
|
|
127
|
-
|
|
128
|
-
verticalOffset = (bounds.height - fontSizeValue) / 2;
|
|
131
|
+
verticalOffset = (bounds.height - fontHeight) / 2;
|
|
129
132
|
}
|
|
130
133
|
const textBounds = bounds.add(x, verticalOffset, 0, 0);
|
|
131
134
|
ctx.save();
|
|
132
135
|
pathFn([
|
|
133
|
-
new vector_1.Vector(
|
|
134
|
-
new vector_1.Vector(
|
|
135
|
-
new vector_1.Vector(
|
|
136
|
-
new vector_1.Vector(
|
|
136
|
+
new vector_1.Vector(clipBounds.left, clipBounds.top),
|
|
137
|
+
new vector_1.Vector(clipBounds.left + clipBounds.width, clipBounds.top),
|
|
138
|
+
new vector_1.Vector(clipBounds.left + clipBounds.width, clipBounds.top + clipBounds.height),
|
|
139
|
+
new vector_1.Vector(clipBounds.left, clipBounds.top + clipBounds.height)
|
|
137
140
|
]);
|
|
138
141
|
ctx.clip();
|
|
139
142
|
// A <textarea> soft-wraps its value at the element width (unless
|
|
@@ -141,7 +144,6 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
141
144
|
// single-line. Render textareas line-by-line so long values wrap
|
|
142
145
|
// instead of being clipped on one line (issue #228).
|
|
143
146
|
const lines = (0, exports.wrapTextToWidth)(ctx, container.value, bounds.width, styles.whiteSpace === white_space_1.WHITE_SPACE.PRE || styles.whiteSpace === white_space_1.WHITE_SPACE.NOWRAP);
|
|
144
|
-
const fontSizeValue = (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0);
|
|
145
147
|
const lineHeight = (0, line_height_1.computeLineHeight)(styles.lineHeight, fontSizeValue);
|
|
146
148
|
lines.forEach((line, index) => {
|
|
147
149
|
let lineX = textBounds.left;
|
|
@@ -162,6 +164,17 @@ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
|
162
164
|
ctx.textAlign = 'left';
|
|
163
165
|
}
|
|
164
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Native single-line inputs may paint value text into their padding area when
|
|
169
|
+
* fixed height and vertical padding leave a content box shorter than the font.
|
|
170
|
+
* Preserve the content box horizontally, but avoid clipping those glyphs to the
|
|
171
|
+
* collapsed vertical content box.
|
|
172
|
+
* @internal exported for testing
|
|
173
|
+
*/
|
|
174
|
+
const inputTextClipBounds = (contentBounds, inputPaddingBox, fontHeight) => contentBounds.height < fontHeight
|
|
175
|
+
? new bounds_1.Bounds(contentBounds.left, inputPaddingBox.top, contentBounds.width, inputPaddingBox.height)
|
|
176
|
+
: contentBounds;
|
|
177
|
+
exports.inputTextClipBounds = inputTextClipBounds;
|
|
165
178
|
/**
|
|
166
179
|
* Split text into lines that fit the given pixel width, preserving hard line
|
|
167
180
|
* breaks and (when wrapping is enabled) breaking at word boundaries — mirroring
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Font measurement utility
|
|
4
4
|
*
|
|
5
|
-
* Provides Canvas API-based
|
|
6
|
-
*
|
|
5
|
+
* Provides Canvas API-based font measurement that works correctly with webfonts
|
|
6
|
+
* (which may not be loaded in the original document) and system fonts.
|
|
7
7
|
*
|
|
8
8
|
* Fallback chain:
|
|
9
9
|
* 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
|
|
@@ -11,8 +11,28 @@
|
|
|
11
11
|
* 3. fallback value — coarse CSS fallback
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.measureBaseline = void 0;
|
|
14
|
+
exports.measureBaseline = exports.measureFontMetrics = void 0;
|
|
15
15
|
const SAMPLE_TEXT = 'Mg'; // characters with both ascender and descender
|
|
16
|
+
const isValidMetric = (value) => value !== undefined && !Number.isNaN(value);
|
|
17
|
+
/**
|
|
18
|
+
* Measure the baseline ascent and full font box for the currently set font.
|
|
19
|
+
*/
|
|
20
|
+
const measureFontMetrics = (ctx, fallback) => {
|
|
21
|
+
const tm = ctx.measureText(SAMPLE_TEXT);
|
|
22
|
+
const fontMetrics = tm;
|
|
23
|
+
const baseline = isValidMetric(fontMetrics.fontBoundingBoxAscent)
|
|
24
|
+
? fontMetrics.fontBoundingBoxAscent
|
|
25
|
+
: isValidMetric(tm.actualBoundingBoxAscent)
|
|
26
|
+
? tm.actualBoundingBoxAscent
|
|
27
|
+
: fallback;
|
|
28
|
+
const height = isValidMetric(fontMetrics.fontBoundingBoxAscent) && isValidMetric(fontMetrics.fontBoundingBoxDescent)
|
|
29
|
+
? fontMetrics.fontBoundingBoxAscent + fontMetrics.fontBoundingBoxDescent
|
|
30
|
+
: isValidMetric(tm.actualBoundingBoxAscent) && isValidMetric(tm.actualBoundingBoxDescent)
|
|
31
|
+
? tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent
|
|
32
|
+
: fallback;
|
|
33
|
+
return { baseline, height };
|
|
34
|
+
};
|
|
35
|
+
exports.measureFontMetrics = measureFontMetrics;
|
|
16
36
|
/**
|
|
17
37
|
* Measure the baseline ascent for the currently set font.
|
|
18
38
|
*
|
|
@@ -21,9 +41,6 @@ const SAMPLE_TEXT = 'Mg'; // characters with both ascender and descender
|
|
|
21
41
|
* @returns The distance from the text baseline to the top of the bounding box
|
|
22
42
|
*/
|
|
23
43
|
const measureBaseline = (ctx, fallback) => {
|
|
24
|
-
|
|
25
|
-
const fmAscent = tm.fontBoundingBoxAscent;
|
|
26
|
-
const ascent = fmAscent !== undefined && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
|
|
27
|
-
return ascent !== undefined && !Number.isNaN(ascent) ? ascent : fallback;
|
|
44
|
+
return (0, exports.measureFontMetrics)(ctx, fallback).baseline;
|
|
28
45
|
};
|
|
29
46
|
exports.measureBaseline = measureBaseline;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ElementPaint } from '../stacking-context';
|
|
2
2
|
import { ReplacedElementContainer } from '../../dom/replaced-elements';
|
|
3
3
|
import { ElementContainer } from '../../dom/element-container';
|
|
4
|
+
import { Bounds } from '../../css/layout/bounds';
|
|
4
5
|
import { BoundCurves } from '../bound-curves';
|
|
5
6
|
import { Vector } from '../vector';
|
|
6
7
|
import { Context } from '../../core/context';
|
|
@@ -36,6 +37,14 @@ export declare function renderReplacedElements(ctx: CanvasRenderingContext2D, co
|
|
|
36
37
|
* Render form element content: checkbox, radio, text input.
|
|
37
38
|
*/
|
|
38
39
|
export declare function renderFormElements(ctx: CanvasRenderingContext2D, textRenderer: TextRenderer, pathFn: (paths: Vector[]) => void, container: ElementContainer, styles: CSSParsedDeclaration): void;
|
|
40
|
+
/**
|
|
41
|
+
* Native single-line inputs may paint value text into their padding area when
|
|
42
|
+
* fixed height and vertical padding leave a content box shorter than the font.
|
|
43
|
+
* Preserve the content box horizontally, but avoid clipping those glyphs to the
|
|
44
|
+
* collapsed vertical content box.
|
|
45
|
+
* @internal exported for testing
|
|
46
|
+
*/
|
|
47
|
+
export declare const inputTextClipBounds: (contentBounds: Bounds, inputPaddingBox: Bounds, fontHeight: number) => Bounds;
|
|
39
48
|
/**
|
|
40
49
|
* Split text into lines that fit the given pixel width, preserving hard line
|
|
41
50
|
* breaks and (when wrapping is enabled) breaking at word boundaries — mirroring
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Font measurement utility
|
|
3
3
|
*
|
|
4
|
-
* Provides Canvas API-based
|
|
5
|
-
*
|
|
4
|
+
* Provides Canvas API-based font measurement that works correctly with webfonts
|
|
5
|
+
* (which may not be loaded in the original document) and system fonts.
|
|
6
6
|
*
|
|
7
7
|
* Fallback chain:
|
|
8
8
|
* 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
|
|
9
9
|
* 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
|
|
10
10
|
* 3. fallback value — coarse CSS fallback
|
|
11
11
|
*/
|
|
12
|
+
export interface CanvasFontMetrics {
|
|
13
|
+
baseline: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Measure the baseline ascent and full font box for the currently set font.
|
|
18
|
+
*/
|
|
19
|
+
export declare const measureFontMetrics: (ctx: CanvasRenderingContext2D, fallback: number) => CanvasFontMetrics;
|
|
12
20
|
/**
|
|
13
21
|
* Measure the baseline ascent for the currently set font.
|
|
14
22
|
*
|