html2canvas-pro 1.6.6 → 2.0.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 (123) hide show
  1. package/README.md +1 -0
  2. package/demo/image-smoothing-demo.html +256 -0
  3. package/demo/refactoring-test.html +602 -0
  4. package/dist/html2canvas-pro.esm.js +2846 -1238
  5. package/dist/html2canvas-pro.esm.js.map +1 -1
  6. package/dist/html2canvas-pro.js +2849 -1237
  7. package/dist/html2canvas-pro.js.map +1 -1
  8. package/dist/html2canvas-pro.min.js +5 -4
  9. package/dist/lib/__tests__/index.js +8 -2
  10. package/dist/lib/__tests__/index.js.map +1 -1
  11. package/dist/lib/config.js +72 -0
  12. package/dist/lib/config.js.map +1 -0
  13. package/dist/lib/core/__tests__/cache-storage.js +6 -3
  14. package/dist/lib/core/__tests__/cache-storage.js.map +1 -1
  15. package/dist/lib/core/__tests__/cache-storage.test.js +158 -0
  16. package/dist/lib/core/__tests__/cache-storage.test.js.map +1 -0
  17. package/dist/lib/core/__tests__/validator.js +296 -0
  18. package/dist/lib/core/__tests__/validator.js.map +1 -0
  19. package/dist/lib/core/cache-storage.js +130 -11
  20. package/dist/lib/core/cache-storage.js.map +1 -1
  21. package/dist/lib/core/context.js +5 -2
  22. package/dist/lib/core/context.js.map +1 -1
  23. package/dist/lib/core/debugger.js +3 -0
  24. package/dist/lib/core/debugger.js.map +1 -1
  25. package/dist/lib/core/origin-checker.js +54 -0
  26. package/dist/lib/core/origin-checker.js.map +1 -0
  27. package/dist/lib/core/performance-monitor.js +208 -0
  28. package/dist/lib/core/performance-monitor.js.map +1 -0
  29. package/dist/lib/core/validator.js +501 -0
  30. package/dist/lib/core/validator.js.map +1 -0
  31. package/dist/lib/css/index.js +2 -0
  32. package/dist/lib/css/index.js.map +1 -1
  33. package/dist/lib/css/property-descriptors/__tests__/background-tests.js +7 -1
  34. package/dist/lib/css/property-descriptors/__tests__/background-tests.js.map +1 -1
  35. package/dist/lib/css/property-descriptors/__tests__/image-rendering-integration.test.js +142 -0
  36. package/dist/lib/css/property-descriptors/__tests__/image-rendering-integration.test.js.map +1 -0
  37. package/dist/lib/css/property-descriptors/__tests__/image-rendering-performance.test.js +167 -0
  38. package/dist/lib/css/property-descriptors/__tests__/image-rendering-performance.test.js.map +1 -0
  39. package/dist/lib/css/property-descriptors/__tests__/image-rendering.test.js +61 -0
  40. package/dist/lib/css/property-descriptors/__tests__/image-rendering.test.js.map +1 -0
  41. package/dist/lib/css/property-descriptors/image-rendering.js +34 -0
  42. package/dist/lib/css/property-descriptors/image-rendering.js.map +1 -0
  43. package/dist/lib/css/types/__tests__/image-tests.js +7 -1
  44. package/dist/lib/css/types/__tests__/image-tests.js.map +1 -1
  45. package/dist/lib/css/types/color-math.js +26 -0
  46. package/dist/lib/css/types/color-math.js.map +1 -0
  47. package/dist/lib/css/types/color-spaces/srgb.js +6 -6
  48. package/dist/lib/css/types/color-spaces/srgb.js.map +1 -1
  49. package/dist/lib/css/types/color-utilities.js +13 -22
  50. package/dist/lib/css/types/color-utilities.js.map +1 -1
  51. package/dist/lib/dom/__tests__/dom-normalizer.test.js +113 -0
  52. package/dist/lib/dom/__tests__/dom-normalizer.test.js.map +1 -0
  53. package/dist/lib/dom/__tests__/element-container.test.js +109 -0
  54. package/dist/lib/dom/__tests__/element-container.test.js.map +1 -0
  55. package/dist/lib/dom/document-cloner.js +152 -11
  56. package/dist/lib/dom/document-cloner.js.map +1 -1
  57. package/dist/lib/dom/dom-normalizer.js +80 -0
  58. package/dist/lib/dom/dom-normalizer.js.map +1 -0
  59. package/dist/lib/dom/element-container.js +32 -15
  60. package/dist/lib/dom/element-container.js.map +1 -1
  61. package/dist/lib/dom/node-parser.js +16 -20
  62. package/dist/lib/dom/node-parser.js.map +1 -1
  63. package/dist/lib/dom/node-type-guards.js +44 -0
  64. package/dist/lib/dom/node-type-guards.js.map +1 -0
  65. package/dist/lib/dom/replaced-elements/iframe-element-container.js +5 -4
  66. package/dist/lib/dom/replaced-elements/iframe-element-container.js.map +1 -1
  67. package/dist/lib/index.js +148 -41
  68. package/dist/lib/index.js.map +1 -1
  69. package/dist/lib/render/canvas/__tests__/background-renderer.test.js +65 -0
  70. package/dist/lib/render/canvas/__tests__/background-renderer.test.js.map +1 -0
  71. package/dist/lib/render/canvas/__tests__/border-renderer.test.js +23 -0
  72. package/dist/lib/render/canvas/__tests__/border-renderer.test.js.map +1 -0
  73. package/dist/lib/render/canvas/__tests__/effects-renderer.test.js +30 -0
  74. package/dist/lib/render/canvas/__tests__/effects-renderer.test.js.map +1 -0
  75. package/dist/lib/render/canvas/__tests__/text-renderer.test.js +63 -0
  76. package/dist/lib/render/canvas/__tests__/text-renderer.test.js.map +1 -0
  77. package/dist/lib/render/canvas/background-renderer.js +222 -0
  78. package/dist/lib/render/canvas/background-renderer.js.map +1 -0
  79. package/dist/lib/render/canvas/border-renderer.js +185 -0
  80. package/dist/lib/render/canvas/border-renderer.js.map +1 -0
  81. package/dist/lib/render/canvas/canvas-renderer.js +61 -689
  82. package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
  83. package/dist/lib/render/canvas/effects-renderer.js +89 -0
  84. package/dist/lib/render/canvas/effects-renderer.js.map +1 -0
  85. package/dist/lib/render/canvas/text-renderer.js +508 -0
  86. package/dist/lib/render/canvas/text-renderer.js.map +1 -0
  87. package/dist/lib/render/renderer-interface.js +3 -0
  88. package/dist/lib/render/renderer-interface.js.map +1 -0
  89. package/dist/types/config.d.ts +54 -0
  90. package/dist/types/core/__tests__/cache-storage.test.d.ts +1 -0
  91. package/dist/types/core/__tests__/validator.d.ts +1 -0
  92. package/dist/types/core/cache-storage.d.ts +42 -1
  93. package/dist/types/core/context.d.ts +5 -1
  94. package/dist/types/core/origin-checker.d.ts +33 -0
  95. package/dist/types/core/performance-monitor.d.ts +131 -0
  96. package/dist/types/core/validator.d.ts +132 -0
  97. package/dist/types/css/index.d.ts +2 -0
  98. package/dist/types/css/property-descriptors/__tests__/image-rendering-integration.test.d.ts +1 -0
  99. package/dist/types/css/property-descriptors/__tests__/image-rendering-performance.test.d.ts +1 -0
  100. package/dist/types/css/property-descriptors/__tests__/image-rendering.test.d.ts +1 -0
  101. package/dist/types/css/property-descriptors/image-rendering.d.ts +8 -0
  102. package/dist/types/css/types/color-math.d.ts +12 -0
  103. package/dist/types/css/types/color-utilities.d.ts +2 -3
  104. package/dist/types/dom/__tests__/dom-normalizer.test.d.ts +1 -0
  105. package/dist/types/dom/__tests__/element-container.test.d.ts +1 -0
  106. package/dist/types/dom/document-cloner.d.ts +46 -0
  107. package/dist/types/dom/dom-normalizer.d.ts +43 -0
  108. package/dist/types/dom/element-container.d.ts +20 -1
  109. package/dist/types/dom/node-parser.d.ts +2 -7
  110. package/dist/types/dom/node-type-guards.d.ts +33 -0
  111. package/dist/types/dom/replaced-elements/iframe-element-container.d.ts +4 -1
  112. package/dist/types/index.d.ts +48 -3
  113. package/dist/types/render/canvas/__tests__/background-renderer.test.d.ts +1 -0
  114. package/dist/types/render/canvas/__tests__/border-renderer.test.d.ts +1 -0
  115. package/dist/types/render/canvas/__tests__/effects-renderer.test.d.ts +1 -0
  116. package/dist/types/render/canvas/__tests__/text-renderer.test.d.ts +1 -0
  117. package/dist/types/render/canvas/background-renderer.d.ts +87 -0
  118. package/dist/types/render/canvas/border-renderer.d.ts +67 -0
  119. package/dist/types/render/canvas/canvas-renderer.d.ts +19 -23
  120. package/dist/types/render/canvas/effects-renderer.d.ts +64 -0
  121. package/dist/types/render/canvas/text-renderer.d.ts +57 -0
  122. package/dist/types/render/renderer-interface.d.ts +26 -0
  123. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 1.6.6 <https://yorickshan.github.io/html2canvas-pro/>
2
+ * html2canvas-pro 2.0.0 <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
  */
@@ -1499,6 +1499,97 @@ const isEndingTokenFor = (token, type) => {
1499
1499
  return type === 2 /* TokenType.LEFT_PARENTHESIS_TOKEN */ && token.type === 3 /* TokenType.RIGHT_PARENTHESIS_TOKEN */;
1500
1500
  };
1501
1501
 
1502
+ /**
1503
+ * Color mathematics utilities
1504
+ * Extracted to break circular dependency between srgb.ts and color-utilities.ts
1505
+ */
1506
+ /**
1507
+ * Clamp a value between min and max
1508
+ */
1509
+ const clamp = (value, min, max) => {
1510
+ return Math.min(Math.max(value, min), max);
1511
+ };
1512
+ /**
1513
+ * Multiply two 3x3 matrices
1514
+ */
1515
+ const multiplyMatrices = (A, B) => {
1516
+ return [
1517
+ A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
1518
+ A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
1519
+ A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
1520
+ ];
1521
+ };
1522
+
1523
+ /**
1524
+ * SRGB related functions
1525
+ */
1526
+ /**
1527
+ * Convert XYZ to linear-light sRGB
1528
+ *
1529
+ * @param xyz
1530
+ */
1531
+ const xyz2rgbLinear = (xyz) => {
1532
+ return multiplyMatrices([
1533
+ 3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796, 1.8759675015077202,
1534
+ 0.04155505740717559, 0.05563007969699366, -0.20397695888897652, 1.0569715142428786
1535
+ ], xyz);
1536
+ };
1537
+ /**
1538
+ * Convert XYZ to linear-light sRGB
1539
+ *
1540
+ * @param xyz
1541
+ */
1542
+ const rgbLinear2xyz = (xyz) => {
1543
+ return multiplyMatrices([
1544
+ 0.41239079926595934, 0.357584339383878, 0.1804807884018343, 0.21263900587151027, 0.715168678767756,
1545
+ 0.07219231536073371, 0.01933081871559182, 0.11919477979462598, 0.9505321522496607
1546
+ ], xyz);
1547
+ };
1548
+ /**
1549
+ * Convert sRGB to RGB
1550
+ *
1551
+ * @param rgb
1552
+ */
1553
+ const srgbLinear2rgb = (rgb) => {
1554
+ return rgb.map((c) => {
1555
+ const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
1556
+ return abs > 0.0031308 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * c;
1557
+ });
1558
+ };
1559
+ /**
1560
+ * Convert RGB to sRGB
1561
+ *
1562
+ * @param rgb
1563
+ */
1564
+ const rgb2rgbLinear = (rgb) => {
1565
+ return rgb.map((c) => {
1566
+ const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
1567
+ return abs <= 0.04045 ? c / 12.92 : sign * ((abs + 0.055) / 1.055) ** 2.4;
1568
+ });
1569
+ };
1570
+ /**
1571
+ * XYZ to SRGB
1572
+ *
1573
+ * @param args
1574
+ */
1575
+ const srgbFromXYZ = (args) => {
1576
+ const [r, g, b] = srgbLinear2rgb(xyz2rgbLinear([args[0], args[1], args[2]]));
1577
+ return [r, g, b, args[3]];
1578
+ };
1579
+ /**
1580
+ * XYZ to SRGB-Linear
1581
+ * @param args
1582
+ */
1583
+ const srgbLinearFromXYZ = (args) => {
1584
+ const [r, g, b] = xyz2rgbLinear([args[0], args[1], args[2]]);
1585
+ return [
1586
+ clamp(Math.round(r * 255), 0, 255),
1587
+ clamp(Math.round(g * 255), 0, 255),
1588
+ clamp(Math.round(b * 255), 0, 255),
1589
+ args[3]
1590
+ ];
1591
+ };
1592
+
1502
1593
  const isLength = (token) => token.type === 17 /* TokenType.NUMBER_TOKEN */ || token.type === 15 /* TokenType.DIMENSION_TOKEN */;
1503
1594
 
1504
1595
  const isLengthPercentage = (token) => token.type === 16 /* TokenType.PERCENTAGE_TOKEN */ || isLength(token);
@@ -1716,16 +1807,6 @@ const getTokenColorValue = (token, i) => {
1716
1807
  return 0;
1717
1808
  };
1718
1809
  const isRelativeTransform = (tokens) => (tokens[0].type === 20 /* TokenType.IDENT_TOKEN */ ? tokens[0].value : 'unknown') === 'from';
1719
- const clamp = (value, min, max) => {
1720
- return Math.min(Math.max(value, min), max);
1721
- };
1722
- const multiplyMatrices = (A, B) => {
1723
- return [
1724
- A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
1725
- A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
1726
- A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
1727
- ];
1728
- };
1729
1810
  const packSrgb = (args) => {
1730
1811
  return pack(clamp(Math.round(args[0] * 255), 0, 255), clamp(Math.round(args[1] * 255), 0, 255), clamp(Math.round(args[2] * 255), 0, 255), clamp(args[3], 0, 1));
1731
1812
  };
@@ -2008,76 +2089,6 @@ const convertXyz50 = (args) => {
2008
2089
  return packXYZ([xyz[0], xyz[1], xyz[2], args[3]]);
2009
2090
  };
2010
2091
 
2011
- /**
2012
- * SRGB related functions
2013
- */
2014
- /**
2015
- * Convert XYZ to linear-light sRGB
2016
- *
2017
- * @param xyz
2018
- */
2019
- const xyz2rgbLinear = (xyz) => {
2020
- return multiplyMatrices([
2021
- 3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796, 1.8759675015077202,
2022
- 0.04155505740717559, 0.05563007969699366, -0.20397695888897652, 1.0569715142428786
2023
- ], xyz);
2024
- };
2025
- /**
2026
- * Convert XYZ to linear-light sRGB
2027
- *
2028
- * @param xyz
2029
- */
2030
- const rgbLinear2xyz = (xyz) => {
2031
- return multiplyMatrices([
2032
- 0.41239079926595934, 0.357584339383878, 0.1804807884018343, 0.21263900587151027, 0.715168678767756,
2033
- 0.07219231536073371, 0.01933081871559182, 0.11919477979462598, 0.9505321522496607
2034
- ], xyz);
2035
- };
2036
- /**
2037
- * Convert sRGB to RGB
2038
- *
2039
- * @param rgb
2040
- */
2041
- const srgbLinear2rgb = (rgb) => {
2042
- return rgb.map((c) => {
2043
- const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
2044
- return abs > 0.0031308 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * c;
2045
- });
2046
- };
2047
- /**
2048
- * Convert RGB to sRGB
2049
- *
2050
- * @param rgb
2051
- */
2052
- const rgb2rgbLinear = (rgb) => {
2053
- return rgb.map((c) => {
2054
- const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
2055
- return abs <= 0.04045 ? c / 12.92 : sign * ((abs + 0.055) / 1.055) ** 2.4;
2056
- });
2057
- };
2058
- /**
2059
- * XYZ to SRGB
2060
- *
2061
- * @param args
2062
- */
2063
- const srgbFromXYZ = (args) => {
2064
- const [r, g, b] = srgbLinear2rgb(xyz2rgbLinear([args[0], args[1], args[2]]));
2065
- return [r, g, b, args[3]];
2066
- };
2067
- /**
2068
- * XYZ to SRGB-Linear
2069
- * @param args
2070
- */
2071
- const srgbLinearFromXYZ = (args) => {
2072
- const [r, g, b] = xyz2rgbLinear([args[0], args[1], args[2]]);
2073
- return [
2074
- clamp(Math.round(r * 255), 0, 255),
2075
- clamp(Math.round(g * 255), 0, 255),
2076
- clamp(Math.round(b * 255), 0, 255),
2077
- args[3]
2078
- ];
2079
- };
2080
-
2081
2092
  /**
2082
2093
  * Display-P3 related functions
2083
2094
  */
@@ -4553,6 +4564,37 @@ const textOverflow = {
4553
4564
  }
4554
4565
  };
4555
4566
 
