html2canvas-pro 1.6.7 → 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 (122) 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 +2788 -1321
  5. package/dist/html2canvas-pro.esm.js.map +1 -1
  6. package/dist/html2canvas-pro.js +2791 -1320
  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 +3 -3
  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/dom-normalizer.d.ts +43 -0
  107. package/dist/types/dom/element-container.d.ts +20 -1
  108. package/dist/types/dom/node-parser.d.ts +2 -7
  109. package/dist/types/dom/node-type-guards.d.ts +33 -0
  110. package/dist/types/dom/replaced-elements/iframe-element-container.d.ts +4 -1
  111. package/dist/types/index.d.ts +48 -3
  112. package/dist/types/render/canvas/__tests__/background-renderer.test.d.ts +1 -0
  113. package/dist/types/render/canvas/__tests__/border-renderer.test.d.ts +1 -0
  114. package/dist/types/render/canvas/__tests__/effects-renderer.test.d.ts +1 -0
  115. package/dist/types/render/canvas/__tests__/text-renderer.test.d.ts +1 -0
  116. package/dist/types/render/canvas/background-renderer.d.ts +87 -0
  117. package/dist/types/render/canvas/border-renderer.d.ts +67 -0
  118. package/dist/types/render/canvas/canvas-renderer.d.ts +19 -23
  119. package/dist/types/render/canvas/effects-renderer.d.ts +64 -0
  120. package/dist/types/render/canvas/text-renderer.d.ts +57 -0
  121. package/dist/types/render/renderer-interface.d.ts +26 -0
  122. package/package.json +2 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * html2canvas-pro 1.6.7 <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
  */
@@ -1505,6 +1505,97 @@
1505
1505
  return type === 2 /* TokenType.LEFT_PARENTHESIS_TOKEN */ && token.type === 3 /* TokenType.RIGHT_PARENTHESIS_TOKEN */;
1506
1506
  };
1507
1507
 
1508
+ /**
1509
+ * Color mathematics utilities
1510
+ * Extracted to break circular dependency between srgb.ts and color-utilities.ts
1511
+ */
1512
+ /**
1513
+ * Clamp a value between min and max
1514
+ */
1515
+ const clamp = (value, min, max) => {
1516
+ return Math.min(Math.max(value, min), max);
1517
+ };
1518
+ /**
1519
+ * Multiply two 3x3 matrices
1520
+ */
1521
+ const multiplyMatrices = (A, B) => {
1522
+ return [
1523
+ A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
1524
+ A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
1525
+ A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
1526
+ ];
1527
+ };
1528
+
1529
+ /**
1530
+ * SRGB related functions
1531
+ */
1532
+ /**
1533
+ * Convert XYZ to linear-light sRGB
1534
+ *
1535
+ * @param xyz
1536
+ */
1537
+ const xyz2rgbLinear = (xyz) => {
1538
+ return multiplyMatrices([
1539
+ 3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796, 1.8759675015077202,
1540
+ 0.04155505740717559, 0.05563007969699366, -0.20397695888897652, 1.0569715142428786
1541
+ ], xyz);
1542
+ };
1543
+ /**
1544
+ * Convert XYZ to linear-light sRGB
1545
+ *
1546
+ * @param xyz
1547
+ */
1548
+ const rgbLinear2xyz = (xyz) => {
1549
+ return multiplyMatrices([
1550
+ 0.41239079926595934, 0.357584339383878, 0.1804807884018343, 0.21263900587151027, 0.715168678767756,
1551
+ 0.07219231536073371, 0.01933081871559182, 0.11919477979462598, 0.9505321522496607
1552
+ ], xyz);
1553
+ };
1554
+ /**
1555
+ * Convert sRGB to RGB
1556
+ *
1557
+ * @param rgb
1558
+ */
1559
+ const srgbLinear2rgb = (rgb) => {
1560
+ return rgb.map((c) => {
1561
+ const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
1562
+ return abs > 0.0031308 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * c;
1563
+ });
1564
+ };
1565
+ /**
1566
+ * Convert RGB to sRGB
1567
+ *
1568
+ * @param rgb
1569
+ */
1570
+ const rgb2rgbLinear = (rgb) => {
1571
+ return rgb.map((c) => {
1572
+ const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
1573
+ return abs <= 0.04045 ? c / 12.92 : sign * ((abs + 0.055) / 1.055) ** 2.4;
1574
+ });
1575
+ };
1576
+ /**
1577
+ * XYZ to SRGB
1578
+ *
1579
+ * @param args
1580
+ */
1581
+ const srgbFromXYZ = (args) => {
1582
+ const [r, g, b] = srgbLinear2rgb(xyz2rgbLinear([args[0], args[1], args[2]]));
1583
+ return [r, g, b, args[3]];
1584
+ };
1585
+ /**
1586
+ * XYZ to SRGB-Linear
1587
+ * @param args
1588
+ */
1589
+ const srgbLinearFromXYZ = (args) => {
1590
+ const [r, g, b] = xyz2rgbLinear([args[0], args[1], args[2]]);
1591
+ return [
1592
+ clamp(Math.round(r * 255), 0, 255),
1593
+ clamp(Math.round(g * 255), 0, 255),
1594
+ clamp(Math.round(b * 255), 0, 255),
1595
+ args[3]
1596
+ ];
1597
+ };
1598
+
1508
1599
  const isLength = (token) => token.type === 17 /* TokenType.NUMBER_TOKEN */ || token.type === 15 /* TokenType.DIMENSION_TOKEN */;
1509
1600
 
1510
1601
  const isLengthPercentage = (token) => token.type === 16 /* TokenType.PERCENTAGE_TOKEN */ || isLength(token);
@@ -1722,16 +1813,6 @@
1722
1813
  return 0;
1723
1814
  };
1724
1815
  const isRelativeTransform = (tokens) => (tokens[0].type === 20 /* TokenType.IDENT_TOKEN */ ? tokens[0].value : 'unknown') === 'from';
1725
- const clamp = (value, min, max) => {
1726
- return Math.min(Math.max(value, min), max);
1727
- };
1728
- const multiplyMatrices = (A, B) => {
1729
- return [
1730
- A[0] * B[0] + A[1] * B[1] + A[2] * B[2],
1731
- A[3] * B[0] + A[4] * B[1] + A[5] * B[2],
1732
- A[6] * B[0] + A[7] * B[1] + A[8] * B[2]
1733
- ];
1734
- };
1735
1816
  const packSrgb = (args) => {
1736
1817
  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));
1737
1818
  };
@@ -2014,76 +2095,6 @@
2014
2095
  return packXYZ([xyz[0], xyz[1], xyz[2], args[3]]);
2015
2096
  };
2016
2097
 
2017
- /**
2018
- * SRGB related functions
2019
- */
2020
- /**
2021
- * Convert XYZ to linear-light sRGB
2022
- *
2023
- * @param xyz
2024
- */
2025
- const xyz2rgbLinear = (xyz) => {
2026
- return multiplyMatrices([
2027
- 3.2409699419045226, -1.537383177570094, -0.4986107602930034, -0.9692436362808796, 1.8759675015077202,
2028
- 0.04155505740717559, 0.05563007969699366, -0.20397695888897652, 1.0569715142428786
2029
- ], xyz);
2030
- };
2031
- /**
2032
- * Convert XYZ to linear-light sRGB
2033
- *
2034
- * @param xyz
2035
- */
2036
- const rgbLinear2xyz = (xyz) => {
2037
- return multiplyMatrices([
2038
- 0.41239079926595934, 0.357584339383878, 0.1804807884018343, 0.21263900587151027, 0.715168678767756,
2039
- 0.07219231536073371, 0.01933081871559182, 0.11919477979462598, 0.9505321522496607
2040
- ], xyz);
2041
- };
2042
- /**
2043
- * Convert sRGB to RGB
2044
- *
2045
- * @param rgb
2046
- */
2047
- const srgbLinear2rgb = (rgb) => {
2048
- return rgb.map((c) => {
2049
- const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
2050
- return abs > 0.0031308 ? sign * (1.055 * abs ** (1 / 2.4) - 0.055) : 12.92 * c;
2051
- });
2052
- };
2053
- /**
2054
- * Convert RGB to sRGB
2055
- *
2056
- * @param rgb
2057
- */
2058
- const rgb2rgbLinear = (rgb) => {
2059
- return rgb.map((c) => {
2060
- const sign = c < 0 ? -1 : 1, abs = Math.abs(c);
2061
- return abs <= 0.04045 ? c / 12.92 : sign * ((abs + 0.055) / 1.055) ** 2.4;
2062
- });
2063
- };
2064
- /**
2065
- * XYZ to SRGB
2066
- *
2067
- * @param args
2068
- */
2069
- const srgbFromXYZ = (args) => {
2070
- const [r, g, b] = srgbLinear2rgb(xyz2rgbLinear([args[0], args[1], args[2]]));
2071
- return [r, g, b, args[3]];
2072
- };
2073
- /**
2074
- * XYZ to SRGB-Linear
2075
- * @param args
2076
- */
2077
- const srgbLinearFromXYZ = (args) => {
2078
- const [r, g, b] = xyz2rgbLinear([args[0], args[1], args[2]]);
2079
- return [
2080
- clamp(Math.round(r * 255), 0, 255),
2081
- clamp(Math.round(g * 255), 0, 255),
2082
- clamp(Math.round(b * 255), 0, 255),
2083
- args[3]
2084
- ];
2085
- };
2086
-
2087
2098
  /**
2088
2099
  * Display-P3 related functions
2089
2100
  */
@@ -4559,6 +4570,37 @@
4559
4570
  }
4560
4571
  };
4561
4572
 
4573
+ exports.IMAGE_RENDERING = void 0;
4574
+ (function (IMAGE_RENDERING) {
4575
+ IMAGE_RENDERING[IMAGE_RENDERING["AUTO"] = 0] = "AUTO";
4576
+ IMAGE_RENDERING[IMAGE_RENDERING["CRISP_EDGES"] = 1] = "CRISP_EDGES";
4577
+ IMAGE_RENDERING[IMAGE_RENDERING["PIXELATED"] = 2] = "PIXELATED";
4578
+ IMAGE_RENDERING[IMAGE_RENDERING["SMOOTH"] = 3] = "SMOOTH";
4579
+ })(exports.IMAGE_RENDERING || (exports.IMAGE_RENDERING = {}));
4580
+ const imageRendering = {
4581
+ name: 'image-rendering',
4582
+ initialValue: 'auto',
4583
+ prefix: false,
4584
+ type: 2 /* PropertyDescriptorParsingType.IDENT_VALUE */,
4585
+ parse: (_context, value) => {
4586
+ switch (value.toLowerCase()) {
4587
+ case 'crisp-edges':
4588
+ case '-webkit-crisp-edges':
4589
+ case '-moz-crisp-edges':
4590
+ return exports.IMAGE_RENDERING.CRISP_EDGES;
4591
+ case 'pixelated':
4592
+ case '-webkit-optimize-contrast':
4593
+ return exports.IMAGE_RENDERING.PIXELATED;
4594
+ case 'smooth':
4595
+ case 'high-quality':
4596
+ return exports.IMAGE_RENDERING.SMOOTH;
4597
+ case 'auto':
4598
+ default:
4599
+ return exports.IMAGE_RENDERING.AUTO;
4600
+ }
4601
+ }
4602
+ };
4603
+
4562
4604
  class CSSParsedDeclaration {
4563
4605
  constructor(context, declaration) {
4564
4606
  this.animationDuration = parse(context, duration, declaration.animationDuration);
@@ -4635,6 +4677,7 @@
4635
4677
  this.wordBreak = parse(context, wordBreak, declaration.wordBreak);
4636
4678
  this.zIndex = parse(context, zIndex, declaration.zIndex);
4637
4679
  this.objectFit = parse(context, objectFit, declaration.objectFit);
4680
+ this.imageRendering = parse(context, imageRendering, declaration.imageRendering);
4638
4681
  }
4639
4682
  isVisible() {
4640
4683
  return this.display > 0 && this.opacity > 0 && this.visibility === 0 /* VISIBILITY.VISIBLE */;
@@ -4711,8 +4754,45 @@
4711
4754
  }
4712
4755
  };
4713
4756
 
4757
+ /**
4758
+ * DOM Node Type Guards
4759
+ * Extracted to break circular dependencies
4760
+ */
4761
+ /**
4762
+ * Check if node is an Element
4763
+ */
4764
+ const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE;
4765
+ /**
4766
+ * Check if node is a Text node
4767
+ */
4768
+ const isTextNode = (node) => node.nodeType === Node.TEXT_NODE;
4769
+ /**
4770
+ * Check if element is an SVG element
4771
+ */
4772
+ const isSVGElementNode = (element) => typeof element.className === 'object';
4773
+ /**
4774
+ * Check if node is an HTML element
4775
+ */
4776
+ const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);
4777
+ /**
4778
+ * Check if node is an LI element
4779
+ */
4780
+ const isLIElement = (node) => node.tagName === 'LI';
4781
+ /**
4782
+ * Check if node is an OL element
4783
+ */
4784
+ const isOLElement = (node) => node.tagName === 'OL';
4785
+ /**
4786
+ * Check if element is a custom element
4787
+ * Custom elements must have a hyphen and cannot be SVG elements
4788
+ */
4789
+ const isCustomElement = (element) => !isSVGElementNode(element) && element.tagName.indexOf('-') > 0;
4790
+
4714
4791
  const elementDebuggerAttribute = 'data-html2canvas-debug';
4715
4792
  const getElementDebugType = (element) => {
4793
+ if (typeof element.getAttribute !== 'function') {
4794
+ return 0 /* DebuggerType.NONE */;
4795
+ }
4716
4796
  const attribute = element.getAttribute(elementDebuggerAttribute);
4717
4797
  switch (attribute) {
4718
4798
  case 'all':
@@ -4732,8 +4812,83 @@
4732
4812
  return elementType === 1 /* DebuggerType.ALL */ || type === elementType;
4733
4813
  };
4734
4814
 
4815
+ /**
4816
+ * DOM Normalizer
4817
+ * Handles DOM side effects that need to happen before rendering
4818
+ * Extracted from ElementContainer to follow SRP
4819
+ */
4820
+ /**
4821
+ * Normalize element styles for accurate rendering
4822
+ * This includes disabling animations and resetting transforms
4823
+ */
4824
+ class DOMNormalizer {
4825
+ /**
4826
+ * Normalize a single element and return original styles
4827
+ *
4828
+ * @param element - Element to normalize
4829
+ * @param styles - Parsed CSS styles
4830
+ * @returns Original styles map for restoration
4831
+ */
4832
+ static normalizeElement(element, styles) {
4833
+ const originalStyles = {};
4834
+ if (!isHTMLElementNode(element)) {
4835
+ return originalStyles;
4836
+ }
4837
+ // Disable animations to capture static state
4838
+ if (styles.animationDuration.some((duration) => duration > 0)) {
4839
+ originalStyles.animationDuration = element.style.animationDuration;
4840
+ element.style.animationDuration = '0s';
4841
+ }
4842
+ // Reset transform for accurate bounds calculation
4843
+ // getBoundingClientRect takes transforms into account
4844
+ if (styles.transform !== null) {
4845
+ originalStyles.transform = element.style.transform;
4846
+ element.style.transform = 'none';
4847
+ }
4848
+ // Reset rotate property similarly to transform
4849
+ if (styles.rotate !== null) {
4850
+ originalStyles.rotate = element.style.rotate;
4851
+ element.style.rotate = 'none';
4852
+ }
4853
+ return originalStyles;
4854
+ }
4855
+ /**
4856
+ * Normalize element and its descendants recursively
4857
+ *
4858
+ * @param element - Element to normalize
4859
+ * @param styles - Parsed CSS styles
4860
+ * @returns Original styles map for restoration
4861
+ */
4862
+ static normalizeTree(element, styles) {
4863
+ return this.normalizeElement(element, styles);
4864
+ // Could add recursive normalization here if needed
4865
+ // For now, only normalize the element itself
4866
+ }
4867
+ /**
4868
+ * Restore element styles after rendering
4869
+ *
4870
+ * @param element - Element to restore
4871
+ * @param originalStyles - Original styles to restore
4872
+ */
4873
+ static restoreElement(element, originalStyles) {
4874
+ if (!isHTMLElementNode(element)) {
4875
+ return;
4876
+ }
4877
+ // Restore each property that was saved
4878
+ if (originalStyles.animationDuration !== undefined) {
4879
+ element.style.animationDuration = originalStyles.animationDuration;
4880
+ }
4881
+ if (originalStyles.transform !== undefined) {
4882
+ element.style.transform = originalStyles.transform;
4883
+ }
4884
+ if (originalStyles.rotate !== undefined) {
4885
+ element.style.rotate = originalStyles.rotate;
4886
+ }
4887
+ }
4888
+ }
4889
+
4735
4890
  class ElementContainer {
4736
- constructor(context, element) {
4891
+ constructor(context, element, options = {}) {
4737
4892
  this.context = context;
4738
4893
  this.textNodes = [];
4739
4894
  this.elements = [];
@@ -4741,25 +4896,41 @@
4741
4896
  if (isDebugging(element, 3 /* DebuggerType.PARSE */)) {
4742
4897
  debugger;
4743
4898
  }
4744
- this.styles = new CSSParsedDeclaration(context, window.getComputedStyle(element, null));
4745
- if (isHTMLElementNode(element)) {
4746
- if (this.styles.animationDuration.some((duration) => duration > 0)) {
4747
- element.style.animationDuration = '0s';
4748
- }
4749
- if (this.styles.transform !== null) {
4750
- // getBoundingClientRect takes transforms into account
4751
- element.style.transform = 'none';
4752
- }
4753
- if (this.styles.rotate !== null) {
4754
- // Handle rotate property similarly to transform
4755
- element.style.rotate = 'none';
4756
- }
4899
+ this.styles = new CSSParsedDeclaration(context, context.config.window.getComputedStyle(element, null));
4900
+ // Side effects moved to DOMNormalizer (can be disabled via options)
4901
+ const shouldNormalize = options.normalizeDom !== false; // Default: true
4902
+ if (shouldNormalize && isHTMLElementNode(element)) {
4903
+ this.originalStyles = DOMNormalizer.normalizeElement(element, this.styles);
4904
+ this.originalElement = element; // Save reference for restoration
4757
4905
  }
4758
4906
  this.bounds = parseBounds(this.context, element);
4759
4907
  if (isDebugging(element, 4 /* DebuggerType.RENDER */)) {
4760
4908
  this.flags |= 16 /* FLAGS.DEBUG_RENDER */;
4761
4909
  }
4762
4910
  }
4911
+ /**
4912
+ * Restore original element styles (if normalized)
4913
+ * Call this after rendering is complete to clean up DOM state
4914
+ */
4915
+ restore() {
4916
+ if (this.originalStyles && this.originalElement) {
4917
+ DOMNormalizer.restoreElement(this.originalElement, this.originalStyles);
4918
+ // Clear references to prevent memory leaks
4919
+ this.originalStyles = undefined;
4920
+ this.originalElement = undefined;
4921
+ }
4922
+ }
4923
+ /**
4924
+ * Recursively restore all elements in the tree
4925
+ * Call this on the root container after rendering is complete
4926
+ */
4927
+ restoreTree() {
4928
+ this.restore();
4929
+ // Recursively restore all child elements
4930
+ for (const child of this.elements) {
4931
+ child.restoreTree();
4932
+ }
4933
+ }
4763
4934
  }
4764
4935
 
4765
4936
  /*
@@ -5613,17 +5784,19 @@
5613
5784
  }
5614
5785
 
5615
5786
  class IFrameElementContainer extends ElementContainer {
5616
- constructor(context, iframe) {
5787
+ constructor(context, iframe, parseTreeFn) {
5617
5788
  super(context, iframe);
5618
5789
  this.src = iframe.src;
5619
5790
  this.width = parseInt(iframe.width, 10) || 0;
5620
5791
  this.height = parseInt(iframe.height, 10) || 0;
5621
5792
  this.backgroundColor = this.styles.backgroundColor;
5793
+ this.parseTreeFn = parseTreeFn;
5622
5794
  try {
5623
5795
  if (iframe.contentWindow &&
5624
5796
  iframe.contentWindow.document &&
5625
- iframe.contentWindow.document.documentElement) {
5626
- this.tree = parseTree(context, iframe.contentWindow.document.documentElement);
5797
+ iframe.contentWindow.document.documentElement &&
5798
+ this.parseTreeFn) {
5799
+ this.tree = this.parseTreeFn(context, iframe.contentWindow.document.documentElement);
5627
5800
  // http://www.w3.org/TR/css3-background/#special-backgrounds
5628
5801
  const documentBackgroundColor = iframe.contentWindow.document.documentElement
5629
5802
  ? parseColor(context, getComputedStyle(iframe.contentWindow.document.documentElement).backgroundColor)
@@ -5707,7 +5880,7 @@
5707
5880
  return new TextareaElementContainer(context, element);
5708
5881
  }
5709
5882
  if (isIFrameElement(element)) {
5710
- return new IFrameElementContainer(context, element);
5883
+ return new IFrameElementContainer(context, element, parseTree);
5711
5884
  }
5712
5885
  return new ElementContainer(context, element);
5713
5886
  };
@@ -5736,12 +5909,7 @@
5736
5909
  contains(styles.display, 536870912 /* DISPLAY.INLINE_GRID */) ||
5737
5910
  contains(styles.display, 134217728 /* DISPLAY.INLINE_TABLE */));
5738
5911
  };
5739
- const isTextNode = (node) => node.nodeType === Node.TEXT_NODE;
5740
- const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE;
5741
- const isHTMLElementNode = (node) => isElementNode(node) && typeof node.style !== 'undefined' && !isSVGElementNode(node);
5742
- const isSVGElementNode = (element) => typeof element.className === 'object';
5743
- const isLIElement = (node) => node.tagName === 'LI';
5744
- const isOLElement = (node) => node.tagName === 'OL';
5912
+ // Type guards moved to node-type-guards.ts and re-exported above
5745
5913
  const isInputElement = (node) => node.tagName === 'INPUT';
5746
5914
  const isHTMLElement = (node) => node.tagName === 'HTML';
5747
5915
  const isSVGElement = (node) => node.tagName === 'svg';
@@ -5756,7 +5924,7 @@
5756
5924
  const isSelectElement = (node) => node.tagName === 'SELECT';
5757
5925
  const isSlotElement = (node) => node.tagName === 'SLOT';
5758
5926
  // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name
5759
- const isCustomElement = (node) => node.tagName.indexOf('-') > 0;
5927
+ // isCustomElement moved to node-type-guards.ts and re-exported above
5760
5928
 
