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