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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 2.3.0 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.3.2 <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
  */
@@ -8943,60 +8943,6 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
8943
8943
  }
8944
8944
  };
8945
8945
  //#endregion
8946
- //#region src/core/util.ts
8947
- const SMALL_IMAGE = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
8948
- //#endregion
8949
- //#region src/render/font-metrics.ts
8950
- const SAMPLE_TEXT = "Hidden Text";
8951
- var FontMetrics = class {
8952
- constructor(document) {
8953
- this._data = {};
8954
- this._document = document;
8955
- }
8956
- parseMetrics(fontFamily, fontSize) {
8957
- const container = this._document.createElement("div");
8958
- const img = this._document.createElement("img");
8959
- const span = this._document.createElement("span");
8960
- const body = this._document.body;
8961
- container.style.visibility = "hidden";
8962
- container.style.fontFamily = fontFamily;
8963
- container.style.fontSize = fontSize;
8964
- container.style.margin = "0";
8965
- container.style.padding = "0";
8966
- container.style.whiteSpace = "nowrap";
8967
- body.appendChild(container);
8968
- img.src = SMALL_IMAGE;
8969
- img.width = 1;
8970
- img.height = 1;
8971
- img.style.margin = "0";
8972
- img.style.padding = "0";
8973
- img.style.verticalAlign = "baseline";
8974
- span.style.fontFamily = fontFamily;
8975
- span.style.fontSize = fontSize;
8976
- span.style.margin = "0";
8977
- span.style.padding = "0";
8978
- span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
8979
- container.appendChild(span);
8980
- container.appendChild(img);
8981
- const baseline = img.offsetTop - span.offsetTop + 2;
8982
- container.removeChild(span);
8983
- container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
8984
- container.style.lineHeight = "normal";
8985
- img.style.verticalAlign = "super";
8986
- const middle = img.offsetTop - container.offsetTop + 2;
8987
- body.removeChild(container);
8988
- return {
8989
- baseline,
8990
- middle
8991
- };
8992
- }
8993
- getMetrics(fontFamily, fontSize) {
8994
- const key = `${fontFamily} ${fontSize}`;
8995
- if (typeof this._data[key] === "undefined") this._data[key] = this.parseMetrics(fontFamily, fontSize);
8996
- return this._data[key];
8997
- }
8998
- };
8999
- //#endregion
9000
8946
  //#region src/render/canvas/text/text-decoration-renderer.ts
9001
8947
  var TextDecorationRenderer = class {
9002
8948
  constructor(ctx) {
@@ -9085,6 +9031,33 @@ var TextDecorationRenderer = class {
9085
9031
  }
9086
9032
  };
9087
9033
  //#endregion
9034
+ //#region src/render/canvas/font-utils.ts
9035
+ /**
9036
+ * Font measurement utility
9037
+ *
9038
+ * Provides Canvas API-based baseline measurement that works correctly with
9039
+ * webfonts (which may not be loaded in the original document) and system fonts.
9040
+ *
9041
+ * Fallback chain:
9042
+ * 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
9043
+ * 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
9044
+ * 3. fallback value — coarse CSS fallback
9045
+ */
9046
+ const SAMPLE_TEXT = "Mg";
9047
+ /**
9048
+ * Measure the baseline ascent for the currently set font.
9049
+ *
9050
+ * @param ctx - Canvas 2D rendering context with ctx.font already set
9051
+ * @param fallback - Fallback value when no metrics are available (e.g. fontSize.number)
9052
+ * @returns The distance from the text baseline to the top of the bounding box
9053
+ */
9054
+ const measureBaseline = (ctx, fallback) => {
9055
+ const tm = ctx.measureText(SAMPLE_TEXT);
9056
+ const fmAscent = tm.fontBoundingBoxAscent;
9057
+ const ascent = fmAscent !== void 0 && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
9058
+ return ascent !== void 0 && !Number.isNaN(ascent) ? ascent : fallback;
9059
+ };
9060
+ //#endregion
9088
9061
  //#region src/render/canvas/text-renderer.ts
9089
9062
  const iOSBrokenFonts = ["-apple-system", "system-ui"];
9090
9063
  /**
@@ -9251,19 +9224,19 @@ var TextRenderer = class {
9251
9224
  this.ctx.strokeText(letter, x, y);
9252
9225
  });
9253
9226
  }
9254
- renderTextStrokeWithStyle(text, styles) {
9227
+ renderTextStrokeWithStyle(text, styles, baseline) {
9255
9228
  if (!styles.webkitTextStrokeWidth || !text.text.trim().length) return;
9256
9229
  this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
9257
9230
  this.ctx.lineWidth = styles.webkitTextStrokeWidth;
9258
9231
  this.ctx.lineJoin = getTextStrokeLineJoin();
9259
- this.renderStrokeText(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
9232
+ this.renderStrokeText(text, styles.letterSpacing, baseline, styles.writingMode);
9260
9233
  this.ctx.strokeStyle = "";
9261
9234
  this.ctx.lineWidth = 0;
9262
9235
  this.ctx.lineJoin = "miter";
9263
9236
  }
9264
- renderTextFillWithShadows(text, styles) {
9237
+ renderTextFillWithShadows(text, styles, baseline) {
9265
9238
  this.ctx.fillStyle = asString(styles.color);
9266
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
9239
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
9267
9240
  const textShadows = styles.textShadow;
9268
9241
  if (textShadows.length && text.text.trim().length) {
9269
9242
  textShadows.slice(0).reverse().forEach((textShadow) => {
@@ -9271,7 +9244,7 @@ var TextRenderer = class {
9271
9244
  this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
9272
9245
  this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
9273
9246
  this.ctx.shadowBlur = textShadow.blur.number;
9274
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
9247
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, baseline, styles.writingMode);
9275
9248
  });
9276
9249
  this.ctx.shadowColor = "";
9277
9250
  this.ctx.shadowOffsetX = 0;
@@ -9283,15 +9256,15 @@ var TextRenderer = class {
9283
9256
  * Helper method to render text with paint order support
9284
9257
  * Reduces code duplication in line-clamp and normal rendering
9285
9258
  */
9286
- renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
9259
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers, baseline) {
9287
9260
  paintOrderLayers.forEach((paintOrderLayer) => {
9288
9261
  switch (paintOrderLayer) {
9289
9262
  case 0:
9290
9263
  this.ctx.fillStyle = asString(styles.color);
9291
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
9264
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, baseline, styles.writingMode);
9292
9265
  break;
9293
9266
  case 1:
9294
- this.renderTextStrokeWithStyle(textBound, styles);
9267
+ this.renderTextStrokeWithStyle(textBound, styles, baseline);
9295
9268
  break;
9296
9269
  }
9297
9270
  });
