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.js
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
|
*/
|
|
@@ -3962,6 +3962,7 @@
|
|
|
3962
3962
|
if (isIdentToken(token)) switch (token.value) {
|
|
3963
3963
|
case "padding-box": return 1;
|
|
3964
3964
|
case "content-box": return 2;
|
|
3965
|
+
case "text": return 3;
|
|
3965
3966
|
}
|
|
3966
3967
|
return 0;
|
|
3967
3968
|
});
|
|
@@ -8950,60 +8951,6 @@
|
|
|
8950
8951
|
}
|
|
8951
8952
|
};
|
|
8952
8953
|
//#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
8954
|
//#region src/render/canvas/text/text-decoration-renderer.ts
|
|
9008
8955
|
var TextDecorationRenderer = class {
|
|
9009
8956
|
constructor(ctx) {
|
|
@@ -9092,6 +9039,33 @@
|
|
|
9092
9039
|
}
|
|
9093
9040
|
};
|
|
9094
9041
|
//#endregion
|
|
9042
|
+
//#region src/render/canvas/font-utils.ts
|
|
9043
|
+
/**
|
|
9044
|
+
* Font measurement utility
|
|
9045
|
+
*
|
|
9046
|
+
* Provides Canvas API-based baseline measurement that works correctly with
|
|
9047
|
+
* webfonts (which may not be loaded in the original document) and system fonts.
|
|
9048
|
+
*
|
|
9049
|
+
* Fallback chain:
|
|
9050
|
+
* 1. fontBoundingBoxAscent — font-level metric (Chrome 99+, FF 116+, Safari 17.4+)
|
|
9051
|
+
* 2. actualBoundingBoxAscent — glyph-level metric (widely supported)
|
|
9052
|
+
* 3. fallback value — coarse CSS fallback
|
|
9053
|
+
*/
|
|
9054
|
+
const SAMPLE_TEXT = "Mg";
|
|
9055
|
+
/**
|
|
9056
|
+
* Measure the baseline ascent for the currently set font.
|
|
9057
|
+
*
|
|
9058
|
+
* @param ctx - Canvas 2D rendering context with ctx.font already set
|
|
9059
|
+
* @param fallback - Fallback value when no metrics are available (e.g. fontSize.number)
|
|
9060
|
+
* @returns The distance from the text baseline to the top of the bounding box
|
|
9061
|
+
*/
|
|
9062
|
+
const measureBaseline = (ctx, fallback) => {
|
|
9063
|
+
const tm = ctx.measureText(SAMPLE_TEXT);
|
|
9064
|
+
const fmAscent = tm.fontBoundingBoxAscent;
|
|
9065
|
+
const ascent = fmAscent !== void 0 && !Number.isNaN(fmAscent) ? fmAscent : tm.actualBoundingBoxAscent;
|
|
9066
|
+
return ascent !== void 0 && !Number.isNaN(ascent) ? ascent : fallback;
|
|
9067
|
+
};
|
|
9068
|
+
//#endregion
|
|
9095
9069
|
//#region src/render/canvas/text-renderer.ts
|
|
9096
9070
|
const iOSBrokenFonts = ["-apple-system", "system-ui"];
|
|
9097
9071
|
/**
|
|
@@ -9144,7 +9118,6 @@
|
|
|
9144
9118
|
constructor(deps) {
|
|
9145
9119
|
this.glyphWidthCache = null;
|
|
9146
9120
|
this.ctx = deps.ctx;
|
|
9147
|
-
this.fontMetrics = deps.fontMetrics;
|
|
9148
9121
|
this.options = deps.options;
|
|
9149
9122
|
this.decorationRenderer = new TextDecorationRenderer(deps.ctx);
|
|
9150
9123
|
}
|
|
@@ -9405,13 +9378,13 @@
|
|
|
9405
9378
|
}
|
|
9406
9379
|
async renderTextNode(text, styles, containerBounds) {
|
|
9407
9380
|
this.glyphWidthCache = null;
|
|
9408
|
-
const [fontString
|
|
9381
|
+
const [fontString] = this.createFontStyle(styles);
|
|
9409
9382
|
this.ctx.font = fontString;
|
|
9410
9383
|
this.ctx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
9411
9384
|
this.ctx.textAlign = "left";
|
|
9412
9385
|
this.ctx.textBaseline = "alphabetic";
|
|
9413
9386
|
const paintOrder = styles.paintOrder;
|
|
9414
|
-
const
|
|
9387
|
+
const baseline = measureBaseline(this.ctx, styles.fontSize.number);
|
|
9415
9388
|
if (styles.webkitLineClamp > 0 && (styles.display & 2) !== 0 && styles.overflowY === 1 && text.textBounds.length > 0) {
|
|
9416
9389
|
if (this.renderLineClampedText(text, styles, paintOrder, baseline, containerBounds)) return;
|
|
9417
9390
|
}
|
|
@@ -10307,7 +10280,7 @@
|
|
|
10307
10280
|
/**
|
|
10308
10281
|
* Render form element content: checkbox, radio, text input.
|
|
10309
10282
|
*/
|
|
10310
|
-
function renderFormElements(ctx,
|
|
10283
|
+
function renderFormElements(ctx, textRenderer, pathFn, container, styles) {
|
|
10311
10284
|
if (container instanceof InputElementContainer) {
|
|
10312
10285
|
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
10313
10286
|
if (container.type === "checkbox" && container.checked) {
|
|
@@ -10334,9 +10307,9 @@
|
|
|
10334
10307
|
}
|
|
10335
10308
|
}
|
|
10336
10309
|
if (isTextInputElement(container) && container.value.length) {
|
|
10337
|
-
const [font
|
|
10338
|
-
const { baseline } = fontMetrics.getMetrics(fontFamily, fontSize);
|
|
10310
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
10339
10311
|
ctx.font = font;
|
|
10312
|
+
const baseline = measureBaseline(ctx, getAbsoluteValue(styles.fontSize, 0));
|
|
10340
10313
|
ctx.fillStyle = container instanceof InputElementContainer && container.isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
|
|
10341
10314
|
ctx.textBaseline = "alphabetic";
|
|
10342
10315
|
ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
@@ -10434,7 +10407,6 @@
|
|
|
10434
10407
|
this.canvas.style.width = `${options.width}px`;
|
|
10435
10408
|
this.canvas.style.height = `${options.height}px`;
|
|
10436
10409
|
}
|
|
10437
|
-
this.fontMetrics = new FontMetrics(document);
|
|
10438
10410
|
this.ctx.scale(this.options.scale, this.options.scale);
|
|
10439
10411
|
this.ctx.translate(-options.x, -options.y);
|
|
10440
10412
|
this.ctx.textBaseline = "bottom";
|
|
@@ -10458,7 +10430,6 @@
|
|
|
10458
10430
|
this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
10459
10431
|
this.textRenderer = new TextRenderer({
|
|
10460
10432
|
ctx: this.ctx,
|
|
10461
|
-
fontMetrics: this.fontMetrics,
|
|
10462
10433
|
options: { scale: options.scale }
|
|
10463
10434
|
});
|
|
10464
10435
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
@@ -10506,7 +10477,7 @@
|
|
|
10506
10477
|
width: this.options.width,
|
|
10507
10478
|
height: this.options.height
|
|
10508
10479
|
}, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
|
|
10509
|
-
renderFormElements(this.ctx, this.
|
|
10480
|
+
renderFormElements(this.ctx, this.textRenderer, this.path.bind(this), container, styles);
|
|
10510
10481
|
await renderListMarker(this.ctx, this.context, this.textRenderer, paint, container, styles);
|
|
10511
10482
|
}
|
|
10512
10483
|
async renderStackContent(stack) {
|
|
@@ -10558,6 +10529,7 @@
|
|
|
10558
10529
|
this.effectsRenderer.applyEffects(paint.getEffects(2));
|
|
10559
10530
|
const styles = paint.container.styles;
|
|
10560
10531
|
const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
|
|
10532
|
+
const hasTextClip = hasTextBackgroundClip(styles);
|
|
10561
10533
|
const borders = [
|
|
10562
10534
|
{
|
|
10563
10535
|
style: styles.borderTopStyle,
|
|
@@ -10580,17 +10552,21 @@
|
|
|
10580
10552
|
width: styles.borderLeftWidth
|
|
10581
10553
|
}
|
|
10582
10554
|
];
|
|
10583
|
-
const
|
|
10555
|
+
const backgroundClipValue = getBackgroundValueForIndex(styles.backgroundClip, 0);
|
|
10556
|
+
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(backgroundClipValue, paint.curves);
|
|
10584
10557
|
if (hasBackground || styles.boxShadow.length) {
|
|
10585
|
-
this.
|
|
10586
|
-
|
|
10587
|
-
|
|
10588
|
-
|
|
10589
|
-
this.ctx.
|
|
10590
|
-
|
|
10558
|
+
if (hasTextClip && paint.container.textNodes.length > 0) await this.renderTextClippedBackground(paint);
|
|
10559
|
+
else {
|
|
10560
|
+
this.ctx.save();
|
|
10561
|
+
this.path(backgroundPaintingArea);
|
|
10562
|
+
this.ctx.clip();
|
|
10563
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10564
|
+
this.ctx.fillStyle = asString(styles.backgroundColor);
|
|
10565
|
+
this.ctx.fill();
|
|
10566
|
+
}
|
|
10567
|
+
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10568
|
+
this.ctx.restore();
|
|
10591
10569
|
}
|
|
10592
|
-
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
10593
|
-
this.ctx.restore();
|
|
10594
10570
|
styles.boxShadow.slice(0).reverse().forEach((shadow) => {
|
|
10595
10571
|
this.ctx.save();
|
|
10596
10572
|
const borderBoxArea = calculateBorderBoxPath(paint.curves);
|
|
@@ -10649,6 +10625,82 @@
|
|
|
10649
10625
|
this.effectsRenderer.applyEffects([]);
|
|
10650
10626
|
return this.canvas;
|
|
10651
10627
|
}
|
|
10628
|
+
/**
|
|
10629
|
+
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
10630
|
+
* This implements background-clip: text support.
|
|
10631
|
+
*
|
|
10632
|
+
* Algorithm:
|
|
10633
|
+
* 1. Draw background (color + images) onto an offscreen canvas
|
|
10634
|
+
* 2. Use destination-in compositing to clip the background to the text glyph shapes
|
|
10635
|
+
* 3. Composite the result back onto the main canvas
|
|
10636
|
+
*
|
|
10637
|
+
* The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
|
|
10638
|
+
* background renderer handles scaling internally via BackgroundRenderer options.
|
|
10639
|
+
*/
|
|
10640
|
+
async renderTextClippedBackground(paint) {
|
|
10641
|
+
const container = paint.container;
|
|
10642
|
+
const styles = container.styles;
|
|
10643
|
+
const bounds = container.bounds;
|
|
10644
|
+
if (bounds.width <= 0 || bounds.height <= 0) return;
|
|
10645
|
+
const offscreen = (this.canvas.ownerDocument ?? document).createElement("canvas");
|
|
10646
|
+
const width = Math.ceil(bounds.width);
|
|
10647
|
+
const height = Math.ceil(bounds.height);
|
|
10648
|
+
offscreen.width = width;
|
|
10649
|
+
offscreen.height = height;
|
|
10650
|
+
const offCtx = offscreen.getContext("2d");
|
|
10651
|
+
if (!offCtx) return;
|
|
10652
|
+
const [fontString] = this.textRenderer.createFontStyle(styles);
|
|
10653
|
+
offCtx.font = fontString;
|
|
10654
|
+
offCtx.textBaseline = "alphabetic";
|
|
10655
|
+
offCtx.textAlign = "left";
|
|
10656
|
+
offCtx.direction = styles.direction === 1 ? "rtl" : "ltr";
|
|
10657
|
+
const baseline = measureBaseline(offCtx, styles.fontSize.number);
|
|
10658
|
+
const bgRenderer = new BackgroundRenderer({
|
|
10659
|
+
ctx: offCtx,
|
|
10660
|
+
context: this.context,
|
|
10661
|
+
canvas: offscreen,
|
|
10662
|
+
options: {
|
|
10663
|
+
width,
|
|
10664
|
+
height,
|
|
10665
|
+
scale: 1
|
|
10666
|
+
}
|
|
10667
|
+
});
|
|
10668
|
+
if (!isTransparent(styles.backgroundColor)) {
|
|
10669
|
+
offCtx.fillStyle = asString(styles.backgroundColor);
|
|
10670
|
+
offCtx.fillRect(0, 0, width, height);
|
|
10671
|
+
}
|
|
10672
|
+
offCtx.save();
|
|
10673
|
+
offCtx.translate(-bounds.left, -bounds.top);
|
|
10674
|
+
await bgRenderer.renderBackgroundImage(container);
|
|
10675
|
+
offCtx.restore();
|
|
10676
|
+
offCtx.globalCompositeOperation = "destination-in";
|
|
10677
|
+
offCtx.fillStyle = "#000";
|
|
10678
|
+
const writingMode = styles.writingMode;
|
|
10679
|
+
const letterSpacing = styles.letterSpacing;
|
|
10680
|
+
for (const textNode of container.textNodes) for (const textBound of textNode.textBounds) {
|
|
10681
|
+
const localLeft = textBound.bounds.left - bounds.left;
|
|
10682
|
+
const localTop = textBound.bounds.top - bounds.top + baseline;
|
|
10683
|
+
if (letterSpacing > 0) this.renderTextMaskWithLetterSpacing(offCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
|
|
10684
|
+
else offCtx.fillText(textBound.text, localLeft, localTop);
|
|
10685
|
+
}
|
|
10686
|
+
this.ctx.drawImage(offscreen, bounds.left, bounds.top);
|
|
10687
|
+
}
|
|
10688
|
+
/**
|
|
10689
|
+
* Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
|
|
10690
|
+
* This ensures the mask matches how the text renderer lays out the characters.
|
|
10691
|
+
*/
|
|
10692
|
+
renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
|
|
10693
|
+
const letters = segmentGraphemes(text.text);
|
|
10694
|
+
let offset = x;
|
|
10695
|
+
for (const letter of letters) {
|
|
10696
|
+
if (isVerticalWritingMode(writingMode)) {
|
|
10697
|
+
ctx.fillText(text.text, x, y);
|
|
10698
|
+
return;
|
|
10699
|
+
}
|
|
10700
|
+
ctx.fillText(letter, offset, y);
|
|
10701
|
+
offset += ctx.measureText(letter).width + letterSpacing;
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10652
10704
|
};
|
|
10653
10705
|
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
10654
10706
|
switch (clip) {
|
|
@@ -10657,6 +10709,12 @@
|
|
|
10657
10709
|
default: return calculatePaddingBoxPath(curves);
|
|
10658
10710
|
}
|
|
10659
10711
|
};
|
|
10712
|
+
/**
|
|
10713
|
+
* Check if any background layer uses background-clip: text
|
|
10714
|
+
*/
|
|
10715
|
+
const hasTextBackgroundClip = (styles) => {
|
|
10716
|
+
return styles.backgroundClip.some((clip) => clip === 3);
|
|
10717
|
+
};
|
|
10660
10718
|
//#endregion
|
|
10661
10719
|
//#region src/render/canvas/foreignobject-renderer.ts
|
|
10662
10720
|
var ForeignObjectRenderer = class {
|