5761
5929
  class CounterState {
5762
5930
  constructor() {
@@ -6164,7 +6332,7 @@
6164
6332
  toIFrame(ownerDocument, windowSize) {
6165
6333
  const iframe = createIFrameContainer(ownerDocument, windowSize, this.options.iframeContainer);
6166
6334
  if (!iframe.contentWindow) {
6167
- return Promise.reject(`Unable to find iframe window`);
6335
+ throw new Error('Unable to find iframe window');
6168
6336
  }
6169
6337
  const scrollX = ownerDocument.defaultView.pageXOffset;
6170
6338
  const scrollY = ownerDocument.defaultView.pageYOffset;
@@ -6186,7 +6354,7 @@
6186
6354
  const onclone = this.options.onclone;
6187
6355
  const referenceElement = this.clonedReferenceElement;
6188
6356
  if (typeof referenceElement === 'undefined') {
6189
- return Promise.reject(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
6357
+ throw new Error(`Error finding the ${this.referenceElement.nodeName} in the cloned document`);
6190
6358
  }
6191
6359
  if (documentClone.fonts && documentClone.fonts.ready) {
6192
6360
  await documentClone.fonts.ready;
@@ -6262,7 +6430,7 @@
6262
6430
  clone.loading = 'eager';
6263
6431
  }
6264
6432
  }
6265
- if (isCustomElement(clone)) {
6433
+ if (isCustomElement(clone) && !isSVGElementNode(clone)) {
6266
6434
  return this.createCustomElementClone(clone);
6267
6435
  }
6268
6436
  return clone;
@@ -6818,152 +6986,14 @@
6818
6986
  headEle?.insertBefore(baseNode, headEle?.firstChild ?? null);
6819
6987
  };
6820
6988
 
6821
- class CacheStorage {
6822
- static getOrigin(url) {
6823
- const link = CacheStorage._link;
6824
- if (!link) {
6825
- return 'about:blank';
6826
- }
6827
- link.href = url;
6828
- link.href = link.href; // IE9, LOL! - http://jsfiddle.net/niklasvh/2e48b/
6829
- return link.protocol + link.hostname + link.port;
6830
- }
6831
- static isSameOrigin(src) {
6832
- return CacheStorage.getOrigin(src) === CacheStorage._origin;
6989
+ class Vector {
6990
+ constructor(x, y) {
6991
+ this.type = 0 /* PathType.VECTOR */;
6992
+ this.x = x;
6993
+ this.y = y;
6833
6994
  }
6834
- static setContext(window) {
6835
- CacheStorage._link = window.document.createElement('a');
6836
- CacheStorage._origin = CacheStorage.getOrigin(window.location.href);
6837
- }
6838
- }
6839
- CacheStorage._origin = 'about:blank';
6840
- class Cache {
6841
- constructor(context, _options) {
6842
- this.context = context;
6843
- this._options = _options;
6844
- this._cache = {};
6845
- }
6846
- addImage(src) {
6847
- const result = Promise.resolve();
6848
- if (this.has(src)) {
6849
- return result;
6850
- }
6851
- if (isBlobImage(src) || isRenderable(src)) {
6852
- (this._cache[src] = this.loadImage(src)).catch(() => {
6853
- // prevent unhandled rejection
6854
- });
6855
- return result;
6856
- }
6857
- return result;
6858
- }
6859
- match(src) {
6860
- return this._cache[src];
6861
- }
6862
- async loadImage(key) {
6863
- const isSameOrigin = typeof this._options.customIsSameOrigin === 'function'
6864
- ? await this._options.customIsSameOrigin(key, CacheStorage.isSameOrigin)
6865
- : CacheStorage.isSameOrigin(key);
6866
- const useCORS = !isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
6867
- const useProxy = !isInlineImage(key) &&
6868
- !isSameOrigin &&
6869
- !isBlobImage(key) &&
6870
- typeof this._options.proxy === 'string' &&
6871
- FEATURES.SUPPORT_CORS_XHR &&
6872
- !useCORS;
6873
- if (!isSameOrigin &&
6874
- this._options.allowTaint === false &&
6875
- !isInlineImage(key) &&
6876
- !isBlobImage(key) &&
6877
- !useProxy &&
6878
- !useCORS) {
6879
- return;
6880
- }
6881
- let src = key;
6882
- if (useProxy) {
6883
- src = await this.proxy(src);
6884
- }
6885
- this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
6886
- return await new Promise((resolve, reject) => {
6887
- const img = new Image();
6888
- img.onload = () => resolve(img);
6889
- img.onerror = reject;
6890
- //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
6891
- if (isInlineBase64Image(src) || useCORS) {
6892
- img.crossOrigin = 'anonymous';
6893
- }
6894
- img.src = src;
6895
- if (img.complete === true) {
6896
- // Inline XML images may fail to parse, throwing an Error later on
6897
- setTimeout(() => resolve(img), 500);
6898
- }
6899
- if (this._options.imageTimeout > 0) {
6900
- setTimeout(() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout);
6901
- }
6902
- });
6903
- }
6904
- has(key) {
6905
- return typeof this._cache[key] !== 'undefined';
6906
- }
6907
- keys() {
6908
- return Promise.resolve(Object.keys(this._cache));
6909
- }
6910
- proxy(src) {
6911
- const proxy = this._options.proxy;
6912
- if (!proxy) {
6913
- throw new Error('No proxy defined');
6914
- }
6915
- const key = src.substring(0, 256);
6916
- return new Promise((resolve, reject) => {
6917
- const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
6918
- const xhr = new XMLHttpRequest();
6919
- xhr.onload = () => {
6920
- if (xhr.status === 200) {
6921
- if (responseType === 'text') {
6922
- resolve(xhr.response);
6923
- }
6924
- else {
6925
- const reader = new FileReader();
6926
- reader.addEventListener('load', () => resolve(reader.result), false);
6927
- reader.addEventListener('error', (e) => reject(e), false);
6928
- reader.readAsDataURL(xhr.response);
6929
- }
6930
- }
6931
- else {
6932
- reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
6933
- }
6934
- };
6935
- xhr.onerror = reject;
6936
- const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
6937
- xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
6938
- if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
6939
- xhr.responseType = responseType;
6940
- }
6941
- if (this._options.imageTimeout) {
6942
- const timeout = this._options.imageTimeout;
6943
- xhr.timeout = timeout;
6944
- xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
6945
- }
6946
- xhr.send();
6947
- });
6948
- }
6949
- }
6950
- const INLINE_SVG = /^data:image\/svg\+xml/i;
6951
- const INLINE_BASE64 = /^data:image\/.*;base64,/i;
6952
- const INLINE_IMG = /^data:image\/.*/i;
6953
- const isRenderable = (src) => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
6954
- const isInlineImage = (src) => INLINE_IMG.test(src);
6955
- const isInlineBase64Image = (src) => INLINE_BASE64.test(src);
6956
- const isBlobImage = (src) => src.substr(0, 4) === 'blob';
6957
- const isSVG = (src) => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
6958
-
6959
- class Vector {
6960
- constructor(x, y) {
6961
- this.type = 0 /* PathType.VECTOR */;
6962
- this.x = x;
6963
- this.y = y;
6964
- }
6965
- add(deltaX, deltaY) {
6966
- return new Vector(this.x + deltaX, this.y + deltaY);
6995
+ add(deltaX, deltaY) {
6996
+ return new Vector(this.x + deltaX, this.y + deltaY);
6967
6997
  }
6968
6998
  }
6969
6999
 
@@ -7394,103 +7424,6 @@
7394
7424
  return root;
7395
7425
  };
7396
7426
 
7397
- const parsePathForBorder = (curves, borderSide) => {
7398
- switch (borderSide) {
7399
- case 0:
7400
- return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox);
7401
- case 1:
7402
- return createPathFromCurves(curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox);
7403
- case 2:
7404
- return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox);
7405
- case 3:
7406
- default:
7407
- return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox, curves.topLeftBorderBox, curves.topLeftPaddingBox);
7408
- }
7409
- };
7410
- const parsePathForBorderDoubleOuter = (curves, borderSide) => {
7411
- switch (borderSide) {
7412
- case 0:
7413
- return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox);
7414
- case 1:
7415
- return createPathFromCurves(curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox);
7416
- case 2:
7417
- return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox);
7418
- case 3:
7419
- default:
7420
- return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox, curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox);
7421
- }
7422
- };
7423
- const parsePathForBorderDoubleInner = (curves, borderSide) => {
7424
- switch (borderSide) {
7425
- case 0:
7426
- return createPathFromCurves(curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox);
7427
- case 1:
7428
- return createPathFromCurves(curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox);
7429
- case 2:
7430
- return createPathFromCurves(curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox);
7431
- case 3:
7432
- default:
7433
- return createPathFromCurves(curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox, curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox);
7434
- }
7435
- };
7436
- const parsePathForBorderStroke = (curves, borderSide) => {
7437
- switch (borderSide) {
7438
- case 0:
7439
- return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
7440
- case 1:
7441
- return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
7442
- case 2:
7443
- return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
7444
- case 3:
7445
- default:
7446
- return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
7447
- }
7448
- };
7449
- const createStrokePathFromCurves = (outer1, outer2) => {
7450
- const path = [];
7451
- if (isBezierCurve(outer1)) {
7452
- path.push(outer1.subdivide(0.5, false));
7453
- }
7454
- else {
7455
- path.push(outer1);
7456
- }
7457
- if (isBezierCurve(outer2)) {
7458
- path.push(outer2.subdivide(0.5, true));
7459
- }
7460
- else {
7461
- path.push(outer2);
7462
- }
7463
- return path;
7464
- };
7465
- const createPathFromCurves = (outer1, inner1, outer2, inner2) => {
7466
- const path = [];
7467
- if (isBezierCurve(outer1)) {
7468
- path.push(outer1.subdivide(0.5, false));
7469
- }
7470
- else {
7471
- path.push(outer1);
7472
- }
7473
- if (isBezierCurve(outer2)) {
7474
- path.push(outer2.subdivide(0.5, true));
7475
- }
7476
- else {
7477
- path.push(outer2);
7478
- }
7479
- if (isBezierCurve(inner2)) {
7480
- path.push(inner2.subdivide(0.5, true).reverse());
7481
- }
7482
- else {
7483
- path.push(inner2);
7484
- }
7485
- if (isBezierCurve(inner1)) {
7486
- path.push(inner1.subdivide(0.5, false).reverse());
7487
- }
7488
- else {
7489
- path.push(inner1);
7490
- }
7491
- return path;
7492
- };
7493
-
7494
7427
  const paddingBox = (element) => {
7495
7428
  const bounds = element.bounds;
7496
7429
  const styles = element.styles;
@@ -7735,248 +7668,834 @@
7735
7668
  }
7736
7669
  }
7737
7670
 
