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.
Files changed (90) hide show
  1. package/dist/html2canvas-pro.esm.js +10226 -10540
  2. package/dist/html2canvas-pro.esm.js.map +1 -1
  3. package/dist/html2canvas-pro.js +10869 -11185
  4. package/dist/html2canvas-pro.js.map +1 -1
  5. package/dist/html2canvas-pro.min.js +8 -8
  6. package/dist/lib/config.js +0 -22
  7. package/dist/lib/core/cache-storage.js +1 -38
  8. package/dist/lib/core/constants.js +25 -0
  9. package/dist/lib/core/context.js +1 -0
  10. package/dist/lib/core/features.js +1 -0
  11. package/dist/lib/core/validator.js +3 -3
  12. package/dist/lib/css/grouped/background-styles.js +36 -0
  13. package/dist/lib/css/grouped/border-styles.js +75 -0
  14. package/dist/lib/css/grouped/font-styles.js +93 -0
  15. package/dist/lib/css/grouped/layout-styles.js +127 -0
  16. package/dist/lib/css/index.js +74 -46
  17. package/dist/lib/css/layout/text.js +7 -6
  18. package/dist/lib/css/property-descriptors/background-blend-mode.js +41 -0
  19. package/dist/lib/css/property-descriptors/border-image-repeat.js +42 -0
  20. package/dist/lib/css/property-descriptors/border-image-slice.js +45 -0
  21. package/dist/lib/css/property-descriptors/border-image-source.js +21 -0
  22. package/dist/lib/css/property-descriptors/border-radius.js +1 -1
  23. package/dist/lib/css/property-descriptors/box-decoration-break.js +18 -0
  24. package/dist/lib/css/property-descriptors/counter-increment.js +17 -12
  25. package/dist/lib/css/property-descriptors/counter-reset.js +4 -12
  26. package/dist/lib/css/property-descriptors/filter.js +76 -0
  27. package/dist/lib/css/property-descriptors/font-variant-ligatures.js +34 -0
  28. package/dist/lib/css/property-descriptors/object-fit.js +1 -1
  29. package/dist/lib/css/property-descriptors/object-position.js +42 -0
  30. package/dist/lib/css/property-descriptors/visibility.js +1 -1
  31. package/dist/lib/css/property-descriptors/zoom.js +18 -0
  32. package/dist/lib/css/syntax/parser.js +0 -1
  33. package/dist/lib/css/types/color.js +5 -1
  34. package/dist/lib/css/types/functions/repeating-linear-gradient.js +9 -0
  35. package/dist/lib/css/types/image.js +12 -2
  36. package/dist/lib/css/types/length-percentage.js +6 -2
  37. package/dist/lib/css/types/safe-eval.js +80 -0
  38. package/dist/lib/dom/document-cloner.js +23 -163
  39. package/dist/lib/dom/slot-cloner.js +176 -0
  40. package/dist/lib/index.js +1 -17
  41. package/dist/lib/render/canvas/background-renderer.js +165 -32
  42. package/dist/lib/render/canvas/border-image-renderer.js +153 -0
  43. package/dist/lib/render/canvas/canvas-renderer.js +34 -189
  44. package/dist/lib/render/canvas/content-renderer.js +202 -0
  45. package/dist/lib/render/canvas/effects-renderer.js +3 -0
  46. package/dist/lib/render/canvas/text/text-decoration-renderer.js +99 -0
  47. package/dist/lib/render/canvas/text-renderer.js +100 -224
  48. package/dist/lib/render/effects.js +38 -3
  49. package/dist/lib/render/object-fit.js +19 -15
  50. package/dist/lib/render/stacking-context.js +11 -0
  51. package/dist/types/config.d.ts +0 -10
  52. package/dist/types/core/cache-storage.d.ts +0 -24
  53. package/dist/types/core/constants.d.ts +22 -0
  54. package/dist/types/core/context.d.ts +3 -0
  55. package/dist/types/core/performance-monitor.d.ts +4 -4
  56. package/dist/types/core/validator.d.ts +6 -8
  57. package/dist/types/css/grouped/background-styles.d.ts +16 -0
  58. package/dist/types/css/grouped/border-styles.d.ts +31 -0
  59. package/dist/types/css/grouped/font-styles.d.ts +35 -0
  60. package/dist/types/css/grouped/layout-styles.d.ts +46 -0
  61. package/dist/types/css/index.d.ts +30 -0
  62. package/dist/types/css/property-descriptors/background-blend-mode.d.ts +23 -0
  63. package/dist/types/css/property-descriptors/border-image-repeat.d.ts +12 -0
  64. package/dist/types/css/property-descriptors/border-image-slice.d.ts +10 -0
  65. package/dist/types/css/property-descriptors/border-image-source.d.ts +4 -0
  66. package/dist/types/css/property-descriptors/box-decoration-break.d.ts +6 -0
  67. package/dist/types/css/property-descriptors/counter-increment.d.ts +3 -0
  68. package/dist/types/css/property-descriptors/filter.d.ts +3 -0
  69. package/dist/types/css/property-descriptors/font-variant-ligatures.d.ts +14 -0
  70. package/dist/types/css/property-descriptors/object-position.d.ts +4 -0
  71. package/dist/types/css/property-descriptors/zoom.d.ts +3 -0
  72. package/dist/types/css/types/functions/repeating-linear-gradient.d.ts +4 -0
  73. package/dist/types/css/types/image.d.ts +4 -2
  74. package/dist/types/css/types/safe-eval.d.ts +8 -0
  75. package/dist/types/dom/document-cloner.d.ts +3 -44
  76. package/dist/types/dom/slot-cloner.d.ts +66 -0
  77. package/dist/types/index.d.ts +3 -7
  78. package/dist/types/options.d.ts +11 -0
  79. package/dist/types/render/canvas/background-renderer.d.ts +23 -0
  80. package/dist/types/render/canvas/border-image-renderer.d.ts +18 -0
  81. package/dist/types/render/canvas/canvas-renderer.d.ts +1 -0
  82. package/dist/types/render/canvas/content-renderer.d.ts +44 -0
  83. package/dist/types/render/canvas/text/text-decoration-renderer.d.ts +18 -0
  84. package/dist/types/render/canvas/text-renderer.d.ts +12 -1
  85. package/dist/types/render/effects.d.ts +12 -2
  86. package/dist/types/render/object-fit.d.ts +2 -1
  87. package/dist/types/render/renderer-interface.d.ts +11 -9
  88. package/package.json +5 -10
  89. package/dist/lib/dom/replaced-elements/pseudo-elements.js +0 -0
  90. 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.ctx.fillStyle = (0, color_utilities_1.asString)(styles.textDecorationColor || styles.color);
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
- const [font] = this.createFontStyle(styles);
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
- // Calculate line height for text layout detection (used by both line-clamp and ellipsis)
413
- const lineHeight = styles.fontSize.number * 1.5;
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 (shouldApplyLineClamp) {
421
- // Group text bounds by lines based on their Y position
422
- const lines = [];
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
- // Check if we need to apply text-overflow: ellipsis
477
- // Issue #203: Only apply ellipsis for single-line text overflow
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
- // Calculate total text width if ellipsis might be needed
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
- // Normal rendering (no ellipsis needed)
527
- text.textBounds.forEach((text) => {
528
- paintOrder.forEach((paintOrderLayer) => {
529
- switch (paintOrderLayer) {
530
- case 0 /* PAINT_ORDER_LAYER.FILL */: {
531
- this.renderTextFillWithShadows(text, styles);
532
- if (styles.textDecorationLine.length) {
533
- this.renderTextDecoration(text.bounds, styles);
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(compositeOperation) {
46
- this.compositeOperation = compositeOperation;
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 calculateObjectFitRendering = (intrinsicWidth, intrinsicHeight, box, objectFit) => {
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) / 2;
22
+ dy += (box.height - dh) * posY;
19
23
  }
20
24
  else {
21
25
  dw = dh * imgRatio;
22
- dx += (box.width - dw) / 2;
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) / 2;
32
+ sx += (intrinsicWidth - sw) * posX;
29
33
  }
30
34
  else {
31
35
  sh = sw / boxRatio;
32
- sy += (intrinsicHeight - sh) / 2;
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) / 2;
41
+ sx += (sw - dw) * posX;
38
42
  sw = dw;
39
43
  }
40
44
  else {
41
- dx += (dw - sw) / 2;
45
+ dx += (dw - sw) * posX;
42
46
  dw = sw;
43
47
  }
44
48
  if (sh > dh) {
45
- sy += (sh - dh) / 2;
49
+ sy += (sh - dh) * posY;
46
50
  sh = dh;
47
51
  }
48
52
  else {
49
- dy += (dh - sh) / 2;
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) / 2;
63
+ dy += (box.height - dh) * posY;
60
64
  }
61
65
  else {
62
66
  dw = dh * imgRatio;
63
- dx += (box.width - dw) / 2;
67
+ dx += (box.width - dw) * posX;
64
68
  }
65
69
  }
66
70
  else {
67
71
  if (sw > dw) {
68
- sx += (sw - dw) / 2;
72
+ sx += (sw - dw) * posX;
69
73
  sw = dw;
70
74
  }
71
75
  else {
72
- dx += (dw - sw) / 2;
76
+ dx += (dw - sw) * posX;
73
77
  dw = sw;
74
78
  }
75
79
  if (sh > dh) {
76
- sy += (sh - dh) / 2;
80
+ sy += (sh - dh) * posY;
77
81
  sh = dh;
78
82
  }
79
83
  else {
80
- dy += (dh - sh) / 2;
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;
@@ -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, any>;
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, any>): void;
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, any>): T;
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, any>): Promise<T>;
83
+ measureAsync<T>(name: string, fn: () => Promise<T>, metadata?: Record<string, unknown>): Promise<T>;
84
84
  /**
85
85
  * Get all completed metrics
86
86
  *