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