4567
+ var IMAGE_RENDERING;
4568
+ (function (IMAGE_RENDERING) {
4569
+ IMAGE_RENDERING[IMAGE_RENDERING["AUTO"] = 0] = "AUTO";
4570
+ IMAGE_RENDERING[IMAGE_RENDERING["CRISP_EDGES"] = 1] = "CRISP_EDGES";
4571
+ IMAGE_RENDERING[IMAGE_RENDERING["PIXELATED"] = 2] = "PIXELATED";
4572
+ IMAGE_RENDERING[IMAGE_RENDERING["SMOOTH"] = 3] = "SMOOTH";
4573
+ })(IMAGE_RENDERING || (IMAGE_RENDERING = {}));
4574
+ const imageRendering = {
4575
+ name: 'image-rendering',
4576
+ initialValue: 'auto',
4577
+ prefix: false,
4578
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4579
+ parse: (_context, value) => {
4580
+ switch (value.toLowerCase()) {
4581
+ case 'crisp-edges':
4582
+ case '-webkit-crisp-edges':
4583
+ case '-moz-crisp-edges':
4584
+ return IMAGE_RENDERING.CRISP_EDGES;
4585
+ case 'pixelated':
4586
+ case '-webkit-optimize-contrast':
4587
+ return IMAGE_RENDERING.PIXELATED;
4588
+ case 'smooth':
4589
+ case 'high-quality':
4590
+ return IMAGE_RENDERING.SMOOTH;
4591
+ case 'auto':
4592
+ default:
4593
+ return IMAGE_RENDERING.AUTO;
4594
+ }
4595
+ }
4596
+ };
4597
+
4556
4598
  class CSSParsedDeclaration {
4557
4599
  constructor(context, declaration) {
4558
4600
  this.animationDuration = parse(context, duration, declaration.animationDuration);
@@ -4629,6 +4671,7 @@ class CSSParsedDeclaration {
4629
4671
  this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
4630
4672
  this.zIndex = parse(context, zIndex, declaration.zIndex);
4631
4673
  this.objectFit = parse(context, objectFit, declaration.objectFit);
4674
+ this.imageRendering = parse(context, imageRendering, declaration.imageRendering);
4632
4675
  }
4633
4676
  isVisible() {
4634
4677
  return this.display > 0 && this.opacity > 0 && this.visibility === 0 /* VISIBILITY.VISIBLE */;
@@ -4705,8 +4748,45 @@ const parse = (context, descriptor, style) => {
4705
4748
  }
4706
4749
  };
4707
4750
 
4751
+ /**
4752
+ * DOM Node Type Guards
4753
+ * Extracted to break circular dependencies
4754
+ */
4755
+ /**
4756
+ * Check if node is an Element
4757
+ */
4758
+ const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE;
4759
+ /**
4760
+ * Check if node is a Text node
4761
+ */
4762
+ const isTextNode = (node) => node.nodeType === Node.TEXT_NODE;
4763
+ /**
4764
+ * Check if element is an SVG element
4765
+ */
4766
+ const isSVGElementNode = (element) => typeof element.className === 'object';
4767
+ /**
4768
+ * Check if node is an HTML element
4769
+ */
4770
+ const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);
4771
+ /**
4772
+ * Check if node is an LI element
4773
+ */
4774
+ const isLIElement = (node) => node.tagName === 'LI';
4775
+ /**
4776
+ * Check if node is an OL element
4777
+ */
4778
+ const isOLElement = (node) => node.tagName === 'OL';
4779
+ /**
4780
+ * Check if element is a custom element
4781
+ * Custom elements must have a hyphen and cannot be SVG elements
4782
+ */
4783
+ const isCustomElement = (element) => !isSVGElementNode(element) && element.tagName.indexOf('-') > 0;
4784
+
4708
4785
  const elementDebuggerAttribute = 'data-html2canvas-debug';
4709
4786
  const getElementDebugType = (element) => {
4787
+ if (typeof element.getAttribute !== 'function') {
4788
+ return 0 /* DebuggerType.NONE */;
4789
+ }
4710
4790
  const attribute = element.getAttribute(elementDebuggerAttribute);
4711
4791
  switch (attribute) {
4712
4792
  case 'all':
@@ -4726,8 +4806,83 @@ const isDebugging = (element, type) => {
4726
4806
  return elementType === 1 /* DebuggerType.ALL */ || type === elementType;
4727
4807
  };
4728
4808
 
4809
+ /**
4810
+ * DOM Normalizer
4811
+ * Handles DOM side effects that need to happen before rendering
4812
+ * Extracted from ElementContainer to follow SRP
4813
+ */
4814
+ /**
4815
+ * Normalize element styles for accurate rendering
4816
+ * This includes disabling animations and resetting transforms
4817
+ */
4818
+ class DOMNormalizer {
4819
+ /**
4820
+ * Normalize a single element and return original styles
4821
+ *
4822
+ * @param element - Element to normalize
4823
+ * @param styles - Parsed CSS styles
4824
+ * @returns Original styles map for restoration
4825
+ */
4826
+ static normalizeElement(element, styles) {
4827
+ const originalStyles = {};
4828
+ if (!isHTMLElementNode(element)) {
4829
+ return originalStyles;
4830
+ }
4831
+ // Disable animations to capture static state
4832
+ if (styles.animationDuration.some((duration) => duration > 0)) {
4833
+ originalStyles.animationDuration = element.style.animationDuration;
4834
+ element.style.animationDuration = '0s';
4835
+ }
4836
+ // Reset transform for accurate bounds calculation
4837
+ // getBoundingClientRect takes transforms into account
4838
+ if (styles.transform !== null) {
4839
+ originalStyles.transform = element.style.transform;
4840
+ element.style.transform = 'none';
4841
+ }
4842
+ // Reset rotate property similarly to transform
4843
+ if (styles.rotate !== null) {
4844
+ originalStyles.rotate = element.style.rotate;
4845
+ element.style.rotate = 'none';
4846
+ }
4847
+ return originalStyles;
4848
+ }
4849
+ /**
4850
+ * Normalize element and its descendants recursively
4851
+ *
4852
+ * @param element - Element to normalize
4853
+ * @param styles - Parsed CSS styles
4854
+ * @returns Original styles map for restoration
4855
+ */
4856
+ static normalizeTree(element, styles) {
4857
+ return this.normalizeElement(element, styles);
4858
+ // Could add recursive normalization here if needed
4859
+ // For now, only normalize the element itself
4860
+ }
4861
+ /**
4862
+ * Restore element styles after rendering
4863
+ *
4864
+ * @param element - Element to restore
4865
+ * @param originalStyles - Original styles to restore
4866
+ */
4867
+ static restoreElement(element, originalStyles) {
4868
+ if (!isHTMLElementNode(element)) {
4869
+ return;
4870
+ }
4871
+ // Restore each property that was saved
4872
+ if (originalStyles.animationDuration !== undefined) {
4873
+ element.style.animationDuration = originalStyles.animationDuration;
4874
+ }
4875
+ if (originalStyles.transform !== undefined) {
4876
+ element.style.transform = originalStyles.transform;
4877
+ }
4878
+ if (originalStyles.rotate !== undefined) {
4879
+ element.style.rotate = originalStyles.rotate;
4880
+ }
4881
+ }
4882
+ }
4883
+
4729
4884
  class ElementContainer {
4730
- constructor(context, element) {
4885
+ constructor(context, element, options = {}) {
4731
4886
  this.context = context;
4732
4887
  this.textNodes = [];
4733
4888
  this.elements = [];
@@ -4735,25 +4890,41 @@ class ElementContainer {
4735
4890
  if (isDebugging(element, 3 /* DebuggerType.PARSE */)) {
4736
4891
  debugger;
4737
4892
  }
4738
- this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
4739
- if (isHTMLElementNode(element)) {
4740
- if (this.styles.animationDuration.some((duration) => duration > 0)) {
4741
- element.style.animationDuration = '0s';
4742
- }
4743
- if (this.styles.transform !== null) {
4744
- // getBoundingClientRect takes transforms into account
4745
- element.style.transform = 'none';
4746
- }
4747
- if (this.styles.rotate !== null) {
4748
- // Handle rotate property similarly to transform
4749
- element.style.rotate = 'none';
4750
- }
4893
+ this.styles = new CSSParsedDeclaration(context, context.config.window.getComputedStyle(element, null));
4894
+ // Side effects moved to DOMNormalizer (can be disabled via options)
4895
+ const shouldNormalize = options.normalizeDom !== false; // Default: true
4896
+ if (shouldNormalize && isHTMLElementNode(element)) {
4897
+ this.originalStyles = DOMNormalizer.normalizeElement(element, this.styles);
4898
+ this.originalElement = element; // Save reference for restoration
4751
4899
  }
4752
4900
  this.bounds = parseBounds(this.context, element);
4753
4901
  if (isDebugging(element, 4 /* DebuggerType.RENDER */)) {
4754
4902
  this.flags |= 16 /* FLAGS.DEBUG_RENDER */;
4755
4903
  }
4756
4904
  }
4905
+ /**
4906
+ * Restore original element styles (if normalized)
4907
+ * Call this after rendering is complete to clean up DOM state
4908
+ */
4909
+ restore() {
4910
+ if (this.originalStyles && this.originalElement) {
4911
+ DOMNormalizer.restoreElement(this.originalElement, this.originalStyles);
4912
+ // Clear references to prevent memory leaks
4913
+ this.originalStyles = undefined;
4914
+ this.originalElement = undefined;
4915
+ }
4916
+ }
4917
+ /**
4918
+ * Recursively restore all elements in the tree
4919
+ * Call this on the root container after rendering is complete
4920
+ */
4921
+ restoreTree() {
4922
+ this.restore();
4923
+ // Recursively restore all child elements
4924
+ for (const child of this.elements) {
4925
+ child.restoreTree();
4926
+ }
4927
+ }
4757
4928
  }
4758
4929
 
4759
4930
  /*
@@ -5607,17 +5778,19 @@ class TextareaElementContainer extends ElementContainer {
5607
5778
  }
5608
5779
 
5609
5780
  class IFrameElementContainer extends ElementContainer {
5610
- constructor(context, iframe) {
5781
+ constructor(context, iframe, parseTreeFn) {
5611
5782
  super(context, iframe);
5612
5783
  this.src = iframe.src;
5613
5784
  this.width = parseInt(iframe.width, 10) || 0;
5614
5785
  this.height = parseInt(iframe.height, 10) || 0;
5615
5786
  this.backgroundColor = this.styles.backgroundColor;
5787
+ this.parseTreeFn = parseTreeFn;
5616
5788
  try {
5617
5789
  if (iframe.contentWindow &&
5618
5790
  iframe.contentWindow.document &&
5619
- iframe.contentWindow.document.documentElement) {
5620
- this.tree = parseTree(context, iframe.contentWindow.document.documentElement);
5791
+ iframe.contentWindow.document.documentElement &&
5792
+ this.parseTreeFn) {
5793
+ this.tree = this.parseTreeFn(context, iframe.contentWindow.document.documentElement);
5621
5794
  // http://www.w3.org/TR/css3-background/#special-backgrounds
5622
5795
  const documentBackgroundColor = iframe.contentWindow.document.documentElement
5623
5796
  ? parseColor(context, getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor)
@@ -5701,7 +5874,7 @@ const createContainer = (context, element) => {
5701
5874
  return new TextareaElementContainer(context, element);
5702
5875
  }
5703
5876
  if (isIFrameElement(element)) {
5704
- return new IFrameElementContainer(context, element);
5877
+ return new IFrameElementContainer(context, element, parseTree);
5705
5878
  }
5706
5879
  return new ElementContainer(context, element);
5707
5880
  };
@@ -5730,12 +5903,7 @@ const createsStackingContext = (styles) => {
5730
5903
  contains(styles.display, 536870912 /* DISPLAY.INLINE_GRID */) ||
5731
5904
  contains(styles.display, 134217728 /* DISPLAY.INLINE_TABLE */));
5732
5905
  };
5733
- const isTextNode = (node) => node.nodeType === Node.TEXT_NODE;
5734
- const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE;
5735
- const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);
5736
- const isSVGElementNode = (element) => typeof element.className === 'object';
5737
- const isLIElement = (node) => node.tagName === 'LI';
5738
- const isOLElement = (node) => node.tagName === 'OL';
5906
+ // Type guards moved to node-type-guards.ts and re-exported above
5739
5907
  const isInputElement = (node) => node.tagName === 'INPUT';
5740
5908
  const isHTMLElement = (node) => node.tagName === 'HTML';
5741
5909
  const isSVGElement = (node) => node.tagName === 'svg';
@@ -5750,7 +5918,7 @@ const isTextareaElement = (node) => node.tagName === 'TEXTAREA';
5750
5918
  const isSelectElement = (node) => node.tagName === 'SELECT';
5751
5919
  const isSlotElement = (node) => node.tagName === 'SLOT';
5752
5920
  // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
5753
- const isCustomElement = (node) => node.tagName.indexOf('-') > 0;
5921
+ // isCustomElement moved to node-type-guards.ts and re-exported above
5754
5922
 
5755
5923
  class CounterState {
5756
5924
  constructor() {
@@ -6158,7 +6326,7 @@ class DocumentCloner {
6158
6326
  toIFrame(ownerDocument, windowSize) {
6159
6327
  const iframe = createIFrameContainer(ownerDocument, windowSize, this.options.iframeContainer);
6160
6328
  if (!iframe.contentWindow) {
6161
- return Promise.reject(`Unable to find iframe window`);
6329
+ throw new Error('Unable to find iframe window');
6162
6330
  }
6163
6331
  const scrollX = ownerDocument.defaultView.pageXOffset;
6164
6332
  const scrollY = ownerDocument.defaultView.pageYOffset;
@@ -6180,7 +6348,7 @@ class DocumentCloner {
6180
6348
  const onclone = this.options.onclone;
6181
6349
  const referenceElement = this.clonedReferenceElement;
6182
6350
  if (typeof referenceElement === 'undefined') {
6183
- return Promise.reject(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
6351
+ throw new Error(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
6184
6352
  }
6185
6353
  if (documentClone.fonts && documentClone.fonts.ready) {
6186
6354
  await documentClone.fonts.ready;
@@ -6256,7 +6424,7 @@ class DocumentCloner {
6256
6424
  clone.loading = 'eager';
6257
6425
  }
6258
6426
  }
6259
- if (isCustomElement(clone)) {
6427
+ if (isCustomElement(clone) && !isSVGElementNode(clone)) {
6260
6428
  return this.createCustomElementClone(clone);
6261
6429
  }
6262
6430
  return clone;
@@ -6382,19 +6550,160 @@ class DocumentCloner {
6382
6550
  }
6383
6551
  }
6384
6552
  }
6385
- cloneChildNodes(node, clone, copyStyles) {
6386
- // Clone shadow DOM content if it exists
6387
- if (node.shadowRoot && clone.shadowRoot) {
6388
- for (let child = node.shadowRoot.firstChild; child; child = child.nextSibling) {
6389
- // Clone all shadow DOM children including <slot> elements
6390
- // The browser will automatically handle slot assignment
6391
- clone.shadowRoot.appendChild(this.cloneNode(child, copyStyles));
6392
- }
6393
- }
6394
- // Clone light DOM content (always, even if shadow DOM exists)
6395
- for (let child = node.firstChild; child; child = child.nextSibling) {
6396
- this.appendChildNode(clone, child, copyStyles);
6397
- }
6553
+ /**
6554
+ * Check if a child node should be cloned based on filtering rules
6555
+ * Filters out: scripts, ignored elements, and optionally styles
6556
+ */
6557
+ shouldCloneChild(child) {
6558
+ return (!isElementNode(child) ||
6559
+ (!isScriptElement(child) &&
6560
+ !child.hasAttribute(IGNORE_ATTRIBUTE) &&
6561
+ (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))));
6562
+ }
6563
+ /**
6564
+ * Check if a style element should be cloned based on copyStyles option
6565
+ */
6566
+ shouldCloneStyleElement(child) {
6567
+ return !this.options.copyStyles || !isElementNode(child) || !isStyleElement(child);
6568
+ }
6569
+ /**
6570
+ * Safely append a cloned child to a target, applying all filtering rules
6571
+ */
6572
+ safeAppendClonedChild(target, child, copyStyles) {
6573
+ if (this.shouldCloneChild(child) && this.shouldCloneStyleElement(child)) {
6574
+ target.appendChild(this.cloneNode(child, copyStyles));
6575
+ }
6576
+ }
6577
+ /**
6578
+ * Clone assigned nodes from a slot element to the target
6579
+ */
6580
+ cloneAssignedNodes(assignedNodes, target, copyStyles) {
6581
+ assignedNodes.forEach((node) => {
6582
+ this.safeAppendClonedChild(target, node, copyStyles);
6583
+ });
6584
+ }
6585
+ /**
6586
+ * Clone fallback content from a slot element when no nodes are assigned
6587
+ */
6588
+ cloneSlotFallbackContent(slot, target, copyStyles) {
6589
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6590
+ this.safeAppendClonedChild(target, child, copyStyles);
6591
+ }
6592
+ }
6593
+ /**
6594
+ * Handle cloning of a slot element, including assigned nodes or fallback content
6595
+ */
6596
+ cloneSlotElement(slot, targetShadowRoot, copyStyles) {
6597
+ if (!isSlotElement(slot)) {
6598
+ return;
6599
+ }
6600
+ const slotElement = slot;
6601
+ // Defensive check: ensure assignedNodes method exists
6602
+ if (typeof slotElement.assignedNodes !== 'function') {
6603
+ this.context.logger.warn('HTMLSlotElement.assignedNodes is not available', slot);
6604
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6605
+ return;
6606
+ }
6607
+ const assignedNodes = slotElement.assignedNodes();
6608
+ // Defensive check: ensure assignedNodes returns an array
6609
+ if (!assignedNodes || !Array.isArray(assignedNodes)) {
6610
+ this.context.logger.warn('assignedNodes() did not return a valid array', slot);
6611
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6612
+ return;
6613
+ }
6614
+ if (assignedNodes.length > 0) {
6615
+ // Clone assigned nodes
6616
+ this.cloneAssignedNodes(assignedNodes, targetShadowRoot, copyStyles);
6617
+ }
6618
+ else {
6619
+ // Clone fallback content
6620
+ this.cloneSlotFallbackContent(slot, targetShadowRoot, copyStyles);
6621
+ }
6622
+ }
6623
+ /**
6624
+ * Clone shadow DOM children to the target shadow root
6625
+ */
6626
+ cloneShadowDOMChildren(shadowRoot, targetShadowRoot, copyStyles) {
6627
+ for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
6628
+ if (isElementNode(child) && isSlotElement(child)) {
6629
+ // Handle slot elements specially
6630
+ this.cloneSlotElement(child, targetShadowRoot, copyStyles);
6631
+ }
6632
+ else {
6633
+ // Clone regular elements
6634
+ this.safeAppendClonedChild(targetShadowRoot, child, copyStyles);
6635
+ }
6636
+ }
6637
+ }
6638
+ /**
6639
+ * Clone light DOM children to the target element
6640
+ */
6641
+ cloneLightDOMChildren(node, clone, copyStyles) {
6642
+ for (let child = node.firstChild; child; child = child.nextSibling) {
6643
+ this.appendChildNode(clone, child, copyStyles);
6644
+ }
6645
+ }
6646
+ /**
6647
+ * Clone slot element as light DOM when shadow root creation failed
6648
+ */
6649
+ cloneSlotElementAsLightDOM(slot, clone, copyStyles) {
6650
+ if (!isSlotElement(slot)) {
6651
+ return;
6652
+ }
6653
+ const slotElement = slot;
6654
+ if (typeof slotElement.assignedNodes !== 'function') {
6655
+ // Fallback: clone slot's children
6656
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6657
+ this.appendChildNode(clone, child, copyStyles);
6658
+ }
6659
+ return;
6660
+ }
6661
+ const assignedNodes = slotElement.assignedNodes();
6662
+ if (assignedNodes && Array.isArray(assignedNodes) && assignedNodes.length > 0) {
6663
+ // Clone assigned nodes as light DOM
6664
+ assignedNodes.forEach((node) => this.appendChildNode(clone, node, copyStyles));
6665
+ }
6666
+ else {
6667
+ // Clone fallback content as light DOM
6668
+ for (let child = slot.firstChild; child; child = child.nextSibling) {
6669
+ this.appendChildNode(clone, child, copyStyles);
6670
+ }
6671
+ }
6672
+ }
6673
+ /**
6674
+ * Clone shadow DOM content as light DOM when shadow root creation failed
6675
+ * This is a fallback mechanism to ensure content is not lost
6676
+ */
6677
+ cloneShadowDOMAsLightDOM(shadowRoot, clone, copyStyles) {
6678
+ for (let child = shadowRoot.firstChild; child; child = child.nextSibling) {
6679
+ if (isElementNode(child) && isSlotElement(child)) {
6680
+ this.cloneSlotElementAsLightDOM(child, clone, copyStyles);
6681
+ }
6682
+ else {
6683
+ this.appendChildNode(clone, child, copyStyles);
6684
+ }
6685
+ }
6686
+ }
6687
+ /**
6688
+ * Clone child nodes from source element to clone element
6689
+ * Handles shadow DOM, slots, and light DOM appropriately
6690
+ */
6691
+ cloneChildNodes(node, clone, copyStyles) {
6692
+ if (node.shadowRoot && clone.shadowRoot) {
6693
+ // Both original and clone have shadow roots - clone shadow DOM content
6694
+ this.cloneShadowDOMChildren(node.shadowRoot, clone.shadowRoot, copyStyles);
6695
+ // Also clone light DOM (slot content sources)
6696
+ this.cloneLightDOMChildren(node, clone, copyStyles);
6697
+ }
6698
+ else if (node.shadowRoot && !clone.shadowRoot) {
6699
+ // Original has shadow root but clone doesn't (creation failed)
6700
+ // Fallback: clone shadow DOM content as light DOM to preserve content
6701
+ this.cloneShadowDOMAsLightDOM(node.shadowRoot, clone, copyStyles);
6702
+ }
6703
+ else {
6704
+ // No shadow DOM - just clone light DOM children
6705
+ this.cloneLightDOMChildren(node, clone, copyStyles);
6706
+ }
6398
6707
  }
6399
6708
  cloneNode(node, copyStyles) {
6400
6709
  if (isTextNode(node)) {
@@ -6671,144 +6980,6 @@ const addBase = (targetELement, baseUri) => {
6671
6980
  headEle?.insertBefore(baseNode, headEle?.firstChild ?? null);
6672
6981
  };
6673
6982
 
6674
- class CacheStorage {
6675
- static getOrigin(url) {
6676
- const link = CacheStorage._link;
6677
- if (!link) {
6678
- return 'about:blank';
6679
- }
6680
- link.href = url;
6681
- link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
6682
- return link.protocol + link.hostname + link.port;
6683
- }
6684
- static isSameOrigin(src) {
6685
- return CacheStorage.getOrigin(src) === CacheStorage._origin;
6686
- }
6687
- static setContext(window) {
6688
- CacheStorage._link = window.document.createElement('a');
6689
- CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
6690
- }
6691
- }
6692
- CacheStorage._origin = 'about:blank';
6693
- class Cache {
6694
- constructor(context, _options) {
6695
- this.context = context;
6696
- this._options = _options;
6697
- this._cache = {};
6698
- }
6699
- addImage(src) {
6700
- const result = Promise.resolve();
6701
- if (this.has(src)) {
6702
- return result;
6703
- }
6704
- if (isBlobImage(src) || isRenderable(src)) {
6705
- (this._cache[src] = this.loadImage(src)).catch(() => {
6706
- // prevent unhandled rejection
6707
- });
6708
- return result;
6709
- }
6710
- return result;
6711
- }
6712
- match(src) {
6713
- return this._cache[src];
6714
- }
6715
- async loadImage(key) {
6716
- const isSameOrigin = typeof this._options.customIsSameOrigin === 'function'
6717
- ? await this._options.customIsSameOrigin(key, CacheStorage.isSameOrigin)
6718
- : CacheStorage.isSameOrigin(key);
6719
- const useCORS = !isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
6720
- const useProxy = !isInlineImage(key) &&
6721
- !isSameOrigin &&
6722
- !isBlobImage(key) &&
6723
- typeof this._options.proxy === 'string' &&
6724
- FEATURES.SUPPORT_CORS_XHR &&
6725
- !useCORS;
6726
- if (!isSameOrigin &&
6727
- this._options.allowTaint === false &&
6728
- !isInlineImage(key) &&
6729
- !isBlobImage(key) &&
6730
- !useProxy &&
6731
- !useCORS) {
6732
- return;
6733
- }
6734
- let src = key;
6735
- if (useProxy) {
6736
- src = await this.proxy(src);
6737
- }
6738
- this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
6739
- return await new Promise((resolve, reject) => {
6740
- const img = new Image();
6741
- img.onload = () => resolve(img);
6742
- img.onerror = reject;
6743
- //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
6744
- if (isInlineBase64Image(src) || useCORS) {
6745
- img.crossOrigin = 'anonymous';
6746
- }
6747
- img.src = src;
6748
- if (img.complete === true) {
6749
- // Inline XML images may fail to parse, throwing an Error later on
6750
- setTimeout(() => resolve(img), 500);
6751
- }
6752
- if (this._options.imageTimeout > 0) {
6753
- setTimeout(() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout);
6754
- }
6755
- });
6756
- }
6757
- has(key) {
6758
- return typeof this._cache[key] !== 'undefined';
6759
- }
6760
- keys() {
6761
- return Promise.resolve(Object.keys(this._cache));
6762
- }
6763
- proxy(src) {
6764
- const proxy = this._options.proxy;
6765
- if (!proxy) {
6766
- throw new Error('No proxy defined');
6767
- }
6768
- const key = src.substring(0, 256);
6769
- return new Promise((resolve, reject) => {
6770
- const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
6771
- const xhr = new XMLHttpRequest();
6772
- xhr.onload = () => {
6773
- if (xhr.status === 200) {
6774
- if (responseType === 'text') {
6775
- resolve(xhr.response);
6776
- }
6777
- else {
6778
- const reader = new FileReader();
6779
- reader.addEventListener('load', () => resolve(reader.result), false);
6780
- reader.addEventListener('error', (e) => reject(e), false);
6781
- reader.readAsDataURL(xhr.response);
6782
- }
6783
- }
6784
- else {
6785
- reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
6786
- }
6787
- };
6788
- xhr.onerror = reject;
6789
- const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
6790
- xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
6791
- if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
6792
- xhr.responseType = responseType;
6793
- }
6794
- if (this._options.imageTimeout) {
6795
- const timeout = this._options.imageTimeout;
6796
- xhr.timeout = timeout;
6797
- xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
6798
- }
6799
- xhr.send();
6800
- });
6801
- }
6802
- }
6803
- const INLINE_SVG = /^data:image\/svg\+xml/i;
6804
- const INLINE_BASE64 = /^data:image\/.*;base64,/i;
6805
- const INLINE_IMG = /^data:image\/.*/i;
6806
- const isRenderable = (src) => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
6807
- const isInlineImage = (src) => INLINE_IMG.test(src);
6808
- const isInlineBase64Image = (src) => INLINE_BASE64.test(src);
6809
- const isBlobImage = (src) => src.substr(0, 4) === 'blob';
6810
- const isSVG = (src) => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
6811
-
6812
6983
  class Vector {
6813
6984
  constructor(x, y) {
6814
6985
  this.type = 0 /* PathType.VECTOR */;
@@ -7247,130 +7418,33 @@ const parseStackingContexts = (container) => {
7247
7418
  return root;
7248
7419
  };
7249
7420
 
7250
- const parsePathForBorder = (curves, borderSide) => {
7251
- switch (borderSide) {
7252
- case 0:
7253
- return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox);
7254
- case 1:
7255
- return createPathFromCurves(curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox);
7256
- case 2:
7257
- return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox);
7258
- case 3:
7259
- default:
7260
- return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox, curves.topLeftBorderBox, curves.topLeftPaddingBox);
7261
- }
7421
+ const paddingBox = (element) => {
7422
+ const bounds = element.bounds;
7423
+ const styles = element.styles;
7424
+ return bounds.add(styles.borderLeftWidth, styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth), -(styles.borderTopWidth + styles.borderBottomWidth));
7262
7425
  };
7263
- const parsePathForBorderDoubleOuter = (curves, borderSide) => {
7264
- switch (borderSide) {
7265
- case 0:
7266
- return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox);
7267
- case 1:
7268
- return createPathFromCurves(curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox);
7269
- case 2:
7270
- return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox);
7271
- case 3:
7272
- default:
7273
- return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox, curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox);
7274
- }
7426
+ const contentBox = (element) => {
7427
+ const styles = element.styles;
7428
+ const bounds = element.bounds;
7429
+ const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
7430
+ const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
7431
+ const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
7432
+ const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
7433
+ return bounds.add(paddingLeft + styles.borderLeftWidth, paddingTop + styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight), -(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom));
7275
7434
  };
7276
- const parsePathForBorderDoubleInner = (curves, borderSide) => {
7277
- switch (borderSide) {
7278
- case 0:
7279
- return createPathFromCurves(curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox);
7280
- case 1:
7281
- return createPathFromCurves(curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox);
7282
- case 2:
7283
- return createPathFromCurves(curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox);
7284
- case 3:
7285
- default:
7286
- return createPathFromCurves(curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox, curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox);
7435
+
7436
+ const calculateBackgroundPositioningArea = (backgroundOrigin, element) => {
7437
+ if (backgroundOrigin === 0 /* BACKGROUND_ORIGIN.BORDER_BOX */) {
7438
+ return element.bounds;
7287
7439
  }
7288
- };
7289
- const parsePathForBorderStroke = (curves, borderSide) => {
7290
- switch (borderSide) {
7291
- case 0:
7292
- return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
7293
- case 1:
7294
- return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
7295
- case 2:
7296
- return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
7297
- case 3:
7298
- default:
7299
- return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
7440
+ if (backgroundOrigin === 2 /* BACKGROUND_ORIGIN.CONTENT_BOX */) {
7441
+ return contentBox(element);
7300
7442
  }
7443
+ return paddingBox(element);
7301
7444
  };
7302
- const createStrokePathFromCurves = (outer1, outer2) => {
7303
- const path = [];
7304
- if (isBezierCurve(outer1)) {
7305
- path.push(outer1.subdivide(0.5, false));
7306
- }
7307
- else {
7308
- path.push(outer1);
7309
- }
7310
- if (isBezierCurve(outer2)) {
7311
- path.push(outer2.subdivide(0.5, true));
7312
- }
7313
- else {
7314
- path.push(outer2);
7315
- }
7316
- return path;
7317
- };
7318
- const createPathFromCurves = (outer1, inner1, outer2, inner2) => {
7319
- const path = [];
7320
- if (isBezierCurve(outer1)) {
7321
- path.push(outer1.subdivide(0.5, false));
7322
- }
7323
- else {
7324
- path.push(outer1);
7325
- }
7326
- if (isBezierCurve(outer2)) {
7327
- path.push(outer2.subdivide(0.5, true));
7328
- }
7329
- else {
7330
- path.push(outer2);
7331
- }
7332
- if (isBezierCurve(inner2)) {
7333
- path.push(inner2.subdivide(0.5, true).reverse());
7334
- }
7335
- else {
7336
- path.push(inner2);
7337
- }
7338
- if (isBezierCurve(inner1)) {
7339
- path.push(inner1.subdivide(0.5, false).reverse());
7340
- }
7341
- else {
7342
- path.push(inner1);
7343
- }
7344
- return path;
7345
- };
7346
-
7347
- const paddingBox = (element) => {
7348
- const bounds = element.bounds;
7349
- const styles = element.styles;
7350
- return bounds.add(styles.borderLeftWidth, styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth), -(styles.borderTopWidth + styles.borderBottomWidth));
7351
- };
7352
- const contentBox = (element) => {
7353
- const styles = element.styles;
7354
- const bounds = element.bounds;
7355
- const paddingLeft = getAbsoluteValue(styles.paddingLeft, bounds.width);
7356
- const paddingRight = getAbsoluteValue(styles.paddingRight, bounds.width);
7357
- const paddingTop = getAbsoluteValue(styles.paddingTop, bounds.width);
7358
- const paddingBottom = getAbsoluteValue(styles.paddingBottom, bounds.width);
7359
- return bounds.add(paddingLeft + styles.borderLeftWidth, paddingTop + styles.borderTopWidth, -(styles.borderRightWidth + styles.borderLeftWidth + paddingLeft + paddingRight), -(styles.borderTopWidth + styles.borderBottomWidth + paddingTop + paddingBottom));
7360
- };
7361
-
7362
- const calculateBackgroundPositioningArea = (backgroundOrigin, element) => {
7363
- if (backgroundOrigin === 0 /* BACKGROUND_ORIGIN.BORDER_BOX */) {
7364
- return element.bounds;
7365
- }
7366
- if (backgroundOrigin === 2 /* BACKGROUND_ORIGIN.CONTENT_BOX */) {
7367
- return contentBox(element);
7368
- }
7369
- return paddingBox(element);
7370
- };
7371
- const calculateBackgroundPaintingArea = (backgroundClip, element) => {
7372
- if (backgroundClip === 0 /* BACKGROUND_CLIP.BORDER_BOX */) {
7373
- return element.bounds;
7445
+ const calculateBackgroundPaintingArea = (backgroundClip, element) => {
7446
+ if (backgroundClip === 0 /* BACKGROUND_CLIP.BORDER_BOX */) {
7447
+ return element.bounds;
7374
7448
  }
7375
7449
  if (backgroundClip === 2 /* BACKGROUND_CLIP.CONTENT_BOX */) {
7376
7450
  return contentBox(element);
@@ -7588,135 +7662,717 @@ class Renderer {
7588
7662
  }
7589
7663
  }
7590
7664
 
7591
- const MASK_OFFSET = 10000;
7592
- class CanvasRenderer extends Renderer {
7593
- constructor(context, options) {
7594
- super(context, options);
7595
- this._activeEffects = [];
7596
- this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
7597
- this.ctx = this.canvas.getContext('2d');
7598
- if (!options.canvas) {
7599
- this.canvas.width = Math.floor(options.width * options.scale);
7600
- this.canvas.height = Math.floor(options.height * options.scale);
7601
- this.canvas.style.width = `${options.width}px`;
7602
- this.canvas.style.height = `${options.height}px`;
7603
- }
7604
- this.fontMetrics = new FontMetrics(document);
7605
- this.ctx.scale(this.options.scale, this.options.scale);
7606
- this.ctx.translate(-options.x, -options.y);
7607
- this.ctx.textBaseline = 'bottom';
7608
- this._activeEffects = [];
7609
- this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
7665
+ /**
7666
+ * Background Renderer
7667
+ *
7668
+ * Handles rendering of element backgrounds including:
7669
+ * - Background colors
7670
+ * - Background images (URL)
7671
+ * - Linear gradients
7672
+ * - Radial gradients
7673
+ * - Background patterns and repeats
7674
+ */
7675
+ /**
7676
+ * Background Renderer
7677
+ *
7678
+ * Specialized renderer for element backgrounds.
7679
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
7680
+ */
7681
+ class BackgroundRenderer {
7682
+ constructor(deps) {
7683
+ this.ctx = deps.ctx;
7684
+ this.context = deps.context;
7685
+ this.canvas = deps.canvas;
7686
+ // Options stored in deps but not needed as instance property
7610
7687
  }
7611
- applyEffects(effects) {
7612
- while (this._activeEffects.length) {
7613
- this.popEffect();
7688
+ /**
7689
+ * Render background images for a container
7690
+ * Supports URL images, linear gradients, and radial gradients
7691
+ *
7692
+ * @param container - Element container with background styles
7693
+ */
7694
+ async renderBackgroundImage(container) {
7695
+ let index = container.styles.backgroundImage.length - 1;
7696
+ for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
7697
+ if (backgroundImage.type === 0 /* CSSImageType.URL */) {
7698
+ await this.renderBackgroundURLImage(container, backgroundImage, index);
7699
+ }
7700
+ else if (isLinearGradient(backgroundImage)) {
7701
+ this.renderLinearGradient(container, backgroundImage, index);
7702
+ }
7703
+ else if (isRadialGradient(backgroundImage)) {
7704
+ this.renderRadialGradient(container, backgroundImage, index);
7705
+ }
7706
+ index--;
7614
7707
  }
7615
- effects.forEach((effect) => this.applyEffect(effect));
7616
7708
  }
7617
- applyEffect(effect) {
7618
- this.ctx.save();
7619
- if (isOpacityEffect(effect)) {
7620
- this.ctx.globalAlpha = effect.opacity;
7621
- }
7622
- if (isTransformEffect(effect)) {
7623
- this.ctx.translate(effect.offsetX, effect.offsetY);
7624
- this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
7625
- this.ctx.translate(-effect.offsetX, -effect.offsetY);
7709
+ /**
7710
+ * Render a URL-based background image
7711
+ */
7712
+ async renderBackgroundURLImage(container, backgroundImage, index) {
7713
+ let image;
7714
+ const url = backgroundImage.url;
7715
+ try {
7716
+ image = await this.context.cache.match(url);
7626
7717
  }
7627
- if (isClipEffect(effect)) {
7628
- this.path(effect.path);
7629
- this.ctx.clip();
7718
+ catch (e) {
7719
+ this.context.logger.error(`Error loading background-image ${url}`);
7720
+ }
7721
+ if (image) {
7722
+ const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
7723
+ const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
7724
+ const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
7725
+ imageWidth,
7726
+ imageHeight,
7727
+ imageWidth / imageHeight
7728
+ ]);
7729
+ const pattern = this.ctx.createPattern(this.resizeImage(image, width, height, container.styles.imageRendering), 'repeat');
7730
+ this.renderRepeat(path, pattern, x, y);
7630
7731
  }
7631
- this._activeEffects.push(effect);
7632
- }
7633
- popEffect() {
7634
- this._activeEffects.pop();
7635
- this.ctx.restore();
7636
7732
  }
7637
- async renderStack(stack) {
7638
- const styles = stack.element.container.styles;
7639
- if (styles.isVisible()) {
7640
- await this.renderStackContent(stack);
7733
+ /**
7734
+ * Render a linear gradient background
7735
+ */
7736
+ renderLinearGradient(container, backgroundImage, index) {
7737
+ const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
7738
+ const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
7739
+ const ownerDocument = this.canvas.ownerDocument ?? document;
7740
+ const canvas = ownerDocument.createElement('canvas');
7741
+ canvas.width = width;
7742
+ canvas.height = height;
7743
+ const ctx = canvas.getContext('2d');
7744
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
7745
+ processColorStops(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, asString(colorStop.color)));
7746
+ ctx.fillStyle = gradient;
7747
+ ctx.fillRect(0, 0, width, height);
7748
+ if (width > 0 && height > 0) {
7749
+ const pattern = this.ctx.createPattern(canvas, 'repeat');
7750
+ this.renderRepeat(path, pattern, x, y);
7641
7751
  }
7642
7752
  }
7643
- async renderNode(paint) {
7644
- if (contains(paint.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
7645
- debugger;
7646
- }
7647
- if (paint.container.styles.isVisible()) {
7648
- await this.renderNodeBackgroundAndBorders(paint);
7649
- await this.renderNodeContent(paint);
7753
+ /**
7754
+ * Render a radial gradient background
7755
+ */
7756
+ renderRadialGradient(container, backgroundImage, index) {
7757
+ const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
7758
+ const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
7759
+ const x = getAbsoluteValue(position[0], width);
7760
+ const y = getAbsoluteValue(position[position.length - 1], height);
7761
+ let [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
7762
+ // Handle edge case where radial gradient size is 0
7763
+ // Use a minimum value of 0.01 to ensure gradient is still rendered
7764
+ if (rx === 0 || ry === 0) {
7765
+ rx = Math.max(rx, 0.01);
7766
+ ry = Math.max(ry, 0.01);
7767
+ }
7768
+ if (rx > 0 && ry > 0) {
7769
+ const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
7770
+ processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)));
7771
+ this.path(path);
7772
+ this.ctx.fillStyle = radialGradient;
7773
+ if (rx !== ry) {
7774
+ // transforms for elliptical radial gradient
7775
+ const midX = container.bounds.left + 0.5 * container.bounds.width;
7776
+ const midY = container.bounds.top + 0.5 * container.bounds.height;
7777
+ const f = ry / rx;
7778
+ const invF = 1 / f;
7779
+ this.ctx.save();
7780
+ this.ctx.translate(midX, midY);
7781
+ this.ctx.transform(1, 0, 0, f, 0, 0);
7782
+ this.ctx.translate(-midX, -midY);
7783
+ this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
7784
+ this.ctx.restore();
7785
+ }
7786
+ else {
7787
+ this.ctx.fill();
7788
+ }
7650
7789
  }
7651
7790
  }
7652
- renderTextWithLetterSpacing(text, letterSpacing, baseline) {
7653
- if (letterSpacing === 0) {
7654
- // Use alphabetic baseline for consistent text positioning across browsers
7655
- // Issue #129: text.bounds.top + text.bounds.height causes text to render too low
7656
- this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
7791
+ /**
7792
+ * Render a repeating pattern with offset
7793
+ *
7794
+ * @param path - Path to fill
7795
+ * @param pattern - Canvas pattern or gradient
7796
+ * @param offsetX - X offset for pattern
7797
+ * @param offsetY - Y offset for pattern
7798
+ */
7799
+ renderRepeat(path, pattern, offsetX, offsetY) {
7800
+ this.path(path);
7801
+ this.ctx.fillStyle = pattern;
7802
+ this.ctx.translate(offsetX, offsetY);
7803
+ this.ctx.fill();
7804
+ this.ctx.translate(-offsetX, -offsetY);
7805
+ }
7806
+ /**
7807
+ * Resize an image to target dimensions
7808
+ *
7809
+ * @param image - Source image
7810
+ * @param width - Target width
7811
+ * @param height - Target height
7812
+ * @param imageRendering - CSS image-rendering property value
7813
+ * @returns Resized canvas or original image
7814
+ */
7815
+ resizeImage(image, width, height, imageRendering) {
7816
+ // https://github.com/niklasvh/html2canvas/pull/2911
7817
+ // if (image.width === width && image.height === height) {
7818
+ // return image;
7819
+ // }
7820
+ const ownerDocument = this.canvas.ownerDocument ?? document;
7821
+ const canvas = ownerDocument.createElement('canvas');
7822
+ canvas.width = Math.max(1, width);
7823
+ canvas.height = Math.max(1, height);
7824
+ const ctx = canvas.getContext('2d');
7825
+ // Apply image smoothing based on CSS image-rendering property
7826
+ if (imageRendering === IMAGE_RENDERING.PIXELATED || imageRendering === IMAGE_RENDERING.CRISP_EDGES) {
7827
+ this.context.logger.debug(`Disabling image smoothing for background image due to CSS image-rendering`);
7828
+ ctx.imageSmoothingEnabled = false;
7829
+ }
7830
+ else if (imageRendering === IMAGE_RENDERING.SMOOTH) {
7831
+ this.context.logger.debug(`Enabling image smoothing for background image due to CSS image-rendering: smooth`);
7832
+ ctx.imageSmoothingEnabled = true;
7657
7833
  }
7658
7834
  else {
7659
- const letters = segmentGraphemes(text.text);
7660
- letters.reduce((left, letter) => {
7661
- this.ctx.fillText(letter, left, text.bounds.top + baseline);
7662
- return left + this.ctx.measureText(letter).width;
7663
- }, text.bounds.left);
7835
+ // AUTO: inherit from main renderer context
7836
+ ctx.imageSmoothingEnabled = this.ctx.imageSmoothingEnabled;
7664
7837
  }
7838
+ // Inherit quality setting
7839
+ if (this.ctx.imageSmoothingQuality) {
7840
+ ctx.imageSmoothingQuality = this.ctx.imageSmoothingQuality;
7841
+ }
7842
+ ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
7843
+ return canvas;
7665
7844
  }
7666
7845
  /**
7667
- * Helper method to render text with paint order support
7668
- * Reduces code duplication in line-clamp and normal rendering
7846
+ * Create a canvas path from path array
7847
+ *
7848
+ * @param paths - Array of path points
7669
7849
  */
7670
- renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
7671
- paintOrderLayers.forEach((paintOrderLayer) => {
7672
- switch (paintOrderLayer) {
7673
- case 0 /* PAINT_ORDER_LAYER.FILL */:
7674
- this.ctx.fillStyle = asString(styles.color);
7675
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7676
- break;
7677
- case 1 /* PAINT_ORDER_LAYER.STROKE */:
7678
- if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
7679
- this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7680
- this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7681
- this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7682
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7683
- }
7684
- break;
7850
+ path(paths) {
7851
+ this.ctx.beginPath();
7852
+ this.formatPath(paths);
7853
+ this.ctx.closePath();
7854
+ }
7855
+ /**
7856
+ * Format path points into canvas path
7857
+ *
7858
+ * @param paths - Array of path points
7859
+ */
7860
+ formatPath(paths) {
7861
+ paths.forEach((point, index) => {
7862
+ const start = isBezierCurve(point) ? point.start : point;
7863
+ if (index === 0) {
7864
+ this.ctx.moveTo(start.x, start.y);
7865
+ }
7866
+ else {
7867
+ this.ctx.lineTo(start.x, start.y);
7868
+ }
7869
+ if (isBezierCurve(point)) {
7870
+ this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
7685
7871
  }
7686
7872
  });
7687
7873
  }
7688
- renderTextDecoration(bounds, styles) {
7689
- this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7690
- // Calculate decoration line thickness
7691
- let thickness = 1; // default
7692
- if (typeof styles.textDecorationThickness === 'number') {
7693
- thickness = styles.textDecorationThickness;
7694
- }
7695
- else if (styles.textDecorationThickness === 'from-font') {
7696
- // Use a reasonable default based on font size
7697
- thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
7698
- }
7699
- // 'auto' uses default thickness of 1
7700
- // Calculate underline offset
7701
- let underlineOffset = 0;
7702
- if (typeof styles.textUnderlineOffset === 'number') {
7703
- // It's a pixel value
7704
- underlineOffset = styles.textUnderlineOffset;
7705
- }
7706
- // 'auto' uses default offset of 0
7707
- const decorationStyle = styles.textDecorationStyle;
7708
- styles.textDecorationLine.forEach((textDecorationLine) => {
7709
- let y = 0;
7710
- switch (textDecorationLine) {
7711
- case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7712
- y = bounds.top + bounds.height - thickness + underlineOffset;
7713
- break;
7714
- case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7715
- y = bounds.top;
7716
- break;
7717
- case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7718
- y = bounds.top + (bounds.height / 2 - thickness / 2);
7719
- break;
7874
+ }
7875
+
7876
+ const parsePathForBorder = (curves, borderSide) => {
7877
+ switch (borderSide) {
7878
+ case 0:
7879
+ return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox);
7880
+ case 1:
7881
+ return createPathFromCurves(curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox);
7882
+ case 2:
7883
+ return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox);
7884
+ case 3:
7885
+ default:
7886
+ return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox, curves.topLeftBorderBox, curves.topLeftPaddingBox);
7887
+ }
7888
+ };
7889
+ const parsePathForBorderDoubleOuter = (curves, borderSide) => {
7890
+ switch (borderSide) {
7891
+ case 0:
7892
+ return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox);
7893
+ case 1:
7894
+ return createPathFromCurves(curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox);
7895
+ case 2:
7896
+ return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox);
7897
+ case 3:
7898
+ default:
7899
+ return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox, curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox);
7900
+ }
7901
+ };
7902
+ const parsePathForBorderDoubleInner = (curves, borderSide) => {
7903
+ switch (borderSide) {
7904
+ case 0:
7905
+ return createPathFromCurves(curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox);
7906
+ case 1:
7907
+ return createPathFromCurves(curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox);
7908
+ case 2:
7909
+ return createPathFromCurves(curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox);
7910
+ case 3:
7911
+ default:
7912
+ return createPathFromCurves(curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox, curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox);
7913
+ }
7914
+ };
7915
+ const parsePathForBorderStroke = (curves, borderSide) => {
7916
+ switch (borderSide) {
7917
+ case 0:
7918
+ return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
7919
+ case 1:
7920
+ return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
7921
+ case 2:
7922
+ return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
7923
+ case 3:
7924
+ default:
7925
+ return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
7926
+ }
7927
+ };
7928
+ const createStrokePathFromCurves = (outer1, outer2) => {
7929
+ const path = [];
7930
+ if (isBezierCurve(outer1)) {
7931
+ path.push(outer1.subdivide(0.5, false));
7932
+ }
7933
+ else {
7934
+ path.push(outer1);
7935
+ }
7936
+ if (isBezierCurve(outer2)) {
7937
+ path.push(outer2.subdivide(0.5, true));
7938
+ }
7939
+ else {
7940
+ path.push(outer2);
7941
+ }
7942
+ return path;
7943
+ };
7944
+ const createPathFromCurves = (outer1, inner1, outer2, inner2) => {
7945
+ const path = [];
7946
+ if (isBezierCurve(outer1)) {
7947
+ path.push(outer1.subdivide(0.5, false));
7948
+ }
7949
+ else {
7950
+ path.push(outer1);
7951
+ }
7952
+ if (isBezierCurve(outer2)) {
7953
+ path.push(outer2.subdivide(0.5, true));
7954
+ }
7955
+ else {
7956
+ path.push(outer2);
7957
+ }
7958
+ if (isBezierCurve(inner2)) {
7959
+ path.push(inner2.subdivide(0.5, true).reverse());
7960
+ }
7961
+ else {
7962
+ path.push(inner2);
7963
+ }
7964
+ if (isBezierCurve(inner1)) {
7965
+ path.push(inner1.subdivide(0.5, false).reverse());
7966
+ }
7967
+ else {
7968
+ path.push(inner1);
7969
+ }
7970
+ return path;
7971
+ };
7972
+
7973
+ /**
7974
+ * Border Renderer
7975
+ *
7976
+ * Handles rendering of element borders including:
7977
+ * - Solid borders
7978
+ * - Double borders
7979
+ * - Dashed borders
7980
+ * - Dotted borders
7981
+ */
7982
+ /**
7983
+ * Border Renderer
7984
+ *
7985
+ * Specialized renderer for element borders.
7986
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
7987
+ */
7988
+ class BorderRenderer {
7989
+ constructor(deps, pathCallbacks) {
7990
+ this.ctx = deps.ctx;
7991
+ this.pathCallbacks = pathCallbacks;
7992
+ }
7993
+ /**
7994
+ * Render a solid border
7995
+ *
7996
+ * @param color - Border color
7997
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
7998
+ * @param curvePoints - Border curve points
7999
+ */
8000
+ async renderSolidBorder(color, side, curvePoints) {
8001
+ this.pathCallbacks.path(parsePathForBorder(curvePoints, side));
8002
+ this.ctx.fillStyle = asString(color);
8003
+ this.ctx.fill();
8004
+ }
8005
+ /**
8006
+ * Render a double border
8007
+ * Falls back to solid border if width is too small
8008
+ *
8009
+ * @param color - Border color
8010
+ * @param width - Border width
8011
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
8012
+ * @param curvePoints - Border curve points
8013
+ */
8014
+ async renderDoubleBorder(color, width, side, curvePoints) {
8015
+ if (width < 3) {
8016
+ await this.renderSolidBorder(color, side, curvePoints);
8017
+ return;
8018
+ }
8019
+ const outerPaths = parsePathForBorderDoubleOuter(curvePoints, side);
8020
+ this.pathCallbacks.path(outerPaths);
8021
+ this.ctx.fillStyle = asString(color);
8022
+ this.ctx.fill();
8023
+ const innerPaths = parsePathForBorderDoubleInner(curvePoints, side);
8024
+ this.pathCallbacks.path(innerPaths);
8025
+ this.ctx.fill();
8026
+ }
8027
+ /**
8028
+ * Render a dashed or dotted border
8029
+ *
8030
+ * @param color - Border color
8031
+ * @param width - Border width
8032
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
8033
+ * @param curvePoints - Border curve points
8034
+ * @param style - Border style (DASHED or DOTTED)
8035
+ */
8036
+ async renderDashedDottedBorder(color, width, side, curvePoints, style) {
8037
+ this.ctx.save();
8038
+ const strokePaths = parsePathForBorderStroke(curvePoints, side);
8039
+ const boxPaths = parsePathForBorder(curvePoints, side);
8040
+ if (style === 2 /* BORDER_STYLE.DASHED */) {
8041
+ this.pathCallbacks.path(boxPaths);
8042
+ this.ctx.clip();
8043
+ }
8044
+ // Extract start and end coordinates
8045
+ let startX, startY, endX, endY;
8046
+ if (isBezierCurve(boxPaths[0])) {
8047
+ startX = boxPaths[0].start.x;
8048
+ startY = boxPaths[0].start.y;
8049
+ }
8050
+ else {
8051
+ startX = boxPaths[0].x;
8052
+ startY = boxPaths[0].y;
8053
+ }
8054
+ if (isBezierCurve(boxPaths[1])) {
8055
+ endX = boxPaths[1].end.x;
8056
+ endY = boxPaths[1].end.y;
8057
+ }
8058
+ else {
8059
+ endX = boxPaths[1].x;
8060
+ endY = boxPaths[1].y;
8061
+ }
8062
+ // Calculate border length
8063
+ let length;
8064
+ if (side === 0 || side === 2) {
8065
+ length = Math.abs(startX - endX);
8066
+ }
8067
+ else {
8068
+ length = Math.abs(startY - endY);
8069
+ }
8070
+ this.ctx.beginPath();
8071
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8072
+ this.pathCallbacks.formatPath(strokePaths);
8073
+ }
8074
+ else {
8075
+ this.pathCallbacks.formatPath(boxPaths.slice(0, 2));
8076
+ }
8077
+ // Calculate dash and space lengths
8078
+ let dashLength = width < 3 ? width * 3 : width * 2;
8079
+ let spaceLength = width < 3 ? width * 2 : width;
8080
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8081
+ dashLength = width;
8082
+ spaceLength = width;
8083
+ }
8084
+ // Adjust dash pattern for border length
8085
+ let useLineDash = true;
8086
+ if (length <= dashLength * 2) {
8087
+ useLineDash = false;
8088
+ }
8089
+ else if (length <= dashLength * 2 + spaceLength) {
8090
+ const multiplier = length / (2 * dashLength + spaceLength);
8091
+ dashLength *= multiplier;
8092
+ spaceLength *= multiplier;
8093
+ }
8094
+ else {
8095
+ const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
8096
+ const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
8097
+ const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
8098
+ spaceLength =
8099
+ maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
8100
+ ? minSpace
8101
+ : maxSpace;
8102
+ }
8103
+ // Apply line dash pattern
8104
+ if (useLineDash) {
8105
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8106
+ this.ctx.setLineDash([0, dashLength + spaceLength]);
8107
+ }
8108
+ else {
8109
+ this.ctx.setLineDash([dashLength, spaceLength]);
8110
+ }
8111
+ }
8112
+ // Set line style and stroke
8113
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8114
+ this.ctx.lineCap = 'round';
8115
+ this.ctx.lineWidth = width;
8116
+ }
8117
+ else {
8118
+ this.ctx.lineWidth = width * 2 + 1.1;
8119
+ }
8120
+ this.ctx.strokeStyle = asString(color);
8121
+ this.ctx.stroke();
8122
+ this.ctx.setLineDash([]);
8123
+ // Fill dashed round edge gaps
8124
+ if (style === 2 /* BORDER_STYLE.DASHED */) {
8125
+ if (isBezierCurve(boxPaths[0])) {
8126
+ const path1 = boxPaths[3];
8127
+ const path2 = boxPaths[0];
8128
+ this.ctx.beginPath();
8129
+ this.pathCallbacks.formatPath([
8130
+ new Vector(path1.end.x, path1.end.y),
8131
+ new Vector(path2.start.x, path2.start.y)
8132
+ ]);
8133
+ this.ctx.stroke();
8134
+ }
8135
+ if (isBezierCurve(boxPaths[1])) {
8136
+ const path1 = boxPaths[1];
8137
+ const path2 = boxPaths[2];
8138
+ this.ctx.beginPath();
8139
+ this.pathCallbacks.formatPath([
8140
+ new Vector(path1.end.x, path1.end.y),
8141
+ new Vector(path2.start.x, path2.start.y)
8142
+ ]);
8143
+ this.ctx.stroke();
8144
+ }
8145
+ }
8146
+ this.ctx.restore();
8147
+ }
8148
+ }
8149
+
8150
+ /**
8151
+ * Effects Renderer
8152
+ *
8153
+ * Handles rendering effects including:
8154
+ * - Opacity effects
8155
+ * - Transform effects (matrix transformations)
8156
+ * - Clip effects (clipping paths)
8157
+ */
8158
+ /**
8159
+ * Effects Renderer
8160
+ *
8161
+ * Manages rendering effects stack including opacity, transforms, and clipping.
8162
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
8163
+ */
8164
+ class EffectsRenderer {
8165
+ constructor(deps, pathCallback) {
8166
+ this.activeEffects = [];
8167
+ this.ctx = deps.ctx;
8168
+ this.pathCallback = pathCallback;
8169
+ }
8170
+ /**
8171
+ * Apply multiple effects
8172
+ * Clears existing effects and applies new ones
8173
+ *
8174
+ * @param effects - Array of effects to apply
8175
+ */
8176
+ applyEffects(effects) {
8177
+ // Clear all existing effects
8178
+ while (this.activeEffects.length) {
8179
+ this.popEffect();
8180
+ }
8181
+ // Apply new effects
8182
+ effects.forEach((effect) => this.applyEffect(effect));
8183
+ }
8184
+ /**
8185
+ * Apply a single effect
8186
+ *
8187
+ * @param effect - Effect to apply
8188
+ */
8189
+ applyEffect(effect) {
8190
+ this.ctx.save();
8191
+ // Apply opacity effect
8192
+ if (isOpacityEffect(effect)) {
8193
+ this.ctx.globalAlpha = effect.opacity;
8194
+ }
8195
+ // Apply transform effect
8196
+ if (isTransformEffect(effect)) {
8197
+ this.ctx.translate(effect.offsetX, effect.offsetY);
8198
+ this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
8199
+ this.ctx.translate(-effect.offsetX, -effect.offsetY);
8200
+ }
8201
+ // Apply clip effect
8202
+ if (isClipEffect(effect)) {
8203
+ this.pathCallback.path(effect.path);
8204
+ this.ctx.clip();
8205
+ }
8206
+ this.activeEffects.push(effect);
8207
+ }
8208
+ /**
8209
+ * Remove the most recent effect
8210
+ * Restores the canvas state before the effect was applied
8211
+ */
8212
+ popEffect() {
8213
+ this.activeEffects.pop();
8214
+ this.ctx.restore();
8215
+ }
8216
+ /**
8217
+ * Get the current number of active effects
8218
+ *
8219
+ * @returns Number of active effects
8220
+ */
8221
+ getActiveEffectCount() {
8222
+ return this.activeEffects.length;
8223
+ }
8224
+ /**
8225
+ * Check if there are any active effects
8226
+ *
8227
+ * @returns True if there are active effects
8228
+ */
8229
+ hasActiveEffects() {
8230
+ return this.activeEffects.length > 0;
8231
+ }
8232
+ }
8233
+
8234
+ /**
8235
+ * Text Renderer
8236
+ *
8237
+ * Handles rendering of text content including:
8238
+ * - Text with letter spacing
8239
+ * - Text decorations (underline, overline, line-through)
8240
+ * - Text shadows
8241
+ * - Webkit line clamp
8242
+ * - Text overflow ellipsis
8243
+ * - Paint order (fill/stroke)
8244
+ * - Font styles
8245
+ */
8246
+ // iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
8247
+ const iOSBrokenFonts = ['-apple-system', 'system-ui'];
8248
+ /**
8249
+ * Detect iOS version from user agent
8250
+ * Returns null if not iOS or version cannot be determined
8251
+ */
8252
+ const getIOSVersion = () => {
8253
+ if (typeof navigator === 'undefined') {
8254
+ return null;
8255
+ }
8256
+ const userAgent = navigator.userAgent;
8257
+ // Check if it's iOS or iPadOS
8258
+ // iPadOS 13+ may identify as Macintosh, check for touch support
8259
+ const isIOS = /iPhone|iPad|iPod/.test(userAgent);
8260
+ const isIPadOS = /Macintosh/.test(userAgent) && navigator.maxTouchPoints && navigator.maxTouchPoints > 1;
8261
+ if (!isIOS && !isIPadOS) {
8262
+ return null;
8263
+ }
8264
+ // Extract version number from various iOS user agent formats:
8265
+ // - "iPhone OS 15_0" or "iPhone OS 15_0_1"
8266
+ // - "CPU OS 15_0 like Mac OS X"
8267
+ // - "CPU iPhone OS 15_0 like Mac OS X"
8268
+ // - "Version/15.0" (for iPadOS)
8269
+ const patterns = [
8270
+ /(?:iPhone|CPU(?:\siPhone)?)\sOS\s(\d+)[\._](\d+)/, // iPhone OS, CPU OS, CPU iPhone OS
8271
+ /Version\/(\d+)\.(\d+)/ // Version/15.0 (iPadOS)
8272
+ ];
8273
+ for (const pattern of patterns) {
8274
+ const match = userAgent.match(pattern);
8275
+ if (match && match[1]) {
8276
+ return parseInt(match[1], 10);
8277
+ }
8278
+ }
8279
+ return null;
8280
+ };
8281
+ const fixIOSSystemFonts = (fontFamilies) => {
8282
+ const iosVersion = getIOSVersion();
8283
+ // On iOS 15.0 and 15.1, system fonts have rendering issues
8284
+ // Fixed in iOS 17+
8285
+ if (iosVersion !== null && iosVersion >= 15 && iosVersion < 17) {
8286
+ return fontFamilies.map((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) !== -1
8287
+ ? `-apple-system, "Helvetica Neue", Arial, sans-serif`
8288
+ : fontFamily);
8289
+ }
8290
+ return fontFamilies;
8291
+ };
8292
+ /**
8293
+ * Text Renderer
8294
+ *
8295
+ * Specialized renderer for text content.
8296
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
8297
+ */
8298
+ class TextRenderer {
8299
+ constructor(deps) {
8300
+ this.ctx = deps.ctx;
8301
+ // context stored but not used directly in this renderer
8302
+ this.options = deps.options;
8303
+ }
8304
+ /**
8305
+ * Render text with letter spacing
8306
+ * Public method used by list rendering
8307
+ */
8308
+ renderTextWithLetterSpacing(text, letterSpacing, baseline) {
8309
+ if (letterSpacing === 0) {
8310
+ // Use alphabetic baseline for consistent text positioning across browsers
8311
+ // Issue #129: text.bounds.top + text.bounds.height causes text to render too low
8312
+ this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
8313
+ }
8314
+ else {
8315
+ const letters = segmentGraphemes(text.text);
8316
+ letters.reduce((left, letter) => {
8317
+ this.ctx.fillText(letter, left, text.bounds.top + baseline);
8318
+ return left + this.ctx.measureText(letter).width;
8319
+ }, text.bounds.left);
8320
+ }
8321
+ }
8322
+ /**
8323
+ * Helper method to render text with paint order support
8324
+ * Reduces code duplication in line-clamp and normal rendering
8325
+ */
8326
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
8327
+ paintOrderLayers.forEach((paintOrderLayer) => {
8328
+ switch (paintOrderLayer) {
8329
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
8330
+ this.ctx.fillStyle = asString(styles.color);
8331
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
8332
+ break;
8333
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
8334
+ if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
8335
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8336
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8337
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8338
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
8339
+ }
8340
+ break;
8341
+ }
8342
+ });
8343
+ }
8344
+ renderTextDecoration(bounds, styles) {
8345
+ this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
8346
+ // Calculate decoration line thickness
8347
+ let thickness = 1; // default
8348
+ if (typeof styles.textDecorationThickness === 'number') {
8349
+ thickness = styles.textDecorationThickness;
8350
+ }
8351
+ else if (styles.textDecorationThickness === 'from-font') {
8352
+ // Use a reasonable default based on font size
8353
+ thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
8354
+ }
8355
+ // 'auto' uses default thickness of 1
8356
+ // Calculate underline offset
8357
+ let underlineOffset = 0;
8358
+ if (typeof styles.textUnderlineOffset === 'number') {
8359
+ // It's a pixel value
8360
+ underlineOffset = styles.textUnderlineOffset;
8361
+ }
8362
+ // 'auto' uses default offset of 0
8363
+ const decorationStyle = styles.textDecorationStyle;
8364
+ styles.textDecorationLine.forEach((textDecorationLine) => {
8365
+ let y = 0;
8366
+ switch (textDecorationLine) {
8367
+ case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
8368
+ y = bounds.top + bounds.height - thickness + underlineOffset;
8369
+ break;
8370
+ case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
8371
+ y = bounds.top;
8372
+ break;
8373
+ case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
8374
+ y = bounds.top + (bounds.height / 2 - thickness / 2);
8375
+ break;
7720
8376
  default:
7721
8377
  return;
7722
8378
  }
@@ -7813,6 +8469,10 @@ class CanvasRenderer extends Renderer {
7813
8469
  return result.join('') + ellipsis;
7814
8470
  }
7815
8471
  }
8472
+ /**
8473
+ * Create font style array
8474
+ * Public method used by list rendering
8475
+ */
7816
8476
  createFontStyle(styles) {
7817
8477
  const fontVariant = styles.fontVariant
7818
8478
  .filter((variant) => variant === 'normal' || variant === 'small-caps')
@@ -7995,846 +8655,1756 @@ class CanvasRenderer extends Renderer {
7995
8655
  }
7996
8656
  break;
7997
8657
  case 1 /* PAINT_ORDER_LAYER.STROKE */:
7998
- if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
8658
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
8659
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8660
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8661
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8662
+ if (styles.letterSpacing === 0) {
8663
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8664
+ }
8665
+ else {
8666
+ const letters = segmentGraphemes(truncatedText);
8667
+ letters.reduce((left, letter) => {
8668
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8669
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8670
+ }, firstBound.bounds.left);
8671
+ }
8672
+ }
8673
+ break;
8674
+ }
8675
+ });
8676
+ return;
8677
+ }
8678
+ // Normal rendering (no ellipsis needed)
8679
+ text.textBounds.forEach((text) => {
8680
+ paintOrder.forEach((paintOrderLayer) => {
8681
+ switch (paintOrderLayer) {
8682
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
8683
+ this.ctx.fillStyle = asString(styles.color);
8684
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8685
+ const textShadows = styles.textShadow;
8686
+ if (textShadows.length && text.text.trim().length) {
8687
+ textShadows
8688
+ .slice(0)
8689
+ .reverse()
8690
+ .forEach((textShadow) => {
8691
+ this.ctx.shadowColor = asString(textShadow.color);
8692
+ this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
8693
+ this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
8694
+ this.ctx.shadowBlur = textShadow.blur.number;
8695
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8696
+ });
8697
+ this.ctx.shadowColor = '';
8698
+ this.ctx.shadowOffsetX = 0;
8699
+ this.ctx.shadowOffsetY = 0;
8700
+ this.ctx.shadowBlur = 0;
8701
+ }
8702
+ if (styles.textDecorationLine.length) {
8703
+ this.renderTextDecoration(text.bounds, styles);
8704
+ }
8705
+ break;
8706
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
8707
+ if (styles.webkitTextStrokeWidth && text.text.trim().length) {
7999
8708
  this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8000
8709
  this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8001
8710
  this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8711
+ // Issue #110: Use baseline (fontSize) for consistent positioning with fill
8712
+ // Previously used text.bounds.height which caused stroke to render too low
8713
+ const baseline = styles.fontSize.number;
8002
8714
  if (styles.letterSpacing === 0) {
8003
- this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8715
+ this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
8004
8716
  }
8005
8717
  else {
8006
- const letters = segmentGraphemes(truncatedText);
8718
+ const letters = segmentGraphemes(text.text);
8007
8719
  letters.reduce((left, letter) => {
8008
- this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8009
- return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8010
- }, firstBound.bounds.left);
8720
+ this.ctx.strokeText(letter, left, text.bounds.top + baseline);
8721
+ return left + this.ctx.measureText(letter).width;
8722
+ }, text.bounds.left);
8011
8723
  }
8012
8724
  }
8725
+ this.ctx.strokeStyle = '';
8726
+ this.ctx.lineWidth = 0;
8727
+ this.ctx.lineJoin = 'miter';
8013
8728
  break;
8014
8729
  }
8015
8730
  });
8016
- return;
8731
+ });
8732
+ }
8733
+ }
8734
+
8735
+ const MASK_OFFSET = 10000;
8736
+ class CanvasRenderer extends Renderer {
8737
+ constructor(context, options) {
8738
+ super(context, options);
8739
+ this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
8740
+ this.ctx = this.canvas.getContext('2d');
8741
+ if (!options.canvas) {
8742
+ this.canvas.width = Math.floor(options.width * options.scale);
8743
+ this.canvas.height = Math.floor(options.height * options.scale);
8744
+ this.canvas.style.width = `${options.width}px`;
8745
+ this.canvas.style.height = `${options.height}px`;
8746
+ }
8747
+ this.fontMetrics = new FontMetrics(document);
8748
+ this.ctx.scale(this.options.scale, this.options.scale);
8749
+ this.ctx.translate(-options.x, -options.y);
8750
+ this.ctx.textBaseline = 'bottom';
8751
+ // Set image smoothing options
8752
+ if (options.imageSmoothing !== undefined) {
8753
+ this.ctx.imageSmoothingEnabled = options.imageSmoothing;
8754
+ }
8755
+ if (options.imageSmoothingQuality) {
8756
+ this.ctx.imageSmoothingQuality = options.imageSmoothingQuality;
8757
+ }
8758
+ // Initialize specialized renderers
8759
+ this.backgroundRenderer = new BackgroundRenderer({
8760
+ ctx: this.ctx,
8761
+ context: this.context,
8762
+ canvas: this.canvas,
8763
+ options: {
8764
+ width: options.width,
8765
+ height: options.height,
8766
+ scale: options.scale
8767
+ }
8768
+ });
8769
+ this.borderRenderer = new BorderRenderer({ ctx: this.ctx }, {
8770
+ path: (paths) => this.path(paths),
8771
+ formatPath: (paths) => this.formatPath(paths)
8772
+ });
8773
+ this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
8774
+ this.textRenderer = new TextRenderer({
8775
+ ctx: this.ctx,
8776
+ context: this.context,
8777
+ options: { scale: options.scale }
8778
+ });
8779
+ this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
8780
+ }
8781
+ async renderStack(stack) {
8782
+ const styles = stack.element.container.styles;
8783
+ if (styles.isVisible()) {
8784
+ await this.renderStackContent(stack);
8785
+ }
8786
+ }
8787
+ async renderNode(paint) {
8788
+ if (contains(paint.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
8789
+ debugger;
8790
+ }
8791
+ if (paint.container.styles.isVisible()) {
8792
+ await this.renderNodeBackgroundAndBorders(paint);
8793
+ await this.renderNodeContent(paint);
8794
+ }
8795
+ }
8796
+ /**
8797
+ * Helper method to render text with paint order support
8798
+ * Reduces code duplication in line-clamp and normal rendering
8799
+ */
8800
+ // Helper method to truncate text and add ellipsis if needed
8801
+ renderReplacedElement(container, curves, image) {
8802
+ const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
8803
+ const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
8804
+ if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
8805
+ const box = contentBox(container);
8806
+ const path = calculatePaddingBoxPath(curves);
8807
+ this.path(path);
8808
+ this.ctx.save();
8809
+ this.ctx.clip();
8810
+ let sx = 0, sy = 0, sw = intrinsicWidth, sh = intrinsicHeight, dx = box.left, dy = box.top, dw = box.width, dh = box.height;
8811
+ const { objectFit } = container.styles;
8812
+ const boxRatio = dw / dh;
8813
+ const imgRatio = sw / sh;
8814
+ if (objectFit === 2 /* OBJECT_FIT.CONTAIN */) {
8815
+ if (imgRatio > boxRatio) {
8816
+ dh = dw / imgRatio;
8817
+ dy += (box.height - dh) / 2;
8818
+ }
8819
+ else {
8820
+ dw = dh * imgRatio;
8821
+ dx += (box.width - dw) / 2;
8822
+ }
8823
+ }
8824
+ else if (objectFit === 4 /* OBJECT_FIT.COVER */) {
8825
+ if (imgRatio > boxRatio) {
8826
+ sw = sh * boxRatio;
8827
+ sx += (intrinsicWidth - sw) / 2;
8828
+ }
8829
+ else {
8830
+ sh = sw / boxRatio;
8831
+ sy += (intrinsicHeight - sh) / 2;
8832
+ }
8833
+ }
8834
+ else if (objectFit === 8 /* OBJECT_FIT.NONE */) {
8835
+ if (sw > dw) {
8836
+ sx += (sw - dw) / 2;
8837
+ sw = dw;
8838
+ }
8839
+ else {
8840
+ dx += (dw - sw) / 2;
8841
+ dw = sw;
8842
+ }
8843
+ if (sh > dh) {
8844
+ sy += (sh - dh) / 2;
8845
+ sh = dh;
8846
+ }
8847
+ else {
8848
+ dy += (dh - sh) / 2;
8849
+ dh = sh;
8850
+ }
8851
+ }
8852
+ else if (objectFit === 16 /* OBJECT_FIT.SCALE_DOWN */) {
8853
+ const containW = imgRatio > boxRatio ? dw : dh * imgRatio;
8854
+ const noneW = sw > dw ? sw : dw;
8855
+ if (containW < noneW) {
8856
+ if (imgRatio > boxRatio) {
8857
+ dh = dw / imgRatio;
8858
+ dy += (box.height - dh) / 2;
8859
+ }
8860
+ else {
8861
+ dw = dh * imgRatio;
8862
+ dx += (box.width - dw) / 2;
8863
+ }
8864
+ }
8865
+ else {
8866
+ if (sw > dw) {
8867
+ sx += (sw - dw) / 2;
8868
+ sw = dw;
8869
+ }
8870
+ else {
8871
+ dx += (dw - sw) / 2;
8872
+ dw = sw;
8873
+ }
8874
+ if (sh > dh) {
8875
+ sy += (sh - dh) / 2;
8876
+ sh = dh;
8877
+ }
8878
+ else {
8879
+ dy += (dh - sh) / 2;
8880
+ dh = sh;
8881
+ }
8882
+ }
8883
+ }
8884
+ this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
8885
+ this.ctx.restore();
8886
+ }
8887
+ }
8888
+ async renderNodeContent(paint) {
8889
+ this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
8890
+ const container = paint.container;
8891
+ const curves = paint.curves;
8892
+ const styles = container.styles;
8893
+ // Use content box for text overflow calculation (excludes padding and border)
8894
+ // This matches browser behavior where text-overflow uses the content width
8895
+ const textBounds = contentBox(container);
8896
+ for (const child of container.textNodes) {
8897
+ await this.textRenderer.renderTextNode(child, styles, textBounds);
8898
+ }
8899
+ if (container instanceof ImageElementContainer) {
8900
+ try {
8901
+ const image = await this.context.cache.match(container.src);
8902
+ // Apply image smoothing based on CSS image-rendering property and global options
8903
+ const prevSmoothing = this.ctx.imageSmoothingEnabled;
8904
+ // CSS image-rendering property overrides global settings
8905
+ if (styles.imageRendering === IMAGE_RENDERING.PIXELATED ||
8906
+ styles.imageRendering === IMAGE_RENDERING.CRISP_EDGES) {
8907
+ this.context.logger.debug(`Disabling image smoothing for ${container.src} due to CSS image-rendering: ${styles.imageRendering === IMAGE_RENDERING.PIXELATED ? 'pixelated' : 'crisp-edges'}`);
8908
+ this.ctx.imageSmoothingEnabled = false;
8909
+ }
8910
+ else if (styles.imageRendering === IMAGE_RENDERING.SMOOTH) {
8911
+ this.context.logger.debug(`Enabling image smoothing for ${container.src} due to CSS image-rendering: smooth`);
8912
+ this.ctx.imageSmoothingEnabled = true;
8913
+ }
8914
+ // IMAGE_RENDERING.AUTO: keep current global setting
8915
+ this.renderReplacedElement(container, curves, image);
8916
+ // Restore previous smoothing state
8917
+ this.ctx.imageSmoothingEnabled = prevSmoothing;
8918
+ }
8919
+ catch (e) {
8920
+ this.context.logger.error(`Error loading image ${container.src}`);
8921
+ }
8922
+ }
8923
+ if (container instanceof CanvasElementContainer) {
8924
+ this.renderReplacedElement(container, curves, container.canvas);
8925
+ }
8926
+ if (container instanceof SVGElementContainer) {
8927
+ try {
8928
+ const image = await this.context.cache.match(container.svg);
8929
+ this.renderReplacedElement(container, curves, image);
8930
+ }
8931
+ catch (e) {
8932
+ this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
8933
+ }
8934
+ }
8935
+ if (container instanceof IFrameElementContainer && container.tree) {
8936
+ const iframeRenderer = new CanvasRenderer(this.context, {
8937
+ scale: this.options.scale,
8938
+ backgroundColor: container.backgroundColor,
8939
+ x: 0,
8940
+ y: 0,
8941
+ width: container.width,
8942
+ height: container.height
8943
+ });
8944
+ const canvas = await iframeRenderer.render(container.tree);
8945
+ if (container.width && container.height) {
8946
+ this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
8947
+ }
8948
+ }
8949
+ if (container instanceof InputElementContainer) {
8950
+ const size = Math.min(container.bounds.width, container.bounds.height);
8951
+ if (container.type === CHECKBOX) {
8952
+ if (container.checked) {
8953
+ this.ctx.save();
8954
+ this.path([
8955
+ new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
8956
+ new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
8957
+ new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
8958
+ new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
8959
+ new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
8960
+ new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
8961
+ new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
8962
+ ]);
8963
+ this.ctx.fillStyle = asString(INPUT_COLOR);
8964
+ this.ctx.fill();
8965
+ this.ctx.restore();
8966
+ }
8967
+ }
8968
+ else if (container.type === RADIO) {
8969
+ if (container.checked) {
8970
+ this.ctx.save();
8971
+ this.ctx.beginPath();
8972
+ this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
8973
+ this.ctx.fillStyle = asString(INPUT_COLOR);
8974
+ this.ctx.fill();
8975
+ this.ctx.restore();
8976
+ }
8977
+ }
8978
+ }
8979
+ if (isTextInputElement(container) && container.value.length) {
8980
+ const [font, fontFamily, fontSize] = this.textRenderer.createFontStyle(styles);
8981
+ const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
8982
+ this.ctx.font = font;
8983
+ // Fix for Issue #92: Use placeholder color when rendering placeholder text
8984
+ const isPlaceholder = container instanceof InputElementContainer && container.isPlaceholder;
8985
+ this.ctx.fillStyle = isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
8986
+ this.ctx.textBaseline = 'alphabetic';
8987
+ this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
8988
+ const bounds = contentBox(container);
8989
+ let x = 0;
8990
+ switch (container.styles.textAlign) {
8991
+ case 1 /* TEXT_ALIGN.CENTER */:
8992
+ x += bounds.width / 2;
8993
+ break;
8994
+ case 2 /* TEXT_ALIGN.RIGHT */:
8995
+ x += bounds.width;
8996
+ break;
8997
+ }
8998
+ // Fix for Issue #92: Position text vertically centered in single-line input
8999
+ // Only apply vertical centering for InputElementContainer, not for textarea or select
9000
+ let verticalOffset = 0;
9001
+ if (container instanceof InputElementContainer) {
9002
+ const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
9003
+ verticalOffset = (bounds.height - fontSizeValue) / 2;
9004
+ }
9005
+ // Create text bounds with horizontal and vertical offsets
9006
+ // Height is not modified as it doesn't affect text rendering position
9007
+ const textBounds = bounds.add(x, verticalOffset, 0, 0);
9008
+ this.ctx.save();
9009
+ this.path([
9010
+ new Vector(bounds.left, bounds.top),
9011
+ new Vector(bounds.left + bounds.width, bounds.top),
9012
+ new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
9013
+ new Vector(bounds.left, bounds.top + bounds.height)
9014
+ ]);
9015
+ this.ctx.clip();
9016
+ this.textRenderer.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
9017
+ this.ctx.restore();
9018
+ this.ctx.textBaseline = 'alphabetic';
9019
+ this.ctx.textAlign = 'left';
8017
9020
  }
8018
- // Normal rendering (no ellipsis needed)
8019
- text.textBounds.forEach((text) => {
8020
- paintOrder.forEach((paintOrderLayer) => {
8021
- switch (paintOrderLayer) {
8022
- case 0 /* PAINT_ORDER_LAYER.FILL */:
8023
- this.ctx.fillStyle = asString(styles.color);
8024
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8025
- const textShadows = styles.textShadow;
8026
- if (textShadows.length && text.text.trim().length) {
8027
- textShadows
8028
- .slice(0)
8029
- .reverse()
8030
- .forEach((textShadow) => {
8031
- this.ctx.shadowColor = asString(textShadow.color);
8032
- this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
8033
- this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
8034
- this.ctx.shadowBlur = textShadow.blur.number;
8035
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8036
- });
8037
- this.ctx.shadowColor = '';
8038
- this.ctx.shadowOffsetX = 0;
8039
- this.ctx.shadowOffsetY = 0;
8040
- this.ctx.shadowBlur = 0;
8041
- }
8042
- if (styles.textDecorationLine.length) {
8043
- this.renderTextDecoration(text.bounds, styles);
8044
- }
8045
- break;
8046
- case 1 /* PAINT_ORDER_LAYER.STROKE */:
8047
- if (styles.webkitTextStrokeWidth && text.text.trim().length) {
8048
- this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8049
- this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8050
- this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8051
- // Issue #110: Use baseline (fontSize) for consistent positioning with fill
8052
- // Previously used text.bounds.height which caused stroke to render too low
8053
- const baseline = styles.fontSize.number;
8054
- if (styles.letterSpacing === 0) {
8055
- this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
8056
- }
8057
- else {
8058
- const letters = segmentGraphemes(text.text);
8059
- letters.reduce((left, letter) => {
8060
- this.ctx.strokeText(letter, left, text.bounds.top + baseline);
8061
- return left + this.ctx.measureText(letter).width;
8062
- }, text.bounds.left);
8063
- }
8064
- }
8065
- this.ctx.strokeStyle = '';
8066
- this.ctx.lineWidth = 0;
8067
- this.ctx.lineJoin = 'miter';
8068
- break;
9021
+ if (contains(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
9022
+ if (container.styles.listStyleImage !== null) {
9023
+ const img = container.styles.listStyleImage;
9024
+ if (img.type === 0 /* CSSImageType.URL */) {
9025
+ let image;
9026
+ const url = img.url;
9027
+ try {
9028
+ image = await this.context.cache.match(url);
9029
+ this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
9030
+ }
9031
+ catch (e) {
9032
+ this.context.logger.error(`Error loading list-style-image ${url}`);
9033
+ }
8069
9034
  }
8070
- });
9035
+ }
9036
+ else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
9037
+ const [font] = this.textRenderer.createFontStyle(styles);
9038
+ this.ctx.font = font;
9039
+ this.ctx.fillStyle = asString(styles.color);
9040
+ this.ctx.textBaseline = 'middle';
9041
+ this.ctx.textAlign = 'right';
9042
+ const bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
9043
+ this.textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2);
9044
+ this.ctx.textBaseline = 'bottom';
9045
+ this.ctx.textAlign = 'left';
9046
+ }
9047
+ }
9048
+ }
9049
+ async renderStackContent(stack) {
9050
+ if (contains(stack.element.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
9051
+ debugger;
9052
+ }
9053
+ // https://www.w3.org/TR/css-position-3/#painting-order
9054
+ // 1. the background and borders of the element forming the stacking context.
9055
+ await this.renderNodeBackgroundAndBorders(stack.element);
9056
+ // 2. the child stacking contexts with negative stack levels (most negative first).
9057
+ for (const child of stack.negativeZIndex) {
9058
+ await this.renderStack(child);
9059
+ }
9060
+ // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
9061
+ await this.renderNodeContent(stack.element);
9062
+ for (const child of stack.nonInlineLevel) {
9063
+ await this.renderNode(child);
9064
+ }
9065
+ // 4. All non-positioned floating descendants, in tree order. For each one of these,
9066
+ // treat the element as if it created a new stacking context, but any positioned descendants and descendants
9067
+ // which actually create a new stacking context should be considered part of the parent stacking context,
9068
+ // not this new one.
9069
+ for (const child of stack.nonPositionedFloats) {
9070
+ await this.renderStack(child);
9071
+ }
9072
+ // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
9073
+ for (const child of stack.nonPositionedInlineLevel) {
9074
+ await this.renderStack(child);
9075
+ }
9076
+ for (const child of stack.inlineLevel) {
9077
+ await this.renderNode(child);
9078
+ }
9079
+ // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
9080
+ // All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
9081
+ // For those with 'z-index: auto', treat the element as if it created a new stacking context,
9082
+ // but any positioned descendants and descendants which actually create a new stacking context should be
9083
+ // considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
9084
+ // treat the stacking context generated atomically.
9085
+ //
9086
+ // All opacity descendants with opacity less than 1
9087
+ //
9088
+ // All transform descendants with transform other than none
9089
+ for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
9090
+ await this.renderStack(child);
9091
+ }
9092
+ // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
9093
+ // order (smallest first) then tree order.
9094
+ for (const child of stack.positiveZIndex) {
9095
+ await this.renderStack(child);
9096
+ }
9097
+ }
9098
+ mask(paths) {
9099
+ this.ctx.beginPath();
9100
+ this.ctx.moveTo(0, 0);
9101
+ // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
9102
+ // because context has already been scaled by this.options.scale
9103
+ // Fix for Issue #126: Using canvas pixel dimensions causes broken output
9104
+ this.ctx.lineTo(this.options.width, 0);
9105
+ this.ctx.lineTo(this.options.width, this.options.height);
9106
+ this.ctx.lineTo(0, this.options.height);
9107
+ this.ctx.lineTo(0, 0);
9108
+ this.formatPath(paths.slice(0).reverse());
9109
+ this.ctx.closePath();
9110
+ }
9111
+ path(paths) {
9112
+ this.ctx.beginPath();
9113
+ this.formatPath(paths);
9114
+ this.ctx.closePath();
9115
+ }
9116
+ formatPath(paths) {
9117
+ paths.forEach((point, index) => {
9118
+ const start = isBezierCurve(point) ? point.start : point;
9119
+ if (index === 0) {
9120
+ this.ctx.moveTo(start.x, start.y);
9121
+ }
9122
+ else {
9123
+ this.ctx.lineTo(start.x, start.y);
9124
+ }
9125
+ if (isBezierCurve(point)) {
9126
+ this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
9127
+ }
8071
9128
  });
8072
9129
  }
8073
- renderReplacedElement(container, curves, image) {
8074
- const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
8075
- const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
8076
- if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
8077
- const box = contentBox(container);
8078
- const path = calculatePaddingBoxPath(curves);
8079
- this.path(path);
9130
+ async renderNodeBackgroundAndBorders(paint) {
9131
+ this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
9132
+ const styles = paint.container.styles;
9133
+ const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
9134
+ const borders = [
9135
+ { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
9136
+ { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
9137
+ { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
9138
+ { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
9139
+ ];
9140
+ const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
9141
+ if (hasBackground || styles.boxShadow.length) {
8080
9142
  this.ctx.save();
9143
+ this.path(backgroundPaintingArea);
8081
9144
  this.ctx.clip();
8082
- let sx = 0, sy = 0, sw = intrinsicWidth, sh = intrinsicHeight, dx = box.left, dy = box.top, dw = box.width, dh = box.height;
8083
- const { objectFit } = container.styles;
8084
- const boxRatio = dw / dh;
8085
- const imgRatio = sw / sh;
8086
- if (objectFit === 2 /* OBJECT_FIT.CONTAIN */) {
8087
- if (imgRatio > boxRatio) {
8088
- dh = dw / imgRatio;
8089
- dy += (box.height - dh) / 2;
8090
- }
8091
- else {
8092
- dw = dh * imgRatio;
8093
- dx += (box.width - dw) / 2;
8094
- }
8095
- }
8096
- else if (objectFit === 4 /* OBJECT_FIT.COVER */) {
8097
- if (imgRatio > boxRatio) {
8098
- sw = sh * boxRatio;
8099
- sx += (intrinsicWidth - sw) / 2;
8100
- }
8101
- else {
8102
- sh = sw / boxRatio;
8103
- sy += (intrinsicHeight - sh) / 2;
8104
- }
9145
+ if (!isTransparent(styles.backgroundColor)) {
9146
+ this.ctx.fillStyle = asString(styles.backgroundColor);
9147
+ this.ctx.fill();
8105
9148
  }
8106
- else if (objectFit === 8 /* OBJECT_FIT.NONE */) {
8107
- if (sw > dw) {
8108
- sx += (sw - dw) / 2;
8109
- sw = dw;
9149
+ await this.backgroundRenderer.renderBackgroundImage(paint.container);
9150
+ this.ctx.restore();
9151
+ styles.boxShadow
9152
+ .slice(0)
9153
+ .reverse()
9154
+ .forEach((shadow) => {
9155
+ this.ctx.save();
9156
+ const borderBoxArea = calculateBorderBoxPath(paint.curves);
9157
+ const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
9158
+ const shadowPaintingArea = transformPath(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
9159
+ if (shadow.inset) {
9160
+ this.path(borderBoxArea);
9161
+ this.ctx.clip();
9162
+ this.mask(shadowPaintingArea);
8110
9163
  }
8111
9164
  else {
8112
- dx += (dw - sw) / 2;
8113
- dw = sw;
9165
+ this.mask(borderBoxArea);
9166
+ this.ctx.clip();
9167
+ this.path(shadowPaintingArea);
8114
9168
  }
8115
- if (sh > dh) {
8116
- sy += (sh - dh) / 2;
8117
- sh = dh;
9169
+ this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
9170
+ this.ctx.shadowOffsetY = shadow.offsetY.number;
9171
+ this.ctx.shadowColor = asString(shadow.color);
9172
+ this.ctx.shadowBlur = shadow.blur.number;
9173
+ this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
9174
+ this.ctx.fill();
9175
+ this.ctx.restore();
9176
+ });
9177
+ }
9178
+ let side = 0;
9179
+ for (const border of borders) {
9180
+ if (border.style !== 0 /* BORDER_STYLE.NONE */ && !isTransparent(border.color) && border.width > 0) {
9181
+ if (border.style === 2 /* BORDER_STYLE.DASHED */) {
9182
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
8118
9183
  }
8119
- else {
8120
- dy += (dh - sh) / 2;
8121
- dh = sh;
9184
+ else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
9185
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
8122
9186
  }
8123
- }
8124
- else if (objectFit === 16 /* OBJECT_FIT.SCALE_DOWN */) {
8125
- const containW = imgRatio > boxRatio ? dw : dh * imgRatio;
8126
- const noneW = sw > dw ? sw : dw;
8127
- if (containW < noneW) {
8128
- if (imgRatio > boxRatio) {
8129
- dh = dw / imgRatio;
8130
- dy += (box.height - dh) / 2;
8131
- }
8132
- else {
8133
- dw = dh * imgRatio;
8134
- dx += (box.width - dw) / 2;
8135
- }
9187
+ else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
9188
+ await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
8136
9189
  }
8137
9190
  else {
8138
- if (sw > dw) {
8139
- sx += (sw - dw) / 2;
8140
- sw = dw;
8141
- }
8142
- else {
8143
- dx += (dw - sw) / 2;
8144
- dw = sw;
8145
- }
8146
- if (sh > dh) {
8147
- sy += (sh - dh) / 2;
8148
- sh = dh;
8149
- }
8150
- else {
8151
- dy += (dh - sh) / 2;
8152
- dh = sh;
8153
- }
9191
+ await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
8154
9192
  }
8155
9193
  }
8156
- this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
8157
- this.ctx.restore();
9194
+ side++;
8158
9195
  }
8159
9196
  }
8160
- async renderNodeContent(paint) {
8161
- this.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
8162
- const container = paint.container;
8163
- const curves = paint.curves;
8164
- const styles = container.styles;
8165
- // Use content box for text overflow calculation (excludes padding and border)
8166
- // This matches browser behavior where text-overflow uses the content width
8167
- const textBounds = contentBox(container);
8168
- for (const child of container.textNodes) {
8169
- await this.renderTextNode(child, styles, textBounds);
8170
- }
8171
- if (container instanceof ImageElementContainer) {
8172
- try {
8173
- const image = await this.context.cache.match(container.src);
8174
- this.renderReplacedElement(container, curves, image);
8175
- }
8176
- catch (e) {
8177
- this.context.logger.error(`Error loading image ${container.src}`);
8178
- }
8179
- }
8180
- if (container instanceof CanvasElementContainer) {
8181
- this.renderReplacedElement(container, curves, container.canvas);
8182
- }
8183
- if (container instanceof SVGElementContainer) {
8184
- try {
8185
- const image = await this.context.cache.match(container.svg);
8186
- this.renderReplacedElement(container, curves, image);
8187
- }
8188
- catch (e) {
8189
- this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
8190
- }
8191
- }
8192
- if (container instanceof IFrameElementContainer && container.tree) {
8193
- const iframeRenderer = new CanvasRenderer(this.context, {
8194
- scale: this.options.scale,
8195
- backgroundColor: container.backgroundColor,
8196
- x: 0,
8197
- y: 0,
8198
- width: container.width,
8199
- height: container.height
8200
- });
8201
- const canvas = await iframeRenderer.render(container.tree);
8202
- if (container.width && container.height) {
8203
- this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
8204
- }
9197
+ async render(element) {
9198
+ if (this.options.backgroundColor) {
9199
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
9200
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
8205
9201
  }
8206
- if (container instanceof InputElementContainer) {
8207
- const size = Math.min(container.bounds.width, container.bounds.height);
8208
- if (container.type === CHECKBOX) {
8209
- if (container.checked) {
8210
- this.ctx.save();
8211
- this.path([
8212
- new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
8213
- new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
8214
- new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
8215
- new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
8216
- new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
8217
- new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
8218
- new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
8219
- ]);
8220
- this.ctx.fillStyle = asString(INPUT_COLOR);
8221
- this.ctx.fill();
8222
- this.ctx.restore();
8223
- }
8224
- }
8225
- else if (container.type === RADIO) {
8226
- if (container.checked) {
8227
- this.ctx.save();
8228
- this.ctx.beginPath();
8229
- this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
8230
- this.ctx.fillStyle = asString(INPUT_COLOR);
8231
- this.ctx.fill();
8232
- this.ctx.restore();
8233
- }
8234
- }
9202
+ const stack = parseStackingContexts(element);
9203
+ await this.renderStack(stack);
9204
+ this.effectsRenderer.applyEffects([]);
9205
+ return this.canvas;
9206
+ }
9207
+ }
9208
+ const isTextInputElement = (container) => {
9209
+ if (container instanceof TextareaElementContainer) {
9210
+ return true;
9211
+ }
9212
+ else if (container instanceof SelectElementContainer) {
9213
+ return true;
9214
+ }
9215
+ else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
9216
+ return true;
9217
+ }
9218
+ return false;
9219
+ };
9220
+ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
9221
+ switch (clip) {
9222
+ case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
9223
+ return calculateBorderBoxPath(curves);
9224
+ case 2 /* BACKGROUND_CLIP.CONTENT_BOX */:
9225
+ return calculateContentBoxPath(curves);
9226
+ case 1 /* BACKGROUND_CLIP.PADDING_BOX */:
9227
+ default:
9228
+ return calculatePaddingBoxPath(curves);
9229
+ }
9230
+ };
9231
+ const canvasTextAlign = (textAlign) => {
9232
+ switch (textAlign) {
9233
+ case 1 /* TEXT_ALIGN.CENTER */:
9234
+ return 'center';
9235
+ case 2 /* TEXT_ALIGN.RIGHT */:
9236
+ return 'right';
9237
+ case 0 /* TEXT_ALIGN.LEFT */:
9238
+ default:
9239
+ return 'left';
9240
+ }
9241
+ };
9242
+
9243
+ class ForeignObjectRenderer extends Renderer {
9244
+ constructor(context, options) {
9245
+ super(context, options);
9246
+ this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
9247
+ this.ctx = this.canvas.getContext('2d');
9248
+ this.options = options;
9249
+ this.canvas.width = Math.floor(options.width * options.scale);
9250
+ this.canvas.height = Math.floor(options.height * options.scale);
9251
+ this.canvas.style.width = `${options.width}px`;
9252
+ this.canvas.style.height = `${options.height}px`;
9253
+ this.ctx.scale(this.options.scale, this.options.scale);
9254
+ this.ctx.translate(-options.x, -options.y);
9255
+ this.context.logger.debug(`EXPERIMENTAL ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`);
9256
+ }
9257
+ async render(element) {
9258
+ const svg = createForeignObjectSVG(this.options.width * this.options.scale, this.options.height * this.options.scale, this.options.scale, this.options.scale, element);
9259
+ const img = await loadSerializedSVG(svg);
9260
+ if (this.options.backgroundColor) {
9261
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
9262
+ this.ctx.fillRect(0, 0, this.options.width * this.options.scale, this.options.height * this.options.scale);
8235
9263
  }
8236
- if (isTextInputElement(container) && container.value.length) {
8237
- const [font, fontFamily, fontSize] = this.createFontStyle(styles);
8238
- const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
8239
- this.ctx.font = font;
8240
- // Fix for Issue #92: Use placeholder color when rendering placeholder text
8241
- const isPlaceholder = container instanceof InputElementContainer && container.isPlaceholder;
8242
- this.ctx.fillStyle = isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
8243
- this.ctx.textBaseline = 'alphabetic';
8244
- this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
8245
- const bounds = contentBox(container);
8246
- let x = 0;
8247
- switch (container.styles.textAlign) {
8248
- case 1 /* TEXT_ALIGN.CENTER */:
8249
- x += bounds.width / 2;
8250
- break;
8251
- case 2 /* TEXT_ALIGN.RIGHT */:
8252
- x += bounds.width;
8253
- break;
9264
+ this.ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
9265
+ return this.canvas;
9266
+ }
9267
+ }
9268
+ const loadSerializedSVG = (svg) => new Promise((resolve, reject) => {
9269
+ const img = new Image();
9270
+ img.onload = () => {
9271
+ resolve(img);
9272
+ };
9273
+ img.onerror = reject;
9274
+ img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
9275
+ });
9276
+
9277
+ class Logger {
9278
+ constructor({ id, enabled }) {
9279
+ this.id = id;
9280
+ this.enabled = enabled;
9281
+ this.start = Date.now();
9282
+ }
9283
+ debug(...args) {
9284
+ if (this.enabled) {
9285
+ // eslint-disable-next-line no-console
9286
+ if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
9287
+ // eslint-disable-next-line no-console
9288
+ console.debug(this.id, `${this.getTime()}ms`, ...args);
8254
9289
  }
8255
- // Fix for Issue #92: Position text vertically centered in single-line input
8256
- // Only apply vertical centering for InputElementContainer, not for textarea or select
8257
- let verticalOffset = 0;
8258
- if (container instanceof InputElementContainer) {
8259
- const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
8260
- verticalOffset = (bounds.height - fontSizeValue) / 2;
9290
+ else {
9291
+ this.info(...args);
8261
9292
  }
8262
- // Create text bounds with horizontal and vertical offsets
8263
- // Height is not modified as it doesn't affect text rendering position
8264
- const textBounds = bounds.add(x, verticalOffset, 0, 0);
8265
- this.ctx.save();
8266
- this.path([
8267
- new Vector(bounds.left, bounds.top),
8268
- new Vector(bounds.left + bounds.width, bounds.top),
8269
- new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
8270
- new Vector(bounds.left, bounds.top + bounds.height)
8271
- ]);
8272
- this.ctx.clip();
8273
- this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
8274
- this.ctx.restore();
8275
- this.ctx.textBaseline = 'alphabetic';
8276
- this.ctx.textAlign = 'left';
8277
9293
  }
8278
- if (contains(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
8279
- if (container.styles.listStyleImage !== null) {
8280
- const img = container.styles.listStyleImage;
8281
- if (img.type === 0 /* CSSImageType.URL */) {
8282
- let image;
8283
- const url = img.url;
8284
- try {
8285
- image = await this.context.cache.match(url);
8286
- this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
8287
- }
8288
- catch (e) {
8289
- this.context.logger.error(`Error loading list-style-image ${url}`);
8290
- }
8291
- }
9294
+ }
9295
+ getTime() {
9296
+ return Date.now() - this.start;
9297
+ }
9298
+ info(...args) {
9299
+ if (this.enabled) {
9300
+ // eslint-disable-next-line no-console
9301
+ if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
9302
+ // eslint-disable-next-line no-console
9303
+ console.info(this.id, `${this.getTime()}ms`, ...args);
8292
9304
  }
8293
- else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
8294
- const [font] = this.createFontStyle(styles);
8295
- this.ctx.font = font;
8296
- this.ctx.fillStyle = asString(styles.color);
8297
- this.ctx.textBaseline = 'middle';
8298
- this.ctx.textAlign = 'right';
8299
- const bounds = new Bounds(container.bounds.left, container.bounds.top + getAbsoluteValue(container.styles.paddingTop, container.bounds.width), container.bounds.width, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 1);
8300
- this.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2);
8301
- this.ctx.textBaseline = 'bottom';
8302
- this.ctx.textAlign = 'left';
9305
+ }
9306
+ }
9307
+ warn(...args) {
9308
+ if (this.enabled) {
9309
+ if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
9310
+ console.warn(this.id, `${this.getTime()}ms`, ...args);
9311
+ }
9312
+ else {
9313
+ this.info(...args);
8303
9314
  }
8304
9315
  }
8305
9316
  }
8306
- async renderStackContent(stack) {
8307
- if (contains(stack.element.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
8308
- debugger;
9317
+ error(...args) {
9318
+ if (this.enabled) {
9319
+ if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
9320
+ console.error(this.id, `${this.getTime()}ms`, ...args);
9321
+ }
9322
+ else {
9323
+ this.info(...args);
9324
+ }
8309
9325
  }
8310
- // https://www.w3.org/TR/css-position-3/#painting-order
8311
- // 1. the background and borders of the element forming the stacking context.
8312
- await this.renderNodeBackgroundAndBorders(stack.element);
8313
- // 2. the child stacking contexts with negative stack levels (most negative first).
8314
- for (const child of stack.negativeZIndex) {
8315
- await this.renderStack(child);
9326
+ }
9327
+ }
9328
+ Logger.instances = {};
9329
+
9330
+ class Cache {
9331
+ constructor(context, _options) {
9332
+ this.context = context;
9333
+ this._options = _options;
9334
+ this._cache = new Map();
9335
+ this._pendingOperations = new Map();
9336
+ // Default cache size: 100 items
9337
+ this.maxSize = _options.maxCacheSize ?? 100;
9338
+ if (this.maxSize < 1) {
9339
+ throw new Error('Cache maxSize must be at least 1');
8316
9340
  }
8317
- // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
8318
- await this.renderNodeContent(stack.element);
8319
- for (const child of stack.nonInlineLevel) {
8320
- await this.renderNode(child);
9341
+ if (this.maxSize > 10000) {
9342
+ this.context.logger.warn(`Cache maxSize ${this.maxSize} is very large and may cause memory issues. ` +
9343
+ `Consider using a smaller value (recommended: 100-1000).`);
8321
9344
  }
8322
- // 4. All non-positioned floating descendants, in tree order. For each one of these,
8323
- // treat the element as if it created a new stacking context, but any positioned descendants and descendants
8324
- // which actually create a new stacking context should be considered part of the parent stacking context,
8325
- // not this new one.
8326
- for (const child of stack.nonPositionedFloats) {
8327
- await this.renderStack(child);
9345
+ }
9346
+ addImage(src) {
9347
+ // Wait for any pending operations on this key
9348
+ const pending = this._pendingOperations.get(src);
9349
+ if (pending) {
9350
+ return pending;
8328
9351
  }
8329
- // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
8330
- for (const child of stack.nonPositionedInlineLevel) {
8331
- await this.renderStack(child);
9352
+ if (this.has(src)) {
9353
+ // Update last accessed time
9354
+ const entry = this._cache.get(src);
9355
+ if (entry) {
9356
+ entry.lastAccessed = Date.now();
9357
+ }
9358
+ return Promise.resolve();
8332
9359
  }
8333
- for (const child of stack.inlineLevel) {
8334
- await this.renderNode(child);
9360
+ if (isBlobImage(src) || isRenderable(src)) {
9361
+ // Create a pending operation to ensure atomicity
9362
+ const operation = this._addImageInternal(src);
9363
+ this._pendingOperations.set(src, operation);
9364
+ operation.finally(() => {
9365
+ this._pendingOperations.delete(src);
9366
+ });
9367
+ return operation;
8335
9368
  }
8336
- // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
8337
- // All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
8338
- // For those with 'z-index: auto', treat the element as if it created a new stacking context,
8339
- // but any positioned descendants and descendants which actually create a new stacking context should be
8340
- // considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
8341
- // treat the stacking context generated atomically.
8342
- //
8343
- // All opacity descendants with opacity less than 1
8344
- //
8345
- // All transform descendants with transform other than none
8346
- for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
8347
- await this.renderStack(child);
9369
+ return Promise.resolve();
9370
+ }
9371
+ async _addImageInternal(src) {
9372
+ // Create image load promise with timeout protection
9373
+ const timeoutMs = this._options.imageTimeout ?? 15000;
9374
+ const timeoutPromise = new Promise((_, reject) => {
9375
+ setTimeout(() => {
9376
+ reject(new Error(`Image load timeout after ${timeoutMs}ms: ${src}`));
9377
+ }, timeoutMs);
9378
+ });
9379
+ // Race between image load and timeout
9380
+ const imageWithTimeout = Promise.race([this.loadImage(src), timeoutPromise]);
9381
+ // Handle errors to prevent unhandled rejections
9382
+ imageWithTimeout.catch((error) => {
9383
+ this.context.logger.error(`Failed to load image ${src}: ${error instanceof Error ? error.message : 'Unknown error'}`);
9384
+ });
9385
+ // Store the promise with timeout in cache
9386
+ this.set(src, imageWithTimeout);
9387
+ }
9388
+ match(src) {
9389
+ const entry = this._cache.get(src);
9390
+ if (entry) {
9391
+ // Update last accessed time on access
9392
+ entry.lastAccessed = Date.now();
9393
+ return entry.value;
8348
9394
  }
8349
- // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
8350
- // order (smallest first) then tree order.
8351
- for (const child of stack.positiveZIndex) {
8352
- await this.renderStack(child);
9395
+ return undefined;
9396
+ }
9397
+ /**
9398
+ * Set a value in cache with LRU eviction
9399
+ */
9400
+ set(key, value) {
9401
+ // If key already exists, update it without eviction
9402
+ if (this._cache.has(key)) {
9403
+ const entry = this._cache.get(key);
9404
+ entry.value = value;
9405
+ entry.lastAccessed = Date.now();
9406
+ return;
9407
+ }
9408
+ // For new keys, check if we need to evict
9409
+ if (this._cache.size >= this.maxSize) {
9410
+ this.evictLRU();
8353
9411
  }
9412
+ this._cache.set(key, {
9413
+ value,
9414
+ lastAccessed: Date.now()
9415
+ });
8354
9416
  }
8355
- mask(paths) {
8356
- this.ctx.beginPath();
8357
- this.ctx.moveTo(0, 0);
8358
- // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
8359
- // because context has already been scaled by this.options.scale
8360
- // Fix for Issue #126: Using canvas pixel dimensions causes broken output
8361
- this.ctx.lineTo(this.options.width, 0);
8362
- this.ctx.lineTo(this.options.width, this.options.height);
8363
- this.ctx.lineTo(0, this.options.height);
8364
- this.ctx.lineTo(0, 0);
8365
- this.formatPath(paths.slice(0).reverse());
8366
- this.ctx.closePath();
9417
+ /**
9418
+ * Evict least recently used entry
9419
+ */
9420
+ evictLRU() {
9421
+ let oldestKey = null;
9422
+ let oldestTime = Infinity;
9423
+ for (const [key, entry] of this._cache.entries()) {
9424
+ if (entry.lastAccessed < oldestTime) {
9425
+ oldestTime = entry.lastAccessed;
9426
+ oldestKey = key;
9427
+ }
9428
+ }
9429
+ if (oldestKey) {
9430
+ this._cache.delete(oldestKey);
9431
+ this.context.logger.debug(`Cache: Evicted LRU entry: ${oldestKey}`);
9432
+ }
8367
9433
  }
8368
- path(paths) {
8369
- this.ctx.beginPath();
8370
- this.formatPath(paths);
8371
- this.ctx.closePath();
9434
+ /**
9435
+ * Get cache size
9436
+ */
9437
+ size() {
9438
+ return this._cache.size;
8372
9439
  }
8373
- formatPath(paths) {
8374
- paths.forEach((point, index) => {
8375
- const start = isBezierCurve(point) ? point.start : point;
8376
- if (index === 0) {
8377
- this.ctx.moveTo(start.x, start.y);
9440
+ /**
9441
+ * Get max cache size
9442
+ */
9443
+ getMaxSize() {
9444
+ return this.maxSize;
9445
+ }
9446
+ /**
9447
+ * Clear all cache entries
9448
+ */
9449
+ clear() {
9450
+ this._cache.clear();
9451
+ }
9452
+ async loadImage(key) {
9453
+ const originChecker = this.context.originChecker;
9454
+ const defaultIsSameOrigin = (src) => originChecker.isSameOrigin(src);
9455
+ const isSameOrigin = typeof this._options.customIsSameOrigin === 'function'
9456
+ ? await this._options.customIsSameOrigin(key, defaultIsSameOrigin)
9457
+ : defaultIsSameOrigin(key);
9458
+ const useCORS = !isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
9459
+ const useProxy = !isInlineImage(key) &&
9460
+ !isSameOrigin &&
9461
+ !isBlobImage(key) &&
9462
+ typeof this._options.proxy === 'string' &&
9463
+ FEATURES.SUPPORT_CORS_XHR &&
9464
+ !useCORS;
9465
+ if (!isSameOrigin &&
9466
+ this._options.allowTaint === false &&
9467
+ !isInlineImage(key) &&
9468
+ !isBlobImage(key) &&
9469
+ !useProxy &&
9470
+ !useCORS) {
9471
+ return;
9472
+ }
9473
+ let src = key;
9474
+ if (useProxy) {
9475
+ src = await this.proxy(src);
9476
+ }
9477
+ this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
9478
+ return await new Promise((resolve, reject) => {
9479
+ const img = new Image();
9480
+ img.onload = () => resolve(img);
9481
+ img.onerror = reject;
9482
+ //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
9483
+ if (isInlineBase64Image(src) || useCORS) {
9484
+ img.crossOrigin = 'anonymous';
8378
9485
  }
8379
- else {
8380
- this.ctx.lineTo(start.x, start.y);
9486
+ img.src = src;
9487
+ if (img.complete === true) {
9488
+ // Inline XML images may fail to parse, throwing an Error later on
9489
+ setTimeout(() => resolve(img), 500);
8381
9490
  }
8382
- if (isBezierCurve(point)) {
8383
- this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
9491
+ if (this._options.imageTimeout > 0) {
9492
+ setTimeout(() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout);
9493
+ }
9494
+ });
9495
+ }
9496
+ has(key) {
9497
+ return this._cache.has(key);
9498
+ }
9499
+ keys() {
9500
+ return Promise.resolve(Object.keys(this._cache));
9501
+ }
9502
+ proxy(src) {
9503
+ const proxy = this._options.proxy;
9504
+ if (!proxy) {
9505
+ throw new Error('No proxy defined');
9506
+ }
9507
+ const key = src.substring(0, 256);
9508
+ return new Promise((resolve, reject) => {
9509
+ const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
9510
+ const xhr = new XMLHttpRequest();
9511
+ xhr.onload = () => {
9512
+ if (xhr.status === 200) {
9513
+ if (responseType === 'text') {
9514
+ resolve(xhr.response);
9515
+ }
9516
+ else {
9517
+ const reader = new FileReader();
9518
+ reader.addEventListener('load', () => resolve(reader.result), false);
9519
+ reader.addEventListener('error', (e) => reject(e), false);
9520
+ reader.readAsDataURL(xhr.response);
9521
+ }
9522
+ }
9523
+ else {
9524
+ reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
9525
+ }
9526
+ };
9527
+ xhr.onerror = reject;
9528
+ const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
9529
+ xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
9530
+ if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
9531
+ xhr.responseType = responseType;
9532
+ }
9533
+ if (this._options.imageTimeout) {
9534
+ const timeout = this._options.imageTimeout;
9535
+ xhr.timeout = timeout;
9536
+ xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
8384
9537
  }
9538
+ xhr.send();
9539
+ });
9540
+ }
9541
+ }
9542
+ const INLINE_SVG = /^data:image\/svg\+xml/i;
9543
+ const INLINE_BASE64 = /^data:image\/.*;base64,/i;
9544
+ const INLINE_IMG = /^data:image\/.*/i;
9545
+ const isRenderable = (src) => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
9546
+ const isInlineImage = (src) => INLINE_IMG.test(src);
9547
+ const isInlineBase64Image = (src) => INLINE_BASE64.test(src);
9548
+ const isBlobImage = (src) => src.substr(0, 4) === 'blob';
9549
+ const isSVG = (src) => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
9550
+
9551
+ /**
9552
+ * Origin Checker
9553
+ *
9554
+ * Provides origin checking functionality without global static state.
9555
+ * Each instance maintains its own anchor element and origin reference.
9556
+ *
9557
+ * Replaces the static methods in CacheStorage with instance-based approach.
9558
+ */
9559
+ class OriginChecker {
9560
+ constructor(window) {
9561
+ if (!window || !window.document) {
9562
+ throw new Error('Valid window object required for OriginChecker');
9563
+ }
9564
+ if (!window.location || !window.location.href) {
9565
+ throw new Error('Window object must have valid location');
9566
+ }
9567
+ this.link = window.document.createElement('a');
9568
+ this.origin = this.getOrigin(window.location.href);
9569
+ }
9570
+ /**
9571
+ * Get the origin (protocol + hostname + port) of a URL
9572
+ *
9573
+ * @param url - URL to parse
9574
+ * @returns Origin string (e.g., "https://example.com:8080")
9575
+ */
9576
+ getOrigin(url) {
9577
+ this.link.href = url;
9578
+ // IE9 hack: accessing href twice to ensure it's properly parsed
9579
+ this.link.href = this.link.href;
9580
+ return this.link.protocol + this.link.hostname + this.link.port;
9581
+ }
9582
+ /**
9583
+ * Check if a URL is from the same origin as the context
9584
+ *
9585
+ * @param src - URL to check
9586
+ * @returns true if same origin, false otherwise
9587
+ */
9588
+ isSameOrigin(src) {
9589
+ return this.getOrigin(src) === this.origin;
9590
+ }
9591
+ /**
9592
+ * Get the current context origin
9593
+ *
9594
+ * @returns The origin of the context window
9595
+ */
9596
+ getContextOrigin() {
9597
+ return this.origin;
9598
+ }
9599
+ }
9600
+
9601
+ class Context {
9602
+ constructor(options, windowBounds, config) {
9603
+ this.windowBounds = windowBounds;
9604
+ this.instanceName = `#${Context.instanceCount++}`;
9605
+ this.config = config;
9606
+ this.logger = new Logger({ id: this.instanceName, enabled: options.logging });
9607
+ this.originChecker = new OriginChecker(config.window);
9608
+ this.cache = options.cache ?? config.cache ?? new Cache(this, options);
9609
+ }
9610
+ }
9611
+ Context.instanceCount = 1;
9612
+
9613
+ /**
9614
+ * Html2Canvas Configuration
9615
+ *
9616
+ * Manages configuration state for html2canvas rendering.
9617
+ * Eliminates the need for global static variables.
9618
+ */
9619
+ class Html2CanvasConfig {
9620
+ constructor(options = {}) {
9621
+ // Try to get window from options first, then fall back to global window
9622
+ this.window = options.window || (typeof window !== 'undefined' ? window : null);
9623
+ if (!this.window) {
9624
+ throw new Error('Window object is required but not available');
9625
+ }
9626
+ this.cspNonce = options.cspNonce;
9627
+ this.cache = options.cache;
9628
+ }
9629
+ /**
9630
+ * Create configuration from an element
9631
+ * Extracts window from element's owner document
9632
+ */
9633
+ static fromElement(element, options = {}) {
9634
+ const ownerDocument = element.ownerDocument;
9635
+ if (!ownerDocument) {
9636
+ throw new Error('Element is not attached to a document');
9637
+ }
9638
+ const defaultView = ownerDocument.defaultView;
9639
+ if (!defaultView) {
9640
+ throw new Error('Document is not attached to a window');
9641
+ }
9642
+ return new Html2CanvasConfig({
9643
+ window: defaultView,
9644
+ ...options
8385
9645
  });
8386
9646
  }
8387
- renderRepeat(path, pattern, offsetX, offsetY) {
8388
- this.path(path);
8389
- this.ctx.fillStyle = pattern;
8390
- this.ctx.translate(offsetX, offsetY);
8391
- this.ctx.fill();
8392
- this.ctx.translate(-offsetX, -offsetY);
9647
+ /**
9648
+ * Clone configuration with override options
9649
+ */
9650
+ clone(options = {}) {
9651
+ return new Html2CanvasConfig({
9652
+ window: options.window || this.window,
9653
+ cspNonce: options.cspNonce ?? this.cspNonce,
9654
+ cache: options.cache ?? this.cache
9655
+ });
8393
9656
  }
8394
- resizeImage(image, width, height) {
8395
- // https://github.com/niklasvh/html2canvas/pull/2911
8396
- // if (image.width === width && image.height === height) {
8397
- // return image;
8398
- // }
8399
- const ownerDocument = this.canvas.ownerDocument ?? document;
8400
- const canvas = ownerDocument.createElement('canvas');
8401
- canvas.width = Math.max(1, width);
8402
- canvas.height = Math.max(1, height);
8403
- const ctx = canvas.getContext('2d');
8404
- ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
8405
- return canvas;
9657
+ }
9658
+ /**
9659
+ * Set default configuration
9660
+ * @deprecated Pass configuration directly to html2canvas instead
9661
+ */
9662
+ function setDefaultConfig(config) {
9663
+ console.warn('[html2canvas-pro] setDefaultConfig is deprecated. Pass configuration to html2canvas directly.');
9664
+ }
9665
+
9666
+ /**
9667
+ * Input Validator
9668
+ *
9669
+ * Provides validation and sanitization for user inputs to prevent security vulnerabilities
9670
+ * including SSRF, XSS, and injection attacks.
9671
+ */
9672
+ /**
9673
+ * Input Validator
9674
+ *
9675
+ * Validates and sanitizes user inputs for security and correctness.
9676
+ */
9677
+ class Validator {
9678
+ constructor(config = {}) {
9679
+ this.config = {
9680
+ maxImageTimeout: 300000, // 5 minutes default
9681
+ allowDataUrls: true,
9682
+ ...config
9683
+ };
8406
9684
  }
8407
- async renderBackgroundImage(container) {
8408
- let index = container.styles.backgroundImage.length - 1;
8409
- for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
8410
- if (backgroundImage.type === 0 /* CSSImageType.URL */) {
8411
- let image;
8412
- const url = backgroundImage.url;
8413
- try {
8414
- image = await this.context.cache.match(url);
8415
- }
8416
- catch (e) {
8417
- this.context.logger.error(`Error loading background-image ${url}`);
8418
- }
8419
- if (image) {
8420
- const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
8421
- const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
8422
- const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
8423
- imageWidth,
8424
- imageHeight,
8425
- imageWidth / imageHeight
8426
- ]);
8427
- const pattern = this.ctx.createPattern(this.resizeImage(image, width, height), 'repeat');
8428
- this.renderRepeat(path, pattern, x, y);
8429
- }
9685
+ /**
9686
+ * Validate a URL
9687
+ *
9688
+ * @param url - URL to validate
9689
+ * @param context - Context for validation (e.g., 'proxy', 'image')
9690
+ * @returns Validation result
9691
+ */
9692
+ validateUrl(url, context = 'general') {
9693
+ if (!url || typeof url !== 'string') {
9694
+ return {
9695
+ valid: false,
9696
+ error: 'URL must be a non-empty string'
9697
+ };
9698
+ }
9699
+ // Check for data URLs
9700
+ if (url.startsWith('data:')) {
9701
+ if (!this.config.allowDataUrls) {
9702
+ return {
9703
+ valid: false,
9704
+ error: 'Data URLs are not allowed'
9705
+ };
8430
9706
  }
8431
- else if (isLinearGradient(backgroundImage)) {
8432
- const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
8433
- const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
8434
- const canvas = document.createElement('canvas');
8435
- canvas.width = width;
8436
- canvas.height = height;
8437
- const ctx = canvas.getContext('2d');
8438
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
8439
- processColorStops(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, asString(colorStop.color)));
8440
- ctx.fillStyle = gradient;
8441
- ctx.fillRect(0, 0, width, height);
8442
- if (width > 0 && height > 0) {
8443
- const pattern = this.ctx.createPattern(canvas, 'repeat');
8444
- this.renderRepeat(path, pattern, x, y);
9707
+ return { valid: true, sanitized: url };
9708
+ }
9709
+ // Check for blob URLs
9710
+ if (url.startsWith('blob:')) {
9711
+ return { valid: true, sanitized: url };
9712
+ }
9713
+ // Validate URL format
9714
+ try {
9715
+ const parsedUrl = new URL(url);
9716
+ // Only allow http and https protocols
9717
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
9718
+ return {
9719
+ valid: false,
9720
+ error: `Protocol ${parsedUrl.protocol} is not allowed. Only http and https are permitted.`
9721
+ };
9722
+ }
9723
+ // For proxy URLs, check domain whitelist
9724
+ if (context === 'proxy' && this.config.allowedProxyDomains && this.config.allowedProxyDomains.length > 0) {
9725
+ const hostname = parsedUrl.hostname.toLowerCase();
9726
+ const isAllowed = this.config.allowedProxyDomains.some((domain) => {
9727
+ const normalizedDomain = domain.toLowerCase();
9728
+ return hostname === normalizedDomain || hostname.endsWith('.' + normalizedDomain);
9729
+ });
9730
+ if (!isAllowed) {
9731
+ return {
9732
+ valid: false,
9733
+ error: `Proxy domain ${parsedUrl.hostname} is not in the allowed list`
9734
+ };
8445
9735
  }
8446
9736
  }
8447
- else if (isRadialGradient(backgroundImage)) {
8448
- const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
8449
- null,
8450
- null,
8451
- null
8452
- ]);
8453
- const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
8454
- const x = getAbsoluteValue(position[0], width);
8455
- const y = getAbsoluteValue(position[position.length - 1], height);
8456
- let [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
8457
- // Handle edge case where radial gradient size is 0
8458
- // Use a minimum value of 0.01 to ensure gradient is still rendered
8459
- if (rx === 0 || ry === 0) {
8460
- rx = Math.max(rx, 0.01);
8461
- ry = Math.max(ry, 0.01);
8462
- }
8463
- if (rx > 0 && ry > 0) {
8464
- const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
8465
- processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)));
8466
- this.path(path);
8467
- this.ctx.fillStyle = radialGradient;
8468
- if (rx !== ry) {
8469
- // transforms for elliptical radial gradient
8470
- const midX = container.bounds.left + 0.5 * container.bounds.width;
8471
- const midY = container.bounds.top + 0.5 * container.bounds.height;
8472
- const f = ry / rx;
8473
- const invF = 1 / f;
8474
- this.ctx.save();
8475
- this.ctx.translate(midX, midY);
8476
- this.ctx.transform(1, 0, 0, f, 0, 0);
8477
- this.ctx.translate(-midX, -midY);
8478
- this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
8479
- this.ctx.restore();
9737
+ // Check for localhost/private IPs to prevent SSRF (skip when allowLocalhostProxy for dev/test)
9738
+ if (context === 'proxy') {
9739
+ if (!this.config.allowLocalhostProxy) {
9740
+ const hostname = parsedUrl.hostname.toLowerCase();
9741
+ // Check for localhost
9742
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
9743
+ return {
9744
+ valid: false,
9745
+ error: 'Localhost is not allowed for proxy URLs'
9746
+ };
8480
9747
  }
8481
- else {
8482
- this.ctx.fill();
9748
+ // For private IP ranges (simplified check)
9749
+ if (this.isPrivateIP(hostname)) {
9750
+ return {
9751
+ valid: false,
9752
+ error: 'Private IP addresses are not allowed for proxy URLs'
9753
+ };
9754
+ }
9755
+ // For link-local addresses
9756
+ if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) {
9757
+ return {
9758
+ valid: false,
9759
+ error: 'Link-local addresses are not allowed for proxy URLs'
9760
+ };
8483
9761
  }
8484
9762
  }
9763
+ // For proxy URLs, mark that runtime validation is recommended
9764
+ // to prevent DNS rebinding attacks
9765
+ return {
9766
+ valid: true,
9767
+ sanitized: url,
9768
+ requiresRuntimeCheck: true
9769
+ };
8485
9770
  }
8486
- index--;
9771
+ return { valid: true, sanitized: url };
8487
9772
  }
8488
- }
8489
- async renderSolidBorder(color, side, curvePoints) {
8490
- this.path(parsePathForBorder(curvePoints, side));
8491
- this.ctx.fillStyle = asString(color);
8492
- this.ctx.fill();
8493
- }
8494
- async renderDoubleBorder(color, width, side, curvePoints) {
8495
- if (width < 3) {
8496
- await this.renderSolidBorder(color, side, curvePoints);
8497
- return;
9773
+ catch (e) {
9774
+ return {
9775
+ valid: false,
9776
+ error: `Invalid URL format: ${e instanceof Error ? e.message : 'Unknown error'}`
9777
+ };
8498
9778
  }
8499
- const outerPaths = parsePathForBorderDoubleOuter(curvePoints, side);
8500
- this.path(outerPaths);
8501
- this.ctx.fillStyle = asString(color);
8502
- this.ctx.fill();
8503
- const innerPaths = parsePathForBorderDoubleInner(curvePoints, side);
8504
- this.path(innerPaths);
8505
- this.ctx.fill();
8506
9779
  }
8507
- async renderNodeBackgroundAndBorders(paint) {
8508
- this.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
8509
- const styles = paint.container.styles;
8510
- const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
8511
- const borders = [
8512
- { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
8513
- { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
8514
- { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
8515
- { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
9780
+ /**
9781
+ * Check if a hostname is a private IP address
9782
+ */
9783
+ isPrivateIP(hostname) {
9784
+ // IPv4 private ranges
9785
+ const privateIPv4Patterns = [
9786
+ /^0\./, // 0.0.0.0/8 (This network)
9787
+ /^10\./, // 10.0.0.0/8 (Private)
9788
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
9789
+ /^127\./, // 127.0.0.0/8 (Loopback)
9790
+ /^169\.254\./, // 169.254.0.0/16 (Link-local)
9791
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (Private)
9792
+ /^192\.0\.0\./, // 192.0.0.0/24 (IETF Protocol Assignments)
9793
+ /^192\.0\.2\./, // 192.0.2.0/24 (TEST-NET-1)
9794
+ /^192\.168\./, // 192.168.0.0/16 (Private)
9795
+ /^198\.(1[8-9])\./, // 198.18.0.0/15 (Network benchmark)
9796
+ /^198\.51\.100\./, // 198.51.100.0/24 (TEST-NET-2)
9797
+ /^203\.0\.113\./, // 203.0.113.0/24 (TEST-NET-3)
9798
+ /^2(2[4-9]|3[0-9])\./, // 224.0.0.0/4 (Multicast)
9799
+ /^24[0-9]\./, // 240.0.0.0/4 (Reserved)
9800
+ /^255\.255\.255\.255$/ // 255.255.255.255/32 (Broadcast)
8516
9801
  ];
8517
- const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
8518
- if (hasBackground || styles.boxShadow.length) {
8519
- this.ctx.save();
8520
- this.path(backgroundPaintingArea);
8521
- this.ctx.clip();
8522
- if (!isTransparent(styles.backgroundColor)) {
8523
- this.ctx.fillStyle = asString(styles.backgroundColor);
8524
- this.ctx.fill();
9802
+ // Check IPv4
9803
+ if (privateIPv4Patterns.some((pattern) => pattern.test(hostname))) {
9804
+ return true;
9805
+ }
9806
+ // IPv6 private ranges and special addresses
9807
+ if (hostname.includes(':')) {
9808
+ return this.isPrivateIPv6(hostname);
9809
+ }
9810
+ return false;
9811
+ }
9812
+ /**
9813
+ * Check if an IPv6 address is private or special
9814
+ * Handles compressed IPv6 addresses (e.g., ::1, fc00::1)
9815
+ */
9816
+ isPrivateIPv6(hostname) {
9817
+ const normalizedHost = hostname.toLowerCase().trim();
9818
+ // Remove square brackets if present (e.g., [::1])
9819
+ const addr = normalizedHost.replace(/^\[|\]$/g, '');
9820
+ // Remove zone ID if present (e.g., fe80::1%eth0)
9821
+ const addrWithoutZone = addr.split('%')[0];
9822
+ // Loopback ::1 (also matches 0:0:0:0:0:0:0:1)
9823
+ if (/^(0:){7}1$/.test(addrWithoutZone) || addrWithoutZone === '::1') {
9824
+ return true;
9825
+ }
9826
+ // Unspecified address :: (also matches 0:0:0:0:0:0:0:0)
9827
+ if (/^(0:){7}0$/.test(addrWithoutZone) || addrWithoutZone === '::') {
9828
+ return true;
9829
+ }
9830
+ // Expand :: compression to check prefixes
9831
+ // This handles cases like fc00::1, fe80::, etc.
9832
+ const expandedAddr = this.expandIPv6(addrWithoutZone);
9833
+ if (!expandedAddr) {
9834
+ // If we can't expand it, fall back to prefix matching
9835
+ return this.isPrivateIPv6Prefix(addrWithoutZone);
9836
+ }
9837
+ // fc00::/7 (Unique Local Address)
9838
+ // Check if first byte is in range fc00-fdff
9839
+ const firstByte = parseInt(expandedAddr.substring(0, 2), 16);
9840
+ if (firstByte >= 0xfc && firstByte <= 0xfd) {
9841
+ return true;
9842
+ }
9843
+ // fe80::/10 (Link-local)
9844
+ // First 10 bits should be 1111 1110 10
9845
+ if (firstByte === 0xfe) {
9846
+ const secondByte = parseInt(expandedAddr.substring(2, 4), 16);
9847
+ // Check if bits 11-12 are 10 (0x80-0xbf)
9848
+ if (secondByte >= 0x80 && secondByte <= 0xbf) {
9849
+ return true;
8525
9850
  }
8526
- await this.renderBackgroundImage(paint.container);
8527
- this.ctx.restore();
8528
- styles.boxShadow
8529
- .slice(0)
8530
- .reverse()
8531
- .forEach((shadow) => {
8532
- this.ctx.save();
8533
- const borderBoxArea = calculateBorderBoxPath(paint.curves);
8534
- const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
8535
- const shadowPaintingArea = transformPath(borderBoxArea, -maskOffset + (shadow.inset ? 1 : -1) * shadow.spread.number, (shadow.inset ? 1 : -1) * shadow.spread.number, shadow.spread.number * (shadow.inset ? -2 : 2), shadow.spread.number * (shadow.inset ? -2 : 2));
8536
- if (shadow.inset) {
8537
- this.path(borderBoxArea);
8538
- this.ctx.clip();
8539
- this.mask(shadowPaintingArea);
8540
- }
8541
- else {
8542
- this.mask(borderBoxArea);
8543
- this.ctx.clip();
8544
- this.path(shadowPaintingArea);
8545
- }
8546
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
8547
- this.ctx.shadowOffsetY = shadow.offsetY.number;
8548
- this.ctx.shadowColor = asString(shadow.color);
8549
- this.ctx.shadowBlur = shadow.blur.number;
8550
- this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
8551
- this.ctx.fill();
8552
- this.ctx.restore();
8553
- });
8554
9851
  }
8555
- let side = 0;
8556
- for (const border of borders) {
8557
- if (border.style !== 0 /* BORDER_STYLE.NONE */ && !isTransparent(border.color) && border.width > 0) {
8558
- if (border.style === 2 /* BORDER_STYLE.DASHED */) {
8559
- await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
8560
- }
8561
- else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
8562
- await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
9852
+ // ff00::/8 (Multicast)
9853
+ if (firstByte === 0xff) {
9854
+ return true;
9855
+ }
9856
+ return false;
9857
+ }
9858
+ /**
9859
+ * Expand compressed IPv6 address to full form
9860
+ * e.g., "::1" -> "0000:0000:0000:0000:0000:0000:0000:0001"
9861
+ */
9862
+ expandIPv6(addr) {
9863
+ try {
9864
+ // Handle :: compression
9865
+ if (addr.includes('::')) {
9866
+ const parts = addr.split('::');
9867
+ if (parts.length > 2) {
9868
+ return null; // Invalid: more than one ::
8563
9869
  }
8564
- else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
8565
- await this.renderDoubleBorder(border.color, border.width, side, paint.curves);
9870
+ const leftParts = parts[0] ? parts[0].split(':') : [];
9871
+ const rightParts = parts[1] ? parts[1].split(':') : [];
9872
+ const missingParts = 8 - leftParts.length - rightParts.length;
9873
+ if (missingParts < 0) {
9874
+ return null; // Invalid
8566
9875
  }
8567
- else {
8568
- await this.renderSolidBorder(border.color, side, paint.curves);
9876
+ const middleParts = Array(missingParts).fill('0000');
9877
+ const allParts = [...leftParts, ...middleParts, ...rightParts];
9878
+ return allParts.map((p) => p.padStart(4, '0')).join(':');
9879
+ }
9880
+ else {
9881
+ // No compression, just normalize
9882
+ const parts = addr.split(':');
9883
+ if (parts.length !== 8) {
9884
+ return null; // Invalid
8569
9885
  }
9886
+ return parts.map((p) => p.padStart(4, '0')).join(':');
8570
9887
  }
8571
- side++;
9888
+ }
9889
+ catch {
9890
+ return null;
8572
9891
  }
8573
9892
  }
8574
- async renderDashedDottedBorder(color, width, side, curvePoints, style) {
8575
- this.ctx.save();
8576
- const strokePaths = parsePathForBorderStroke(curvePoints, side);
8577
- const boxPaths = parsePathForBorder(curvePoints, side);
8578
- if (style === 2 /* BORDER_STYLE.DASHED */) {
8579
- this.path(boxPaths);
8580
- this.ctx.clip();
9893
+ /**
9894
+ * Fallback prefix matching for IPv6 when expansion fails
9895
+ */
9896
+ isPrivateIPv6Prefix(addr) {
9897
+ // fc00::/7 (Unique Local Address)
9898
+ if (/^fc[0-9a-f]{0,2}:?/i.test(addr) || /^fd[0-9a-f]{0,2}:?/i.test(addr)) {
9899
+ return true;
8581
9900
  }
8582
- let startX, startY, endX, endY;
8583
- if (isBezierCurve(boxPaths[0])) {
8584
- startX = boxPaths[0].start.x;
8585
- startY = boxPaths[0].start.y;
9901
+ // fe80::/10 (Link-local)
9902
+ if (/^fe[89ab][0-9a-f]:?/i.test(addr)) {
9903
+ return true;
8586
9904
  }
8587
- else {
8588
- startX = boxPaths[0].x;
8589
- startY = boxPaths[0].y;
9905
+ // ff00::/8 (Multicast)
9906
+ if (/^ff[0-9a-f]{0,2}:?/i.test(addr)) {
9907
+ return true;
8590
9908
  }
8591
- if (isBezierCurve(boxPaths[1])) {
8592
- endX = boxPaths[1].end.x;
8593
- endY = boxPaths[1].end.y;
9909
+ return false;
9910
+ }
9911
+ /**
9912
+ * Validate CSP nonce
9913
+ *
9914
+ * @param nonce - CSP nonce to validate
9915
+ * @returns Validation result
9916
+ */
9917
+ validateCspNonce(nonce) {
9918
+ if (!nonce || typeof nonce !== 'string') {
9919
+ return {
9920
+ valid: false,
9921
+ error: 'CSP nonce must be a non-empty string'
9922
+ };
8594
9923
  }
8595
- else {
8596
- endX = boxPaths[1].x;
8597
- endY = boxPaths[1].y;
9924
+ // Basic format validation - nonce should be base64-like
9925
+ // Typical format: base64 string, often 32+ characters
9926
+ if (nonce.length < 16) {
9927
+ return {
9928
+ valid: false,
9929
+ error: 'CSP nonce is too short (minimum 16 characters recommended)'
9930
+ };
8598
9931
  }
8599
- let length;
8600
- if (side === 0 || side === 2) {
8601
- length = Math.abs(startX - endX);
9932
+ // Check for suspicious characters
9933
+ if (!/^[A-Za-z0-9+/=_-]+$/.test(nonce)) {
9934
+ return {
9935
+ valid: false,
9936
+ error: 'CSP nonce contains invalid characters'
9937
+ };
8602
9938
  }
8603
- else {
8604
- length = Math.abs(startY - endY);
9939
+ return { valid: true, sanitized: nonce };
9940
+ }
9941
+ /**
9942
+ * Validate image timeout
9943
+ *
9944
+ * @param timeout - Timeout in milliseconds
9945
+ * @returns Validation result
9946
+ */
9947
+ validateImageTimeout(timeout) {
9948
+ if (typeof timeout !== 'number' || isNaN(timeout)) {
9949
+ return {
9950
+ valid: false,
9951
+ error: 'Image timeout must be a number'
9952
+ };
8605
9953
  }
8606
- this.ctx.beginPath();
8607
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8608
- this.formatPath(strokePaths);
9954
+ if (timeout < 0) {
9955
+ return {
9956
+ valid: false,
9957
+ error: 'Image timeout cannot be negative'
9958
+ };
8609
9959
  }
8610
- else {
8611
- this.formatPath(boxPaths.slice(0, 2));
9960
+ if (this.config.maxImageTimeout && timeout > this.config.maxImageTimeout) {
9961
+ return {
9962
+ valid: false,
9963
+ error: `Image timeout ${timeout}ms exceeds maximum allowed ${this.config.maxImageTimeout}ms`
9964
+ };
8612
9965
  }
8613
- let dashLength = width < 3 ? width * 3 : width * 2;
8614
- let spaceLength = width < 3 ? width * 2 : width;
8615
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8616
- dashLength = width;
8617
- spaceLength = width;
9966
+ return { valid: true, sanitized: timeout };
9967
+ }
9968
+ /**
9969
+ * Validate window dimensions
9970
+ *
9971
+ * @param width - Window width
9972
+ * @param height - Window height
9973
+ * @returns Validation result
9974
+ */
9975
+ validateDimensions(width, height) {
9976
+ if (typeof width !== 'number' || typeof height !== 'number') {
9977
+ return {
9978
+ valid: false,
9979
+ error: 'Dimensions must be numbers'
9980
+ };
8618
9981
  }
8619
- let useLineDash = true;
8620
- if (length <= dashLength * 2) {
8621
- useLineDash = false;
9982
+ if (isNaN(width) || isNaN(height)) {
9983
+ return {
9984
+ valid: false,
9985
+ error: 'Dimensions cannot be NaN'
9986
+ };
8622
9987
  }
8623
- else if (length <= dashLength * 2 + spaceLength) {
8624
- const multiplier = length / (2 * dashLength + spaceLength);
8625
- dashLength *= multiplier;
8626
- spaceLength *= multiplier;
9988
+ if (width <= 0 || height <= 0) {
9989
+ return {
9990
+ valid: false,
9991
+ error: 'Dimensions must be positive'
9992
+ };
9993
+ }
9994
+ // Reasonable maximum to prevent memory issues
9995
+ const MAX_DIMENSION = 32767; // Common canvas limit
9996
+ if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
9997
+ return {
9998
+ valid: false,
9999
+ error: `Dimensions exceed maximum allowed (${MAX_DIMENSION}px)`
10000
+ };
10001
+ }
10002
+ return { valid: true, sanitized: { width, height } };
10003
+ }
10004
+ /**
10005
+ * Validate scale factor
10006
+ *
10007
+ * @param scale - Scale factor
10008
+ * @returns Validation result
10009
+ */
10010
+ validateScale(scale) {
10011
+ if (typeof scale !== 'number' || isNaN(scale)) {
10012
+ return {
10013
+ valid: false,
10014
+ error: 'Scale must be a number'
10015
+ };
10016
+ }
10017
+ if (scale <= 0) {
10018
+ return {
10019
+ valid: false,
10020
+ error: 'Scale must be positive'
10021
+ };
10022
+ }
10023
+ // Reasonable scale limits
10024
+ if (scale > 10) {
10025
+ return {
10026
+ valid: false,
10027
+ error: 'Scale factor too large (maximum 10x)'
10028
+ };
10029
+ }
10030
+ return { valid: true, sanitized: scale };
10031
+ }
10032
+ /**
10033
+ * Validate HTML element
10034
+ *
10035
+ * @param element - Element to validate
10036
+ * @returns Validation result
10037
+ */
10038
+ validateElement(element) {
10039
+ if (!element) {
10040
+ return {
10041
+ valid: false,
10042
+ error: 'Element is required'
10043
+ };
10044
+ }
10045
+ if (typeof element !== 'object') {
10046
+ return {
10047
+ valid: false,
10048
+ error: 'Element must be an object'
10049
+ };
10050
+ }
10051
+ // Accept real HTMLElement, or any element-like object with the minimal shape
10052
+ // required by the implementation (ownerDocument + defaultView) for backward
10053
+ // compatibility and test environments.
10054
+ if (typeof HTMLElement !== 'undefined' && element instanceof HTMLElement) {
10055
+ // Real DOM element
10056
+ if (!element.ownerDocument) {
10057
+ return { valid: false, error: 'Element must be attached to a document' };
10058
+ }
10059
+ return { valid: true };
10060
+ }
10061
+ // Duck-typing: accept object with ownerDocument and defaultView (minimal contract)
10062
+ if (!element.ownerDocument) {
10063
+ return {
10064
+ valid: false,
10065
+ error: 'Element must be attached to a document (ownerDocument required)'
10066
+ };
8627
10067
  }
8628
- else {
8629
- const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
8630
- const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
8631
- const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
8632
- spaceLength =
8633
- maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
8634
- ? minSpace
8635
- : maxSpace;
10068
+ if (!element.ownerDocument.defaultView) {
10069
+ return {
10070
+ valid: false,
10071
+ error: 'Document must be attached to a window (ownerDocument.defaultView required)'
10072
+ };
8636
10073
  }
8637
- if (useLineDash) {
8638
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8639
- this.ctx.setLineDash([0, dashLength + spaceLength]);
10074
+ return { valid: true };
10075
+ }
10076
+ /**
10077
+ * Validate entire options object
10078
+ *
10079
+ * @param options - Options to validate
10080
+ * @returns Validation result with all errors
10081
+ */
10082
+ validateOptions(options) {
10083
+ const errors = [];
10084
+ // Validate proxy URL only when a non-empty string (allow null/undefined to mean "no proxy")
10085
+ const proxyUrl = options.proxy;
10086
+ if (proxyUrl !== undefined && proxyUrl !== null && typeof proxyUrl === 'string' && proxyUrl.length > 0) {
10087
+ const proxyResult = this.validateUrl(proxyUrl, 'proxy');
10088
+ if (!proxyResult.valid) {
10089
+ errors.push(`Proxy: ${proxyResult.error}`);
8640
10090
  }
8641
- else {
8642
- this.ctx.setLineDash([dashLength, spaceLength]);
10091
+ // Note: Proxy URLs are marked with requiresRuntimeCheck to prevent DNS rebinding
10092
+ // Consider implementing runtime IP validation in production environments
10093
+ }
10094
+ // Validate image timeout
10095
+ if (options.imageTimeout !== undefined) {
10096
+ const timeoutResult = this.validateImageTimeout(options.imageTimeout);
10097
+ if (!timeoutResult.valid) {
10098
+ errors.push(`Image timeout: ${timeoutResult.error}`);
8643
10099
  }
8644
10100
  }
8645
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8646
- this.ctx.lineCap = 'round';
8647
- this.ctx.lineWidth = width;
10101
+ // Validate dimensions
10102
+ if (options.width !== undefined || options.height !== undefined) {
10103
+ const width = options.width ?? 800;
10104
+ const height = options.height ?? 600;
10105
+ const dimensionsResult = this.validateDimensions(width, height);
10106
+ if (!dimensionsResult.valid) {
10107
+ errors.push(`Dimensions: ${dimensionsResult.error}`);
10108
+ }
8648
10109
  }
8649
- else {
8650
- this.ctx.lineWidth = width * 2 + 1.1;
10110
+ // Validate scale
10111
+ if (options.scale !== undefined) {
10112
+ const scaleResult = this.validateScale(options.scale);
10113
+ if (!scaleResult.valid) {
10114
+ errors.push(`Scale: ${scaleResult.error}`);
10115
+ }
8651
10116
  }
8652
- this.ctx.strokeStyle = asString(color);
8653
- this.ctx.stroke();
8654
- this.ctx.setLineDash([]);
8655
- // dashed round edge gap
8656
- if (style === 2 /* BORDER_STYLE.DASHED */) {
8657
- if (isBezierCurve(boxPaths[0])) {
8658
- const path1 = boxPaths[3];
8659
- const path2 = boxPaths[0];
8660
- this.ctx.beginPath();
8661
- this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]);
8662
- this.ctx.stroke();
10117
+ // Validate CSP nonce
10118
+ if (options.cspNonce !== undefined) {
10119
+ const nonceResult = this.validateCspNonce(options.cspNonce);
10120
+ if (!nonceResult.valid) {
10121
+ errors.push(`CSP nonce: ${nonceResult.error}`);
8663
10122
  }
8664
- if (isBezierCurve(boxPaths[1])) {
8665
- const path1 = boxPaths[1];
8666
- const path2 = boxPaths[2];
8667
- this.ctx.beginPath();
8668
- this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]);
8669
- this.ctx.stroke();
10123
+ }
10124
+ // Custom validation
10125
+ if (this.config.customValidator) {
10126
+ const customResult = this.config.customValidator(options, 'options');
10127
+ if (!customResult.valid) {
10128
+ errors.push(`Custom validation: ${customResult.error}`);
8670
10129
  }
8671
10130
  }
8672
- this.ctx.restore();
8673
- }
8674
- async render(element) {
8675
- if (this.options.backgroundColor) {
8676
- this.ctx.fillStyle = asString(this.options.backgroundColor);
8677
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
10131
+ if (errors.length > 0) {
10132
+ return {
10133
+ valid: false,
10134
+ error: errors.join('; ')
10135
+ };
8678
10136
  }
8679
- const stack = parseStackingContexts(element);
8680
- await this.renderStack(stack);
8681
- this.applyEffects([]);
8682
- return this.canvas;
10137
+ return { valid: true };
8683
10138
  }
8684
10139
  }
8685
- const isTextInputElement = (container) => {
8686
- if (container instanceof TextareaElementContainer) {
8687
- return true;
8688
- }
8689
- else if (container instanceof SelectElementContainer) {
8690
- return true;
8691
- }
8692
- else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
8693
- return true;
8694
- }
8695
- return false;
8696
- };
8697
- const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
8698
- switch (clip) {
8699
- case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
8700
- return calculateBorderBoxPath(curves);
8701
- case 2 /* BACKGROUND_CLIP.CONTENT_BOX */:
8702
- return calculateContentBoxPath(curves);
8703
- case 1 /* BACKGROUND_CLIP.PADDING_BOX */:
8704
- default:
8705
- return calculatePaddingBoxPath(curves);
8706
- }
8707
- };
8708
- const canvasTextAlign = (textAlign) => {
8709
- switch (textAlign) {
8710
- case 1 /* TEXT_ALIGN.CENTER */:
8711
- return 'center';
8712
- case 2 /* TEXT_ALIGN.RIGHT */:
8713
- return 'right';
8714
- case 0 /* TEXT_ALIGN.LEFT */:
8715
- default:
8716
- return 'left';
8717
- }
8718
- };
8719
- // see https://github.com/niklasvh/html2canvas/pull/2645
8720
- const iOSBrokenFonts = ['-apple-system', 'system-ui'];
8721
- const fixIOSSystemFonts = (fontFamilies) => {
8722
- return /iPhone OS 15_(0|1)/.test(window.navigator.userAgent)
8723
- ? fontFamilies.filter((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) === -1)
8724
- : fontFamilies;
8725
- };
8726
-
8727
- class ForeignObjectRenderer extends Renderer {
8728
- constructor(context, options) {
8729
- super(context, options);
8730
- this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
8731
- this.ctx = this.canvas.getContext('2d');
8732
- this.options = options;
8733
- this.canvas.width = Math.floor(options.width * options.scale);
8734
- this.canvas.height = Math.floor(options.height * options.scale);
8735
- this.canvas.style.width = `${options.width}px`;
8736
- this.canvas.style.height = `${options.height}px`;
8737
- this.ctx.scale(this.options.scale, this.options.scale);
8738
- this.ctx.translate(-options.x, -options.y);
8739
- this.context.logger.debug(`EXPERIMENTAL ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`);
8740
- }
8741
- async render(element) {
8742
- const svg = createForeignObjectSVG(this.options.width * this.options.scale, this.options.height * this.options.scale, this.options.scale, this.options.scale, element);
8743
- const img = await loadSerializedSVG(svg);
8744
- if (this.options.backgroundColor) {
8745
- this.ctx.fillStyle = asString(this.options.backgroundColor);
8746
- this.ctx.fillRect(0, 0, this.options.width * this.options.scale, this.options.height * this.options.scale);
8747
- }
8748
- this.ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
8749
- return this.canvas;
8750
- }
10140
+ /**
10141
+ * Create a default validator instance
10142
+ */
10143
+ function createDefaultValidator(config = {}) {
10144
+ return new Validator({
10145
+ allowDataUrls: true,
10146
+ maxImageTimeout: 300000, // 5 minutes
10147
+ ...config
10148
+ });
8751
10149
  }
8752
- const loadSerializedSVG = (svg) => new Promise((resolve, reject) => {
8753
- const img = new Image();
8754
- img.onload = () => {
8755
- resolve(img);
8756
- };
8757
- img.onerror = reject;
8758
- img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
8759
- });
8760
10150
 
8761
- class Logger {
8762
- constructor({ id, enabled }) {
8763
- this.id = id;
10151
+ /**
10152
+ * Performance Monitor
10153
+ *
10154
+ * Tracks performance metrics throughout the rendering pipeline.
10155
+ * Provides insights into where time is spent during rendering.
10156
+ *
10157
+ * Usage:
10158
+ * ```typescript
10159
+ * const monitor = new PerformanceMonitor(context);
10160
+ *
10161
+ * monitor.start('clone');
10162
+ * await cloneDocument();
10163
+ * monitor.end('clone');
10164
+ *
10165
+ * const summary = monitor.getSummary();
10166
+ * ```
10167
+ */
10168
+ class PerformanceMonitor {
10169
+ constructor(context, enabled = true) {
10170
+ this.context = context;
10171
+ this.activeMetrics = new Map();
10172
+ this.completedMetrics = [];
8764
10173
  this.enabled = enabled;
8765
- this.start = Date.now();
10174
+ // Fallback for environments without performance.now()
10175
+ this.getTime =
10176
+ typeof performance !== 'undefined' && typeof performance.now === 'function'
10177
+ ? () => performance.now()
10178
+ : () => Date.now();
8766
10179
  }
8767
- debug(...args) {
8768
- if (this.enabled) {
8769
- // eslint-disable-next-line no-console
8770
- if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
8771
- // eslint-disable-next-line no-console
8772
- console.debug(this.id, `${this.getTime()}ms`, ...args);
8773
- }
8774
- else {
8775
- this.info(...args);
8776
- }
10180
+ /**
10181
+ * Start measuring a performance metric
10182
+ *
10183
+ * @param name - Unique name for this metric
10184
+ * @param metadata - Optional metadata to attach
10185
+ */
10186
+ start(name, metadata) {
10187
+ if (!this.enabled) {
10188
+ return;
8777
10189
  }
10190
+ if (this.activeMetrics.has(name)) {
10191
+ this.context?.logger.warn(`Performance metric '${name}' already started. Overwriting.`);
10192
+ }
10193
+ this.activeMetrics.set(name, {
10194
+ name,
10195
+ startTime: this.getTime(),
10196
+ metadata
10197
+ });
8778
10198
  }
8779
- getTime() {
8780
- return Date.now() - this.start;
10199
+ /**
10200
+ * End measuring a performance metric
10201
+ *
10202
+ * @param name - Name of the metric to end
10203
+ * @returns The completed metric, or undefined if not found
10204
+ */
10205
+ end(name) {
10206
+ if (!this.enabled) {
10207
+ return undefined;
10208
+ }
10209
+ const metric = this.activeMetrics.get(name);
10210
+ if (!metric) {
10211
+ this.context?.logger.warn(`Performance metric '${name}' not found. Was start() called?`);
10212
+ return undefined;
10213
+ }
10214
+ metric.endTime = this.getTime();
10215
+ metric.duration = metric.endTime - metric.startTime;
10216
+ this.completedMetrics.push(metric);
10217
+ this.activeMetrics.delete(name);
10218
+ this.context?.logger.debug(`⏱️ ${name}: ${metric.duration.toFixed(2)}ms`, metric.metadata);
10219
+ return metric;
8781
10220
  }
8782
- info(...args) {
8783
- if (this.enabled) {
8784
- // eslint-disable-next-line no-console
8785
- if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
8786
- // eslint-disable-next-line no-console
8787
- console.info(this.id, `${this.getTime()}ms`, ...args);
8788
- }
10221
+ /**
10222
+ * Measure a synchronous function
10223
+ *
10224
+ * @param name - Name for this measurement
10225
+ * @param fn - Function to measure
10226
+ * @param metadata - Optional metadata
10227
+ * @returns The function's return value
10228
+ */
10229
+ measure(name, fn, metadata) {
10230
+ this.start(name, metadata);
10231
+ try {
10232
+ const result = fn();
10233
+ this.end(name);
10234
+ return result;
10235
+ }
10236
+ catch (error) {
10237
+ this.end(name);
10238
+ throw error;
8789
10239
  }
8790
10240
  }
8791
- warn(...args) {
8792
- if (this.enabled) {
8793
- if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
8794
- console.warn(this.id, `${this.getTime()}ms`, ...args);
8795
- }
8796
- else {
8797
- this.info(...args);
8798
- }
10241
+ /**
10242
+ * Measure an asynchronous function
10243
+ *
10244
+ * @param name - Name for this measurement
10245
+ * @param fn - Async function to measure
10246
+ * @param metadata - Optional metadata
10247
+ * @returns Promise resolving to the function's return value
10248
+ */
10249
+ async measureAsync(name, fn, metadata) {
10250
+ this.start(name, metadata);
10251
+ try {
10252
+ const result = await fn();
10253
+ this.end(name);
10254
+ return result;
10255
+ }
10256
+ catch (error) {
10257
+ this.end(name);
10258
+ throw error;
8799
10259
  }
8800
10260
  }
8801
- error(...args) {
8802
- if (this.enabled) {
8803
- if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
8804
- console.error(this.id, `${this.getTime()}ms`, ...args);
8805
- }
8806
- else {
8807
- this.info(...args);
8808
- }
10261
+ /**
10262
+ * Get all completed metrics
10263
+ *
10264
+ * @returns Array of completed performance metrics
10265
+ */
10266
+ getMetrics() {
10267
+ return [...this.completedMetrics];
10268
+ }
10269
+ /**
10270
+ * Get a specific metric by name
10271
+ *
10272
+ * @param name - Metric name
10273
+ * @returns The metric, or undefined if not found
10274
+ */
10275
+ getMetric(name) {
10276
+ return this.completedMetrics.find((m) => m.name === name);
10277
+ }
10278
+ /**
10279
+ * Get performance summary
10280
+ *
10281
+ * @returns Aggregated performance data
10282
+ */
10283
+ getSummary() {
10284
+ const totalDuration = this.completedMetrics.reduce((sum, metric) => sum + (metric.duration || 0), 0);
10285
+ const breakdown = this.completedMetrics.map((metric) => ({
10286
+ name: metric.name,
10287
+ duration: metric.duration || 0,
10288
+ percentage: totalDuration > 0 ? (((metric.duration || 0) / totalDuration) * 100).toFixed(1) + '%' : '0%'
10289
+ }));
10290
+ return {
10291
+ totalDuration,
10292
+ metrics: this.getMetrics(),
10293
+ breakdown
10294
+ };
10295
+ }
10296
+ /**
10297
+ * Log performance summary to console
10298
+ */
10299
+ logSummary() {
10300
+ if (!this.enabled || this.completedMetrics.length === 0 || !this.context) {
10301
+ return;
8809
10302
  }
10303
+ const summary = this.getSummary();
10304
+ this.context.logger.info(`\n📊 Performance Summary (Total: ${summary.totalDuration.toFixed(2)}ms):`);
10305
+ summary.breakdown
10306
+ .sort((a, b) => b.duration - a.duration)
10307
+ .forEach((item) => {
10308
+ this.context.logger.info(` ${item.name.padEnd(20)} ${item.duration.toFixed(2).padStart(8)}ms ${item.percentage.padStart(6)}`);
10309
+ });
8810
10310
  }
8811
- }
8812
- Logger.instances = {};
8813
-
8814
- class Context {
8815
- constructor(options, windowBounds) {
8816
- this.windowBounds = windowBounds;
8817
- this.instanceName = `#${Context.instanceCount++}`;
8818
- this.logger = new Logger({ id: this.instanceName, enabled: options.logging });
8819
- this.cache = options.cache ?? new Cache(this, options);
10311
+ /**
10312
+ * Clear all metrics
10313
+ */
10314
+ clear() {
10315
+ this.activeMetrics.clear();
10316
+ this.completedMetrics.splice(0);
10317
+ }
10318
+ /**
10319
+ * Check if monitoring is enabled
10320
+ */
10321
+ isEnabled() {
10322
+ return this.enabled;
10323
+ }
10324
+ /**
10325
+ * Get active (uncompleted) metrics
10326
+ * Useful for debugging leaked measurements
10327
+ */
10328
+ getActiveMetrics() {
10329
+ return Array.from(this.activeMetrics.keys());
8820
10330
  }
8821
10331
  }
8822
- Context.instanceCount = 1;
8823
10332
 
8824
- let cspNonce;
8825
- const setCspNonce = (nonce) => {
8826
- cspNonce = nonce;
10333
+ /**
10334
+ * Main html2canvas function with improved configuration management
10335
+ *
10336
+ * @param element - Element to render
10337
+ * @param options - Rendering options
10338
+ * @param config - Optional configuration (for advanced use cases)
10339
+ * @returns Promise resolving to rendered canvas
10340
+ */
10341
+ const html2canvas = (element, options = {}, config) => {
10342
+ // Create configuration from element if not provided
10343
+ const finalConfig = config ||
10344
+ Html2CanvasConfig.fromElement(element, {
10345
+ cspNonce: options.cspNonce,
10346
+ cache: options.cache
10347
+ });
10348
+ return renderElement(element, options, finalConfig);
8827
10349
  };
8828
- const html2canvas = (element, options = {}) => {
8829
- return renderElement(element, options);
10350
+ /**
10351
+ * Set CSP nonce for inline styles
10352
+ * @deprecated Use options.cspNonce instead
10353
+ */
10354
+ const setCspNonce = (nonce) => {
10355
+ console.warn('[html2canvas-pro] setCspNonce is deprecated. ' +
10356
+ 'Pass cspNonce in options instead: html2canvas(element, { cspNonce: "..." })');
10357
+ // For backward compatibility, set default config
10358
+ if (typeof window !== 'undefined') {
10359
+ setDefaultConfig(new Html2CanvasConfig({ window, cspNonce: nonce }));
10360
+ }
8830
10361
  };
8831
10362
  html2canvas.setCspNonce = setCspNonce;
8832
- if (typeof window !== 'undefined') {
8833
- CacheStorage.setContext(window);
8834
- }
8835
- const renderElement = async (element, opts) => {
10363
+ /**
10364
+ * Coerce number-like option values for backward compatibility (e.g. string "2" from form/query).
10365
+ * Mutates opts in place; callers should avoid reusing the same options object if they rely on original types.
10366
+ */
10367
+ const coerceNumberOptions = (opts) => {
10368
+ const numKeys = [
10369
+ 'scale',
10370
+ 'width',
10371
+ 'height',
10372
+ 'imageTimeout',
10373
+ 'x',
10374
+ 'y',
10375
+ 'windowWidth',
10376
+ 'windowHeight',
10377
+ 'scrollX',
10378
+ 'scrollY'
10379
+ ];
10380
+ numKeys.forEach((key) => {
10381
+ const v = opts[key];
10382
+ if (v !== undefined && v !== null && typeof v !== 'number') {
10383
+ const n = Number(v);
10384
+ if (!Number.isNaN(n)) {
10385
+ opts[key] = n;
10386
+ }
10387
+ }
10388
+ });
10389
+ };
10390
+ const renderElement = async (element, opts, config) => {
10391
+ coerceNumberOptions(opts);
10392
+ // Input validation (unless explicitly skipped)
10393
+ if (!opts.skipValidation) {
10394
+ const validator = opts.validator || createDefaultValidator();
10395
+ // Validate element
10396
+ const elementValidation = validator.validateElement(element);
10397
+ if (!elementValidation.valid) {
10398
+ throw new Error(elementValidation.error);
10399
+ }
10400
+ // Validate options
10401
+ const optionsValidation = validator.validateOptions(opts);
10402
+ if (!optionsValidation.valid) {
10403
+ throw new Error(`Invalid options: ${optionsValidation.error}`);
10404
+ }
10405
+ }
8836
10406
  if (!element || typeof element !== 'object') {
8837
- return Promise.reject('Invalid element provided as first argument');
10407
+ throw new Error('Invalid element provided as first argument');
8838
10408
  }
8839
10409
  const ownerDocument = element.ownerDocument;
8840
10410
  if (!ownerDocument) {
@@ -8853,17 +10423,29 @@ const renderElement = async (element, opts) => {
8853
10423
  };
8854
10424
  const contextOptions = {
8855
10425
  logging: opts.logging ?? true,
8856
- cache: opts.cache,
10426
+ cache: opts.cache ?? config.cache,
8857
10427
  ...resourceOptions
8858
10428
  };
10429
+ // Fallbacks for minimal window (e.g. element-like mocks) so we don't get NaN
10430
+ const DEFAULT_WINDOW_WIDTH = 800;
10431
+ const DEFAULT_WINDOW_HEIGHT = 600;
10432
+ const DEFAULT_SCROLL = 0;
10433
+ const win = defaultView;
8859
10434
  const windowOptions = {
8860
- windowWidth: opts.windowWidth ?? defaultView.innerWidth,
8861
- windowHeight: opts.windowHeight ?? defaultView.innerHeight,
8862
- scrollX: opts.scrollX ?? defaultView.pageXOffset,
8863
- scrollY: opts.scrollY ?? defaultView.pageYOffset
10435
+ windowWidth: opts.windowWidth ?? win.innerWidth ?? DEFAULT_WINDOW_WIDTH,
10436
+ windowHeight: opts.windowHeight ?? win.innerHeight ?? DEFAULT_WINDOW_HEIGHT,
10437
+ scrollX: opts.scrollX ?? win.pageXOffset ?? DEFAULT_SCROLL,
10438
+ scrollY: opts.scrollY ?? win.pageYOffset ?? DEFAULT_SCROLL
8864
10439
  };
8865
10440
  const windowBounds = new Bounds(windowOptions.scrollX, windowOptions.scrollY, windowOptions.windowWidth, windowOptions.windowHeight);
8866
- const context = new Context(contextOptions, windowBounds);
10441
+ const context = new Context(contextOptions, windowBounds, config);
10442
+ // Initialize performance monitoring if enabled
10443
+ const performanceMonitoring = opts.enablePerformanceMonitoring ?? opts.logging ?? false;
10444
+ const perfMonitor = new PerformanceMonitor(context, performanceMonitoring);
10445
+ perfMonitor.start('total', {
10446
+ width: windowOptions.windowWidth,
10447
+ height: windowOptions.windowHeight
10448
+ });
8867
10449
  const foreignObjectRendering = opts.foreignObjectRendering ?? false;
8868
10450
  const cloneOptions = {
8869
10451
  allowTaint: opts.allowTaint ?? false,
@@ -8872,15 +10454,17 @@ const renderElement = async (element, opts) => {
8872
10454
  iframeContainer: opts.iframeContainer,
8873
10455
  inlineImages: foreignObjectRendering,
8874
10456
  copyStyles: foreignObjectRendering,
8875
- cspNonce
10457
+ cspNonce: opts.cspNonce ?? config.cspNonce
8876
10458
  };
8877
10459
  context.logger.debug(`Starting document clone with size ${windowBounds.width}x${windowBounds.height} scrolled to ${-windowBounds.left},${-windowBounds.top}`);
10460
+ perfMonitor.start('clone');
8878
10461
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
8879
10462
  const clonedElement = documentCloner.clonedReferenceElement;
8880
10463
  if (!clonedElement) {
8881
- return Promise.reject(`Unable to find element in cloned iframe`);
10464
+ throw new Error('Unable to find element in cloned iframe');
8882
10465
  }
8883
10466
  const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
10467
+ perfMonitor.end('clone');
8884
10468
  const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement)
8885
10469
  ? parseDocumentSize(clonedElement.ownerDocument)
8886
10470
  : parseBounds(context, clonedElement);
@@ -8892,32 +10476,56 @@ const renderElement = async (element, opts) => {
8892
10476
  x: (opts.x ?? 0) + left,
8893
10477
  y: (opts.y ?? 0) + top,
8894
10478
  width: opts.width ?? Math.ceil(width),
8895
- height: opts.height ?? Math.ceil(height)
10479
+ height: opts.height ?? Math.ceil(height),
10480
+ imageSmoothing: opts.imageSmoothing,
10481
+ imageSmoothingQuality: opts.imageSmoothingQuality
8896
10482
  };
8897
10483
  let canvas;
8898
- if (foreignObjectRendering) {
8899
- context.logger.debug(`Document cloned, using foreign object rendering`);
8900
- const renderer = new ForeignObjectRenderer(context, renderOptions);
8901
- canvas = await renderer.render(clonedElement);
8902
- }
8903
- else {
8904
- context.logger.debug(`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`);
8905
- context.logger.debug(`Starting DOM parsing`);
8906
- const root = parseTree(context, clonedElement);
8907
- if (backgroundColor === root.styles.backgroundColor) {
8908
- root.styles.backgroundColor = COLORS.TRANSPARENT;
10484
+ let root;
10485
+ try {
10486
+ if (foreignObjectRendering) {
10487
+ context.logger.debug(`Document cloned, using foreign object rendering`);
10488
+ perfMonitor.start('render-foreignobject');
10489
+ const renderer = new ForeignObjectRenderer(context, renderOptions);
10490
+ canvas = await renderer.render(clonedElement);
10491
+ perfMonitor.end('render-foreignobject');
10492
+ }
10493
+ else {
10494
+ context.logger.debug(`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`);
10495
+ context.logger.debug(`Starting DOM parsing`);
10496
+ perfMonitor.start('parse');
10497
+ root = parseTree(context, clonedElement);
10498
+ perfMonitor.end('parse');
10499
+ if (backgroundColor === root.styles.backgroundColor) {
10500
+ root.styles.backgroundColor = COLORS.TRANSPARENT;
10501
+ }
10502
+ context.logger.debug(`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`);
10503
+ perfMonitor.start('render');
10504
+ const renderer = new CanvasRenderer(context, renderOptions);
10505
+ canvas = await renderer.render(root);
10506
+ perfMonitor.end('render');
10507
+ }
10508
+ perfMonitor.start('cleanup');
10509
+ if (opts.removeContainer ?? true) {
10510
+ if (!DocumentCloner.destroy(container)) {
10511
+ context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
10512
+ }
10513
+ }
10514
+ perfMonitor.end('cleanup');
10515
+ perfMonitor.end('total');
10516
+ context.logger.debug(`Finished rendering`);
10517
+ // Log performance summary if monitoring is enabled
10518
+ if (performanceMonitoring) {
10519
+ perfMonitor.logSummary();
8909
10520
  }
8910
- context.logger.debug(`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`);
8911
- const renderer = new CanvasRenderer(context, renderOptions);
8912
- canvas = await renderer.render(root);
10521
+ return canvas;
8913
10522
  }
8914
- if (opts.removeContainer ?? true) {
8915
- if (!DocumentCloner.destroy(container)) {
8916
- context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
10523
+ finally {
10524
+ // Restore DOM modifications (animations, transforms) in cloned document
10525
+ if (root) {
10526
+ root.restoreTree();
8917
10527
  }
8918
10528
  }
8919
- context.logger.debug(`Finished rendering`);
8920
- return canvas;
8921
10529
  };
8922
10530
  const parseBackgroundColor = (context, element, backgroundColorOverride) => {
8923
10531
  const ownerDocument = element.ownerDocument;
@@ -8942,5 +10550,5 @@ const parseBackgroundColor = (context, element, backgroundColorOverride) => {
8942
10550
  : defaultBackgroundColor;
8943
10551
  };
8944
10552
 
8945
- export { html2canvas as default, html2canvas, setCspNonce };
10553
+ export { Html2CanvasConfig, IMAGE_RENDERING, PerformanceMonitor, Validator, createDefaultValidator, html2canvas as default, html2canvas, setCspNonce };
8946
10554
  //# sourceMappingURL=html2canvas-pro.esm.js.map