@@ -9351,7 +9324,7 @@ var TextRenderer = class {
9351
9324
  * Groups text bounds by their Y position into visual lines, then renders
9352
9325
  * only the first N-1 complete lines followed by an ellipsis on the Nth line.
9353
9326
  */
9354
- renderLineClampedText(text, styles, paintOrder, containerBounds) {
9327
+ renderLineClampedText(text, styles, paintOrder, baseline, containerBounds) {
9355
9328
  const lineHeight = styles.fontSize.number * 1.5;
9356
9329
  const lines = [];
9357
9330
  let currentLine = [];
@@ -9366,7 +9339,7 @@ var TextRenderer = class {
9366
9339
  if (currentLine.length > 0) lines.push(currentLine);
9367
9340
  const maxLines = styles.webkitLineClamp;
9368
9341
  if (lines.length <= maxLines) return false;
9369
- for (let i = 0; i < maxLines - 1; i++) lines[i].forEach((tb) => this.renderTextBoundWithPaintOrder(tb, styles, paintOrder));
9342
+ for (let i = 0; i < maxLines - 1; i++) lines[i].forEach((tb) => this.renderTextBoundWithPaintOrder(tb, styles, paintOrder, baseline));
9370
9343
  const lastLine = lines[maxLines - 1];
9371
9344
  if (lastLine?.length && containerBounds) {
9372
9345
  const textStr = lastLine.map((tb) => tb.text).join("");
@@ -9375,8 +9348,8 @@ var TextRenderer = class {
9375
9348
  const bounds = new TextBounds(this.truncateTextWithEllipsis(textStr, avail, styles.letterSpacing), first.bounds);
9376
9349
  for (const layer of paintOrder) if (layer === 0) {
9377
9350
  this.ctx.fillStyle = asString(styles.color);
9378
- this.renderTextWithLetterSpacing(bounds, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
9379
- } else if (layer === 1) this.renderTextStrokeWithStyle(bounds, styles);
9351
+ this.renderTextWithLetterSpacing(bounds, styles.letterSpacing, baseline, styles.writingMode);
9352
+ } else if (layer === 1) this.renderTextStrokeWithStyle(bounds, styles, baseline);
9380
9353
  }
9381
9354
  return true;
9382
9355
  }
@@ -9384,34 +9357,36 @@ var TextRenderer = class {
9384
9357
  * Render single-line text with text-overflow: ellipsis.
9385
9358
  * Returns true if ellipsis was applied (caller should skip normal rendering).
9386
9359
  */
9387
- renderEllipsisText(text, styles, paintOrder, containerBounds) {
9360
+ renderEllipsisText(text, styles, paintOrder, baseline, containerBounds) {
9388
9361
  const lineHeight = styles.fontSize.number * 1.5;
9389
9362
  const firstTop = text.textBounds[0].bounds.top;
9390
9363
  if (!text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * .5)) return false;
9391
9364
  let fullText = text.textBounds.map((tb) => tb.text).join("").replace(/\s+/g, " ").trim();
9392
9365
  if (this.ctx.measureText(fullText).width <= containerBounds.width) return false;
9393
9366
  const bounds = new TextBounds(this.truncateTextWithEllipsis(fullText, containerBounds.width, styles.letterSpacing), text.textBounds[0].bounds);
9394
- for (const layer of paintOrder) if (layer === 0) this.renderTextFillWithShadows(bounds, styles);
9395
- else if (layer === 1) this.renderTextStrokeWithStyle(bounds, styles);
9367
+ for (const layer of paintOrder) if (layer === 0) this.renderTextFillWithShadows(bounds, styles, baseline);
9368
+ else if (layer === 1) this.renderTextStrokeWithStyle(bounds, styles, baseline);
9396
9369
  return true;
9397
9370
  }
9398
9371
  async renderTextNode(text, styles, containerBounds) {
9399
9372
  this.glyphWidthCache = null;
9400
- this.ctx.font = this.createFontStyle(styles)[0];
9373
+ const [fontString] = this.createFontStyle(styles);
9374
+ this.ctx.font = fontString;
9401
9375
  this.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
9402
9376
  this.ctx.textAlign = "left";
9403
9377
  this.ctx.textBaseline = "alphabetic";
9404
9378
  const paintOrder = styles.paintOrder;
9379
+ const baseline = measureBaseline(this.ctx, styles.fontSize.number);
9405
9380
  if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
9406
- if (this.renderLineClampedText(text, styles, paintOrder, containerBounds)) return;
9381
+ if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
9407
9382
  }
9408
- if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, containerBounds)) return;
9383
+ if (styles.textOverflow === 1 && containerBounds && styles.overflowX === 1 && text.textBounds.length > 0 && this.renderEllipsisText(text, styles, paintOrder, baseline, containerBounds)) return;
9409
9384
  text.textBounds.forEach((tb) => {
9410
9385
  paintOrder.forEach((layer) => {
9411
9386
  if (layer === 0) {
9412
- this.renderTextFillWithShadows(tb, styles);
9387
+ this.renderTextFillWithShadows(tb, styles, baseline);
9413
9388
  if (styles.textDecorationLine.length) this.renderTextDecoration(tb.bounds, styles);
9414
- } else if (layer === 1) this.renderTextStrokeWithStyle(tb, styles);
9389
+ } else if (layer === 1) this.renderTextStrokeWithStyle(tb, styles, baseline);
9415
9390
  });
9416
9391
  });
9417
9392
  }
@@ -10297,7 +10272,7 @@ async function renderReplacedElements(ctx, context, options, iframeRendererFacto
10297
10272
  /**
10298
10273
  * Render form element content: checkbox, radio, text input.
10299
10274
  */
10300
- function renderFormElements(ctx, fontMetrics, textRenderer, pathFn, container, styles) {
10275
+ function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
10301
10276
  if (container instanceof InputElementContainer) {
10302
10277
  const size = Math.min(container.bounds.width, container.bounds.height);
10303
10278
  if (container.type === "checkbox" && container.checked) {
@@ -10324,9 +10299,9 @@ function renderFormElements(ctx, fontMetrics, textRenderer, pathFn, container, s
10324
10299
  }
10325
10300
  }
10326
10301
  if (isTextInputElement(container) && container.value.length) {
10327
- const [font, fontFamily, fontSize] = textRenderer.createFontStyle(styles);
10328
- const { baseline } = fontMetrics.getMetrics(fontFamily, fontSize);
10302
+ const [font] = textRenderer.createFontStyle(styles);
10329
10303
  ctx.font = font;
10304
+ const baseline = measureBaseline(ctx, getAbsoluteValue(styles.fontSize, 0));
10330
10305
  ctx.fillStyle = container instanceof InputElementContainer && container.isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
10331
10306
  ctx.textBaseline = "alphabetic";
10332
10307
  ctx.textAlign = canvasTextAlign(container.styles.textAlign);
@@ -10424,7 +10399,6 @@ var CanvasRenderer = class CanvasRenderer {
10424
10399
  this.canvas.style.width = `${options.width}px`;
10425
10400
  this.canvas.style.height = `${options.height}px`;
10426
10401
  }
10427
- this.fontMetrics = new FontMetrics(document);
10428
10402
  this.ctx.scale(this.options.scale, this.options.scale);
10429
10403
  this.ctx.translate(-options.x, -options.y);
10430
10404
  this.ctx.textBaseline = "bottom";
@@ -10448,7 +10422,6 @@ var CanvasRenderer = class CanvasRenderer {
10448
10422
  this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
10449
10423
  this.textRenderer = new TextRenderer({
10450
10424
  ctx: this.ctx,
10451
- context: this.context,
10452
10425
  options: { scale: options.scale }
10453
10426
  });
10454
10427
  this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
@@ -10496,7 +10469,7 @@ var CanvasRenderer = class CanvasRenderer {
10496
10469
  width: this.options.width,
10497
10470
  height: this.options.height
10498
10471
  }, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
10499
- renderFormElements(this.ctx, this.fontMetrics, this.textRenderer, this.path.bind(this), container, styles);
10472
+ renderFormElements(this.ctx, this.textRenderer, this.path.bind(this), container, styles);
10500
10473
  await renderListMarker(this.ctx, this.context, this.textRenderer, paint, container, styles);
10501
10474
  }
10502
10475
  async renderStackContent(stack) {