html2canvas-pro 2.3.0 → 2.3.2

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.
@@ -18,6 +18,7 @@ const color_utilities_1 = require("../../css/types/color-utilities");
18
18
  const parser_1 = require("../../css/syntax/parser");
19
19
  const writing_mode_1 = require("../../css/property-descriptors/writing-mode");
20
20
  const text_decoration_renderer_1 = require("./text/text-decoration-renderer");
21
+ const font_utils_1 = require("./font-utils");
21
22
  // iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
22
23
  const iOSBrokenFonts = ['-apple-system', 'system-ui'];
23
24
  /**
@@ -236,21 +237,21 @@ class TextRenderer {
236
237
  });
237
238
  }
238
239
  }
239
- renderTextStrokeWithStyle(text, styles) {
240
+ renderTextStrokeWithStyle(text, styles, baseline) {
240
241
  if (!styles.webkitTextStrokeWidth || !text.text.trim().length) {
241
242
  return;
242
243
  }
243
244
  this.ctx.strokeStyle = (0, color_utilities_1.asString)(styles.webkitTextStrokeColor);
244
245
  this.ctx.lineWidth = styles.webkitTextStrokeWidth;
245
246
  this.ctx.lineJoin = getTextStrokeLineJoin();
246
- this.renderStrokeText(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
247
+ this.renderStrokeText(text, styles.letterSpacing, baseline, styles.writingMode);
247
248
  this.ctx.strokeStyle = '';
248
249
  this.ctx.lineWidth = 0;
249
250
  this.ctx.lineJoin = 'miter';
250
251
  }
251
- renderTextFillWithShadows(text, styles) {
252
+ renderTextFillWithShadows(text, styles, baseline) {
252
253
  this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
253
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
254
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
254
255
  const textShadows = styles.textShadow;
255
256
  if (textShadows.length && text.text.trim().length) {
256
257
  textShadows
@@ -261,7 +262,7 @@ class TextRenderer {
261
262
  this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
262
263
  this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
263
264
  this.ctx.shadowBlur = textShadow.blur.number;
264
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
265
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
265
266
  });
266
267
  this.ctx.shadowColor = '';
267
268
  this.ctx.shadowOffsetX = 0;
@@ -273,15 +274,15 @@ class TextRenderer {
273
274
  * Helper method to render text with paint order support
274
275
  * Reduces code duplication in line-clamp and normal rendering
275
276
  */
276
- renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
277
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers, baseline) {
277
278
  paintOrderLayers.forEach((paintOrderLayer) => {
278
279
  switch (paintOrderLayer) {
279
280
  case 0 /* PAINT_ORDER_LAYER.FILL */:
280
281
  this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
281
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
282
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, baseline, styles.writingMode);
282
283
  break;
283
284
  case 1 /* PAINT_ORDER_LAYER.STROKE */:
284
- this.renderTextStrokeWithStyle(textBound, styles);
285
+ this.renderTextStrokeWithStyle(textBound, styles, baseline);
285
286
  break;
286
287
  }
287
288
  });
