html2canvas-pro 2.1.1 → 2.2.1
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 +19 -19
- package/dist/html2canvas-pro.esm.js +10232 -10540
- package/dist/html2canvas-pro.esm.js.map +1 -1
- package/dist/html2canvas-pro.js +10875 -11185
- package/dist/html2canvas-pro.js.map +1 -1
- package/dist/html2canvas-pro.min.js +8 -8
- package/dist/lib/config.js +0 -22
- package/dist/lib/core/cache-storage.js +1 -38
- package/dist/lib/core/constants.js +25 -0
- package/dist/lib/core/context.js +1 -0
- package/dist/lib/core/features.js +1 -0
- package/dist/lib/core/render-element.js +2 -1
- package/dist/lib/core/validator.js +3 -3
- package/dist/lib/css/grouped/background-styles.js +36 -0
- package/dist/lib/css/grouped/border-styles.js +75 -0
- package/dist/lib/css/grouped/font-styles.js +93 -0
- package/dist/lib/css/grouped/layout-styles.js +127 -0
- package/dist/lib/css/index.js +81 -47
- package/dist/lib/css/layout/text.js +7 -6
- package/dist/lib/css/property-descriptors/background-blend-mode.js +41 -0
- package/dist/lib/css/property-descriptors/border-image-repeat.js +42 -0
- package/dist/lib/css/property-descriptors/border-image-slice.js +45 -0
- package/dist/lib/css/property-descriptors/border-image-source.js +21 -0
- package/dist/lib/css/property-descriptors/border-radius.js +1 -1
- package/dist/lib/css/property-descriptors/box-decoration-break.js +18 -0
- package/dist/lib/css/property-descriptors/counter-increment.js +17 -12
- package/dist/lib/css/property-descriptors/counter-reset.js +4 -12
- package/dist/lib/css/property-descriptors/filter.js +76 -0
- package/dist/lib/css/property-descriptors/font-variant-ligatures.js +34 -0
- package/dist/lib/css/property-descriptors/object-fit.js +1 -1
- package/dist/lib/css/property-descriptors/object-position.js +42 -0
- package/dist/lib/css/property-descriptors/visibility.js +1 -1
- package/dist/lib/css/property-descriptors/zoom.js +18 -0
- package/dist/lib/css/syntax/parser.js +0 -1
- package/dist/lib/css/types/color.js +5 -1
- package/dist/lib/css/types/functions/repeating-linear-gradient.js +9 -0
- package/dist/lib/css/types/image.js +12 -2
- package/dist/lib/css/types/length-percentage.js +6 -2
- package/dist/lib/css/types/safe-eval.js +80 -0
- package/dist/lib/dom/document-cloner.js +23 -163
- package/dist/lib/dom/slot-cloner.js +176 -0
- package/dist/lib/index.js +1 -17
- package/dist/lib/render/canvas/background-renderer.js +165 -32
- package/dist/lib/render/canvas/border-image-renderer.js +153 -0
- package/dist/lib/render/canvas/canvas-renderer.js +34 -189
- package/dist/lib/render/canvas/content-renderer.js +202 -0
- package/dist/lib/render/canvas/effects-renderer.js +10 -0
- package/dist/lib/render/canvas/text/text-decoration-renderer.js +99 -0
- package/dist/lib/render/canvas/text-renderer.js +100 -224
- package/dist/lib/render/effects.js +38 -3
- package/dist/lib/render/object-fit.js +19 -15
- package/dist/lib/render/stacking-context.js +11 -0
- package/dist/types/config.d.ts +0 -10
- package/dist/types/core/cache-storage.d.ts +0 -24
- package/dist/types/core/constants.d.ts +22 -0
- package/dist/types/core/context.d.ts +3 -0
- package/dist/types/core/performance-monitor.d.ts +4 -4
- package/dist/types/core/validator.d.ts +6 -8
- package/dist/types/css/grouped/background-styles.d.ts +16 -0
- package/dist/types/css/grouped/border-styles.d.ts +31 -0
- package/dist/types/css/grouped/font-styles.d.ts +35 -0
- package/dist/types/css/grouped/layout-styles.d.ts +46 -0
- package/dist/types/css/index.d.ts +30 -0
- package/dist/types/css/property-descriptors/background-blend-mode.d.ts +23 -0
- package/dist/types/css/property-descriptors/border-image-repeat.d.ts +12 -0
- package/dist/types/css/property-descriptors/border-image-slice.d.ts +10 -0
- package/dist/types/css/property-descriptors/border-image-source.d.ts +4 -0
- package/dist/types/css/property-descriptors/box-decoration-break.d.ts +6 -0
- package/dist/types/css/property-descriptors/counter-increment.d.ts +3 -0
- package/dist/types/css/property-descriptors/filter.d.ts +3 -0
- package/dist/types/css/property-descriptors/font-variant-ligatures.d.ts +14 -0
- package/dist/types/css/property-descriptors/object-position.d.ts +4 -0
- package/dist/types/css/property-descriptors/zoom.d.ts +3 -0
- package/dist/types/css/types/functions/repeating-linear-gradient.d.ts +4 -0
- package/dist/types/css/types/image.d.ts +4 -2
- package/dist/types/css/types/safe-eval.d.ts +8 -0
- package/dist/types/dom/document-cloner.d.ts +3 -44
- package/dist/types/dom/slot-cloner.d.ts +66 -0
- package/dist/types/index.d.ts +3 -7
- package/dist/types/options.d.ts +11 -0
- package/dist/types/render/canvas/background-renderer.d.ts +23 -0
- package/dist/types/render/canvas/border-image-renderer.d.ts +18 -0
- package/dist/types/render/canvas/canvas-renderer.d.ts +1 -0
- package/dist/types/render/canvas/content-renderer.d.ts +44 -0
- package/dist/types/render/canvas/text/text-decoration-renderer.d.ts +18 -0
- package/dist/types/render/canvas/text-renderer.d.ts +12 -1
- package/dist/types/render/effects.d.ts +12 -2
- package/dist/types/render/object-fit.d.ts +2 -1
- package/dist/types/render/renderer-interface.d.ts +11 -9
- package/package.json +5 -10
- package/dist/lib/dom/replaced-elements/pseudo-elements.js +0 -0
- package/dist/types/dom/replaced-elements/pseudo-elements.d.ts +0 -0
|
@@ -5,29 +5,17 @@ const stacking_context_1 = require("../stacking-context");
|
|
|
5
5
|
const color_utilities_1 = require("../../css/types/color-utilities");
|
|
6
6
|
const path_1 = require("../path");
|
|
7
7
|
const bound_curves_1 = require("../bound-curves");
|
|
8
|
-
const vector_1 = require("../vector");
|
|
9
8
|
const background_1 = require("../background");
|
|
10
|
-
const text_1 = require("../../css/layout/text");
|
|
11
|
-
const image_element_container_1 = require("../../dom/replaced-elements/image-element-container");
|
|
12
9
|
const box_sizing_1 = require("../box-sizing");
|
|
13
|
-
const canvas_element_container_1 = require("../../dom/replaced-elements/canvas-element-container");
|
|
14
|
-
const svg_element_container_1 = require("../../dom/replaced-elements/svg-element-container");
|
|
15
|
-
const bitwise_1 = require("../../core/bitwise");
|
|
16
|
-
const length_percentage_1 = require("../../css/types/length-percentage");
|
|
17
10
|
const font_metrics_1 = require("../font-metrics");
|
|
18
|
-
const
|
|
19
|
-
const image_rendering_1 = require("../../css/property-descriptors/image-rendering");
|
|
20
|
-
const line_height_1 = require("../../css/property-descriptors/line-height");
|
|
21
|
-
const input_element_container_1 = require("../../dom/replaced-elements/input-element-container");
|
|
22
|
-
const textarea_element_container_1 = require("../../dom/elements/textarea-element-container");
|
|
23
|
-
const select_element_container_1 = require("../../dom/elements/select-element-container");
|
|
24
|
-
const iframe_element_container_1 = require("../../dom/replaced-elements/iframe-element-container");
|
|
11
|
+
const text_renderer_1 = require("./text-renderer");
|
|
25
12
|
const background_renderer_1 = require("./background-renderer");
|
|
26
13
|
const border_renderer_1 = require("./border-renderer");
|
|
14
|
+
const border_image_renderer_1 = require("./border-image-renderer");
|
|
27
15
|
const effects_renderer_1 = require("./effects-renderer");
|
|
28
|
-
const text_renderer_1 = require("./text-renderer");
|
|
29
16
|
const canvas_path_1 = require("./canvas-path");
|
|
30
17
|
const object_fit_1 = require("../object-fit");
|
|
18
|
+
const content_renderer_1 = require("./content-renderer");
|
|
31
19
|
const MASK_OFFSET = 10000;
|
|
32
20
|
class CanvasRenderer {
|
|
33
21
|
constructor(context, options) {
|
|
@@ -71,6 +59,7 @@ class CanvasRenderer {
|
|
|
71
59
|
path: (paths) => this.path(paths),
|
|
72
60
|
formatPath: (paths) => this.formatPath(paths)
|
|
73
61
|
});
|
|
62
|
+
this.borderImageRenderer = new border_image_renderer_1.BorderImageRenderer(this.ctx);
|
|
74
63
|
this.effectsRenderer = new effects_renderer_1.EffectsRenderer({ ctx: this.ctx }, { path: (paths) => this.path(paths) });
|
|
75
64
|
this.textRenderer = new text_renderer_1.TextRenderer({
|
|
76
65
|
ctx: this.ctx,
|
|
@@ -108,7 +97,7 @@ class CanvasRenderer {
|
|
|
108
97
|
this.path(path);
|
|
109
98
|
this.ctx.save();
|
|
110
99
|
this.ctx.clip();
|
|
111
|
-
const { sx, sy, sw, sh, dx, dy, dw, dh } = (0, object_fit_1.calculateObjectFitRendering)(intrinsicWidth, intrinsicHeight, box, container.styles.objectFit);
|
|
100
|
+
const { sx, sy, sw, sh, dx, dy, dw, dh } = (0, object_fit_1.calculateObjectFitRendering)(intrinsicWidth, intrinsicHeight, box, container.styles.objectFit, container.styles.objectPosition);
|
|
112
101
|
this.ctx.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
113
102
|
this.ctx.restore();
|
|
114
103
|
}
|
|
@@ -119,160 +108,20 @@ class CanvasRenderer {
|
|
|
119
108
|
const curves = paint.curves;
|
|
120
109
|
const styles = container.styles;
|
|
121
110
|
// Use content box for text overflow calculation (excludes padding and border)
|
|
122
|
-
// This matches browser behavior where text-overflow uses the content width
|
|
123
111
|
const textBounds = (0, box_sizing_1.contentBox)(container);
|
|
124
112
|
for (const child of container.textNodes) {
|
|
125
113
|
await this.textRenderer.renderTextNode(child, styles, textBounds);
|
|
126
114
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
else if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.SMOOTH) {
|
|
139
|
-
this.context.logger.debug(`Enabling image smoothing for ${container.src} due to CSS image-rendering: smooth`);
|
|
140
|
-
this.ctx.imageSmoothingEnabled = true;
|
|
141
|
-
}
|
|
142
|
-
// IMAGE_RENDERING.AUTO: keep current global setting
|
|
143
|
-
this.renderReplacedElement(container, curves, image);
|
|
144
|
-
// Restore previous smoothing state
|
|
145
|
-
this.ctx.imageSmoothingEnabled = prevSmoothing;
|
|
146
|
-
}
|
|
147
|
-
catch (e) {
|
|
148
|
-
this.context.logger.error(`Error loading image ${container.src}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (container instanceof canvas_element_container_1.CanvasElementContainer) {
|
|
152
|
-
this.renderReplacedElement(container, curves, container.canvas);
|
|
153
|
-
}
|
|
154
|
-
if (container instanceof svg_element_container_1.SVGElementContainer) {
|
|
155
|
-
try {
|
|
156
|
-
const image = await this.context.cache.match(container.svg);
|
|
157
|
-
this.renderReplacedElement(container, curves, image);
|
|
158
|
-
}
|
|
159
|
-
catch (e) {
|
|
160
|
-
this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (container instanceof iframe_element_container_1.IFrameElementContainer && container.tree) {
|
|
164
|
-
const iframeRenderer = new CanvasRenderer(this.context, {
|
|
165
|
-
scale: this.options.scale,
|
|
166
|
-
backgroundColor: container.backgroundColor,
|
|
167
|
-
x: 0,
|
|
168
|
-
y: 0,
|
|
169
|
-
width: container.width,
|
|
170
|
-
height: container.height
|
|
171
|
-
});
|
|
172
|
-
const canvas = await iframeRenderer.render(container.tree);
|
|
173
|
-
if (container.width && container.height) {
|
|
174
|
-
this.ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (container instanceof input_element_container_1.InputElementContainer) {
|
|
178
|
-
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
179
|
-
if (container.type === input_element_container_1.CHECKBOX) {
|
|
180
|
-
if (container.checked) {
|
|
181
|
-
this.ctx.save();
|
|
182
|
-
this.path([
|
|
183
|
-
new vector_1.Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
|
|
184
|
-
new vector_1.Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
|
|
185
|
-
new vector_1.Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
|
|
186
|
-
new vector_1.Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
|
|
187
|
-
new vector_1.Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
|
|
188
|
-
new vector_1.Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
|
|
189
|
-
new vector_1.Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
|
|
190
|
-
]);
|
|
191
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(input_element_container_1.INPUT_COLOR);
|
|
192
|
-
this.ctx.fill();
|
|
193
|
-
this.ctx.restore();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else if (container.type === input_element_container_1.RADIO) {
|
|
197
|
-
if (container.checked) {
|
|
198
|
-
this.ctx.save();
|
|
199
|
-
this.ctx.beginPath();
|
|
200
|
-
this.ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
|
|
201
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(input_element_container_1.INPUT_COLOR);
|
|
202
|
-
this.ctx.fill();
|
|
203
|
-
this.ctx.restore();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (isTextInputElement(container) && container.value.length) {
|
|
208
|
-
const [font, fontFamily, fontSize] = this.textRenderer.createFontStyle(styles);
|
|
209
|
-
const { baseline } = this.fontMetrics.getMetrics(fontFamily, fontSize);
|
|
210
|
-
this.ctx.font = font;
|
|
211
|
-
// Fix for Issue #92: Use placeholder color when rendering placeholder text
|
|
212
|
-
const isPlaceholder = container instanceof input_element_container_1.InputElementContainer && container.isPlaceholder;
|
|
213
|
-
this.ctx.fillStyle = isPlaceholder ? (0, color_utilities_1.asString)(input_element_container_1.PLACEHOLDER_COLOR) : (0, color_utilities_1.asString)(styles.color);
|
|
214
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
215
|
-
this.ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
216
|
-
const bounds = (0, box_sizing_1.contentBox)(container);
|
|
217
|
-
let x = 0;
|
|
218
|
-
switch (container.styles.textAlign) {
|
|
219
|
-
case 1 /* TEXT_ALIGN.CENTER */:
|
|
220
|
-
x += bounds.width / 2;
|
|
221
|
-
break;
|
|
222
|
-
case 2 /* TEXT_ALIGN.RIGHT */:
|
|
223
|
-
x += bounds.width;
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
// Fix for Issue #92: Position text vertically centered in single-line input
|
|
227
|
-
// Only apply vertical centering for InputElementContainer, not for textarea or select
|
|
228
|
-
let verticalOffset = 0;
|
|
229
|
-
if (container instanceof input_element_container_1.InputElementContainer) {
|
|
230
|
-
const fontSizeValue = (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0);
|
|
231
|
-
verticalOffset = (bounds.height - fontSizeValue) / 2;
|
|
232
|
-
}
|
|
233
|
-
// Create text bounds with horizontal and vertical offsets
|
|
234
|
-
// Height is not modified as it doesn't affect text rendering position
|
|
235
|
-
const textBounds = bounds.add(x, verticalOffset, 0, 0);
|
|
236
|
-
this.ctx.save();
|
|
237
|
-
this.path([
|
|
238
|
-
new vector_1.Vector(bounds.left, bounds.top),
|
|
239
|
-
new vector_1.Vector(bounds.left + bounds.width, bounds.top),
|
|
240
|
-
new vector_1.Vector(bounds.left + bounds.width, bounds.top + bounds.height),
|
|
241
|
-
new vector_1.Vector(bounds.left, bounds.top + bounds.height)
|
|
242
|
-
]);
|
|
243
|
-
this.ctx.clip();
|
|
244
|
-
this.textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(container.value, textBounds), styles.letterSpacing, baseline, styles.writingMode);
|
|
245
|
-
this.ctx.restore();
|
|
246
|
-
this.ctx.textBaseline = 'alphabetic';
|
|
247
|
-
this.ctx.textAlign = 'left';
|
|
248
|
-
}
|
|
249
|
-
if ((0, bitwise_1.contains)(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
|
|
250
|
-
if (container.styles.listStyleImage !== null) {
|
|
251
|
-
const img = container.styles.listStyleImage;
|
|
252
|
-
if (img.type === 0 /* CSSImageType.URL */) {
|
|
253
|
-
let image;
|
|
254
|
-
const url = img.url;
|
|
255
|
-
try {
|
|
256
|
-
image = await this.context.cache.match(url);
|
|
257
|
-
this.ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
|
|
258
|
-
}
|
|
259
|
-
catch (e) {
|
|
260
|
-
this.context.logger.error(`Error loading list-style-image ${url}`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
|
|
265
|
-
const [font] = this.textRenderer.createFontStyle(styles);
|
|
266
|
-
this.ctx.font = font;
|
|
267
|
-
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
268
|
-
this.ctx.textBaseline = 'middle';
|
|
269
|
-
this.ctx.textAlign = 'right';
|
|
270
|
-
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);
|
|
271
|
-
this.textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(paint.listValue, bounds), styles.letterSpacing, (0, line_height_1.computeLineHeight)(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
|
|
272
|
-
this.ctx.textBaseline = 'bottom';
|
|
273
|
-
this.ctx.textAlign = 'left';
|
|
274
|
-
}
|
|
275
|
-
}
|
|
115
|
+
await (0, content_renderer_1.renderReplacedElements)(this.ctx, this.context, {
|
|
116
|
+
scale: this.options.scale,
|
|
117
|
+
backgroundColor: this.options.backgroundColor,
|
|
118
|
+
x: this.options.x,
|
|
119
|
+
y: this.options.y,
|
|
120
|
+
width: this.options.width,
|
|
121
|
+
height: this.options.height
|
|
122
|
+
}, (ctx, opts) => new CanvasRenderer(ctx, opts), container, curves, styles, (c, cv, img) => this.renderReplacedElement(c, cv, img));
|
|
123
|
+
(0, content_renderer_1.renderFormElements)(this.ctx, this.fontMetrics, this.textRenderer, this.path.bind(this), container, styles);
|
|
124
|
+
await (0, content_renderer_1.renderListMarker)(this.ctx, this.context, this.textRenderer, paint, container, styles);
|
|
276
125
|
}
|
|
277
126
|
async renderStackContent(stack) {
|
|
278
127
|
if (stack.element.container.debugRender) {
|
|
@@ -403,6 +252,25 @@ class CanvasRenderer {
|
|
|
403
252
|
this.ctx.restore();
|
|
404
253
|
});
|
|
405
254
|
}
|
|
255
|
+
// Render border-image if present (replaces traditional borders per CSS spec)
|
|
256
|
+
if (styles.borderImageSource) {
|
|
257
|
+
const source = styles.borderImageSource;
|
|
258
|
+
if (source.type === 0 /* CSSImageType.URL */) {
|
|
259
|
+
const url = source.url;
|
|
260
|
+
try {
|
|
261
|
+
const image = await this.context.cache.match(url);
|
|
262
|
+
if (image) {
|
|
263
|
+
const bounds = paint.container.bounds;
|
|
264
|
+
this.borderImageRenderer.renderBorderImage(bounds, image, styles.borderImageSlice, styles.borderImageRepeat, Math.max(0, styles.borderTopWidth), Math.max(0, styles.borderRightWidth), Math.max(0, styles.borderBottomWidth), Math.max(0, styles.borderLeftWidth));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
this.context.logger.error(`Error loading border-image ${url}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// When border-image is present, skip regular border rendering
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
406
274
|
let side = 0;
|
|
407
275
|
for (const border of borders) {
|
|
408
276
|
if (border.style !== 0 /* BORDER_STYLE.NONE */ && !(0, color_utilities_1.isTransparent)(border.color) && border.width > 0) {
|
|
@@ -434,18 +302,6 @@ class CanvasRenderer {
|
|
|
434
302
|
}
|
|
435
303
|
}
|
|
436
304
|
exports.CanvasRenderer = CanvasRenderer;
|
|
437
|
-
const isTextInputElement = (container) => {
|
|
438
|
-
if (container instanceof textarea_element_container_1.TextareaElementContainer) {
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
else if (container instanceof select_element_container_1.SelectElementContainer) {
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
else if (container instanceof input_element_container_1.InputElementContainer && container.type !== input_element_container_1.RADIO && container.type !== input_element_container_1.CHECKBOX) {
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
return false;
|
|
448
|
-
};
|
|
449
305
|
const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
450
306
|
switch (clip) {
|
|
451
307
|
case 0 /* BACKGROUND_CLIP.BORDER_BOX */:
|
|
@@ -457,14 +313,3 @@ const calculateBackgroundCurvedPaintingArea = (clip, curves) => {
|
|
|
457
313
|
return (0, bound_curves_1.calculatePaddingBoxPath)(curves);
|
|
458
314
|
}
|
|
459
315
|
};
|
|
460
|
-
const canvasTextAlign = (textAlign) => {
|
|
461
|
-
switch (textAlign) {
|
|
462
|
-
case 1 /* TEXT_ALIGN.CENTER */:
|
|
463
|
-
return 'center';
|
|
464
|
-
case 2 /* TEXT_ALIGN.RIGHT */:
|
|
465
|
-
return 'right';
|
|
466
|
-
case 0 /* TEXT_ALIGN.LEFT */:
|
|
467
|
-
default:
|
|
468
|
-
return 'left';
|
|
469
|
-
}
|
|
470
|
-
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderReplacedElements = renderReplacedElements;
|
|
4
|
+
exports.renderFormElements = renderFormElements;
|
|
5
|
+
exports.renderListMarker = renderListMarker;
|
|
6
|
+
const color_utilities_1 = require("../../css/types/color-utilities");
|
|
7
|
+
const image_element_container_1 = require("../../dom/replaced-elements/image-element-container");
|
|
8
|
+
const canvas_element_container_1 = require("../../dom/replaced-elements/canvas-element-container");
|
|
9
|
+
const svg_element_container_1 = require("../../dom/replaced-elements/svg-element-container");
|
|
10
|
+
const iframe_element_container_1 = require("../../dom/replaced-elements/iframe-element-container");
|
|
11
|
+
const input_element_container_1 = require("../../dom/replaced-elements/input-element-container");
|
|
12
|
+
const textarea_element_container_1 = require("../../dom/elements/textarea-element-container");
|
|
13
|
+
const select_element_container_1 = require("../../dom/elements/select-element-container");
|
|
14
|
+
const bounds_1 = require("../../css/layout/bounds");
|
|
15
|
+
const text_1 = require("../../css/layout/text");
|
|
16
|
+
const vector_1 = require("../vector");
|
|
17
|
+
const box_sizing_1 = require("../box-sizing");
|
|
18
|
+
const image_rendering_1 = require("../../css/property-descriptors/image-rendering");
|
|
19
|
+
const length_percentage_1 = require("../../css/types/length-percentage");
|
|
20
|
+
const line_height_1 = require("../../css/property-descriptors/line-height");
|
|
21
|
+
const bitwise_1 = require("../../core/bitwise");
|
|
22
|
+
/**
|
|
23
|
+
* Render replaced elements: Image, Canvas, SVG, IFrame.
|
|
24
|
+
*/
|
|
25
|
+
async function renderReplacedElements(ctx, context, options, iframeRendererFactory, container, curves, styles, renderReplacedElementFn) {
|
|
26
|
+
if (container instanceof image_element_container_1.ImageElementContainer) {
|
|
27
|
+
try {
|
|
28
|
+
const image = await context.cache.match(container.src);
|
|
29
|
+
const prevSmoothing = ctx.imageSmoothingEnabled;
|
|
30
|
+
if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED ||
|
|
31
|
+
styles.imageRendering === image_rendering_1.IMAGE_RENDERING.CRISP_EDGES) {
|
|
32
|
+
ctx.imageSmoothingEnabled = false;
|
|
33
|
+
}
|
|
34
|
+
else if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.SMOOTH) {
|
|
35
|
+
ctx.imageSmoothingEnabled = true;
|
|
36
|
+
}
|
|
37
|
+
renderReplacedElementFn(container, curves, image);
|
|
38
|
+
ctx.imageSmoothingEnabled = prevSmoothing;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
context.logger.error(`Error loading image ${container.src}`);
|
|
42
|
+
context.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (container instanceof canvas_element_container_1.CanvasElementContainer) {
|
|
46
|
+
renderReplacedElementFn(container, curves, container.canvas);
|
|
47
|
+
}
|
|
48
|
+
if (container instanceof svg_element_container_1.SVGElementContainer) {
|
|
49
|
+
try {
|
|
50
|
+
const image = await context.cache.match(container.svg);
|
|
51
|
+
renderReplacedElementFn(container, curves, image);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
|
|
55
|
+
context.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (container instanceof iframe_element_container_1.IFrameElementContainer && container.tree) {
|
|
59
|
+
const iframeRenderer = iframeRendererFactory(context, {
|
|
60
|
+
scale: options.scale,
|
|
61
|
+
backgroundColor: container.backgroundColor,
|
|
62
|
+
x: 0,
|
|
63
|
+
y: 0,
|
|
64
|
+
width: container.width,
|
|
65
|
+
height: container.height
|
|
66
|
+
});
|
|
67
|
+
const canvas = await iframeRenderer.render(container.tree);
|
|
68
|
+
if (container.width && container.height) {
|
|
69
|
+
ctx.drawImage(canvas, 0, 0, container.width, container.height, container.bounds.left, container.bounds.top, container.bounds.width, container.bounds.height);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Render form element content: checkbox, radio, text input.
|
|
75
|
+
*/
|
|
76
|
+
function renderFormElements(ctx, fontMetrics, textRenderer, pathFn, container, styles) {
|
|
77
|
+
if (container instanceof input_element_container_1.InputElementContainer) {
|
|
78
|
+
const size = Math.min(container.bounds.width, container.bounds.height);
|
|
79
|
+
if (container.type === input_element_container_1.CHECKBOX && container.checked) {
|
|
80
|
+
ctx.save();
|
|
81
|
+
pathFn([
|
|
82
|
+
new vector_1.Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79),
|
|
83
|
+
new vector_1.Vector(container.bounds.left + size * 0.16, container.bounds.top + size * 0.5549),
|
|
84
|
+
new vector_1.Vector(container.bounds.left + size * 0.27347, container.bounds.top + size * 0.44071),
|
|
85
|
+
new vector_1.Vector(container.bounds.left + size * 0.39694, container.bounds.top + size * 0.5649),
|
|
86
|
+
new vector_1.Vector(container.bounds.left + size * 0.72983, container.bounds.top + size * 0.23),
|
|
87
|
+
new vector_1.Vector(container.bounds.left + size * 0.84, container.bounds.top + size * 0.34085),
|
|
88
|
+
new vector_1.Vector(container.bounds.left + size * 0.39363, container.bounds.top + size * 0.79)
|
|
89
|
+
]);
|
|
90
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(input_element_container_1.INPUT_COLOR);
|
|
91
|
+
ctx.fill();
|
|
92
|
+
ctx.restore();
|
|
93
|
+
}
|
|
94
|
+
else if (container.type === input_element_container_1.RADIO && container.checked) {
|
|
95
|
+
ctx.save();
|
|
96
|
+
ctx.beginPath();
|
|
97
|
+
ctx.arc(container.bounds.left + size / 2, container.bounds.top + size / 2, size / 4, 0, Math.PI * 2, true);
|
|
98
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(input_element_container_1.INPUT_COLOR);
|
|
99
|
+
ctx.fill();
|
|
100
|
+
ctx.restore();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (isTextInputElement(container) && container.value.length) {
|
|
104
|
+
const [font, fontFamily, fontSize] = textRenderer.createFontStyle(styles);
|
|
105
|
+
const { baseline } = fontMetrics.getMetrics(fontFamily, fontSize);
|
|
106
|
+
ctx.font = font;
|
|
107
|
+
const isPlaceholder = container instanceof input_element_container_1.InputElementContainer && container.isPlaceholder;
|
|
108
|
+
ctx.fillStyle = isPlaceholder ? (0, color_utilities_1.asString)(input_element_container_1.PLACEHOLDER_COLOR) : (0, color_utilities_1.asString)(styles.color);
|
|
109
|
+
ctx.textBaseline = 'alphabetic';
|
|
110
|
+
ctx.textAlign = canvasTextAlign(container.styles.textAlign);
|
|
111
|
+
const bounds = (0, box_sizing_1.contentBox)(container);
|
|
112
|
+
let x = 0;
|
|
113
|
+
switch (container.styles.textAlign) {
|
|
114
|
+
case 1 /* TEXT_ALIGN.CENTER */:
|
|
115
|
+
x += bounds.width / 2;
|
|
116
|
+
break;
|
|
117
|
+
case 2 /* TEXT_ALIGN.RIGHT */:
|
|
118
|
+
x += bounds.width;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
let verticalOffset = 0;
|
|
122
|
+
if (container instanceof input_element_container_1.InputElementContainer) {
|
|
123
|
+
const fontSizeValue = (0, length_percentage_1.getAbsoluteValue)(styles.fontSize, 0);
|
|
124
|
+
verticalOffset = (bounds.height - fontSizeValue) / 2;
|
|
125
|
+
}
|
|
126
|
+
const textBounds = bounds.add(x, verticalOffset, 0, 0);
|
|
127
|
+
ctx.save();
|
|
128
|
+
pathFn([
|
|
129
|
+
new vector_1.Vector(bounds.left, bounds.top),
|
|
130
|
+
new vector_1.Vector(bounds.left + bounds.width, bounds.top),
|
|
131
|
+
new vector_1.Vector(bounds.left + bounds.width, bounds.top + bounds.height),
|
|
132
|
+
new vector_1.Vector(bounds.left, bounds.top + bounds.height)
|
|
133
|
+
]);
|
|
134
|
+
ctx.clip();
|
|
135
|
+
textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(container.value, textBounds), styles.letterSpacing, baseline, styles.writingMode);
|
|
136
|
+
ctx.restore();
|
|
137
|
+
ctx.textBaseline = 'alphabetic';
|
|
138
|
+
ctx.textAlign = 'left';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Render list-item marker (image or text).
|
|
143
|
+
*/
|
|
144
|
+
async function renderListMarker(ctx, context, textRenderer, paint, container, styles) {
|
|
145
|
+
if (!(0, bitwise_1.contains)(container.styles.display, 2048 /* DISPLAY.LIST_ITEM */)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (container.styles.listStyleImage !== null) {
|
|
149
|
+
const img = container.styles.listStyleImage;
|
|
150
|
+
if (img.type === 0 /* CSSImageType.URL */) {
|
|
151
|
+
const url = img.url;
|
|
152
|
+
try {
|
|
153
|
+
const image = await context.cache.match(url);
|
|
154
|
+
ctx.drawImage(image, container.bounds.left - (image.width + 10), container.bounds.top);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
context.logger.error(`Error loading list-style-image ${url}`);
|
|
158
|
+
context.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (paint.listValue && container.styles.listStyleType !== -1 /* LIST_STYLE_TYPE.NONE */) {
|
|
163
|
+
const [font] = textRenderer.createFontStyle(styles);
|
|
164
|
+
ctx.font = font;
|
|
165
|
+
ctx.fillStyle = (0, color_utilities_1.asString)(styles.color);
|
|
166
|
+
ctx.textBaseline = 'middle';
|
|
167
|
+
ctx.textAlign = 'right';
|
|
168
|
+
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);
|
|
169
|
+
textRenderer.renderTextWithLetterSpacing(new text_1.TextBounds(paint.listValue, bounds), styles.letterSpacing, (0, line_height_1.computeLineHeight)(styles.lineHeight, styles.fontSize.number) / 2 + 2, styles.writingMode);
|
|
170
|
+
ctx.textBaseline = 'bottom';
|
|
171
|
+
ctx.textAlign = 'left';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Type guard for text input containers.
|
|
176
|
+
*/
|
|
177
|
+
const isTextInputElement = (container) => {
|
|
178
|
+
if (container instanceof textarea_element_container_1.TextareaElementContainer) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
else if (container instanceof select_element_container_1.SelectElementContainer) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
else if (container instanceof input_element_container_1.InputElementContainer && container.type !== input_element_container_1.RADIO && container.type !== input_element_container_1.CHECKBOX) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Map CSS text-align to Canvas textAlign.
|
|
191
|
+
*/
|
|
192
|
+
const canvasTextAlign = (textAlign) => {
|
|
193
|
+
switch (textAlign) {
|
|
194
|
+
case 1 /* TEXT_ALIGN.CENTER */:
|
|
195
|
+
return 'center';
|
|
196
|
+
case 2 /* TEXT_ALIGN.RIGHT */:
|
|
197
|
+
return 'right';
|
|
198
|
+
case 0 /* TEXT_ALIGN.LEFT */:
|
|
199
|
+
default:
|
|
200
|
+
return 'left';
|
|
201
|
+
}
|
|
202
|
+
};
|
|
@@ -66,6 +66,16 @@ class EffectsRenderer {
|
|
|
66
66
|
else if ((0, effects_1.isBlendEffect)(effect)) {
|
|
67
67
|
this.ctx.globalCompositeOperation = effect.compositeOperation;
|
|
68
68
|
}
|
|
69
|
+
else if ((0, effects_1.isFilterEffect)(effect)) {
|
|
70
|
+
// Canvas 2D `ctx.filter` accepts CSS filter strings including
|
|
71
|
+
// drop-shadow(). However, using drop-shadow() on the canvas context
|
|
72
|
+
// can taint the canvas in some browsers (Chrome, Firefox) even for
|
|
73
|
+
// same-origin content. Our filter parser wraps shadows with
|
|
74
|
+
// drop-shadow(...) — strip that single function so we never set
|
|
75
|
+
// a filter that could taint the canvas.
|
|
76
|
+
const safe = effect.filterString.replace(/drop-shadow\([^)]+\)\s*/g, '').trim();
|
|
77
|
+
this.ctx.filter = safe || 'none';
|
|
78
|
+
}
|
|
69
79
|
this.activeEffects.push(effect);
|
|
70
80
|
}
|
|
71
81
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TextDecorationRenderer = void 0;
|
|
4
|
+
const color_utilities_1 = require("../../../css/types/color-utilities");
|
|
5
|
+
class TextDecorationRenderer {
|
|
6
|
+
constructor(ctx) {
|
|
7
|
+
this.ctx = ctx;
|
|
8
|
+
}
|
|
9
|
+
render(bounds, styles) {
|
|
10
|
+
this.ctx.fillStyle = (0, color_utilities_1.asString)(styles.textDecorationColor || styles.color);
|
|
11
|
+
let thickness = 1;
|
|
12
|
+
if (typeof styles.textDecorationThickness === 'number') {
|
|
13
|
+
thickness = styles.textDecorationThickness;
|
|
14
|
+
}
|
|
15
|
+
else if (styles.textDecorationThickness === 'from-font') {
|
|
16
|
+
thickness = Math.max(1, Math.floor(styles.fontSize.number * 0.05));
|
|
17
|
+
}
|
|
18
|
+
let underlineOffset = 0;
|
|
19
|
+
if (typeof styles.textUnderlineOffset === 'number') {
|
|
20
|
+
underlineOffset = styles.textUnderlineOffset;
|
|
21
|
+
}
|
|
22
|
+
const decorationStyle = styles.textDecorationStyle;
|
|
23
|
+
styles.textDecorationLine.forEach((line) => {
|
|
24
|
+
let y = 0;
|
|
25
|
+
switch (line) {
|
|
26
|
+
case 1 /* TEXT_DECORATION_LINE.UNDERLINE */:
|
|
27
|
+
y = bounds.top + bounds.height - thickness + underlineOffset;
|
|
28
|
+
break;
|
|
29
|
+
case 2 /* TEXT_DECORATION_LINE.OVERLINE */:
|
|
30
|
+
y = bounds.top;
|
|
31
|
+
break;
|
|
32
|
+
case 3 /* TEXT_DECORATION_LINE.LINE_THROUGH */:
|
|
33
|
+
y = bounds.top + (bounds.height / 2 - thickness / 2);
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.draw(bounds.left, y, bounds.width, thickness, decorationStyle);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
draw(x, y, width, thickness, style) {
|
|
42
|
+
switch (style) {
|
|
43
|
+
case 0 /* TEXT_DECORATION_STYLE.SOLID */:
|
|
44
|
+
this.ctx.fillRect(x, y, width, thickness);
|
|
45
|
+
break;
|
|
46
|
+
case 1 /* TEXT_DECORATION_STYLE.DOUBLE */: {
|
|
47
|
+
const gap = Math.max(1, thickness);
|
|
48
|
+
this.ctx.fillRect(x, y, width, thickness);
|
|
49
|
+
this.ctx.fillRect(x, y + thickness + gap, width, thickness);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 2 /* TEXT_DECORATION_STYLE.DOTTED */:
|
|
53
|
+
this.drawPattern(x, y, width, thickness, [thickness, thickness * 2]);
|
|
54
|
+
break;
|
|
55
|
+
case 3 /* TEXT_DECORATION_STYLE.DASHED */:
|
|
56
|
+
this.drawPattern(x, y, width, thickness, [thickness * 3, thickness * 2]);
|
|
57
|
+
break;
|
|
58
|
+
case 4 /* TEXT_DECORATION_STYLE.WAVY */:
|
|
59
|
+
this.drawWavy(x, y, width, thickness);
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
this.ctx.fillRect(x, y, width, thickness);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
drawPattern(x, y, width, thickness, dash) {
|
|
66
|
+
this.ctx.save();
|
|
67
|
+
this.ctx.beginPath();
|
|
68
|
+
this.ctx.setLineDash(dash);
|
|
69
|
+
this.ctx.lineWidth = thickness;
|
|
70
|
+
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
71
|
+
this.ctx.moveTo(x, y + thickness / 2);
|
|
72
|
+
this.ctx.lineTo(x + width, y + thickness / 2);
|
|
73
|
+
this.ctx.stroke();
|
|
74
|
+
this.ctx.restore();
|
|
75
|
+
}
|
|
76
|
+
drawWavy(x, y, width, thickness) {
|
|
77
|
+
this.ctx.save();
|
|
78
|
+
this.ctx.beginPath();
|
|
79
|
+
this.ctx.lineWidth = thickness;
|
|
80
|
+
this.ctx.strokeStyle = this.ctx.fillStyle;
|
|
81
|
+
const amplitude = thickness * 2;
|
|
82
|
+
const wavelength = thickness * 4;
|
|
83
|
+
let currentX = x;
|
|
84
|
+
this.ctx.moveTo(currentX, y + thickness / 2);
|
|
85
|
+
while (currentX < x + width) {
|
|
86
|
+
const nextX = Math.min(currentX + wavelength / 2, x + width);
|
|
87
|
+
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 - amplitude, nextX, y + thickness / 2);
|
|
88
|
+
currentX = nextX;
|
|
89
|
+
if (currentX < x + width) {
|
|
90
|
+
const nextX2 = Math.min(currentX + wavelength / 2, x + width);
|
|
91
|
+
this.ctx.quadraticCurveTo(currentX + wavelength / 4, y + thickness / 2 + amplitude, nextX2, y + thickness / 2);
|
|
92
|
+
currentX = nextX2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.ctx.stroke();
|
|
96
|
+
this.ctx.restore();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.TextDecorationRenderer = TextDecorationRenderer;
|