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