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