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