html2canvas-pro 2.3.0 → 2.3.1

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.
@@ -100,6 +100,7 @@ class TextRenderer {
100
100
  */
101
101
  this.glyphWidthCache = null;
102
102
  this.ctx = deps.ctx;
103
+ this.fontMetrics = deps.fontMetrics;
103
104
  this.options = deps.options;
104
105
  this.decorationRenderer = new text_decoration_renderer_1.TextDecorationRenderer(deps.ctx);
105
106
  }
@@ -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,23 @@ 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, fontFamily, fontSize] = 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
+ // Use the measured baseline from FontMetrics instead of fontSize,
450
+ // which correctly accounts for fonts with large descenders (e.g. Paul)
451
+ // where the alphabetic baseline is closer to the top of the em-square.
452
+ const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
447
453
  // -webkit-line-clamp
448
454
  const clamp = styles.webkitLineClamp > 0 &&
449
455
  (styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
450
456
  styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
451
457
  text.textBounds.length > 0;
452
458
  if (clamp) {
453
- if (this.renderLineClampedText(text, styles, paintOrder, containerBounds))
459
+ if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds))
454
460
  return;
455
461
  }
456
462
  // text-overflow: ellipsis (single-line only)
@@ -458,18 +464,18 @@ class TextRenderer {
458
464
  containerBounds &&
459
465
  styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
460
466
  text.textBounds.length > 0;
461
- if (ellipsis && this.renderEllipsisText(text, styles, paintOrder, containerBounds))
467
+ if (ellipsis && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds))
462
468
  return;
463
469
  // Normal rendering: fill + stroke + decorations per text bound
464
470
  text.textBounds.forEach((tb) => {
465
471
  paintOrder.forEach((layer) => {
466
472
  if (layer === 0 /* PAINT_ORDER_LAYER.FILL */) {
467
- this.renderTextFillWithShadows(tb, styles);
473
+ this.renderTextFillWithShadows(tb, styles, baseline);
468
474
  if (styles.textDecorationLine.length)
469
475
  this.renderTextDecoration(tb.bounds, styles);
470
476
  }
471
477
  else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */) {
472
- this.renderTextStrokeWithStyle(tb, styles);
478
+ this.renderTextStrokeWithStyle(tb, styles, baseline);
473
479
  }
474
480
  });
475
481
  });
@@ -10,18 +10,18 @@
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';
17
16
  import { TextBounds } from '../../css/layout/text';
18
17
  import { WRITING_MODE } from '../../css/property-descriptors/writing-mode';
18
+ import { FontMetrics } from '../font-metrics';
19
19
  /**
20
20
  * Dependencies required for TextRenderer
21
21
  */
22
22
  export interface TextRendererDependencies {
23
23
  ctx: CanvasRenderingContext2D;
24
- context: Context;
24
+ fontMetrics: FontMetrics;
25
25
  options: {
26
26
  scale: number;
27
27
  };
@@ -35,6 +35,7 @@ export declare const hasCJKCharacters: (text: string) => boolean;
35
35
  */
36
36
  export declare class TextRenderer {
37
37
  private readonly ctx;
38
+ private readonly fontMetrics;
38
39
  private readonly options;
39
40
  private readonly decorationRenderer;
40
41
  /**
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.1",
23
23
  "author": {
24
24
  "name": "yorickshan",
25
25
  "email": "yorickshan@gmail.com",