Skip to main content

script/dom/canvas/2d/
canvas_state.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5#![cfg_attr(crown, allow(crown::jscontext_first_arg))]
6
7use std::cell::Cell;
8use std::fmt;
9use std::str::FromStr;
10use std::sync::Arc;
11
12use app_units::Au;
13use cssparser::Parser;
14use cssparser::color::clamp_unit_f32;
15use euclid::default::{Point2D, Rect, Size2D, Transform2D};
16use euclid::{Vector2D, vec2};
17use fonts::{
18    FontBaseline, FontContext, FontGroup, FontIdentifier, FontMetrics, FontRef, ShapingFlags,
19    ShapingOptions,
20};
21use icu_locale_core::subtags::Language;
22use js::context::{JSContext, NoGC};
23use net_traits::image_cache::{ImageCache, ImageResponse};
24use net_traits::request::CorsSettings;
25use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
26use script_bindings::cell::DomRefCell;
27use servo_arc::Arc as ServoArc;
28use servo_base::generic_channel::GenericBufferedSender;
29use servo_base::{Epoch, generic_channel};
30use servo_canvas_traits::canvas::{
31    CanvasCommand, CanvasFont, CanvasId, CanvasMsg, CompositionOptions, CompositionOrBlending,
32    FillOrStrokeStyle, FillRule, GlyphAndPosition, LineCapStyle, LineJoinStyle, LineOptions,
33    LinearGradientStyle, Path, RadialGradientStyle, RangeError, RepetitionStyle, RoundRectRadius,
34    ShadowOptions, TextRun,
35};
36use servo_constellation_traits::ScriptToConstellationMessage;
37use servo_url::{ImmutableOrigin, ServoUrl};
38use style::Zero;
39use style::color::{AbsoluteColor, ColorFlags, ColorSpace};
40use style::computed_values::font_variant_position::T as FontVariantPosition;
41use style::properties::longhands::font_variant_caps::computed_value::T as FontVariantCaps;
42use style::properties::style_structs::Font;
43use style::stylesheets::CssRuleType;
44use style::values::computed::color::Color as ComputedColor;
45use style::values::computed::font::FontStyle;
46use style::values::computed::{
47    FontFeatureSettings, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric,
48};
49use style::values::specified::color::Color;
50use style_traits::values::ToCss;
51use style_traits::{CssWriter, ParsingMode};
52use unicode_script::Script;
53use webrender_api::ImageKey;
54
55use crate::canvas_context::{CanvasContext, OffscreenRenderingContext, RenderingContext};
56use crate::conversions::Convert;
57use crate::css::css::{ANONYMOUS_CONTENT_URL_DATA, parser_context_for_anonymous_content};
58use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
59    CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin,
60    CanvasTextAlign, CanvasTextBaseline, ImageDataMethods,
61};
62use crate::dom::bindings::codegen::Bindings::DOMMatrixBinding::DOMMatrix2DInit;
63use crate::dom::bindings::codegen::UnionTypes::{
64    StringOrCanvasGradientOrCanvasPattern, UnrestrictedDoubleOrDOMPointInit,
65    UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence,
66};
67use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
68use crate::dom::bindings::inheritance::Castable;
69use crate::dom::bindings::num::Finite;
70use crate::dom::bindings::root::{Dom, DomRoot};
71use crate::dom::bindings::str::DOMString;
72use crate::dom::canvasgradient::{CanvasGradient, CanvasGradientStyle, ToFillOrStrokeStyle};
73use crate::dom::canvaspattern::CanvasPattern;
74use crate::dom::dommatrix::DOMMatrix;
75use crate::dom::dommatrixreadonly::dommatrix2dinit_to_matrix;
76use crate::dom::element::Element;
77use crate::dom::globalscope::GlobalScope;
78use crate::dom::html::htmlcanvaselement::HTMLCanvasElement;
79use crate::dom::html::htmlimageelement::HTMLImageElement;
80use crate::dom::html::htmlvideoelement::HTMLVideoElement;
81use crate::dom::imagebitmap::ImageBitmap;
82use crate::dom::imagedata::ImageData;
83use crate::dom::node::{Node, NodeTraits};
84use crate::dom::offscreencanvas::OffscreenCanvas;
85use crate::dom::paintworkletglobalscope::PaintWorkletGlobalScope;
86use crate::dom::textmetrics::TextMetrics;
87
88const HANGING_BASELINE_DEFAULT: f64 = 0.8;
89const IDEOGRAPHIC_BASELINE_DEFAULT: f64 = 0.5;
90
91#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
92#[derive(Clone, JSTraceable, MallocSizeOf)]
93pub(super) enum CanvasFillOrStrokeStyle {
94    Color(#[no_trace] AbsoluteColor),
95    Gradient(Dom<CanvasGradient>),
96    Pattern(Dom<CanvasPattern>),
97}
98
99impl CanvasFillOrStrokeStyle {
100    fn to_fill_or_stroke_style(&self) -> FillOrStrokeStyle {
101        match self {
102            CanvasFillOrStrokeStyle::Color(rgba) => FillOrStrokeStyle::Color(*rgba),
103            CanvasFillOrStrokeStyle::Gradient(gradient) => gradient.to_fill_or_stroke_style(),
104            CanvasFillOrStrokeStyle::Pattern(pattern) => pattern.to_fill_or_stroke_style(),
105        }
106    }
107}
108
109#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
110#[derive(Clone, JSTraceable, MallocSizeOf)]
111pub(super) struct CanvasContextState {
112    global_alpha: f64,
113    #[no_trace]
114    global_composition: CompositionOrBlending,
115    image_smoothing_enabled: bool,
116    fill_style: CanvasFillOrStrokeStyle,
117    stroke_style: CanvasFillOrStrokeStyle,
118    line_width: f64,
119    #[no_trace]
120    line_cap: LineCapStyle,
121    #[no_trace]
122    line_join: LineJoinStyle,
123    miter_limit: f64,
124    line_dash: Vec<f64>,
125    line_dash_offset: f64,
126    #[no_trace]
127    transform: Transform2D<f64>,
128    shadow_offset_x: f64,
129    shadow_offset_y: f64,
130    shadow_blur: f64,
131    #[no_trace]
132    shadow_color: AbsoluteColor,
133    #[no_trace]
134    #[conditional_malloc_size_of]
135    font_style: Option<ServoArc<Font>>,
136    text_align: CanvasTextAlign,
137    text_baseline: CanvasTextBaseline,
138    direction: CanvasDirection,
139    /// The number of clips pushed onto the context while in this state.
140    /// When restoring old state, same number of clips will be popped to restore state.
141    clips_pushed: usize,
142}
143
144impl CanvasContextState {
145    const DEFAULT_FONT_STYLE: &'static str = "10px sans-serif";
146
147    pub(super) fn new() -> CanvasContextState {
148        CanvasContextState {
149            global_alpha: 1.0,
150            global_composition: CompositionOrBlending::default(),
151            image_smoothing_enabled: true,
152            fill_style: CanvasFillOrStrokeStyle::Color(AbsoluteColor::BLACK),
153            stroke_style: CanvasFillOrStrokeStyle::Color(AbsoluteColor::BLACK),
154            line_width: 1.0,
155            line_cap: LineCapStyle::Butt,
156            line_join: LineJoinStyle::Miter,
157            miter_limit: 10.0,
158            transform: Transform2D::identity(),
159            shadow_offset_x: 0.0,
160            shadow_offset_y: 0.0,
161            shadow_blur: 0.0,
162            shadow_color: AbsoluteColor::TRANSPARENT_BLACK,
163            font_style: None,
164            text_align: CanvasTextAlign::Start,
165            text_baseline: CanvasTextBaseline::Alphabetic,
166            direction: CanvasDirection::Inherit,
167            line_dash: Vec::new(),
168            line_dash_offset: 0.0,
169            clips_pushed: 0,
170        }
171    }
172
173    fn composition_options(&self) -> CompositionOptions {
174        CompositionOptions {
175            alpha: self.global_alpha,
176            composition_operation: self.global_composition,
177        }
178    }
179
180    fn shadow_options(&self) -> ShadowOptions {
181        ShadowOptions {
182            offset_x: self.shadow_offset_x,
183            offset_y: self.shadow_offset_y,
184            blur: self.shadow_blur,
185            color: self.shadow_color,
186        }
187    }
188
189    fn line_options(&self) -> LineOptions {
190        LineOptions {
191            width: self.line_width,
192            cap_style: self.line_cap,
193            join_style: self.line_join,
194            miter_limit: self.miter_limit,
195            dash: self.line_dash.iter().map(|x| *x as f32).collect(),
196            dash_offset: self.line_dash_offset,
197        }
198    }
199}
200
201#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
202#[derive(JSTraceable, MallocSizeOf)]
203pub(super) struct CanvasState {
204    #[no_trace]
205    canvas_id: CanvasId,
206    #[no_trace]
207    size: Cell<Size2D<u64>>,
208    state: DomRefCell<CanvasContextState>,
209    origin_clean: Cell<bool>,
210    #[ignore_malloc_size_of = "ImageCache"]
211    #[no_trace]
212    image_cache: Arc<dyn ImageCache>,
213    /// The base URL for resolving CSS image URL values.
214    /// Needed because of <https://github.com/servo/servo/issues/17625>
215    #[no_trace]
216    base_url: ServoUrl,
217    #[no_trace]
218    origin: ImmutableOrigin,
219    /// Any missing image URLs.
220    #[no_trace]
221    missing_image_urls: DomRefCell<Vec<ServoUrl>>,
222    saved_states: DomRefCell<Vec<CanvasContextState>>,
223    /// <https://html.spec.whatwg.org/multipage/#current-default-path>
224    #[no_trace]
225    current_default_path: DomRefCell<Path>,
226    /// Buffered sender for batching canvas commands.
227    #[no_trace]
228    pub(super) buffered_sender: GenericBufferedSender<CanvasMsg, CanvasCommand>,
229}
230
231/// Part of step 5 of <https://html.spec.whatwg.org/multipage/#dom-context-2d-roundrect>
232/// Validation is done later as order of operations matter.
233pub(super) fn round_rect_radii(
234    radii: &UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence,
235) -> Vec<RoundRectRadius> {
236    fn from_inner(radius: &UnrestrictedDoubleOrDOMPointInit) -> RoundRectRadius {
237        match radius {
238            UnrestrictedDoubleOrDOMPointInit::UnrestrictedDouble(radius) => RoundRectRadius {
239                x: *radius,
240                y: *radius,
241            },
242            UnrestrictedDoubleOrDOMPointInit::DOMPointInit(point) => RoundRectRadius {
243                x: point.x,
244                y: point.y,
245            },
246        }
247    }
248
249    match radii {
250        UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence::UnrestrictedDouble(radius) => {
251            vec![RoundRectRadius {
252                x: *radius,
253                y: *radius,
254            }]
255        },
256        UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence::DOMPointInit(point) => {
257            vec![RoundRectRadius {
258                x: point.x,
259                y: point.y,
260            }]
261        },
262        UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence::UnrestrictedDoubleOrDOMPointInitSequence(radii) => {
263            radii.iter().map(from_inner).collect()
264        },
265    }
266}
267
268/// Converts a [`RangeError`] from `Path::round_rect` into a DOM `Error`.
269pub(super) fn round_rect_error(error: RangeError) -> Error {
270    match error {
271        RangeError::InvalidSize => {
272            Error::Range(c"radii must be a list of size one, two, three, or four".to_owned())
273        },
274        RangeError::NegativeRadius => Error::Range(c"radii must not be negative".to_owned()),
275    }
276}
277
278impl CanvasState {
279    pub(super) fn new(global: &GlobalScope, size: Size2D<u64>) -> Option<CanvasState> {
280        debug!("Creating new canvas rendering context.");
281        let (sender, receiver) =
282            profile_traits::generic_channel::channel(global.time_profiler_chan().clone()).unwrap();
283        let script_to_constellation_chan = global.script_to_constellation_chan();
284        debug!("Asking constellation to create new canvas thread.");
285        let size = adjust_canvas_size(size);
286        script_to_constellation_chan
287            .send(ScriptToConstellationMessage::CreateCanvasPaintThread(
288                size, sender,
289            ))
290            .unwrap();
291        let (canvas_thread_sender, canvas_id) = receiver.recv().ok()??;
292        debug!("Done.");
293        // Worklets always receive a unique origin. This messes with fetching
294        // cached images in the case of paint worklets, since the image cache
295        // is keyed on the origin requesting the image data.
296        let origin = if global.is::<PaintWorkletGlobalScope>() {
297            global.api_base_url().origin()
298        } else {
299            global.origin().immutable().clone()
300        };
301        Some(CanvasState {
302            canvas_id,
303            size: Cell::new(size),
304            state: DomRefCell::new(CanvasContextState::new()),
305            origin_clean: Cell::new(true),
306            image_cache: global.image_cache(),
307            base_url: global.api_base_url(),
308            missing_image_urls: DomRefCell::new(Vec::new()),
309            saved_states: DomRefCell::new(Vec::new()),
310            origin,
311            current_default_path: DomRefCell::new(Path::new()),
312            buffered_sender: GenericBufferedSender::new(
313                canvas_thread_sender,
314                Box::new(move |cmds| (canvas_id, CanvasCommand::ProcessBatchMessages(cmds))),
315                servo_config::pref!(dom_canvas_msg_buffer_size) as usize,
316            ),
317        })
318    }
319
320    pub(super) fn set_image_key(&self, image_key: ImageKey) {
321        // Flushing is not required for correctness, but optimization heuristics for heavy command.
322        self.send_canvas_command_immediate(CanvasCommand::SetImageKey(image_key));
323    }
324
325    pub(super) fn get_missing_image_urls(&self) -> &DomRefCell<Vec<ServoUrl>> {
326        &self.missing_image_urls
327    }
328
329    pub(super) fn get_canvas_id(&self) -> CanvasId {
330        self.canvas_id
331    }
332
333    pub(super) fn bitmap_dimensions(&self) -> Size2D<u64> {
334        self.size.get()
335    }
336
337    pub(super) fn is_paintable(&self) -> bool {
338        !self.size.get().is_empty()
339    }
340
341    pub(super) fn send_canvas_command(&self, msg: CanvasCommand) {
342        if !self.is_paintable() {
343            return;
344        }
345        self.buffered_sender.send(msg).unwrap();
346    }
347
348    /// Send a canvas command immediately.
349    /// If there are buffered commands, they are combined with this message
350    /// into a single `ProcessBatchMessages` to preserve ordering and avoid
351    /// two separate sends.
352    pub(super) fn send_canvas_command_immediate(&self, msg: CanvasCommand) {
353        if !self.is_paintable() {
354            self.buffered_sender.flush().unwrap();
355        } else {
356            self.buffered_sender.send_immediate(msg).unwrap();
357        }
358    }
359
360    /// Updates WR image and blocks on completion
361    pub(super) fn update_rendering(&self, canvas_epoch: Option<Epoch>) -> bool {
362        if !self.is_paintable() {
363            return false;
364        }
365
366        self.send_canvas_command_immediate(CanvasCommand::UpdateImage(canvas_epoch));
367        true
368    }
369
370    /// <https://html.spec.whatwg.org/multipage/#concept-canvas-set-bitmap-dimensions>
371    pub(super) fn set_bitmap_dimensions(&self, size: Size2D<u64>) {
372        // Step 1. Reset the rendering context to its default state.
373        self.reset_to_initial_state();
374
375        // Step 2. Resize the output bitmap to the new width and height.
376        self.size.replace(adjust_canvas_size(size));
377        // Discard buffered commands targeting the old canvas.
378        self.buffered_sender.discard();
379        // Flushing is not required for correctness, but optimization heuristics for heavy command.
380        self.send_canvas_command_immediate(CanvasCommand::Recreate(Some(self.size.get())));
381    }
382
383    /// <https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state>
384    pub(super) fn reset(&self) {
385        self.reset_to_initial_state();
386
387        if !self.is_paintable() {
388            return;
389        }
390
391        // Step 1. Clear canvas's bitmap to transparent black.
392        // Discard buffered commands targeting the old canvas.
393        self.buffered_sender.discard();
394        // Flushing is not required for correctness, but optimization heuristics for heavy command.
395        self.send_canvas_command_immediate(CanvasCommand::Recreate(None));
396    }
397
398    /// <https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state>
399    fn reset_to_initial_state(&self) {
400        // Step 2. Empty the list of subpaths in context's current default path.
401        *self.current_default_path.borrow_mut() = Path::new();
402
403        // Step 3. Clear the context's drawing state stack.
404        self.saved_states.borrow_mut().clear();
405
406        // Step 4. Reset everything that drawing state consists of to their initial values.
407        *self.state.borrow_mut() = CanvasContextState::new();
408
409        // <https://html.spec.whatwg.org/multipage/#security-with-canvas-elements>
410        // The flag can be reset in certain situations; for example, when changing the value of the
411        // width or the height content attribute of the canvas element to which a
412        // CanvasRenderingContext2D is bound, the bitmap is cleared and its origin-clean flag is
413        // reset.
414        self.set_origin_clean(true);
415    }
416
417    pub(super) fn reset_bitmap(&self) {
418        if !self.is_paintable() {
419            return;
420        }
421
422        self.send_canvas_command(CanvasCommand::ClearRect(
423            self.size.get().to_f32().into(),
424            Transform2D::identity(),
425        ));
426    }
427
428    fn create_drawable_rect(&self, x: f64, y: f64, w: f64, h: f64) -> Option<Rect<f32>> {
429        if !([x, y, w, h].iter().all(|val| val.is_finite())) {
430            return None;
431        }
432
433        if w == 0.0 && h == 0.0 {
434            return None;
435        }
436
437        Some(Rect::new(
438            Point2D::new(x as f32, y as f32),
439            Size2D::new(w as f32, h as f32),
440        ))
441    }
442
443    pub(super) fn origin_is_clean(&self) -> bool {
444        self.origin_clean.get()
445    }
446
447    fn set_origin_clean(&self, origin_clean: bool) {
448        self.origin_clean.set(origin_clean);
449    }
450
451    /// <https://html.spec.whatwg.org/multipage/#the-image-argument-is-not-origin-clean>
452    fn is_origin_clean(&self, source: CanvasImageSource) -> bool {
453        match source {
454            CanvasImageSource::HTMLImageElement(image) => {
455                image.same_origin(&GlobalScope::entry().origin())
456            },
457            CanvasImageSource::HTMLVideoElement(video) => video.origin_is_clean(),
458            CanvasImageSource::HTMLCanvasElement(canvas) => canvas.origin_is_clean(),
459            CanvasImageSource::ImageBitmap(bitmap) => bitmap.origin_is_clean(),
460            CanvasImageSource::OffscreenCanvas(canvas) => canvas.origin_is_clean(),
461            CanvasImageSource::CSSStyleValue(_) => true,
462        }
463    }
464
465    fn fetch_image_data(
466        &self,
467        url: ServoUrl,
468        cors_setting: Option<CorsSettings>,
469    ) -> Option<Snapshot> {
470        let raster_image = match self.request_image_from_cache(url, cors_setting) {
471            ImageResponse::Loaded(image, _) => {
472                if let Some(image) = image.as_raster_image() {
473                    image
474                } else {
475                    // TODO: https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
476                    warn!("Vector images are not supported as image source in canvas2d");
477                    return None;
478                }
479            },
480            ImageResponse::FailedToLoadOrDecode | ImageResponse::MetadataLoaded(_) => {
481                return None;
482            },
483        };
484
485        Some(raster_image.as_snapshot())
486    }
487
488    fn request_image_from_cache(
489        &self,
490        url: ServoUrl,
491        cors_setting: Option<CorsSettings>,
492    ) -> ImageResponse {
493        match self
494            .image_cache
495            .get_image(url.clone(), self.origin.clone(), cors_setting)
496        {
497            Some(image) => ImageResponse::Loaded(image, url),
498            None => {
499                // Rather annoyingly, we get the same response back from
500                // A load which really failed and from a load which hasn't started yet.
501                self.missing_image_urls.borrow_mut().push(url);
502                ImageResponse::FailedToLoadOrDecode
503            },
504        }
505    }
506
507    ///
508    /// drawImage coordinates explained
509    ///
510    /// ```
511    ///  Source Image      Destination Canvas
512    /// +-------------+     +-------------+
513    /// |             |     |             |
514    /// |(sx,sy)      |     |(dx,dy)      |
515    /// |   +----+    |     |   +----+    |
516    /// |   |    |    |     |   |    |    |
517    /// |   |    |sh  |---->|   |    |dh  |
518    /// |   |    |    |     |   |    |    |
519    /// |   +----+    |     |   +----+    |
520    /// |     sw      |     |     dw      |
521    /// |             |     |             |
522    /// +-------------+     +-------------+
523    /// ```
524    ///
525    /// The rectangle (sx, sy, sw, sh) from the source image
526    /// is copied on the rectangle (dx, dy, dh, dw) of the destination canvas
527    ///
528    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
529    #[expect(clippy::too_many_arguments)]
530    fn draw_image_internal(
531        &self,
532        htmlcanvas: Option<&HTMLCanvasElement>,
533        image: CanvasImageSource,
534        sx: f64,
535        sy: f64,
536        sw: Option<f64>,
537        sh: Option<f64>,
538        dx: f64,
539        dy: f64,
540        dw: Option<f64>,
541        dh: Option<f64>,
542    ) -> ErrorResult {
543        if !self.is_paintable() {
544            return Ok(());
545        }
546
547        let result = match image {
548            CanvasImageSource::HTMLImageElement(ref image) => {
549                // https://html.spec.whatwg.org/multipage/#drawing-images
550                // 2. Let usability be the result of checking the usability of image.
551                // 3. If usability is bad, then return (without drawing anything).
552                if !image.is_usable()? {
553                    return Ok(());
554                }
555
556                self.draw_html_image_element(image, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh);
557                Ok(())
558            },
559            CanvasImageSource::HTMLVideoElement(ref video) => {
560                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
561                // Step 2. Let usability be the result of checking the usability of image.
562                // Step 3. If usability is bad, then return (without drawing anything).
563                if !video.is_usable() {
564                    return Ok(());
565                }
566
567                self.draw_html_video_element(video, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh);
568                Ok(())
569            },
570            CanvasImageSource::HTMLCanvasElement(ref canvas) => {
571                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
572                if canvas.get_size().is_empty() {
573                    return Err(Error::InvalidState(None));
574                }
575
576                self.draw_html_canvas_element(canvas, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh)
577            },
578            CanvasImageSource::ImageBitmap(ref bitmap) => {
579                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
580                if bitmap.is_detached() {
581                    return Err(Error::InvalidState(None));
582                }
583
584                self.draw_image_bitmap(bitmap, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh);
585                Ok(())
586            },
587            CanvasImageSource::OffscreenCanvas(ref canvas) => {
588                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
589                if canvas.get_size().is_empty() {
590                    return Err(Error::InvalidState(None));
591                }
592
593                self.draw_offscreen_canvas(canvas, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh)
594            },
595            CanvasImageSource::CSSStyleValue(ref value) => {
596                let url = value
597                    .get_url(self.base_url.clone())
598                    .ok_or(Error::InvalidState(None))?;
599                self.fetch_and_draw_image_data(
600                    htmlcanvas, url, None, sx, sy, sw, sh, dx, dy, dw, dh,
601                )
602            },
603        };
604
605        if result.is_ok() && !self.is_origin_clean(image) {
606            self.set_origin_clean(false);
607        }
608        result
609    }
610
611    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
612    #[expect(clippy::too_many_arguments)]
613    fn draw_html_image_element(
614        &self,
615        image: &HTMLImageElement,
616        canvas: Option<&HTMLCanvasElement>,
617        sx: f64,
618        sy: f64,
619        sw: Option<f64>,
620        sh: Option<f64>,
621        dx: f64,
622        dy: f64,
623        dw: Option<f64>,
624        dh: Option<f64>,
625    ) {
626        let Some(snapshot) = image.get_raster_image_data() else {
627            return;
628        };
629
630        // Step 4. Establish the source and destination rectangles.
631        let image_size = snapshot.size();
632        let dw = dw.unwrap_or(image_size.width as f64);
633        let dh = dh.unwrap_or(image_size.height as f64);
634        let sw = sw.unwrap_or(image_size.width as f64);
635        let sh = sh.unwrap_or(image_size.height as f64);
636
637        let (source_rect, dest_rect) =
638            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
639
640        // Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
641        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
642            return;
643        }
644
645        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
646
647        self.send_canvas_command(CanvasCommand::DrawImage(
648            snapshot.to_shared(),
649            dest_rect,
650            source_rect,
651            smoothing_enabled,
652            self.state.borrow().shadow_options(),
653            self.state.borrow().composition_options(),
654            self.state.borrow().transform,
655        ));
656
657        self.mark_as_dirty(canvas);
658    }
659
660    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
661    #[expect(clippy::too_many_arguments)]
662    fn draw_html_video_element(
663        &self,
664        video: &HTMLVideoElement,
665        canvas: Option<&HTMLCanvasElement>,
666        sx: f64,
667        sy: f64,
668        sw: Option<f64>,
669        sh: Option<f64>,
670        dx: f64,
671        dy: f64,
672        dw: Option<f64>,
673        dh: Option<f64>,
674    ) {
675        let Some(snapshot) = video.get_current_frame_data() else {
676            return;
677        };
678
679        // Step 4. Establish the source and destination rectangles.
680        let video_size = snapshot.size();
681        let dw = dw.unwrap_or(video_size.width as f64);
682        let dh = dh.unwrap_or(video_size.height as f64);
683        let sw = sw.unwrap_or(video_size.width as f64);
684        let sh = sh.unwrap_or(video_size.height as f64);
685
686        let (source_rect, dest_rect) =
687            self.adjust_source_dest_rects(video_size, sx, sy, sw, sh, dx, dy, dw, dh);
688
689        // Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
690        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
691            return;
692        }
693
694        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
695
696        self.send_canvas_command(CanvasCommand::DrawImage(
697            snapshot.to_shared(),
698            dest_rect,
699            source_rect,
700            smoothing_enabled,
701            self.state.borrow().shadow_options(),
702            self.state.borrow().composition_options(),
703            self.state.borrow().transform,
704        ));
705
706        self.mark_as_dirty(canvas);
707    }
708
709    #[expect(clippy::too_many_arguments)]
710    fn draw_offscreen_canvas(
711        &self,
712        canvas: &OffscreenCanvas,
713        htmlcanvas: Option<&HTMLCanvasElement>,
714        sx: f64,
715        sy: f64,
716        sw: Option<f64>,
717        sh: Option<f64>,
718        dx: f64,
719        dy: f64,
720        dw: Option<f64>,
721        dh: Option<f64>,
722    ) -> ErrorResult {
723        let canvas_size = canvas
724            .context()
725            .map_or_else(|| canvas.get_size(), |context| context.size());
726
727        let dw = dw.unwrap_or(canvas_size.width as f64);
728        let dh = dh.unwrap_or(canvas_size.height as f64);
729        let sw = sw.unwrap_or(canvas_size.width as f64);
730        let sh = sh.unwrap_or(canvas_size.height as f64);
731
732        let image_size = Size2D::new(canvas_size.width, canvas_size.height);
733        // 2. Establish the source and destination rectangles
734        let (source_rect, dest_rect) =
735            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
736
737        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
738            return Ok(());
739        }
740
741        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
742
743        if let Some(context) = canvas.context() {
744            match *context {
745                OffscreenRenderingContext::Context2d(ref context) => {
746                    self.buffered_sender.flush().unwrap();
747                    context.send_canvas_command_immediate(CanvasCommand::DrawImageInOther(
748                        self.get_canvas_id(),
749                        dest_rect,
750                        source_rect,
751                        smoothing_enabled,
752                        self.state.borrow().shadow_options(),
753                        self.state.borrow().composition_options(),
754                        self.state.borrow().transform,
755                    ));
756                },
757                OffscreenRenderingContext::BitmapRenderer(ref context) => {
758                    let Some(snapshot) = context.get_image_data() else {
759                        return Ok(());
760                    };
761
762                    self.send_canvas_command(CanvasCommand::DrawImage(
763                        snapshot.to_shared(),
764                        dest_rect,
765                        source_rect,
766                        smoothing_enabled,
767                        self.state.borrow().shadow_options(),
768                        self.state.borrow().composition_options(),
769                        self.state.borrow().transform,
770                    ));
771                },
772                #[cfg(feature = "webgl")]
773                OffscreenRenderingContext::WebGL(ref context) => {
774                    let Some(snapshot) = context.get_image_data() else {
775                        return Ok(());
776                    };
777
778                    self.send_canvas_command(CanvasCommand::DrawImage(
779                        snapshot.to_shared(),
780                        dest_rect,
781                        source_rect,
782                        smoothing_enabled,
783                        self.state.borrow().shadow_options(),
784                        self.state.borrow().composition_options(),
785                        self.state.borrow().transform,
786                    ));
787                },
788                #[cfg(feature = "webgl")]
789                OffscreenRenderingContext::WebGL2(ref context) => {
790                    let Some(snapshot) = context.get_image_data() else {
791                        return Ok(());
792                    };
793
794                    self.send_canvas_command(CanvasCommand::DrawImage(
795                        snapshot.to_shared(),
796                        dest_rect,
797                        source_rect,
798                        smoothing_enabled,
799                        self.state.borrow().shadow_options(),
800                        self.state.borrow().composition_options(),
801                        self.state.borrow().transform,
802                    ));
803                },
804                OffscreenRenderingContext::Detached => return Err(Error::InvalidState(None)),
805            }
806        } else {
807            self.send_canvas_command(CanvasCommand::DrawEmptyImage(
808                image_size,
809                dest_rect,
810                source_rect,
811                self.state.borrow().shadow_options(),
812                self.state.borrow().composition_options(),
813                self.state.borrow().transform,
814            ));
815        }
816
817        self.mark_as_dirty(htmlcanvas);
818        Ok(())
819    }
820
821    #[expect(clippy::too_many_arguments)]
822    fn draw_html_canvas_element(
823        &self,
824        canvas: &HTMLCanvasElement,             // source canvas
825        htmlcanvas: Option<&HTMLCanvasElement>, // destination canvas
826        sx: f64,
827        sy: f64,
828        sw: Option<f64>,
829        sh: Option<f64>,
830        dx: f64,
831        dy: f64,
832        dw: Option<f64>,
833        dh: Option<f64>,
834    ) -> ErrorResult {
835        let canvas_size = canvas
836            .context()
837            .map_or_else(|| canvas.get_size(), |context| context.size());
838
839        let dw = dw.unwrap_or(canvas_size.width as f64);
840        let dh = dh.unwrap_or(canvas_size.height as f64);
841        let sw = sw.unwrap_or(canvas_size.width as f64);
842        let sh = sh.unwrap_or(canvas_size.height as f64);
843
844        let image_size = Size2D::new(canvas_size.width, canvas_size.height);
845        // 2. Establish the source and destination rectangles
846        let (source_rect, dest_rect) =
847            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
848
849        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
850            return Ok(());
851        }
852
853        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
854
855        if let Some(context) = canvas.context() {
856            match *context {
857                RenderingContext::Context2d(ref context) => {
858                    self.buffered_sender.flush().unwrap();
859                    context.send_canvas_command_immediate(CanvasCommand::DrawImageInOther(
860                        self.get_canvas_id(),
861                        dest_rect,
862                        source_rect,
863                        smoothing_enabled,
864                        self.state.borrow().shadow_options(),
865                        self.state.borrow().composition_options(),
866                        self.state.borrow().transform,
867                    ));
868                },
869                RenderingContext::BitmapRenderer(ref context) => {
870                    let Some(snapshot) = context.get_image_data() else {
871                        return Ok(());
872                    };
873
874                    self.send_canvas_command(CanvasCommand::DrawImage(
875                        snapshot.to_shared(),
876                        dest_rect,
877                        source_rect,
878                        smoothing_enabled,
879                        self.state.borrow().shadow_options(),
880                        self.state.borrow().composition_options(),
881                        self.state.borrow().transform,
882                    ));
883                },
884                RenderingContext::Placeholder(ref context) => {
885                    let Some(context) = context.context() else {
886                        return Err(Error::InvalidState(None));
887                    };
888                    match *context {
889                        OffscreenRenderingContext::Context2d(ref context) => {
890                            self.buffered_sender.flush().unwrap();
891                            context.send_canvas_command_immediate(CanvasCommand::DrawImageInOther(
892                                self.get_canvas_id(),
893                                dest_rect,
894                                source_rect,
895                                smoothing_enabled,
896                                self.state.borrow().shadow_options(),
897                                self.state.borrow().composition_options(),
898                                self.state.borrow().transform,
899                            ));
900                        },
901                        OffscreenRenderingContext::BitmapRenderer(ref context) => {
902                            let Some(snapshot) = context.get_image_data() else {
903                                return Ok(());
904                            };
905
906                            self.send_canvas_command(CanvasCommand::DrawImage(
907                                snapshot.to_shared(),
908                                dest_rect,
909                                source_rect,
910                                smoothing_enabled,
911                                self.state.borrow().shadow_options(),
912                                self.state.borrow().composition_options(),
913                                self.state.borrow().transform,
914                            ));
915                        },
916                        #[cfg(feature = "webgl")]
917                        OffscreenRenderingContext::WebGL(ref context) => {
918                            let Some(snapshot) = context.get_image_data() else {
919                                return Ok(());
920                            };
921
922                            self.send_canvas_command(CanvasCommand::DrawImage(
923                                snapshot.to_shared(),
924                                dest_rect,
925                                source_rect,
926                                smoothing_enabled,
927                                self.state.borrow().shadow_options(),
928                                self.state.borrow().composition_options(),
929                                self.state.borrow().transform,
930                            ));
931                        },
932
933                        #[cfg(feature = "webgl")]
934                        OffscreenRenderingContext::WebGL2(ref context) => {
935                            let Some(snapshot) = context.get_image_data() else {
936                                return Ok(());
937                            };
938
939                            self.send_canvas_command(CanvasCommand::DrawImage(
940                                snapshot.to_shared(),
941                                dest_rect,
942                                source_rect,
943                                smoothing_enabled,
944                                self.state.borrow().shadow_options(),
945                                self.state.borrow().composition_options(),
946                                self.state.borrow().transform,
947                            ));
948                        },
949                        OffscreenRenderingContext::Detached => {
950                            return Err(Error::InvalidState(None));
951                        },
952                    }
953                },
954                _ => return Err(Error::InvalidState(None)),
955            }
956        } else {
957            self.send_canvas_command(CanvasCommand::DrawEmptyImage(
958                image_size,
959                dest_rect,
960                source_rect,
961                self.state.borrow().shadow_options(),
962                self.state.borrow().composition_options(),
963                self.state.borrow().transform,
964            ));
965        }
966
967        self.mark_as_dirty(htmlcanvas);
968        Ok(())
969    }
970
971    #[expect(clippy::too_many_arguments)]
972    fn fetch_and_draw_image_data(
973        &self,
974        canvas: Option<&HTMLCanvasElement>,
975        url: ServoUrl,
976        cors_setting: Option<CorsSettings>,
977        sx: f64,
978        sy: f64,
979        sw: Option<f64>,
980        sh: Option<f64>,
981        dx: f64,
982        dy: f64,
983        dw: Option<f64>,
984        dh: Option<f64>,
985    ) -> ErrorResult {
986        debug!("Fetching image {}.", url);
987        let snapshot = self
988            .fetch_image_data(url, cors_setting)
989            .ok_or(Error::InvalidState(None))?;
990        let image_size = snapshot.size();
991
992        let dw = dw.unwrap_or(image_size.width as f64);
993        let dh = dh.unwrap_or(image_size.height as f64);
994        let sw = sw.unwrap_or(image_size.width as f64);
995        let sh = sh.unwrap_or(image_size.height as f64);
996
997        // Establish the source and destination rectangles
998        let (source_rect, dest_rect) =
999            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
1000
1001        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
1002            return Ok(());
1003        }
1004
1005        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
1006        self.send_canvas_command(CanvasCommand::DrawImage(
1007            snapshot.to_shared(),
1008            dest_rect,
1009            source_rect,
1010            smoothing_enabled,
1011            self.state.borrow().shadow_options(),
1012            self.state.borrow().composition_options(),
1013            self.state.borrow().transform,
1014        ));
1015        self.mark_as_dirty(canvas);
1016        Ok(())
1017    }
1018
1019    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
1020    #[expect(clippy::too_many_arguments)]
1021    fn draw_image_bitmap(
1022        &self,
1023        bitmap: &ImageBitmap,
1024        canvas: Option<&HTMLCanvasElement>,
1025        sx: f64,
1026        sy: f64,
1027        sw: Option<f64>,
1028        sh: Option<f64>,
1029        dx: f64,
1030        dy: f64,
1031        dw: Option<f64>,
1032        dh: Option<f64>,
1033    ) {
1034        let Some(snapshot) = bitmap.bitmap_data().clone() else {
1035            return;
1036        };
1037
1038        // Step 4. Establish the source and destination rectangles.
1039        let bitmap_size = snapshot.size();
1040        let dw = dw.unwrap_or(bitmap_size.width as f64);
1041        let dh = dh.unwrap_or(bitmap_size.height as f64);
1042        let sw = sw.unwrap_or(bitmap_size.width as f64);
1043        let sh = sh.unwrap_or(bitmap_size.height as f64);
1044
1045        let (source_rect, dest_rect) =
1046            self.adjust_source_dest_rects(bitmap_size, sx, sy, sw, sh, dx, dy, dw, dh);
1047
1048        // Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
1049        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
1050            return;
1051        }
1052
1053        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
1054
1055        self.send_canvas_command(CanvasCommand::DrawImage(
1056            snapshot.to_shared(),
1057            dest_rect,
1058            source_rect,
1059            smoothing_enabled,
1060            self.state.borrow().shadow_options(),
1061            self.state.borrow().composition_options(),
1062            self.state.borrow().transform,
1063        ));
1064
1065        self.mark_as_dirty(canvas);
1066    }
1067
1068    pub(super) fn mark_as_dirty(&self, canvas: Option<&HTMLCanvasElement>) {
1069        if let Some(canvas) = canvas {
1070            canvas.mark_as_dirty();
1071        }
1072    }
1073
1074    /// It is used by DrawImage to calculate the size of the source and destination rectangles based
1075    /// on the drawImage call arguments
1076    /// source rectangle = area of the original image to be copied
1077    /// destination rectangle = area of the destination canvas where the source image is going to be drawn
1078    #[expect(clippy::too_many_arguments)]
1079    fn adjust_source_dest_rects(
1080        &self,
1081        image_size: Size2D<u32>,
1082        sx: f64,
1083        sy: f64,
1084        sw: f64,
1085        sh: f64,
1086        dx: f64,
1087        dy: f64,
1088        dw: f64,
1089        dh: f64,
1090    ) -> (Rect<f64>, Rect<f64>) {
1091        let image_rect = Rect::new(
1092            Point2D::zero(),
1093            Size2D::new(image_size.width, image_size.height),
1094        );
1095
1096        // The source rectangle is the rectangle whose corners are the four points (sx, sy),
1097        // (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
1098        let source_rect = Rect::new(
1099            Point2D::new(sx.min(sx + sw), sy.min(sy + sh)),
1100            Size2D::new(sw.abs(), sh.abs()),
1101        );
1102
1103        // When the source rectangle is outside the source image,
1104        // the source rectangle must be clipped to the source image
1105        let source_rect_clipped = source_rect
1106            .intersection(&image_rect.to_f64())
1107            .unwrap_or(Rect::zero());
1108
1109        // Width and height ratios between the non clipped and clipped source rectangles
1110        let width_ratio: f64 = source_rect_clipped.size.width / source_rect.size.width;
1111        let height_ratio: f64 = source_rect_clipped.size.height / source_rect.size.height;
1112
1113        // When the source rectangle is outside the source image,
1114        // the destination rectangle must be clipped in the same proportion.
1115        let dest_rect_width_scaled: f64 = dw * width_ratio;
1116        let dest_rect_height_scaled: f64 = dh * height_ratio;
1117
1118        // The destination rectangle is the rectangle whose corners are the four points (dx, dy),
1119        // (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
1120        let dest_rect = Rect::new(
1121            Point2D::new(
1122                dx.min(dx + dest_rect_width_scaled),
1123                dy.min(dy + dest_rect_height_scaled),
1124            ),
1125            Size2D::new(dest_rect_width_scaled.abs(), dest_rect_height_scaled.abs()),
1126        );
1127
1128        let source_rect = Rect::new(
1129            Point2D::new(source_rect_clipped.origin.x, source_rect_clipped.origin.y),
1130            Size2D::new(
1131                source_rect_clipped.size.width,
1132                source_rect_clipped.size.height,
1133            ),
1134        );
1135
1136        (source_rect, dest_rect)
1137    }
1138
1139    fn update_transform(&self, transform: Transform2D<f64>) {
1140        let mut state = self.state.borrow_mut();
1141        self.current_default_path
1142            .borrow_mut()
1143            .transform(state.transform.cast());
1144        state.transform = transform;
1145        if let Some(inverse) = transform.inverse() {
1146            self.current_default_path
1147                .borrow_mut()
1148                .transform(inverse.cast());
1149        }
1150    }
1151
1152    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-fillrect>
1153    pub(super) fn fill_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1154        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1155            let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
1156            self.send_canvas_command(CanvasCommand::FillRect(
1157                rect,
1158                style,
1159                self.state.borrow().shadow_options(),
1160                self.state.borrow().composition_options(),
1161                self.state.borrow().transform,
1162            ));
1163        }
1164    }
1165
1166    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-clearrect>
1167    pub(super) fn clear_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1168        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1169            self.send_canvas_command(CanvasCommand::ClearRect(
1170                rect,
1171                self.state.borrow().transform,
1172            ));
1173        }
1174    }
1175
1176    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-strokerect>
1177    pub(super) fn stroke_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1178        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1179            let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
1180            self.send_canvas_command(CanvasCommand::StrokeRect(
1181                rect,
1182                style,
1183                self.state.borrow().line_options(),
1184                self.state.borrow().shadow_options(),
1185                self.state.borrow().composition_options(),
1186                self.state.borrow().transform,
1187            ));
1188        }
1189    }
1190
1191    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsetx>
1192    pub(super) fn shadow_offset_x(&self) -> f64 {
1193        self.state.borrow().shadow_offset_x
1194    }
1195
1196    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsetx>
1197    pub(super) fn set_shadow_offset_x(&self, value: f64) {
1198        if !value.is_finite() || value == self.state.borrow().shadow_offset_x {
1199            return;
1200        }
1201        self.state.borrow_mut().shadow_offset_x = value;
1202    }
1203
1204    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsety>
1205    pub(super) fn shadow_offset_y(&self) -> f64 {
1206        self.state.borrow().shadow_offset_y
1207    }
1208
1209    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsety>
1210    pub(super) fn set_shadow_offset_y(&self, value: f64) {
1211        if !value.is_finite() || value == self.state.borrow().shadow_offset_y {
1212            return;
1213        }
1214        self.state.borrow_mut().shadow_offset_y = value;
1215    }
1216
1217    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowblur>
1218    pub(super) fn shadow_blur(&self) -> f64 {
1219        self.state.borrow().shadow_blur
1220    }
1221
1222    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowblur>
1223    pub(super) fn set_shadow_blur(&self, value: f64) {
1224        if !value.is_finite() || value < 0f64 || value == self.state.borrow().shadow_blur {
1225            return;
1226        }
1227        self.state.borrow_mut().shadow_blur = value;
1228    }
1229
1230    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowcolor>
1231    pub(super) fn shadow_color(&self) -> DOMString {
1232        let mut result = String::new();
1233        serialize(&self.state.borrow().shadow_color, &mut result).unwrap();
1234        DOMString::from(result)
1235    }
1236
1237    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowcolor>
1238    pub(super) fn set_shadow_color(&self, canvas: Option<&HTMLCanvasElement>, value: DOMString) {
1239        if let Ok(rgba) = parse_color(canvas, &value) {
1240            self.state.borrow_mut().shadow_color = rgba;
1241        }
1242    }
1243
1244    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle>
1245    pub(super) fn stroke_style(&self) -> StringOrCanvasGradientOrCanvasPattern {
1246        match self.state.borrow().stroke_style {
1247            CanvasFillOrStrokeStyle::Color(ref rgba) => {
1248                let mut result = String::new();
1249                serialize(rgba, &mut result).unwrap();
1250                StringOrCanvasGradientOrCanvasPattern::String(DOMString::from(result))
1251            },
1252            CanvasFillOrStrokeStyle::Gradient(ref gradient) => {
1253                StringOrCanvasGradientOrCanvasPattern::CanvasGradient(DomRoot::from_ref(gradient))
1254            },
1255            CanvasFillOrStrokeStyle::Pattern(ref pattern) => {
1256                StringOrCanvasGradientOrCanvasPattern::CanvasPattern(DomRoot::from_ref(pattern))
1257            },
1258        }
1259    }
1260
1261    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle>
1262    pub(super) fn set_stroke_style(
1263        &self,
1264        canvas: Option<&HTMLCanvasElement>,
1265        value: StringOrCanvasGradientOrCanvasPattern,
1266    ) {
1267        match value {
1268            StringOrCanvasGradientOrCanvasPattern::String(string) => {
1269                if let Ok(rgba) = parse_color(canvas, &string) {
1270                    self.state.borrow_mut().stroke_style = CanvasFillOrStrokeStyle::Color(rgba);
1271                }
1272            },
1273            StringOrCanvasGradientOrCanvasPattern::CanvasGradient(gradient) => {
1274                self.state.borrow_mut().stroke_style =
1275                    CanvasFillOrStrokeStyle::Gradient(Dom::from_ref(&*gradient));
1276            },
1277            StringOrCanvasGradientOrCanvasPattern::CanvasPattern(pattern) => {
1278                self.state.borrow_mut().stroke_style =
1279                    CanvasFillOrStrokeStyle::Pattern(Dom::from_ref(&*pattern));
1280                if !pattern.origin_is_clean() {
1281                    self.set_origin_clean(false);
1282                }
1283            },
1284        }
1285    }
1286
1287    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle>
1288    pub(super) fn fill_style(&self) -> StringOrCanvasGradientOrCanvasPattern {
1289        match self.state.borrow().fill_style {
1290            CanvasFillOrStrokeStyle::Color(ref rgba) => {
1291                let mut result = String::new();
1292                serialize(rgba, &mut result).unwrap();
1293                StringOrCanvasGradientOrCanvasPattern::String(DOMString::from(result))
1294            },
1295            CanvasFillOrStrokeStyle::Gradient(ref gradient) => {
1296                StringOrCanvasGradientOrCanvasPattern::CanvasGradient(DomRoot::from_ref(gradient))
1297            },
1298            CanvasFillOrStrokeStyle::Pattern(ref pattern) => {
1299                StringOrCanvasGradientOrCanvasPattern::CanvasPattern(DomRoot::from_ref(pattern))
1300            },
1301        }
1302    }
1303
1304    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle>
1305    pub(super) fn set_fill_style(
1306        &self,
1307        canvas: Option<&HTMLCanvasElement>,
1308        value: StringOrCanvasGradientOrCanvasPattern,
1309    ) {
1310        match value {
1311            StringOrCanvasGradientOrCanvasPattern::String(string) => {
1312                if let Ok(rgba) = parse_color(canvas, &string) {
1313                    self.state.borrow_mut().fill_style = CanvasFillOrStrokeStyle::Color(rgba);
1314                }
1315            },
1316            StringOrCanvasGradientOrCanvasPattern::CanvasGradient(gradient) => {
1317                self.state.borrow_mut().fill_style =
1318                    CanvasFillOrStrokeStyle::Gradient(Dom::from_ref(&*gradient));
1319            },
1320            StringOrCanvasGradientOrCanvasPattern::CanvasPattern(pattern) => {
1321                self.state.borrow_mut().fill_style =
1322                    CanvasFillOrStrokeStyle::Pattern(Dom::from_ref(&*pattern));
1323                if !pattern.origin_is_clean() {
1324                    self.set_origin_clean(false);
1325                }
1326            },
1327        }
1328    }
1329
1330    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createlineargradient>
1331    pub(super) fn create_linear_gradient(
1332        &self,
1333        global: &GlobalScope,
1334        cx: &mut JSContext,
1335        x0: Finite<f64>,
1336        y0: Finite<f64>,
1337        x1: Finite<f64>,
1338        y1: Finite<f64>,
1339    ) -> DomRoot<CanvasGradient> {
1340        CanvasGradient::new(
1341            global,
1342            cx,
1343            CanvasGradientStyle::Linear(LinearGradientStyle::new(*x0, *y0, *x1, *y1, Vec::new())),
1344        )
1345    }
1346
1347    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createradialgradient>
1348    #[expect(clippy::too_many_arguments)]
1349    pub(super) fn create_radial_gradient(
1350        &self,
1351        global: &GlobalScope,
1352        cx: &mut JSContext,
1353        x0: Finite<f64>,
1354        y0: Finite<f64>,
1355        r0: Finite<f64>,
1356        x1: Finite<f64>,
1357        y1: Finite<f64>,
1358        r1: Finite<f64>,
1359    ) -> Fallible<DomRoot<CanvasGradient>> {
1360        if *r0 < 0. || *r1 < 0. {
1361            return Err(Error::IndexSize(None));
1362        }
1363
1364        Ok(CanvasGradient::new(
1365            global,
1366            cx,
1367            CanvasGradientStyle::Radial(RadialGradientStyle::new(
1368                *x0,
1369                *y0,
1370                *r0,
1371                *x1,
1372                *y1,
1373                *r1,
1374                Vec::new(),
1375            )),
1376        ))
1377    }
1378
1379    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createpattern>
1380    pub(super) fn create_pattern(
1381        &self,
1382        global: &GlobalScope,
1383        cx: &mut JSContext,
1384        image: CanvasImageSource,
1385        mut repetition: DOMString,
1386    ) -> Fallible<Option<DomRoot<CanvasPattern>>> {
1387        let snapshot = match image {
1388            CanvasImageSource::HTMLImageElement(ref image) => {
1389                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1390                if !image.is_usable()? {
1391                    return Ok(None);
1392                }
1393
1394                image
1395                    .get_raster_image_data()
1396                    .ok_or(Error::InvalidState(None))?
1397            },
1398            CanvasImageSource::HTMLVideoElement(ref video) => {
1399                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1400                if !video.is_usable() {
1401                    return Ok(None);
1402                }
1403
1404                video
1405                    .get_current_frame_data()
1406                    .ok_or(Error::InvalidState(None))?
1407            },
1408            CanvasImageSource::HTMLCanvasElement(ref canvas) => {
1409                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1410                if canvas.get_size().is_empty() {
1411                    return Err(Error::InvalidState(None));
1412                }
1413
1414                canvas.get_image_data().ok_or(Error::InvalidState(None))?
1415            },
1416            CanvasImageSource::ImageBitmap(ref bitmap) => {
1417                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1418                if bitmap.is_detached() {
1419                    return Err(Error::InvalidState(None));
1420                }
1421
1422                bitmap
1423                    .bitmap_data()
1424                    .clone()
1425                    .ok_or(Error::InvalidState(None))?
1426            },
1427            CanvasImageSource::OffscreenCanvas(ref canvas) => {
1428                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1429                if canvas.get_size().is_empty() {
1430                    return Err(Error::InvalidState(None));
1431                }
1432
1433                canvas.get_image_data().ok_or(Error::InvalidState(None))?
1434            },
1435            CanvasImageSource::CSSStyleValue(ref value) => value
1436                .get_url(self.base_url.clone())
1437                .and_then(|url| self.fetch_image_data(url, None))
1438                .ok_or(Error::InvalidState(None))?,
1439        };
1440
1441        if repetition.is_empty() {
1442            repetition.push_str("repeat");
1443        }
1444
1445        if let Ok(rep) = RepetitionStyle::from_str(&repetition.str()) {
1446            let size = snapshot.size();
1447            Ok(Some(CanvasPattern::new(
1448                global,
1449                cx,
1450                snapshot,
1451                size.cast(),
1452                rep,
1453                self.is_origin_clean(image),
1454            )))
1455        } else {
1456            Err(Error::Syntax(None))
1457        }
1458    }
1459
1460    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-save>
1461    pub(super) fn save(&self) {
1462        self.saved_states
1463            .borrow_mut()
1464            .push(self.state.borrow().clone());
1465    }
1466
1467    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
1468    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-restore>
1469    pub(super) fn restore(&self) {
1470        let mut saved_states = self.saved_states.borrow_mut();
1471        if let Some(state) = saved_states.pop() {
1472            let clips_to_pop = self.state.borrow().clips_pushed;
1473            if clips_to_pop != 0 {
1474                self.send_canvas_command(CanvasCommand::PopClips(clips_to_pop));
1475            }
1476            self.state.borrow_mut().clone_from(&state);
1477        }
1478    }
1479
1480    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-globalalpha>
1481    pub(super) fn global_alpha(&self) -> f64 {
1482        self.state.borrow().global_alpha
1483    }
1484
1485    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-globalalpha>
1486    pub(super) fn set_global_alpha(&self, alpha: f64) {
1487        if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
1488            return;
1489        }
1490
1491        self.state.borrow_mut().global_alpha = alpha;
1492    }
1493
1494    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-globalcompositeoperation>
1495    pub(super) fn global_composite_operation(&self) -> DOMString {
1496        match self.state.borrow().global_composition {
1497            CompositionOrBlending::Composition(op) => DOMString::from(op.to_string()),
1498            CompositionOrBlending::Blending(op) => DOMString::from(op.to_string()),
1499        }
1500    }
1501
1502    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-globalcompositeoperation>
1503    pub(super) fn set_global_composite_operation(&self, op_str: DOMString) {
1504        if let Ok(op) = CompositionOrBlending::from_str(&op_str.str()) {
1505            self.state.borrow_mut().global_composition = op;
1506        }
1507    }
1508
1509    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-imagesmoothingenabled>
1510    pub(super) fn image_smoothing_enabled(&self) -> bool {
1511        self.state.borrow().image_smoothing_enabled
1512    }
1513
1514    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-imagesmoothingenabled>
1515    pub(super) fn set_image_smoothing_enabled(&self, value: bool) {
1516        self.state.borrow_mut().image_smoothing_enabled = value;
1517    }
1518
1519    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-filltext>
1520    pub(super) fn fill_text(
1521        &self,
1522        global_scope: &GlobalScope,
1523        canvas: Option<&HTMLCanvasElement>,
1524        text: DOMString,
1525        x: f64,
1526        y: f64,
1527        max_width: Option<f64>,
1528    ) {
1529        // Step 1: If any of the arguments are infinite or NaN, then return.
1530        if !x.is_finite() ||
1531            !y.is_finite() ||
1532            max_width.is_some_and(|max_width| !max_width.is_finite())
1533        {
1534            return;
1535        }
1536
1537        if self.state.borrow().font_style.is_none() {
1538            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into())
1539        }
1540        // This may be `None` if if this is offscreen canvas, in which case just use
1541        // the initial values for the text style.
1542        let size = self.font_style().font_size.computed_size().px() as f64;
1543
1544        let Some((bounds, text_run)) = self.text_with_size(
1545            global_scope,
1546            &text.str(),
1547            Point2D::new(x, y),
1548            size,
1549            max_width,
1550        ) else {
1551            return;
1552        };
1553        self.send_canvas_command(CanvasCommand::FillText(
1554            bounds,
1555            text_run,
1556            self.state.borrow().fill_style.to_fill_or_stroke_style(),
1557            self.state.borrow().shadow_options(),
1558            self.state.borrow().composition_options(),
1559            self.state.borrow().transform,
1560        ));
1561    }
1562
1563    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-stroketext>
1564    pub(super) fn stroke_text(
1565        &self,
1566        global_scope: &GlobalScope,
1567        canvas: Option<&HTMLCanvasElement>,
1568        text: DOMString,
1569        x: f64,
1570        y: f64,
1571        max_width: Option<f64>,
1572    ) {
1573        // Step 1: If any of the arguments are infinite or NaN, then return.
1574        if !x.is_finite() ||
1575            !y.is_finite() ||
1576            max_width.is_some_and(|max_width| !max_width.is_finite())
1577        {
1578            return;
1579        }
1580
1581        if self.state.borrow().font_style.is_none() {
1582            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into())
1583        }
1584        // This may be `None` if if this is offscreen canvas, in which case just use
1585        // the initial values for the text style.
1586        let size = self.font_style().font_size.computed_size().px() as f64;
1587
1588        let Some((bounds, text_run)) = self.text_with_size(
1589            global_scope,
1590            &text.str(),
1591            Point2D::new(x, y),
1592            size,
1593            max_width,
1594        ) else {
1595            return;
1596        };
1597        self.send_canvas_command(CanvasCommand::StrokeText(
1598            bounds,
1599            text_run,
1600            self.state.borrow().stroke_style.to_fill_or_stroke_style(),
1601            self.state.borrow().line_options(),
1602            self.state.borrow().shadow_options(),
1603            self.state.borrow().composition_options(),
1604            self.state.borrow().transform,
1605        ));
1606    }
1607
1608    /// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
1609    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-measuretext>
1610    /// <https://html.spec.whatwg.org/multipage/#textmetrics>
1611    pub(super) fn measure_text(
1612        &self,
1613        global: &GlobalScope,
1614        canvas: Option<&HTMLCanvasElement>,
1615        text: DOMString,
1616        cx: &mut JSContext,
1617    ) -> DomRoot<TextMetrics> {
1618        // > Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.0
1619        // Max width is not provided for `measureText()`.
1620
1621        // > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
1622        let text = replace_ascii_whitespace(&text.str());
1623
1624        // > Step 3: Let font be the current font of target, as given by that object's font
1625        // > attribute.
1626        if self.state.borrow().font_style.is_none() {
1627            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into());
1628        }
1629
1630        let font_context = &global.font_context();
1631        let font_style = self.font_style();
1632        let font_group = font_context.font_group(font_style);
1633        let Some(font) = font_group.first(font_context) else {
1634            warn!("Could not measure canvas text, because there was no first font.");
1635            return TextMetrics::new(global, cx, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.);
1636        };
1637        let ascent = font.metrics().ascent.to_f64_px();
1638        let descent = font.metrics().descent.to_f64_px();
1639        let runs = self.build_unshaped_text_runs(font_context, &text, &font_group);
1640
1641        let mut total_advance = 0.0;
1642        let shaped_runs: Vec<_> = runs
1643            .into_iter()
1644            .filter_map(|unshaped_text_run| {
1645                let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
1646                total_advance += text_run.advance;
1647                Some(text_run)
1648            })
1649            .collect();
1650
1651        let bounding_box = shaped_runs
1652            .iter()
1653            .map(|text_run| text_run.bounds)
1654            .reduce(|a, b| a.union(&b))
1655            .unwrap_or_default();
1656
1657        let baseline = font.baseline().unwrap_or_else(|| FontBaseline {
1658            hanging_baseline: (ascent * HANGING_BASELINE_DEFAULT) as f32,
1659            ideographic_baseline: (-descent * IDEOGRAPHIC_BASELINE_DEFAULT) as f32,
1660            alphabetic_baseline: 0.,
1661        });
1662        let ideographic_baseline = baseline.ideographic_baseline as f64;
1663        let alphabetic_baseline = baseline.alphabetic_baseline as f64;
1664        let hanging_baseline = baseline.hanging_baseline as f64;
1665
1666        let state = self.state.borrow();
1667        let anchor_x = match state.text_align {
1668            CanvasTextAlign::End => total_advance,
1669            CanvasTextAlign::Center => total_advance / 2.,
1670            CanvasTextAlign::Right => total_advance,
1671            _ => 0.,
1672        } as f64;
1673        let anchor_y = match state.text_baseline {
1674            CanvasTextBaseline::Top => ascent,
1675            CanvasTextBaseline::Hanging => hanging_baseline,
1676            CanvasTextBaseline::Ideographic => ideographic_baseline,
1677            CanvasTextBaseline::Middle => (ascent - descent) / 2.,
1678            CanvasTextBaseline::Alphabetic => alphabetic_baseline,
1679            CanvasTextBaseline::Bottom => -descent,
1680        };
1681
1682        TextMetrics::new(
1683            global,
1684            cx,
1685            total_advance as f64,
1686            anchor_x - bounding_box.min_x(),
1687            bounding_box.max_x() - anchor_x,
1688            bounding_box.max_y() - anchor_y,
1689            anchor_y - bounding_box.min_y(),
1690            ascent - anchor_y,
1691            descent + anchor_y,
1692            ascent - anchor_y,
1693            descent + anchor_y,
1694            hanging_baseline - anchor_y,
1695            alphabetic_baseline - anchor_y,
1696            ideographic_baseline - anchor_y,
1697        )
1698    }
1699
1700    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-font>
1701    pub(super) fn set_font(&self, canvas: Option<&HTMLCanvasElement>, value: DOMString) {
1702        let canvas = match canvas {
1703            Some(element) => element,
1704            None => return, // offscreen canvas doesn't have a placeholder canvas
1705        };
1706        let node = canvas.upcast::<Node>();
1707        let window = canvas.owner_window();
1708
1709        let Some(resolved_font_style) = window.resolved_font_style_query(node, String::from(value))
1710        else {
1711            // This will happen when there is a syntax error.
1712            return;
1713        };
1714        self.state.borrow_mut().font_style = Some(resolved_font_style);
1715    }
1716
1717    fn font_style(&self) -> ServoArc<Font> {
1718        self.state
1719            .borrow()
1720            .font_style
1721            .clone()
1722            .unwrap_or_else(|| ServoArc::new(Font::initial_values()))
1723    }
1724
1725    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-font>
1726    pub(super) fn font(&self) -> DOMString {
1727        self.state.borrow().font_style.as_ref().map_or_else(
1728            || CanvasContextState::DEFAULT_FONT_STYLE.into(),
1729            |style| {
1730                let mut result = String::new();
1731                serialize_font(style, &mut result).unwrap();
1732                DOMString::from(result)
1733            },
1734        )
1735    }
1736
1737    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign>
1738    pub(super) fn text_align(&self) -> CanvasTextAlign {
1739        self.state.borrow().text_align
1740    }
1741
1742    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign>
1743    pub(super) fn set_text_align(&self, value: CanvasTextAlign) {
1744        self.state.borrow_mut().text_align = value;
1745    }
1746
1747    pub(super) fn text_baseline(&self) -> CanvasTextBaseline {
1748        self.state.borrow().text_baseline
1749    }
1750
1751    pub(super) fn set_text_baseline(&self, value: CanvasTextBaseline) {
1752        self.state.borrow_mut().text_baseline = value;
1753    }
1754
1755    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-direction>
1756    pub(super) fn direction(&self) -> CanvasDirection {
1757        self.state.borrow().direction
1758    }
1759
1760    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-direction>
1761    pub(super) fn set_direction(&self, value: CanvasDirection) {
1762        self.state.borrow_mut().direction = value;
1763    }
1764
1765    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linewidth>
1766    pub(super) fn line_width(&self) -> f64 {
1767        self.state.borrow().line_width
1768    }
1769
1770    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linewidth>
1771    pub(super) fn set_line_width(&self, width: f64) {
1772        if !width.is_finite() || width <= 0.0 {
1773            return;
1774        }
1775
1776        self.state.borrow_mut().line_width = width;
1777    }
1778
1779    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linecap>
1780    pub(super) fn line_cap(&self) -> CanvasLineCap {
1781        match self.state.borrow().line_cap {
1782            LineCapStyle::Butt => CanvasLineCap::Butt,
1783            LineCapStyle::Round => CanvasLineCap::Round,
1784            LineCapStyle::Square => CanvasLineCap::Square,
1785        }
1786    }
1787
1788    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linecap>
1789    pub(super) fn set_line_cap(&self, cap: CanvasLineCap) {
1790        let line_cap = match cap {
1791            CanvasLineCap::Butt => LineCapStyle::Butt,
1792            CanvasLineCap::Round => LineCapStyle::Round,
1793            CanvasLineCap::Square => LineCapStyle::Square,
1794        };
1795        self.state.borrow_mut().line_cap = line_cap;
1796    }
1797
1798    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linejoin>
1799    pub(super) fn line_join(&self) -> CanvasLineJoin {
1800        match self.state.borrow().line_join {
1801            LineJoinStyle::Round => CanvasLineJoin::Round,
1802            LineJoinStyle::Bevel => CanvasLineJoin::Bevel,
1803            LineJoinStyle::Miter => CanvasLineJoin::Miter,
1804        }
1805    }
1806
1807    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linejoin>
1808    pub(super) fn set_line_join(&self, join: CanvasLineJoin) {
1809        let line_join = match join {
1810            CanvasLineJoin::Round => LineJoinStyle::Round,
1811            CanvasLineJoin::Bevel => LineJoinStyle::Bevel,
1812            CanvasLineJoin::Miter => LineJoinStyle::Miter,
1813        };
1814        self.state.borrow_mut().line_join = line_join;
1815    }
1816
1817    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-miterlimit>
1818    pub(super) fn miter_limit(&self) -> f64 {
1819        self.state.borrow().miter_limit
1820    }
1821
1822    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-miterlimit>
1823    pub(super) fn set_miter_limit(&self, limit: f64) {
1824        if !limit.is_finite() || limit <= 0.0 {
1825            return;
1826        }
1827
1828        self.state.borrow_mut().miter_limit = limit;
1829    }
1830
1831    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-getlinedash>
1832    pub(super) fn line_dash(&self) -> Vec<f64> {
1833        // > return a sequence whose values are the values of
1834        // > the object's dash list, in the same order.
1835        self.state.borrow().line_dash.clone()
1836    }
1837
1838    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-setlinedash>
1839    pub(super) fn set_line_dash(&self, segments: Vec<f64>) {
1840        // > If any value in segments is not finite (e.g. an Infinity or a NaN value),
1841        // > or if any value is negative (less than zero), then return (without throwing
1842        // > an exception; user agents could show a message on a developer console,
1843        // > though, as that would be helpful for debugging).
1844        if segments
1845            .iter()
1846            .any(|segment| !segment.is_finite() || *segment < 0.0)
1847        {
1848            return;
1849        }
1850
1851        // > If the number of elements in segments is odd, then let segments
1852        // > be the concatenation of two copies of segments.
1853        let mut line_dash: Vec<_> = segments.clone();
1854        if segments.len() & 1 == 1 {
1855            line_dash.extend(line_dash.clone());
1856        }
1857
1858        // > Let the object's dash list be segments.
1859        self.state.borrow_mut().line_dash = line_dash.clone();
1860    }
1861
1862    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linedashoffset>
1863    pub(super) fn line_dash_offset(&self) -> f64 {
1864        // > On getting, it must return the current value.
1865        self.state.borrow().line_dash_offset
1866    }
1867
1868    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linedashoffset?
1869    pub(super) fn set_line_dash_offset(&self, offset: f64) {
1870        // > On setting, infinite and NaN values must be ignored,
1871        // > leaving the value unchanged;
1872        if !offset.is_finite() {
1873            return;
1874        }
1875
1876        // > other values must change the current value to the new value.
1877        self.state.borrow_mut().line_dash_offset = offset;
1878    }
1879
1880    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createimagedata>
1881    pub(super) fn create_image_data(
1882        &self,
1883        cx: &mut JSContext,
1884        global: &GlobalScope,
1885        sw: i32,
1886        sh: i32,
1887    ) -> Fallible<DomRoot<ImageData>> {
1888        if sw == 0 || sh == 0 {
1889            return Err(Error::IndexSize(None));
1890        }
1891        ImageData::new(cx, global, sw.unsigned_abs(), sh.unsigned_abs(), None)
1892    }
1893
1894    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createimagedata>
1895    pub(super) fn create_image_data_(
1896        &self,
1897        cx: &mut JSContext,
1898        global: &GlobalScope,
1899        imagedata: &ImageData,
1900    ) -> Fallible<DomRoot<ImageData>> {
1901        ImageData::new(cx, global, imagedata.Width(), imagedata.Height(), None)
1902    }
1903
1904    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-getimagedata>
1905    #[expect(clippy::too_many_arguments)]
1906    pub(super) fn get_image_data(
1907        &self,
1908        cx: &mut JSContext,
1909        canvas_size: Size2D<u32>,
1910        global: &GlobalScope,
1911        sx: i32,
1912        sy: i32,
1913        sw: i32,
1914        sh: i32,
1915    ) -> Fallible<DomRoot<ImageData>> {
1916        // FIXME(nox): There are many arithmetic operations here that can
1917        // overflow or underflow, this should probably be audited.
1918
1919        if sw == 0 || sh == 0 {
1920            return Err(Error::IndexSize(None));
1921        }
1922
1923        if !self.origin_is_clean() {
1924            return Err(Error::Security(None));
1925        }
1926
1927        let (origin, size) = adjust_size_sign(Point2D::new(sx, sy), Size2D::new(sw, sh));
1928        let read_rect = match pixels::clip(origin, size.to_u32(), canvas_size) {
1929            Some(rect) => rect,
1930            None => {
1931                // All the pixels are outside the canvas surface.
1932                return ImageData::new(cx, global, size.width, size.height, None);
1933            },
1934        };
1935
1936        let data = if self.is_paintable() {
1937            let (sender, receiver) = generic_channel::channel().unwrap();
1938            self.send_canvas_command_immediate(CanvasCommand::GetImageData(
1939                Some(read_rect),
1940                sender,
1941            ));
1942
1943            let mut snapshot = receiver.recv().unwrap().to_owned();
1944            snapshot.transform(
1945                SnapshotAlphaMode::Transparent {
1946                    premultiplied: false,
1947                },
1948                SnapshotPixelFormat::RGBA,
1949            );
1950            Some(snapshot.into())
1951        } else {
1952            None
1953        };
1954
1955        ImageData::new(cx, global, size.width, size.height, data)
1956    }
1957
1958    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata>
1959    pub(super) fn put_image_data(
1960        &self,
1961        no_gc: &NoGC,
1962        canvas_size: Size2D<u32>,
1963        imagedata: &ImageData,
1964        dx: i32,
1965        dy: i32,
1966    ) {
1967        self.put_image_data_(
1968            no_gc,
1969            canvas_size,
1970            imagedata,
1971            dx,
1972            dy,
1973            0,
1974            0,
1975            imagedata.Width() as i32,
1976            imagedata.Height() as i32,
1977        )
1978    }
1979
1980    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata>
1981    #[expect(clippy::too_many_arguments)]
1982    pub(super) fn put_image_data_(
1983        &self,
1984        no_gc: &NoGC,
1985        canvas_size: Size2D<u32>,
1986        imagedata: &ImageData,
1987        dx: i32,
1988        dy: i32,
1989        dirty_x: i32,
1990        dirty_y: i32,
1991        dirty_width: i32,
1992        dirty_height: i32,
1993    ) {
1994        if !self.is_paintable() {
1995            return;
1996        }
1997
1998        // FIXME(nox): There are many arithmetic operations here that can
1999        // overflow or underflow, this should probably be audited.
2000
2001        let imagedata_size = Size2D::new(imagedata.Width(), imagedata.Height());
2002        if imagedata_size.area() == 0 {
2003            return;
2004        }
2005
2006        // Step 1.
2007        // Done later.
2008
2009        // Step 2.
2010        // TODO: throw InvalidState if buffer is detached.
2011
2012        // Steps 3-6.
2013        let (src_origin, src_size) = adjust_size_sign(
2014            Point2D::new(dirty_x, dirty_y),
2015            Size2D::new(dirty_width, dirty_height),
2016        );
2017        let src_rect = match pixels::clip(src_origin, src_size.to_u32(), imagedata_size.to_u32()) {
2018            Some(rect) => rect,
2019            None => return,
2020        };
2021        let (dst_origin, _) = adjust_size_sign(
2022            Point2D::new(dirty_x.saturating_add(dx), dirty_y.saturating_add(dy)),
2023            Size2D::new(dirty_width, dirty_height),
2024        );
2025        // By clipping to the canvas surface, we avoid sending any pixel
2026        // that would fall outside it.
2027        let dst_rect = match pixels::clip(dst_origin, src_rect.size, canvas_size) {
2028            Some(rect) => rect,
2029            None => return,
2030        };
2031
2032        // Step 7.
2033        let snapshot =
2034            imagedata.get_snapshot_rect(no_gc, Rect::new(src_rect.origin, dst_rect.size));
2035        // Flushing is not required for correctness, but optimization heuristics for heavy command.
2036        self.send_canvas_command_immediate(CanvasCommand::PutImageData(
2037            dst_rect,
2038            snapshot.to_shared(),
2039        ));
2040    }
2041
2042    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
2043    pub(super) fn draw_image(
2044        &self,
2045        canvas: Option<&HTMLCanvasElement>,
2046        image: CanvasImageSource,
2047        dx: f64,
2048        dy: f64,
2049    ) -> ErrorResult {
2050        if !(dx.is_finite() && dy.is_finite()) {
2051            return Ok(());
2052        }
2053
2054        self.draw_image_internal(canvas, image, 0f64, 0f64, None, None, dx, dy, None, None)
2055    }
2056
2057    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
2058    pub(super) fn draw_image_(
2059        &self,
2060        canvas: Option<&HTMLCanvasElement>,
2061        image: CanvasImageSource,
2062        dx: f64,
2063        dy: f64,
2064        dw: f64,
2065        dh: f64,
2066    ) -> ErrorResult {
2067        if !(dx.is_finite() && dy.is_finite() && dw.is_finite() && dh.is_finite()) {
2068            return Ok(());
2069        }
2070
2071        self.draw_image_internal(
2072            canvas,
2073            image,
2074            0f64,
2075            0f64,
2076            None,
2077            None,
2078            dx,
2079            dy,
2080            Some(dw),
2081            Some(dh),
2082        )
2083    }
2084
2085    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
2086    #[expect(clippy::too_many_arguments)]
2087    pub(super) fn draw_image__(
2088        &self,
2089        canvas: Option<&HTMLCanvasElement>,
2090        image: CanvasImageSource,
2091        sx: f64,
2092        sy: f64,
2093        sw: f64,
2094        sh: f64,
2095        dx: f64,
2096        dy: f64,
2097        dw: f64,
2098        dh: f64,
2099    ) -> ErrorResult {
2100        if !(sx.is_finite() &&
2101            sy.is_finite() &&
2102            sw.is_finite() &&
2103            sh.is_finite() &&
2104            dx.is_finite() &&
2105            dy.is_finite() &&
2106            dw.is_finite() &&
2107            dh.is_finite())
2108        {
2109            return Ok(());
2110        }
2111
2112        self.draw_image_internal(
2113            canvas,
2114            image,
2115            sx,
2116            sy,
2117            Some(sw),
2118            Some(sh),
2119            dx,
2120            dy,
2121            Some(dw),
2122            Some(dh),
2123        )
2124    }
2125
2126    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beginpath>
2127    pub(super) fn begin_path(&self) {
2128        *self.current_default_path.borrow_mut() = Path::new();
2129    }
2130
2131    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-fill>
2132    pub(super) fn fill(&self, fill_rule: CanvasFillRule) {
2133        let path = self.current_default_path.borrow().clone();
2134        self.fill_(path, fill_rule);
2135    }
2136
2137    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-fill>
2138    pub(super) fn fill_(&self, path: Path, fill_rule: CanvasFillRule) {
2139        let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
2140        self.send_canvas_command(CanvasCommand::FillPath(
2141            style,
2142            path,
2143            fill_rule.convert(),
2144            self.state.borrow().shadow_options(),
2145            self.state.borrow().composition_options(),
2146            self.state.borrow().transform,
2147        ));
2148    }
2149
2150    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke>
2151    pub(super) fn stroke(&self) {
2152        let path = self.current_default_path.borrow().clone();
2153        self.stroke_(path);
2154    }
2155
2156    pub(super) fn stroke_(&self, path: Path) {
2157        let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
2158        self.send_canvas_command(CanvasCommand::StrokePath(
2159            path,
2160            style,
2161            self.state.borrow().line_options(),
2162            self.state.borrow().shadow_options(),
2163            self.state.borrow().composition_options(),
2164            self.state.borrow().transform,
2165        ));
2166    }
2167
2168    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-clip>
2169    pub(super) fn clip(&self, fill_rule: CanvasFillRule) {
2170        let path = self.current_default_path.borrow().clone();
2171        self.clip_(path, fill_rule);
2172    }
2173
2174    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-clip>
2175    pub(super) fn clip_(&self, path: Path, fill_rule: CanvasFillRule) {
2176        self.state.borrow_mut().clips_pushed += 1;
2177        self.send_canvas_command(CanvasCommand::ClipPath(
2178            path,
2179            fill_rule.convert(),
2180            self.state.borrow().transform,
2181        ));
2182    }
2183
2184    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath>
2185    pub(super) fn is_point_in_path(
2186        &self,
2187        global: &GlobalScope,
2188        x: f64,
2189        y: f64,
2190        fill_rule: CanvasFillRule,
2191    ) -> bool {
2192        let mut path = self.current_default_path.borrow().clone();
2193        path.transform(self.state.borrow().transform.cast());
2194        self.is_point_in_path_(global, path, x, y, fill_rule)
2195    }
2196
2197    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath>
2198    pub(super) fn is_point_in_path_(
2199        &self,
2200        _global: &GlobalScope,
2201        path: Path,
2202        x: f64,
2203        y: f64,
2204        fill_rule: CanvasFillRule,
2205    ) -> bool {
2206        let fill_rule = match fill_rule {
2207            CanvasFillRule::Nonzero => FillRule::Nonzero,
2208            CanvasFillRule::Evenodd => FillRule::Evenodd,
2209        };
2210        path.is_point_in_path(x, y, fill_rule)
2211    }
2212
2213    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-scale>
2214    pub(super) fn scale(&self, x: f64, y: f64) {
2215        if !(x.is_finite() && y.is_finite()) {
2216            return;
2217        }
2218
2219        let transform = self.state.borrow().transform;
2220        self.update_transform(transform.pre_scale(x, y))
2221    }
2222
2223    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rotate>
2224    pub(super) fn rotate(&self, angle: f64) {
2225        if angle == 0.0 || !angle.is_finite() {
2226            return;
2227        }
2228
2229        let (sin, cos) = (angle.sin(), angle.cos());
2230        let transform = self.state.borrow().transform;
2231        self.update_transform(Transform2D::new(cos, sin, -sin, cos, 0.0, 0.0).then(&transform))
2232    }
2233
2234    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-translate>
2235    pub(super) fn translate(&self, x: f64, y: f64) {
2236        if !(x.is_finite() && y.is_finite()) {
2237            return;
2238        }
2239
2240        let transform = self.state.borrow().transform;
2241        self.update_transform(transform.pre_translate(vec2(x, y)))
2242    }
2243
2244    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-transform>
2245    pub(super) fn transform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
2246        if !(a.is_finite() &&
2247            b.is_finite() &&
2248            c.is_finite() &&
2249            d.is_finite() &&
2250            e.is_finite() &&
2251            f.is_finite())
2252        {
2253            return;
2254        }
2255
2256        let transform = self.state.borrow().transform;
2257        self.update_transform(Transform2D::new(a, b, c, d, e, f).then(&transform))
2258    }
2259
2260    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-gettransform>
2261    pub(super) fn get_transform(
2262        &self,
2263        global: &GlobalScope,
2264        cx: &mut JSContext,
2265    ) -> DomRoot<DOMMatrix> {
2266        let transform = self.state.safe_borrow_mut(cx.no_gc()).transform;
2267        DOMMatrix::new(cx, global, true, transform.to_3d())
2268    }
2269
2270    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
2271    pub(super) fn set_transform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
2272        // Step 1. If any of the arguments are infinite or NaN, then return.
2273        if !a.is_finite() ||
2274            !b.is_finite() ||
2275            !c.is_finite() ||
2276            !d.is_finite() ||
2277            !e.is_finite() ||
2278            !f.is_finite()
2279        {
2280            return;
2281        }
2282
2283        // Step 2. Reset the current transformation matrix to the matrix described by:
2284        self.update_transform(Transform2D::new(a, b, c, d, e, f))
2285    }
2286
2287    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
2288    pub(super) fn set_transform_(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
2289        // Step 1. Let matrix be the result of creating a DOMMatrix from the 2D
2290        // dictionary transform.
2291        let matrix = dommatrix2dinit_to_matrix(transform)?;
2292
2293        // Step 2. If one or more of matrix's m11 element, m12 element, m21
2294        // element, m22 element, m41 element, or m42 element are infinite or
2295        // NaN, then return.
2296        if !matrix.m11.is_finite() ||
2297            !matrix.m12.is_finite() ||
2298            !matrix.m21.is_finite() ||
2299            !matrix.m22.is_finite() ||
2300            !matrix.m31.is_finite() ||
2301            !matrix.m32.is_finite()
2302        {
2303            return Ok(());
2304        }
2305
2306        // Step 3. Reset the current transformation matrix to matrix.
2307        self.update_transform(matrix.cast());
2308        Ok(())
2309    }
2310
2311    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform>
2312    pub(super) fn reset_transform(&self) {
2313        self.update_transform(Transform2D::identity())
2314    }
2315
2316    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath>
2317    pub(super) fn close_path(&self) {
2318        self.current_default_path.borrow_mut().close_path();
2319    }
2320
2321    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto>
2322    pub(super) fn move_to(&self, x: f64, y: f64) {
2323        self.current_default_path.borrow_mut().move_to(x, y);
2324    }
2325
2326    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto>
2327    pub(super) fn line_to(&self, x: f64, y: f64) {
2328        self.current_default_path.borrow_mut().line_to(x, y);
2329    }
2330
2331    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-rect>
2332    pub(super) fn rect(&self, x: f64, y: f64, width: f64, height: f64) {
2333        self.current_default_path
2334            .borrow_mut()
2335            .rect(x, y, width, height);
2336    }
2337
2338    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-roundrect>
2339    pub(super) fn round_rect(
2340        &self,
2341        x: f64,
2342        y: f64,
2343        width: f64,
2344        height: f64,
2345        radii: UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence,
2346    ) -> ErrorResult {
2347        let radii = round_rect_radii(&radii);
2348        self.current_default_path
2349            .borrow_mut()
2350            .round_rect(x, y, width, height, &radii)
2351            .map_err(round_rect_error)
2352    }
2353
2354    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto>
2355    pub(super) fn quadratic_curve_to(&self, cpx: f64, cpy: f64, x: f64, y: f64) {
2356        self.current_default_path
2357            .borrow_mut()
2358            .quadratic_curve_to(cpx, cpy, x, y);
2359    }
2360
2361    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto>
2362    pub(super) fn bezier_curve_to(
2363        &self,
2364        cp1x: f64,
2365        cp1y: f64,
2366        cp2x: f64,
2367        cp2y: f64,
2368        x: f64,
2369        y: f64,
2370    ) {
2371        self.current_default_path
2372            .borrow_mut()
2373            .bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y);
2374    }
2375
2376    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arc>
2377    pub(super) fn arc(
2378        &self,
2379        x: f64,
2380        y: f64,
2381        r: f64,
2382        start: f64,
2383        end: f64,
2384        ccw: bool,
2385    ) -> ErrorResult {
2386        self.current_default_path
2387            .borrow_mut()
2388            .arc(x, y, r, start, end, ccw)
2389            .map_err(|_| Error::IndexSize(None))
2390    }
2391
2392    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto>
2393    pub(super) fn arc_to(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, r: f64) -> ErrorResult {
2394        self.current_default_path
2395            .borrow_mut()
2396            .arc_to(cp1x, cp1y, cp2x, cp2y, r)
2397            .map_err(|_| Error::IndexSize(None))
2398    }
2399
2400    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
2401    #[expect(clippy::too_many_arguments)]
2402    pub(super) fn ellipse(
2403        &self,
2404        x: f64,
2405        y: f64,
2406        rx: f64,
2407        ry: f64,
2408        rotation: f64,
2409        start: f64,
2410        end: f64,
2411        ccw: bool,
2412    ) -> ErrorResult {
2413        self.current_default_path
2414            .borrow_mut()
2415            .ellipse(x, y, rx, ry, rotation, start, end, ccw)
2416            .map_err(|_| Error::IndexSize(None))
2417    }
2418
2419    fn text_with_size(
2420        &self,
2421        global_scope: &GlobalScope,
2422        text: &str,
2423        origin: Point2D<f64>,
2424        size: f64,
2425        max_width: Option<f64>,
2426    ) -> Option<(Rect<f64>, Vec<TextRun>)> {
2427        let font_context = &global_scope.font_context();
2428
2429        // Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
2430        if max_width.is_some_and(|max_width| max_width.is_nan() || max_width <= 0.) {
2431            return None;
2432        }
2433
2434        // > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
2435        let text = replace_ascii_whitespace(text);
2436
2437        // > Step 3: Let font be the current font of target, as given by that object's font
2438        // > attribute.
2439        let font_style = self.font_style();
2440        let font_group = font_context.font_group_with_size(font_style, Au::from_f64_px(size));
2441        let Some(first_font) = font_group.first(font_context) else {
2442            warn!("Could not render canvas text, because there was no first font.");
2443            return None;
2444        };
2445
2446        let runs = self.build_unshaped_text_runs(font_context, &text, &font_group);
2447
2448        // TODO: This doesn't do any kind of line layout at all. In particular, there needs
2449        // to be some alignment along a baseline and also support for bidi text.
2450        let mut total_advance = 0.0;
2451        let mut shaped_runs: Vec<_> = runs
2452            .into_iter()
2453            .filter_map(|unshaped_text_run| {
2454                let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
2455                total_advance += text_run.advance;
2456                Some(text_run)
2457            })
2458            .collect();
2459
2460        // > Step 6: If maxWidth was provided and the hypothetical width of the inline box in the
2461        // > hypothetical line box is greater than maxWidth CSS pixels, then change font to have a
2462        // > more condensed font (if one is available or if a reasonably readable one can be
2463        // > synthesized by applying a horizontal scale factor to the font) or a smaller font, and
2464        // > return to the previous step.
2465        //
2466        // TODO: We only try decreasing the font size here. Eventually it would make sense to use
2467        // other methods to try to decrease the size, such as finding a narrower font or decreasing
2468        // spacing.
2469        let total_advance = total_advance as f64;
2470        if let Some(max_width) = max_width {
2471            let new_size = (max_width / total_advance * size).floor().max(5.);
2472            if total_advance > max_width && new_size != size {
2473                return self.text_with_size(global_scope, &text, origin, new_size, Some(max_width));
2474            }
2475        }
2476
2477        // > Step 7: Find the anchor point for the line of text.
2478        let start =
2479            self.find_anchor_point_for_line_of_text(origin, first_font.metrics(), total_advance);
2480
2481        // > Step 8: Let result be an array constructed by iterating over each glyph in the inline box
2482        // > from left to right (if any), adding to the array, for each glyph, the shape of the glyph
2483        // > as it is in the inline box, positioned on a coordinate space using CSS pixels with its
2484        // > origin is at the anchor point.
2485        let mut bounds = None;
2486        for text_run in shaped_runs.iter_mut() {
2487            for glyph_and_position in text_run.glyphs_and_positions.iter_mut() {
2488                glyph_and_position.point += Vector2D::new(start.x as f32, start.y as f32);
2489            }
2490            bounds
2491                .get_or_insert(text_run.bounds)
2492                .union(&text_run.bounds);
2493        }
2494
2495        Some((
2496            bounds
2497                .unwrap_or_default()
2498                .translate(start.to_vector().cast_unit()),
2499            shaped_runs,
2500        ))
2501    }
2502
2503    fn build_unshaped_text_runs<'text>(
2504        &self,
2505        font_context: &FontContext,
2506        text: &'text str,
2507        font_group: &FontGroup,
2508    ) -> Vec<UnshapedTextRun<'text>> {
2509        let mut runs = Vec::new();
2510        // TODO: canvas also has experimental `lang` attribute (https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lang),
2511        // which Servo doesn't support yet. When this attribute is supported, some changes may be needed here.
2512        let x_language = self.font_style()._x_lang.clone();
2513        let language = x_language.0.parse().unwrap_or(Language::UNKNOWN);
2514        let mut current_text_run = UnshapedTextRun::new(language);
2515        let mut current_text_run_start_index = 0;
2516
2517        // Variation Selectors (U+FE00–U+FE0F) and Variation Selectors Supplement (U+E0100–U+E01EF)
2518        // are used to select specific glyph variants (e.g., text vs. emoji presentation).
2519        // They must be attached to the preceding base character and do not start new runs.
2520        // - VS1–VS16 (U+FE00..U+FE0F) in the Variation Selectors block.
2521        // - VS17–VS256 (U+E0100..U+E01EF) in the Variation Selectors Supplement block.
2522        fn is_variation_selector(c: char) -> bool {
2523            matches!(c as u32, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
2524        }
2525
2526        for (index, character) in text.char_indices() {
2527            let next_char = text[index + character.len_utf8()..].chars().next();
2528
2529            let script = Script::from(character);
2530
2531            let font = font_group.find_by_codepoint(font_context, character, next_char, language);
2532
2533            if !is_variation_selector(character) &&
2534                !current_text_run.script_and_font_compatible(script, &font)
2535            {
2536                let previous_text_run = std::mem::replace(
2537                    &mut current_text_run,
2538                    UnshapedTextRun {
2539                        font: font.clone(),
2540                        script,
2541                        string: Default::default(),
2542                        language,
2543                    },
2544                );
2545                current_text_run_start_index = index;
2546                if !previous_text_run.string.is_empty() && previous_text_run.font.is_some() {
2547                    runs.push(previous_text_run);
2548                }
2549            }
2550
2551            current_text_run.string =
2552                &text[current_text_run_start_index..index + character.len_utf8()];
2553        }
2554
2555        if !current_text_run.string.is_empty() && current_text_run.font.is_some() {
2556            runs.push(current_text_run);
2557        }
2558        runs
2559    }
2560
2561    /// Find the *anchor_point* for the given parameters of a line of text.
2562    /// See <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>.
2563    fn find_anchor_point_for_line_of_text(
2564        &self,
2565        origin: Point2D<f64>,
2566        metrics: &FontMetrics,
2567        width: f64,
2568    ) -> Point2D<f64> {
2569        let state = self.state.borrow();
2570        let is_rtl = match state.direction {
2571            CanvasDirection::Ltr => false,
2572            CanvasDirection::Rtl => true,
2573            CanvasDirection::Inherit => false, // TODO: resolve direction wrt to canvas element
2574        };
2575
2576        let text_align = match self.text_align() {
2577            CanvasTextAlign::Start if is_rtl => CanvasTextAlign::Right,
2578            CanvasTextAlign::Start => CanvasTextAlign::Left,
2579            CanvasTextAlign::End if is_rtl => CanvasTextAlign::Left,
2580            CanvasTextAlign::End => CanvasTextAlign::Right,
2581            text_align => text_align,
2582        };
2583        let anchor_x = match text_align {
2584            CanvasTextAlign::Center => -width / 2.,
2585            CanvasTextAlign::Right => -width,
2586            _ => 0.,
2587        };
2588
2589        let ascent = metrics.ascent.to_f64_px();
2590        let descent = metrics.descent.to_f64_px();
2591        let anchor_y = match self.text_baseline() {
2592            CanvasTextBaseline::Top => ascent,
2593            CanvasTextBaseline::Hanging => ascent * HANGING_BASELINE_DEFAULT,
2594            CanvasTextBaseline::Ideographic => -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
2595            CanvasTextBaseline::Middle => (ascent - descent) / 2.,
2596            CanvasTextBaseline::Alphabetic => 0.,
2597            CanvasTextBaseline::Bottom => -descent,
2598        };
2599
2600        origin + Vector2D::new(anchor_x, anchor_y)
2601    }
2602}
2603
2604impl Drop for CanvasState {
2605    fn drop(&mut self) {
2606        // Discard buffered commands before closing the canvas.
2607        self.buffered_sender.discard();
2608        if let Err(err) = self.buffered_sender.send_immediate(CanvasCommand::Destroy) {
2609            warn!("Could not close canvas: {}", err)
2610        }
2611    }
2612}
2613
2614struct UnshapedTextRun<'a> {
2615    font: Option<FontRef>,
2616    script: Script,
2617    string: &'a str,
2618    language: Language,
2619}
2620
2621impl UnshapedTextRun<'_> {
2622    fn new(language: Language) -> Self {
2623        Self {
2624            font: Default::default(),
2625            script: Default::default(),
2626            string: Default::default(),
2627            language,
2628        }
2629    }
2630
2631    fn script_and_font_compatible(&self, script: Script, other_font: &Option<FontRef>) -> bool {
2632        if self.script != script {
2633            return false;
2634        }
2635
2636        match (&self.font, other_font) {
2637            (Some(font_a), Some(font_b)) => *font_a.identifier() == *font_b.identifier(),
2638            (None, None) => true,
2639            _ => false,
2640        }
2641    }
2642
2643    fn into_shaped_text_run(self, previous_advance: f32) -> Option<TextRun> {
2644        debug_assert!(!self.string.is_empty() && self.font.is_some());
2645        let font = self.font?;
2646
2647        let options = ShapingOptions {
2648            letter_spacing: Au::zero(),
2649            word_spacing: Au::zero(),
2650            script: self.script,
2651            language: self.language,
2652            flags: ShapingFlags::empty(),
2653            ligatures: FontVariantLigatures::NORMAL,
2654            numeric: FontVariantNumeric::NORMAL,
2655            east_asian: FontVariantEastAsian::NORMAL,
2656            feature_settings: FontFeatureSettings::normal(),
2657            position: FontVariantPosition::Normal,
2658            alternates: Default::default(),
2659        };
2660
2661        let glyphs = font.shape_text(self.string, &options);
2662
2663        let mut advance = 0.0;
2664        let mut bounds = None;
2665        let glyphs_and_positions = glyphs
2666            .glyphs()
2667            .map(|glyph| {
2668                let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
2669                let glyph_and_position = GlyphAndPosition {
2670                    id: glyph.id(),
2671                    point: Point2D::new(previous_advance + advance, glyph_offset.y.to_f32_px()),
2672                };
2673
2674                let glyph_bounds = font
2675                    .typographic_bounds(glyph.id())
2676                    .translate(Vector2D::new(advance + previous_advance, 0.0));
2677                bounds = Some(bounds.get_or_insert(glyph_bounds).union(&glyph_bounds));
2678
2679                advance += glyph.advance().to_f32_px();
2680
2681                glyph_and_position
2682            })
2683            .collect();
2684
2685        let identifier = font.identifier();
2686        let font_data = match &*identifier {
2687            FontIdentifier::Local(_) => None,
2688            FontIdentifier::Web(_) | FontIdentifier::ArrayBuffer(_) => {
2689                Some(font.font_data_and_index().ok()?)
2690            },
2691        }
2692        .cloned();
2693        let canvas_font = CanvasFont {
2694            identifier: identifier.to_owned(),
2695            data: font_data,
2696        };
2697
2698        Some(TextRun {
2699            font: canvas_font,
2700            pt_size: font.descriptor.pt_size.to_f32_px(),
2701            glyphs_and_positions,
2702            advance,
2703            bounds: bounds.unwrap_or_default().cast(),
2704        })
2705    }
2706}
2707
2708/// <https://drafts.csswg.org/css-color/#parse-a-css-color-value>
2709pub(super) fn parse_color(
2710    canvas: Option<&HTMLCanvasElement>,
2711    string: &DOMString,
2712) -> Result<AbsoluteColor, ()> {
2713    let string = string.str();
2714    let mut parser = Parser::new(&string);
2715    let context = parser_context_for_anonymous_content(
2716        CssRuleType::Style,
2717        ParsingMode::DEFAULT,
2718        &ANONYMOUS_CONTENT_URL_DATA,
2719    );
2720
2721    // Step 1. Parse input as a <color>. If the result is failure, return failure;
2722    // otherwise, let color be the result.
2723    Color::parse_and_compute(&context, &mut parser, None).map(|color| {
2724        // Step 2. Let used color be the result of resolving color to a used color.
2725        // If the value of other properties on the element a <color> is on is required to do the
2726        // resolution (such as resolving a currentcolor or system color), use element if it was
2727        // passed, or the initial values of the properties if not.
2728        //
2729        // Fast path: if the parsed color is already absolute we can return immediately.
2730        if let ComputedColor::Absolute(color) = color {
2731            return color;
2732        }
2733
2734        // TODO: https://github.com/whatwg/html/issues/1099
2735        // Reconsider how to calculate currentColor in a display:none canvas
2736
2737        // TODO: will need to check that the context bitmap mode is fixed
2738        // once we implement CanvasProxy
2739        let current_color = match canvas {
2740            // https://drafts.css-houdini.org/css-paint-api/#2d-rendering-context
2741            // Whenever "currentColor" is used as a color in the PaintRenderingContext2D API,
2742            // it is treated as opaque black.
2743            None => AbsoluteColor::BLACK,
2744            Some(canvas) => {
2745                let canvas_element = canvas.upcast::<Element>();
2746                match canvas_element.style() {
2747                    Some(ref s) if canvas_element.has_css_layout_box() => {
2748                        s.get_inherited_text().color
2749                    },
2750                    _ => AbsoluteColor::BLACK,
2751                }
2752            },
2753        };
2754
2755        color.resolve_to_absolute(&current_color)
2756    })
2757}
2758
2759// Used by drawImage to determine if a source or destination rectangle is valid
2760// Origin coordinates and size cannot be negative. Size has to be greater than zero
2761pub(super) fn is_rect_valid(rect: Rect<f64>) -> bool {
2762    rect.size.width > 0.0 && rect.size.height > 0.0
2763}
2764
2765/// <https://html.spec.whatwg.org/multipage/#serialisation-of-a-color>
2766pub(super) fn serialize<W>(color: &AbsoluteColor, dest: &mut W) -> fmt::Result
2767where
2768    W: fmt::Write,
2769{
2770    let srgb = match color.color_space {
2771        ColorSpace::Srgb if color.flags.contains(ColorFlags::IS_LEGACY_SRGB) => *color,
2772        ColorSpace::Hsl | ColorSpace::Hwb => color.into_srgb_legacy(),
2773        _ => return color.to_css(&mut CssWriter::new(dest)),
2774    };
2775    debug_assert!(srgb.flags.contains(ColorFlags::IS_LEGACY_SRGB));
2776    let red = clamp_unit_f32(srgb.components.0);
2777    let green = clamp_unit_f32(srgb.components.1);
2778    let blue = clamp_unit_f32(srgb.components.2);
2779    let alpha = srgb.alpha;
2780    if alpha == 1.0 {
2781        write!(
2782            dest,
2783            "#{:x}{:x}{:x}{:x}{:x}{:x}",
2784            red >> 4,
2785            red & 0xF,
2786            green >> 4,
2787            green & 0xF,
2788            blue >> 4,
2789            blue & 0xF
2790        )
2791    } else {
2792        write!(dest, "rgba({}, {}, {}, {})", red, green, blue, alpha)
2793    }
2794}
2795
2796pub(super) fn adjust_size_sign(
2797    mut origin: Point2D<i32>,
2798    mut size: Size2D<i32>,
2799) -> (Point2D<i32>, Size2D<u32>) {
2800    if size.width < 0 {
2801        size.width = -size.width;
2802        origin.x = origin.x.saturating_sub(size.width);
2803    }
2804    if size.height < 0 {
2805        size.height = -size.height;
2806        origin.y = origin.y.saturating_sub(size.height);
2807    }
2808    (origin, size.to_u32())
2809}
2810
2811fn serialize_font<W>(style: &Font, dest: &mut W) -> fmt::Result
2812where
2813    W: fmt::Write,
2814{
2815    if style.font_style == FontStyle::ITALIC {
2816        write!(dest, "{} ", style.font_style.to_css_string())?;
2817    }
2818    if style.font_weight.is_bold() {
2819        write!(dest, "{} ", style.font_weight.to_css_string())?;
2820    }
2821    if style.font_variant_caps == FontVariantCaps::SmallCaps {
2822        write!(dest, "{} ", style.font_variant_caps.to_css_string())?;
2823    }
2824    write!(
2825        dest,
2826        "{} {}",
2827        style.font_size.to_css_string(),
2828        style.font_family.to_css_string()
2829    )
2830}
2831
2832fn adjust_canvas_size(size: Size2D<u64>) -> Size2D<u64> {
2833    // Firefox limits width/height to 32767 pixels and Chromium to 65535 pixels,
2834    // but slows down dramatically before it reaches that limit.
2835    // We limit by area instead, giving us larger maximum dimensions,
2836    // in exchange for a smaller maximum canvas size.
2837    const MAX_CANVAS_AREA: u64 = 32768 * 8192;
2838    // Max width/height to 65535 in CSS pixels.
2839    const MAX_CANVAS_SIZE: u64 = 65535;
2840
2841    if !size.is_empty() &&
2842        size.greater_than(Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE))
2843            .none() &&
2844        size.area() < MAX_CANVAS_AREA
2845    {
2846        size
2847    } else {
2848        Size2D::zero()
2849    }
2850}
2851
2852impl Convert<FillRule> for CanvasFillRule {
2853    fn convert(self) -> FillRule {
2854        match self {
2855            CanvasFillRule::Nonzero => FillRule::Nonzero,
2856            CanvasFillRule::Evenodd => FillRule::Evenodd,
2857        }
2858    }
2859}
2860
2861fn replace_ascii_whitespace(text: &str) -> String {
2862    text.chars()
2863        .map(|c| match c {
2864            ' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
2865            _ => c,
2866        })
2867        .collect()
2868}