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