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