7738
- const MASK_OFFSET = 10000;
7739
- class CanvasRenderer extends Renderer {
7740
- constructor(context, options) {
7741
- super(context, options);
7742
- this._activeEffects = [];
7743
- this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
7744
- this.ctx = this.canvas.getContext('2d');
7745
- if (!options.canvas) {
7746
- this.canvas.width = Math.floor(options.width * options.scale);
7747
- this.canvas.height = Math.floor(options.height * options.scale);
7748
- this.canvas.style.width = `${options.width}px`;
7749
- this.canvas.style.height = `${options.height}px`;
7750
- }
7751
- this.fontMetrics = new FontMetrics(document);
7752
- this.ctx.scale(this.options.scale, this.options.scale);
7753
- this.ctx.translate(-options.x, -options.y);
7754
- this.ctx.textBaseline = 'bottom';
7755
- this._activeEffects = [];
7756
- this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
7671
+ /**
7672
+ * Background Renderer
7673
+ *
7674
+ * Handles rendering of element backgrounds including:
7675
+ * - Background colors
7676
+ * - Background images (URL)
7677
+ * - Linear gradients
7678
+ * - Radial gradients
7679
+ * - Background patterns and repeats
7680
+ */
7681
+ /**
7682
+ * Background Renderer
7683
+ *
7684
+ * Specialized renderer for element backgrounds.
7685
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
7686
+ */
7687
+ class BackgroundRenderer {
7688
+ constructor(deps) {
7689
+ this.ctx = deps.ctx;
7690
+ this.context = deps.context;
7691
+ this.canvas = deps.canvas;
7692
+ // Options stored in deps but not needed as instance property
7757
7693
  }
7758
- applyEffects(effects) {
7759
- while (this._activeEffects.length) {
7760
- this.popEffect();
7694
+ /**
7695
+ * Render background images for a container
7696
+ * Supports URL images, linear gradients, and radial gradients
7697
+ *
7698
+ * @param container - Element container with background styles
7699
+ */
7700
+ async renderBackgroundImage(container) {
7701
+ let index = container.styles.backgroundImage.length - 1;
7702
+ for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
7703
+ if (backgroundImage.type === 0 /* CSSImageType.URL */) {
7704
+ await this.renderBackgroundURLImage(container, backgroundImage, index);
7705
+ }
7706
+ else if (isLinearGradient(backgroundImage)) {
7707
+ this.renderLinearGradient(container, backgroundImage, index);
7708
+ }
7709
+ else if (isRadialGradient(backgroundImage)) {
7710
+ this.renderRadialGradient(container, backgroundImage, index);
7711
+ }
7712
+ index--;
7761
7713
  }
7762
- effects.forEach((effect) => this.applyEffect(effect));
7763
7714
  }
7764
- applyEffect(effect) {
7765
- this.ctx.save();
7766
- if (isOpacityEffect(effect)) {
7767
- this.ctx.globalAlpha = effect.opacity;
7768
- }
7769
- if (isTransformEffect(effect)) {
7770
- this.ctx.translate(effect.offsetX, effect.offsetY);
7771
- this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
7772
- this.ctx.translate(-effect.offsetX, -effect.offsetY);
7773
- }
7774
- if (isClipEffect(effect)) {
7775
- this.path(effect.path);
7776
- this.ctx.clip();
7715
+ /**
7716
+ * Render a URL-based background image
7717
+ */
7718
+ async renderBackgroundURLImage(container, backgroundImage, index) {
7719
+ let image;
7720
+ const url = backgroundImage.url;
7721
+ try {
7722
+ image = await this.context.cache.match(url);
7777
7723
  }
7778
- this._activeEffects.push(effect);
7779
- }
7780
- popEffect() {
7781
- this._activeEffects.pop();
7782
- this.ctx.restore();
7783
- }
7784
- async renderStack(stack) {
7785
- const styles = stack.element.container.styles;
7786
- if (styles.isVisible()) {
7787
- await this.renderStackContent(stack);
7724
+ catch (e) {
7725
+ this.context.logger.error(`Error loading background-image ${url}`);
7726
+ }
7727
+ if (image) {
7728
+ const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
7729
+ const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
7730
+ const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
7731
+ imageWidth,
7732
+ imageHeight,
7733
+ imageWidth / imageHeight
7734
+ ]);
7735
+ const pattern = this.ctx.createPattern(this.resizeImage(image, width, height, container.styles.imageRendering), 'repeat');
7736
+ this.renderRepeat(path, pattern, x, y);
7788
7737
  }
7789
7738
  }
7790
- async renderNode(paint) {
7791
- if (contains(paint.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
7792
- debugger;
7793
- }
7794
- if (paint.container.styles.isVisible()) {
7795
- await this.renderNodeBackgroundAndBorders(paint);
7796
- await this.renderNodeContent(paint);
7739
+ /**
7740
+ * Render a linear gradient background
7741
+ */
7742
+ renderLinearGradient(container, backgroundImage, index) {
7743
+ const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
7744
+ const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
7745
+ const ownerDocument = this.canvas.ownerDocument ?? document;
7746
+ const canvas = ownerDocument.createElement('canvas');
7747
+ canvas.width = width;
7748
+ canvas.height = height;
7749
+ const ctx = canvas.getContext('2d');
7750
+ const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
7751
+ processColorStops(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, asString(colorStop.color)));
7752
+ ctx.fillStyle = gradient;
7753
+ ctx.fillRect(0, 0, width, height);
7754
+ if (width > 0 && height > 0) {
7755
+ const pattern = this.ctx.createPattern(canvas, 'repeat');
7756
+ this.renderRepeat(path, pattern, x, y);
7797
7757
  }
7798
7758
  }
7799
- renderTextWithLetterSpacing(text, letterSpacing, baseline) {
7800
- if (letterSpacing === 0) {
7801
- // Use alphabetic baseline for consistent text positioning across browsers
7802
- // Issue #129: text.bounds.top + text.bounds.height causes text to render too low
7803
- this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
7804
- }
7805
- else {
7806
- const letters = segmentGraphemes(text.text);
7807
- letters.reduce((left, letter) => {
7808
- this.ctx.fillText(letter, left, text.bounds.top + baseline);
7809
- return left + this.ctx.measureText(letter).width;
7810
- }, text.bounds.left);
7759
+ /**
7760
+ * Render a radial gradient background
7761
+ */
7762
+ renderRadialGradient(container, backgroundImage, index) {
7763
+ const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
7764
+ const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
7765
+ const x = getAbsoluteValue(position[0], width);
7766
+ const y = getAbsoluteValue(position[position.length - 1], height);
7767
+ let [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
7768
+ // Handle edge case where radial gradient size is 0
7769
+ // Use a minimum value of 0.01 to ensure gradient is still rendered
7770
+ if (rx === 0 || ry === 0) {
7771
+ rx = Math.max(rx, 0.01);
7772
+ ry = Math.max(ry, 0.01);
7773
+ }
7774
+ if (rx > 0 && ry > 0) {
7775
+ const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
7776
+ processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)));
7777
+ this.path(path);
7778
+ this.ctx.fillStyle = radialGradient;
7779
+ if (rx !== ry) {
7780
+ // transforms for elliptical radial gradient
7781
+ const midX = container.bounds.left + 0.5 * container.bounds.width;
7782
+ const midY = container.bounds.top + 0.5 * container.bounds.height;
7783
+ const f = ry / rx;
7784
+ const invF = 1 / f;
7785
+ this.ctx.save();
7786
+ this.ctx.translate(midX, midY);
7787
+ this.ctx.transform(1, 0, 0, f, 0, 0);
7788
+ this.ctx.translate(-midX, -midY);
7789
+ this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
7790
+ this.ctx.restore();
7791
+ }
7792
+ else {
7793
+ this.ctx.fill();
7794
+ }
7811
7795
  }
7812
7796
  }
7813
7797
  /**
7814
- * Helper method to render text with paint order support
7815
- * Reduces code duplication in line-clamp and normal rendering
7798
+ * Render a repeating pattern with offset
7799
+ *
7800
+ * @param path - Path to fill
7801
+ * @param pattern - Canvas pattern or gradient
7802
+ * @param offsetX - X offset for pattern
7803
+ * @param offsetY - Y offset for pattern
7816
7804
  */
7817
- renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
7818
- paintOrderLayers.forEach((paintOrderLayer) => {
7819
- switch (paintOrderLayer) {
7820
- case 0 /* PAINT_ORDER_LAYER.FILL */:
7821
- this.ctx.fillStyle = asString(styles.color);
7822
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7823
- break;
7824
- case 1 /* PAINT_ORDER_LAYER.STROKE */:
7825
- if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
7826
- this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
7827
- this.ctx.lineWidth = styles.webkitTextStrokeWidth;
7828
- this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
7829
- this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
7830
- }
7831
- break;
7832
- }
7833
- });
7805
+ renderRepeat(path, pattern, offsetX, offsetY) {
7806
+ this.path(path);
7807
+ this.ctx.fillStyle = pattern;
7808
+ this.ctx.translate(offsetX, offsetY);
7809
+ this.ctx.fill();
7810
+ this.ctx.translate(-offsetX, -offsetY);
7834
7811
  }
7835
- renderTextDecoration(bounds, styles) {
7836
- this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
7837
- // Calculate decoration line thickness
7838
- let thickness = 1; // default
7839
- if (typeof styles.textDecorationThickness === 'number') {
7840
- thickness = styles.textDecorationThickness;
7812
+ /**
7813
+ * Resize an image to target dimensions
7814
+ *
7815
+ * @param image - Source image
7816
+ * @param width - Target width
7817
+ * @param height - Target height
7818
+ * @param imageRendering - CSS image-rendering property value
7819
+ * @returns Resized canvas or original image
7820
+ */
7821
+ resizeImage(image, width, height, imageRendering) {
7822
+ // https://github.com/niklasvh/html2canvas/pull/2911
7823
+ // if (image.width === width && image.height === height) {
7824
+ // return image;
7825
+ // }
7826
+ const ownerDocument = this.canvas.ownerDocument ?? document;
7827
+ const canvas = ownerDocument.createElement('canvas');
7828
+ canvas.width = Math.max(1, width);
7829
+ canvas.height = Math.max(1, height);
7830
+ const ctx = canvas.getContext('2d');
7831
+ // Apply image smoothing based on CSS image-rendering property
7832
+ if (imageRendering === exports.IMAGE_RENDERING.PIXELATED || imageRendering === exports.IMAGE_RENDERING.CRISP_EDGES) {
7833
+ this.context.logger.debug(`Disabling image smoothing for background image due to CSS image-rendering`);
7834
+ ctx.imageSmoothingEnabled = false;
7841
7835
  }
7842
- else if (styles.textDecorationThickness === 'from-font') {
7843
- // Use a reasonable default based on font size
7844
- thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
7836
+ else if (imageRendering === exports.IMAGE_RENDERING.SMOOTH) {
7837
+ this.context.logger.debug(`Enabling image smoothing for background image due to CSS image-rendering: smooth`);
7838
+ ctx.imageSmoothingEnabled = true;
7845
7839
  }
7846
- // 'auto' uses default thickness of 1
7847
- // Calculate underline offset
7848
- let underlineOffset = 0;
7849
- if (typeof styles.textUnderlineOffset === 'number') {
7850
- // It's a pixel value
7851
- underlineOffset = styles.textUnderlineOffset;
7840
+ else {
7841
+ // AUTO: inherit from main renderer context
7842
+ ctx.imageSmoothingEnabled = this.ctx.imageSmoothingEnabled;
7852
7843
  }
7853
- // 'auto' uses default offset of 0
7854
- const decorationStyle = styles.textDecorationStyle;
7855
- styles.textDecorationLine.forEach((textDecorationLine) => {
7856
- let y = 0;
7857
- switch (textDecorationLine) {
7858
- case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
7859
- y = bounds.top + bounds.height - thickness + underlineOffset;
7860
- break;
7861
- case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
7862
- y = bounds.top;
7863
- break;
7864
- case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
7865
- y = bounds.top + (bounds.height / 2 - thickness / 2);
7866
- break;
7867
- default:
7868
- return;
7869
- }
7870
- this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
7871
- });
7872
- }
7873
- drawDecorationLine(x, y, width, thickness, style) {
7874
- switch (style) {
7875
- case 0 /* TEXT_DECORATION_STYLE.SOLID */:
7876
- // Solid line (default)
7877
- this.ctx.fillRect(x, y, width, thickness);
7878
- break;
7879
- case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
7880
- // Double line
7881
- const gap = Math.max(1, thickness);
7882
- this.ctx.fillRect(x, y, width, thickness);
7883
- this.ctx.fillRect(x, y + thickness + gap, width, thickness);
7884
- break;
7885
- case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
7886
- // Dotted line
7887
- this.ctx.save();
7888
- this.ctx.beginPath();
7889
- this.ctx.setLineDash([thickness, thickness * 2]);
7890
- this.ctx.lineWidth = thickness;
7891
- this.ctx.strokeStyle = this.ctx.fillStyle;
7892
- this.ctx.moveTo(x, y + thickness / 2);
7893
- this.ctx.lineTo(x + width, y + thickness / 2);
7894
- this.ctx.stroke();
7895
- this.ctx.restore();
7896
- break;
7897
- case 3 /* TEXT_DECORATION_STYLE.DASHED */:
7898
- // Dashed line
7899
- this.ctx.save();
7900
- this.ctx.beginPath();
7901
- this.ctx.setLineDash([thickness * 3, thickness * 2]);
7902
- this.ctx.lineWidth = thickness;
7903
- this.ctx.strokeStyle = this.ctx.fillStyle;
7904
- this.ctx.moveTo(x, y + thickness / 2);
7905
- this.ctx.lineTo(x + width, y + thickness / 2);
7906
- this.ctx.stroke();
7907
- this.ctx.restore();
7908
- break;
7909
- case 4 /* TEXT_DECORATION_STYLE.WAVY */:
7910
- // Wavy line (approximation using quadratic curves)
7911
- this.ctx.save();
7912
- this.ctx.beginPath();
7913
- this.ctx.lineWidth = thickness;
7914
- this.ctx.strokeStyle = this.ctx.fillStyle;
7915
- const amplitude = thickness * 2;
7916
- const wavelength = thickness * 4;
7917
- let currentX = x;
7918
- this.ctx.moveTo(currentX, y + thickness / 2);
7919
- while (currentX < x + width) {
7920
- const nextX = Math.min(currentX + wavelength / 2, x + width);
7921
- this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
7922
- currentX = nextX;
7923
- if (currentX < x + width) {
7924
- const nextX2 = Math.min(currentX + wavelength / 2, x + width);
7925
- this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
7926
- currentX = nextX2;
7927
- }
7928
- }
7929
- this.ctx.stroke();
7930
- this.ctx.restore();
7931
- break;
7932
- default:
7933
- // Fallback to solid
7934
- this.ctx.fillRect(x, y, width, thickness);
7844
+ // Inherit quality setting
7845
+ if (this.ctx.imageSmoothingQuality) {
7846
+ ctx.imageSmoothingQuality = this.ctx.imageSmoothingQuality;
7935
7847
  }
7848
+ ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
7849
+ return canvas;
7936
7850
  }
7937
- // Helper method to truncate text and add ellipsis if needed
7938
- truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
7939
- const ellipsis = '...';
7940
- const ellipsisWidth = this.ctx.measureText(ellipsis).width;
7941
- if (letterSpacing === 0) {
7942
- let truncated = text;
7943
- while (this.ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
7944
- truncated = truncated.slice(0, -1);
7851
+ /**
7852
+ * Create a canvas path from path array
7853
+ *
7854
+ * @param paths - Array of path points
7855
+ */
7856
+ path(paths) {
7857
+ this.ctx.beginPath();
7858
+ this.formatPath(paths);
7859
+ this.ctx.closePath();
7860
+ }
7861
+ /**
7862
+ * Format path points into canvas path
7863
+ *
7864
+ * @param paths - Array of path points
7865
+ */
7866
+ formatPath(paths) {
7867
+ paths.forEach((point, index) => {
7868
+ const start = isBezierCurve(point) ? point.start : point;
7869
+ if (index === 0) {
7870
+ this.ctx.moveTo(start.x, start.y);
7945
7871
  }
7946
- return truncated + ellipsis;
7947
- }
7948
- else {
7949
- const letters = segmentGraphemes(text);
7950
- let width = ellipsisWidth;
7951
- let result = [];
7952
- for (const letter of letters) {
7953
- const letterWidth = this.ctx.measureText(letter).width + letterSpacing;
7954
- if (width + letterWidth > maxWidth) {
7955
- break;
7956
- }
7957
- result.push(letter);
7958
- width += letterWidth;
7872
+ else {
7873
+ this.ctx.lineTo(start.x, start.y);
7959
7874
  }
7960
- return result.join('') + ellipsis;
7961
- }
7962
- }
7963
- createFontStyle(styles) {
7964
- const fontVariant = styles.fontVariant
7965
- .filter((variant) => variant === 'normal' || variant === 'small-caps')
7966
- .join('');
7967
- const fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
7968
- const fontSize = isDimensionToken(styles.fontSize)
7969
- ? `${styles.fontSize.number}${styles.fontSize.unit}`
7970
- : `${styles.fontSize.number}px`;
7971
- return [
7972
- [styles.fontStyle, fontVariant, styles.fontWeight, fontSize, fontFamily].join(' '),
7973
- fontFamily,
7974
- fontSize
7975
- ];
7875
+ if (isBezierCurve(point)) {
7876
+ this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
7877
+ }
7878
+ });
7976
7879
  }
7977
- async renderTextNode(text, styles, containerBounds) {
7978
- const [font] = this.createFontStyle(styles);
7979
- this.ctx.font = font;
7880
+ }
7881
+
7882
+ const parsePathForBorder = (curves, borderSide) => {
7883
+ switch (borderSide) {
7884
+ case 0:
7885
+ return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftPaddingBox, curves.topRightBorderBox, curves.topRightPaddingBox);
7886
+ case 1:
7887
+ return createPathFromCurves(curves.topRightBorderBox, curves.topRightPaddingBox, curves.bottomRightBorderBox, curves.bottomRightPaddingBox);
7888
+ case 2:
7889
+ return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox);
7890
+ case 3:
7891
+ default:
7892
+ return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftPaddingBox, curves.topLeftBorderBox, curves.topLeftPaddingBox);
7893
+ }
7894
+ };
7895
+ const parsePathForBorderDoubleOuter = (curves, borderSide) => {
7896
+ switch (borderSide) {
7897
+ case 0:
7898
+ return createPathFromCurves(curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox, curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox);
7899
+ case 1:
7900
+ return createPathFromCurves(curves.topRightBorderBox, curves.topRightBorderDoubleOuterBox, curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox);
7901
+ case 2:
7902
+ return createPathFromCurves(curves.bottomRightBorderBox, curves.bottomRightBorderDoubleOuterBox, curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox);
7903
+ case 3:
7904
+ default:
7905
+ return createPathFromCurves(curves.bottomLeftBorderBox, curves.bottomLeftBorderDoubleOuterBox, curves.topLeftBorderBox, curves.topLeftBorderDoubleOuterBox);
7906
+ }
7907
+ };
7908
+ const parsePathForBorderDoubleInner = (curves, borderSide) => {
7909
+ switch (borderSide) {
7910
+ case 0:
7911
+ return createPathFromCurves(curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox, curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox);
7912
+ case 1:
7913
+ return createPathFromCurves(curves.topRightBorderDoubleInnerBox, curves.topRightPaddingBox, curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox);
7914
+ case 2:
7915
+ return createPathFromCurves(curves.bottomRightBorderDoubleInnerBox, curves.bottomRightPaddingBox, curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox);
7916
+ case 3:
7917
+ default:
7918
+ return createPathFromCurves(curves.bottomLeftBorderDoubleInnerBox, curves.bottomLeftPaddingBox, curves.topLeftBorderDoubleInnerBox, curves.topLeftPaddingBox);
7919
+ }
7920
+ };
7921
+ const parsePathForBorderStroke = (curves, borderSide) => {
7922
+ switch (borderSide) {
7923
+ case 0:
7924
+ return createStrokePathFromCurves(curves.topLeftBorderStroke, curves.topRightBorderStroke);
7925
+ case 1:
7926
+ return createStrokePathFromCurves(curves.topRightBorderStroke, curves.bottomRightBorderStroke);
7927
+ case 2:
7928
+ return createStrokePathFromCurves(curves.bottomRightBorderStroke, curves.bottomLeftBorderStroke);
7929
+ case 3:
7930
+ default:
7931
+ return createStrokePathFromCurves(curves.bottomLeftBorderStroke, curves.topLeftBorderStroke);
7932
+ }
7933
+ };
7934
+ const createStrokePathFromCurves = (outer1, outer2) => {
7935
+ const path = [];
7936
+ if (isBezierCurve(outer1)) {
7937
+ path.push(outer1.subdivide(0.5, false));
7938
+ }
7939
+ else {
7940
+ path.push(outer1);
7941
+ }
7942
+ if (isBezierCurve(outer2)) {
7943
+ path.push(outer2.subdivide(0.5, true));
7944
+ }
7945
+ else {
7946
+ path.push(outer2);
7947
+ }
7948
+ return path;
7949
+ };
7950
+ const createPathFromCurves = (outer1, inner1, outer2, inner2) => {
7951
+ const path = [];
7952
+ if (isBezierCurve(outer1)) {
7953
+ path.push(outer1.subdivide(0.5, false));
7954
+ }
7955
+ else {
7956
+ path.push(outer1);
7957
+ }
7958
+ if (isBezierCurve(outer2)) {
7959
+ path.push(outer2.subdivide(0.5, true));
7960
+ }
7961
+ else {
7962
+ path.push(outer2);
7963
+ }
7964
+ if (isBezierCurve(inner2)) {
7965
+ path.push(inner2.subdivide(0.5, true).reverse());
7966
+ }
7967
+ else {
7968
+ path.push(inner2);
7969
+ }
7970
+ if (isBezierCurve(inner1)) {
7971
+ path.push(inner1.subdivide(0.5, false).reverse());
7972
+ }
7973
+ else {
7974
+ path.push(inner1);
7975
+ }
7976
+ return path;
7977
+ };
7978
+
7979
+ /**
7980
+ * Border Renderer
7981
+ *
7982
+ * Handles rendering of element borders including:
7983
+ * - Solid borders
7984
+ * - Double borders
7985
+ * - Dashed borders
7986
+ * - Dotted borders
7987
+ */
7988
+ /**
7989
+ * Border Renderer
7990
+ *
7991
+ * Specialized renderer for element borders.
7992
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
7993
+ */
7994
+ class BorderRenderer {
7995
+ constructor(deps, pathCallbacks) {
7996
+ this.ctx = deps.ctx;
7997
+ this.pathCallbacks = pathCallbacks;
7998
+ }
7999
+ /**
8000
+ * Render a solid border
8001
+ *
8002
+ * @param color - Border color
8003
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
8004
+ * @param curvePoints - Border curve points
8005
+ */
8006
+ async renderSolidBorder(color, side, curvePoints) {
8007
+ this.pathCallbacks.path(parsePathForBorder(curvePoints, side));
8008
+ this.ctx.fillStyle = asString(color);
8009
+ this.ctx.fill();
8010
+ }
8011
+ /**
8012
+ * Render a double border
8013
+ * Falls back to solid border if width is too small
8014
+ *
8015
+ * @param color - Border color
8016
+ * @param width - Border width
8017
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
8018
+ * @param curvePoints - Border curve points
8019
+ */
8020
+ async renderDoubleBorder(color, width, side, curvePoints) {
8021
+ if (width < 3) {
8022
+ await this.renderSolidBorder(color, side, curvePoints);
8023
+ return;
8024
+ }
8025
+ const outerPaths = parsePathForBorderDoubleOuter(curvePoints, side);
8026
+ this.pathCallbacks.path(outerPaths);
8027
+ this.ctx.fillStyle = asString(color);
8028
+ this.ctx.fill();
8029
+ const innerPaths = parsePathForBorderDoubleInner(curvePoints, side);
8030
+ this.pathCallbacks.path(innerPaths);
8031
+ this.ctx.fill();
8032
+ }
8033
+ /**
8034
+ * Render a dashed or dotted border
8035
+ *
8036
+ * @param color - Border color
8037
+ * @param width - Border width
8038
+ * @param side - Border side (0=top, 1=right, 2=bottom, 3=left)
8039
+ * @param curvePoints - Border curve points
8040
+ * @param style - Border style (DASHED or DOTTED)
8041
+ */
8042
+ async renderDashedDottedBorder(color, width, side, curvePoints, style) {
8043
+ this.ctx.save();
8044
+ const strokePaths = parsePathForBorderStroke(curvePoints, side);
8045
+ const boxPaths = parsePathForBorder(curvePoints, side);
8046
+ if (style === 2 /* BORDER_STYLE.DASHED */) {
8047
+ this.pathCallbacks.path(boxPaths);
8048
+ this.ctx.clip();
8049
+ }
8050
+ // Extract start and end coordinates
8051
+ let startX, startY, endX, endY;
8052
+ if (isBezierCurve(boxPaths[0])) {
8053
+ startX = boxPaths[0].start.x;
8054
+ startY = boxPaths[0].start.y;
8055
+ }
8056
+ else {
8057
+ startX = boxPaths[0].x;
8058
+ startY = boxPaths[0].y;
8059
+ }
8060
+ if (isBezierCurve(boxPaths[1])) {
8061
+ endX = boxPaths[1].end.x;
8062
+ endY = boxPaths[1].end.y;
8063
+ }
8064
+ else {
8065
+ endX = boxPaths[1].x;
8066
+ endY = boxPaths[1].y;
8067
+ }
8068
+ // Calculate border length
8069
+ let length;
8070
+ if (side === 0 || side === 2) {
8071
+ length = Math.abs(startX - endX);
8072
+ }
8073
+ else {
8074
+ length = Math.abs(startY - endY);
8075
+ }
8076
+ this.ctx.beginPath();
8077
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8078
+ this.pathCallbacks.formatPath(strokePaths);
8079
+ }
8080
+ else {
8081
+ this.pathCallbacks.formatPath(boxPaths.slice(0, 2));
8082
+ }
8083
+ // Calculate dash and space lengths
8084
+ let dashLength = width < 3 ? width * 3 : width * 2;
8085
+ let spaceLength = width < 3 ? width * 2 : width;
8086
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8087
+ dashLength = width;
8088
+ spaceLength = width;
8089
+ }
8090
+ // Adjust dash pattern for border length
8091
+ let useLineDash = true;
8092
+ if (length <= dashLength * 2) {
8093
+ useLineDash = false;
8094
+ }
8095
+ else if (length <= dashLength * 2 + spaceLength) {
8096
+ const multiplier = length / (2 * dashLength + spaceLength);
8097
+ dashLength *= multiplier;
8098
+ spaceLength *= multiplier;
8099
+ }
8100
+ else {
8101
+ const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
8102
+ const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
8103
+ const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
8104
+ spaceLength =
8105
+ maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
8106
+ ? minSpace
8107
+ : maxSpace;
8108
+ }
8109
+ // Apply line dash pattern
8110
+ if (useLineDash) {
8111
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8112
+ this.ctx.setLineDash([0, dashLength + spaceLength]);
8113
+ }
8114
+ else {
8115
+ this.ctx.setLineDash([dashLength, spaceLength]);
8116
+ }
8117
+ }
8118
+ // Set line style and stroke
8119
+ if (style === 3 /* BORDER_STYLE.DOTTED */) {
8120
+ this.ctx.lineCap = 'round';
8121
+ this.ctx.lineWidth = width;
8122
+ }
8123
+ else {
8124
+ this.ctx.lineWidth = width * 2 + 1.1;
8125
+ }
8126
+ this.ctx.strokeStyle = asString(color);
8127
+ this.ctx.stroke();
8128
+ this.ctx.setLineDash([]);
8129
+ // Fill dashed round edge gaps
8130
+ if (style === 2 /* BORDER_STYLE.DASHED */) {
8131
+ if (isBezierCurve(boxPaths[0])) {
8132
+ const path1 = boxPaths[3];
8133
+ const path2 = boxPaths[0];
8134
+ this.ctx.beginPath();
8135
+ this.pathCallbacks.formatPath([
8136
+ new Vector(path1.end.x, path1.end.y),
8137
+ new Vector(path2.start.x, path2.start.y)
8138
+ ]);
8139
+ this.ctx.stroke();
8140
+ }
8141
+ if (isBezierCurve(boxPaths[1])) {
8142
+ const path1 = boxPaths[1];
8143
+ const path2 = boxPaths[2];
8144
+ this.ctx.beginPath();
8145
+ this.pathCallbacks.formatPath([
8146
+ new Vector(path1.end.x, path1.end.y),
8147
+ new Vector(path2.start.x, path2.start.y)
8148
+ ]);
8149
+ this.ctx.stroke();
8150
+ }
8151
+ }
8152
+ this.ctx.restore();
8153
+ }
8154
+ }
8155
+
8156
+ /**
8157
+ * Effects Renderer
8158
+ *
8159
+ * Handles rendering effects including:
8160
+ * - Opacity effects
8161
+ * - Transform effects (matrix transformations)
8162
+ * - Clip effects (clipping paths)
8163
+ */
8164
+ /**
8165
+ * Effects Renderer
8166
+ *
8167
+ * Manages rendering effects stack including opacity, transforms, and clipping.
8168
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
8169
+ */
8170
+ class EffectsRenderer {
8171
+ constructor(deps, pathCallback) {
8172
+ this.activeEffects = [];
8173
+ this.ctx = deps.ctx;
8174
+ this.pathCallback = pathCallback;
8175
+ }
8176
+ /**
8177
+ * Apply multiple effects
8178
+ * Clears existing effects and applies new ones
8179
+ *
8180
+ * @param effects - Array of effects to apply
8181
+ */
8182
+ applyEffects(effects) {
8183
+ // Clear all existing effects
8184
+ while (this.activeEffects.length) {
8185
+ this.popEffect();
8186
+ }
8187
+ // Apply new effects
8188
+ effects.forEach((effect) => this.applyEffect(effect));
8189
+ }
8190
+ /**
8191
+ * Apply a single effect
8192
+ *
8193
+ * @param effect - Effect to apply
8194
+ */
8195
+ applyEffect(effect) {
8196
+ this.ctx.save();
8197
+ // Apply opacity effect
8198
+ if (isOpacityEffect(effect)) {
8199
+ this.ctx.globalAlpha = effect.opacity;
8200
+ }
8201
+ // Apply transform effect
8202
+ if (isTransformEffect(effect)) {
8203
+ this.ctx.translate(effect.offsetX, effect.offsetY);
8204
+ this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
8205
+ this.ctx.translate(-effect.offsetX, -effect.offsetY);
8206
+ }
8207
+ // Apply clip effect
8208
+ if (isClipEffect(effect)) {
8209
+ this.pathCallback.path(effect.path);
8210
+ this.ctx.clip();
8211
+ }
8212
+ this.activeEffects.push(effect);
8213
+ }
8214
+ /**
8215
+ * Remove the most recent effect
8216
+ * Restores the canvas state before the effect was applied
8217
+ */
8218
+ popEffect() {
8219
+ this.activeEffects.pop();
8220
+ this.ctx.restore();
8221
+ }
8222
+ /**
8223
+ * Get the current number of active effects
8224
+ *
8225
+ * @returns Number of active effects
8226
+ */
8227
+ getActiveEffectCount() {
8228
+ return this.activeEffects.length;
8229
+ }
8230
+ /**
8231
+ * Check if there are any active effects
8232
+ *
8233
+ * @returns True if there are active effects
8234
+ */
8235
+ hasActiveEffects() {
8236
+ return this.activeEffects.length > 0;
8237
+ }
8238
+ }
8239
+
8240
+ /**
8241
+ * Text Renderer
8242
+ *
8243
+ * Handles rendering of text content including:
8244
+ * - Text with letter spacing
8245
+ * - Text decorations (underline, overline, line-through)
8246
+ * - Text shadows
8247
+ * - Webkit line clamp
8248
+ * - Text overflow ellipsis
8249
+ * - Paint order (fill/stroke)
8250
+ * - Font styles
8251
+ */
8252
+ // iOS font fix - see https://github.com/niklasvh/html2canvas/pull/2645
8253
+ const iOSBrokenFonts = ['-apple-system', 'system-ui'];
8254
+ /**
8255
+ * Detect iOS version from user agent
8256
+ * Returns null if not iOS or version cannot be determined
8257
+ */
8258
+ const getIOSVersion = () => {
8259
+ if (typeof navigator === 'undefined') {
8260
+ return null;
8261
+ }
8262
+ const userAgent = navigator.userAgent;
8263
+ // Check if it's iOS or iPadOS
8264
+ // iPadOS 13+ may identify as Macintosh, check for touch support
8265
+ const isIOS = /iPhone|iPad|iPod/.test(userAgent);
8266
+ const isIPadOS = /Macintosh/.test(userAgent) && navigator.maxTouchPoints && navigator.maxTouchPoints > 1;
8267
+ if (!isIOS && !isIPadOS) {
8268
+ return null;
8269
+ }
8270
+ // Extract version number from various iOS user agent formats:
8271
+ // - "iPhone OS 15_0" or "iPhone OS 15_0_1"
8272
+ // - "CPU OS 15_0 like Mac OS X"
8273
+ // - "CPU iPhone OS 15_0 like Mac OS X"
8274
+ // - "Version/15.0" (for iPadOS)
8275
+ const patterns = [
8276
+ /(?:iPhone|CPU(?:\siPhone)?)\sOS\s(\d+)[\._](\d+)/, // iPhone OS, CPU OS, CPU iPhone OS
8277
+ /Version\/(\d+)\.(\d+)/ // Version/15.0 (iPadOS)
8278
+ ];
8279
+ for (const pattern of patterns) {
8280
+ const match = userAgent.match(pattern);
8281
+ if (match && match[1]) {
8282
+ return parseInt(match[1], 10);
8283
+ }
8284
+ }
8285
+ return null;
8286
+ };
8287
+ const fixIOSSystemFonts = (fontFamilies) => {
8288
+ const iosVersion = getIOSVersion();
8289
+ // On iOS 15.0 and 15.1, system fonts have rendering issues
8290
+ // Fixed in iOS 17+
8291
+ if (iosVersion !== null && iosVersion >= 15 && iosVersion < 17) {
8292
+ return fontFamilies.map((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) !== -1
8293
+ ? `-apple-system, "Helvetica Neue", Arial, sans-serif`
8294
+ : fontFamily);
8295
+ }
8296
+ return fontFamilies;
8297
+ };
8298
+ /**
8299
+ * Text Renderer
8300
+ *
8301
+ * Specialized renderer for text content.
8302
+ * Extracted from CanvasRenderer to improve code organization and maintainability.
8303
+ */
8304
+ class TextRenderer {
8305
+ constructor(deps) {
8306
+ this.ctx = deps.ctx;
8307
+ // context stored but not used directly in this renderer
8308
+ this.options = deps.options;
8309
+ }
8310
+ /**
8311
+ * Render text with letter spacing
8312
+ * Public method used by list rendering
8313
+ */
8314
+ renderTextWithLetterSpacing(text, letterSpacing, baseline) {
8315
+ if (letterSpacing === 0) {
8316
+ // Use alphabetic baseline for consistent text positioning across browsers
8317
+ // Issue #129: text.bounds.top + text.bounds.height causes text to render too low
8318
+ this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
8319
+ }
8320
+ else {
8321
+ const letters = segmentGraphemes(text.text);
8322
+ letters.reduce((left, letter) => {
8323
+ this.ctx.fillText(letter, left, text.bounds.top + baseline);
8324
+ return left + this.ctx.measureText(letter).width;
8325
+ }, text.bounds.left);
8326
+ }
8327
+ }
8328
+ /**
8329
+ * Helper method to render text with paint order support
8330
+ * Reduces code duplication in line-clamp and normal rendering
8331
+ */
8332
+ renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
8333
+ paintOrderLayers.forEach((paintOrderLayer) => {
8334
+ switch (paintOrderLayer) {
8335
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
8336
+ this.ctx.fillStyle = asString(styles.color);
8337
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
8338
+ break;
8339
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
8340
+ if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
8341
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8342
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8343
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8344
+ this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
8345
+ }
8346
+ break;
8347
+ }
8348
+ });
8349
+ }
8350
+ renderTextDecoration(bounds, styles) {
8351
+ this.ctx.fillStyle = asString(styles.textDecorationColor || styles.color);
8352
+ // Calculate decoration line thickness
8353
+ let thickness = 1; // default
8354
+ if (typeof styles.textDecorationThickness === 'number') {
8355
+ thickness = styles.textDecorationThickness;
8356
+ }
8357
+ else if (styles.textDecorationThickness === 'from-font') {
8358
+ // Use a reasonable default based on font size
8359
+ thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
8360
+ }
8361
+ // 'auto' uses default thickness of 1
8362
+ // Calculate underline offset
8363
+ let underlineOffset = 0;
8364
+ if (typeof styles.textUnderlineOffset === 'number') {
8365
+ // It's a pixel value
8366
+ underlineOffset = styles.textUnderlineOffset;
8367
+ }
8368
+ // 'auto' uses default offset of 0
8369
+ const decorationStyle = styles.textDecorationStyle;
8370
+ styles.textDecorationLine.forEach((textDecorationLine) => {
8371
+ let y = 0;
8372
+ switch (textDecorationLine) {
8373
+ case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
8374
+ y = bounds.top + bounds.height - thickness + underlineOffset;
8375
+ break;
8376
+ case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
8377
+ y = bounds.top;
8378
+ break;
8379
+ case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
8380
+ y = bounds.top + (bounds.height / 2 - thickness / 2);
8381
+ break;
8382
+ default:
8383
+ return;
8384
+ }
8385
+ this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
8386
+ });
8387
+ }
8388
+ drawDecorationLine(x, y, width, thickness, style) {
8389
+ switch (style) {
8390
+ case 0 /* TEXT_DECORATION_STYLE.SOLID */:
8391
+ // Solid line (default)
8392
+ this.ctx.fillRect(x, y, width, thickness);
8393
+ break;
8394
+ case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
8395
+ // Double line
8396
+ const gap = Math.max(1, thickness);
8397
+ this.ctx.fillRect(x, y, width, thickness);
8398
+ this.ctx.fillRect(x, y + thickness + gap, width, thickness);
8399
+ break;
8400
+ case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
8401
+ // Dotted line
8402
+ this.ctx.save();
8403
+ this.ctx.beginPath();
8404
+ this.ctx.setLineDash([thickness, thickness * 2]);
8405
+ this.ctx.lineWidth = thickness;
8406
+ this.ctx.strokeStyle = this.ctx.fillStyle;
8407
+ this.ctx.moveTo(x, y + thickness / 2);
8408
+ this.ctx.lineTo(x + width, y + thickness / 2);
8409
+ this.ctx.stroke();
8410
+ this.ctx.restore();
8411
+ break;
8412
+ case 3 /* TEXT_DECORATION_STYLE.DASHED */:
8413
+ // Dashed line
8414
+ this.ctx.save();
8415
+ this.ctx.beginPath();
8416
+ this.ctx.setLineDash([thickness * 3, thickness * 2]);
8417
+ this.ctx.lineWidth = thickness;
8418
+ this.ctx.strokeStyle = this.ctx.fillStyle;
8419
+ this.ctx.moveTo(x, y + thickness / 2);
8420
+ this.ctx.lineTo(x + width, y + thickness / 2);
8421
+ this.ctx.stroke();
8422
+ this.ctx.restore();
8423
+ break;
8424
+ case 4 /* TEXT_DECORATION_STYLE.WAVY */:
8425
+ // Wavy line (approximation using quadratic curves)
8426
+ this.ctx.save();
8427
+ this.ctx.beginPath();
8428
+ this.ctx.lineWidth = thickness;
8429
+ this.ctx.strokeStyle = this.ctx.fillStyle;
8430
+ const amplitude = thickness * 2;
8431
+ const wavelength = thickness * 4;
8432
+ let currentX = x;
8433
+ this.ctx.moveTo(currentX, y + thickness / 2);
8434
+ while (currentX < x + width) {
8435
+ const nextX = Math.min(currentX + wavelength / 2, x + width);
8436
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
8437
+ currentX = nextX;
8438
+ if (currentX < x + width) {
8439
+ const nextX2 = Math.min(currentX + wavelength / 2, x + width);
8440
+ this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
8441
+ currentX = nextX2;
8442
+ }
8443
+ }
8444
+ this.ctx.stroke();
8445
+ this.ctx.restore();
8446
+ break;
8447
+ default:
8448
+ // Fallback to solid
8449
+ this.ctx.fillRect(x, y, width, thickness);
8450
+ }
8451
+ }
8452
+ // Helper method to truncate text and add ellipsis if needed
8453
+ truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
8454
+ const ellipsis = '...';
8455
+ const ellipsisWidth = this.ctx.measureText(ellipsis).width;
8456
+ if (letterSpacing === 0) {
8457
+ let truncated = text;
8458
+ while (this.ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
8459
+ truncated = truncated.slice(0, -1);
8460
+ }
8461
+ return truncated + ellipsis;
8462
+ }
8463
+ else {
8464
+ const letters = segmentGraphemes(text);
8465
+ let width = ellipsisWidth;
8466
+ let result = [];
8467
+ for (const letter of letters) {
8468
+ const letterWidth = this.ctx.measureText(letter).width + letterSpacing;
8469
+ if (width + letterWidth > maxWidth) {
8470
+ break;
8471
+ }
8472
+ result.push(letter);
8473
+ width += letterWidth;
8474
+ }
8475
+ return result.join('') + ellipsis;
8476
+ }
8477
+ }
8478
+ /**
8479
+ * Create font style array
8480
+ * Public method used by list rendering
8481
+ */
8482
+ createFontStyle(styles) {
8483
+ const fontVariant = styles.fontVariant
8484
+ .filter((variant) => variant === 'normal' || variant === 'small-caps')
8485
+ .join('');
8486
+ const fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
8487
+ const fontSize = isDimensionToken(styles.fontSize)
8488
+ ? `${styles.fontSize.number}${styles.fontSize.unit}`
8489
+ : `${styles.fontSize.number}px`;
8490
+ return [
8491
+ [styles.fontStyle, fontVariant, styles.fontWeight, fontSize, fontFamily].join(' '),
8492
+ fontFamily,
8493
+ fontSize
8494
+ ];
8495
+ }
8496
+ async renderTextNode(text, styles, containerBounds) {
8497
+ const [font] = this.createFontStyle(styles);
8498
+ this.ctx.font = font;
7980
8499
  this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
7981
8500
  this.ctx.textAlign = 'left';
7982
8501
  this.ctx.textBaseline = 'alphabetic';
@@ -8142,846 +8661,1756 @@
8142
8661
  }
8143
8662
  break;
8144
8663
  case 1 /* PAINT_ORDER_LAYER.STROKE */:
8145
- if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
8664
+ if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
8665
+ this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8666
+ this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8667
+ this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8668
+ if (styles.letterSpacing === 0) {
8669
+ this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8670
+ }
8671
+ else {
8672
+ const letters = segmentGraphemes(truncatedText);
8673
+ letters.reduce((left, letter) => {
8674
+ this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8675
+ return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8676
+ }, firstBound.bounds.left);
8677
+ }
8678
+ }
8679
+ break;
8680
+ }
8681
+ });
8682
+ return;
8683
+ }
8684
+ // Normal rendering (no ellipsis needed)
8685
+ text.textBounds.forEach((text) => {
8686
+ paintOrder.forEach((paintOrderLayer) => {
8687
+ switch (paintOrderLayer) {
8688
+ case 0 /* PAINT_ORDER_LAYER.FILL */:
8689
+ this.ctx.fillStyle = asString(styles.color);
8690
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8691
+ const textShadows = styles.textShadow;
8692
+ if (textShadows.length && text.text.trim().length) {
8693
+ textShadows
8694
+ .slice(0)
8695
+ .reverse()
8696
+ .forEach((textShadow) => {
8697
+ this.ctx.shadowColor = asString(textShadow.color);
8698
+ this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
8699
+ this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
8700
+ this.ctx.shadowBlur = textShadow.blur.number;
8701
+ this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8702
+ });
8703
+ this.ctx.shadowColor = '';
8704
+ this.ctx.shadowOffsetX = 0;
8705
+ this.ctx.shadowOffsetY = 0;
8706
+ this.ctx.shadowBlur = 0;
8707
+ }
8708
+ if (styles.textDecorationLine.length) {
8709
+ this.renderTextDecoration(text.bounds, styles);
8710
+ }
8711
+ break;
8712
+ case 1 /* PAINT_ORDER_LAYER.STROKE */:
8713
+ if (styles.webkitTextStrokeWidth && text.text.trim().length) {
8146
8714
  this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8147
8715
  this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8148
8716
  this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8717
+ // Issue #110: Use baseline (fontSize) for consistent positioning with fill
8718
+ // Previously used text.bounds.height which caused stroke to render too low
8719
+ const baseline = styles.fontSize.number;
8149
8720
  if (styles.letterSpacing === 0) {
8150
- this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
8721
+ this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
8151
8722
  }
8152
8723
  else {
8153
- const letters = segmentGraphemes(truncatedText);
8724
+ const letters = segmentGraphemes(text.text);
8154
8725
  letters.reduce((left, letter) => {
8155
- this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
8156
- return left + this.ctx.measureText(letter).width + styles.letterSpacing;
8157
- }, firstBound.bounds.left);
8726
+ this.ctx.strokeText(letter, left, text.bounds.top + baseline);
8727
+ return left + this.ctx.measureText(letter).width;
8728
+ }, text.bounds.left);
8158
8729
  }
8159
8730
  }
8731
+ this.ctx.strokeStyle = '';
8732
+ this.ctx.lineWidth = 0;
8733
+ this.ctx.lineJoin = 'miter';
8160
8734
  break;
8161
8735
  }
8162
8736
  });
