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