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
|
@@ -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
|
*/
|
|
@@ -3955,6 +3955,7 @@ const backgroundClip = {
|
|
|
3955
3955
|
if (isIdentToken(token)) switch (token.value) {
|
|
3956
3956
|
case "padding-box": return 1;
|
|
3957
3957
|
case "content-box": return 2;
|
|
3958
|
+
case "text": return 3;
|
|
3958
3959
|
}
|
|
3959
3960
|
return 0;
|
|
3960
3961
|
});
|
|
@@ -8943,60 +8944,6 @@ const calculateBackgroundRepeatPath = (repeat, [x, y], [width, height], backgrou
|
|
|
8943
8944
|
}
|
|
8944
8945
|
};
|
|
8945
8946
|
//#endregion
|
|
8946
|
-
//#region src/core/util.ts
|
|
8947
|
-
const SMALL_IMAGE = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
8948
|
-
//#endregion
|
|
8949
|
-
//#region src/render/font-metrics.ts
|
|
8950
|
-
const SAMPLE_TEXT = "Hidden Text";
|
|
8951
|
-
var FontMetrics = class {
|
|
8952
|
-
constructor(document) {
|
|
8953
|
-
this._data = {};
|
|
8954
|
-
this._document = document;
|
|
8955
|
-
}
|
|
8956
|
-
parseMetrics(fontFamily, fontSize) {
|
|
8957
|
-
const container = this._document.createElement("div");
|
|
8958
|
-
const img = this._document.createElement("img");
|
|
8959
|
-
const span = this._document.createElement("span");
|
|
8960
|
-
const body = this._document.body;
|
|
8961
|
-
container.style.visibility = "hidden";
|
|
8962
|
-
container.style.fontFamily = fontFamily;
|
|
8963
|
-
container.style.fontSize = fontSize;
|
|
8964
|
-
container.style.margin = "0";
|
|
8965
|
-
container.style.padding = "0";
|
|
8966
|
-
container.style.whiteSpace = "nowrap";
|
|
8967
|
-
body.appendChild(container);
|
|
8968
|
-
img.src = SMALL_IMAGE;
|
|
8969
|
-
img.width = 1;
|
|
8970
|
-
img.height = 1;
|
|
8971
|
-
img.style.margin = "0";
|
|
8972
|
-
img.style.padding = "0";
|
|
8973
|
-
img.style.verticalAlign = "baseline";
|
|
8974
|
-
span.style.fontFamily = fontFamily;
|
|
8975
|
-
span.style.fontSize = fontSize;
|
|
8976
|
-
span.style.margin = "0";
|
|
8977
|
-
span.style.padding = "0";
|
|
8978
|
-
span.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
|
8979
|
-
container.appendChild(span);
|
|
8980
|
-
container.appendChild(img);
|
|
8981
|
-
const baseline = img.offsetTop - span.offsetTop + 2;
|
|
8982
|
-
container.removeChild(span);
|
|
8983
|
-
container.appendChild(this._document.createTextNode(SAMPLE_TEXT));
|
|
8984
|
-
container.style.lineHeight = "normal";
|
|
8985
|
-
img.style.verticalAlign = "super";
|
|
8986
|
-
const middle = img.offsetTop - container.offsetTop + 2;
|
|
8987
|
-
body.removeChild(container);
|
|
8988
|
-
return {
|
|
8989
|
-
baseline,
|
|
8990
|
-
middle
|
|
8991
|
-
};
|
|
8992
|
-
}
|
|
8993
|
-
getMetrics(fontFamily, fontSize) {
|
|
8994
|
-
const key = `${fontFamily} ${fontSize}`;
|
|
8995
|
-
if (typeof this._data[key] === "undefined") this._data[key] = this.parseMetrics(fontFamily, fontSize);
|
|
8996
|
-
return this._data[key];
|
|
8997
|
-
}
|
|
8998
|
-
};
|
|
8999
|
-
//#endregion
|
|
9000
8947
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9001
8948
|
var TextDecorationRenderer = class {
|
|
9002
8949
|
constructor(ctx) {
|
|
@@ -9085,6 +9032,33 @@ var TextDecorationRenderer = class {
|
|
|
9085
9032
|
}
|
|
9086
9033
|
};
|
|
9087
9034
|
//#endregion
|
|
9035
|
+
//#region src/render/canvas/font-utils.ts
|
|
9036
|
+
/**
|
|
9037
|
+
* Font measurement utility
|
|
9038
|
+
*
|
|
9039
|
+
* Provides Canvas API-based baseline measurement that works correctly with
|
|
9040
|
+
* webfonts (which may not be loaded in the original document) and system fonts.
|
|
9041
|
+
*
|
|
9042
|
+
* Fallback chain:
|
|
9043
|
+
* 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
|
|
9044
|
+
* 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
|
|
9045
|
+
* 3. fallback value — coarse CSS fallback
|
|
9046
|
+
*/
|
|
9047
|
+
const SAMPLE_TEXT = "Mg";
|
|
9048
|
+
/**
|
|
9049
|
+
* Measure the baseline ascent for the currently set font.
|
|
9050
|
+
*
|
|
9051
|
+
* @param ctx - Canvas 2D rendering context with ctx.font already set
|
|
9052
|
+
* @param fallback - Fallback value when no metrics are available (e.g. fontSize.number)
|
|
9053
|
+
* @returns The distance from the text baseline to the top of the bounding box
|
|
9054
|
+
*/
|
|
9055
|
+
const measureBaseline = (ctx, fallback) => {
|
|
9056
|
+
const tm = ctx.measureText(SAMPLE_TEXT);
|
|
9057
|
+
const fmAscent = tm.fontBoundingBoxAscent;
|
|
9058
|
+
const ascent = fmAscent !== void 0 && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
|
|
9059
|
+
return ascent !== void 0 && !Number.isNaN(ascent) ? ascent : fallback;
|
|
9060
|
+
};
|
|
9061
|
+
//#endregion
|
|
9088
9062
|
//#region src/render/canvas/text-renderer.ts
|
|
9089
9063
|
const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
9090
9064
|
/**
|
|
@@ -9137,7 +9111,6 @@ var TextRenderer = class {
|
|
|
9137
9111
|
constructor(deps) {
|
|
9138
9112
|
this.glyphWidthCache = null;
|
|
9139
9113
|
this.ctx = deps.ctx;
|
|
9140
|
-
this.fontMetrics = deps.fontMetrics;
|
|
9141
9114
|
this.options = deps.options;
|
|
9142
9115
|
this.decorationRenderer = new TextDecorationRenderer(deps.ctx);
|
|
9143
9116
|
}
|
|
@@ -9398,13 +9371,13 @@ var TextRenderer = class {
|
|
|
9398
9371
|
}
|
|
9399
9372
|
async renderTextNode(text, styles, containerBounds) {
|
|
9400
9373
|
this.glyphWidthCache = null;
|
|
9401
|
-
const [fontString
|
|
9374
|
+
const [fontString] = this.createFontStyle(styles);
|
|
9402
9375
|
this.ctx.font = fontString;
|
|
9403
9376
|
this.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
9404
9377
|
this.ctx.textAlign = "left";
|
|
9405
9378
|
this.ctx.textBaseline = "alphabetic";
|
|
9406
9379
|
const paintOrder = styles.paintOrder;
|
|
9407
|
-
const
|
|
9380
|
+
const baseline = measureBaseline(this.ctx, styles.fontSize.number);
|
|
9408
9381
|
if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
|
|
9409
9382
|
if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
9410
9383
|
}
|
|
@@ -10300,7 +10273,7 @@ async function renderReplacedElements(ctx, context, options, iframeRendererFacto
|
|
|
10300
10273
|
/**
|
|
10301
10274
|
* Render form element content: checkbox, radio, text input.
|
|
10302
10275
|
*/
|
|
10303
|
-
function renderFormElements(ctx,
|
|
10276
|
+
function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
10304
10277
|
if (container instanceof InputElementContainer) {
|
|
10305
10278
|
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
10306
10279
|
if (container.type === "checkbox" && container.checked) {
|
|
@@ -10327,9 +10300,9 @@ function renderFormElements(ctx, fontMetrics, textRenderer, pathFn, container, s
|
|
|
10327
10300
|
}
|
|
10328
10301
|
}
|
|
10329
10302
|
if (isTextInputElement(container) && container.value.length) {
|
|
10330
|
-
const [font
|
|
10331
|
-
const { baseline } = fontMetrics.getMetrics(fontFamily, fontSize);
|
|
10303
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
10332
10304
|
ctx.font = font;
|
|
10305
|
+
const baseline = measureBaseline(ctx, getAbsoluteValue(styles.fontSize, 0));
|
|
10333
10306
|
ctx.fillStyle = container instanceof InputElementContainer && container.isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
|
|
10334
10307
|
ctx.textBaseline = "alphabetic";
|
|
10335
10308
|
ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
@@ -10427,7 +10400,6 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10427
10400
|
this.canvas.style.width = `${options.width}px`;
|
|
10428
10401
|
this.canvas.style.height = `${options.height}px`;
|
|
10429
10402
|
}
|
|
10430
|
-
this.fontMetrics = new FontMetrics(document);
|
|
10431
10403
|
this.ctx.scale(this.options.scale, this.options.scale);
|
|
10432
10404
|
this.ctx.translate(-options.x, -options.y);
|
|
10433
10405
|
this.ctx.textBaseline = "bottom";
|
|
@@ -10451,7 +10423,6 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10451
10423
|
this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
10452
10424
|
this.textRenderer = new TextRenderer({
|
|
10453
10425
|
ctx: this.ctx,
|
|
10454
|
-
fontMetrics: this.fontMetrics,
|
|
10455
10426
|
options: { scale: options.scale }
|
|
10456
10427
|
});
|
|
10457
10428
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
@@ -10499,7 +10470,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10499
10470
|
width: this.options.width,
|
|
10500
10471
|
height: this.options.height
|
|
10501
10472
|
}, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
|
|
10502
|
-
renderFormElements(this.ctx, this.
|
|
10473
|
+
renderFormElements(this.ctx, this.textRenderer, this.path.bind(this), container, styles);
|
|
10503
10474
|
await renderListMarker(this.ctx, this.context, this.textRenderer, paint, container, styles);
|
|
10504
10475
|
}
|
|
10505
10476
|
async renderStackContent(stack) {
|
|
@@ -10551,6 +10522,7 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10551
10522
|
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
10552
10523
|
const styles = paint.container.styles;
|
|
10553
10524
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10525
|
+
const hasTextClip = hasTextBackgroundClip(styles);
|
|
10554
10526
|
const borders = [
|
|
10555
10527
|
{
|
|
10556
10528
|
style: styles.borderTopStyle,
|
|
@@ -10573,17 +10545,21 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10573
10545
|
width: styles.borderLeftWidth
|
|
10574
10546
|
}
|
|
10575
10547
|
];
|
|
10576
|
-
const
|
|
10548
|
+
const backgroundClipValue = getBackgroundValueForIndex(styles.backgroundClip, 0);
|
|
10549
|
+
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(backgroundClipValue, paint.curves);
|
|
10577
10550
|
if (hasBackground || styles.boxShadow.length) {
|
|
10578
|
-
this.
|
|
10579
|
-
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
this.ctx.
|
|
10583
|
-
|
|
10551
|
+
if (hasTextClip && paint.container.textNodes.length > 0) await this.renderTextClippedBackground(paint);
|
|
10552
|
+
else {
|
|
10553
|
+
this.ctx.save();
|
|
10554
|
+
this.path(backgroundPaintingArea);
|
|
10555
|
+
this.ctx.clip();
|
|
10556
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10557
|
+
this.ctx.fillStyle = asString(styles.backgroundColor);
|
|
10558
|
+
this.ctx.fill();
|
|
10559
|
+
}
|
|
10560
|
+
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10561
|
+
this.ctx.restore();
|
|
10584
10562
|
}
|
|
10585
|
-
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10586
|
-
this.ctx.restore();
|
|
10587
10563
|
styles.boxShadow.slice(0).reverse().forEach((shadow) => {
|
|
10588
10564
|
this.ctx.save();
|
|
10589
10565
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
@@ -10642,6 +10618,82 @@ var CanvasRenderer = class CanvasRenderer {
|
|
|
10642
10618
|
this.effectsRenderer.applyEffects([]);
|
|
10643
10619
|
return this.canvas;
|
|
10644
10620
|
}
|
|
10621
|
+
/**
|
|
10622
|
+
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
10623
|
+
* This implements background-clip: text support.
|
|
10624
|
+
*
|
|
10625
|
+
* Algorithm:
|
|
10626
|
+
* 1. Draw background (color + images) onto an offscreen canvas
|
|
10627
|
+
* 2. Use destination-in compositing to clip the background to the text glyph shapes
|
|
10628
|
+
* 3. Composite the result back onto the main canvas
|
|
10629
|
+
*
|
|
10630
|
+
* The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
|
|
10631
|
+
* background renderer handles scaling internally via BackgroundRenderer options.
|
|
10632
|
+
*/
|
|
10633
|
+
async renderTextClippedBackground(paint) {
|
|
10634
|
+
const container = paint.container;
|
|
10635
|
+
const styles = container.styles;
|
|
10636
|
+
const bounds = container.bounds;
|
|
10637
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
10638
|
+
const offscreen = (this.canvas.ownerDocument ?? document).createElement("canvas");
|
|
10639
|
+
const width = Math.ceil(bounds.width);
|
|
10640
|
+
const height = Math.ceil(bounds.height);
|
|
10641
|
+
offscreen.width = width;
|
|
10642
|
+
offscreen.height = height;
|
|
10643
|
+
const offCtx = offscreen.getContext("2d");
|
|
10644
|
+
if (!offCtx) return;
|
|
10645
|
+
const [fontString] = this.textRenderer.createFontStyle(styles);
|
|
10646
|
+
offCtx.font = fontString;
|
|
10647
|
+
offCtx.textBaseline = "alphabetic";
|
|
10648
|
+
offCtx.textAlign = "left";
|
|
10649
|
+
offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
10650
|
+
const baseline = measureBaseline(offCtx, styles.fontSize.number);
|
|
10651
|
+
const bgRenderer = new BackgroundRenderer({
|
|
10652
|
+
ctx: offCtx,
|
|
10653
|
+
context: this.context,
|
|
10654
|
+
canvas: offscreen,
|
|
10655
|
+
options: {
|
|
10656
|
+
width,
|
|
10657
|
+
height,
|
|
10658
|
+
scale: 1
|
|
10659
|
+
}
|
|
10660
|
+
});
|
|
10661
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10662
|
+
offCtx.fillStyle = asString(styles.backgroundColor);
|
|
10663
|
+
offCtx.fillRect(0, 0, width, height);
|
|
10664
|
+
}
|
|
10665
|
+
offCtx.save();
|
|
10666
|
+
offCtx.translate(-bounds.left, -bounds.top);
|
|
10667
|
+
await bgRenderer.renderBackgroundImage(container);
|
|
10668
|
+
offCtx.restore();
|
|
10669
|
+
offCtx.globalCompositeOperation = "destination-in";
|
|
10670
|
+
offCtx.fillStyle = "#000";
|
|
10671
|
+
const writingMode = styles.writingMode;
|
|
10672
|
+
const letterSpacing = styles.letterSpacing;
|
|
10673
|
+
for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
|
|
10674
|
+
const localLeft = textBound.bounds.left - bounds.left;
|
|
10675
|
+
const localTop = textBound.bounds.top - bounds.top + baseline;
|
|
10676
|
+
if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(offCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
|
|
10677
|
+
else offCtx.fillText(textBound.text, localLeft, localTop);
|
|
10678
|
+
}
|
|
10679
|
+
this.ctx.drawImage(offscreen, bounds.left, bounds.top);
|
|
10680
|
+
}
|
|
10681
|
+
/**
|
|
10682
|
+
* Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
|
|
10683
|
+
* This ensures the mask matches how the text renderer lays out the characters.
|
|
10684
|
+
*/
|
|
10685
|
+
renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
|
|
10686
|
+
const letters = segmentGraphemes(text.text);
|
|
10687
|
+
let offset = x;
|
|
10688
|
+
for (const letter of letters) {
|
|
10689
|
+
if (isVerticalWritingMode(writingMode)) {
|
|
10690
|
+
ctx.fillText(text.text, x, y);
|
|
10691
|
+
return;
|
|
10692
|
+
}
|
|
10693
|
+
ctx.fillText(letter, offset, y);
|
|
10694
|
+
offset += ctx.measureText(letter).width + letterSpacing;
|
|
10695
|
+
}
|
|
10696
|
+
}
|
|
10645
10697
|
};
|
|
10646
10698
|
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
10647
10699
|
switch (clip) {
|
|
@@ -10650,6 +10702,12 @@ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
|
10650
10702
|
default: return calculatePaddingBoxPath(curves);
|
|
10651
10703
|
}
|
|
10652
10704
|
};
|
|
10705
|
+
/**
|
|
10706
|
+
* Check if any background layer uses background-clip: text
|
|
10707
|
+
*/
|
|
10708
|
+
const hasTextBackgroundClip = (styles) => {
|
|
10709
|
+
return styles.backgroundClip.some((clip) => clip === 3);
|
|
10710
|
+
};
|
|
10653
10711
|
//#endregion
|
|
10654
10712
|
//#region src/render/canvas/foreignobject-renderer.ts
|
|
10655
10713
|
var ForeignObjectRenderer = class {
|