8163
- return;
8737
+ });
8738
+ }
8739
+ }
8740
+
8741
+ const MASK_OFFSET = 10000;
8742
+ class CanvasRenderer extends Renderer {
8743
+ constructor(context, options) {
8744
+ super(context, options);
8745
+ this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
8746
+ this.ctx = this.canvas.getContext('2d');
8747
+ if (!options.canvas) {
8748
+ this.canvas.width = Math.floor(options.width * options.scale);
8749
+ this.canvas.height = Math.floor(options.height * options.scale);
8750
+ this.canvas.style.width = `${options.width}px`;
8751
+ this.canvas.style.height = `${options.height}px`;
8752
+ }
8753
+ this.fontMetrics = new FontMetrics(document);
8754
+ this.ctx.scale(this.options.scale, this.options.scale);
8755
+ this.ctx.translate(-options.x, -options.y);
8756
+ this.ctx.textBaseline = 'bottom';
8757
+ // Set image smoothing options
8758
+ if (options.imageSmoothing !== undefined) {
8759
+ this.ctx.imageSmoothingEnabled = options.imageSmoothing;
8760
+ }
8761
+ if (options.imageSmoothingQuality) {
8762
+ this.ctx.imageSmoothingQuality = options.imageSmoothingQuality;
8763
+ }
8764
+ // Initialize specialized renderers
8765
+ this.backgroundRenderer = new BackgroundRenderer({
8766
+ ctx: this.ctx,
8767
+ context: this.context,
8768
+ canvas: this.canvas,
8769
+ options: {
8770
+ width: options.width,
8771
+ height: options.height,
8772
+ scale: options.scale
8773
+ }
8774
+ });
8775
+ this.borderRenderer = new BorderRenderer({ ctx: this.ctx }, {
8776
+ path: (paths) => this.path(paths),
8777
+ formatPath: (paths) => this.formatPath(paths)
8778
+ });
8779
+ this.effectsRenderer = new EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
8780
+ this.textRenderer = new TextRenderer({
8781
+ ctx: this.ctx,
8782
+ context: this.context,
8783
+ options: { scale: options.scale }
8784
+ });
8785
+ this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
8786
+ }
8787
+ async renderStack(stack) {
8788
+ const styles = stack.element.container.styles;
8789
+ if (styles.isVisible()) {
8790
+ await this.renderStackContent(stack);
8791
+ }
8792
+ }
8793
+ async renderNode(paint) {
8794
+ if (contains(paint.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
8795
+ debugger;
8796
+ }
8797
+ if (paint.container.styles.isVisible()) {
8798
+ await this.renderNodeBackgroundAndBorders(paint);
8799
+ await this.renderNodeContent(paint);
8800
+ }
8801
+ }
8802
+ /**
8803
+ * Helper method to render text with paint order support
8804
+ * Reduces code duplication in line-clamp and normal rendering
8805
+ */
8806
+ // Helper method to truncate text and add ellipsis if needed
8807
+ renderReplacedElement(container, curves, image) {
8808
+ const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
8809
+ const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
8810
+ if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
8811
+ const box = contentBox(container);
8812
+ const path = calculatePaddingBoxPath(curves);
8813
+ this.path(path);
8814
+ this.ctx.save();
8815
+ this.ctx.clip();
8816
+ let sx = 0, sy = 0, sw = intrinsicWidth, sh = intrinsicHeight, dx = box.left, dy = box.top, dw = box.width, dh = box.height;
8817
+ const { objectFit } = container.styles;
8818
+ const boxRatio = dw / dh;
8819
+ const imgRatio = sw / sh;
8820
+ if (objectFit === 2 /* OBJECT_FIT.CONTAIN */) {
8821
+ if (imgRatio > boxRatio) {
8822
+ dh = dw / imgRatio;
8823
+ dy += (box.height - dh) / 2;
8824
+ }
8825
+ else {
8826
+ dw = dh * imgRatio;
8827
+ dx += (box.width - dw) / 2;
8828
+ }
8829
+ }
8830
+ else if (objectFit === 4 /* OBJECT_FIT.COVER */) {
8831
+ if (imgRatio > boxRatio) {
8832
+ sw = sh * boxRatio;
8833
+ sx += (intrinsicWidth - sw) / 2;
8834
+ }
8835
+ else {
8836
+ sh = sw / boxRatio;
8837
+ sy += (intrinsicHeight - sh) / 2;
8838
+ }
8839
+ }
8840
+ else if (objectFit === 8 /* OBJECT_FIT.NONE */) {
8841
+ if (sw > dw) {
8842
+ sx += (sw - dw) / 2;
8843
+ sw = dw;
8844
+ }
8845
+ else {
8846
+ dx += (dw - sw) / 2;
8847
+ dw = sw;
8848
+ }
8849
+ if (sh > dh) {
8850
+ sy += (sh - dh) / 2;
8851
+ sh = dh;
8852
+ }
8853
+ else {
8854
+ dy += (dh - sh) / 2;
8855
+ dh = sh;
8856
+ }
8857
+ }
8858
+ else if (objectFit === 16 /* OBJECT_FIT.SCALE_DOWN */) {
8859
+ const containW = imgRatio > boxRatio ? dw : dh * imgRatio;
8860
+ const noneW = sw > dw ? sw : dw;
8861
+ if (containW < noneW) {
8862
+ if (imgRatio > boxRatio) {
8863
+ dh = dw / imgRatio;
8864
+ dy += (box.height - dh) / 2;
8865
+ }
8866
+ else {
8867
+ dw = dh * imgRatio;
8868
+ dx += (box.width - dw) / 2;
8869
+ }
8870
+ }
8871
+ else {
8872
+ if (sw > dw) {
8873
+ sx += (sw - dw) / 2;
8874
+ sw = dw;
8875
+ }
8876
+ else {
8877
+ dx += (dw - sw) / 2;
8878
+ dw = sw;
8879
+ }
8880
+ if (sh > dh) {
8881
+ sy += (sh - dh) / 2;
8882
+ sh = dh;
8883
+ }
8884
+ else {
8885
+ dy += (dh - sh) / 2;
8886
+ dh = sh;
8887
+ }
8888
+ }
8889
+ }
8890
+ this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
8891
+ this.ctx.restore();
8892
+ }
8893
+ }
8894
+ async renderNodeContent(paint) {
8895
+ this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
8896
+ const container = paint.container;
8897
+ const curves = paint.curves;
8898
+ const styles = container.styles;
8899
+ // Use content box for text overflow calculation (excludes padding and border)
8900
+ // This matches browser behavior where text-overflow uses the content width
8901
+ const textBounds = contentBox(container);
8902
+ for (const child of container.textNodes) {
8903
+ await this.textRenderer.renderTextNode(child, styles, textBounds);
8904
+ }
8905
+ if (container instanceof ImageElementContainer) {
8906
+ try {
8907
+ const image = await this.context.cache.match(container.src);
8908
+ // Apply image smoothing based on CSS image-rendering property and global options
8909
+ const prevSmoothing = this.ctx.imageSmoothingEnabled;
8910
+ // CSS image-rendering property overrides global settings
8911
+ if (styles.imageRendering === exports.IMAGE_RENDERING.PIXELATED ||
8912
+ styles.imageRendering === exports.IMAGE_RENDERING.CRISP_EDGES) {
8913
+ this.context.logger.debug(`Disabling image smoothing for ${container.src} due to CSS image-rendering: ${styles.imageRendering === exports.IMAGE_RENDERING.PIXELATED ? 'pixelated' : 'crisp-edges'}`);
8914
+ this.ctx.imageSmoothingEnabled = false;
8915
+ }
8916
+ else if (styles.imageRendering === exports.IMAGE_RENDERING.SMOOTH) {
8917
+ this.context.logger.debug(`Enabling image smoothing for ${container.src} due to CSS image-rendering: smooth`);
8918
+ this.ctx.imageSmoothingEnabled = true;
8919
+ }
8920
+ // IMAGE_RENDERING.AUTO: keep current global setting
8921
+ this.renderReplacedElement(container, curves, image);
8922
+ // Restore previous smoothing state
8923
+ this.ctx.imageSmoothingEnabled = prevSmoothing;
8924
+ }
8925
+ catch (e) {
8926
+ this.context.logger.error(`Error loading image ${container.src}`);
8927
+ }
8928
+ }
8929
+ if (container instanceof CanvasElementContainer) {
8930
+ this.renderReplacedElement(container, curves, container.canvas);
8931
+ }
8932
+ if (container instanceof SVGElementContainer) {
8933
+ try {
8934
+ const image = await this.context.cache.match(container.svg);
8935
+ this.renderReplacedElement(container, curves, image);
8936
+ }
8937
+ catch (e) {
8938
+ this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
8939
+ }
8940
+ }
8941
+ if (container instanceof IFrameElementContainer && container.tree) {
8942
+ const iframeRenderer = new CanvasRenderer(this.context, {
8943
+ scale: this.options.scale,
8944
+ backgroundColor: container.backgroundColor,
8945
+ x: 0,
8946
+ y: 0,
8947
+ width: container.width,
8948
+ height: container.height
8949
+ });
8950
+ const canvas = await iframeRenderer.render(container.tree);
8951
+ if (container.width && container.height) {
8952
+ this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
8953
+ }
8954
+ }
8955
+ if (container instanceof InputElementContainer) {
8956
+ const size = Math.min(container.bounds.width, container.bounds.height);
8957
+ if (container.type === CHECKBOX) {
8958
+ if (container.checked) {
8959
+ this.ctx.save();
8960
+ this.path([
8961
+ new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
8962
+ new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
8963
+ new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
8964
+ new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
8965
+ new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
8966
+ new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
8967
+ new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
8968
+ ]);
8969
+ this.ctx.fillStyle = asString(INPUT_COLOR);
8970
+ this.ctx.fill();
8971
+ this.ctx.restore();
8972
+ }
8973
+ }
8974
+ else if (container.type === RADIO) {
8975
+ if (container.checked) {
8976
+ this.ctx.save();
8977
+ this.ctx.beginPath();
8978
+ this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
8979
+ this.ctx.fillStyle = asString(INPUT_COLOR);
8980
+ this.ctx.fill();
8981
+ this.ctx.restore();
8982
+ }
8983
+ }
8984
+ }
8985
+ if (isTextInputElement(container) && container.value.length) {
8986
+ const [font, fontFamily, fontSize] = this.textRenderer.createFontStyle(styles);
8987
+ const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
8988
+ this.ctx.font = font;
8989
+ // Fix for Issue #92: Use placeholder color when rendering placeholder text
8990
+ const isPlaceholder = container instanceof InputElementContainer && container.isPlaceholder;
8991
+ this.ctx.fillStyle = isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
8992
+ this.ctx.textBaseline = 'alphabetic';
8993
+ this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
8994
+ const bounds = contentBox(container);
8995
+ let x = 0;
8996
+ switch (container.styles.textAlign) {
8997
+ case 1 /* TEXT_ALIGN.CENTER */:
8998
+ x += bounds.width / 2;
8999
+ break;
9000
+ case 2 /* TEXT_ALIGN.RIGHT */:
9001
+ x += bounds.width;
9002
+ break;
9003
+ }
9004
+ // Fix for Issue #92: Position text vertically centered in single-line input
9005
+ // Only apply vertical centering for InputElementContainer, not for textarea or select
9006
+ let verticalOffset = 0;
9007
+ if (container instanceof InputElementContainer) {
9008
+ const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
9009
+ verticalOffset = (bounds.height - fontSizeValue) / 2;
9010
+ }
9011
+ // Create text bounds with horizontal and vertical offsets
9012
+ // Height is not modified as it doesn't affect text rendering position
9013
+ const textBounds = bounds.add(x, verticalOffset, 0, 0);
9014
+ this.ctx.save();
9015
+ this.path([
9016
+ new Vector(bounds.left, bounds.top),
9017
+ new Vector(bounds.left + bounds.width, bounds.top),
9018
+ new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
9019
+ new Vector(bounds.left, bounds.top + bounds.height)
9020
+ ]);
9021
+ this.ctx.clip();
9022
+ this.textRenderer.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
9023
+ this.ctx.restore();
9024
+ this.ctx.textBaseline = 'alphabetic';
9025
+ this.ctx.textAlign = 'left';
8164
9026
  }
8165
- // Normal rendering (no ellipsis needed)
8166
- text.textBounds.forEach((text) => {
8167
- paintOrder.forEach((paintOrderLayer) => {
8168
- switch (paintOrderLayer) {
8169
- case 0 /* PAINT_ORDER_LAYER.FILL */:
8170
- this.ctx.fillStyle = asString(styles.color);
8171
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8172
- const textShadows = styles.textShadow;
8173
- if (textShadows.length && text.text.trim().length) {
8174
- textShadows
8175
- .slice(0)
8176
- .reverse()
8177
- .forEach((textShadow) => {
8178
- this.ctx.shadowColor = asString(textShadow.color);
8179
- this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
8180
- this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
8181
- this.ctx.shadowBlur = textShadow.blur.number;
8182
- this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
8183
- });
8184
- this.ctx.shadowColor = '';
8185
- this.ctx.shadowOffsetX = 0;
8186
- this.ctx.shadowOffsetY = 0;
8187
- this.ctx.shadowBlur = 0;
8188
- }
8189
- if (styles.textDecorationLine.length) {
8190
- this.renderTextDecoration(text.bounds, styles);
8191
- }
8192
- break;
8193
- case 1 /* PAINT_ORDER_LAYER.STROKE */:
8194
- if (styles.webkitTextStrokeWidth && text.text.trim().length) {
8195
- this.ctx.strokeStyle = asString(styles.webkitTextStrokeColor);
8196
- this.ctx.lineWidth = styles.webkitTextStrokeWidth;
8197
- this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
8198
- // Issue #110: Use baseline (fontSize) for consistent positioning with fill
8199
- // Previously used text.bounds.height which caused stroke to render too low
8200
- const baseline = styles.fontSize.number;
8201
- if (styles.letterSpacing === 0) {
8202
- this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
8203
- }
8204
- else {
8205
- const letters = segmentGraphemes(text.text);
8206
- letters.reduce((left, letter) => {
8207
- this.ctx.strokeText(letter, left, text.bounds.top + baseline);
8208
- return left + this.ctx.measureText(letter).width;
8209
- }, text.bounds.left);
8210
- }
8211
- }
8212
- this.ctx.strokeStyle = '';
8213
- this.ctx.lineWidth = 0;
8214
- this.ctx.lineJoin = 'miter';
8215
- break;
9027
+ if (contains(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
9028
+ if (container.styles.listStyleImage !== null) {
9029
+ const img = container.styles.listStyleImage;
9030
+ if (img.type === 0 /* CSSImageType.URL */) {
9031
+ let image;
9032
+ const url = img.url;
9033
+ try {
9034
+ image = await this.context.cache.match(url);
9035
+ this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
9036
+ }
9037
+ catch (e) {
9038
+ this.context.logger.error(`Error loading list-style-image ${url}`);
9039
+ }
8216
9040
  }
8217
- });
9041
+ }
9042
+ else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
9043
+ const [font] = this.textRenderer.createFontStyle(styles);
9044
+ this.ctx.font = font;
9045
+ this.ctx.fillStyle = asString(styles.color);
9046
+ this.ctx.textBaseline = 'middle';
9047
+ this.ctx.textAlign = 'right';
9048
+ 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);
9049
+ this.textRenderer.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2);
9050
+ this.ctx.textBaseline = 'bottom';
9051
+ this.ctx.textAlign = 'left';
9052
+ }
9053
+ }
9054
+ }
9055
+ async renderStackContent(stack) {
9056
+ if (contains(stack.element.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
9057
+ debugger;
9058
+ }
9059
+ // https://www.w3.org/TR/css-position-3/#painting-order
9060
+ // 1. the background and borders of the element forming the stacking context.
9061
+ await this.renderNodeBackgroundAndBorders(stack.element);
9062
+ // 2. the child stacking contexts with negative stack levels (most negative first).
9063
+ for (const child of stack.negativeZIndex) {
9064
+ await this.renderStack(child);
9065
+ }
9066
+ // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
9067
+ await this.renderNodeContent(stack.element);
9068
+ for (const child of stack.nonInlineLevel) {
9069
+ await this.renderNode(child);
9070
+ }
9071
+ // 4. All non-positioned floating descendants, in tree order. For each one of these,
9072
+ // treat the element as if it created a new stacking context, but any positioned descendants and descendants
9073
+ // which actually create a new stacking context should be considered part of the parent stacking context,
9074
+ // not this new one.
9075
+ for (const child of stack.nonPositionedFloats) {
9076
+ await this.renderStack(child);
9077
+ }
9078
+ // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
9079
+ for (const child of stack.nonPositionedInlineLevel) {
9080
+ await this.renderStack(child);
9081
+ }
9082
+ for (const child of stack.inlineLevel) {
9083
+ await this.renderNode(child);
9084
+ }
9085
+ // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
9086
+ // All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
9087
+ // For those with 'z-index: auto', treat the element as if it created a new stacking context,
9088
+ // but any positioned descendants and descendants which actually create a new stacking context should be
9089
+ // considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
9090
+ // treat the stacking context generated atomically.
9091
+ //
9092
+ // All opacity descendants with opacity less than 1
9093
+ //
9094
+ // All transform descendants with transform other than none
9095
+ for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
9096
+ await this.renderStack(child);
9097
+ }
9098
+ // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
9099
+ // order (smallest first) then tree order.
9100
+ for (const child of stack.positiveZIndex) {
9101
+ await this.renderStack(child);
9102
+ }
9103
+ }
9104
+ mask(paths) {
9105
+ this.ctx.beginPath();
9106
+ this.ctx.moveTo(0, 0);
9107
+ // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
9108
+ // because context has already been scaled by this.options.scale
9109
+ // Fix for Issue #126: Using canvas pixel dimensions causes broken output
9110
+ this.ctx.lineTo(this.options.width, 0);
9111
+ this.ctx.lineTo(this.options.width, this.options.height);
9112
+ this.ctx.lineTo(0, this.options.height);
9113
+ this.ctx.lineTo(0, 0);
9114
+ this.formatPath(paths.slice(0).reverse());
9115
+ this.ctx.closePath();
9116
+ }
9117
+ path(paths) {
9118
+ this.ctx.beginPath();
9119
+ this.formatPath(paths);
9120
+ this.ctx.closePath();
9121
+ }
9122
+ formatPath(paths) {
9123
+ paths.forEach((point, index) => {
9124
+ const start = isBezierCurve(point) ? point.start : point;
9125
+ if (index === 0) {
9126
+ this.ctx.moveTo(start.x, start.y);
9127
+ }
9128
+ else {
9129
+ this.ctx.lineTo(start.x, start.y);
9130
+ }
9131
+ if (isBezierCurve(point)) {
9132
+ this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
9133
+ }
8218
9134
  });
8219
9135
  }
8220
- renderReplacedElement(container, curves, image) {
8221
- const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
8222
- const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
8223
- if (image && intrinsicWidth > 0 && intrinsicHeight > 0) {
8224
- const box = contentBox(container);
8225
- const path = calculatePaddingBoxPath(curves);
8226
- this.path(path);
9136
+ async renderNodeBackgroundAndBorders(paint) {
9137
+ this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
9138
+ const styles = paint.container.styles;
9139
+ const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
9140
+ const borders = [
9141
+ { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
9142
+ { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
9143
+ { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
9144
+ { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
9145
+ ];
9146
+ const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
9147
+ if (hasBackground || styles.boxShadow.length) {
8227
9148
  this.ctx.save();
9149
+ this.path(backgroundPaintingArea);
8228
9150
  this.ctx.clip();
8229
- let sx = 0, sy = 0, sw = intrinsicWidth, sh = intrinsicHeight, dx = box.left, dy = box.top, dw = box.width, dh = box.height;
8230
- const { objectFit } = container.styles;
8231
- const boxRatio = dw / dh;
8232
- const imgRatio = sw / sh;
8233
- if (objectFit === 2 /* OBJECT_FIT.CONTAIN */) {
8234
- if (imgRatio > boxRatio) {
8235
- dh = dw / imgRatio;
8236
- dy += (box.height - dh) / 2;
8237
- }
8238
- else {
8239
- dw = dh * imgRatio;
8240
- dx += (box.width - dw) / 2;
8241
- }
8242
- }
8243
- else if (objectFit === 4 /* OBJECT_FIT.COVER */) {
8244
- if (imgRatio > boxRatio) {
8245
- sw = sh * boxRatio;
8246
- sx += (intrinsicWidth - sw) / 2;
8247
- }
8248
- else {
8249
- sh = sw / boxRatio;
8250
- sy += (intrinsicHeight - sh) / 2;
8251
- }
9151
+ if (!isTransparent(styles.backgroundColor)) {
9152
+ this.ctx.fillStyle = asString(styles.backgroundColor);
9153
+ this.ctx.fill();
8252
9154
  }
8253
- else if (objectFit === 8 /* OBJECT_FIT.NONE */) {
8254
- if (sw > dw) {
8255
- sx += (sw - dw) / 2;
8256
- sw = dw;
9155
+ await this.backgroundRenderer.renderBackgroundImage(paint.container);
9156
+ this.ctx.restore();
9157
+ styles.boxShadow
9158
+ .slice(0)
9159
+ .reverse()
9160
+ .forEach((shadow) => {
9161
+ this.ctx.save();
9162
+ const borderBoxArea = calculateBorderBoxPath(paint.curves);
9163
+ const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
9164
+ 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));
9165
+ if (shadow.inset) {
9166
+ this.path(borderBoxArea);
9167
+ this.ctx.clip();
9168
+ this.mask(shadowPaintingArea);
8257
9169
  }
8258
9170
  else {
8259
- dx += (dw - sw) / 2;
8260
- dw = sw;
9171
+ this.mask(borderBoxArea);
9172
+ this.ctx.clip();
9173
+ this.path(shadowPaintingArea);
8261
9174
  }
8262
- if (sh > dh) {
8263
- sy += (sh - dh) / 2;
8264
- sh = dh;
9175
+ this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
9176
+ this.ctx.shadowOffsetY = shadow.offsetY.number;
9177
+ this.ctx.shadowColor = asString(shadow.color);
9178
+ this.ctx.shadowBlur = shadow.blur.number;
9179
+ this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
9180
+ this.ctx.fill();
9181
+ this.ctx.restore();
9182
+ });
9183
+ }
9184
+ let side = 0;
9185
+ for (const border of borders) {
9186
+ if (border.style !== 0 /* BORDER_STYLE.NONE */ && !isTransparent(border.color) && border.width > 0) {
9187
+ if (border.style === 2 /* BORDER_STYLE.DASHED */) {
9188
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
8265
9189
  }
8266
- else {
8267
- dy += (dh - sh) / 2;
8268
- dh = sh;
9190
+ else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
9191
+ await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
8269
9192
  }
8270
- }
8271
- else if (objectFit === 16 /* OBJECT_FIT.SCALE_DOWN */) {
8272
- const containW = imgRatio > boxRatio ? dw : dh * imgRatio;
8273
- const noneW = sw > dw ? sw : dw;
8274
- if (containW < noneW) {
8275
- if (imgRatio > boxRatio) {
8276
- dh = dw / imgRatio;
8277
- dy += (box.height - dh) / 2;
8278
- }
8279
- else {
8280
- dw = dh * imgRatio;
8281
- dx += (box.width - dw) / 2;
8282
- }
9193
+ else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
9194
+ await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
8283
9195
  }
8284
9196
  else {
8285
- if (sw > dw) {
8286
- sx += (sw - dw) / 2;
8287
- sw = dw;
8288
- }
8289
- else {
8290
- dx += (dw - sw) / 2;
8291
- dw = sw;
8292
- }
8293
- if (sh > dh) {
8294
- sy += (sh - dh) / 2;
8295
- sh = dh;
8296
- }
8297
- else {
8298
- dy += (dh - sh) / 2;
8299
- dh = sh;
8300
- }
9197
+ await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
8301
9198
  }
8302
9199
  }
8303
- this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
8304
- this.ctx.restore();
9200
+ side++;
8305
9201
  }
8306
9202
  }
8307
- async renderNodeContent(paint) {
8308
- this.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
8309
- const container = paint.container;
8310
- const curves = paint.curves;
8311
- const styles = container.styles;
8312
- // Use content box for text overflow calculation (excludes padding and border)
8313
- // This matches browser behavior where text-overflow uses the content width
8314
- const textBounds = contentBox(container);
8315
- for (const child of container.textNodes) {
8316
- await this.renderTextNode(child, styles, textBounds);
8317
- }
8318
- if (container instanceof ImageElementContainer) {
8319
- try {
8320
- const image = await this.context.cache.match(container.src);
8321
- this.renderReplacedElement(container, curves, image);
8322
- }
8323
- catch (e) {
8324
- this.context.logger.error(`Error loading image ${container.src}`);
8325
- }
8326
- }
8327
- if (container instanceof CanvasElementContainer) {
8328
- this.renderReplacedElement(container, curves, container.canvas);
8329
- }
8330
- if (container instanceof SVGElementContainer) {
8331
- try {
8332
- const image = await this.context.cache.match(container.svg);
8333
- this.renderReplacedElement(container, curves, image);
8334
- }
8335
- catch (e) {
8336
- this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
8337
- }
8338
- }
8339
- if (container instanceof IFrameElementContainer && container.tree) {
8340
- const iframeRenderer = new CanvasRenderer(this.context, {
8341
- scale: this.options.scale,
8342
- backgroundColor: container.backgroundColor,
8343
- x: 0,
8344
- y: 0,
8345
- width: container.width,
8346
- height: container.height
8347
- });
8348
- const canvas = await iframeRenderer.render(container.tree);
8349
- if (container.width && container.height) {
8350
- this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
8351
- }
9203
+ async render(element) {
9204
+ if (this.options.backgroundColor) {
9205
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
9206
+ this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
8352
9207
  }
8353
- if (container instanceof InputElementContainer) {
8354
- const size = Math.min(container.bounds.width, container.bounds.height);
8355
- if (container.type === CHECKBOX) {
8356
- if (container.checked) {
8357
- this.ctx.save();
8358
- this.path([
8359
- new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
8360
- new Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
8361
- new Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
8362
- new Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
8363
- new Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
8364
- new Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
8365
- new Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
8366
- ]);
8367
- this.ctx.fillStyle = asString(INPUT_COLOR);
8368
- this.ctx.fill();
8369
- this.ctx.restore();
8370
- }
8371
- }
8372
- else if (container.type === RADIO) {
8373
- if (container.checked) {
8374
- this.ctx.save();
8375
- this.ctx.beginPath();
8376
- this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
8377
- this.ctx.fillStyle = asString(INPUT_COLOR);
8378
- this.ctx.fill();
8379
- this.ctx.restore();
8380
- }
8381
- }
9208
+ const stack = parseStackingContexts(element);
9209
+ await this.renderStack(stack);
9210
+ this.effectsRenderer.applyEffects([]);
9211
+ return this.canvas;
9212
+ }
9213
+ }
9214
+ const isTextInputElement = (container) => {
9215
+ if (container instanceof TextareaElementContainer) {
9216
+ return true;
9217
+ }
9218
+ else if (container instanceof SelectElementContainer) {
9219
+ return true;
9220
+ }
9221
+ else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
9222
+ return true;
9223
+ }
9224
+ return false;
9225
+ };
9226
+ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
9227
+ switch (clip) {
9228
+ case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
9229
+ return calculateBorderBoxPath(curves);
9230
+ case 2 /* BACKGROUND_CLIP.CONTENT_BOX */:
9231
+ return calculateContentBoxPath(curves);
9232
+ case 1 /* BACKGROUND_CLIP.PADDING_BOX */:
9233
+ default:
9234
+ return calculatePaddingBoxPath(curves);
9235
+ }
9236
+ };
9237
+ const canvasTextAlign = (textAlign) => {
9238
+ switch (textAlign) {
9239
+ case 1 /* TEXT_ALIGN.CENTER */:
9240
+ return 'center';
9241
+ case 2 /* TEXT_ALIGN.RIGHT */:
9242
+ return 'right';
9243
+ case 0 /* TEXT_ALIGN.LEFT */:
9244
+ default:
9245
+ return 'left';
9246
+ }
9247
+ };
9248
+
9249
+ class ForeignObjectRenderer extends Renderer {
9250
+ constructor(context, options) {
9251
+ super(context, options);
9252
+ this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
9253
+ this.ctx = this.canvas.getContext('2d');
9254
+ this.options = options;
9255
+ this.canvas.width = Math.floor(options.width * options.scale);
9256
+ this.canvas.height = Math.floor(options.height * options.scale);
9257
+ this.canvas.style.width = `${options.width}px`;
9258
+ this.canvas.style.height = `${options.height}px`;
9259
+ this.ctx.scale(this.options.scale, this.options.scale);
9260
+ this.ctx.translate(-options.x, -options.y);
9261
+ this.context.logger.debug(`EXPERIMENTAL ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`);
9262
+ }
9263
+ async render(element) {
9264
+ const svg = createForeignObjectSVG(this.options.width * this.options.scale, this.options.height * this.options.scale, this.options.scale, this.options.scale, element);
9265
+ const img = await loadSerializedSVG(svg);
9266
+ if (this.options.backgroundColor) {
9267
+ this.ctx.fillStyle = asString(this.options.backgroundColor);
9268
+ this.ctx.fillRect(0, 0, this.options.width * this.options.scale, this.options.height * this.options.scale);
8382
9269
  }
8383
- if (isTextInputElement(container) && container.value.length) {
8384
- const [font, fontFamily, fontSize] = this.createFontStyle(styles);
8385
- const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
8386
- this.ctx.font = font;
8387
- // Fix for Issue #92: Use placeholder color when rendering placeholder text
8388
- const isPlaceholder = container instanceof InputElementContainer && container.isPlaceholder;
8389
- this.ctx.fillStyle = isPlaceholder ? asString(PLACEHOLDER_COLOR) : asString(styles.color);
8390
- this.ctx.textBaseline = 'alphabetic';
8391
- this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
8392
- const bounds = contentBox(container);
8393
- let x = 0;
8394
- switch (container.styles.textAlign) {
8395
- case 1 /* TEXT_ALIGN.CENTER */:
8396
- x += bounds.width / 2;
8397
- break;
8398
- case 2 /* TEXT_ALIGN.RIGHT */:
8399
- x += bounds.width;
8400
- break;
9270
+ this.ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
9271
+ return this.canvas;
9272
+ }
9273
+ }
9274
+ const loadSerializedSVG = (svg) => new Promise((resolve, reject) => {
9275
+ const img = new Image();
9276
+ img.onload = () => {
9277
+ resolve(img);
9278
+ };
9279
+ img.onerror = reject;
9280
+ img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
9281
+ });
9282
+
9283
+ class Logger {
9284
+ constructor({ id, enabled }) {
9285
+ this.id = id;
9286
+ this.enabled = enabled;
9287
+ this.start = Date.now();
9288
+ }
9289
+ debug(...args) {
9290
+ if (this.enabled) {
9291
+ // eslint-disable-next-line no-console
9292
+ if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
9293
+ // eslint-disable-next-line no-console
9294
+ console.debug(this.id, `${this.getTime()}ms`, ...args);
8401
9295
  }
8402
- // Fix for Issue #92: Position text vertically centered in single-line input
8403
- // Only apply vertical centering for InputElementContainer, not for textarea or select
8404
- let verticalOffset = 0;
8405
- if (container instanceof InputElementContainer) {
8406
- const fontSizeValue = getAbsoluteValue(styles.fontSize, 0);
8407
- verticalOffset = (bounds.height - fontSizeValue) / 2;
9296
+ else {
9297
+ this.info(...args);
8408
9298
  }
8409
- // Create text bounds with horizontal and vertical offsets
8410
- // Height is not modified as it doesn't affect text rendering position
8411
- const textBounds = bounds.add(x, verticalOffset, 0, 0);
8412
- this.ctx.save();
8413
- this.path([
8414
- new Vector(bounds.left, bounds.top),
8415
- new Vector(bounds.left + bounds.width, bounds.top),
8416
- new Vector(bounds.left + bounds.width, bounds.top + bounds.height),
8417
- new Vector(bounds.left, bounds.top + bounds.height)
8418
- ]);
8419
- this.ctx.clip();
8420
- this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
8421
- this.ctx.restore();
8422
- this.ctx.textBaseline = 'alphabetic';
8423
- this.ctx.textAlign = 'left';
8424
9299
  }
8425
- if (contains(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
8426
- if (container.styles.listStyleImage !== null) {
8427
- const img = container.styles.listStyleImage;
8428
- if (img.type === 0 /* CSSImageType.URL */) {
8429
- let image;
8430
- const url = img.url;
8431
- try {
8432
- image = await this.context.cache.match(url);
8433
- this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
8434
- }
8435
- catch (e) {
8436
- this.context.logger.error(`Error loading list-style-image ${url}`);
8437
- }
8438
- }
9300
+ }
9301
+ getTime() {
9302
+ return Date.now() - this.start;
9303
+ }
9304
+ info(...args) {
9305
+ if (this.enabled) {
9306
+ // eslint-disable-next-line no-console
9307
+ if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
9308
+ // eslint-disable-next-line no-console
9309
+ console.info(this.id, `${this.getTime()}ms`, ...args);
8439
9310
  }
8440
- else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
8441
- const [font] = this.createFontStyle(styles);
8442
- this.ctx.font = font;
8443
- this.ctx.fillStyle = asString(styles.color);
8444
- this.ctx.textBaseline = 'middle';
8445
- this.ctx.textAlign = 'right';
8446
- 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);
8447
- this.renderTextWithLetterSpacing(new TextBounds(paint.listValue, bounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number) / 2 + 2);
8448
- this.ctx.textBaseline = 'bottom';
8449
- this.ctx.textAlign = 'left';
9311
+ }
9312
+ }
9313
+ warn(...args) {
9314
+ if (this.enabled) {
9315
+ if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
9316
+ console.warn(this.id, `${this.getTime()}ms`, ...args);
9317
+ }
9318
+ else {
9319
+ this.info(...args);
8450
9320
  }
8451
9321
  }
8452
9322
  }
8453
- async renderStackContent(stack) {
8454
- if (contains(stack.element.container.flags, 16 /* FLAGS.DEBUG_RENDER */)) {
8455
- debugger;
9323
+ error(...args) {
9324
+ if (this.enabled) {
9325
+ if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
9326
+ console.error(this.id, `${this.getTime()}ms`, ...args);
9327
+ }
9328
+ else {
9329
+ this.info(...args);
9330
+ }
8456
9331
  }
8457
- // https://www.w3.org/TR/css-position-3/#painting-order
8458
- // 1. the background and borders of the element forming the stacking context.
8459
- await this.renderNodeBackgroundAndBorders(stack.element);
8460
- // 2. the child stacking contexts with negative stack levels (most negative first).
8461
- for (const child of stack.negativeZIndex) {
8462
- await this.renderStack(child);
9332
+ }
9333
+ }
9334
+ Logger.instances = {};
9335
+
9336
+ class Cache {
9337
+ constructor(context, _options) {
9338
+ this.context = context;
9339
+ this._options = _options;
9340
+ this._cache = new Map();
9341
+ this._pendingOperations = new Map();
9342
+ // Default cache size: 100 items
9343
+ this.maxSize = _options.maxCacheSize ?? 100;
9344
+ if (this.maxSize < 1) {
9345
+ throw new Error('Cache maxSize must be at least 1');
8463
9346
  }
8464
- // 3. For all its in-flow, non-positioned, block-level descendants in tree order:
8465
- await this.renderNodeContent(stack.element);
8466
- for (const child of stack.nonInlineLevel) {
8467
- await this.renderNode(child);
9347
+ if (this.maxSize > 10000) {
9348
+ this.context.logger.warn(`Cache maxSize ${this.maxSize} is very large and may cause memory issues. ` +
9349
+ `Consider using a smaller value (recommended: 100-1000).`);
8468
9350
  }
8469
- // 4. All non-positioned floating descendants, in tree order. For each one of these,
8470
- // treat the element as if it created a new stacking context, but any positioned descendants and descendants
8471
- // which actually create a new stacking context should be considered part of the parent stacking context,
8472
- // not this new one.
8473
- for (const child of stack.nonPositionedFloats) {
8474
- await this.renderStack(child);
9351
+ }
9352
+ addImage(src) {
9353
+ // Wait for any pending operations on this key
9354
+ const pending = this._pendingOperations.get(src);
9355
+ if (pending) {
9356
+ return pending;
8475
9357
  }
8476
- // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
8477
- for (const child of stack.nonPositionedInlineLevel) {
8478
- await this.renderStack(child);
9358
+ if (this.has(src)) {
9359
+ // Update last accessed time
9360
+ const entry = this._cache.get(src);
9361
+ if (entry) {
9362
+ entry.lastAccessed = Date.now();
9363
+ }
9364
+ return Promise.resolve();
8479
9365
  }
8480
- for (const child of stack.inlineLevel) {
8481
- await this.renderNode(child);
9366
+ if (isBlobImage(src) || isRenderable(src)) {
9367
+ // Create a pending operation to ensure atomicity
9368
+ const operation = this._addImageInternal(src);
9369
+ this._pendingOperations.set(src, operation);
9370
+ operation.finally(() => {
9371
+ this._pendingOperations.delete(src);
9372
+ });
9373
+ return operation;
8482
9374
  }
8483
- // 6. All positioned, opacity or transform descendants, in tree order that fall into the following categories:
8484
- // All positioned descendants with 'z-index: auto' or 'z-index: 0', in tree order.
8485
- // For those with 'z-index: auto', treat the element as if it created a new stacking context,
8486
- // but any positioned descendants and descendants which actually create a new stacking context should be
8487
- // considered part of the parent stacking context, not this new one. For those with 'z-index: 0',
8488
- // treat the stacking context generated atomically.
8489
- //
8490
- // All opacity descendants with opacity less than 1
8491
- //
8492
- // All transform descendants with transform other than none
8493
- for (const child of stack.zeroOrAutoZIndexOrTransformedOrOpacity) {
8494
- await this.renderStack(child);
9375
+ return Promise.resolve();
9376
+ }
9377
+ async _addImageInternal(src) {
9378
+ // Create image load promise with timeout protection
9379
+ const timeoutMs = this._options.imageTimeout ?? 15000;
9380
+ const timeoutPromise = new Promise((_, reject) => {
9381
+ setTimeout(() => {
9382
+ reject(new Error(`Image load timeout after ${timeoutMs}ms: ${src}`));
9383
+ }, timeoutMs);
9384
+ });
9385
+ // Race between image load and timeout
9386
+ const imageWithTimeout = Promise.race([this.loadImage(src), timeoutPromise]);
9387
+ // Handle errors to prevent unhandled rejections
9388
+ imageWithTimeout.catch((error) => {
9389
+ this.context.logger.error(`Failed to load image ${src}: ${error instanceof Error ? error.message : 'Unknown error'}`);
9390
+ });
9391
+ // Store the promise with timeout in cache
9392
+ this.set(src, imageWithTimeout);
9393
+ }
9394
+ match(src) {
9395
+ const entry = this._cache.get(src);
9396
+ if (entry) {
9397
+ // Update last accessed time on access
9398
+ entry.lastAccessed = Date.now();
9399
+ return entry.value;
8495
9400
  }
8496
- // 7. Stacking contexts formed by positioned descendants with z-indices greater than or equal to 1 in z-index
8497
- // order (smallest first) then tree order.
8498
- for (const child of stack.positiveZIndex) {
8499
- await this.renderStack(child);
9401
+ return undefined;
9402
+ }
9403
+ /**
9404
+ * Set a value in cache with LRU eviction
9405
+ */
9406
+ set(key, value) {
9407
+ // If key already exists, update it without eviction
9408
+ if (this._cache.has(key)) {
9409
+ const entry = this._cache.get(key);
9410
+ entry.value = value;
9411
+ entry.lastAccessed = Date.now();
9412
+ return;
9413
+ }
9414
+ // For new keys, check if we need to evict
9415
+ if (this._cache.size >= this.maxSize) {
9416
+ this.evictLRU();
8500
9417
  }
9418
+ this._cache.set(key, {
9419
+ value,
9420
+ lastAccessed: Date.now()
9421
+ });
8501
9422
  }
8502
- mask(paths) {
8503
- this.ctx.beginPath();
8504
- this.ctx.moveTo(0, 0);
8505
- // Use logical dimensions (options.width/height) instead of canvas pixel dimensions
8506
- // because context has already been scaled by this.options.scale
8507
- // Fix for Issue #126: Using canvas pixel dimensions causes broken output
8508
- this.ctx.lineTo(this.options.width, 0);
8509
- this.ctx.lineTo(this.options.width, this.options.height);
8510
- this.ctx.lineTo(0, this.options.height);
8511
- this.ctx.lineTo(0, 0);
8512
- this.formatPath(paths.slice(0).reverse());
8513
- this.ctx.closePath();
9423
+ /**
9424
+ * Evict least recently used entry
9425
+ */
9426
+ evictLRU() {
9427
+ let oldestKey = null;
9428
+ let oldestTime = Infinity;
9429
+ for (const [key, entry] of this._cache.entries()) {
9430
+ if (entry.lastAccessed < oldestTime) {
9431
+ oldestTime = entry.lastAccessed;
9432
+ oldestKey = key;
9433
+ }
9434
+ }
9435
+ if (oldestKey) {
9436
+ this._cache.delete(oldestKey);
9437
+ this.context.logger.debug(`Cache: Evicted LRU entry: ${oldestKey}`);
9438
+ }
8514
9439
  }
8515
- path(paths) {
8516
- this.ctx.beginPath();
8517
- this.formatPath(paths);
8518
- this.ctx.closePath();
9440
+ /**
9441
+ * Get cache size
9442
+ */
9443
+ size() {
9444
+ return this._cache.size;
8519
9445
  }
8520
- formatPath(paths) {
8521
- paths.forEach((point, index) => {
8522
- const start = isBezierCurve(point) ? point.start : point;
8523
- if (index === 0) {
8524
- this.ctx.moveTo(start.x, start.y);
9446
+ /**
9447
+ * Get max cache size
9448
+ */
9449
+ getMaxSize() {
9450
+ return this.maxSize;
9451
+ }
9452
+ /**
9453
+ * Clear all cache entries
9454
+ */
9455
+ clear() {
9456
+ this._cache.clear();
9457
+ }
9458
+ async loadImage(key) {
9459
+ const originChecker = this.context.originChecker;
9460
+ const defaultIsSameOrigin = (src) => originChecker.isSameOrigin(src);
9461
+ const isSameOrigin = typeof this._options.customIsSameOrigin === 'function'
9462
+ ? await this._options.customIsSameOrigin(key, defaultIsSameOrigin)
9463
+ : defaultIsSameOrigin(key);
9464
+ const useCORS = !isInlineImage(key) && this._options.useCORS === true && FEATURES.SUPPORT_CORS_IMAGES && !isSameOrigin;
9465
+ const useProxy = !isInlineImage(key) &&
9466
+ !isSameOrigin &&
9467
+ !isBlobImage(key) &&
9468
+ typeof this._options.proxy === 'string' &&
9469
+ FEATURES.SUPPORT_CORS_XHR &&
9470
+ !useCORS;
9471
+ if (!isSameOrigin &&
9472
+ this._options.allowTaint === false &&
9473
+ !isInlineImage(key) &&
9474
+ !isBlobImage(key) &&
9475
+ !useProxy &&
9476
+ !useCORS) {
9477
+ return;
9478
+ }
9479
+ let src = key;
9480
+ if (useProxy) {
9481
+ src = await this.proxy(src);
9482
+ }
9483
+ this.context.logger.debug(`Added image ${key.substring(0, 256)}`);
9484
+ return await new Promise((resolve, reject) => {
9485
+ const img = new Image();
9486
+ img.onload = () => resolve(img);
9487
+ img.onerror = reject;
9488
+ //ios safari 10.3 taints canvas with data urls unless crossOrigin is set to anonymous
9489
+ if (isInlineBase64Image(src) || useCORS) {
9490
+ img.crossOrigin = 'anonymous';
8525
9491
  }
8526
- else {
8527
- this.ctx.lineTo(start.x, start.y);
9492
+ img.src = src;
9493
+ if (img.complete === true) {
9494
+ // Inline XML images may fail to parse, throwing an Error later on
9495
+ setTimeout(() => resolve(img), 500);
8528
9496
  }
8529
- if (isBezierCurve(point)) {
8530
- this.ctx.bezierCurveTo(point.startControl.x, point.startControl.y, point.endControl.x, point.endControl.y, point.end.x, point.end.y);
9497
+ if (this._options.imageTimeout > 0) {
9498
+ setTimeout(() => reject(`Timed out (${this._options.imageTimeout}ms) loading image`), this._options.imageTimeout);
9499
+ }
9500
+ });
9501
+ }
9502
+ has(key) {
9503
+ return this._cache.has(key);
9504
+ }
9505
+ keys() {
9506
+ return Promise.resolve(Object.keys(this._cache));
9507
+ }
9508
+ proxy(src) {
9509
+ const proxy = this._options.proxy;
9510
+ if (!proxy) {
9511
+ throw new Error('No proxy defined');
9512
+ }
9513
+ const key = src.substring(0, 256);
9514
+ return new Promise((resolve, reject) => {
9515
+ const responseType = FEATURES.SUPPORT_RESPONSE_TYPE ? 'blob' : 'text';
9516
+ const xhr = new XMLHttpRequest();
9517
+ xhr.onload = () => {
9518
+ if (xhr.status === 200) {
9519
+ if (responseType === 'text') {
9520
+ resolve(xhr.response);
9521
+ }
9522
+ else {
9523
+ const reader = new FileReader();
9524
+ reader.addEventListener('load', () => resolve(reader.result), false);
9525
+ reader.addEventListener('error', (e) => reject(e), false);
9526
+ reader.readAsDataURL(xhr.response);
9527
+ }
9528
+ }
9529
+ else {
9530
+ reject(`Failed to proxy resource ${key} with status code ${xhr.status}`);
9531
+ }
9532
+ };
9533
+ xhr.onerror = reject;
9534
+ const queryString = proxy.indexOf('?') > -1 ? '&' : '?';
9535
+ xhr.open('GET', `${proxy}${queryString}url=${encodeURIComponent(src)}&responseType=${responseType}`);
9536
+ if (responseType !== 'text' && xhr instanceof XMLHttpRequest) {
9537
+ xhr.responseType = responseType;
9538
+ }
9539
+ if (this._options.imageTimeout) {
9540
+ const timeout = this._options.imageTimeout;
9541
+ xhr.timeout = timeout;
9542
+ xhr.ontimeout = () => reject(`Timed out (${timeout}ms) proxying ${key}`);
8531
9543
  }
9544
+ xhr.send();
9545
+ });
9546
+ }
9547
+ }
9548
+ const INLINE_SVG = /^data:image\/svg\+xml/i;
9549
+ const INLINE_BASE64 = /^data:image\/.*;base64,/i;
9550
+ const INLINE_IMG = /^data:image\/.*/i;
9551
+ const isRenderable = (src) => FEATURES.SUPPORT_SVG_DRAWING || !isSVG(src);
9552
+ const isInlineImage = (src) => INLINE_IMG.test(src);
9553
+ const isInlineBase64Image = (src) => INLINE_BASE64.test(src);
9554
+ const isBlobImage = (src) => src.substr(0, 4) === 'blob';
9555
+ const isSVG = (src) => src.substr(-3).toLowerCase() === 'svg' || INLINE_SVG.test(src);
9556
+
9557
+ /**
9558
+ * Origin Checker
9559
+ *
9560
+ * Provides origin checking functionality without global static state.
9561
+ * Each instance maintains its own anchor element and origin reference.
9562
+ *
9563
+ * Replaces the static methods in CacheStorage with instance-based approach.
9564
+ */
9565
+ class OriginChecker {
9566
+ constructor(window) {
9567
+ if (!window || !window.document) {
9568
+ throw new Error('Valid window object required for OriginChecker');
9569
+ }
9570
+ if (!window.location || !window.location.href) {
9571
+ throw new Error('Window object must have valid location');
9572
+ }
9573
+ this.link = window.document.createElement('a');
9574
+ this.origin = this.getOrigin(window.location.href);
9575
+ }
9576
+ /**
9577
+ * Get the origin (protocol + hostname + port) of a URL
9578
+ *
9579
+ * @param url - URL to parse
9580
+ * @returns Origin string (e.g., "https://example.com:8080")
9581
+ */
9582
+ getOrigin(url) {
9583
+ this.link.href = url;
9584
+ // IE9 hack: accessing href twice to ensure it's properly parsed
9585
+ this.link.href = this.link.href;
9586
+ return this.link.protocol + this.link.hostname + this.link.port;
9587
+ }
9588
+ /**
9589
+ * Check if a URL is from the same origin as the context
9590
+ *
9591
+ * @param src - URL to check
9592
+ * @returns true if same origin, false otherwise
9593
+ */
9594
+ isSameOrigin(src) {
9595
+ return this.getOrigin(src) === this.origin;
9596
+ }
9597
+ /**
9598
+ * Get the current context origin
9599
+ *
9600
+ * @returns The origin of the context window
9601
+ */
9602
+ getContextOrigin() {
9603
+ return this.origin;
9604
+ }
9605
+ }
9606
+
9607
+ class Context {
9608
+ constructor(options, windowBounds, config) {
9609
+ this.windowBounds = windowBounds;
9610
+ this.instanceName = `#${Context.instanceCount++}`;
9611
+ this.config = config;
9612
+ this.logger = new Logger({ id: this.instanceName, enabled: options.logging });
9613
+ this.originChecker = new OriginChecker(config.window);
9614
+ this.cache = options.cache ?? config.cache ?? new Cache(this, options);
9615
+ }
9616
+ }
9617
+ Context.instanceCount = 1;
9618
+
9619
+ /**
9620
+ * Html2Canvas Configuration
9621
+ *
9622
+ * Manages configuration state for html2canvas rendering.
9623
+ * Eliminates the need for global static variables.
9624
+ */
9625
+ class Html2CanvasConfig {
9626
+ constructor(options = {}) {
9627
+ // Try to get window from options first, then fall back to global window
9628
+ this.window = options.window || (typeof window !== 'undefined' ? window : null);
9629
+ if (!this.window) {
9630
+ throw new Error('Window object is required but not available');
9631
+ }
9632
+ this.cspNonce = options.cspNonce;
9633
+ this.cache = options.cache;
9634
+ }
9635
+ /**
9636
+ * Create configuration from an element
9637
+ * Extracts window from element's owner document
9638
+ */
9639
+ static fromElement(element, options = {}) {
9640
+ const ownerDocument = element.ownerDocument;
9641
+ if (!ownerDocument) {
9642
+ throw new Error('Element is not attached to a document');
9643
+ }
9644
+ const defaultView = ownerDocument.defaultView;
9645
+ if (!defaultView) {
9646
+ throw new Error('Document is not attached to a window');
9647
+ }
9648
+ return new Html2CanvasConfig({
9649
+ window: defaultView,
9650
+ ...options
8532
9651
  });
8533
9652
  }
8534
- renderRepeat(path, pattern, offsetX, offsetY) {
8535
- this.path(path);
8536
- this.ctx.fillStyle = pattern;
8537
- this.ctx.translate(offsetX, offsetY);
8538
- this.ctx.fill();
8539
- this.ctx.translate(-offsetX, -offsetY);
9653
+ /**
9654
+ * Clone configuration with override options
9655
+ */
9656
+ clone(options = {}) {
9657
+ return new Html2CanvasConfig({
9658
+ window: options.window || this.window,
9659
+ cspNonce: options.cspNonce ?? this.cspNonce,
9660
+ cache: options.cache ?? this.cache
9661
+ });
8540
9662
  }
8541
- resizeImage(image, width, height) {
8542
- // https://github.com/niklasvh/html2canvas/pull/2911
8543
- // if (image.width === width && image.height === height) {
8544
- // return image;
8545
- // }
8546
- const ownerDocument = this.canvas.ownerDocument ?? document;
8547
- const canvas = ownerDocument.createElement('canvas');
8548
- canvas.width = Math.max(1, width);
8549
- canvas.height = Math.max(1, height);
8550
- const ctx = canvas.getContext('2d');
8551
- ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
8552
- return canvas;
9663
+ }
9664
+ /**
9665
+ * Set default configuration
9666
+ * @deprecated Pass configuration directly to html2canvas instead
9667
+ */
9668
+ function setDefaultConfig(config) {
9669
+ console.warn('[html2canvas-pro] setDefaultConfig is deprecated. Pass configuration to html2canvas directly.');
9670
+ }
9671
+
9672
+ /**
9673
+ * Input Validator
9674
+ *
9675
+ * Provides validation and sanitization for user inputs to prevent security vulnerabilities
9676
+ * including SSRF, XSS, and injection attacks.
9677
+ */
9678
+ /**
9679
+ * Input Validator
9680
+ *
9681
+ * Validates and sanitizes user inputs for security and correctness.
9682
+ */
9683
+ class Validator {
9684
+ constructor(config = {}) {
9685
+ this.config = {
9686
+ maxImageTimeout: 300000, // 5 minutes default
9687
+ allowDataUrls: true,
9688
+ ...config
9689
+ };
8553
9690
  }
8554
- async renderBackgroundImage(container) {
8555
- let index = container.styles.backgroundImage.length - 1;
8556
- for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
8557
- if (backgroundImage.type === 0 /* CSSImageType.URL */) {
8558
- let image;
8559
- const url = backgroundImage.url;
8560
- try {
8561
- image = await this.context.cache.match(url);
8562
- }
8563
- catch (e) {
8564
- this.context.logger.error(`Error loading background-image ${url}`);
8565
- }
8566
- if (image) {
8567
- const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
8568
- const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
8569
- const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [
8570
- imageWidth,
8571
- imageHeight,
8572
- imageWidth / imageHeight
8573
- ]);
8574
- const pattern = this.ctx.createPattern(this.resizeImage(image, width, height), 'repeat');
8575
- this.renderRepeat(path, pattern, x, y);
8576
- }
9691
+ /**
9692
+ * Validate a URL
9693
+ *
9694
+ * @param url - URL to validate
9695
+ * @param context - Context for validation (e.g., 'proxy', 'image')
9696
+ * @returns Validation result
9697
+ */
9698
+ validateUrl(url, context = 'general') {
9699
+ if (!url || typeof url !== 'string') {
9700
+ return {
9701
+ valid: false,
9702
+ error: 'URL must be a non-empty string'
9703
+ };
9704
+ }
9705
+ // Check for data URLs
9706
+ if (url.startsWith('data:')) {
9707
+ if (!this.config.allowDataUrls) {
9708
+ return {
9709
+ valid: false,
9710
+ error: 'Data URLs are not allowed'
9711
+ };
8577
9712
  }
8578
- else if (isLinearGradient(backgroundImage)) {
8579
- const [path, x, y, width, height] = calculateBackgroundRendering(container, index, [null, null, null]);
8580
- const [lineLength, x0, x1, y0, y1] = calculateGradientDirection(backgroundImage.angle, width, height);
8581
- const canvas = document.createElement('canvas');
8582
- canvas.width = width;
8583
- canvas.height = height;
8584
- const ctx = canvas.getContext('2d');
8585
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
8586
- processColorStops(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, asString(colorStop.color)));
8587
- ctx.fillStyle = gradient;
8588
- ctx.fillRect(0, 0, width, height);
8589
- if (width > 0 && height > 0) {
8590
- const pattern = this.ctx.createPattern(canvas, 'repeat');
8591
- this.renderRepeat(path, pattern, x, y);
9713
+ return { valid: true, sanitized: url };
9714
+ }
9715
+ // Check for blob URLs
9716
+ if (url.startsWith('blob:')) {
9717
+ return { valid: true, sanitized: url };
9718
+ }
9719
+ // Validate URL format
9720
+ try {
9721
+ const parsedUrl = new URL(url);
9722
+ // Only allow http and https protocols
9723
+ if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
9724
+ return {
9725
+ valid: false,
9726
+ error: `Protocol ${parsedUrl.protocol} is not allowed. Only http and https are permitted.`
9727
+ };
9728
+ }
9729
+ // For proxy URLs, check domain whitelist
9730
+ if (context === 'proxy' && this.config.allowedProxyDomains && this.config.allowedProxyDomains.length > 0) {
9731
+ const hostname = parsedUrl.hostname.toLowerCase();
9732
+ const isAllowed = this.config.allowedProxyDomains.some((domain) => {
9733
+ const normalizedDomain = domain.toLowerCase();
9734
+ return hostname === normalizedDomain || hostname.endsWith('.' + normalizedDomain);
9735
+ });
9736
+ if (!isAllowed) {
9737
+ return {
9738
+ valid: false,
9739
+ error: `Proxy domain ${parsedUrl.hostname} is not in the allowed list`
9740
+ };
8592
9741
  }
8593
9742
  }
8594
- else if (isRadialGradient(backgroundImage)) {
8595
- const [path, left, top, width, height] = calculateBackgroundRendering(container, index, [
8596
- null,
8597
- null,
8598
- null
8599
- ]);
8600
- const position = backgroundImage.position.length === 0 ? [FIFTY_PERCENT] : backgroundImage.position;
8601
- const x = getAbsoluteValue(position[0], width);
8602
- const y = getAbsoluteValue(position[position.length - 1], height);
8603
- let [rx, ry] = calculateRadius(backgroundImage, x, y, width, height);
8604
- // Handle edge case where radial gradient size is 0
8605
- // Use a minimum value of 0.01 to ensure gradient is still rendered
8606
- if (rx === 0 || ry === 0) {
8607
- rx = Math.max(rx, 0.01);
8608
- ry = Math.max(ry, 0.01);
8609
- }
8610
- if (rx > 0 && ry > 0) {
8611
- const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
8612
- processColorStops(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, asString(colorStop.color)));
8613
- this.path(path);
8614
- this.ctx.fillStyle = radialGradient;
8615
- if (rx !== ry) {
8616
- // transforms for elliptical radial gradient
8617
- const midX = container.bounds.left + 0.5 * container.bounds.width;
8618
- const midY = container.bounds.top + 0.5 * container.bounds.height;
8619
- const f = ry / rx;
8620
- const invF = 1 / f;
8621
- this.ctx.save();
8622
- this.ctx.translate(midX, midY);
8623
- this.ctx.transform(1, 0, 0, f, 0, 0);
8624
- this.ctx.translate(-midX, -midY);
8625
- this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
8626
- this.ctx.restore();
9743
+ // Check for localhost/private IPs to prevent SSRF (skip when allowLocalhostProxy for dev/test)
9744
+ if (context === 'proxy') {
9745
+ if (!this.config.allowLocalhostProxy) {
9746
+ const hostname = parsedUrl.hostname.toLowerCase();
9747
+ // Check for localhost
9748
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
9749
+ return {
9750
+ valid: false,
9751
+ error: 'Localhost is not allowed for proxy URLs'
9752
+ };
8627
9753
  }
8628
- else {
8629
- this.ctx.fill();
9754
+ // For private IP ranges (simplified check)
9755
+ if (this.isPrivateIP(hostname)) {
9756
+ return {
9757
+ valid: false,
9758
+ error: 'Private IP addresses are not allowed for proxy URLs'
9759
+ };
9760
+ }
9761
+ // For link-local addresses
9762
+ if (hostname.startsWith('169.254.') || hostname.startsWith('fe80:')) {
9763
+ return {
9764
+ valid: false,
9765
+ error: 'Link-local addresses are not allowed for proxy URLs'
9766
+ };
8630
9767
  }
8631
9768
  }
9769
+ // For proxy URLs, mark that runtime validation is recommended
9770
+ // to prevent DNS rebinding attacks
9771
+ return {
9772
+ valid: true,
9773
+ sanitized: url,
9774
+ requiresRuntimeCheck: true
9775
+ };
8632
9776
  }
8633
- index--;
9777
+ return { valid: true, sanitized: url };
8634
9778
  }
8635
- }
8636
- async renderSolidBorder(color, side, curvePoints) {
8637
- this.path(parsePathForBorder(curvePoints, side));
8638
- this.ctx.fillStyle = asString(color);
8639
- this.ctx.fill();
8640
- }
8641
- async renderDoubleBorder(color, width, side, curvePoints) {
8642
- if (width < 3) {
8643
- await this.renderSolidBorder(color, side, curvePoints);
8644
- return;
9779
+ catch (e) {
9780
+ return {
9781
+ valid: false,
9782
+ error: `Invalid URL format: ${e instanceof Error ? e.message : 'Unknown error'}`
9783
+ };
8645
9784
  }
8646
- const outerPaths = parsePathForBorderDoubleOuter(curvePoints, side);
8647
- this.path(outerPaths);
8648
- this.ctx.fillStyle = asString(color);
8649
- this.ctx.fill();
8650
- const innerPaths = parsePathForBorderDoubleInner(curvePoints, side);
8651
- this.path(innerPaths);
8652
- this.ctx.fill();
8653
9785
  }
8654
- async renderNodeBackgroundAndBorders(paint) {
8655
- this.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
8656
- const styles = paint.container.styles;
8657
- const hasBackground = !isTransparent(styles.backgroundColor) || styles.backgroundImage.length;
8658
- const borders = [
8659
- { style: styles.borderTopStyle, color: styles.borderTopColor, width: styles.borderTopWidth },
8660
- { style: styles.borderRightStyle, color: styles.borderRightColor, width: styles.borderRightWidth },
8661
- { style: styles.borderBottomStyle, color: styles.borderBottomColor, width: styles.borderBottomWidth },
8662
- { style: styles.borderLeftStyle, color: styles.borderLeftColor, width: styles.borderLeftWidth }
9786
+ /**
9787
+ * Check if a hostname is a private IP address
9788
+ */
9789
+ isPrivateIP(hostname) {
9790
+ // IPv4 private ranges
9791
+ const privateIPv4Patterns = [
9792
+ /^0\./, // 0.0.0.0/8 (This network)
9793
+ /^10\./, // 10.0.0.0/8 (Private)
9794
+ /^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./, // 100.64.0.0/10 (CGNAT)
9795
+ /^127\./, // 127.0.0.0/8 (Loopback)
9796
+ /^169\.254\./, // 169.254.0.0/16 (Link-local)
9797
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12 (Private)
9798
+ /^192\.0\.0\./, // 192.0.0.0/24 (IETF Protocol Assignments)
9799
+ /^192\.0\.2\./, // 192.0.2.0/24 (TEST-NET-1)
9800
+ /^192\.168\./, // 192.168.0.0/16 (Private)
9801
+ /^198\.(1[8-9])\./, // 198.18.0.0/15 (Network benchmark)
9802
+ /^198\.51\.100\./, // 198.51.100.0/24 (TEST-NET-2)
9803
+ /^203\.0\.113\./, // 203.0.113.0/24 (TEST-NET-3)
9804
+ /^2(2[4-9]|3[0-9])\./, // 224.0.0.0/4 (Multicast)
9805
+ /^24[0-9]\./, // 240.0.0.0/4 (Reserved)
9806
+ /^255\.255\.255\.255$/ // 255.255.255.255/32 (Broadcast)
8663
9807
  ];
8664
- const backgroundPaintingArea = calculateBackgroundCurvedPaintingArea(getBackgroundValueForIndex(styles.backgroundClip, 0), paint.curves);
8665
- if (hasBackground || styles.boxShadow.length) {
8666
- this.ctx.save();
8667
- this.path(backgroundPaintingArea);
8668
- this.ctx.clip();
8669
- if (!isTransparent(styles.backgroundColor)) {
8670
- this.ctx.fillStyle = asString(styles.backgroundColor);
8671
- this.ctx.fill();
9808
+ // Check IPv4
9809
+ if (privateIPv4Patterns.some((pattern) => pattern.test(hostname))) {
9810
+ return true;
9811
+ }
9812
+ // IPv6 private ranges and special addresses
9813
+ if (hostname.includes(':')) {
9814
+ return this.isPrivateIPv6(hostname);
9815
+ }
9816
+ return false;
9817
+ }
9818
+ /**
9819
+ * Check if an IPv6 address is private or special
9820
+ * Handles compressed IPv6 addresses (e.g., ::1, fc00::1)
9821
+ */
9822
+ isPrivateIPv6(hostname) {
9823
+ const normalizedHost = hostname.toLowerCase().trim();
9824
+ // Remove square brackets if present (e.g., [::1])
9825
+ const addr = normalizedHost.replace(/^\[|\]$/g, '');
9826
+ // Remove zone ID if present (e.g., fe80::1%eth0)
9827
+ const addrWithoutZone = addr.split('%')[0];
9828
+ // Loopback ::1 (also matches 0:0:0:0:0:0:0:1)
9829
+ if (/^(0:){7}1$/.test(addrWithoutZone) || addrWithoutZone === '::1') {
9830
+ return true;
9831
+ }
9832
+ // Unspecified address :: (also matches 0:0:0:0:0:0:0:0)
9833
+ if (/^(0:){7}0$/.test(addrWithoutZone) || addrWithoutZone === '::') {
9834
+ return true;
9835
+ }
9836
+ // Expand :: compression to check prefixes
9837
+ // This handles cases like fc00::1, fe80::, etc.
9838
+ const expandedAddr = this.expandIPv6(addrWithoutZone);
9839
+ if (!expandedAddr) {
9840
+ // If we can't expand it, fall back to prefix matching
9841
+ return this.isPrivateIPv6Prefix(addrWithoutZone);
9842
+ }
9843
+ // fc00::/7 (Unique Local Address)
9844
+ // Check if first byte is in range fc00-fdff
9845
+ const firstByte = parseInt(expandedAddr.substring(0, 2), 16);
9846
+ if (firstByte >= 0xfc && firstByte <= 0xfd) {
9847
+ return true;
9848
+ }
9849
+ // fe80::/10 (Link-local)
9850
+ // First 10 bits should be 1111 1110 10
9851
+ if (firstByte === 0xfe) {
9852
+ const secondByte = parseInt(expandedAddr.substring(2, 4), 16);
9853
+ // Check if bits 11-12 are 10 (0x80-0xbf)
9854
+ if (secondByte >= 0x80 && secondByte <= 0xbf) {
9855
+ return true;
8672
9856
  }
8673
- await this.renderBackgroundImage(paint.container);
8674
- this.ctx.restore();
8675
- styles.boxShadow
8676
- .slice(0)
8677
- .reverse()
8678
- .forEach((shadow) => {
8679
- this.ctx.save();
8680
- const borderBoxArea = calculateBorderBoxPath(paint.curves);
8681
- const maskOffset = shadow.inset ? 0 : MASK_OFFSET;
8682
- 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));
8683
- if (shadow.inset) {
8684
- this.path(borderBoxArea);
8685
- this.ctx.clip();
8686
- this.mask(shadowPaintingArea);
8687
- }
8688
- else {
8689
- this.mask(borderBoxArea);
8690
- this.ctx.clip();
8691
- this.path(shadowPaintingArea);
8692
- }
8693
- this.ctx.shadowOffsetX = shadow.offsetX.number + maskOffset;
8694
- this.ctx.shadowOffsetY = shadow.offsetY.number;
8695
- this.ctx.shadowColor = asString(shadow.color);
8696
- this.ctx.shadowBlur = shadow.blur.number;
8697
- this.ctx.fillStyle = shadow.inset ? asString(shadow.color) : 'rgba(0,0,0,1)';
8698
- this.ctx.fill();
8699
- this.ctx.restore();
8700
- });
8701
9857
  }
8702
- let side = 0;
8703
- for (const border of borders) {
8704
- if (border.style !== 0 /* BORDER_STYLE.NONE */ && !isTransparent(border.color) && border.width > 0) {
8705
- if (border.style === 2 /* BORDER_STYLE.DASHED */) {
8706
- await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
8707
- }
8708
- else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
8709
- await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
9858
+ // ff00::/8 (Multicast)
9859
+ if (firstByte === 0xff) {
9860
+ return true;
9861
+ }
9862
+ return false;
9863
+ }
9864
+ /**
9865
+ * Expand compressed IPv6 address to full form
9866
+ * e.g., "::1" -> "0000:0000:0000:0000:0000:0000:0000:0001"
9867
+ */
9868
+ expandIPv6(addr) {
9869
+ try {
9870
+ // Handle :: compression
9871
+ if (addr.includes('::')) {
9872
+ const parts = addr.split('::');
9873
+ if (parts.length > 2) {
9874
+ return null; // Invalid: more than one ::
8710
9875
  }
8711
- else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
8712
- await this.renderDoubleBorder(border.color, border.width, side, paint.curves);
9876
+ const leftParts = parts[0] ? parts[0].split(':') : [];
9877
+ const rightParts = parts[1] ? parts[1].split(':') : [];
9878
+ const missingParts = 8 - leftParts.length - rightParts.length;
9879
+ if (missingParts < 0) {
9880
+ return null; // Invalid
8713
9881
  }
8714
- else {
8715
- await this.renderSolidBorder(border.color, side, paint.curves);
9882
+ const middleParts = Array(missingParts).fill('0000');
9883
+ const allParts = [...leftParts, ...middleParts, ...rightParts];
9884
+ return allParts.map((p) => p.padStart(4, '0')).join(':');
9885
+ }
9886
+ else {
9887
+ // No compression, just normalize
9888
+ const parts = addr.split(':');
9889
+ if (parts.length !== 8) {
9890
+ return null; // Invalid
8716
9891
  }
9892
+ return parts.map((p) => p.padStart(4, '0')).join(':');
8717
9893
  }
8718
- side++;
9894
+ }
9895
+ catch {
9896
+ return null;
8719
9897
  }
8720
9898
  }
8721
- async renderDashedDottedBorder(color, width, side, curvePoints, style) {
8722
- this.ctx.save();
8723
- const strokePaths = parsePathForBorderStroke(curvePoints, side);
8724
- const boxPaths = parsePathForBorder(curvePoints, side);
8725
- if (style === 2 /* BORDER_STYLE.DASHED */) {
8726
- this.path(boxPaths);
8727
- this.ctx.clip();
9899
+ /**
9900
+ * Fallback prefix matching for IPv6 when expansion fails
9901
+ */
9902
+ isPrivateIPv6Prefix(addr) {
9903
+ // fc00::/7 (Unique Local Address)
9904
+ if (/^fc[0-9a-f]{0,2}:?/i.test(addr) || /^fd[0-9a-f]{0,2}:?/i.test(addr)) {
9905
+ return true;
8728
9906
  }
8729
- let startX, startY, endX, endY;
8730
- if (isBezierCurve(boxPaths[0])) {
8731
- startX = boxPaths[0].start.x;
8732
- startY = boxPaths[0].start.y;
9907
+ // fe80::/10 (Link-local)
9908
+ if (/^fe[89ab][0-9a-f]:?/i.test(addr)) {
9909
+ return true;
8733
9910
  }
8734
- else {
8735
- startX = boxPaths[0].x;
8736
- startY = boxPaths[0].y;
9911
+ // ff00::/8 (Multicast)
9912
+ if (/^ff[0-9a-f]{0,2}:?/i.test(addr)) {
9913
+ return true;
8737
9914
  }
8738
- if (isBezierCurve(boxPaths[1])) {
8739
- endX = boxPaths[1].end.x;
8740
- endY = boxPaths[1].end.y;
9915
+ return false;
9916
+ }
9917
+ /**
9918
+ * Validate CSP nonce
9919
+ *
9920
+ * @param nonce - CSP nonce to validate
9921
+ * @returns Validation result
9922
+ */
9923
+ validateCspNonce(nonce) {
9924
+ if (!nonce || typeof nonce !== 'string') {
9925
+ return {
9926
+ valid: false,
9927
+ error: 'CSP nonce must be a non-empty string'
9928
+ };
8741
9929
  }
8742
- else {
8743
- endX = boxPaths[1].x;
8744
- endY = boxPaths[1].y;
9930
+ // Basic format validation - nonce should be base64-like
9931
+ // Typical format: base64 string, often 32+ characters
9932
+ if (nonce.length < 16) {
9933
+ return {
9934
+ valid: false,
9935
+ error: 'CSP nonce is too short (minimum 16 characters recommended)'
9936
+ };
8745
9937
  }
8746
- let length;
8747
- if (side === 0 || side === 2) {
8748
- length = Math.abs(startX - endX);
9938
+ // Check for suspicious characters
9939
+ if (!/^[A-Za-z0-9+/=_-]+$/.test(nonce)) {
9940
+ return {
9941
+ valid: false,
9942
+ error: 'CSP nonce contains invalid characters'
9943
+ };
8749
9944
  }
8750
- else {
8751
- length = Math.abs(startY - endY);
9945
+ return { valid: true, sanitized: nonce };
9946
+ }
9947
+ /**
9948
+ * Validate image timeout
9949
+ *
9950
+ * @param timeout - Timeout in milliseconds
9951
+ * @returns Validation result
9952
+ */
9953
+ validateImageTimeout(timeout) {
9954
+ if (typeof timeout !== 'number' || isNaN(timeout)) {
9955
+ return {
9956
+ valid: false,
9957
+ error: 'Image timeout must be a number'
9958
+ };
8752
9959
  }
8753
- this.ctx.beginPath();
8754
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8755
- this.formatPath(strokePaths);
9960
+ if (timeout < 0) {
9961
+ return {
9962
+ valid: false,
9963
+ error: 'Image timeout cannot be negative'
9964
+ };
8756
9965
  }
8757
- else {
8758
- this.formatPath(boxPaths.slice(0, 2));
9966
+ if (this.config.maxImageTimeout && timeout > this.config.maxImageTimeout) {
9967
+ return {
9968
+ valid: false,
9969
+ error: `Image timeout ${timeout}ms exceeds maximum allowed ${this.config.maxImageTimeout}ms`
9970
+ };
8759
9971
  }
8760
- let dashLength = width < 3 ? width * 3 : width * 2;
8761
- let spaceLength = width < 3 ? width * 2 : width;
8762
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8763
- dashLength = width;
8764
- spaceLength = width;
9972
+ return { valid: true, sanitized: timeout };
9973
+ }
9974
+ /**
9975
+ * Validate window dimensions
9976
+ *
9977
+ * @param width - Window width
9978
+ * @param height - Window height
9979
+ * @returns Validation result
9980
+ */
9981
+ validateDimensions(width, height) {
9982
+ if (typeof width !== 'number' || typeof height !== 'number') {
9983
+ return {
9984
+ valid: false,
9985
+ error: 'Dimensions must be numbers'
9986
+ };
8765
9987
  }
8766
- let useLineDash = true;
8767
- if (length <= dashLength * 2) {
8768
- useLineDash = false;
9988
+ if (isNaN(width) || isNaN(height)) {
9989
+ return {
9990
+ valid: false,
9991
+ error: 'Dimensions cannot be NaN'
9992
+ };
8769
9993
  }
8770
- else if (length <= dashLength * 2 + spaceLength) {
8771
- const multiplier = length / (2 * dashLength + spaceLength);
8772
- dashLength *= multiplier;
8773
- spaceLength *= multiplier;
9994
+ if (width <= 0 || height <= 0) {
9995
+ return {
9996
+ valid: false,
9997
+ error: 'Dimensions must be positive'
9998
+ };
9999
+ }
10000
+ // Reasonable maximum to prevent memory issues
10001
+ const MAX_DIMENSION = 32767; // Common canvas limit
10002
+ if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
10003
+ return {
10004
+ valid: false,
10005
+ error: `Dimensions exceed maximum allowed (${MAX_DIMENSION}px)`
10006
+ };
10007
+ }
10008
+ return { valid: true, sanitized: { width, height } };
10009
+ }
10010
+ /**
10011
+ * Validate scale factor
10012
+ *
10013
+ * @param scale - Scale factor
10014
+ * @returns Validation result
10015
+ */
10016
+ validateScale(scale) {
10017
+ if (typeof scale !== 'number' || isNaN(scale)) {
10018
+ return {
10019
+ valid: false,
10020
+ error: 'Scale must be a number'
10021
+ };
10022
+ }
10023
+ if (scale <= 0) {
10024
+ return {
10025
+ valid: false,
10026
+ error: 'Scale must be positive'
10027
+ };
10028
+ }
10029
+ // Reasonable scale limits
10030
+ if (scale > 10) {
10031
+ return {
10032
+ valid: false,
10033
+ error: 'Scale factor too large (maximum 10x)'
10034
+ };
10035
+ }
10036
+ return { valid: true, sanitized: scale };
10037
+ }
10038
+ /**
10039
+ * Validate HTML element
10040
+ *
10041
+ * @param element - Element to validate
10042
+ * @returns Validation result
10043
+ */
10044
+ validateElement(element) {
10045
+ if (!element) {
10046
+ return {
10047
+ valid: false,
10048
+ error: 'Element is required'
10049
+ };
10050
+ }
10051
+ if (typeof element !== 'object') {
10052
+ return {
10053
+ valid: false,
10054
+ error: 'Element must be an object'
10055
+ };
10056
+ }
10057
+ // Accept real HTMLElement, or any element-like object with the minimal shape
10058
+ // required by the implementation (ownerDocument + defaultView) for backward
10059
+ // compatibility and test environments.
10060
+ if (typeof HTMLElement !== 'undefined' && element instanceof HTMLElement) {
10061
+ // Real DOM element
10062
+ if (!element.ownerDocument) {
10063
+ return { valid: false, error: 'Element must be attached to a document' };
10064
+ }
10065
+ return { valid: true };
10066
+ }
10067
+ // Duck-typing: accept object with ownerDocument and defaultView (minimal contract)
10068
+ if (!element.ownerDocument) {
10069
+ return {
10070
+ valid: false,
10071
+ error: 'Element must be attached to a document (ownerDocument required)'
10072
+ };
8774
10073
  }
8775
- else {
8776
- const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
8777
- const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
8778
- const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
8779
- spaceLength =
8780
- maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
8781
- ? minSpace
8782
- : maxSpace;
10074
+ if (!element.ownerDocument.defaultView) {
10075
+ return {
10076
+ valid: false,
10077
+ error: 'Document must be attached to a window (ownerDocument.defaultView required)'
10078
+ };
8783
10079
  }
8784
- if (useLineDash) {
8785
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8786
- this.ctx.setLineDash([0, dashLength + spaceLength]);
10080
+ return { valid: true };
10081
+ }
10082
+ /**
10083
+ * Validate entire options object
10084
+ *
10085
+ * @param options - Options to validate
10086
+ * @returns Validation result with all errors
10087
+ */
10088
+ validateOptions(options) {
10089
+ const errors = [];
10090
+ // Validate proxy URL only when a non-empty string (allow null/undefined to mean "no proxy")
10091
+ const proxyUrl = options.proxy;
10092
+ if (proxyUrl !== undefined && proxyUrl !== null && typeof proxyUrl === 'string' && proxyUrl.length > 0) {
10093
+ const proxyResult = this.validateUrl(proxyUrl, 'proxy');
10094
+ if (!proxyResult.valid) {
10095
+ errors.push(`Proxy: ${proxyResult.error}`);
8787
10096
  }
8788
- else {
8789
- this.ctx.setLineDash([dashLength, spaceLength]);
10097
+ // Note: Proxy URLs are marked with requiresRuntimeCheck to prevent DNS rebinding
10098
+ // Consider implementing runtime IP validation in production environments
10099
+ }
10100
+ // Validate image timeout
10101
+ if (options.imageTimeout !== undefined) {
10102
+ const timeoutResult = this.validateImageTimeout(options.imageTimeout);
10103
+ if (!timeoutResult.valid) {
10104
+ errors.push(`Image timeout: ${timeoutResult.error}`);
8790
10105
  }
8791
10106
  }
8792
- if (style === 3 /* BORDER_STYLE.DOTTED */) {
8793
- this.ctx.lineCap = 'round';
8794
- this.ctx.lineWidth = width;
10107
+ // Validate dimensions
10108
+ if (options.width !== undefined || options.height !== undefined) {
10109
+ const width = options.width ?? 800;
10110
+ const height = options.height ?? 600;
10111
+ const dimensionsResult = this.validateDimensions(width, height);
10112
+ if (!dimensionsResult.valid) {
10113
+ errors.push(`Dimensions: ${dimensionsResult.error}`);
10114
+ }
8795
10115
  }
8796
- else {
8797
- this.ctx.lineWidth = width * 2 + 1.1;
10116
+ // Validate scale
10117
+ if (options.scale !== undefined) {
10118
+ const scaleResult = this.validateScale(options.scale);
10119
+ if (!scaleResult.valid) {
10120
+ errors.push(`Scale: ${scaleResult.error}`);
10121
+ }
8798
10122
  }
8799
- this.ctx.strokeStyle = asString(color);
8800
- this.ctx.stroke();
8801
- this.ctx.setLineDash([]);
8802
- // dashed round edge gap
8803
- if (style === 2 /* BORDER_STYLE.DASHED */) {
8804
- if (isBezierCurve(boxPaths[0])) {
8805
- const path1 = boxPaths[3];
8806
- const path2 = boxPaths[0];
8807
- this.ctx.beginPath();
8808
- this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]);
8809
- this.ctx.stroke();
10123
+ // Validate CSP nonce
10124
+ if (options.cspNonce !== undefined) {
10125
+ const nonceResult = this.validateCspNonce(options.cspNonce);
10126
+ if (!nonceResult.valid) {
10127
+ errors.push(`CSP nonce: ${nonceResult.error}`);
8810
10128
  }
8811
- if (isBezierCurve(boxPaths[1])) {
8812
- const path1 = boxPaths[1];
8813
- const path2 = boxPaths[2];
8814
- this.ctx.beginPath();
8815
- this.formatPath([new Vector(path1.end.x, path1.end.y), new Vector(path2.start.x, path2.start.y)]);
8816
- this.ctx.stroke();
10129
+ }
10130
+ // Custom validation
10131
+ if (this.config.customValidator) {
10132
+ const customResult = this.config.customValidator(options, 'options');
10133
+ if (!customResult.valid) {
10134
+ errors.push(`Custom validation: ${customResult.error}`);
8817
10135
  }
8818
10136
  }
8819
- this.ctx.restore();
8820
- }
8821
- async render(element) {
8822
- if (this.options.backgroundColor) {
8823
- this.ctx.fillStyle = asString(this.options.backgroundColor);
8824
- this.ctx.fillRect(this.options.x, this.options.y, this.options.width, this.options.height);
10137
+ if (errors.length > 0) {
10138
+ return {
10139
+ valid: false,
10140
+ error: errors.join('; ')
10141
+ };
8825
10142
  }
8826
- const stack = parseStackingContexts(element);
8827
- await this.renderStack(stack);
8828
- this.applyEffects([]);
8829
- return this.canvas;
10143
+ return { valid: true };
8830
10144
  }
8831
10145
  }
8832
- const isTextInputElement = (container) => {
8833
- if (container instanceof TextareaElementContainer) {
8834
- return true;
8835
- }
8836
- else if (container instanceof SelectElementContainer) {
8837
- return true;
8838
- }
8839
- else if (container instanceof InputElementContainer && container.type !== RADIO && container.type !== CHECKBOX) {
8840
- return true;
8841
- }
8842
- return false;
8843
- };
8844
- const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
8845
- switch (clip) {
8846
- case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
8847
- return calculateBorderBoxPath(curves);
8848
- case 2 /* BACKGROUND_CLIP.CONTENT_BOX */:
8849
- return calculateContentBoxPath(curves);
8850
- case 1 /* BACKGROUND_CLIP.PADDING_BOX */:
8851
- default:
8852
- return calculatePaddingBoxPath(curves);
8853
- }
8854
- };
8855
- const canvasTextAlign = (textAlign) => {
8856
- switch (textAlign) {
8857
- case 1 /* TEXT_ALIGN.CENTER */:
8858
- return 'center';
8859
- case 2 /* TEXT_ALIGN.RIGHT */:
8860
- return 'right';
8861
- case 0 /* TEXT_ALIGN.LEFT */:
8862
- default:
8863
- return 'left';
8864
- }
8865
- };
8866
- // see https://github.com/niklasvh/html2canvas/pull/2645
8867
- const iOSBrokenFonts = ['-apple-system', 'system-ui'];
8868
- const fixIOSSystemFonts = (fontFamilies) => {
8869
- return /iPhone OS 15_(0|1)/.test(window.navigator.userAgent)
8870
- ? fontFamilies.filter((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) === -1)
8871
- : fontFamilies;
8872
- };
8873
-
8874
- class ForeignObjectRenderer extends Renderer {
8875
- constructor(context, options) {
8876
- super(context, options);
8877
- this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
8878
- this.ctx = this.canvas.getContext('2d');
8879
- this.options = options;
8880
- this.canvas.width = Math.floor(options.width * options.scale);
8881
- this.canvas.height = Math.floor(options.height * options.scale);
8882
- this.canvas.style.width = `${options.width}px`;
8883
- this.canvas.style.height = `${options.height}px`;
8884
- this.ctx.scale(this.options.scale, this.options.scale);
8885
- this.ctx.translate(-options.x, -options.y);
8886
- this.context.logger.debug(`EXPERIMENTAL ForeignObject renderer initialized (${options.width}x${options.height} at ${options.x},${options.y}) with scale ${options.scale}`);
8887
- }
8888
- async render(element) {
8889
- const svg = createForeignObjectSVG(this.options.width * this.options.scale, this.options.height * this.options.scale, this.options.scale, this.options.scale, element);
8890
- const img = await loadSerializedSVG(svg);
8891
- if (this.options.backgroundColor) {
8892
- this.ctx.fillStyle = asString(this.options.backgroundColor);
8893
- this.ctx.fillRect(0, 0, this.options.width * this.options.scale, this.options.height * this.options.scale);
8894
- }
8895
- this.ctx.drawImage(img, -this.options.x * this.options.scale, -this.options.y * this.options.scale);
8896
- return this.canvas;
8897
- }
10146
+ /**
10147
+ * Create a default validator instance
10148
+ */
10149
+ function createDefaultValidator(config = {}) {
10150
+ return new Validator({
10151
+ allowDataUrls: true,
10152
+ maxImageTimeout: 300000, // 5 minutes
10153
+ ...config
10154
+ });
8898
10155
  }
8899
- const loadSerializedSVG = (svg) => new Promise((resolve, reject) => {
8900
- const img = new Image();
8901
- img.onload = () => {
8902
- resolve(img);
8903
- };
8904
- img.onerror = reject;
8905
- img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
8906
- });
8907
10156
 
8908
- class Logger {
8909
- constructor({ id, enabled }) {
8910
- this.id = id;
10157
+ /**
10158
+ * Performance Monitor
10159
+ *
10160
+ * Tracks performance metrics throughout the rendering pipeline.
10161
+ * Provides insights into where time is spent during rendering.
10162
+ *
10163
+ * Usage:
10164
+ * ```typescript
10165
+ * const monitor = new PerformanceMonitor(context);
10166
+ *
10167
+ * monitor.start('clone');
10168
+ * await cloneDocument();
10169
+ * monitor.end('clone');
10170
+ *
10171
+ * const summary = monitor.getSummary();
10172
+ * ```
10173
+ */
10174
+ class PerformanceMonitor {
10175
+ constructor(context, enabled = true) {
10176
+ this.context = context;
10177
+ this.activeMetrics = new Map();
10178
+ this.completedMetrics = [];
8911
10179
  this.enabled = enabled;
8912
- this.start = Date.now();
10180
+ // Fallback for environments without performance.now()
10181
+ this.getTime =
10182
+ typeof performance !== 'undefined' && typeof performance.now === 'function'
10183
+ ? () => performance.now()
10184
+ : () => Date.now();
8913
10185
  }
8914
- debug(...args) {
8915
- if (this.enabled) {
8916
- // eslint-disable-next-line no-console
8917
- if (typeof window !== 'undefined' && window.console && typeof console.debug === 'function') {
8918
- // eslint-disable-next-line no-console
8919
- console.debug(this.id, `${this.getTime()}ms`, ...args);
8920
- }
8921
- else {
8922
- this.info(...args);
8923
- }
10186
+ /**
10187
+ * Start measuring a performance metric
10188
+ *
10189
+ * @param name - Unique name for this metric
10190
+ * @param metadata - Optional metadata to attach
10191
+ */
10192
+ start(name, metadata) {
10193
+ if (!this.enabled) {
10194
+ return;
8924
10195
  }
10196
+ if (this.activeMetrics.has(name)) {
10197
+ this.context?.logger.warn(`Performance metric '${name}' already started. Overwriting.`);
10198
+ }
10199
+ this.activeMetrics.set(name, {
10200
+ name,
10201
+ startTime: this.getTime(),
10202
+ metadata
10203
+ });
8925
10204
  }
8926
- getTime() {
8927
- return Date.now() - this.start;
10205
+ /**
10206
+ * End measuring a performance metric
10207
+ *
10208
+ * @param name - Name of the metric to end
10209
+ * @returns The completed metric, or undefined if not found
10210
+ */
10211
+ end(name) {
10212
+ if (!this.enabled) {
10213
+ return undefined;
10214
+ }
10215
+ const metric = this.activeMetrics.get(name);
10216
+ if (!metric) {
10217
+ this.context?.logger.warn(`Performance metric '${name}' not found. Was start() called?`);
10218
+ return undefined;
10219
+ }
10220
+ metric.endTime = this.getTime();
10221
+ metric.duration = metric.endTime - metric.startTime;
10222
+ this.completedMetrics.push(metric);
10223
+ this.activeMetrics.delete(name);
10224
+ this.context?.logger.debug(`⏱️ ${name}: ${metric.duration.toFixed(2)}ms`, metric.metadata);
10225
+ return metric;
8928
10226
  }
8929
- info(...args) {
8930
- if (this.enabled) {
8931
- // eslint-disable-next-line no-console
8932
- if (typeof window !== 'undefined' && window.console && typeof console.info === 'function') {
8933
- // eslint-disable-next-line no-console
8934
- console.info(this.id, `${this.getTime()}ms`, ...args);
8935
- }
10227
+ /**
10228
+ * Measure a synchronous function
10229
+ *
10230
+ * @param name - Name for this measurement
10231
+ * @param fn - Function to measure
10232
+ * @param metadata - Optional metadata
10233
+ * @returns The function's return value
10234
+ */
10235
+ measure(name, fn, metadata) {
10236
+ this.start(name, metadata);
10237
+ try {
10238
+ const result = fn();
10239
+ this.end(name);
10240
+ return result;
10241
+ }
10242
+ catch (error) {
10243
+ this.end(name);
10244
+ throw error;
8936
10245
  }
8937
10246
  }
8938
- warn(...args) {
8939
- if (this.enabled) {
8940
- if (typeof window !== 'undefined' && window.console && typeof console.warn === 'function') {
8941
- console.warn(this.id, `${this.getTime()}ms`, ...args);
8942
- }
8943
- else {
8944
- this.info(...args);
8945
- }
10247
+ /**
10248
+ * Measure an asynchronous function
10249
+ *
10250
+ * @param name - Name for this measurement
10251
+ * @param fn - Async function to measure
10252
+ * @param metadata - Optional metadata
10253
+ * @returns Promise resolving to the function's return value
10254
+ */
10255
+ async measureAsync(name, fn, metadata) {
10256
+ this.start(name, metadata);
10257
+ try {
10258
+ const result = await fn();
10259
+ this.end(name);
10260
+ return result;
10261
+ }
10262
+ catch (error) {
10263
+ this.end(name);
10264
+ throw error;
8946
10265
  }
8947
10266
  }
8948
- error(...args) {
8949
- if (this.enabled) {
8950
- if (typeof window !== 'undefined' && window.console && typeof console.error === 'function') {
8951
- console.error(this.id, `${this.getTime()}ms`, ...args);
8952
- }
8953
- else {
8954
- this.info(...args);
8955
- }
10267
+ /**
10268
+ * Get all completed metrics
10269
+ *
10270
+ * @returns Array of completed performance metrics
10271
+ */
10272
+ getMetrics() {
10273
+ return [...this.completedMetrics];
10274
+ }
10275
+ /**
10276
+ * Get a specific metric by name
10277
+ *
10278
+ * @param name - Metric name
10279
+ * @returns The metric, or undefined if not found
10280
+ */
10281
+ getMetric(name) {
10282
+ return this.completedMetrics.find((m) => m.name === name);
10283
+ }
10284
+ /**
10285
+ * Get performance summary
10286
+ *
10287
+ * @returns Aggregated performance data
10288
+ */
10289
+ getSummary() {
10290
+ const totalDuration = this.completedMetrics.reduce((sum, metric) => sum + (metric.duration || 0), 0);
10291
+ const breakdown = this.completedMetrics.map((metric) => ({
10292
+ name: metric.name,
10293
+ duration: metric.duration || 0,
10294
+ percentage: totalDuration > 0 ? (((metric.duration || 0) / totalDuration) * 100).toFixed(1) + '%' : '0%'
10295
+ }));
10296
+ return {
10297
+ totalDuration,
10298
+ metrics: this.getMetrics(),
10299
+ breakdown
10300
+ };
10301
+ }
10302
+ /**
10303
+ * Log performance summary to console
10304
+ */
10305
+ logSummary() {
10306
+ if (!this.enabled || this.completedMetrics.length === 0 || !this.context) {
10307
+ return;
8956
10308
  }
10309
+ const summary = this.getSummary();
10310
+ this.context.logger.info(`\n📊 Performance Summary (Total: ${summary.totalDuration.toFixed(2)}ms):`);
10311
+ summary.breakdown
10312
+ .sort((a, b) => b.duration - a.duration)
10313
+ .forEach((item) => {
10314
+ this.context.logger.info(` ${item.name.padEnd(20)} ${item.duration.toFixed(2).padStart(8)}ms ${item.percentage.padStart(6)}`);
10315
+ });
8957
10316
  }
8958
- }
8959
- Logger.instances = {};
8960
-
8961
- class Context {
8962
- constructor(options, windowBounds) {
8963
- this.windowBounds = windowBounds;
8964
- this.instanceName = `#${Context.instanceCount++}`;
8965
- this.logger = new Logger({ id: this.instanceName, enabled: options.logging });
8966
- this.cache = options.cache ?? new Cache(this, options);
10317
+ /**
10318
+ * Clear all metrics
10319
+ */
10320
+ clear() {
10321
+ this.activeMetrics.clear();
10322
+ this.completedMetrics.splice(0);
10323
+ }
10324
+ /**
10325
+ * Check if monitoring is enabled
10326
+ */
10327
+ isEnabled() {
10328
+ return this.enabled;
10329
+ }
10330
+ /**
10331
+ * Get active (uncompleted) metrics
10332
+ * Useful for debugging leaked measurements
10333
+ */
10334
+ getActiveMetrics() {
10335
+ return Array.from(this.activeMetrics.keys());
8967
10336
  }
8968
10337
  }
8969
- Context.instanceCount = 1;
8970
10338
 
8971
- let cspNonce;
8972
- const setCspNonce = (nonce) => {
8973
- cspNonce = nonce;
10339
+ /**
10340
+ * Main html2canvas function with improved configuration management
10341
+ *
10342
+ * @param element - Element to render
10343
+ * @param options - Rendering options
10344
+ * @param config - Optional configuration (for advanced use cases)
10345
+ * @returns Promise resolving to rendered canvas
10346
+ */
10347
+ const html2canvas = (element, options = {}, config) => {
10348
+ // Create configuration from element if not provided
10349
+ const finalConfig = config ||
10350
+ Html2CanvasConfig.fromElement(element, {
10351
+ cspNonce: options.cspNonce,
10352
+ cache: options.cache
10353
+ });
10354
+ return renderElement(element, options, finalConfig);
8974
10355
  };
8975
- const html2canvas = (element, options = {}) => {
8976
- return renderElement(element, options);
10356
+ /**
10357
+ * Set CSP nonce for inline styles
10358
+ * @deprecated Use options.cspNonce instead
10359
+ */
10360
+ const setCspNonce = (nonce) => {
10361
+ console.warn('[html2canvas-pro] setCspNonce is deprecated. ' +
10362
+ 'Pass cspNonce in options instead: html2canvas(element, { cspNonce: "..." })');
10363
+ // For backward compatibility, set default config
10364
+ if (typeof window !== 'undefined') {
10365
+ setDefaultConfig(new Html2CanvasConfig({ window, cspNonce: nonce }));
10366
+ }
8977
10367
  };
8978
10368
  html2canvas.setCspNonce = setCspNonce;
8979
- if (typeof window !== 'undefined') {
8980
- CacheStorage.setContext(window);
8981
- }
8982
- const renderElement = async (element, opts) => {
10369
+ /**
10370
+ * Coerce number-like option values for backward compatibility (e.g. string "2" from form/query).
10371
+ * Mutates opts in place; callers should avoid reusing the same options object if they rely on original types.
10372
+ */
10373
+ const coerceNumberOptions = (opts) => {
10374
+ const numKeys = [
10375
+ 'scale',
10376
+ 'width',
10377
+ 'height',
10378
+ 'imageTimeout',
10379
+ 'x',
10380
+ 'y',
10381
+ 'windowWidth',
10382
+ 'windowHeight',
10383
+ 'scrollX',
10384
+ 'scrollY'
10385
+ ];
10386
+ numKeys.forEach((key) => {
10387
+ const v = opts[key];
10388
+ if (v !== undefined && v !== null && typeof v !== 'number') {
10389
+ const n = Number(v);
10390
+ if (!Number.isNaN(n)) {
10391
+ opts[key] = n;
10392
+ }
10393
+ }
10394
+ });
10395
+ };
10396
+ const renderElement = async (element, opts, config) => {
10397
+ coerceNumberOptions(opts);
10398
+ // Input validation (unless explicitly skipped)
10399
+ if (!opts.skipValidation) {
10400
+ const validator = opts.validator || createDefaultValidator();
10401
+ // Validate element
10402
+ const elementValidation = validator.validateElement(element);
10403
+ if (!elementValidation.valid) {
10404
+ throw new Error(elementValidation.error);
10405
+ }
10406
+ // Validate options
10407
+ const optionsValidation = validator.validateOptions(opts);
10408
+ if (!optionsValidation.valid) {
10409
+ throw new Error(`Invalid options: ${optionsValidation.error}`);
10410
+ }
10411
+ }
8983
10412
  if (!element || typeof element !== 'object') {
8984
- return Promise.reject('Invalid element provided as first argument');
10413
+ throw new Error('Invalid element provided as first argument');
8985
10414
  }
8986
10415
  const ownerDocument = element.ownerDocument;
8987
10416
  if (!ownerDocument) {
@@ -9000,17 +10429,29 @@
9000
10429
  };
9001
10430
  const contextOptions = {
9002
10431
  logging: opts.logging ?? true,
9003
- cache: opts.cache,
10432
+ cache: opts.cache ?? config.cache,
9004
10433
  ...resourceOptions
9005
10434
  };
10435
+ // Fallbacks for minimal window (e.g. element-like mocks) so we don't get NaN
10436
+ const DEFAULT_WINDOW_WIDTH = 800;
10437
+ const DEFAULT_WINDOW_HEIGHT = 600;
10438
+ const DEFAULT_SCROLL = 0;
10439
+ const win = defaultView;
9006
10440
  const windowOptions = {
9007
- windowWidth: opts.windowWidth ?? defaultView.innerWidth,
9008
- windowHeight: opts.windowHeight ?? defaultView.innerHeight,
9009
- scrollX: opts.scrollX ?? defaultView.pageXOffset,
9010
- scrollY: opts.scrollY ?? defaultView.pageYOffset
10441
+ windowWidth: opts.windowWidth ?? win.innerWidth ?? DEFAULT_WINDOW_WIDTH,
10442
+ windowHeight: opts.windowHeight ?? win.innerHeight ?? DEFAULT_WINDOW_HEIGHT,
10443
+ scrollX: opts.scrollX ?? win.pageXOffset ?? DEFAULT_SCROLL,
10444
+ scrollY: opts.scrollY ?? win.pageYOffset ?? DEFAULT_SCROLL
9011
10445
  };
9012
10446
  const windowBounds = new Bounds(windowOptions.scrollX, windowOptions.scrollY, windowOptions.windowWidth, windowOptions.windowHeight);
9013
- const context = new Context(contextOptions, windowBounds);
10447
+ const context = new Context(contextOptions, windowBounds, config);
10448
+ // Initialize performance monitoring if enabled
10449
+ const performanceMonitoring = opts.enablePerformanceMonitoring ?? opts.logging ?? false;
10450
+ const perfMonitor = new PerformanceMonitor(context, performanceMonitoring);
10451
+ perfMonitor.start('total', {
10452
+ width: windowOptions.windowWidth,
10453
+ height: windowOptions.windowHeight
10454
+ });
9014
10455
  const foreignObjectRendering = opts.foreignObjectRendering ?? false;
9015
10456
  const cloneOptions = {
9016
10457
  allowTaint: opts.allowTaint ?? false,
@@ -9019,15 +10460,17 @@
9019
10460
  iframeContainer: opts.iframeContainer,
9020
10461
  inlineImages: foreignObjectRendering,
9021
10462
  copyStyles: foreignObjectRendering,
9022
- cspNonce
10463
+ cspNonce: opts.cspNonce ?? config.cspNonce
9023
10464
  };
9024
10465
  context.logger.debug(`Starting document clone with size ${windowBounds.width}x${windowBounds.height} scrolled to ${-windowBounds.left},${-windowBounds.top}`);
10466
+ perfMonitor.start('clone');
9025
10467
  const documentCloner = new DocumentCloner(context, element, cloneOptions);
9026
10468
  const clonedElement = documentCloner.clonedReferenceElement;
9027
10469
  if (!clonedElement) {
9028
- return Promise.reject(`Unable to find element in cloned iframe`);
10470
+ throw new Error('Unable to find element in cloned iframe');
9029
10471
  }
9030
10472
  const container = await documentCloner.toIFrame(ownerDocument, windowBounds);
10473
+ perfMonitor.end('clone');
9031
10474
  const { width, height, left, top } = isBodyElement(clonedElement) || isHTMLElement(clonedElement)
9032
10475
  ? parseDocumentSize(clonedElement.ownerDocument)
9033
10476
  : parseBounds(context, clonedElement);
@@ -9039,32 +10482,56 @@
9039
10482
  x: (opts.x ?? 0) + left,
9040
10483
  y: (opts.y ?? 0) + top,
9041
10484
  width: opts.width ?? Math.ceil(width),
9042
- height: opts.height ?? Math.ceil(height)
10485
+ height: opts.height ?? Math.ceil(height),
10486
+ imageSmoothing: opts.imageSmoothing,
10487
+ imageSmoothingQuality: opts.imageSmoothingQuality
9043
10488
  };
9044
10489
  let canvas;
9045
- if (foreignObjectRendering) {
9046
- context.logger.debug(`Document cloned, using foreign object rendering`);
9047
- const renderer = new ForeignObjectRenderer(context, renderOptions);
9048
- canvas = await renderer.render(clonedElement);
9049
- }
9050
- else {
9051
- context.logger.debug(`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`);
9052
- context.logger.debug(`Starting DOM parsing`);
9053
- const root = parseTree(context, clonedElement);
9054
- if (backgroundColor === root.styles.backgroundColor) {
9055
- root.styles.backgroundColor = COLORS.TRANSPARENT;
10490
+ let root;
10491
+ try {
10492
+ if (foreignObjectRendering) {
10493
+ context.logger.debug(`Document cloned, using foreign object rendering`);
10494
+ perfMonitor.start('render-foreignobject');
10495
+ const renderer = new ForeignObjectRenderer(context, renderOptions);
10496
+ canvas = await renderer.render(clonedElement);
10497
+ perfMonitor.end('render-foreignobject');
10498
+ }
10499
+ else {
10500
+ context.logger.debug(`Document cloned, element located at ${left},${top} with size ${width}x${height} using computed rendering`);
10501
+ context.logger.debug(`Starting DOM parsing`);
10502
+ perfMonitor.start('parse');
10503
+ root = parseTree(context, clonedElement);
10504
+ perfMonitor.end('parse');
10505
+ if (backgroundColor === root.styles.backgroundColor) {
10506
+ root.styles.backgroundColor = COLORS.TRANSPARENT;
10507
+ }
10508
+ context.logger.debug(`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`);
10509
+ perfMonitor.start('render');
10510
+ const renderer = new CanvasRenderer(context, renderOptions);
10511
+ canvas = await renderer.render(root);
10512
+ perfMonitor.end('render');
10513
+ }
10514
+ perfMonitor.start('cleanup');
10515
+ if (opts.removeContainer ?? true) {
10516
+ if (!DocumentCloner.destroy(container)) {
10517
+ context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
10518
+ }
10519
+ }
10520
+ perfMonitor.end('cleanup');
10521
+ perfMonitor.end('total');
10522
+ context.logger.debug(`Finished rendering`);
10523
+ // Log performance summary if monitoring is enabled
10524
+ if (performanceMonitoring) {
10525
+ perfMonitor.logSummary();
9056
10526
  }
9057
- context.logger.debug(`Starting renderer for element at ${renderOptions.x},${renderOptions.y} with size ${renderOptions.width}x${renderOptions.height}`);
9058
- const renderer = new CanvasRenderer(context, renderOptions);
9059
- canvas = await renderer.render(root);
10527
+ return canvas;
9060
10528
  }
9061
- if (opts.removeContainer ?? true) {
9062
- if (!DocumentCloner.destroy(container)) {
9063
- context.logger.error(`Cannot detach cloned iframe as it is not in the DOM anymore`);
10529
+ finally {
10530
+ // Restore DOM modifications (animations, transforms) in cloned document
10531
+ if (root) {
10532
+ root.restoreTree();
9064
10533
  }
9065
10534
  }
9066
- context.logger.debug(`Finished rendering`);
9067
- return canvas;
9068
10535
  };
9069
10536
  const parseBackgroundColor = (context, element, backgroundColorOverride) => {
9070
10537
  const ownerDocument = element.ownerDocument;
@@ -9089,6 +10556,10 @@
9089
10556
  : defaultBackgroundColor;
9090
10557
  };
9091
10558
 
10559
+ exports.Html2CanvasConfig = Html2CanvasConfig;
10560
+ exports.PerformanceMonitor = PerformanceMonitor;
10561
+ exports.Validator = Validator;
10562
+ exports.createDefaultValidator = createDefaultValidator;
9092
10563
  exports.default = html2canvas;
9093
10564
  exports.html2canvas = html2canvas;
9094
10565
  exports.setCspNonce = setCspNonce;