html2canvas-pro 2.3.9 → 2.4.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 2.3.9 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.4.1 <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
  */
@@ -6990,19 +6990,18 @@
6990
6990
  const documentClone = cloneWindow.document;
6991
6991
  const iframeLoad = iframeLoader(iframe).then(async () => {
6992
6992
  this.scrolledElements.forEach(restoreNodeScroll);
6993
- if (cloneWindow) {
6994
- cloneWindow.scrollTo(windowSize.left, windowSize.top);
6995
- if (/AppleWebKit/g.test(navigator.userAgent) && (cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)) {
6996
- this.context.logger.warn("Unable to restore scroll position for cloned document");
6997
- this.context.windowBounds = this.context.windowBounds.add(cloneWindow.scrollX - windowSize.left, cloneWindow.scrollY - windowSize.top, 0, 0);
6998
- }
6999
- }
6993
+ if (cloneWindow) cloneWindow.scrollTo(windowSize.left, windowSize.top);
7000
6994
  const onclone = this.options.onclone;
7001
6995
  const referenceElement = this.clonedReferenceElement;
7002
6996
  if (typeof referenceElement === "undefined") throw new Error(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
7003
6997
  if (documentClone.fonts && documentClone.fonts.ready) await documentClone.fonts.ready;
7004
6998
  if (/(AppleWebKit)/g.test(navigator.userAgent)) await imagesReady(documentClone);
7005
- if (typeof onclone === "function") return Promise.resolve().then(() => onclone(documentClone, referenceElement)).then(() => iframe);
6999
+ if (typeof onclone === "function") await Promise.resolve().then(() => onclone(documentClone, referenceElement));
7000
+ cloneWindow.scrollTo(windowSize.left, windowSize.top);
7001
+ if (/AppleWebKit/g.test(navigator.userAgent) && (cloneWindow.scrollY !== windowSize.top || cloneWindow.scrollX !== windowSize.left)) {
7002
+ this.context.logger.warn("Unable to restore scroll position for cloned document");
7003
+ this.context.windowBounds = this.context.windowBounds.add(cloneWindow.scrollX - windowSize.left, cloneWindow.scrollY - windowSize.top, 0, 0);
7004
+ }
7006
7005
  return iframe;
7007
7006
  });
7008
7007
  /**
@@ -8090,6 +8089,39 @@
8090
8089
  };
8091
8090
  //#endregion
8092
8091
  //#region src/dom/replaced-elements/svg-element-container.ts
8092
+ const BOX_OFFSET_PROPERTIES = [
8093
+ "position",
8094
+ "inset",
8095
+ "top",
8096
+ "right",
8097
+ "bottom",
8098
+ "left",
8099
+ "inset-inline",
8100
+ "inset-inline-start",
8101
+ "inset-inline-end",
8102
+ "inset-block",
8103
+ "inset-block-start",
8104
+ "inset-block-end",
8105
+ "margin",
8106
+ "margin-top",
8107
+ "margin-right",
8108
+ "margin-bottom",
8109
+ "margin-left",
8110
+ "margin-inline",
8111
+ "margin-inline-start",
8112
+ "margin-inline-end",
8113
+ "margin-block",
8114
+ "margin-block-start",
8115
+ "margin-block-end"
8116
+ ];
8117
+ const serializeWithoutBoxOffsets = (serializer, img) => {
8118
+ const style = img.getAttribute("style");
8119
+ BOX_OFFSET_PROPERTIES.forEach((property) => img.style.removeProperty(property));
8120
+ const serialized = serializer.serializeToString(img);
8121
+ if (style === null) img.removeAttribute("style");
8122
+ else img.setAttribute("style", style);
8123
+ return serialized;
8124
+ };
8093
8125
  var SVGElementContainer = class extends ElementContainer {
8094
8126
  constructor(context, img) {
8095
8127
  super(context, img);
@@ -8097,7 +8129,7 @@
8097
8129
  const bounds = parseBounds(context, img);
8098
8130
  img.setAttribute("width", `${bounds.width}px`);
8099
8131
  img.setAttribute("height", `${bounds.height}px`);
8100
- this.svg = `data:image/svg+xml,${encodeURIComponent(s.serializeToString(img))}`;
8132
+ this.svg = `data:image/svg+xml,${encodeURIComponent(serializeWithoutBoxOffsets(s, img))}`;
8101
8133
  this.intrinsicWidth = img.width.baseVal.value;
8102
8134
  this.intrinsicHeight = img.height.baseVal.value;
8103
8135
  this.context.cache.addImage(this.svg);
@@ -9092,8 +9124,8 @@
9092
9124
  /**
9093
9125
  * Font measurement utility
9094
9126
  *
9095
- * Provides Canvas API-based baseline measurement that works correctly with
9096
- * webfonts (which may not be loaded in the original document) and system fonts.
9127
+ * Provides Canvas API-based font measurement that works correctly with webfonts
9128
+ * (which may not be loaded in the original document) and system fonts.
9097
9129
  *
9098
9130
  * Fallback chain:
9099
9131
  * 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
@@ -9101,6 +9133,18 @@
9101
9133
  * 3. fallback value — coarse CSS fallback
9102
9134
  */
9103
9135
  const SAMPLE_TEXT = "Mg";
9136
+ const isValidMetric = (value) => value !== void 0 && !Number.isNaN(value);
9137
+ /**
9138
+ * Measure the baseline ascent and full font box for the currently set font.
9139
+ */
9140
+ const measureFontMetrics = (ctx, fallback) => {
9141
+ const tm = ctx.measureText(SAMPLE_TEXT);
9142
+ const fontMetrics = tm;
9143
+ return {
9144
+ baseline: isValidMetric(fontMetrics.fontBoundingBoxAscent) ? fontMetrics.fontBoundingBoxAscent : isValidMetric(tm.actualBoundingBoxAscent) ? tm.actualBoundingBoxAscent : fallback,
9145
+ height: isValidMetric(fontMetrics.fontBoundingBoxAscent) && isValidMetric(fontMetrics.fontBoundingBoxDescent) ? fontMetrics.fontBoundingBoxAscent + fontMetrics.fontBoundingBoxDescent : isValidMetric(tm.actualBoundingBoxAscent) && isValidMetric(tm.actualBoundingBoxDescent) ? tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent : fallback
9146
+ };
9147
+ };
9104
9148
  /**
9105
9149
  * Measure the baseline ascent for the currently set font.
9106
9150
  *
@@ -9109,10 +9153,7 @@
9109
9153
  * @returns The distance from the text baseline to the top of the bounding box
9110
9154
  */
9111
9155
  const measureBaseline = (ctx, fallback) => {
9112
- const tm = ctx.measureText(SAMPLE_TEXT);
9113
- const fmAscent = tm.fontBoundingBoxAscent;
9114
- const ascent = fmAscent !== void 0 && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
9115
- return ascent !== void 0 && !Number.isNaN(ascent) ? ascent : fallback;
9156
+ return measureFontMetrics(ctx, fallback).baseline;
9116
9157
  };
9117
9158
  //#endregion
9118
9159
  //#region src/render/canvas/text-renderer.ts
@@ -10359,11 +10400,13 @@
10359
10400
  if (isTextInputElement(container) && container.value.length) {
10360
10401
  const [font] = textRenderer.createFontStyle(styles);
10361
10402
  ctx.font = font;
10362
- const baseline = measureBaseline(ctx, getAbsoluteValue(styles.fontSize, 0));
10403
+ const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
10404
+ const { baseline, height: fontHeight } = measureFontMetrics(ctx, fontSizeValue);
10363
10405
  ctx.fillStyle = container instanceof InputElementContainer && container.isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
10364
10406
  ctx.textBaseline = "alphabetic";
10365
10407
  ctx.textAlign = canvasTextAlign(container.styles.textAlign);
10366
10408
  const bounds = contentBox(container);
10409
+ const clipBounds = container instanceof InputElementContainer ? inputTextClipBounds(bounds, paddingBox(container), fontHeight) : bounds;
10367
10410
  let x = 0;
10368
10411
  switch (container.styles.textAlign) {
10369
10412
  case 1:
@@ -10374,21 +10417,17 @@
10374
10417
  break;
10375
10418
  }
10376
10419
  let verticalOffset = 0;
10377
- if (container instanceof InputElementContainer) {
10378
- const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
10379
- verticalOffset = (bounds.height - fontSizeValue) / 2;
10380
- }
10420
+ if (container instanceof InputElementContainer) verticalOffset = (bounds.height - fontHeight) / 2;
10381
10421
  const textBounds = bounds.add(x, verticalOffset, 0, 0);
10382
10422
  ctx.save();
10383
10423
  pathFn([
10384
- new Vector(bounds.left, bounds.top),
10385
- new Vector(bounds.left + bounds.width, bounds.top),
10386
- new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
10387
- new Vector(bounds.left, bounds.top + bounds.height)
10424
+ new Vector(clipBounds.left, clipBounds.top),
10425
+ new Vector(clipBounds.left + clipBounds.width, clipBounds.top),
10426
+ new Vector(clipBounds.left + clipBounds.width, clipBounds.top + clipBounds.height),
10427
+ new Vector(clipBounds.left, clipBounds.top + clipBounds.height)
10388
10428
  ]);
10389
10429
  ctx.clip();
10390
10430
  const lines = wrapTextToWidth(ctx, container.value, bounds.width, styles.whiteSpace === "pre" || styles.whiteSpace === "nowrap");
10391
- const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
10392
10431
  const lineHeight = computeLineHeight(styles.lineHeight, fontSizeValue);
10393
10432
  lines.forEach((line, index) => {
10394
10433
  let lineX = textBounds.left;
@@ -10410,6 +10449,14 @@
10410
10449
  }
10411
10450
  }
10412
10451
  /**
10452
+ * Native single-line inputs may paint value text into their padding area when
10453
+ * fixed height and vertical padding leave a content box shorter than the font.
10454
+ * Preserve the content box horizontally, but avoid clipping those glyphs to the
10455
+ * collapsed vertical content box.
10456
+ * @internal exported for testing
10457
+ */
10458
+ const inputTextClipBounds = (contentBounds, inputPaddingBox, fontHeight) => contentBounds.height < fontHeight ? new Bounds(contentBounds.left, inputPaddingBox.top, contentBounds.width, inputPaddingBox.height) : contentBounds;
10459
+ /**
10413
10460
  * Split text into lines that fit the given pixel width, preserving hard line
10414
10461
  * breaks and (when wrapping is enabled) breaking at word boundaries — mirroring
10415
10462
  * how a <textarea> lays out its value.