@@ -362,7 +363,7 @@ class TextRenderer {
362
363
  * Groups text bounds by their Y position into visual lines, then renders
363
364
  * only the first N-1 complete lines followed by an ellipsis on the Nth line.
364
365
  */
365
- renderLineClampedText(text, styles, paintOrder, containerBounds) {
366
+ renderLineClampedText(text, styles, paintOrder, baseline, containerBounds) {
366
367
  const lineHeight = styles.fontSize.number * 1.5;
367
368
  const lines = [];
368
369
  let currentLine = [];
@@ -385,7 +386,7 @@ class TextRenderer {
385
386
  return false; // fall through to normal rendering
386
387
  // Render full lines (0..N-2)
387
388
  for (let i = 0; i < maxLines - 1; i++) {
388
- lines[i].forEach((tb) => this.renderTextBoundWithPaintOrder(tb, styles, paintOrder));
389
+ lines[i].forEach((tb) => this.renderTextBoundWithPaintOrder(tb, styles, paintOrder, baseline));
389
390
  }
390
391
  // Nth line: truncated with ellipsis
391
392
  const lastLine = lines[maxLines - 1];
@@ -398,10 +399,10 @@ class TextRenderer {
398
399
  for (const layer of paintOrder) {
399
400
  if (layer === 0 /* PAINT_ORDER_LAYER.FILL */) {
400
401
  this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
401
- this.renderTextWithLetterSpacing(bounds, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
402
+ this.renderTextWithLetterSpacing(bounds, styles.letterSpacing, baseline, styles.writingMode);
402
403
  }
403
404
  else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */) {
404
- this.renderTextStrokeWithStyle(bounds, styles);
405
+ this.renderTextStrokeWithStyle(bounds, styles, baseline);
405
406
  }
406
407
  }
407
408
  }
@@ -411,7 +412,7 @@ class TextRenderer {
411
412
  * Render single-line text with text-overflow: ellipsis.
412
413
  * Returns true if ellipsis was applied (caller should skip normal rendering).
413
414
  */
414
- renderEllipsisText(text, styles, paintOrder, containerBounds) {
415
+ renderEllipsisText(text, styles, paintOrder, baseline, containerBounds) {
415
416
  const lineHeight = styles.fontSize.number * 1.5;
416
417
  const firstTop = text.textBounds[0].bounds.top;
417
418
  const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
@@ -429,9 +430,9 @@ class TextRenderer {
429
430
  const bounds = new text_1.TextBounds(truncated, text.textBounds[0].bounds);
430
431
  for (const layer of paintOrder) {
431
432
  if (layer === 0 /* PAINT_ORDER_LAYER.FILL */)
432
- this.renderTextFillWithShadows(bounds, styles);
433
+ this.renderTextFillWithShadows(bounds, styles, baseline);
433
434
  else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */)
434
- this.renderTextStrokeWithStyle(bounds, styles);
435
+ this.renderTextStrokeWithStyle(bounds, styles, baseline);
435
436
  }
436
437
  return true;
437
438
  }
@@ -439,18 +440,24 @@ class TextRenderer {
439
440
  // Reset glyph width cache at the start of each text node render —
440
441
  // the font may change between nodes.
441
442
  this.glyphWidthCache = null;
442
- this.ctx.font = this.createFontStyle(styles)[0];
443
+ const [fontString] = this.createFontStyle(styles);
444
+ this.ctx.font = fontString;
443
445
  this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
444
446
  this.ctx.textAlign = 'left';
445
447
  this.ctx.textBaseline = 'alphabetic';
446
448
  const paintOrder = styles.paintOrder;
449
+ // Compute baseline using the actual rendered font via Canvas API.
450
+ // This ensures correct positioning whether using webfonts (which may not
451
+ // be loaded in the original document but are active in the Canvas) or
452
+ // system fonts.
453
+ const baseline = (0, font_utils_1.measureBaseline)(this.ctx, styles.fontSize.number);
447
454
  // -webkit-line-clamp
448
455
  const clamp = styles.webkitLineClamp > 0 &&
449
456
  (styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
450
457
  styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
451
458
  text.textBounds.length > 0;
452
459
  if (clamp) {
453
- if (this.renderLineClampedText(text, styles, paintOrder, containerBounds))
460
+ if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds))
454
461
  return;
455
462
  }
456
463
  // text-overflow: ellipsis (single-line only)
@@ -458,18 +465,18 @@ class TextRenderer {
458
465
  containerBounds &&
459
466
  styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
460
467
  text.textBounds.length > 0;
461
- if (ellipsis && this.renderEllipsisText(text, styles, paintOrder, containerBounds))
468
+ if (ellipsis && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds))
462
469
  return;
463
470
  // Normal rendering: fill + stroke + decorations per text bound
464
471
  text.textBounds.forEach((tb) => {
465
472
  paintOrder.forEach((layer) => {
466
473
  if (layer === 0 /* PAINT_ORDER_LAYER.FILL */) {
467
- this.renderTextFillWithShadows(tb, styles);
474
+ this.renderTextFillWithShadows(tb, styles, baseline);
468
475
  if (styles.textDecorationLine.length)
469
476
  this.renderTextDecoration(tb.bounds, styles);
470
477
  }
471
478
  else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */) {
472
- this.renderTextStrokeWithStyle(tb, styles);
479
+ this.renderTextStrokeWithStyle(tb, styles, baseline);
473
480
  }
474
481
  });
475
482
  });
@@ -37,7 +37,6 @@ export declare class CanvasRenderer {
37
37
  ctx: CanvasRenderingContext2D;
38
38
  private readonly context;
39
39
  private readonly options;
40
- private readonly fontMetrics;
41
40
  private readonly backgroundRenderer;
42
41
  private readonly borderRenderer;
43
42
  private readonly borderImageRenderer;
@@ -5,7 +5,6 @@ import { BoundCurves } from '../bound-curves';
5
5
  import { Vector } from '../vector';
6
6
  import { Context } from '../../core/context';
7
7
  import { TextRenderer } from './text-renderer';
8
- import { FontMetrics } from '../font-metrics';
9
8
  import { Color } from '../../css/types/color';
10
9
  import { CSSParsedDeclaration } from '../../css/index';
11
10
  /**
@@ -36,7 +35,7 @@ export declare function renderReplacedElements(ctx: CanvasRenderingContext2D, co
36
35
  /**
37
36
  * Render form element content: checkbox, radio, text input.
38
37
  */
39
- export declare function renderFormElements(ctx: CanvasRenderingContext2D, fontMetrics: FontMetrics, textRenderer: TextRenderer, pathFn: (paths: Vector[]) => void, container: ElementContainer, styles: CSSParsedDeclaration): void;
38
+ export declare function renderFormElements(ctx: CanvasRenderingContext2D, textRenderer: TextRenderer, pathFn: (paths: Vector[]) => void, container: ElementContainer, styles: CSSParsedDeclaration): void;
40
39
  /**
41
40
  * Render list-item marker (image or text).
42
41
  */
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Font measurement utility
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.
6
+ *
7
+ * Fallback chain:
8
+ * 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
9
+ * 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
10
+ * 3. fallback value — coarse CSS fallback
11
+ */
12
+ /**
13
+ * Measure the baseline ascent for the currently set font.
14
+ *
15
+ * @param ctx - Canvas 2D rendering context with ctx.font already set
16
+ * @param fallback - Fallback value when no metrics are available (e.g. fontSize.number)
17
+ * @returns The distance from the text baseline to the top of the bounding box
18
+ */
19
+ export declare const measureBaseline: (ctx: CanvasRenderingContext2D, fallback: number) => number;
@@ -10,7 +10,6 @@
10
10
  * - Paint order (fill/stroke)
11
11
  * - Font styles
12
12
  */
13
- import { Context } from '../../core/context';
14
13
  import { TextContainer } from '../../dom/text-container';
15
14
  import { CSSParsedDeclaration } from '../../css';
16
15
  import { Bounds } from '../../css/layout/bounds';
@@ -21,7 +20,6 @@ import { WRITING_MODE } from '../../css/property-descriptors/writing-mode';
21
20
  */
22
21
  export interface TextRendererDependencies {
23
22
  ctx: CanvasRenderingContext2D;
24
- context: Context;
25
23
  options: {
26
24
  scale: number;
27
25
  };
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "default": "./dist/html2canvas-pro.esm.js"
20
20
  }
21
21
  },
22
- "version": "2.3.0",
22
+ "version": "2.3.2",
23
23
  "author": {
24
24
  "name": "yorickshan",
25
25
  "email": "yorickshan@gmail.com",