html2canvas-pro 2.3.1 → 2.3.3
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.
- package/dist/html2canvas-pro.cjs +131 -73
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +131 -73
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +131 -73
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +3 -3
- package/dist/lib/css/property-descriptors/background-clip.js +2 -0
- package/dist/lib/render/canvas/canvas-renderer.js +124 -13
- package/dist/lib/render/canvas/content-renderer.js +5 -3
- package/dist/lib/render/canvas/font-utils.js +29 -0
- package/dist/lib/render/canvas/text-renderer.js +7 -6
- package/dist/types/css/property-descriptors/background-clip.d.ts +2 -1
- package/dist/types/render/canvas/canvas-renderer.d.ts +18 -1
- package/dist/types/render/canvas/content-renderer.d.ts +1 -2
- package/dist/types/render/canvas/font-utils.d.ts +19 -0
- package/dist/types/render/canvas/text-renderer.d.ts +0 -3
- package/package.json +1 -1
package/dist/html2canvas-pro.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* html2canvas-pro 2.3.
|
|
2
|
+
* html2canvas-pro 2.3.3 <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
|
*/
|
|
@@ -3959,6 +3959,7 @@ const backgroundClip = {
|
|
|
3959
3959
|
if (isIdentToken(token)) switch (token.value) {
|
|
3960
3960
|
case "padding-box": return 1;
|
|
3961
3961
|
case "content-box": return 2;
|
|
3962
|
+
case "text": return 3;
|
|
3962
3963
|
}
|
|
3963
3964
|
return 0;
|
|
3964
3965
|
});
|
|
@@ -8947,60 +8948,6 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
|
|
|
8947
8948
|
}
|
|
8948
8949
|
};
|
|
8949
8950
|
//#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
8951
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9005
8952
|
var TextDecorationRenderer = class {
|
|
9006
8953
|
constructor(ctx) {
|
|
@@ -9089,6 +9036,33 @@ var TextDecorationRenderer = class {
|
|
|
9089
9036
|
}
|
|
9090
9037
|
};
|
|
9091
9038
|
//#endregion
|
|
9039
|
+
//#region src/render/canvas/font-utils.ts
|
|
9040
|
+
/**
|
|
9041
|
+
* Font measurement utility
|
|
9042
|
+
*
|
|
9043
|
+
* Provides Canvas API-based baseline measurement that works correctly with
|
|
9044
|
+
* webfonts (which may not be loaded in the original document) and system fonts.
|
|
9045
|
+
*
|
|
9046
|
+
* Fallback chain:
|
|
9047
|
+
* 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
|
|
9048
|
+
* 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
|
|
9049
|
+
* 3. fallback value — coarse CSS fallback
|
|
9050
|
+
*/
|
|
9051
|
+
const SAMPLE_TEXT = "Mg";
|
|
9052
|
+
/**
|
|
9053
|
+
* Measure the baseline ascent for the currently set font.
|
|
9054
|
+
*
|
|
9055
|
+
* @param ctx - Canvas 2D rendering context with ctx.font already set
|
|
9056
|
+
* @param fallback - Fallback value when no metrics are available (e.g. fontSize.number)
|
|
9057
|
+
* @returns The distance from the text baseline to the top of the bounding box
|
|
9058
|
+
*/
|
|
9059
|
+
const measureBaseline = (ctx, fallback) => {
|
|
9060
|
+
const tm = ctx.measureText(SAMPLE_TEXT);
|
|
9061
|
+
const fmAscent = tm.fontBoundingBoxAscent;
|
|
9062
|
+
const ascent = fmAscent !== void 0 && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
|
|
9063
|
+
return ascent !== void 0 && !Number.isNaN(ascent) ? ascent : fallback;
|
|
9064
|
+
};
|
|
9065
|
+
//#endregion
|
|
9092
9066
|
//#region src/render/canvas/text-renderer.ts
|
|
9093
9067
|
const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
9094
9068
|
/**
|
|
@@ -9141,7 +9115,6 @@ var TextRenderer = class {
|
|
|
9141
9115
|
constructor(deps) {
|
|
9142
9116
|
this.glyphWidthCache = null;
|
|
9143
9117
|
this.ctx = deps.ctx;
|
|
9144
|
-
this.fontMetrics = deps.fontMetrics;
|
|
9145
9118
|
this.options = deps.options;
|
|
9146
9119
|
this.decorationRenderer = new TextDecorationRenderer(deps.ctx);
|
|
9147
9120
|
}
|
|
@@ -9402,13 +9375,13 @@ var TextRenderer = class {
|
|
|
9402
9375
|
}
|
|
9403
9376
|
async renderTextNode(text, styles, containerBounds) {
|
|
9404
9377
|
this.glyphWidthCache = null;
|
|
9405
|
-
const [fontString
|
|
9378
|
+
const [fontString] = this.createFontStyle(styles);
|
|
9406
9379
|
this.ctx.font = fontString;
|
|
9407
9380
|
this.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
9408
9381
|
this.ctx.textAlign = "left";
|
|
9409
9382
|
this.ctx.textBaseline = "alphabetic";
|
|
9410
9383
|
const paintOrder = styles.paintOrder;
|
|
9411
|
-
const
|
|
9384
|
+
const baseline = measureBaseline(this.ctx, styles.fontSize.number);
|
|
9412
9385
|
if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
|
|
9413
9386
|
if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
9414
9387
|
}
|
|
@@ -10304,7 +10277,7 @@ async function renderReplacedElements(ctx, context, options, iframeRendererFacto
|
|
|
10304
10277
|
/**
|
|
10305
10278
|
* Render form element content: checkbox, radio, text input.
|
|
10306
10279
|
*/
|
|
10307
|
-
function renderFormElements(ctx,
|
|
10280
|
+
function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
10308
10281
|
if (container instanceof InputElementContainer) {
|
|
10309
10282
|
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
10310
10283
|
if (container.type === "checkbox" && container.checked) {
|
|
@@ -10331,9 +10304,9 @@ function renderFormElements(ctx, fontMetrics, textRenderer, pathFn, container, s
|
|
|
10331
10304
|
}
|
|
10332
10305
|
}
|
|
10333
10306
|
if (isTextInputElement(container) && container.value.length) {
|
|
10334
|
-
const [font
|
|
10335
|
-
const { baseline } = fontMetrics.getMetrics(fontFamily, fontSize);
|
|
10307
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
10336
10308
|
ctx.font = font;
|
|
10309
|
+
const baseline = measureBaseline(ctx, getAbsoluteValue(styles.fontSize, 0));
|
|
10337
10310
|
ctx.fillStyle = container instanceof InputElementContainer && container.isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
|
|
10338
10311
|
ctx.textBaseline = "alphabetic";
|
|
10339
10312
|
ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
@@ -10431,7 +10404,6 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10431
10404
|
this.canvas.style.width = `${options.width}px`;
|
|
10432
10405
|
this.canvas.style.height = `${options.height}px`;
|
|
10433
10406
|
}
|
|
10434
|
-
this.fontMetrics = new FontMetrics(document);
|
|
10435
10407
|
this.ctx.scale(this.options.scale, this.options.scale);
|
|
10436
10408
|
this.ctx.translate(-options.x, -options.y);
|
|
10437
10409
|
this.ctx.textBaseline = "bottom";
|
|
@@ -10455,7 +10427,6 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10455
10427
|
this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
10456
10428
|
this.textRenderer = new TextRenderer({
|
|
10457
10429
|
ctx: this.ctx,
|
|
10458
|
-
fontMetrics: this.fontMetrics,
|
|
10459
10430
|
options: { scale: options.scale }
|
|
10460
10431
|
});
|
|
10461
10432
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
@@ -10503,7 +10474,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10503
10474
|
width: this.options.width,
|
|
10504
10475
|
height: this.options.height
|
|
10505
10476
|
}, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
|
|
10506
|
-
renderFormElements(this.ctx, this.
|
|
10477
|
+
renderFormElements(this.ctx, this.textRenderer, this.path.bind(this), container, styles);
|
|
10507
10478
|
await renderListMarker(this.ctx, this.context, this.textRenderer, paint, container, styles);
|
|
10508
10479
|
}
|
|
10509
10480
|
async renderStackContent(stack) {
|
|
@@ -10555,6 +10526,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10555
10526
|
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
10556
10527
|
const styles = paint.container.styles;
|
|
10557
10528
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10529
|
+
const hasTextClip = hasTextBackgroundClip(styles);
|
|
10558
10530
|
const borders = [
|
|
10559
10531
|
{
|
|
10560
10532
|
style: styles.borderTopStyle,
|
|
@@ -10577,17 +10549,21 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10577
10549
|
width: styles.borderLeftWidth
|
|
10578
10550
|
}
|
|
10579
10551
|
];
|
|
10580
|
-
const
|
|
10552
|
+
const backgroundClipValue = getBackgroundValueForIndex(styles.backgroundClip, 0);
|
|
10553
|
+
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(backgroundClipValue, paint.curves);
|
|
10581
10554
|
if (hasBackground || styles.boxShadow.length) {
|
|
10582
|
-
this.
|
|
10583
|
-
|
|
10584
|
-
|
|
10585
|
-
|
|
10586
|
-
this.ctx.
|
|
10587
|
-
|
|
10555
|
+
if (hasTextClip && paint.container.textNodes.length > 0) await this.renderTextClippedBackground(paint);
|
|
10556
|
+
else {
|
|
10557
|
+
this.ctx.save();
|
|
10558
|
+
this.path(backgroundPaintingArea);
|
|
10559
|
+
this.ctx.clip();
|
|
10560
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10561
|
+
this.ctx.fillStyle = asString(styles.backgroundColor);
|
|
10562
|
+
this.ctx.fill();
|
|
10563
|
+
}
|
|
10564
|
+
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10565
|
+
this.ctx.restore();
|
|
10588
10566
|
}
|
|
10589
|
-
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10590
|
-
this.ctx.restore();
|
|
10591
10567
|
styles.boxShadow.slice(0).reverse().forEach((shadow) => {
|
|
10592
10568
|
this.ctx.save();
|
|
10593
10569
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
@@ -10646,6 +10622,82 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10646
10622
|
this.effectsRenderer.applyEffects([]);
|
|
10647
10623
|
return this.canvas;
|
|
10648
10624
|
}
|
|
10625
|
+
/**
|
|
10626
|
+
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
10627
|
+
* This implements background-clip: text support.
|
|
10628
|
+
*
|
|
10629
|
+
* Algorithm:
|
|
10630
|
+
* 1. Draw background (color + images) onto an offscreen canvas
|
|
10631
|
+
* 2. Use destination-in compositing to clip the background to the text glyph shapes
|
|
10632
|
+
* 3. Composite the result back onto the main canvas
|
|
10633
|
+
*
|
|
10634
|
+
* The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
|
|
10635
|
+
* background renderer handles scaling internally via BackgroundRenderer options.
|
|
10636
|
+
*/
|
|
10637
|
+
async renderTextClippedBackground(paint) {
|
|
10638
|
+
const container = paint.container;
|
|
10639
|
+
const styles = container.styles;
|
|
10640
|
+
const bounds = container.bounds;
|
|
10641
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
10642
|
+
const offscreen = (this.canvas.ownerDocument ?? document).createElement("canvas");
|
|
10643
|
+
const width = Math.ceil(bounds.width);
|
|
10644
|
+
const height = Math.ceil(bounds.height);
|
|
10645
|
+
offscreen.width = width;
|
|
10646
|
+
offscreen.height = height;
|
|
10647
|
+
const offCtx = offscreen.getContext("2d");
|
|
10648
|
+
if (!offCtx) return;
|
|
10649
|
+
const [fontString] = this.textRenderer.createFontStyle(styles);
|
|
10650
|
+
offCtx.font = fontString;
|
|
10651
|
+
offCtx.textBaseline = "alphabetic";
|
|
10652
|
+
offCtx.textAlign = "left";
|
|
10653
|
+
offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
10654
|
+
const baseline = measureBaseline(offCtx, styles.fontSize.number);
|
|
10655
|
+
const bgRenderer = new BackgroundRenderer({
|
|
10656
|
+
ctx: offCtx,
|
|
10657
|
+
context: this.context,
|
|
10658
|
+
canvas: offscreen,
|
|
10659
|
+
options: {
|
|
10660
|
+
width,
|
|
10661
|
+
height,
|
|
10662
|
+
scale: 1
|
|
10663
|
+
}
|
|
10664
|
+
});
|
|
10665
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10666
|
+
offCtx.fillStyle = asString(styles.backgroundColor);
|
|
10667
|
+
offCtx.fillRect(0, 0, width, height);
|
|
10668
|
+
}
|
|
10669
|
+
offCtx.save();
|
|
10670
|
+
offCtx.translate(-bounds.left, -bounds.top);
|
|
10671
|
+
await bgRenderer.renderBackgroundImage(container);
|
|
10672
|
+
offCtx.restore();
|
|
10673
|
+
offCtx.globalCompositeOperation = "destination-in";
|
|
10674
|
+
offCtx.fillStyle = "#000";
|
|
10675
|
+
const writingMode = styles.writingMode;
|
|
10676
|
+
const letterSpacing = styles.letterSpacing;
|
|
10677
|
+
for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
|
|
10678
|
+
const localLeft = textBound.bounds.left - bounds.left;
|
|
10679
|
+
const localTop = textBound.bounds.top - bounds.top + baseline;
|
|
10680
|
+
if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(offCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
|
|
10681
|
+
else offCtx.fillText(textBound.text, localLeft, localTop);
|
|
10682
|
+
}
|
|
10683
|
+
this.ctx.drawImage(offscreen, bounds.left, bounds.top);
|
|
10684
|
+
}
|
|
10685
|
+
/**
|
|
10686
|
+
* Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
|
|
10687
|
+
* This ensures the mask matches how the text renderer lays out the characters.
|
|
10688
|
+
*/
|
|
10689
|
+
renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
|
|
10690
|
+
const letters = segmentGraphemes(text.text);
|
|
10691
|
+
let offset = x;
|
|
10692
|
+
for (const letter of letters) {
|
|
10693
|
+
if (isVerticalWritingMode(writingMode)) {
|
|
10694
|
+
ctx.fillText(text.text, x, y);
|
|
10695
|
+
return;
|
|
10696
|
+
}
|
|
10697
|
+
ctx.fillText(letter, offset, y);
|
|
10698
|
+
offset += ctx.measureText(letter).width + letterSpacing;
|
|
10699
|
+
}
|
|
10700
|
+
}
|
|
10649
10701
|
};
|
|
10650
10702
|
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
10651
10703
|
switch (clip) {
|
|
@@ -10654,6 +10706,12 @@ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
|
10654
10706
|
default: return calculatePaddingBoxPath(curves);
|
|
10655
10707
|
}
|
|
10656
10708
|
};
|
|
10709
|
+
/**
|
|
10710
|
+
* Check if any background layer uses background-clip: text
|
|
10711
|
+
*/
|
|
10712
|
+
const hasTextBackgroundClip = (styles) => {
|
|
10713
|
+
return styles.backgroundClip.some((clip) => clip === 3);
|
|
10714
|
+
};
|
|
10657
10715
|
//#endregion
|
|
10658
10716
|
//#region src/render/canvas/foreignobject-renderer.ts
|
|
10659
10717
|
var ForeignObjectRenderer = class {
|