Serenity Operating System
1/*
2 * Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
4 * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/OwnPtr.h>
10#include <LibGfx/Painter.h>
11#include <LibGfx/Quad.h>
12#include <LibGfx/Rect.h>
13#include <LibUnicode/Segmentation.h>
14#include <LibWeb/Bindings/Intrinsics.h>
15#include <LibWeb/HTML/CanvasRenderingContext2D.h>
16#include <LibWeb/HTML/HTMLCanvasElement.h>
17#include <LibWeb/HTML/HTMLImageElement.h>
18#include <LibWeb/HTML/ImageData.h>
19#include <LibWeb/HTML/Path2D.h>
20#include <LibWeb/HTML/TextMetrics.h>
21#include <LibWeb/Infra/CharacterTypes.h>
22#include <LibWeb/Layout/TextNode.h>
23#include <LibWeb/Platform/FontPlugin.h>
24#include <LibWeb/WebIDL/ExceptionOr.h>
25
26namespace Web::HTML {
27
28WebIDL::ExceptionOr<JS::NonnullGCPtr<CanvasRenderingContext2D>> CanvasRenderingContext2D::create(JS::Realm& realm, HTMLCanvasElement& element)
29{
30 return MUST_OR_THROW_OOM(realm.heap().allocate<CanvasRenderingContext2D>(realm, realm, element));
31}
32
33CanvasRenderingContext2D::CanvasRenderingContext2D(JS::Realm& realm, HTMLCanvasElement& element)
34 : PlatformObject(realm)
35 , CanvasPath(static_cast<Bindings::PlatformObject&>(*this))
36 , m_element(element)
37{
38}
39
40CanvasRenderingContext2D::~CanvasRenderingContext2D() = default;
41
42JS::ThrowCompletionOr<void> CanvasRenderingContext2D::initialize(JS::Realm& realm)
43{
44 MUST_OR_THROW_OOM(Base::initialize(realm));
45 set_prototype(&Bindings::ensure_web_prototype<Bindings::CanvasRenderingContext2DPrototype>(realm, "CanvasRenderingContext2D"));
46
47 return {};
48}
49
50void CanvasRenderingContext2D::visit_edges(Cell::Visitor& visitor)
51{
52 Base::visit_edges(visitor);
53 visitor.visit(m_element.ptr());
54}
55
56HTMLCanvasElement& CanvasRenderingContext2D::canvas_element()
57{
58 return *m_element;
59}
60
61HTMLCanvasElement const& CanvasRenderingContext2D::canvas_element() const
62{
63 return *m_element;
64}
65
66JS::NonnullGCPtr<HTMLCanvasElement> CanvasRenderingContext2D::canvas_for_binding() const
67{
68 return *m_element;
69}
70
71void CanvasRenderingContext2D::fill_rect(float x, float y, float width, float height)
72{
73 auto painter = this->antialiased_painter();
74 if (!painter.has_value())
75 return;
76
77 auto& drawing_state = this->drawing_state();
78
79 auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height));
80 auto color_fill = drawing_state.fill_style.as_color();
81 if (color_fill.has_value()) {
82 painter->fill_rect(rect, *color_fill);
83 } else {
84 // FIXME: This should use AntiAliasingPainter::fill_rect() too but that does not support FillPath yet.
85 painter->underlying_painter().fill_rect(rect.to_rounded<int>(), *drawing_state.fill_style.to_gfx_paint_style());
86 }
87 did_draw(rect);
88}
89
90void CanvasRenderingContext2D::clear_rect(float x, float y, float width, float height)
91{
92 auto painter = this->painter();
93 if (!painter)
94 return;
95
96 auto rect = drawing_state().transform.map(Gfx::FloatRect(x, y, width, height));
97 painter->clear_rect(enclosing_int_rect(rect), Color());
98 did_draw(rect);
99}
100
101void CanvasRenderingContext2D::stroke_rect(float x, float y, float width, float height)
102{
103 auto painter = this->antialiased_painter();
104 if (!painter.has_value())
105 return;
106
107 auto& drawing_state = this->drawing_state();
108
109 auto rect = drawing_state.transform.map(Gfx::FloatRect(x, y, width, height));
110 // We could remove the rounding here, but the lines look better when they have whole number pixel endpoints.
111 auto top_left = drawing_state.transform.map(Gfx::FloatPoint(x, y)).to_rounded<float>();
112 auto top_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y)).to_rounded<float>();
113 auto bottom_left = drawing_state.transform.map(Gfx::FloatPoint(x, y + height - 1)).to_rounded<float>();
114 auto bottom_right = drawing_state.transform.map(Gfx::FloatPoint(x + width - 1, y + height - 1)).to_rounded<float>();
115
116 Gfx::Path path;
117 path.move_to(top_left);
118 path.line_to(top_right);
119 path.line_to(bottom_right);
120 path.line_to(bottom_left);
121 path.line_to(top_left);
122 painter->stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width);
123
124 did_draw(rect);
125}
126
127// 4.12.5.1.14 Drawing images, https://html.spec.whatwg.org/multipage/canvas.html#drawing-images
128WebIDL::ExceptionOr<void> CanvasRenderingContext2D::draw_image_internal(CanvasImageSource const& image, float source_x, float source_y, float source_width, float source_height, float destination_x, float destination_y, float destination_width, float destination_height)
129{
130 // 1. If any of the arguments are infinite or NaN, then return.
131 if (!isfinite(source_x) || !isfinite(source_y) || !isfinite(source_width) || !isfinite(source_height) || !isfinite(destination_x) || !isfinite(destination_y) || !isfinite(destination_width) || !isfinite(destination_height))
132 return {};
133
134 // 2. Let usability be the result of checking the usability of image.
135 auto usability = TRY(check_usability_of_image(image));
136
137 // 3. If usability is bad, then return (without drawing anything).
138 if (usability == CanvasImageSourceUsability::Bad)
139 return {};
140
141 auto const* bitmap = image.visit([](auto const& source) -> Gfx::Bitmap const* { return source->bitmap(); });
142 if (!bitmap)
143 return {};
144
145 // 4. Establish the source and destination rectangles as follows:
146 // If not specified, the dw and dh arguments must default to the values of sw and sh, interpreted such that one CSS pixel in the image is treated as one unit in the output bitmap's coordinate space.
147 // If the sx, sy, sw, and sh arguments are omitted, then they must default to 0, 0, the image's intrinsic width in image pixels, and the image's intrinsic height in image pixels, respectively.
148 // If the image has no intrinsic dimensions, then the concrete object size must be used instead, as determined using the CSS "Concrete Object Size Resolution" algorithm, with the specified size having
149 // neither a definite width nor height, nor any additional constraints, the object's intrinsic properties being those of the image argument, and the default object size being the size of the output bitmap.
150 // The source rectangle is the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
151 // The destination rectangle is the rectangle whose corners are the four points (dx, dy), (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
152 // NOTE: Implemented in drawImage() overloads
153
154 // The source rectangle is the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
155 auto source_rect = Gfx::FloatRect { source_x, source_y, source_width, source_height };
156 // The destination rectangle is the rectangle whose corners are the four points (dx, dy), (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
157 auto destination_rect = Gfx::FloatRect { destination_x, destination_y, destination_width, destination_height };
158 // When the source rectangle is outside the source image, the source rectangle must be clipped
159 // to the source image and the destination rectangle must be clipped in the same proportion.
160 auto clipped_source = source_rect.intersected(bitmap->rect().to_type<float>());
161 auto clipped_destination = destination_rect;
162 if (clipped_source != source_rect) {
163 clipped_destination.set_width(clipped_destination.width() * (clipped_source.width() / source_rect.width()));
164 clipped_destination.set_height(clipped_destination.height() * (clipped_source.height() / source_rect.height()));
165 }
166
167 // 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
168 if (source_width == 0 || source_height == 0)
169 return {};
170
171 // 6. Paint the region of the image argument specified by the source rectangle on the region of the rendering context's output bitmap specified by the destination rectangle, after applying the current transformation matrix to the destination rectangle.
172 auto painter = this->painter();
173 if (!painter)
174 return {};
175
176 painter->draw_scaled_bitmap_with_transform(destination_rect.to_rounded<int>(), *bitmap, source_rect, drawing_state().transform, 1.0f, Gfx::Painter::ScalingMode::BilinearBlend);
177
178 // 7. If image is not origin-clean, then set the CanvasRenderingContext2D's origin-clean flag to false.
179 if (image_is_not_origin_clean(image))
180 m_origin_clean = false;
181
182 return {};
183}
184
185void CanvasRenderingContext2D::did_draw(Gfx::FloatRect const&)
186{
187 // FIXME: Make use of the rect to reduce the invalidated area when possible.
188 if (!canvas_element().layout_node())
189 return;
190 canvas_element().layout_node()->set_needs_display();
191}
192
193Gfx::Painter* CanvasRenderingContext2D::painter()
194{
195 if (!canvas_element().bitmap()) {
196 if (!canvas_element().create_bitmap())
197 return nullptr;
198 m_painter = make<Gfx::Painter>(*canvas_element().bitmap());
199 }
200 return m_painter.ptr();
201}
202
203Optional<Gfx::AntiAliasingPainter> CanvasRenderingContext2D::antialiased_painter()
204{
205 auto painter = this->painter();
206 if (painter)
207 return Gfx::AntiAliasingPainter { *painter };
208 return {};
209}
210
211void CanvasRenderingContext2D::fill_text(DeprecatedString const& text, float x, float y, Optional<double> max_width)
212{
213 if (max_width.has_value() && max_width.value() <= 0)
214 return;
215
216 auto painter = this->painter();
217 if (!painter)
218 return;
219
220 auto& drawing_state = this->drawing_state();
221
222 auto text_rect = Gfx::FloatRect(x, y, max_width.has_value() ? static_cast<float>(max_width.value()) : painter->font().width(text), painter->font().pixel_size());
223 auto transformed_rect = drawing_state.transform.map(text_rect);
224 painter->draw_text(transformed_rect, text, Gfx::TextAlignment::TopLeft, drawing_state.fill_style.to_color_but_fixme_should_accept_any_paint_style());
225 did_draw(transformed_rect);
226}
227
228void CanvasRenderingContext2D::stroke_text(DeprecatedString const& text, float x, float y, Optional<double> max_width)
229{
230 // FIXME: Stroke the text instead of filling it.
231 fill_text(text, x, y, max_width);
232}
233
234void CanvasRenderingContext2D::begin_path()
235{
236 path().clear();
237}
238
239void CanvasRenderingContext2D::stroke_internal(Gfx::Path const& path)
240{
241 auto painter = this->antialiased_painter();
242 if (!painter.has_value())
243 return;
244
245 auto& drawing_state = this->drawing_state();
246
247 painter->stroke_path(path, drawing_state.stroke_style.to_color_but_fixme_should_accept_any_paint_style(), drawing_state.line_width);
248 did_draw(path.bounding_box());
249}
250
251void CanvasRenderingContext2D::stroke()
252{
253 auto transformed_path = path().copy_transformed(drawing_state().transform);
254 stroke_internal(transformed_path);
255}
256
257void CanvasRenderingContext2D::stroke(Path2D const& path)
258{
259 auto transformed_path = path.path().copy_transformed(drawing_state().transform);
260 stroke_internal(transformed_path);
261}
262
263void CanvasRenderingContext2D::fill_internal(Gfx::Path& path, DeprecatedString const& fill_rule)
264{
265 auto painter = this->antialiased_painter();
266 if (!painter.has_value())
267 return;
268
269 path.close_all_subpaths();
270
271 auto winding = Gfx::Painter::WindingRule::Nonzero;
272 if (fill_rule == "evenodd")
273 winding = Gfx::Painter::WindingRule::EvenOdd;
274 else if (fill_rule == "nonzero")
275 winding = Gfx::Painter::WindingRule::Nonzero;
276 else
277 dbgln("Unrecognized fillRule for CRC2D.fill() - this problem goes away once we pass an enum instead of a string");
278
279 painter->fill_path(path, *drawing_state().fill_style.to_gfx_paint_style(), winding);
280 did_draw(path.bounding_box());
281}
282
283void CanvasRenderingContext2D::fill(DeprecatedString const& fill_rule)
284{
285 return fill_internal(path(), fill_rule);
286}
287
288void CanvasRenderingContext2D::fill(Path2D& path, DeprecatedString const& fill_rule)
289{
290 auto transformed_path = path.path().copy_transformed(drawing_state().transform);
291 return fill_internal(transformed_path, fill_rule);
292}
293
294JS::GCPtr<ImageData> CanvasRenderingContext2D::create_image_data(int width, int height) const
295{
296 return ImageData::create_with_size(realm(), width, height);
297}
298
299// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-getimagedata
300WebIDL::ExceptionOr<JS::GCPtr<ImageData>> CanvasRenderingContext2D::get_image_data(int x, int y, int width, int height) const
301{
302 // 1. If either the sw or sh arguments are zero, then throw an "IndexSizeError" DOMException.
303 if (width == 0 || height == 0)
304 return WebIDL::IndexSizeError::create(realm(), "Width and height must not be zero");
305
306 // 2. If the CanvasRenderingContext2D's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
307 if (!m_origin_clean)
308 return WebIDL::SecurityError::create(realm(), "CanvasRenderingContext2D is not origin-clean");
309
310 // 3. Let imageData be a new ImageData object.
311 // 4. Initialize imageData given sw, sh, settings set to settings, and defaultColorSpace set to this's color space.
312 auto image_data = ImageData::create_with_size(realm(), width, height);
313
314 // NOTE: We don't attempt to create the underlying bitmap here; if it doesn't exist, it's like copying only transparent black pixels (which is a no-op).
315 if (!canvas_element().bitmap())
316 return image_data;
317 auto const& bitmap = *canvas_element().bitmap();
318
319 // 5. Let the source rectangle be the rectangle whose corners are the four points (sx, sy), (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
320 auto source_rect = Gfx::Rect { x, y, width, height };
321 auto source_rect_intersected = source_rect.intersected(bitmap.rect());
322
323 // 6. Set the pixel values of imageData to be the pixels of this's output bitmap in the area specified by the source rectangle in the bitmap's coordinate space units, converted from this's color space to imageData's colorSpace using 'relative-colorimetric' rendering intent.
324 // FIXME: Can't use a Gfx::Painter + blit() here as it doesn't support ImageData bitmap's RGBA8888 format.
325 for (int target_y = 0; target_y < source_rect_intersected.height(); ++target_y) {
326 for (int target_x = 0; target_x < source_rect_intersected.width(); ++target_x) {
327 auto pixel = bitmap.get_pixel(target_x + x, target_y + y);
328 image_data->bitmap().set_pixel(target_x, target_y, pixel);
329 }
330 }
331
332 // 7. Set the pixels values of imageData for areas of the source rectangle that are outside of the output bitmap to transparent black.
333 // NOTE: No-op, already done during creation.
334
335 // 8. Return imageData.
336 return image_data;
337}
338
339void CanvasRenderingContext2D::put_image_data(ImageData const& image_data, float x, float y)
340{
341 auto painter = this->painter();
342 if (!painter)
343 return;
344
345 painter->blit(Gfx::IntPoint(x, y), image_data.bitmap(), image_data.bitmap().rect());
346
347 did_draw(Gfx::FloatRect(x, y, image_data.width(), image_data.height()));
348}
349
350// https://html.spec.whatwg.org/multipage/canvas.html#reset-the-rendering-context-to-its-default-state
351void CanvasRenderingContext2D::reset_to_default_state()
352{
353 auto painter = this->painter();
354
355 // 1. Clear canvas's bitmap to transparent black.
356 if (painter)
357 painter->clear_rect(painter->target()->rect(), Color::Transparent);
358
359 // 2. Empty the list of subpaths in context's current default path.
360 path().clear();
361
362 // 3. Clear the context's drawing state stack.
363 clear_drawing_state_stack();
364
365 // 4. Reset everything that drawing state consists of to their initial values.
366 reset_drawing_state();
367
368 if (painter)
369 did_draw(painter->target()->rect().to_type<float>());
370}
371
372// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-measuretext
373JS::NonnullGCPtr<TextMetrics> CanvasRenderingContext2D::measure_text(DeprecatedString const& text)
374{
375 // The measureText(text) method steps are to run the text preparation
376 // algorithm, passing it text and the object implementing the CanvasText
377 // interface, and then using the returned inline box must return a new
378 // TextMetrics object with members behaving as described in the following
379 // list:
380 auto prepared_text = prepare_text(text);
381 auto metrics = TextMetrics::create(realm()).release_value_but_fixme_should_propagate_errors();
382 // FIXME: Use the font that was used to create the glyphs in prepared_text.
383 auto& font = Platform::FontPlugin::the().default_font();
384
385 // width attribute: The width of that inline box, in CSS pixels. (The text's advance width.)
386 metrics->set_width(prepared_text.bounding_box.width());
387 // actualBoundingBoxLeft attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the left side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going left from the given alignment point.
388 metrics->set_actual_bounding_box_left(-prepared_text.bounding_box.left());
389 // actualBoundingBoxRight attribute: The distance parallel to the baseline from the alignment point given by the textAlign attribute to the right side of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going right from the given alignment point.
390 metrics->set_actual_bounding_box_right(prepared_text.bounding_box.right());
391 // fontBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ascent metric of the first available font, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
392 metrics->set_font_bounding_box_ascent(font.baseline());
393 // fontBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the descent metric of the first available font, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
394 metrics->set_font_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
395 // actualBoundingBoxAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the top of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going up from the given baseline.
396 metrics->set_actual_bounding_box_ascent(font.baseline());
397 // actualBoundingBoxDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the bottom of the bounding rectangle of the given text, in CSS pixels; positive numbers indicating a distance going down from the given baseline.
398 metrics->set_actual_bounding_box_descent(prepared_text.bounding_box.height() - font.baseline());
399 // emHeightAscent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the highest top of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the top of that em square (so this value will usually be positive). Zero if the given baseline is the top of that em square; half the font size if the given baseline is the middle of that em square.
400 metrics->set_em_height_ascent(font.baseline());
401 // emHeightDescent attribute: The distance from the horizontal line indicated by the textBaseline attribute to the lowest bottom of the em squares in the inline box, in CSS pixels; positive numbers indicating that the given baseline is above the bottom of that em square. (Zero if the given baseline is the bottom of that em square.)
402 metrics->set_em_height_descent(prepared_text.bounding_box.height() - font.baseline());
403 // hangingBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the hanging baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the hanging baseline. (Zero if the given baseline is the hanging baseline.)
404 metrics->set_hanging_baseline(font.baseline());
405 // alphabeticBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the alphabetic baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the alphabetic baseline. (Zero if the given baseline is the alphabetic baseline.)
406 metrics->set_font_bounding_box_ascent(0);
407 // ideographicBaseline attribute: The distance from the horizontal line indicated by the textBaseline attribute to the ideographic-under baseline of the inline box, in CSS pixels; positive numbers indicating that the given baseline is below the ideographic-under baseline. (Zero if the given baseline is the ideographic-under baseline.)
408 metrics->set_font_bounding_box_ascent(0);
409
410 return metrics;
411}
412
413// https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm
414CanvasRenderingContext2D::PreparedText CanvasRenderingContext2D::prepare_text(DeprecatedString const& text, float max_width)
415{
416 // 1. If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
417 if (max_width <= 0 || max_width != max_width) {
418 return {};
419 }
420
421 // 2. Replace all ASCII whitespace in text with U+0020 SPACE characters.
422 StringBuilder builder { text.length() };
423 for (auto c : text) {
424 builder.append(Infra::is_ascii_whitespace(c) ? ' ' : c);
425 }
426 auto replaced_text = builder.to_deprecated_string();
427
428 // 3. Let font be the current font of target, as given by that object's font attribute.
429 // FIXME: Once we have CanvasTextDrawingStyles, implement font selection.
430
431 // 4. Apply the appropriate step from the following list to determine the value of direction:
432 // 4.1. If the target object's direction attribute has the value "ltr": Let direction be 'ltr'.
433 // 4.2. If the target object's direction attribute has the value "rtl": Let direction be 'rtl'.
434 // 4.3. If the target object's font style source object is an element: Let direction be the directionality of the target object's font style source object.
435 // 4.4. If the target object's font style source object is a Document with a non-null document element: Let direction be the directionality of the target object's font style source object's document element.
436 // 4.5. Otherwise: Let direction be 'ltr'.
437 // FIXME: Once we have CanvasTextDrawingStyles, implement directionality.
438
439 // 5. Form a hypothetical infinitely-wide CSS line box containing a single inline box containing the text text, with its CSS properties set as follows:
440 // 'direction' -> direction
441 // 'font' -> font
442 // 'font-kerning' -> target's fontKerning
443 // 'font-stretch' -> target's fontStretch
444 // 'font-variant-caps' -> target's fontVariantCaps
445 // 'letter-spacing' -> target's letterSpacing
446 // SVG text-rendering -> target's textRendering
447 // 'white-space' -> 'pre'
448 // 'word-spacing' -> target's wordSpacing
449 // ...and with all other properties set to their initial values.
450 // FIXME: Actually use a LineBox here instead of, you know, using the default font and measuring its size (which is not the spec at all).
451 // FIXME: Once we have CanvasTextDrawingStyles, add the CSS attributes.
452 auto& font = Platform::FontPlugin::the().default_font();
453 size_t width = 0;
454 size_t height = font.pixel_size();
455
456 Utf8View replaced_text_view { replaced_text };
457 for (auto it = replaced_text_view.begin(); it != replaced_text_view.end(); ++it)
458 width += font.glyph_or_emoji_width(it);
459
460 // 6. If maxWidth was provided and the hypothetical width of the inline box in the hypothetical line box is greater than maxWidth CSS pixels, then change font to have a more condensed font (if one is available or if a reasonably readable one can be synthesized by applying a horizontal scale factor to the font) or a smaller font, and return to the previous step.
461 // FIXME: Record the font size used for this piece of text, and actually retry with a smaller size if needed.
462
463 // 7. The anchor point is a point on the inline box, and the physical alignment is one of the values left, right, and center. These variables are determined by the textAlign and textBaseline values as follows:
464 // Horizontal position:
465 // 7.1. If textAlign is left, if textAlign is start and direction is 'ltr' or if textAlign is end and direction is 'rtl': Let the anchor point's horizontal position be the left edge of the inline box, and let physical alignment be left.
466 // 7.2. If textAlign is right, if textAlign is end and direction is 'ltr' or if textAlign is start and direction is 'rtl': Let the anchor point's horizontal position be the right edge of the inline box, and let physical alignment be right.
467 // 7.3. If textAlign is center: Let the anchor point's horizontal position be half way between the left and right edges of the inline box, and let physical alignment be center.
468 // Vertical position:
469 // 7.4. If textBaseline is top: Let the anchor point's vertical position be the top of the em box of the first available font of the inline box.
470 // 7.5. If textBaseline is hanging: Let the anchor point's vertical position be the hanging baseline of the first available font of the inline box.
471 // 7.6. If textBaseline is middle: Let the anchor point's vertical position be half way between the bottom and the top of the em box of the first available font of the inline box.
472 // 7.7. If textBaseline is alphabetic: Let the anchor point's vertical position be the alphabetic baseline of the first available font of the inline box.
473 // 7.8. If textBaseline is ideographic: Let the anchor point's vertical position be the ideographic-under baseline of the first available font of the inline box.
474 // 7.9. If textBaseline is bottom: Let the anchor point's vertical position be the bottom of the em box of the first available font of the inline box.
475 // FIXME: Once we have CanvasTextDrawingStyles, handle the alignment and baseline.
476 [[maybe_unused]] Gfx::IntPoint anchor { 0, 0 };
477 auto physical_alignment = Gfx::TextAlignment::CenterLeft;
478
479 // 8. Let result be an array constructed by iterating over each glyph in the inline box from left to right (if any), adding to the array, for each glyph, the shape of the glyph as it is in the inline box, positioned on a coordinate space using CSS pixels with its origin is at the anchor point.
480 PreparedText prepared_text { {}, physical_alignment, { 0, 0, static_cast<int>(width), static_cast<int>(height) } };
481 prepared_text.glyphs.ensure_capacity(replaced_text.length());
482
483 size_t previous_grapheme_boundary = 0;
484 Unicode::for_each_grapheme_segmentation_boundary(replaced_text_view, [&](auto boundary) {
485 if (boundary == 0)
486 return IterationDecision::Continue;
487
488 auto glyph_view = replaced_text_view.substring_view(previous_grapheme_boundary, boundary - previous_grapheme_boundary);
489 auto glyph = String::from_utf8(glyph_view.as_string()).release_value_but_fixme_should_propagate_errors();
490
491 prepared_text.glyphs.append({ move(glyph), { static_cast<int>(boundary), 0 } });
492 return IterationDecision::Continue;
493 });
494
495 // 9. Return result, physical alignment, and the inline box.
496 return prepared_text;
497}
498
499void CanvasRenderingContext2D::clip()
500{
501 // FIXME: Implement.
502}
503
504// https://html.spec.whatwg.org/multipage/canvas.html#check-the-usability-of-the-image-argument
505WebIDL::ExceptionOr<CanvasImageSourceUsability> check_usability_of_image(CanvasImageSource const& image)
506{
507 // 1. Switch on image:
508 auto usability = TRY(image.visit(
509 // HTMLOrSVGImageElement
510 [](JS::Handle<HTMLImageElement> const& image_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
511 // FIXME: If image's current request's state is broken, then throw an "InvalidStateError" DOMException.
512
513 // If image is not fully decodable, then return bad.
514 if (!image_element->bitmap())
515 return { CanvasImageSourceUsability::Bad };
516
517 // If image has an intrinsic width or intrinsic height (or both) equal to zero, then return bad.
518 if (image_element->bitmap()->width() == 0 || image_element->bitmap()->height() == 0)
519 return { CanvasImageSourceUsability::Bad };
520 return Optional<CanvasImageSourceUsability> {};
521 },
522
523 // FIXME: HTMLVideoElement
524 // If image's readyState attribute is either HAVE_NOTHING or HAVE_METADATA, then return bad.
525
526 // HTMLCanvasElement
527 // FIXME: OffscreenCanvas
528 [](JS::Handle<HTMLCanvasElement> const& canvas_element) -> WebIDL::ExceptionOr<Optional<CanvasImageSourceUsability>> {
529 // If image has either a horizontal dimension or a vertical dimension equal to zero, then throw an "InvalidStateError" DOMException.
530 if (canvas_element->width() == 0 || canvas_element->height() == 0)
531 return WebIDL::InvalidStateError::create(canvas_element->realm(), "Canvas width or height is zero");
532 return Optional<CanvasImageSourceUsability> {};
533 }));
534 if (usability.has_value())
535 return usability.release_value();
536
537 // 2. Return good.
538 return { CanvasImageSourceUsability::Good };
539}
540
541// https://html.spec.whatwg.org/multipage/canvas.html#the-image-argument-is-not-origin-clean
542bool image_is_not_origin_clean(CanvasImageSource const& image)
543{
544 // An object image is not origin-clean if, switching on image's type:
545 return image.visit(
546 // HTMLOrSVGImageElement
547 [](JS::Handle<HTMLImageElement> const&) {
548 // FIXME: image's current request's image data is CORS-cross-origin.
549 return false;
550 },
551
552 // FIXME: HTMLVideoElement
553 // image's media data is CORS-cross-origin.
554
555 // HTMLCanvasElement
556 // FIXME: ImageBitmap
557 [](JS::Handle<HTMLCanvasElement> const&) {
558 // FIXME: image's bitmap's origin-clean flag is false.
559 return false;
560 });
561}
562
563}