html2canvas-pro 2.1.1 → 2.2.0
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.esm.js +10226 -10540
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +10869 -11185
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +8 -8
- package/dist/lib/config.js +0 -22
- package/dist/lib/core/cache-storage.js +1 -38
- package/dist/lib/core/constants.js +25 -0
- package/dist/lib/core/context.js +1 -0
- package/dist/lib/core/features.js +1 -0
- package/dist/lib/core/validator.js +3 -3
- package/dist/lib/css/grouped/background-styles.js +36 -0
- package/dist/lib/css/grouped/border-styles.js +75 -0
- package/dist/lib/css/grouped/font-styles.js +93 -0
- package/dist/lib/css/grouped/layout-styles.js +127 -0
- package/dist/lib/css/index.js +74 -46
- package/dist/lib/css/layout/text.js +7 -6
- package/dist/lib/css/property-descriptors/background-blend-mode.js +41 -0
- package/dist/lib/css/property-descriptors/border-image-repeat.js +42 -0
- package/dist/lib/css/property-descriptors/border-image-slice.js +45 -0
- package/dist/lib/css/property-descriptors/border-image-source.js +21 -0
- package/dist/lib/css/property-descriptors/border-radius.js +1 -1
- package/dist/lib/css/property-descriptors/box-decoration-break.js +18 -0
- package/dist/lib/css/property-descriptors/counter-increment.js +17 -12
- package/dist/lib/css/property-descriptors/counter-reset.js +4 -12
- package/dist/lib/css/property-descriptors/filter.js +76 -0
- package/dist/lib/css/property-descriptors/font-variant-ligatures.js +34 -0
- package/dist/lib/css/property-descriptors/object-fit.js +1 -1
- package/dist/lib/css/property-descriptors/object-position.js +42 -0
- package/dist/lib/css/property-descriptors/visibility.js +1 -1
- package/dist/lib/css/property-descriptors/zoom.js +18 -0
- package/dist/lib/css/syntax/parser.js +0 -1
- package/dist/lib/css/types/color.js +5 -1
- package/dist/lib/css/types/functions/repeating-linear-gradient.js +9 -0
- package/dist/lib/css/types/image.js +12 -2
- package/dist/lib/css/types/length-percentage.js +6 -2
- package/dist/lib/css/types/safe-eval.js +80 -0
- package/dist/lib/dom/document-cloner.js +23 -163
- package/dist/lib/dom/slot-cloner.js +176 -0
- package/dist/lib/index.js +1 -17
- package/dist/lib/render/canvas/background-renderer.js +165 -32
- package/dist/lib/render/canvas/border-image-renderer.js +153 -0
- package/dist/lib/render/canvas/canvas-renderer.js +34 -189
- package/dist/lib/render/canvas/content-renderer.js +202 -0
- package/dist/lib/render/canvas/effects-renderer.js +3 -0
- package/dist/lib/render/canvas/text/text-decoration-renderer.js +99 -0
- package/dist/lib/render/canvas/text-renderer.js +100 -224
- package/dist/lib/render/effects.js +38 -3
- package/dist/lib/render/object-fit.js +19 -15
- package/dist/lib/render/stacking-context.js +11 -0
- package/dist/types/config.d.ts +0 -10
- package/dist/types/core/cache-storage.d.ts +0 -24
- package/dist/types/core/constants.d.ts +22 -0
- package/dist/types/core/context.d.ts +3 -0
- package/dist/types/core/performance-monitor.d.ts +4 -4
- package/dist/types/core/validator.d.ts +6 -8
- package/dist/types/css/grouped/background-styles.d.ts +16 -0
- package/dist/types/css/grouped/border-styles.d.ts +31 -0
- package/dist/types/css/grouped/font-styles.d.ts +35 -0
- package/dist/types/css/grouped/layout-styles.d.ts +46 -0
- package/dist/types/css/index.d.ts +30 -0
- package/dist/types/css/property-descriptors/background-blend-mode.d.ts +23 -0
- package/dist/types/css/property-descriptors/border-image-repeat.d.ts +12 -0
- package/dist/types/css/property-descriptors/border-image-slice.d.ts +10 -0
- package/dist/types/css/property-descriptors/border-image-source.d.ts +4 -0
- package/dist/types/css/property-descriptors/box-decoration-break.d.ts +6 -0
- package/dist/types/css/property-descriptors/counter-increment.d.ts +3 -0
- package/dist/types/css/property-descriptors/filter.d.ts +3 -0
- package/dist/types/css/property-descriptors/font-variant-ligatures.d.ts +14 -0
- package/dist/types/css/property-descriptors/object-position.d.ts +4 -0
- package/dist/types/css/property-descriptors/zoom.d.ts +3 -0
- package/dist/types/css/types/functions/repeating-linear-gradient.d.ts +4 -0
- package/dist/types/css/types/image.d.ts +4 -2
- package/dist/types/css/types/safe-eval.d.ts +8 -0
- package/dist/types/dom/document-cloner.d.ts +3 -44
- package/dist/types/dom/slot-cloner.d.ts +66 -0
- package/dist/types/index.d.ts +3 -7
- package/dist/types/options.d.ts +11 -0
- package/dist/types/render/canvas/background-renderer.d.ts +23 -0
- package/dist/types/render/canvas/border-image-renderer.d.ts +18 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +1 -0
- package/dist/types/render/canvas/content-renderer.d.ts +44 -0
- package/dist/types/render/canvas/text/text-decoration-renderer.d.ts +18 -0
- package/dist/types/render/canvas/text-renderer.d.ts +12 -1
- package/dist/types/render/effects.d.ts +12 -2
- package/dist/types/render/object-fit.d.ts +2 -1
- package/dist/types/render/renderer-interface.d.ts +11 -9
- package/package.json +5 -10
- package/dist/lib/dom/replaced-elements/pseudo-elements.js +0 -0
- package/dist/types/dom/replaced-elements/pseudo-elements.d.ts +0 -0
|
@@ -17,6 +17,7 @@ const text_1 = require("../../css/layout/text");
|
|
|
17
17
|
const color_utilities_1 = require("../../css/types/color-utilities");
|
|
18
18
|
const parser_1 = require("../../css/syntax/parser");
|
|
19
19
|
const writing_mode_1 = require("../../css/property-descriptors/writing-mode");
|
|
20
|
+
const text_decoration_renderer_1 = require("./text/text-decoration-renderer");
|
|
20
21
|
// iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
|
|
21
22
|
const iOSBrokenFonts = ['-apple-system', 'system-ui'];
|
|
22
23
|
/**
|
|
@@ -93,8 +94,8 @@ const getTextStrokeLineJoin = () => {
|
|
|
93
94
|
class TextRenderer {
|
|
94
95
|
constructor(deps) {
|
|
95
96
|
this.ctx = deps.ctx;
|
|
96
|
-
// context stored but not used directly in this renderer
|
|
97
97
|
this.options = deps.options;
|
|
98
|
+
this.decorationRenderer = new text_decoration_renderer_1.TextDecorationRenderer(deps.ctx);
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
101
|
* Iterate grapheme clusters one-by-one, applying correct letter-spacing and
|
|
@@ -236,106 +237,7 @@ class TextRenderer {
|
|
|
236
237
|
});
|
|
237
238
|
}
|
|
238
239
|
renderTextDecoration(bounds, styles) {
|
|
239
|
-
this.
|
|
240
|
-
// Calculate decoration line thickness
|
|
241
|
-
let thickness = 1; // default
|
|
242
|
-
if (typeof styles.textDecorationThickness === 'number') {
|
|
243
|
-
thickness = styles.textDecorationThickness;
|
|
244
|
-
}
|
|
245
|
-
else if (styles.textDecorationThickness === 'from-font') {
|
|
246
|
-
// Use a reasonable default based on font size
|
|
247
|
-
thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
|
|
248
|
-
}
|
|
249
|
-
// 'auto' uses default thickness of 1
|
|
250
|
-
// Calculate underline offset
|
|
251
|
-
let underlineOffset = 0;
|
|
252
|
-
if (typeof styles.textUnderlineOffset === 'number') {
|
|
253
|
-
// It's a pixel value
|
|
254
|
-
underlineOffset = styles.textUnderlineOffset;
|
|
255
|
-
}
|
|
256
|
-
// 'auto' uses default offset of 0
|
|
257
|
-
const decorationStyle = styles.textDecorationStyle;
|
|
258
|
-
styles.textDecorationLine.forEach((textDecorationLine) => {
|
|
259
|
-
let y = 0;
|
|
260
|
-
switch (textDecorationLine) {
|
|
261
|
-
case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
|
|
262
|
-
y = bounds.top + bounds.height - thickness + underlineOffset;
|
|
263
|
-
break;
|
|
264
|
-
case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
|
|
265
|
-
y = bounds.top;
|
|
266
|
-
break;
|
|
267
|
-
case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
|
|
268
|
-
y = bounds.top + (bounds.height / 2 - thickness / 2);
|
|
269
|
-
break;
|
|
270
|
-
default:
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
drawDecorationLine(x, y, width, thickness, style) {
|
|
277
|
-
switch (style) {
|
|
278
|
-
case 0 /* TEXT_DECORATION_STYLE.SOLID */:
|
|
279
|
-
// Solid line (default)
|
|
280
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
281
|
-
break;
|
|
282
|
-
case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
|
|
283
|
-
// Double line
|
|
284
|
-
const gap = Math.max(1, thickness);
|
|
285
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
286
|
-
this.ctx.fillRect(x, y + thickness + gap, width, thickness);
|
|
287
|
-
break;
|
|
288
|
-
case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
|
|
289
|
-
// Dotted line
|
|
290
|
-
this.ctx.save();
|
|
291
|
-
this.ctx.beginPath();
|
|
292
|
-
this.ctx.setLineDash([thickness, thickness * 2]);
|
|
293
|
-
this.ctx.lineWidth = thickness;
|
|
294
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
295
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
296
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
297
|
-
this.ctx.stroke();
|
|
298
|
-
this.ctx.restore();
|
|
299
|
-
break;
|
|
300
|
-
case 3 /* TEXT_DECORATION_STYLE.DASHED */:
|
|
301
|
-
// Dashed line
|
|
302
|
-
this.ctx.save();
|
|
303
|
-
this.ctx.beginPath();
|
|
304
|
-
this.ctx.setLineDash([thickness * 3, thickness * 2]);
|
|
305
|
-
this.ctx.lineWidth = thickness;
|
|
306
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
307
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
308
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
309
|
-
this.ctx.stroke();
|
|
310
|
-
this.ctx.restore();
|
|
311
|
-
break;
|
|
312
|
-
case 4 /* TEXT_DECORATION_STYLE.WAVY */:
|
|
313
|
-
// Wavy line (approximation using quadratic curves)
|
|
314
|
-
this.ctx.save();
|
|
315
|
-
this.ctx.beginPath();
|
|
316
|
-
this.ctx.lineWidth = thickness;
|
|
317
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
318
|
-
const amplitude = thickness * 2;
|
|
319
|
-
const wavelength = thickness * 4;
|
|
320
|
-
let currentX = x;
|
|
321
|
-
this.ctx.moveTo(currentX, y + thickness / 2);
|
|
322
|
-
while (currentX < x + width) {
|
|
323
|
-
const nextX = Math.min(currentX + wavelength / 2, x + width);
|
|
324
|
-
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
|
|
325
|
-
currentX = nextX;
|
|
326
|
-
if (currentX < x + width) {
|
|
327
|
-
const nextX2 = Math.min(currentX + wavelength / 2, x + width);
|
|
328
|
-
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
|
|
329
|
-
currentX = nextX2;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
this.ctx.stroke();
|
|
333
|
-
this.ctx.restore();
|
|
334
|
-
break;
|
|
335
|
-
default:
|
|
336
|
-
// Fallback to solid
|
|
337
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
338
|
-
}
|
|
240
|
+
this.decorationRenderer.render(bounds, styles);
|
|
339
241
|
}
|
|
340
242
|
// Helper method to truncate text and add ellipsis if needed
|
|
341
243
|
truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
|
|
@@ -402,142 +304,116 @@ class TextRenderer {
|
|
|
402
304
|
fontSize
|
|
403
305
|
];
|
|
404
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Render text with -webkit-line-clamp truncation.
|
|
309
|
+
* Groups text bounds by their Y position into visual lines, then renders
|
|
310
|
+
* only the first N-1 complete lines followed by an ellipsis on the Nth line.
|
|
311
|
+
*/
|
|
312
|
+
renderLineClampedText(text, styles, paintOrder, containerBounds) {
|
|
313
|
+
const lineHeight = styles.fontSize.number * 1.5;
|
|
314
|
+
const lines = [];
|
|
315
|
+
let currentLine = [];
|
|
316
|
+
let currentLineTop = text.textBounds[0].bounds.top;
|
|
317
|
+
text.textBounds.forEach((tb) => {
|
|
318
|
+
if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
|
|
319
|
+
if (currentLine.length > 0)
|
|
320
|
+
lines.push(currentLine);
|
|
321
|
+
currentLine = [tb];
|
|
322
|
+
currentLineTop = tb.bounds.top;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
currentLine.push(tb);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
if (currentLine.length > 0)
|
|
329
|
+
lines.push(currentLine);
|
|
330
|
+
const maxLines = styles.webkitLineClamp;
|
|
331
|
+
if (lines.length <= maxLines)
|
|
332
|
+
return false; // fall through to normal rendering
|
|
333
|
+
// Render full lines (0..N-2)
|
|
334
|
+
for (let i = 0; i < maxLines - 1; i++) {
|
|
335
|
+
lines[i].forEach((tb) => this.renderTextBoundWithPaintOrder(tb, styles, paintOrder));
|
|
336
|
+
}
|
|
337
|
+
// Nth line: truncated with ellipsis
|
|
338
|
+
const lastLine = lines[maxLines - 1];
|
|
339
|
+
if (lastLine?.length && containerBounds) {
|
|
340
|
+
const textStr = lastLine.map((tb) => tb.text).join('');
|
|
341
|
+
const first = lastLine[0];
|
|
342
|
+
const avail = containerBounds.width - (first.bounds.left - containerBounds.left);
|
|
343
|
+
const truncated = this.truncateTextWithEllipsis(textStr, avail, styles.letterSpacing);
|
|
344
|
+
const bounds = new text_1.TextBounds(truncated, first.bounds);
|
|
345
|
+
for (const layer of paintOrder) {
|
|
346
|
+
if (layer === 0 /* PAINT_ORDER_LAYER.FILL */) {
|
|
347
|
+
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
348
|
+
this.renderTextWithLetterSpacing(bounds, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
|
|
349
|
+
}
|
|
350
|
+
else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */) {
|
|
351
|
+
this.renderTextStrokeWithStyle(bounds, styles);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Render single-line text with text-overflow: ellipsis.
|
|
359
|
+
* Returns true if ellipsis was applied (caller should skip normal rendering).
|
|
360
|
+
*/
|
|
361
|
+
renderEllipsisText(text, styles, paintOrder, containerBounds) {
|
|
362
|
+
const lineHeight = styles.fontSize.number * 1.5;
|
|
363
|
+
const firstTop = text.textBounds[0].bounds.top;
|
|
364
|
+
const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
|
|
365
|
+
if (!isSingleLine)
|
|
366
|
+
return false;
|
|
367
|
+
let fullText = text.textBounds
|
|
368
|
+
.map((tb) => tb.text)
|
|
369
|
+
.join('')
|
|
370
|
+
.replace(/\s+/g, ' ')
|
|
371
|
+
.trim();
|
|
372
|
+
const fullWidth = this.ctx.measureText(fullText).width;
|
|
373
|
+
if (fullWidth <= containerBounds.width)
|
|
374
|
+
return false;
|
|
375
|
+
const truncated = this.truncateTextWithEllipsis(fullText, containerBounds.width, styles.letterSpacing);
|
|
376
|
+
const bounds = new text_1.TextBounds(truncated, text.textBounds[0].bounds);
|
|
377
|
+
for (const layer of paintOrder) {
|
|
378
|
+
if (layer === 0 /* PAINT_ORDER_LAYER.FILL */)
|
|
379
|
+
this.renderTextFillWithShadows(bounds, styles);
|
|
380
|
+
else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */)
|
|
381
|
+
this.renderTextStrokeWithStyle(bounds, styles);
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
405
385
|
async renderTextNode(text, styles, containerBounds) {
|
|
406
|
-
|
|
407
|
-
this.ctx.font = font;
|
|
386
|
+
this.ctx.font = this.createFontStyle(styles)[0];
|
|
408
387
|
this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
|
|
409
388
|
this.ctx.textAlign = 'left';
|
|
410
389
|
this.ctx.textBaseline = 'alphabetic';
|
|
411
390
|
const paintOrder = styles.paintOrder;
|
|
412
|
-
//
|
|
413
|
-
const
|
|
414
|
-
// Check if we need to apply -webkit-line-clamp
|
|
415
|
-
// This limits text to a specific number of lines with ellipsis
|
|
416
|
-
const shouldApplyLineClamp = styles.webkitLineClamp > 0 &&
|
|
391
|
+
// -webkit-line-clamp
|
|
392
|
+
const clamp = styles.webkitLineClamp > 0 &&
|
|
417
393
|
(styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
|
|
418
394
|
styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
|
|
419
395
|
text.textBounds.length > 0;
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
let currentLine = [];
|
|
424
|
-
let currentLineTop = text.textBounds[0].bounds.top;
|
|
425
|
-
text.textBounds.forEach((tb) => {
|
|
426
|
-
// If this text bound is on a different line, start a new line
|
|
427
|
-
if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
|
|
428
|
-
if (currentLine.length > 0) {
|
|
429
|
-
lines.push(currentLine);
|
|
430
|
-
}
|
|
431
|
-
currentLine = [tb];
|
|
432
|
-
currentLineTop = tb.bounds.top;
|
|
433
|
-
}
|
|
434
|
-
else {
|
|
435
|
-
currentLine.push(tb);
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
// Don't forget the last line
|
|
439
|
-
if (currentLine.length > 0) {
|
|
440
|
-
lines.push(currentLine);
|
|
441
|
-
}
|
|
442
|
-
// Only render up to webkitLineClamp lines
|
|
443
|
-
const maxLines = styles.webkitLineClamp;
|
|
444
|
-
if (lines.length > maxLines) {
|
|
445
|
-
// Render only the first (maxLines - 1) complete lines
|
|
446
|
-
for (let i = 0; i < maxLines - 1; i++) {
|
|
447
|
-
lines[i].forEach((textBound) => {
|
|
448
|
-
this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
// For the last line, truncate with ellipsis
|
|
452
|
-
const lastLine = lines[maxLines - 1];
|
|
453
|
-
if (lastLine && lastLine.length > 0 && containerBounds) {
|
|
454
|
-
const lastLineText = lastLine.map((tb) => tb.text).join('');
|
|
455
|
-
const firstBound = lastLine[0];
|
|
456
|
-
const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
|
|
457
|
-
const truncatedText = this.truncateTextWithEllipsis(lastLineText, availableWidth, styles.letterSpacing);
|
|
458
|
-
// Build TextBounds once; reused for fill and stroke without re-allocating.
|
|
459
|
-
const truncatedBounds = new text_1.TextBounds(truncatedText, firstBound.bounds);
|
|
460
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
461
|
-
switch (paintOrderLayer) {
|
|
462
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
463
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
464
|
-
this.renderTextWithLetterSpacing(truncatedBounds, styles.letterSpacing, styles.fontSize.number, styles.writingMode);
|
|
465
|
-
break;
|
|
466
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
467
|
-
this.renderTextStrokeWithStyle(truncatedBounds, styles);
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return; // Don't render anything else
|
|
473
|
-
}
|
|
474
|
-
// If lines.length <= maxLines, fall through to normal rendering
|
|
396
|
+
if (clamp) {
|
|
397
|
+
if (this.renderLineClampedText(text, styles, paintOrder, containerBounds))
|
|
398
|
+
return;
|
|
475
399
|
}
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
// Multi-line text truncation (like -webkit-line-clamp) should not be affected
|
|
479
|
-
const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
|
|
400
|
+
// text-overflow: ellipsis (single-line only)
|
|
401
|
+
const ellipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
|
|
480
402
|
containerBounds &&
|
|
481
403
|
styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
|
|
482
404
|
text.textBounds.length > 0;
|
|
483
|
-
|
|
484
|
-
let needsEllipsis = false;
|
|
485
|
-
let truncatedText = '';
|
|
486
|
-
if (shouldApplyEllipsis) {
|
|
487
|
-
// Check if all text bounds are on approximately the same line (single-line scenario)
|
|
488
|
-
// For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
|
|
489
|
-
const firstTop = text.textBounds[0].bounds.top;
|
|
490
|
-
const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
|
|
491
|
-
if (isSingleLine) {
|
|
492
|
-
// Measure the full text content
|
|
493
|
-
// Note: text.textBounds may contain whitespace characters from HTML formatting
|
|
494
|
-
// We need to collapse them like the browser does for white-space: nowrap
|
|
495
|
-
let fullText = text.textBounds.map((tb) => tb.text).join('');
|
|
496
|
-
// Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
|
|
497
|
-
// and trim leading/trailing whitespace
|
|
498
|
-
fullText = fullText.replace(/\s+/g, ' ').trim();
|
|
499
|
-
const fullTextWidth = this.ctx.measureText(fullText).width;
|
|
500
|
-
const availableWidth = containerBounds.width;
|
|
501
|
-
if (fullTextWidth > availableWidth) {
|
|
502
|
-
needsEllipsis = true;
|
|
503
|
-
truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// If ellipsis is needed, render the truncated text once
|
|
508
|
-
if (needsEllipsis) {
|
|
509
|
-
const firstBound = text.textBounds[0];
|
|
510
|
-
// Build TextBounds once; reused across paint layers and every shadow pass
|
|
511
|
-
// to avoid repeated allocation inside forEach callbacks.
|
|
512
|
-
const truncatedBounds = new text_1.TextBounds(truncatedText, firstBound.bounds);
|
|
513
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
514
|
-
switch (paintOrderLayer) {
|
|
515
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */: {
|
|
516
|
-
this.renderTextFillWithShadows(truncatedBounds, styles);
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
520
|
-
this.renderTextStrokeWithStyle(truncatedBounds, styles);
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
});
|
|
405
|
+
if (ellipsis && this.renderEllipsisText(text, styles, paintOrder, containerBounds))
|
|
524
406
|
return;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */: {
|
|
538
|
-
this.renderTextStrokeWithStyle(text, styles);
|
|
539
|
-
break;
|
|
540
|
-
}
|
|
407
|
+
// Normal rendering: fill + stroke + decorations per text bound
|
|
408
|
+
text.textBounds.forEach((tb) => {
|
|
409
|
+
paintOrder.forEach((layer) => {
|
|
410
|
+
if (layer === 0 /* PAINT_ORDER_LAYER.FILL */) {
|
|
411
|
+
this.renderTextFillWithShadows(tb, styles);
|
|
412
|
+
if (styles.textDecorationLine.length)
|
|
413
|
+
this.renderTextDecoration(tb.bounds, styles);
|
|
414
|
+
}
|
|
415
|
+
else if (layer === 1 /* PAINT_ORDER_LAYER.STROKE */) {
|
|
416
|
+
this.renderTextStrokeWithStyle(tb, styles);
|
|
541
417
|
}
|
|
542
418
|
});
|
|
543
419
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isBlendEffect = exports.isClipPathEffect = exports.isOpacityEffect = exports.isClipEffect = exports.isTransformEffect = exports.BlendEffect = exports.ClipPathEffect = exports.OpacityEffect = exports.ClipEffect = exports.TransformEffect = void 0;
|
|
3
|
+
exports.isFilterEffect = exports.isBlendEffect = exports.isClipPathEffect = exports.isOpacityEffect = exports.isClipEffect = exports.isTransformEffect = exports.FilterEffect = exports.BlendEffect = exports.ClipPathEffect = exports.OpacityEffect = exports.ClipEffect = exports.TransformEffect = void 0;
|
|
4
|
+
const mix_blend_mode_1 = require("../css/property-descriptors/mix-blend-mode");
|
|
4
5
|
class TransformEffect {
|
|
5
6
|
constructor(offsetX, offsetY, matrix) {
|
|
6
7
|
this.offsetX = offsetX;
|
|
@@ -41,14 +42,46 @@ class ClipPathEffect {
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
exports.ClipPathEffect = ClipPathEffect;
|
|
45
|
+
/**
|
|
46
|
+
* Maps a CSS mix-blend-mode value to the corresponding Canvas {@link GlobalCompositeOperation}.
|
|
47
|
+
* All CSS blend mode values are supported by the Canvas 2D API except 'normal',
|
|
48
|
+
* which maps to the default 'source-over'.
|
|
49
|
+
*/
|
|
50
|
+
const MIX_BLEND_MODE_TO_COMPOSITE = {
|
|
51
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.NORMAL]: 'source-over',
|
|
52
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.MULTIPLY]: 'multiply',
|
|
53
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.SCREEN]: 'screen',
|
|
54
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.OVERLAY]: 'overlay',
|
|
55
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.DARKEN]: 'darken',
|
|
56
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.LIGHTEN]: 'lighten',
|
|
57
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.COLOR_DODGE]: 'color-dodge',
|
|
58
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.COLOR_BURN]: 'color-burn',
|
|
59
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.HARD_LIGHT]: 'hard-light',
|
|
60
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.SOFT_LIGHT]: 'soft-light',
|
|
61
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.DIFFERENCE]: 'difference',
|
|
62
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.EXCLUSION]: 'exclusion',
|
|
63
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.HUE]: 'hue',
|
|
64
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.SATURATION]: 'saturation',
|
|
65
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.COLOR]: 'color',
|
|
66
|
+
[mix_blend_mode_1.MIX_BLEND_MODE.LUMINOSITY]: 'luminosity'
|
|
67
|
+
};
|
|
44
68
|
class BlendEffect {
|
|
45
|
-
constructor(
|
|
46
|
-
this.
|
|
69
|
+
constructor(mixBlendMode) {
|
|
70
|
+
this.mixBlendMode = mixBlendMode;
|
|
47
71
|
this.type = 4 /* EffectType.BLEND */;
|
|
48
72
|
this.target = 2 /* EffectTarget.BACKGROUND_BORDERS */ | 4 /* EffectTarget.CONTENT */;
|
|
73
|
+
this.compositeOperation = MIX_BLEND_MODE_TO_COMPOSITE[mixBlendMode];
|
|
49
74
|
}
|
|
50
75
|
}
|
|
51
76
|
exports.BlendEffect = BlendEffect;
|
|
77
|
+
class FilterEffect {
|
|
78
|
+
constructor(filterString) {
|
|
79
|
+
this.filterString = filterString;
|
|
80
|
+
this.type = 5 /* EffectType.FILTER */;
|
|
81
|
+
this.target = 2 /* EffectTarget.BACKGROUND_BORDERS */ | 4 /* EffectTarget.CONTENT */;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.FilterEffect = FilterEffect;
|
|
52
85
|
const isTransformEffect = (effect) => effect.type === 0 /* EffectType.TRANSFORM */;
|
|
53
86
|
exports.isTransformEffect = isTransformEffect;
|
|
54
87
|
const isClipEffect = (effect) => effect.type === 1 /* EffectType.CLIP */;
|
|
@@ -59,3 +92,5 @@ const isClipPathEffect = (effect) => effect.type === 3 /* EffectType.CLIP_PATH *
|
|
|
59
92
|
exports.isClipPathEffect = isClipPathEffect;
|
|
60
93
|
const isBlendEffect = (effect) => effect.type === 4 /* EffectType.BLEND */;
|
|
61
94
|
exports.isBlendEffect = isBlendEffect;
|
|
95
|
+
const isFilterEffect = (effect) => effect.type === 5 /* EffectType.FILTER */;
|
|
96
|
+
exports.isFilterEffect = isFilterEffect;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.calculateObjectFitRendering = void 0;
|
|
4
|
-
const
|
|
4
|
+
const length_percentage_1 = require("../css/types/length-percentage");
|
|
5
|
+
const calculateObjectFitRendering = (intrinsicWidth, intrinsicHeight, box, objectFit, objectPosition) => {
|
|
5
6
|
let sx = 0;
|
|
6
7
|
let sy = 0;
|
|
7
8
|
let sw = intrinsicWidth;
|
|
@@ -12,41 +13,44 @@ const calculateObjectFitRendering = (intrinsicWidth, intrinsicHeight, box, objec
|
|
|
12
13
|
let dh = box.height;
|
|
13
14
|
const boxRatio = dw / dh;
|
|
14
15
|
const imgRatio = sw / sh;
|
|
16
|
+
// Resolve object-position offsets (default: 50% 50% = center)
|
|
17
|
+
const posX = objectPosition ? (0, length_percentage_1.getAbsoluteValue)(objectPosition[0], 1) : 0.5;
|
|
18
|
+
const posY = objectPosition ? (0, length_percentage_1.getAbsoluteValue)(objectPosition[1], 1) : 0.5;
|
|
15
19
|
if (objectFit === 2 /* OBJECT_FIT.CONTAIN */) {
|
|
16
20
|
if (imgRatio > boxRatio) {
|
|
17
21
|
dh = dw / imgRatio;
|
|
18
|
-
dy += (box.height - dh)
|
|
22
|
+
dy += (box.height - dh) * posY;
|
|
19
23
|
}
|
|
20
24
|
else {
|
|
21
25
|
dw = dh * imgRatio;
|
|
22
|
-
dx += (box.width - dw)
|
|
26
|
+
dx += (box.width - dw) * posX;
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
else if (objectFit === 4 /* OBJECT_FIT.COVER */) {
|
|
26
30
|
if (imgRatio > boxRatio) {
|
|
27
31
|
sw = sh * boxRatio;
|
|
28
|
-
sx += (intrinsicWidth - sw)
|
|
32
|
+
sx += (intrinsicWidth - sw) * posX;
|
|
29
33
|
}
|
|
30
34
|
else {
|
|
31
35
|
sh = sw / boxRatio;
|
|
32
|
-
sy += (intrinsicHeight - sh)
|
|
36
|
+
sy += (intrinsicHeight - sh) * posY;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
else if (objectFit === 8 /* OBJECT_FIT.NONE */) {
|
|
36
40
|
if (sw > dw) {
|
|
37
|
-
sx += (sw - dw)
|
|
41
|
+
sx += (sw - dw) * posX;
|
|
38
42
|
sw = dw;
|
|
39
43
|
}
|
|
40
44
|
else {
|
|
41
|
-
dx += (dw - sw)
|
|
45
|
+
dx += (dw - sw) * posX;
|
|
42
46
|
dw = sw;
|
|
43
47
|
}
|
|
44
48
|
if (sh > dh) {
|
|
45
|
-
sy += (sh - dh)
|
|
49
|
+
sy += (sh - dh) * posY;
|
|
46
50
|
sh = dh;
|
|
47
51
|
}
|
|
48
52
|
else {
|
|
49
|
-
dy += (dh - sh)
|
|
53
|
+
dy += (dh - sh) * posY;
|
|
50
54
|
dh = sh;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -56,28 +60,28 @@ const calculateObjectFitRendering = (intrinsicWidth, intrinsicHeight, box, objec
|
|
|
56
60
|
if (containW < noneW) {
|
|
57
61
|
if (imgRatio > boxRatio) {
|
|
58
62
|
dh = dw / imgRatio;
|
|
59
|
-
dy += (box.height - dh)
|
|
63
|
+
dy += (box.height - dh) * posY;
|
|
60
64
|
}
|
|
61
65
|
else {
|
|
62
66
|
dw = dh * imgRatio;
|
|
63
|
-
dx += (box.width - dw)
|
|
67
|
+
dx += (box.width - dw) * posX;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
70
|
else {
|
|
67
71
|
if (sw > dw) {
|
|
68
|
-
sx += (sw - dw)
|
|
72
|
+
sx += (sw - dw) * posX;
|
|
69
73
|
sw = dw;
|
|
70
74
|
}
|
|
71
75
|
else {
|
|
72
|
-
dx += (dw - sw)
|
|
76
|
+
dx += (dw - sw) * posX;
|
|
73
77
|
dw = sw;
|
|
74
78
|
}
|
|
75
79
|
if (sh > dh) {
|
|
76
|
-
sy += (sh - dh)
|
|
80
|
+
sy += (sh - dh) * posY;
|
|
77
81
|
sh = dh;
|
|
78
82
|
}
|
|
79
83
|
else {
|
|
80
|
-
dy += (dh - sh)
|
|
84
|
+
dy += (dh - sh) * posY;
|
|
81
85
|
dh = sh;
|
|
82
86
|
}
|
|
83
87
|
}
|
|
@@ -71,6 +71,17 @@ class ElementPaint {
|
|
|
71
71
|
if (this.container.styles.mixBlendMode !== mix_blend_mode_1.MIX_BLEND_MODE.NORMAL) {
|
|
72
72
|
this.effects.push(new effects_1.BlendEffect(this.container.styles.mixBlendMode));
|
|
73
73
|
}
|
|
74
|
+
if (this.container.styles.filter !== null) {
|
|
75
|
+
this.effects.push(new effects_1.FilterEffect(this.container.styles.filter));
|
|
76
|
+
}
|
|
77
|
+
if (this.container.styles.zoom !== 1) {
|
|
78
|
+
const origin = this.container.styles.transformOrigin;
|
|
79
|
+
const offsetX = this.container.bounds.left + (0, length_percentage_1.getAbsoluteValue)(origin[0], this.container.bounds.width);
|
|
80
|
+
const offsetY = this.container.bounds.top + (0, length_percentage_1.getAbsoluteValue)(origin[1], this.container.bounds.height);
|
|
81
|
+
const z = this.container.styles.zoom;
|
|
82
|
+
const zoomMatrix = [z, 0, 0, z, 0, 0];
|
|
83
|
+
this.effects.push(new effects_1.TransformEffect(offsetX, offsetY, zoomMatrix));
|
|
84
|
+
}
|
|
74
85
|
}
|
|
75
86
|
getEffects(target) {
|
|
76
87
|
let inFlow = [2 /* POSITION.ABSOLUTE */, 3 /* POSITION.FIXED */].indexOf(this.container.styles.position) === -1;
|
package/dist/types/config.d.ts
CHANGED
|
@@ -42,13 +42,3 @@ export declare class Html2CanvasConfig {
|
|
|
42
42
|
*/
|
|
43
43
|
clone(options?: Partial<ConfigOptions>): Html2CanvasConfig;
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Set default configuration
|
|
47
|
-
* @deprecated Pass configuration directly to html2canvas instead
|
|
48
|
-
*/
|
|
49
|
-
export declare function setDefaultConfig(config: Html2CanvasConfig): void;
|
|
50
|
-
/**
|
|
51
|
-
* Get default configuration
|
|
52
|
-
* @deprecated Pass configuration directly to html2canvas instead
|
|
53
|
-
*/
|
|
54
|
-
export declare function getDefaultConfig(): Html2CanvasConfig | null;
|
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
import { Context } from './context';
|
|
2
|
-
/**
|
|
3
|
-
* CacheStorage (Deprecated static methods)
|
|
4
|
-
*
|
|
5
|
-
* @deprecated The static methods of CacheStorage are deprecated.
|
|
6
|
-
* Use OriginChecker class instead for instance-based origin checking.
|
|
7
|
-
*
|
|
8
|
-
* For backward compatibility, these methods remain but should not be used in new code.
|
|
9
|
-
*/
|
|
10
|
-
export declare class CacheStorage {
|
|
11
|
-
private static _link?;
|
|
12
|
-
private static _origin;
|
|
13
|
-
/**
|
|
14
|
-
* @deprecated Use OriginChecker.getOrigin() instead
|
|
15
|
-
*/
|
|
16
|
-
static getOrigin(url: string): string;
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Use OriginChecker.isSameOrigin() instead
|
|
19
|
-
*/
|
|
20
|
-
static isSameOrigin(src: string): boolean;
|
|
21
|
-
/**
|
|
22
|
-
* @deprecated No longer needed. OriginChecker is created per Context.
|
|
23
|
-
*/
|
|
24
|
-
static setContext(window: Window): void;
|
|
25
|
-
}
|
|
26
2
|
export interface ResourceOptions {
|
|
27
3
|
imageTimeout: number;
|
|
28
4
|
useCORS: boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared numeric constants used across html2canvas-pro.
|
|
3
|
+
* Centralising magic numbers for maintainability.
|
|
4
|
+
*/
|
|
5
|
+
/** Maximum entries in the CSS parse cache per descriptor (LRU). */
|
|
6
|
+
export declare const PARSE_CACHE_MAX_PER_DESCRIPTOR = 200;
|
|
7
|
+
/** Maximum cached background-image patterns per render session (LRU). */
|
|
8
|
+
export declare const PATTERN_CACHE_MAX = 50;
|
|
9
|
+
/** Maximum size of the image-resource cache (entries). */
|
|
10
|
+
export declare const DEFAULT_IMAGE_CACHE_SIZE = 100;
|
|
11
|
+
/** Maximum allowed image-resource cache size. */
|
|
12
|
+
export declare const MAX_IMAGE_CACHE_SIZE = 10000;
|
|
13
|
+
/** Default image-load timeout in milliseconds. */
|
|
14
|
+
export declare const DEFAULT_IMAGE_TIMEOUT_MS = 15000;
|
|
15
|
+
/** Z-axis mask offset used during box-shadow rendering. */
|
|
16
|
+
export declare const SHADOW_MASK_OFFSET = 10000;
|
|
17
|
+
/** Polling interval (ms) when waiting for the cloned iframe to become ready. */
|
|
18
|
+
export declare const IFRAME_READY_POLL_MS = 50;
|
|
19
|
+
/** Deferred resolution delay (ms) for inline XML images that may fail to parse. */
|
|
20
|
+
export declare const INLINE_IMAGE_RESOLVE_DELAY_MS = 500;
|
|
21
|
+
/** Maximum characters logged for image/resource keys. */
|
|
22
|
+
export declare const RESOURCE_KEY_LOG_LENGTH = 256;
|
|
@@ -6,6 +6,8 @@ import { Html2CanvasConfig } from '../config';
|
|
|
6
6
|
export type ContextOptions = {
|
|
7
7
|
logging: boolean;
|
|
8
8
|
cache?: Cache;
|
|
9
|
+
/** Called when a resource fails to load. */
|
|
10
|
+
onError?: (error: Error) => void;
|
|
9
11
|
} & ResourceOptions;
|
|
10
12
|
export declare class Context {
|
|
11
13
|
windowBounds: Bounds;
|
|
@@ -14,6 +16,7 @@ export declare class Context {
|
|
|
14
16
|
readonly cache: Cache;
|
|
15
17
|
readonly originChecker: OriginChecker;
|
|
16
18
|
readonly config: Html2CanvasConfig;
|
|
19
|
+
readonly onError?: (error: Error) => void;
|
|
17
20
|
private static instanceCount;
|
|
18
21
|
constructor(options: ContextOptions, windowBounds: Bounds, config: Html2CanvasConfig);
|
|
19
22
|
}
|
|
@@ -9,7 +9,7 @@ export interface PerformanceMetric {
|
|
|
9
9
|
startTime: number;
|
|
10
10
|
endTime?: number;
|
|
11
11
|
duration?: number;
|
|
12
|
-
metadata?: Record<string,
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Performance Summary
|
|
@@ -55,7 +55,7 @@ export declare class PerformanceMonitor {
|
|
|
55
55
|
* @param name - Unique name for this metric
|
|
56
56
|
* @param metadata - Optional metadata to attach
|
|
57
57
|
*/
|
|
58
|
-
start(name: string, metadata?: Record<string,
|
|
58
|
+
start(name: string, metadata?: Record<string, unknown>): void;
|
|
59
59
|
/**
|
|
60
60
|
* End measuring a performance metric
|
|
61
61
|
*
|
|
@@ -71,7 +71,7 @@ export declare class PerformanceMonitor {
|
|
|
71
71
|
* @param metadata - Optional metadata
|
|
72
72
|
* @returns The function's return value
|
|
73
73
|
*/
|
|
74
|
-
measure<T>(name: string, fn: () => T, metadata?: Record<string,
|
|
74
|
+
measure<T>(name: string, fn: () => T, metadata?: Record<string, unknown>): T;
|
|
75
75
|
/**
|
|
76
76
|
* Measure an asynchronous function
|
|
77
77
|
*
|
|
@@ -80,7 +80,7 @@ export declare class PerformanceMonitor {
|
|
|
80
80
|
* @param metadata - Optional metadata
|
|
81
81
|
* @returns Promise resolving to the function's return value
|
|
82
82
|
*/
|
|
83
|
-
measureAsync<T>(name: string, fn: () => Promise<T>, metadata?: Record<string,
|
|
83
|
+
measureAsync<T>(name: string, fn: () => Promise<T>, metadata?: Record<string, unknown>): Promise<T>;
|
|
84
84
|
/**
|
|
85
85
|
* Get all completed metrics
|
|
86
86
|
*
|