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