html2canvas-pro 2.1.1 → 2.2.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.
Files changed (90) hide show
  1. package/dist/html2canvas-pro.esm.js +10226 -10540
  2. package/dist/html2canvas-pro.esm.js.map +1 -1
  3. package/dist/html2canvas-pro.js +10869 -11185
  4. package/dist/html2canvas-pro.js.map +1 -1
  5. package/dist/html2canvas-pro.min.js +8 -8
  6. package/dist/lib/config.js +0 -22
  7. package/dist/lib/core/cache-storage.js +1 -38
  8. package/dist/lib/core/constants.js +25 -0
  9. package/dist/lib/core/context.js +1 -0
  10. package/dist/lib/core/features.js +1 -0
  11. package/dist/lib/core/validator.js +3 -3
  12. package/dist/lib/css/grouped/background-styles.js +36 -0
  13. package/dist/lib/css/grouped/border-styles.js +75 -0
  14. package/dist/lib/css/grouped/font-styles.js +93 -0
  15. package/dist/lib/css/grouped/layout-styles.js +127 -0
  16. package/dist/lib/css/index.js +74 -46
  17. package/dist/lib/css/layout/text.js +7 -6
  18. package/dist/lib/css/property-descriptors/background-blend-mode.js +41 -0
  19. package/dist/lib/css/property-descriptors/border-image-repeat.js +42 -0
  20. package/dist/lib/css/property-descriptors/border-image-slice.js +45 -0
  21. package/dist/lib/css/property-descriptors/border-image-source.js +21 -0
  22. package/dist/lib/css/property-descriptors/border-radius.js +1 -1
  23. package/dist/lib/css/property-descriptors/box-decoration-break.js +18 -0
  24. package/dist/lib/css/property-descriptors/counter-increment.js +17 -12
  25. package/dist/lib/css/property-descriptors/counter-reset.js +4 -12
  26. package/dist/lib/css/property-descriptors/filter.js +76 -0
  27. package/dist/lib/css/property-descriptors/font-variant-ligatures.js +34 -0
  28. package/dist/lib/css/property-descriptors/object-fit.js +1 -1
  29. package/dist/lib/css/property-descriptors/object-position.js +42 -0
  30. package/dist/lib/css/property-descriptors/visibility.js +1 -1
  31. package/dist/lib/css/property-descriptors/zoom.js +18 -0
  32. package/dist/lib/css/syntax/parser.js +0 -1
  33. package/dist/lib/css/types/color.js +5 -1
  34. package/dist/lib/css/types/functions/repeating-linear-gradient.js +9 -0
  35. package/dist/lib/css/types/image.js +12 -2
  36. package/dist/lib/css/types/length-percentage.js +6 -2
  37. package/dist/lib/css/types/safe-eval.js +80 -0
  38. package/dist/lib/dom/document-cloner.js +23 -163
  39. package/dist/lib/dom/slot-cloner.js +176 -0
  40. package/dist/lib/index.js +1 -17
  41. package/dist/lib/render/canvas/background-renderer.js +165 -32
  42. package/dist/lib/render/canvas/border-image-renderer.js +153 -0
  43. package/dist/lib/render/canvas/canvas-renderer.js +34 -189
  44. package/dist/lib/render/canvas/content-renderer.js +202 -0
  45. package/dist/lib/render/canvas/effects-renderer.js +3 -0
  46. package/dist/lib/render/canvas/text/text-decoration-renderer.js +99 -0
  47. package/dist/lib/render/canvas/text-renderer.js +100 -224
  48. package/dist/lib/render/effects.js +38 -3
  49. package/dist/lib/render/object-fit.js +19 -15
  50. package/dist/lib/render/stacking-context.js +11 -0
  51. package/dist/types/config.d.ts +0 -10
  52. package/dist/types/core/cache-storage.d.ts +0 -24
  53. package/dist/types/core/constants.d.ts +22 -0
  54. package/dist/types/core/context.d.ts +3 -0
  55. package/dist/types/core/performance-monitor.d.ts +4 -4
  56. package/dist/types/core/validator.d.ts +6 -8
  57. package/dist/types/css/grouped/background-styles.d.ts +16 -0
  58. package/dist/types/css/grouped/border-styles.d.ts +31 -0
  59. package/dist/types/css/grouped/font-styles.d.ts +35 -0
  60. package/dist/types/css/grouped/layout-styles.d.ts +46 -0
  61. package/dist/types/css/index.d.ts +30 -0
  62. package/dist/types/css/property-descriptors/background-blend-mode.d.ts +23 -0
  63. package/dist/types/css/property-descriptors/border-image-repeat.d.ts +12 -0
  64. package/dist/types/css/property-descriptors/border-image-slice.d.ts +10 -0
  65. package/dist/types/css/property-descriptors/border-image-source.d.ts +4 -0
  66. package/dist/types/css/property-descriptors/box-decoration-break.d.ts +6 -0
  67. package/dist/types/css/property-descriptors/counter-increment.d.ts +3 -0
  68. package/dist/types/css/property-descriptors/filter.d.ts +3 -0
  69. package/dist/types/css/property-descriptors/font-variant-ligatures.d.ts +14 -0
  70. package/dist/types/css/property-descriptors/object-position.d.ts +4 -0
  71. package/dist/types/css/property-descriptors/zoom.d.ts +3 -0
  72. package/dist/types/css/types/functions/repeating-linear-gradient.d.ts +4 -0
  73. package/dist/types/css/types/image.d.ts +4 -2
  74. package/dist/types/css/types/safe-eval.d.ts +8 -0
  75. package/dist/types/dom/document-cloner.d.ts +3 -44
  76. package/dist/types/dom/slot-cloner.d.ts +66 -0
  77. package/dist/types/index.d.ts +3 -7
  78. package/dist/types/options.d.ts +11 -0
  79. package/dist/types/render/canvas/background-renderer.d.ts +23 -0
  80. package/dist/types/render/canvas/border-image-renderer.d.ts +18 -0
  81. package/dist/types/render/canvas/canvas-renderer.d.ts +1 -0
  82. package/dist/types/render/canvas/content-renderer.d.ts +44 -0
  83. package/dist/types/render/canvas/text/text-decoration-renderer.d.ts +18 -0
  84. package/dist/types/render/canvas/text-renderer.d.ts +12 -1
  85. package/dist/types/render/effects.d.ts +12 -2
  86. package/dist/types/render/object-fit.d.ts +2 -1
  87. package/dist/types/render/renderer-interface.d.ts +11 -9
  88. package/package.json +5 -10
  89. package/dist/lib/dom/replaced-elements/pseudo-elements.js +0 -0
  90. 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 bounds_1 = require("../../css/layout/bounds");
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
- if (container instanceof image_element_container_1.ImageElementContainer) {
128
- try {
129
- const image = await this.context.cache.match(container.src);
130
- // Apply image smoothing based on CSS image-rendering property and global options
131
- const prevSmoothing = this.ctx.imageSmoothingEnabled;
132
- // CSS image-rendering property overrides global settings
133
- if (styles.imageRendering === image_rendering_1.IMAGE_RENDERING.PIXELATED ||
134
- styles.imageRendering === image_rendering_1.IMAGE_RENDERING.CRISP_EDGES) {
135
- 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'}`);
136
- this.ctx.imageSmoothingEnabled = false;
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,9 @@ 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
+ this.ctx.filter = effect.filterString;
71
+ }
69
72
  this.activeEffects.push(effect);
70
73
  }
71
74
  /**
@@ -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;