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