html2canvas-pro 2.3.2 → 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 +98 -10
- package/dist/html2canvas-pro.cjs.map +1 -1
- package/dist/html2canvas-pro.esm.js +98 -10
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +98 -10
- 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 +123 -9
- package/dist/types/css/property-descriptors/background-clip.d.ts +2 -1
- package/dist/types/render/canvas/canvas-renderer.d.ts +18 -0
- package/package.json +1 -1
|
@@ -16,6 +16,9 @@ const canvas_path_1 = require("./canvas-path");
|
|
|
16
16
|
const object_fit_1 = require("../object-fit");
|
|
17
17
|
const content_renderer_1 = require("./content-renderer");
|
|
18
18
|
const constants_1 = require("../../core/constants");
|
|
19
|
+
const writing_mode_1 = require("../../css/property-descriptors/writing-mode");
|
|
20
|
+
const font_utils_1 = require("./font-utils");
|
|
21
|
+
const text_1 = require("../../css/layout/text");
|
|
19
22
|
class CanvasRenderer {
|
|
20
23
|
constructor(context, options) {
|
|
21
24
|
this.context = context;
|
|
@@ -205,23 +208,31 @@ class CanvasRenderer {
|
|
|
205
208
|
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
|
|
206
209
|
const styles = paint.container.styles;
|
|
207
210
|
const hasBackground = !(0, color_utilities_1.isTransparent)(styles.backgroundColor) || styles.backgroundImage.length;
|
|
211
|
+
const hasTextClip = hasTextBackgroundClip(styles);
|
|
208
212
|
const borders = [
|
|
209
213
|
{ style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
|
|
210
214
|
{ style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
|
|
211
215
|
{ style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
|
|
212
216
|
{ style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
|
|
213
217
|
];
|
|
214
|
-
const
|
|
218
|
+
const backgroundClipValue = (0, background_1.getBackgroundValueForIndex)(styles.backgroundClip, 0);
|
|
219
|
+
const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(backgroundClipValue, paint.curves);
|
|
215
220
|
if (hasBackground || styles.boxShadow.length) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
this.ctx.
|
|
221
|
+
// Handle background-clip: text
|
|
222
|
+
if (hasTextClip && paint.container.textNodes.length > 0) {
|
|
223
|
+
await this.renderTextClippedBackground(paint);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
this.ctx.save();
|
|
227
|
+
this.path(backgroundPaintingArea);
|
|
228
|
+
this.ctx.clip();
|
|
229
|
+
if (!(0, color_utilities_1.isTransparent)(styles.backgroundColor)) {
|
|
230
|
+
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.backgroundColor);
|
|
231
|
+
this.ctx.fill();
|
|
232
|
+
}
|
|
233
|
+
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
234
|
+
this.ctx.restore();
|
|
222
235
|
}
|
|
223
|
-
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
224
|
-
this.ctx.restore();
|
|
225
236
|
styles.boxShadow
|
|
226
237
|
.slice(0)
|
|
227
238
|
.reverse()
|
|
@@ -297,6 +308,103 @@ class CanvasRenderer {
|
|
|
297
308
|
this.effectsRenderer.applyEffects([]);
|
|
298
309
|
return this.canvas;
|
|
299
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
313
|
+
* This implements background-clip: text support.
|
|
314
|
+
*
|
|
315
|
+
* Algorithm:
|
|
316
|
+
* 1. Draw background (color + images) onto an offscreen canvas
|
|
317
|
+
* 2. Use destination-in compositing to clip the background to the text glyph shapes
|
|
318
|
+
* 3. Composite the result back onto the main canvas
|
|
319
|
+
*
|
|
320
|
+
* The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
|
|
321
|
+
* background renderer handles scaling internally via BackgroundRenderer options.
|
|
322
|
+
*/
|
|
323
|
+
async renderTextClippedBackground(paint) {
|
|
324
|
+
const container = paint.container;
|
|
325
|
+
const styles = container.styles;
|
|
326
|
+
const bounds = container.bounds;
|
|
327
|
+
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const ownerDocument = this.canvas.ownerDocument ?? document;
|
|
331
|
+
const offscreen = ownerDocument.createElement('canvas');
|
|
332
|
+
const width = Math.ceil(bounds.width);
|
|
333
|
+
const height = Math.ceil(bounds.height);
|
|
334
|
+
offscreen.width = width;
|
|
335
|
+
offscreen.height = height;
|
|
336
|
+
const offCtx = offscreen.getContext('2d');
|
|
337
|
+
if (!offCtx) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// ── Set up font matching the main context ──
|
|
341
|
+
const [fontString] = this.textRenderer.createFontStyle(styles);
|
|
342
|
+
offCtx.font = fontString;
|
|
343
|
+
offCtx.textBaseline = 'alphabetic';
|
|
344
|
+
offCtx.textAlign = 'left';
|
|
345
|
+
offCtx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
|
|
346
|
+
// Measure baseline from the actual rendered font
|
|
347
|
+
const baseline = (0, font_utils_1.measureBaseline)(offCtx, styles.fontSize.number);
|
|
348
|
+
// ── Draw background onto offscreen canvas ──
|
|
349
|
+
const bgRenderer = new background_renderer_1.BackgroundRenderer({
|
|
350
|
+
ctx: offCtx,
|
|
351
|
+
context: this.context,
|
|
352
|
+
canvas: offscreen,
|
|
353
|
+
options: { width, height, scale: 1 }
|
|
354
|
+
});
|
|
355
|
+
if (!(0, color_utilities_1.isTransparent)(styles.backgroundColor)) {
|
|
356
|
+
offCtx.fillStyle = (0, color_utilities_1.asString)(styles.backgroundColor);
|
|
357
|
+
offCtx.fillRect(0, 0, width, height);
|
|
358
|
+
}
|
|
359
|
+
// Background images are positioned relative to the element bounds,
|
|
360
|
+
// so we need the BackgroundRenderer to compute offsets in the same
|
|
361
|
+
// coordinate space as the main renderer. We do that by translating
|
|
362
|
+
// the offscreen context so that the element origin (bounds.left, bounds.top)
|
|
363
|
+
// falls at (0, 0).
|
|
364
|
+
offCtx.save();
|
|
365
|
+
offCtx.translate(-bounds.left, -bounds.top);
|
|
366
|
+
await bgRenderer.renderBackgroundImage(container);
|
|
367
|
+
offCtx.restore();
|
|
368
|
+
// ── Clip background to text glyphs ──
|
|
369
|
+
// destination-in: keep background pixels only where the source (text) is non-transparent.
|
|
370
|
+
offCtx.globalCompositeOperation = 'destination-in';
|
|
371
|
+
offCtx.fillStyle = '#000';
|
|
372
|
+
const writingMode = styles.writingMode;
|
|
373
|
+
const letterSpacing = styles.letterSpacing;
|
|
374
|
+
for (const textNode of container.textNodes) {
|
|
375
|
+
for (const textBound of textNode.textBounds) {
|
|
376
|
+
// Offset from element bounds to canvas-local coordinates
|
|
377
|
+
const localLeft = textBound.bounds.left - bounds.left;
|
|
378
|
+
const localTop = textBound.bounds.top - bounds.top + baseline;
|
|
379
|
+
if (letterSpacing > 0) {
|
|
380
|
+
this.renderTextMaskWithLetterSpacing(offCtx, textBound, letterSpacing, localLeft, localTop, writingMode);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
offCtx.fillText(textBound.text, localLeft, localTop);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ── Composite back to main canvas ──
|
|
388
|
+
this.ctx.drawImage(offscreen, bounds.left, bounds.top);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
|
|
392
|
+
* This ensures the mask matches how the text renderer lays out the characters.
|
|
393
|
+
*/
|
|
394
|
+
renderTextMaskWithLetterSpacing(ctx, text, letterSpacing, x, y, writingMode) {
|
|
395
|
+
const letters = (0, text_1.segmentGraphemes)(text.text);
|
|
396
|
+
let offset = x;
|
|
397
|
+
for (const letter of letters) {
|
|
398
|
+
if ((0, writing_mode_1.isVerticalWritingMode)(writingMode)) {
|
|
399
|
+
// Vertical writing mode: not yet supported for text-clip;
|
|
400
|
+
// fall back to single fillText call.
|
|
401
|
+
ctx.fillText(text.text, x, y);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
ctx.fillText(letter, offset, y);
|
|
405
|
+
offset += ctx.measureText(letter).width + letterSpacing;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
300
408
|
}
|
|
301
409
|
exports.CanvasRenderer = CanvasRenderer;
|
|
302
410
|
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
@@ -310,3 +418,9 @@ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
|
310
418
|
return (0, bound_curves_1.calculatePaddingBoxPath)(curves);
|
|
311
419
|
}
|
|
312
420
|
};
|
|
421
|
+
/**
|
|
422
|
+
* Check if any background layer uses background-clip: text
|
|
423
|
+
*/
|
|
424
|
+
const hasTextBackgroundClip = (styles) => {
|
|
425
|
+
return styles.backgroundClip.some((clip) => clip === 3 /* BACKGROUND_CLIP.TEXT */);
|
|
426
|
+
};
|
|
@@ -2,7 +2,8 @@ import { IPropertyListDescriptor } from '../property-descriptor';
|
|
|
2
2
|
export declare const enum BACKGROUND_CLIP {
|
|
3
3
|
BORDER_BOX = 0,
|
|
4
4
|
PADDING_BOX = 1,
|
|
5
|
-
CONTENT_BOX = 2
|
|
5
|
+
CONTENT_BOX = 2,
|
|
6
|
+
TEXT = 3
|
|
6
7
|
}
|
|
7
8
|
export type BackgroundClip = BACKGROUND_CLIP[];
|
|
8
9
|
export declare const backgroundClip: IPropertyListDescriptor<BackgroundClip>;
|
|
@@ -57,4 +57,22 @@ export declare class CanvasRenderer {
|
|
|
57
57
|
formatPath(paths: Path[]): void;
|
|
58
58
|
renderNodeBackgroundAndBorders(paint: ElementPaint): Promise<void>;
|
|
59
59
|
render(element: ElementContainer): Promise<HTMLCanvasElement>;
|
|
60
|
+
/**
|
|
61
|
+
* Render background clipped to text shape using offscreen canvas with destination-in compositing.
|
|
62
|
+
* This implements background-clip: text support.
|
|
63
|
+
*
|
|
64
|
+
* Algorithm:
|
|
65
|
+
* 1. Draw background (color + images) onto an offscreen canvas
|
|
66
|
+
* 2. Use destination-in compositing to clip the background to the text glyph shapes
|
|
67
|
+
* 3. Composite the result back onto the main canvas
|
|
68
|
+
*
|
|
69
|
+
* The offscreen canvas uses CSS-pixel dimensions (not device-pixel) because the
|
|
70
|
+
* background renderer handles scaling internally via BackgroundRenderer options.
|
|
71
|
+
*/
|
|
72
|
+
private renderTextClippedBackground;
|
|
73
|
+
/**
|
|
74
|
+
* Render text glyphs one-by-one as a mask, applying letter-spacing between characters.
|
|
75
|
+
* This ensures the mask matches how the text renderer lays out the characters.
|
|
76
|
+
*/
|
|
77
|
+
private renderTextMaskWithLetterSpacing;
|
|
60
78
|
}
|