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.
@@ -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
- return Promise.resolve()
89
- .then(() => onclone(documentClone, referenceElement))
90
- .then(() => iframe);
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
- this.ctx.moveTo(0, 0);
191
- // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
192
- // because context has already been scaled by this.options.scale
193
- // Fix for Issue #126: Using canvas pixel dimensions causes broken output
194
- this.ctx.lineTo(this.options.width, 0);
195
- this.ctx.lineTo(this.options.width, this.options.height);
196
- this.ctx.lineTo(0, this.options.height);
197
- this.ctx.lineTo(0, 0);
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 baseline = (0, font_utils_1.measureBaseline)(ctx, (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0));
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
- const fontSizeValue = (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0);
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(bounds.left, bounds.top),
134
- new vector_1.Vector(bounds.left + bounds.width, bounds.top),
135
- new vector_1.Vector(bounds.left + bounds.width, bounds.top + bounds.height),
136
- new vector_1.Vector(bounds.left, bounds.top + bounds.height)
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 baseline measurement that works correctly with
6
- * webfonts (which may not be loaded in the original document) and system fonts.
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
- const tm = ctx.measureText(SAMPLE_TEXT);
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 baseline measurement that works correctly with
5
- * webfonts (which may not be loaded in the original document) and system fonts.
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
  *
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "default": "./dist/html2canvas-pro.esm.js"
20
20
  }
21
21
  },
22
- "version": "2.3.8",
22
+ "version": "2.4.0",
23
23
  "author": {
24
24
  "name": "yorickshan",
25
25
  "email": "yorickshan@gmail.com",