html2canvas-pro 1.6.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/demo/image-smoothing-demo.html +256 -0
- package/demo/refactoring-test.html +602 -0
- package/dist/html2canvas-pro.esm.js +2846 -1238
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +2849 -1237
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +5 -4
- package/dist/lib/__tests__/index.js +8 -2
- package/dist/lib/__tests__/index.js.map +1 -1
- package/dist/lib/config.js +72 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/core/__tests__/cache-storage.js +6 -3
- package/dist/lib/core/__tests__/cache-storage.js.map +1 -1
- package/dist/lib/core/__tests__/cache-storage.test.js +158 -0
- package/dist/lib/core/__tests__/cache-storage.test.js.map +1 -0
- package/dist/lib/core/__tests__/validator.js +296 -0
- package/dist/lib/core/__tests__/validator.js.map +1 -0
- package/dist/lib/core/cache-storage.js +130 -11
- package/dist/lib/core/cache-storage.js.map +1 -1
- package/dist/lib/core/context.js +5 -2
- package/dist/lib/core/context.js.map +1 -1
- package/dist/lib/core/debugger.js +3 -0
- package/dist/lib/core/debugger.js.map +1 -1
- package/dist/lib/core/origin-checker.js +54 -0
- package/dist/lib/core/origin-checker.js.map +1 -0
- package/dist/lib/core/performance-monitor.js +208 -0
- package/dist/lib/core/performance-monitor.js.map +1 -0
- package/dist/lib/core/validator.js +501 -0
- package/dist/lib/core/validator.js.map +1 -0
- package/dist/lib/css/index.js +2 -0
- package/dist/lib/css/index.js.map +1 -1
- package/dist/lib/css/property-descriptors/__tests__/background-tests.js +7 -1
- package/dist/lib/css/property-descriptors/__tests__/background-tests.js.map +1 -1
- package/dist/lib/css/property-descriptors/__tests__/image-rendering-integration.test.js +142 -0
- package/dist/lib/css/property-descriptors/__tests__/image-rendering-integration.test.js.map +1 -0
- package/dist/lib/css/property-descriptors/__tests__/image-rendering-performance.test.js +167 -0
- package/dist/lib/css/property-descriptors/__tests__/image-rendering-performance.test.js.map +1 -0
- package/dist/lib/css/property-descriptors/__tests__/image-rendering.test.js +61 -0
- package/dist/lib/css/property-descriptors/__tests__/image-rendering.test.js.map +1 -0
- package/dist/lib/css/property-descriptors/image-rendering.js +34 -0
- package/dist/lib/css/property-descriptors/image-rendering.js.map +1 -0
- package/dist/lib/css/types/__tests__/image-tests.js +7 -1
- package/dist/lib/css/types/__tests__/image-tests.js.map +1 -1
- package/dist/lib/css/types/color-math.js +26 -0
- package/dist/lib/css/types/color-math.js.map +1 -0
- package/dist/lib/css/types/color-spaces/srgb.js +6 -6
- package/dist/lib/css/types/color-spaces/srgb.js.map +1 -1
- package/dist/lib/css/types/color-utilities.js +13 -22
- package/dist/lib/css/types/color-utilities.js.map +1 -1
- package/dist/lib/dom/__tests__/dom-normalizer.test.js +113 -0
- package/dist/lib/dom/__tests__/dom-normalizer.test.js.map +1 -0
- package/dist/lib/dom/__tests__/element-container.test.js +109 -0
- package/dist/lib/dom/__tests__/element-container.test.js.map +1 -0
- package/dist/lib/dom/document-cloner.js +152 -11
- package/dist/lib/dom/document-cloner.js.map +1 -1
- package/dist/lib/dom/dom-normalizer.js +80 -0
- package/dist/lib/dom/dom-normalizer.js.map +1 -0
- package/dist/lib/dom/element-container.js +32 -15
- package/dist/lib/dom/element-container.js.map +1 -1
- package/dist/lib/dom/node-parser.js +16 -20
- package/dist/lib/dom/node-parser.js.map +1 -1
- package/dist/lib/dom/node-type-guards.js +44 -0
- package/dist/lib/dom/node-type-guards.js.map +1 -0
- package/dist/lib/dom/replaced-elements/iframe-element-container.js +5 -4
- package/dist/lib/dom/replaced-elements/iframe-element-container.js.map +1 -1
- package/dist/lib/index.js +148 -41
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/render/canvas/__tests__/background-renderer.test.js +65 -0
- package/dist/lib/render/canvas/__tests__/background-renderer.test.js.map +1 -0
- package/dist/lib/render/canvas/__tests__/border-renderer.test.js +23 -0
- package/dist/lib/render/canvas/__tests__/border-renderer.test.js.map +1 -0
- package/dist/lib/render/canvas/__tests__/effects-renderer.test.js +30 -0
- package/dist/lib/render/canvas/__tests__/effects-renderer.test.js.map +1 -0
- package/dist/lib/render/canvas/__tests__/text-renderer.test.js +63 -0
- package/dist/lib/render/canvas/__tests__/text-renderer.test.js.map +1 -0
- package/dist/lib/render/canvas/background-renderer.js +222 -0
- package/dist/lib/render/canvas/background-renderer.js.map +1 -0
- package/dist/lib/render/canvas/border-renderer.js +185 -0
- package/dist/lib/render/canvas/border-renderer.js.map +1 -0
- package/dist/lib/render/canvas/canvas-renderer.js +61 -689
- package/dist/lib/render/canvas/canvas-renderer.js.map +1 -1
- package/dist/lib/render/canvas/effects-renderer.js +89 -0
- package/dist/lib/render/canvas/effects-renderer.js.map +1 -0
- package/dist/lib/render/canvas/text-renderer.js +508 -0
- package/dist/lib/render/canvas/text-renderer.js.map +1 -0
- package/dist/lib/render/renderer-interface.js +3 -0
- package/dist/lib/render/renderer-interface.js.map +1 -0
- package/dist/types/config.d.ts +54 -0
- package/dist/types/core/__tests__/cache-storage.test.d.ts +1 -0
- package/dist/types/core/__tests__/validator.d.ts +1 -0
- package/dist/types/core/cache-storage.d.ts +42 -1
- package/dist/types/core/context.d.ts +5 -1
- package/dist/types/core/origin-checker.d.ts +33 -0
- package/dist/types/core/performance-monitor.d.ts +131 -0
- package/dist/types/core/validator.d.ts +132 -0
- package/dist/types/css/index.d.ts +2 -0
- package/dist/types/css/property-descriptors/__tests__/image-rendering-integration.test.d.ts +1 -0
- package/dist/types/css/property-descriptors/__tests__/image-rendering-performance.test.d.ts +1 -0
- package/dist/types/css/property-descriptors/__tests__/image-rendering.test.d.ts +1 -0
- package/dist/types/css/property-descriptors/image-rendering.d.ts +8 -0
- package/dist/types/css/types/color-math.d.ts +12 -0
- package/dist/types/css/types/color-utilities.d.ts +2 -3
- package/dist/types/dom/__tests__/dom-normalizer.test.d.ts +1 -0
- package/dist/types/dom/__tests__/element-container.test.d.ts +1 -0
- package/dist/types/dom/document-cloner.d.ts +46 -0
- package/dist/types/dom/dom-normalizer.d.ts +43 -0
- package/dist/types/dom/element-container.d.ts +20 -1
- package/dist/types/dom/node-parser.d.ts +2 -7
- package/dist/types/dom/node-type-guards.d.ts +33 -0
- package/dist/types/dom/replaced-elements/iframe-element-container.d.ts +4 -1
- package/dist/types/index.d.ts +48 -3
- package/dist/types/render/canvas/__tests__/background-renderer.test.d.ts +1 -0
- package/dist/types/render/canvas/__tests__/border-renderer.test.d.ts +1 -0
- package/dist/types/render/canvas/__tests__/effects-renderer.test.d.ts +1 -0
- package/dist/types/render/canvas/__tests__/text-renderer.test.d.ts +1 -0
- package/dist/types/render/canvas/background-renderer.d.ts +87 -0
- package/dist/types/render/canvas/border-renderer.d.ts +67 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +19 -23
- package/dist/types/render/canvas/effects-renderer.d.ts +64 -0
- package/dist/types/render/canvas/text-renderer.d.ts +57 -0
- package/dist/types/render/renderer-interface.d.ts +26 -0
- package/package.json +2 -1
|
@@ -7,32 +7,31 @@ const path_1 = require("../path");
|
|
|
7
7
|
const bound_curves_1 = require("../bound-curves");
|
|
8
8
|
const bezier_curve_1 = require("../bezier-curve");
|
|
9
9
|
const vector_1 = require("../vector");
|
|
10
|
-
const image_1 = require("../../css/types/image");
|
|
11
|
-
const border_1 = require("../border");
|
|
12
10
|
const background_1 = require("../background");
|
|
13
|
-
const parser_1 = require("../../css/syntax/parser");
|
|
14
11
|
const text_1 = require("../../css/layout/text");
|
|
15
12
|
const image_element_container_1 = require("../../dom/replaced-elements/image-element-container");
|
|
16
13
|
const box_sizing_1 = require("../box-sizing");
|
|
17
14
|
const canvas_element_container_1 = require("../../dom/replaced-elements/canvas-element-container");
|
|
18
15
|
const svg_element_container_1 = require("../../dom/replaced-elements/svg-element-container");
|
|
19
|
-
const effects_1 = require("../effects");
|
|
20
16
|
const bitwise_1 = require("../../core/bitwise");
|
|
21
|
-
const gradient_1 = require("../../css/types/functions/gradient");
|
|
22
17
|
const length_percentage_1 = require("../../css/types/length-percentage");
|
|
23
18
|
const font_metrics_1 = require("../font-metrics");
|
|
24
19
|
const bounds_1 = require("../../css/layout/bounds");
|
|
20
|
+
const image_rendering_1 = require("../../css/property-descriptors/image-rendering");
|
|
25
21
|
const line_height_1 = require("../../css/property-descriptors/line-height");
|
|
26
22
|
const input_element_container_1 = require("../../dom/replaced-elements/input-element-container");
|
|
27
23
|
const textarea_element_container_1 = require("../../dom/elements/textarea-element-container");
|
|
28
24
|
const select_element_container_1 = require("../../dom/elements/select-element-container");
|
|
29
25
|
const iframe_element_container_1 = require("../../dom/replaced-elements/iframe-element-container");
|
|
30
26
|
const renderer_1 = require("../renderer");
|
|
27
|
+
const background_renderer_1 = require("./background-renderer");
|
|
28
|
+
const border_renderer_1 = require("./border-renderer");
|
|
29
|
+
const effects_renderer_1 = require("./effects-renderer");
|
|
30
|
+
const text_renderer_1 = require("./text-renderer");
|
|
31
31
|
const MASK_OFFSET = 10000;
|
|
32
32
|
class CanvasRenderer extends renderer_1.Renderer {
|
|
33
33
|
constructor(context, options) {
|
|
34
34
|
super(context, options);
|
|
35
|
-
this._activeEffects = [];
|
|
36
35
|
this.canvas = options.canvas ? options.canvas : document.createElement('canvas');
|
|
37
36
|
this.ctx = this.canvas.getContext('2d');
|
|
38
37
|
if (!options.canvas) {
|
|
@@ -45,35 +44,36 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
45
44
|
this.ctx.scale(this.options.scale, this.options.scale);
|
|
46
45
|
this.ctx.translate(-options.x, -options.y);
|
|
47
46
|
this.ctx.textBaseline = 'bottom';
|
|
48
|
-
|
|
47
|
+
// Set image smoothing options
|
|
48
|
+
if (options.imageSmoothing !== undefined) {
|
|
49
|
+
this.ctx.imageSmoothingEnabled = options.imageSmoothing;
|
|
50
|
+
}
|
|
51
|
+
if (options.imageSmoothingQuality) {
|
|
52
|
+
this.ctx.imageSmoothingQuality = options.imageSmoothingQuality;
|
|
53
|
+
}
|
|
54
|
+
// Initialize specialized renderers
|
|
55
|
+
this.backgroundRenderer = new background_renderer_1.BackgroundRenderer({
|
|
56
|
+
ctx: this.ctx,
|
|
57
|
+
context: this.context,
|
|
58
|
+
canvas: this.canvas,
|
|
59
|
+
options: {
|
|
60
|
+
width: options.width,
|
|
61
|
+
height: options.height,
|
|
62
|
+
scale: options.scale
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
this.borderRenderer = new border_renderer_1.BorderRenderer({ ctx: this.ctx }, {
|
|
66
|
+
path: (paths) => this.path(paths),
|
|
67
|
+
formatPath: (paths) => this.formatPath(paths)
|
|
68
|
+
});
|
|
69
|
+
this.effectsRenderer = new effects_renderer_1.EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
70
|
+
this.textRenderer = new text_renderer_1.TextRenderer({
|
|
71
|
+
ctx: this.ctx,
|
|
72
|
+
context: this.context,
|
|
73
|
+
options: { scale: options.scale }
|
|
74
|
+
});
|
|
49
75
|
this.context.logger.debug(`Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`);
|
|
50
76
|
}
|
|
51
|
-
applyEffects(effects) {
|
|
52
|
-
while (this._activeEffects.length) {
|
|
53
|
-
this.popEffect();
|
|
54
|
-
}
|
|
55
|
-
effects.forEach((effect) => this.applyEffect(effect));
|
|
56
|
-
}
|
|
57
|
-
applyEffect(effect) {
|
|
58
|
-
this.ctx.save();
|
|
59
|
-
if ((0, effects_1.isOpacityEffect)(effect)) {
|
|
60
|
-
this.ctx.globalAlpha = effect.opacity;
|
|
61
|
-
}
|
|
62
|
-
if ((0, effects_1.isTransformEffect)(effect)) {
|
|
63
|
-
this.ctx.translate(effect.offsetX, effect.offsetY);
|
|
64
|
-
this.ctx.transform(effect.matrix[0], effect.matrix[1], effect.matrix[2], effect.matrix[3], effect.matrix[4], effect.matrix[5]);
|
|
65
|
-
this.ctx.translate(-effect.offsetX, -effect.offsetY);
|
|
66
|
-
}
|
|
67
|
-
if ((0, effects_1.isClipEffect)(effect)) {
|
|
68
|
-
this.path(effect.path);
|
|
69
|
-
this.ctx.clip();
|
|
70
|
-
}
|
|
71
|
-
this._activeEffects.push(effect);
|
|
72
|
-
}
|
|
73
|
-
popEffect() {
|
|
74
|
-
this._activeEffects.pop();
|
|
75
|
-
this.ctx.restore();
|
|
76
|
-
}
|
|
77
77
|
async renderStack(stack) {
|
|
78
78
|
const styles = stack.element.container.styles;
|
|
79
79
|
if (styles.isVisible()) {
|
|
@@ -89,427 +89,11 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
89
89
|
await this.renderNodeContent(paint);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
renderTextWithLetterSpacing(text, letterSpacing, baseline) {
|
|
93
|
-
if (letterSpacing === 0) {
|
|
94
|
-
// Use alphabetic baseline for consistent text positioning across browsers
|
|
95
|
-
// Issue #129: text.bounds.top + text.bounds.height causes text to render too low
|
|
96
|
-
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const letters = (0, text_1.segmentGraphemes)(text.text);
|
|
100
|
-
letters.reduce((left, letter) => {
|
|
101
|
-
this.ctx.fillText(letter, left, text.bounds.top + baseline);
|
|
102
|
-
return left + this.ctx.measureText(letter).width;
|
|
103
|
-
}, text.bounds.left);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
92
|
/**
|
|
107
93
|
* Helper method to render text with paint order support
|
|
108
94
|
* Reduces code duplication in line-clamp and normal rendering
|
|
109
95
|
*/
|
|
110
|
-
renderTextBoundWithPaintOrder(textBound, styles, paintOrderLayers) {
|
|
111
|
-
paintOrderLayers.forEach((paintOrderLayer) => {
|
|
112
|
-
switch (paintOrderLayer) {
|
|
113
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
114
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
115
|
-
this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
|
|
116
|
-
break;
|
|
117
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
118
|
-
if (styles.webkitTextStrokeWidth && textBound.text.trim().length) {
|
|
119
|
-
this.ctx.strokeStyle = (0, color_utilities_1.asString)(styles.webkitTextStrokeColor);
|
|
120
|
-
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
121
|
-
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
122
|
-
this.renderTextWithLetterSpacing(textBound, styles.letterSpacing, styles.fontSize.number);
|
|
123
|
-
}
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
renderTextDecoration(bounds, styles) {
|
|
129
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.textDecorationColor || styles.color);
|
|
130
|
-
// Calculate decoration line thickness
|
|
131
|
-
let thickness = 1; // default
|
|
132
|
-
if (typeof styles.textDecorationThickness === 'number') {
|
|
133
|
-
thickness = styles.textDecorationThickness;
|
|
134
|
-
}
|
|
135
|
-
else if (styles.textDecorationThickness === 'from-font') {
|
|
136
|
-
// Use a reasonable default based on font size
|
|
137
|
-
thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
|
|
138
|
-
}
|
|
139
|
-
// 'auto' uses default thickness of 1
|
|
140
|
-
// Calculate underline offset
|
|
141
|
-
let underlineOffset = 0;
|
|
142
|
-
if (typeof styles.textUnderlineOffset === 'number') {
|
|
143
|
-
// It's a pixel value
|
|
144
|
-
underlineOffset = styles.textUnderlineOffset;
|
|
145
|
-
}
|
|
146
|
-
// 'auto' uses default offset of 0
|
|
147
|
-
const decorationStyle = styles.textDecorationStyle;
|
|
148
|
-
styles.textDecorationLine.forEach((textDecorationLine) => {
|
|
149
|
-
let y = 0;
|
|
150
|
-
switch (textDecorationLine) {
|
|
151
|
-
case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
|
|
152
|
-
y = bounds.top + bounds.height - thickness + underlineOffset;
|
|
153
|
-
break;
|
|
154
|
-
case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
|
|
155
|
-
y = bounds.top;
|
|
156
|
-
break;
|
|
157
|
-
case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
|
|
158
|
-
y = bounds.top + (bounds.height / 2 - thickness / 2);
|
|
159
|
-
break;
|
|
160
|
-
default:
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
this.drawDecorationLine(bounds.left, y, bounds.width, thickness, decorationStyle);
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
drawDecorationLine(x, y, width, thickness, style) {
|
|
167
|
-
switch (style) {
|
|
168
|
-
case 0 /* TEXT_DECORATION_STYLE.SOLID */:
|
|
169
|
-
// Solid line (default)
|
|
170
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
171
|
-
break;
|
|
172
|
-
case 1 /* TEXT_DECORATION_STYLE.DOUBLE */:
|
|
173
|
-
// Double line
|
|
174
|
-
const gap = Math.max(1, thickness);
|
|
175
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
176
|
-
this.ctx.fillRect(x, y + thickness + gap, width, thickness);
|
|
177
|
-
break;
|
|
178
|
-
case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
|
|
179
|
-
// Dotted line
|
|
180
|
-
this.ctx.save();
|
|
181
|
-
this.ctx.beginPath();
|
|
182
|
-
this.ctx.setLineDash([thickness, thickness * 2]);
|
|
183
|
-
this.ctx.lineWidth = thickness;
|
|
184
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
185
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
186
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
187
|
-
this.ctx.stroke();
|
|
188
|
-
this.ctx.restore();
|
|
189
|
-
break;
|
|
190
|
-
case 3 /* TEXT_DECORATION_STYLE.DASHED */:
|
|
191
|
-
// Dashed line
|
|
192
|
-
this.ctx.save();
|
|
193
|
-
this.ctx.beginPath();
|
|
194
|
-
this.ctx.setLineDash([thickness * 3, thickness * 2]);
|
|
195
|
-
this.ctx.lineWidth = thickness;
|
|
196
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
197
|
-
this.ctx.moveTo(x, y + thickness / 2);
|
|
198
|
-
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
199
|
-
this.ctx.stroke();
|
|
200
|
-
this.ctx.restore();
|
|
201
|
-
break;
|
|
202
|
-
case 4 /* TEXT_DECORATION_STYLE.WAVY */:
|
|
203
|
-
// Wavy line (approximation using quadratic curves)
|
|
204
|
-
this.ctx.save();
|
|
205
|
-
this.ctx.beginPath();
|
|
206
|
-
this.ctx.lineWidth = thickness;
|
|
207
|
-
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
208
|
-
const amplitude = thickness * 2;
|
|
209
|
-
const wavelength = thickness * 4;
|
|
210
|
-
let currentX = x;
|
|
211
|
-
this.ctx.moveTo(currentX, y + thickness / 2);
|
|
212
|
-
while (currentX < x + width) {
|
|
213
|
-
const nextX = Math.min(currentX + wavelength / 2, x + width);
|
|
214
|
-
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
|
|
215
|
-
currentX = nextX;
|
|
216
|
-
if (currentX < x + width) {
|
|
217
|
-
const nextX2 = Math.min(currentX + wavelength / 2, x + width);
|
|
218
|
-
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
|
|
219
|
-
currentX = nextX2;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.ctx.stroke();
|
|
223
|
-
this.ctx.restore();
|
|
224
|
-
break;
|
|
225
|
-
default:
|
|
226
|
-
// Fallback to solid
|
|
227
|
-
this.ctx.fillRect(x, y, width, thickness);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
96
|
// Helper method to truncate text and add ellipsis if needed
|
|
231
|
-
truncateTextWithEllipsis(text, maxWidth, letterSpacing) {
|
|
232
|
-
const ellipsis = '...';
|
|
233
|
-
const ellipsisWidth = this.ctx.measureText(ellipsis).width;
|
|
234
|
-
if (letterSpacing === 0) {
|
|
235
|
-
let truncated = text;
|
|
236
|
-
while (this.ctx.measureText(truncated).width + ellipsisWidth > maxWidth && truncated.length > 0) {
|
|
237
|
-
truncated = truncated.slice(0, -1);
|
|
238
|
-
}
|
|
239
|
-
return truncated + ellipsis;
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
const letters = (0, text_1.segmentGraphemes)(text);
|
|
243
|
-
let width = ellipsisWidth;
|
|
244
|
-
let result = [];
|
|
245
|
-
for (const letter of letters) {
|
|
246
|
-
const letterWidth = this.ctx.measureText(letter).width + letterSpacing;
|
|
247
|
-
if (width + letterWidth > maxWidth) {
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
result.push(letter);
|
|
251
|
-
width += letterWidth;
|
|
252
|
-
}
|
|
253
|
-
return result.join('') + ellipsis;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
createFontStyle(styles) {
|
|
257
|
-
const fontVariant = styles.fontVariant
|
|
258
|
-
.filter((variant) => variant === 'normal' || variant === 'small-caps')
|
|
259
|
-
.join('');
|
|
260
|
-
const fontFamily = fixIOSSystemFonts(styles.fontFamily).join(', ');
|
|
261
|
-
const fontSize = (0, parser_1.isDimensionToken)(styles.fontSize)
|
|
262
|
-
? `${styles.fontSize.number}${styles.fontSize.unit}`
|
|
263
|
-
: `${styles.fontSize.number}px`;
|
|
264
|
-
return [
|
|
265
|
-
[styles.fontStyle, fontVariant, styles.fontWeight, fontSize, fontFamily].join(' '),
|
|
266
|
-
fontFamily,
|
|
267
|
-
fontSize
|
|
268
|
-
];
|
|
269
|
-
}
|
|
270
|
-
async renderTextNode(text, styles, containerBounds) {
|
|
271
|
-
const [font] = this.createFontStyle(styles);
|
|
272
|
-
this.ctx.font = font;
|
|
273
|
-
this.ctx.direction = styles.direction === 1 /* DIRECTION.RTL */ ? 'rtl' : 'ltr';
|
|
274
|
-
this.ctx.textAlign = 'left';
|
|
275
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
276
|
-
const paintOrder = styles.paintOrder;
|
|
277
|
-
// Calculate line height for text layout detection (used by both line-clamp and ellipsis)
|
|
278
|
-
const lineHeight = styles.fontSize.number * 1.5;
|
|
279
|
-
// Check if we need to apply -webkit-line-clamp
|
|
280
|
-
// This limits text to a specific number of lines with ellipsis
|
|
281
|
-
const shouldApplyLineClamp = styles.webkitLineClamp > 0 &&
|
|
282
|
-
(styles.display & 2 /* DISPLAY.BLOCK */) !== 0 &&
|
|
283
|
-
styles.overflowY === 1 /* OVERFLOW.HIDDEN */ &&
|
|
284
|
-
text.textBounds.length > 0;
|
|
285
|
-
if (shouldApplyLineClamp) {
|
|
286
|
-
// Group text bounds by lines based on their Y position
|
|
287
|
-
const lines = [];
|
|
288
|
-
let currentLine = [];
|
|
289
|
-
let currentLineTop = text.textBounds[0].bounds.top;
|
|
290
|
-
text.textBounds.forEach((tb) => {
|
|
291
|
-
// If this text bound is on a different line, start a new line
|
|
292
|
-
if (Math.abs(tb.bounds.top - currentLineTop) >= lineHeight * 0.5) {
|
|
293
|
-
if (currentLine.length > 0) {
|
|
294
|
-
lines.push(currentLine);
|
|
295
|
-
}
|
|
296
|
-
currentLine = [tb];
|
|
297
|
-
currentLineTop = tb.bounds.top;
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
currentLine.push(tb);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
// Don't forget the last line
|
|
304
|
-
if (currentLine.length > 0) {
|
|
305
|
-
lines.push(currentLine);
|
|
306
|
-
}
|
|
307
|
-
// Only render up to webkitLineClamp lines
|
|
308
|
-
const maxLines = styles.webkitLineClamp;
|
|
309
|
-
if (lines.length > maxLines) {
|
|
310
|
-
// Render only the first (maxLines - 1) complete lines
|
|
311
|
-
for (let i = 0; i < maxLines - 1; i++) {
|
|
312
|
-
lines[i].forEach((textBound) => {
|
|
313
|
-
this.renderTextBoundWithPaintOrder(textBound, styles, paintOrder);
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
// For the last line, truncate with ellipsis
|
|
317
|
-
const lastLine = lines[maxLines - 1];
|
|
318
|
-
if (lastLine && lastLine.length > 0 && containerBounds) {
|
|
319
|
-
const lastLineText = lastLine.map((tb) => tb.text).join('');
|
|
320
|
-
const firstBound = lastLine[0];
|
|
321
|
-
const availableWidth = containerBounds.width - (firstBound.bounds.left - containerBounds.left);
|
|
322
|
-
const truncatedText = this.truncateTextWithEllipsis(lastLineText, availableWidth, styles.letterSpacing);
|
|
323
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
324
|
-
switch (paintOrderLayer) {
|
|
325
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
326
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
327
|
-
if (styles.letterSpacing === 0) {
|
|
328
|
-
this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
const letters = (0, text_1.segmentGraphemes)(truncatedText);
|
|
332
|
-
letters.reduce((left, letter) => {
|
|
333
|
-
this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
334
|
-
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
335
|
-
}, firstBound.bounds.left);
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
339
|
-
if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
|
|
340
|
-
this.ctx.strokeStyle = (0, color_utilities_1.asString)(styles.webkitTextStrokeColor);
|
|
341
|
-
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
342
|
-
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
343
|
-
if (styles.letterSpacing === 0) {
|
|
344
|
-
this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
345
|
-
}
|
|
346
|
-
else {
|
|
347
|
-
const letters = (0, text_1.segmentGraphemes)(truncatedText);
|
|
348
|
-
letters.reduce((left, letter) => {
|
|
349
|
-
this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
350
|
-
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
351
|
-
}, firstBound.bounds.left);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
return; // Don't render anything else
|
|
359
|
-
}
|
|
360
|
-
// If lines.length <= maxLines, fall through to normal rendering
|
|
361
|
-
}
|
|
362
|
-
// Check if we need to apply text-overflow: ellipsis
|
|
363
|
-
// Issue #203: Only apply ellipsis for single-line text overflow
|
|
364
|
-
// Multi-line text truncation (like -webkit-line-clamp) should not be affected
|
|
365
|
-
const shouldApplyEllipsis = styles.textOverflow === 1 /* TEXT_OVERFLOW.ELLIPSIS */ &&
|
|
366
|
-
containerBounds &&
|
|
367
|
-
styles.overflowX === 1 /* OVERFLOW.HIDDEN */ &&
|
|
368
|
-
text.textBounds.length > 0;
|
|
369
|
-
// Calculate total text width if ellipsis might be needed
|
|
370
|
-
let needsEllipsis = false;
|
|
371
|
-
let truncatedText = '';
|
|
372
|
-
if (shouldApplyEllipsis) {
|
|
373
|
-
// Check if all text bounds are on approximately the same line (single-line scenario)
|
|
374
|
-
// For multi-line text (like -webkit-line-clamp), textBounds will have different Y positions
|
|
375
|
-
const firstTop = text.textBounds[0].bounds.top;
|
|
376
|
-
const isSingleLine = text.textBounds.every((tb) => Math.abs(tb.bounds.top - firstTop) < lineHeight * 0.5);
|
|
377
|
-
if (isSingleLine) {
|
|
378
|
-
// Measure the full text content
|
|
379
|
-
// Note: text.textBounds may contain whitespace characters from HTML formatting
|
|
380
|
-
// We need to collapse them like the browser does for white-space: nowrap
|
|
381
|
-
let fullText = text.textBounds.map((tb) => tb.text).join('');
|
|
382
|
-
// Collapse whitespace: replace sequences of whitespace (including newlines) with single spaces
|
|
383
|
-
// and trim leading/trailing whitespace
|
|
384
|
-
fullText = fullText.replace(/\s+/g, ' ').trim();
|
|
385
|
-
const fullTextWidth = this.ctx.measureText(fullText).width;
|
|
386
|
-
const availableWidth = containerBounds.width;
|
|
387
|
-
if (fullTextWidth > availableWidth) {
|
|
388
|
-
needsEllipsis = true;
|
|
389
|
-
truncatedText = this.truncateTextWithEllipsis(fullText, availableWidth, styles.letterSpacing);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
// If ellipsis is needed, render the truncated text once
|
|
394
|
-
if (needsEllipsis) {
|
|
395
|
-
const firstBound = text.textBounds[0];
|
|
396
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
397
|
-
switch (paintOrderLayer) {
|
|
398
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
399
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
400
|
-
if (styles.letterSpacing === 0) {
|
|
401
|
-
this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
const letters = (0, text_1.segmentGraphemes)(truncatedText);
|
|
405
|
-
letters.reduce((left, letter) => {
|
|
406
|
-
this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
407
|
-
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
408
|
-
}, firstBound.bounds.left);
|
|
409
|
-
}
|
|
410
|
-
const textShadows = styles.textShadow;
|
|
411
|
-
if (textShadows.length && truncatedText.trim().length) {
|
|
412
|
-
textShadows
|
|
413
|
-
.slice(0)
|
|
414
|
-
.reverse()
|
|
415
|
-
.forEach((textShadow) => {
|
|
416
|
-
this.ctx.shadowColor = (0, color_utilities_1.asString)(textShadow.color);
|
|
417
|
-
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
418
|
-
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
419
|
-
this.ctx.shadowBlur = textShadow.blur.number;
|
|
420
|
-
if (styles.letterSpacing === 0) {
|
|
421
|
-
this.ctx.fillText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
const letters = (0, text_1.segmentGraphemes)(truncatedText);
|
|
425
|
-
letters.reduce((left, letter) => {
|
|
426
|
-
this.ctx.fillText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
427
|
-
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
428
|
-
}, firstBound.bounds.left);
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
this.ctx.shadowColor = '';
|
|
432
|
-
this.ctx.shadowOffsetX = 0;
|
|
433
|
-
this.ctx.shadowOffsetY = 0;
|
|
434
|
-
this.ctx.shadowBlur = 0;
|
|
435
|
-
}
|
|
436
|
-
break;
|
|
437
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
438
|
-
if (styles.webkitTextStrokeWidth && truncatedText.trim().length) {
|
|
439
|
-
this.ctx.strokeStyle = (0, color_utilities_1.asString)(styles.webkitTextStrokeColor);
|
|
440
|
-
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
441
|
-
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
442
|
-
if (styles.letterSpacing === 0) {
|
|
443
|
-
this.ctx.strokeText(truncatedText, firstBound.bounds.left, firstBound.bounds.top + styles.fontSize.number);
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
const letters = (0, text_1.segmentGraphemes)(truncatedText);
|
|
447
|
-
letters.reduce((left, letter) => {
|
|
448
|
-
this.ctx.strokeText(letter, left, firstBound.bounds.top + styles.fontSize.number);
|
|
449
|
-
return left + this.ctx.measureText(letter).width + styles.letterSpacing;
|
|
450
|
-
}, firstBound.bounds.left);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
// Normal rendering (no ellipsis needed)
|
|
459
|
-
text.textBounds.forEach((text) => {
|
|
460
|
-
paintOrder.forEach((paintOrderLayer) => {
|
|
461
|
-
switch (paintOrderLayer) {
|
|
462
|
-
case 0 /* PAINT_ORDER_LAYER.FILL */:
|
|
463
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
464
|
-
this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
|
|
465
|
-
const textShadows = styles.textShadow;
|
|
466
|
-
if (textShadows.length && text.text.trim().length) {
|
|
467
|
-
textShadows
|
|
468
|
-
.slice(0)
|
|
469
|
-
.reverse()
|
|
470
|
-
.forEach((textShadow) => {
|
|
471
|
-
this.ctx.shadowColor = (0, color_utilities_1.asString)(textShadow.color);
|
|
472
|
-
this.ctx.shadowOffsetX = textShadow.offsetX.number * this.options.scale;
|
|
473
|
-
this.ctx.shadowOffsetY = textShadow.offsetY.number * this.options.scale;
|
|
474
|
-
this.ctx.shadowBlur = textShadow.blur.number;
|
|
475
|
-
this.renderTextWithLetterSpacing(text, styles.letterSpacing, styles.fontSize.number);
|
|
476
|
-
});
|
|
477
|
-
this.ctx.shadowColor = '';
|
|
478
|
-
this.ctx.shadowOffsetX = 0;
|
|
479
|
-
this.ctx.shadowOffsetY = 0;
|
|
480
|
-
this.ctx.shadowBlur = 0;
|
|
481
|
-
}
|
|
482
|
-
if (styles.textDecorationLine.length) {
|
|
483
|
-
this.renderTextDecoration(text.bounds, styles);
|
|
484
|
-
}
|
|
485
|
-
break;
|
|
486
|
-
case 1 /* PAINT_ORDER_LAYER.STROKE */:
|
|
487
|
-
if (styles.webkitTextStrokeWidth && text.text.trim().length) {
|
|
488
|
-
this.ctx.strokeStyle = (0, color_utilities_1.asString)(styles.webkitTextStrokeColor);
|
|
489
|
-
this.ctx.lineWidth = styles.webkitTextStrokeWidth;
|
|
490
|
-
this.ctx.lineJoin = !!window.chrome ? 'miter' : 'round';
|
|
491
|
-
// Issue #110: Use baseline (fontSize) for consistent positioning with fill
|
|
492
|
-
// Previously used text.bounds.height which caused stroke to render too low
|
|
493
|
-
const baseline = styles.fontSize.number;
|
|
494
|
-
if (styles.letterSpacing === 0) {
|
|
495
|
-
this.ctx.strokeText(text.text, text.bounds.left, text.bounds.top + baseline);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
const letters = (0, text_1.segmentGraphemes)(text.text);
|
|
499
|
-
letters.reduce((left, letter) => {
|
|
500
|
-
this.ctx.strokeText(letter, left, text.bounds.top + baseline);
|
|
501
|
-
return left + this.ctx.measureText(letter).width;
|
|
502
|
-
}, text.bounds.left);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
this.ctx.strokeStyle = '';
|
|
506
|
-
this.ctx.lineWidth = 0;
|
|
507
|
-
this.ctx.lineJoin = 'miter';
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
97
|
renderReplacedElement(container, curves, image) {
|
|
514
98
|
const intrinsicWidth = image.naturalWidth || container.intrinsicWidth;
|
|
515
99
|
const intrinsicHeight = image.naturalHeight || container.intrinsicHeight;
|
|
@@ -598,7 +182,7 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
598
182
|
}
|
|
599
183
|
}
|
|
600
184
|
async renderNodeContent(paint) {
|
|
601
|
-
this.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
|
|
185
|
+
this.effectsRenderer.applyEffects(paint.getEffects(4 /* EffectTarget.CONTENT */));
|
|
602
186
|
const container = paint.container;
|
|
603
187
|
const curves = paint.curves;
|
|
604
188
|
const styles = container.styles;
|
|
@@ -606,12 +190,27 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
606
190
|
// This matches browser behavior where text-overflow uses the content width
|
|
607
191
|
const textBounds = (0, box_sizing_1.contentBox)(container);
|
|
608
192
|
for (const child of container.textNodes) {
|
|
609
|
-
await this.renderTextNode(child, styles, textBounds);
|
|
193
|
+
await this.textRenderer.renderTextNode(child, styles, textBounds);
|
|
610
194
|
}
|
|
611
195
|
if (container instanceof image_element_container_1.ImageElementContainer) {
|
|
612
196
|
try {
|
|
613
197
|
const image = await this.context.cache.match(container.src);
|
|
198
|
+
// Apply image smoothing based on CSS image-rendering property and global options
|
|
199
|
+
const prevSmoothing = this.ctx.imageSmoothingEnabled;
|
|
200
|
+
// CSS image-rendering property overrides global settings
|
|
201
|
+
if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED ||
|
|
202
|
+
styles.imageRendering === image_rendering_1.IMAGE_RENDERING.CRISP_EDGES) {
|
|
203
|
+
this.context.logger.debug(`Disabling image smoothing for ${container.src} due to CSS image-rendering: ${styles.imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED ? 'pixelated' : 'crisp-edges'}`);
|
|
204
|
+
this.ctx.imageSmoothingEnabled = false;
|
|
205
|
+
}
|
|
206
|
+
else if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.SMOOTH) {
|
|
207
|
+
this.context.logger.debug(`Enabling image smoothing for ${container.src} due to CSS image-rendering: smooth`);
|
|
208
|
+
this.ctx.imageSmoothingEnabled = true;
|
|
209
|
+
}
|
|
210
|
+
// IMAGE_RENDERING.AUTO: keep current global setting
|
|
614
211
|
this.renderReplacedElement(container, curves, image);
|
|
212
|
+
// Restore previous smoothing state
|
|
213
|
+
this.ctx.imageSmoothingEnabled = prevSmoothing;
|
|
615
214
|
}
|
|
616
215
|
catch (e) {
|
|
617
216
|
this.context.logger.error(`Error loading image ${container.src}`);
|
|
@@ -674,7 +273,7 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
674
273
|
}
|
|
675
274
|
}
|
|
676
275
|
if (isTextInputElement(container) && container.value.length) {
|
|
677
|
-
const [font, fontFamily, fontSize] = this.createFontStyle(styles);
|
|
276
|
+
const [font, fontFamily, fontSize] = this.textRenderer.createFontStyle(styles);
|
|
678
277
|
const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
|
679
278
|
this.ctx.font = font;
|
|
680
279
|
// Fix for Issue #92: Use placeholder color when rendering placeholder text
|
|
@@ -710,7 +309,7 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
710
309
|
new vector_1.Vector(bounds.left, bounds.top + bounds.height)
|
|
711
310
|
]);
|
|
712
311
|
this.ctx.clip();
|
|
713
|
-
this.renderTextWithLetterSpacing(new text_1.TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
|
|
312
|
+
this.textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(container.value, textBounds), styles.letterSpacing, baseline);
|
|
714
313
|
this.ctx.restore();
|
|
715
314
|
this.ctx.textBaseline = 'alphabetic';
|
|
716
315
|
this.ctx.textAlign = 'left';
|
|
@@ -731,13 +330,13 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
731
330
|
}
|
|
732
331
|
}
|
|
733
332
|
else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
|
|
734
|
-
const [font] = this.createFontStyle(styles);
|
|
333
|
+
const [font] = this.textRenderer.createFontStyle(styles);
|
|
735
334
|
this.ctx.font = font;
|
|
736
335
|
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
737
336
|
this.ctx.textBaseline = 'middle';
|
|
738
337
|
this.ctx.textAlign = 'right';
|
|
739
338
|
const bounds = new bounds_1.Bounds(container.bounds.left, container.bounds.top + (0, length_percentage_1.getAbsoluteValue)(container.styles.paddingTop, container.bounds.width), container.bounds.width, (0, line_height_1.computeLineHeight)(styles.lineHeight, styles.fontSize.number) / 2 + 1);
|
|
740
|
-
this.renderTextWithLetterSpacing(new text_1.TextBounds(paint.listValue, bounds), styles.letterSpacing, (0, line_height_1.computeLineHeight)(styles.lineHeight, styles.fontSize.number) / 2 + 2);
|
|
339
|
+
this.textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(paint.listValue, bounds), styles.letterSpacing, (0, line_height_1.computeLineHeight)(styles.lineHeight, styles.fontSize.number) / 2 + 2);
|
|
741
340
|
this.ctx.textBaseline = 'bottom';
|
|
742
341
|
this.ctx.textAlign = 'left';
|
|
743
342
|
}
|
|
@@ -824,128 +423,8 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
824
423
|
}
|
|
825
424
|
});
|
|
826
425
|
}
|
|
827
|
-
renderRepeat(path, pattern, offsetX, offsetY) {
|
|
828
|
-
this.path(path);
|
|
829
|
-
this.ctx.fillStyle = pattern;
|
|
830
|
-
this.ctx.translate(offsetX, offsetY);
|
|
831
|
-
this.ctx.fill();
|
|
832
|
-
this.ctx.translate(-offsetX, -offsetY);
|
|
833
|
-
}
|
|
834
|
-
resizeImage(image, width, height) {
|
|
835
|
-
// https://github.com/niklasvh/html2canvas/pull/2911
|
|
836
|
-
// if (image.width === width && image.height === height) {
|
|
837
|
-
// return image;
|
|
838
|
-
// }
|
|
839
|
-
const ownerDocument = this.canvas.ownerDocument ?? document;
|
|
840
|
-
const canvas = ownerDocument.createElement('canvas');
|
|
841
|
-
canvas.width = Math.max(1, width);
|
|
842
|
-
canvas.height = Math.max(1, height);
|
|
843
|
-
const ctx = canvas.getContext('2d');
|
|
844
|
-
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
|
|
845
|
-
return canvas;
|
|
846
|
-
}
|
|
847
|
-
async renderBackgroundImage(container) {
|
|
848
|
-
let index = container.styles.backgroundImage.length - 1;
|
|
849
|
-
for (const backgroundImage of container.styles.backgroundImage.slice(0).reverse()) {
|
|
850
|
-
if (backgroundImage.type === 0 /* CSSImageType.URL */) {
|
|
851
|
-
let image;
|
|
852
|
-
const url = backgroundImage.url;
|
|
853
|
-
try {
|
|
854
|
-
image = await this.context.cache.match(url);
|
|
855
|
-
}
|
|
856
|
-
catch (e) {
|
|
857
|
-
this.context.logger.error(`Error loading background-image ${url}`);
|
|
858
|
-
}
|
|
859
|
-
if (image) {
|
|
860
|
-
const imageWidth = isNaN(image.width) || image.width === 0 ? 1 : image.width;
|
|
861
|
-
const imageHeight = isNaN(image.height) || image.height === 0 ? 1 : image.height;
|
|
862
|
-
const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [
|
|
863
|
-
imageWidth,
|
|
864
|
-
imageHeight,
|
|
865
|
-
imageWidth / imageHeight
|
|
866
|
-
]);
|
|
867
|
-
const pattern = this.ctx.createPattern(this.resizeImage(image, width, height), 'repeat');
|
|
868
|
-
this.renderRepeat(path, pattern, x, y);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
else if ((0, image_1.isLinearGradient)(backgroundImage)) {
|
|
872
|
-
const [path, x, y, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [null, null, null]);
|
|
873
|
-
const [lineLength, x0, x1, y0, y1] = (0, gradient_1.calculateGradientDirection)(backgroundImage.angle, width, height);
|
|
874
|
-
const canvas = document.createElement('canvas');
|
|
875
|
-
canvas.width = width;
|
|
876
|
-
canvas.height = height;
|
|
877
|
-
const ctx = canvas.getContext('2d');
|
|
878
|
-
const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
|
|
879
|
-
(0, gradient_1.processColorStops)(backgroundImage.stops, lineLength || 1).forEach((colorStop) => gradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color)));
|
|
880
|
-
ctx.fillStyle = gradient;
|
|
881
|
-
ctx.fillRect(0, 0, width, height);
|
|
882
|
-
if (width > 0 && height > 0) {
|
|
883
|
-
const pattern = this.ctx.createPattern(canvas, 'repeat');
|
|
884
|
-
this.renderRepeat(path, pattern, x, y);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
else if ((0, image_1.isRadialGradient)(backgroundImage)) {
|
|
888
|
-
const [path, left, top, width, height] = (0, background_1.calculateBackgroundRendering)(container, index, [
|
|
889
|
-
null,
|
|
890
|
-
null,
|
|
891
|
-
null
|
|
892
|
-
]);
|
|
893
|
-
const position = backgroundImage.position.length === 0 ? [length_percentage_1.FIFTY_PERCENT] : backgroundImage.position;
|
|
894
|
-
const x = (0, length_percentage_1.getAbsoluteValue)(position[0], width);
|
|
895
|
-
const y = (0, length_percentage_1.getAbsoluteValue)(position[position.length - 1], height);
|
|
896
|
-
let [rx, ry] = (0, gradient_1.calculateRadius)(backgroundImage, x, y, width, height);
|
|
897
|
-
// Handle edge case where radial gradient size is 0
|
|
898
|
-
// Use a minimum value of 0.01 to ensure gradient is still rendered
|
|
899
|
-
if (rx === 0 || ry === 0) {
|
|
900
|
-
rx = Math.max(rx, 0.01);
|
|
901
|
-
ry = Math.max(ry, 0.01);
|
|
902
|
-
}
|
|
903
|
-
if (rx > 0 && ry > 0) {
|
|
904
|
-
const radialGradient = this.ctx.createRadialGradient(left + x, top + y, 0, left + x, top + y, rx);
|
|
905
|
-
(0, gradient_1.processColorStops)(backgroundImage.stops, rx * 2).forEach((colorStop) => radialGradient.addColorStop(colorStop.stop, (0, color_utilities_1.asString)(colorStop.color)));
|
|
906
|
-
this.path(path);
|
|
907
|
-
this.ctx.fillStyle = radialGradient;
|
|
908
|
-
if (rx !== ry) {
|
|
909
|
-
// transforms for elliptical radial gradient
|
|
910
|
-
const midX = container.bounds.left + 0.5 * container.bounds.width;
|
|
911
|
-
const midY = container.bounds.top + 0.5 * container.bounds.height;
|
|
912
|
-
const f = ry / rx;
|
|
913
|
-
const invF = 1 / f;
|
|
914
|
-
this.ctx.save();
|
|
915
|
-
this.ctx.translate(midX, midY);
|
|
916
|
-
this.ctx.transform(1, 0, 0, f, 0, 0);
|
|
917
|
-
this.ctx.translate(-midX, -midY);
|
|
918
|
-
this.ctx.fillRect(left, invF * (top - midY) + midY, width, height * invF);
|
|
919
|
-
this.ctx.restore();
|
|
920
|
-
}
|
|
921
|
-
else {
|
|
922
|
-
this.ctx.fill();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
index--;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
async renderSolidBorder(color, side, curvePoints) {
|
|
930
|
-
this.path((0, border_1.parsePathForBorder)(curvePoints, side));
|
|
931
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(color);
|
|
932
|
-
this.ctx.fill();
|
|
933
|
-
}
|
|
934
|
-
async renderDoubleBorder(color, width, side, curvePoints) {
|
|
935
|
-
if (width < 3) {
|
|
936
|
-
await this.renderSolidBorder(color, side, curvePoints);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
const outerPaths = (0, border_1.parsePathForBorderDoubleOuter)(curvePoints, side);
|
|
940
|
-
this.path(outerPaths);
|
|
941
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(color);
|
|
942
|
-
this.ctx.fill();
|
|
943
|
-
const innerPaths = (0, border_1.parsePathForBorderDoubleInner)(curvePoints, side);
|
|
944
|
-
this.path(innerPaths);
|
|
945
|
-
this.ctx.fill();
|
|
946
|
-
}
|
|
947
426
|
async renderNodeBackgroundAndBorders(paint) {
|
|
948
|
-
this.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
|
|
427
|
+
this.effectsRenderer.applyEffects(paint.getEffects(2 /* EffectTarget.BACKGROUND_BORDERS */));
|
|
949
428
|
const styles = paint.container.styles;
|
|
950
429
|
const hasBackground = !(0, color_utilities_1.isTransparent)(styles.backgroundColor) || styles.backgroundImage.length;
|
|
951
430
|
const borders = [
|
|
@@ -963,7 +442,7 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
963
442
|
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.backgroundColor);
|
|
964
443
|
this.ctx.fill();
|
|
965
444
|
}
|
|
966
|
-
await this.renderBackgroundImage(paint.container);
|
|
445
|
+
await this.backgroundRenderer.renderBackgroundImage(paint.container);
|
|
967
446
|
this.ctx.restore();
|
|
968
447
|
styles.boxShadow
|
|
969
448
|
.slice(0)
|
|
@@ -996,121 +475,21 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
996
475
|
for (const border of borders) {
|
|
997
476
|
if (border.style !== 0 /* BORDER_STYLE.NONE */ && !(0, color_utilities_1.isTransparent)(border.color) && border.width > 0) {
|
|
998
477
|
if (border.style === 2 /* BORDER_STYLE.DASHED */) {
|
|
999
|
-
await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
|
|
478
|
+
await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 2 /* BORDER_STYLE.DASHED */);
|
|
1000
479
|
}
|
|
1001
480
|
else if (border.style === 3 /* BORDER_STYLE.DOTTED */) {
|
|
1002
|
-
await this.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
|
|
481
|
+
await this.borderRenderer.renderDashedDottedBorder(border.color, border.width, side, paint.curves, 3 /* BORDER_STYLE.DOTTED */);
|
|
1003
482
|
}
|
|
1004
483
|
else if (border.style === 4 /* BORDER_STYLE.DOUBLE */) {
|
|
1005
|
-
await this.renderDoubleBorder(border.color, border.width, side, paint.curves);
|
|
484
|
+
await this.borderRenderer.renderDoubleBorder(border.color, border.width, side, paint.curves);
|
|
1006
485
|
}
|
|
1007
486
|
else {
|
|
1008
|
-
await this.renderSolidBorder(border.color, side, paint.curves);
|
|
487
|
+
await this.borderRenderer.renderSolidBorder(border.color, side, paint.curves);
|
|
1009
488
|
}
|
|
1010
489
|
}
|
|
1011
490
|
side++;
|
|
1012
491
|
}
|
|
1013
492
|
}
|
|
1014
|
-
async renderDashedDottedBorder(color, width, side, curvePoints, style) {
|
|
1015
|
-
this.ctx.save();
|
|
1016
|
-
const strokePaths = (0, border_1.parsePathForBorderStroke)(curvePoints, side);
|
|
1017
|
-
const boxPaths = (0, border_1.parsePathForBorder)(curvePoints, side);
|
|
1018
|
-
if (style === 2 /* BORDER_STYLE.DASHED */) {
|
|
1019
|
-
this.path(boxPaths);
|
|
1020
|
-
this.ctx.clip();
|
|
1021
|
-
}
|
|
1022
|
-
let startX, startY, endX, endY;
|
|
1023
|
-
if ((0, bezier_curve_1.isBezierCurve)(boxPaths[0])) {
|
|
1024
|
-
startX = boxPaths[0].start.x;
|
|
1025
|
-
startY = boxPaths[0].start.y;
|
|
1026
|
-
}
|
|
1027
|
-
else {
|
|
1028
|
-
startX = boxPaths[0].x;
|
|
1029
|
-
startY = boxPaths[0].y;
|
|
1030
|
-
}
|
|
1031
|
-
if ((0, bezier_curve_1.isBezierCurve)(boxPaths[1])) {
|
|
1032
|
-
endX = boxPaths[1].end.x;
|
|
1033
|
-
endY = boxPaths[1].end.y;
|
|
1034
|
-
}
|
|
1035
|
-
else {
|
|
1036
|
-
endX = boxPaths[1].x;
|
|
1037
|
-
endY = boxPaths[1].y;
|
|
1038
|
-
}
|
|
1039
|
-
let length;
|
|
1040
|
-
if (side === 0 || side === 2) {
|
|
1041
|
-
length = Math.abs(startX - endX);
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
length = Math.abs(startY - endY);
|
|
1045
|
-
}
|
|
1046
|
-
this.ctx.beginPath();
|
|
1047
|
-
if (style === 3 /* BORDER_STYLE.DOTTED */) {
|
|
1048
|
-
this.formatPath(strokePaths);
|
|
1049
|
-
}
|
|
1050
|
-
else {
|
|
1051
|
-
this.formatPath(boxPaths.slice(0, 2));
|
|
1052
|
-
}
|
|
1053
|
-
let dashLength = width < 3 ? width * 3 : width * 2;
|
|
1054
|
-
let spaceLength = width < 3 ? width * 2 : width;
|
|
1055
|
-
if (style === 3 /* BORDER_STYLE.DOTTED */) {
|
|
1056
|
-
dashLength = width;
|
|
1057
|
-
spaceLength = width;
|
|
1058
|
-
}
|
|
1059
|
-
let useLineDash = true;
|
|
1060
|
-
if (length <= dashLength * 2) {
|
|
1061
|
-
useLineDash = false;
|
|
1062
|
-
}
|
|
1063
|
-
else if (length <= dashLength * 2 + spaceLength) {
|
|
1064
|
-
const multiplier = length / (2 * dashLength + spaceLength);
|
|
1065
|
-
dashLength *= multiplier;
|
|
1066
|
-
spaceLength *= multiplier;
|
|
1067
|
-
}
|
|
1068
|
-
else {
|
|
1069
|
-
const numberOfDashes = Math.floor((length + spaceLength) / (dashLength + spaceLength));
|
|
1070
|
-
const minSpace = (length - numberOfDashes * dashLength) / (numberOfDashes - 1);
|
|
1071
|
-
const maxSpace = (length - (numberOfDashes + 1) * dashLength) / numberOfDashes;
|
|
1072
|
-
spaceLength =
|
|
1073
|
-
maxSpace <= 0 || Math.abs(spaceLength - minSpace) < Math.abs(spaceLength - maxSpace)
|
|
1074
|
-
? minSpace
|
|
1075
|
-
: maxSpace;
|
|
1076
|
-
}
|
|
1077
|
-
if (useLineDash) {
|
|
1078
|
-
if (style === 3 /* BORDER_STYLE.DOTTED */) {
|
|
1079
|
-
this.ctx.setLineDash([0, dashLength + spaceLength]);
|
|
1080
|
-
}
|
|
1081
|
-
else {
|
|
1082
|
-
this.ctx.setLineDash([dashLength, spaceLength]);
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (style === 3 /* BORDER_STYLE.DOTTED */) {
|
|
1086
|
-
this.ctx.lineCap = 'round';
|
|
1087
|
-
this.ctx.lineWidth = width;
|
|
1088
|
-
}
|
|
1089
|
-
else {
|
|
1090
|
-
this.ctx.lineWidth = width * 2 + 1.1;
|
|
1091
|
-
}
|
|
1092
|
-
this.ctx.strokeStyle = (0, color_utilities_1.asString)(color);
|
|
1093
|
-
this.ctx.stroke();
|
|
1094
|
-
this.ctx.setLineDash([]);
|
|
1095
|
-
// dashed round edge gap
|
|
1096
|
-
if (style === 2 /* BORDER_STYLE.DASHED */) {
|
|
1097
|
-
if ((0, bezier_curve_1.isBezierCurve)(boxPaths[0])) {
|
|
1098
|
-
const path1 = boxPaths[3];
|
|
1099
|
-
const path2 = boxPaths[0];
|
|
1100
|
-
this.ctx.beginPath();
|
|
1101
|
-
this.formatPath([new vector_1.Vector(path1.end.x, path1.end.y), new vector_1.Vector(path2.start.x, path2.start.y)]);
|
|
1102
|
-
this.ctx.stroke();
|
|
1103
|
-
}
|
|
1104
|
-
if ((0, bezier_curve_1.isBezierCurve)(boxPaths[1])) {
|
|
1105
|
-
const path1 = boxPaths[1];
|
|
1106
|
-
const path2 = boxPaths[2];
|
|
1107
|
-
this.ctx.beginPath();
|
|
1108
|
-
this.formatPath([new vector_1.Vector(path1.end.x, path1.end.y), new vector_1.Vector(path2.start.x, path2.start.y)]);
|
|
1109
|
-
this.ctx.stroke();
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
this.ctx.restore();
|
|
1113
|
-
}
|
|
1114
493
|
async render(element) {
|
|
1115
494
|
if (this.options.backgroundColor) {
|
|
1116
495
|
this.ctx.fillStyle = (0, color_utilities_1.asString)(this.options.backgroundColor);
|
|
@@ -1118,7 +497,7 @@ class CanvasRenderer extends renderer_1.Renderer {
|
|
|
1118
497
|
}
|
|
1119
498
|
const stack = (0, stacking_context_1.parseStackingContexts)(element);
|
|
1120
499
|
await this.renderStack(stack);
|
|
1121
|
-
this.applyEffects([]);
|
|
500
|
+
this.effectsRenderer.applyEffects([]);
|
|
1122
501
|
return this.canvas;
|
|
1123
502
|
}
|
|
1124
503
|
}
|
|
@@ -1157,11 +536,4 @@ const canvasTextAlign = (textAlign) => {
|
|
|
1157
536
|
return 'left';
|
|
1158
537
|
}
|
|
1159
538
|
};
|
|
1160
|
-
// see https://github.com/niklasvh/html2canvas/pull/2645
|
|
1161
|
-
const iOSBrokenFonts = ['-apple-system', 'system-ui'];
|
|
1162
|
-
const fixIOSSystemFonts = (fontFamilies) => {
|
|
1163
|
-
return /iPhone OS 15_(0|1)/.test(window.navigator.userAgent)
|
|
1164
|
-
? fontFamilies.filter((fontFamily) => iOSBrokenFonts.indexOf(fontFamily) === -1)
|
|
1165
|
-
: fontFamilies;
|
|
1166
|
-
};
|
|
1167
539
|
//# sourceMappingURL=canvas-renderer.js.map
|