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

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::fmt;
7use std::str::FromStr;
8use std::sync::Arc;
9
10use app_units::Au;
11use cssparser::color::clamp_unit_f32;
12use cssparser::{Parser, ParserInput};
13use euclid::default::{Point2D, Rect, Size2D, Transform2D};
14use euclid::{Vector2D, vec2};
15use fonts::{
16    FontBaseline, FontContext, FontGroup, FontIdentifier, FontMetrics, FontRef, ShapingFlags,
17    ShapingOptions,
18};
19use icu_locid::subtags::Language;
20use js::context::JSContext;
21use net_traits::image_cache::{ImageCache, ImageResponse};
22use net_traits::request::CorsSettings;
23use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
24use servo_arc::Arc as ServoArc;
25use servo_base::generic_channel::GenericSender;
26use servo_base::{Epoch, generic_channel};
27use servo_canvas_traits::canvas::{
28    Canvas2dMsg, CanvasFont, CanvasId, CanvasMsg, CompositionOptions, CompositionOrBlending,
29    FillOrStrokeStyle, FillRule, GlyphAndPosition, LineCapStyle, LineJoinStyle, LineOptions,
30    LinearGradientStyle, Path, RadialGradientStyle, RepetitionStyle, ShadowOptions, TextRun,
31};
32use servo_constellation_traits::ScriptToConstellationMessage;
33use servo_url::{ImmutableOrigin, ServoUrl};
34use style::color::{AbsoluteColor, ColorFlags, ColorSpace};
35use style::properties::longhands::font_variant_caps::computed_value::T as FontVariantCaps;
36use style::properties::style_structs::Font;
37use style::stylesheets::CssRuleType;
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::WebGL(ref context) => {
703                    let Some(snapshot) = context.get_image_data() else {
704                        return Ok(());
705                    };
706
707                    self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
708                        snapshot.to_shared(),
709                        dest_rect,
710                        source_rect,
711                        smoothing_enabled,
712                        self.state.borrow().shadow_options(),
713                        self.state.borrow().composition_options(),
714                        self.state.borrow().transform,
715                    ));
716                },
717
718                OffscreenRenderingContext::WebGL2(ref context) => {
719                    let Some(snapshot) = context.get_image_data() else {
720                        return Ok(());
721                    };
722
723                    self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
724                        snapshot.to_shared(),
725                        dest_rect,
726                        source_rect,
727                        smoothing_enabled,
728                        self.state.borrow().shadow_options(),
729                        self.state.borrow().composition_options(),
730                        self.state.borrow().transform,
731                    ));
732                },
733                OffscreenRenderingContext::Detached => return Err(Error::InvalidState(None)),
734            }
735        } else {
736            self.send_canvas_2d_msg(Canvas2dMsg::DrawEmptyImage(
737                image_size,
738                dest_rect,
739                source_rect,
740                self.state.borrow().shadow_options(),
741                self.state.borrow().composition_options(),
742                self.state.borrow().transform,
743            ));
744        }
745
746        self.mark_as_dirty(htmlcanvas);
747        Ok(())
748    }
749
750    #[expect(clippy::too_many_arguments)]
751    fn draw_html_canvas_element(
752        &self,
753        canvas: &HTMLCanvasElement,             // source canvas
754        htmlcanvas: Option<&HTMLCanvasElement>, // destination canvas
755        sx: f64,
756        sy: f64,
757        sw: Option<f64>,
758        sh: Option<f64>,
759        dx: f64,
760        dy: f64,
761        dw: Option<f64>,
762        dh: Option<f64>,
763    ) -> ErrorResult {
764        let canvas_size = canvas
765            .context()
766            .map_or_else(|| canvas.get_size(), |context| context.size());
767
768        let dw = dw.unwrap_or(canvas_size.width as f64);
769        let dh = dh.unwrap_or(canvas_size.height as f64);
770        let sw = sw.unwrap_or(canvas_size.width as f64);
771        let sh = sh.unwrap_or(canvas_size.height as f64);
772
773        let image_size = Size2D::new(canvas_size.width, canvas_size.height);
774        // 2. Establish the source and destination rectangles
775        let (source_rect, dest_rect) =
776            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
777
778        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
779            return Ok(());
780        }
781
782        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
783
784        if let Some(context) = canvas.context() {
785            match *context {
786                RenderingContext::Context2d(ref context) => {
787                    context.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                },
797                RenderingContext::BitmapRenderer(ref context) => {
798                    let Some(snapshot) = context.get_image_data() else {
799                        return Ok(());
800                    };
801
802                    self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
803                        snapshot.to_shared(),
804                        dest_rect,
805                        source_rect,
806                        smoothing_enabled,
807                        self.state.borrow().shadow_options(),
808                        self.state.borrow().composition_options(),
809                        self.state.borrow().transform,
810                    ));
811                },
812                RenderingContext::Placeholder(ref context) => {
813                    let Some(context) = context.context() else {
814                        return Err(Error::InvalidState(None));
815                    };
816                    match *context {
817                        OffscreenRenderingContext::Context2d(ref context) => context
818                            .send_canvas_2d_msg(Canvas2dMsg::DrawImageInOther(
819                                self.get_canvas_id(),
820                                dest_rect,
821                                source_rect,
822                                smoothing_enabled,
823                                self.state.borrow().shadow_options(),
824                                self.state.borrow().composition_options(),
825                                self.state.borrow().transform,
826                            )),
827                        OffscreenRenderingContext::BitmapRenderer(ref context) => {
828                            let Some(snapshot) = context.get_image_data() else {
829                                return Ok(());
830                            };
831
832                            self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
833                                snapshot.to_shared(),
834                                dest_rect,
835                                source_rect,
836                                smoothing_enabled,
837                                self.state.borrow().shadow_options(),
838                                self.state.borrow().composition_options(),
839                                self.state.borrow().transform,
840                            ));
841                        },
842                        OffscreenRenderingContext::WebGL(ref context) => {
843                            let Some(snapshot) = context.get_image_data() else {
844                                return Ok(());
845                            };
846
847                            self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
848                                snapshot.to_shared(),
849                                dest_rect,
850                                source_rect,
851                                smoothing_enabled,
852                                self.state.borrow().shadow_options(),
853                                self.state.borrow().composition_options(),
854                                self.state.borrow().transform,
855                            ));
856                        },
857
858                        OffscreenRenderingContext::WebGL2(ref context) => {
859                            let Some(snapshot) = context.get_image_data() else {
860                                return Ok(());
861                            };
862
863                            self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
864                                snapshot.to_shared(),
865                                dest_rect,
866                                source_rect,
867                                smoothing_enabled,
868                                self.state.borrow().shadow_options(),
869                                self.state.borrow().composition_options(),
870                                self.state.borrow().transform,
871                            ));
872                        },
873                        OffscreenRenderingContext::Detached => {
874                            return Err(Error::InvalidState(None));
875                        },
876                    }
877                },
878                _ => return Err(Error::InvalidState(None)),
879            }
880        } else {
881            self.send_canvas_2d_msg(Canvas2dMsg::DrawEmptyImage(
882                image_size,
883                dest_rect,
884                source_rect,
885                self.state.borrow().shadow_options(),
886                self.state.borrow().composition_options(),
887                self.state.borrow().transform,
888            ));
889        }
890
891        self.mark_as_dirty(htmlcanvas);
892        Ok(())
893    }
894
895    #[expect(clippy::too_many_arguments)]
896    fn fetch_and_draw_image_data(
897        &self,
898        canvas: Option<&HTMLCanvasElement>,
899        url: ServoUrl,
900        cors_setting: Option<CorsSettings>,
901        sx: f64,
902        sy: f64,
903        sw: Option<f64>,
904        sh: Option<f64>,
905        dx: f64,
906        dy: f64,
907        dw: Option<f64>,
908        dh: Option<f64>,
909    ) -> ErrorResult {
910        debug!("Fetching image {}.", url);
911        let snapshot = self
912            .fetch_image_data(url, cors_setting)
913            .ok_or(Error::InvalidState(None))?;
914        let image_size = snapshot.size();
915
916        let dw = dw.unwrap_or(image_size.width as f64);
917        let dh = dh.unwrap_or(image_size.height as f64);
918        let sw = sw.unwrap_or(image_size.width as f64);
919        let sh = sh.unwrap_or(image_size.height as f64);
920
921        // Establish the source and destination rectangles
922        let (source_rect, dest_rect) =
923            self.adjust_source_dest_rects(image_size, sx, sy, sw, sh, dx, dy, dw, dh);
924
925        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
926            return Ok(());
927        }
928
929        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
930        self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
931            snapshot.to_shared(),
932            dest_rect,
933            source_rect,
934            smoothing_enabled,
935            self.state.borrow().shadow_options(),
936            self.state.borrow().composition_options(),
937            self.state.borrow().transform,
938        ));
939        self.mark_as_dirty(canvas);
940        Ok(())
941    }
942
943    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
944    #[expect(clippy::too_many_arguments)]
945    fn draw_image_bitmap(
946        &self,
947        bitmap: &ImageBitmap,
948        canvas: Option<&HTMLCanvasElement>,
949        sx: f64,
950        sy: f64,
951        sw: Option<f64>,
952        sh: Option<f64>,
953        dx: f64,
954        dy: f64,
955        dw: Option<f64>,
956        dh: Option<f64>,
957    ) {
958        let Some(snapshot) = bitmap.bitmap_data().clone() else {
959            return;
960        };
961
962        // Step 4. Establish the source and destination rectangles.
963        let bitmap_size = snapshot.size();
964        let dw = dw.unwrap_or(bitmap_size.width as f64);
965        let dh = dh.unwrap_or(bitmap_size.height as f64);
966        let sw = sw.unwrap_or(bitmap_size.width as f64);
967        let sh = sh.unwrap_or(bitmap_size.height as f64);
968
969        let (source_rect, dest_rect) =
970            self.adjust_source_dest_rects(bitmap_size, sx, sy, sw, sh, dx, dy, dw, dh);
971
972        // Step 5. If one of the sw or sh arguments is zero, then return. Nothing is painted.
973        if !is_rect_valid(source_rect) || !is_rect_valid(dest_rect) {
974            return;
975        }
976
977        let smoothing_enabled = self.state.borrow().image_smoothing_enabled;
978
979        self.send_canvas_2d_msg(Canvas2dMsg::DrawImage(
980            snapshot.to_shared(),
981            dest_rect,
982            source_rect,
983            smoothing_enabled,
984            self.state.borrow().shadow_options(),
985            self.state.borrow().composition_options(),
986            self.state.borrow().transform,
987        ));
988
989        self.mark_as_dirty(canvas);
990    }
991
992    pub(super) fn mark_as_dirty(&self, canvas: Option<&HTMLCanvasElement>) {
993        if let Some(canvas) = canvas {
994            canvas.mark_as_dirty();
995        }
996    }
997
998    /// It is used by DrawImage to calculate the size of the source and destination rectangles based
999    /// on the drawImage call arguments
1000    /// source rectangle = area of the original image to be copied
1001    /// destination rectangle = area of the destination canvas where the source image is going to be drawn
1002    #[expect(clippy::too_many_arguments)]
1003    fn adjust_source_dest_rects(
1004        &self,
1005        image_size: Size2D<u32>,
1006        sx: f64,
1007        sy: f64,
1008        sw: f64,
1009        sh: f64,
1010        dx: f64,
1011        dy: f64,
1012        dw: f64,
1013        dh: f64,
1014    ) -> (Rect<f64>, Rect<f64>) {
1015        let image_rect = Rect::new(
1016            Point2D::zero(),
1017            Size2D::new(image_size.width, image_size.height),
1018        );
1019
1020        // The source rectangle is the rectangle whose corners are the four points (sx, sy),
1021        // (sx+sw, sy), (sx+sw, sy+sh), (sx, sy+sh).
1022        let source_rect = Rect::new(
1023            Point2D::new(sx.min(sx + sw), sy.min(sy + sh)),
1024            Size2D::new(sw.abs(), sh.abs()),
1025        );
1026
1027        // When the source rectangle is outside the source image,
1028        // the source rectangle must be clipped to the source image
1029        let source_rect_clipped = source_rect
1030            .intersection(&image_rect.to_f64())
1031            .unwrap_or(Rect::zero());
1032
1033        // Width and height ratios between the non clipped and clipped source rectangles
1034        let width_ratio: f64 = source_rect_clipped.size.width / source_rect.size.width;
1035        let height_ratio: f64 = source_rect_clipped.size.height / source_rect.size.height;
1036
1037        // When the source rectangle is outside the source image,
1038        // the destination rectangle must be clipped in the same proportion.
1039        let dest_rect_width_scaled: f64 = dw * width_ratio;
1040        let dest_rect_height_scaled: f64 = dh * height_ratio;
1041
1042        // The destination rectangle is the rectangle whose corners are the four points (dx, dy),
1043        // (dx+dw, dy), (dx+dw, dy+dh), (dx, dy+dh).
1044        let dest_rect = Rect::new(
1045            Point2D::new(
1046                dx.min(dx + dest_rect_width_scaled),
1047                dy.min(dy + dest_rect_height_scaled),
1048            ),
1049            Size2D::new(dest_rect_width_scaled.abs(), dest_rect_height_scaled.abs()),
1050        );
1051
1052        let source_rect = Rect::new(
1053            Point2D::new(source_rect_clipped.origin.x, source_rect_clipped.origin.y),
1054            Size2D::new(
1055                source_rect_clipped.size.width,
1056                source_rect_clipped.size.height,
1057            ),
1058        );
1059
1060        (source_rect, dest_rect)
1061    }
1062
1063    fn update_transform(&self, transform: Transform2D<f64>) {
1064        let mut state = self.state.borrow_mut();
1065        self.current_default_path
1066            .borrow_mut()
1067            .transform(state.transform.cast());
1068        state.transform = transform;
1069        if let Some(inverse) = transform.inverse() {
1070            self.current_default_path
1071                .borrow_mut()
1072                .transform(inverse.cast());
1073        }
1074    }
1075
1076    // https://html.spec.whatwg.org/multipage/#dom-context-2d-fillrect
1077    pub(super) fn fill_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1078        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1079            let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
1080            self.send_canvas_2d_msg(Canvas2dMsg::FillRect(
1081                rect,
1082                style,
1083                self.state.borrow().shadow_options(),
1084                self.state.borrow().composition_options(),
1085                self.state.borrow().transform,
1086            ));
1087        }
1088    }
1089
1090    // https://html.spec.whatwg.org/multipage/#dom-context-2d-clearrect
1091    pub(super) fn clear_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1092        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1093            self.send_canvas_2d_msg(Canvas2dMsg::ClearRect(rect, self.state.borrow().transform));
1094        }
1095    }
1096
1097    // https://html.spec.whatwg.org/multipage/#dom-context-2d-strokerect
1098    pub(super) fn stroke_rect(&self, x: f64, y: f64, width: f64, height: f64) {
1099        if let Some(rect) = self.create_drawable_rect(x, y, width, height) {
1100            let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
1101            self.send_canvas_2d_msg(Canvas2dMsg::StrokeRect(
1102                rect,
1103                style,
1104                self.state.borrow().line_options(),
1105                self.state.borrow().shadow_options(),
1106                self.state.borrow().composition_options(),
1107                self.state.borrow().transform,
1108            ));
1109        }
1110    }
1111
1112    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsetx
1113    pub(super) fn shadow_offset_x(&self) -> f64 {
1114        self.state.borrow().shadow_offset_x
1115    }
1116
1117    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsetx
1118    pub(super) fn set_shadow_offset_x(&self, value: f64) {
1119        if !value.is_finite() || value == self.state.borrow().shadow_offset_x {
1120            return;
1121        }
1122        self.state.borrow_mut().shadow_offset_x = value;
1123    }
1124
1125    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsety
1126    pub(super) fn shadow_offset_y(&self) -> f64 {
1127        self.state.borrow().shadow_offset_y
1128    }
1129
1130    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowoffsety
1131    pub(super) fn set_shadow_offset_y(&self, value: f64) {
1132        if !value.is_finite() || value == self.state.borrow().shadow_offset_y {
1133            return;
1134        }
1135        self.state.borrow_mut().shadow_offset_y = value;
1136    }
1137
1138    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowblur
1139    pub(super) fn shadow_blur(&self) -> f64 {
1140        self.state.borrow().shadow_blur
1141    }
1142
1143    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowblur
1144    pub(super) fn set_shadow_blur(&self, value: f64) {
1145        if !value.is_finite() || value < 0f64 || value == self.state.borrow().shadow_blur {
1146            return;
1147        }
1148        self.state.borrow_mut().shadow_blur = value;
1149    }
1150
1151    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowcolor
1152    pub(super) fn shadow_color(&self) -> DOMString {
1153        let mut result = String::new();
1154        serialize(&self.state.borrow().shadow_color, &mut result).unwrap();
1155        DOMString::from(result)
1156    }
1157
1158    // https://html.spec.whatwg.org/multipage/#dom-context-2d-shadowcolor
1159    pub(super) fn set_shadow_color(&self, canvas: Option<&HTMLCanvasElement>, value: DOMString) {
1160        if let Ok(rgba) = parse_color(canvas, &value) {
1161            self.state.borrow_mut().shadow_color = rgba;
1162        }
1163    }
1164
1165    // https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle
1166    pub(super) fn stroke_style(&self) -> StringOrCanvasGradientOrCanvasPattern {
1167        match self.state.borrow().stroke_style {
1168            CanvasFillOrStrokeStyle::Color(ref rgba) => {
1169                let mut result = String::new();
1170                serialize(rgba, &mut result).unwrap();
1171                StringOrCanvasGradientOrCanvasPattern::String(DOMString::from(result))
1172            },
1173            CanvasFillOrStrokeStyle::Gradient(ref gradient) => {
1174                StringOrCanvasGradientOrCanvasPattern::CanvasGradient(DomRoot::from_ref(gradient))
1175            },
1176            CanvasFillOrStrokeStyle::Pattern(ref pattern) => {
1177                StringOrCanvasGradientOrCanvasPattern::CanvasPattern(DomRoot::from_ref(pattern))
1178            },
1179        }
1180    }
1181
1182    // https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle
1183    pub(super) fn set_stroke_style(
1184        &self,
1185        canvas: Option<&HTMLCanvasElement>,
1186        value: StringOrCanvasGradientOrCanvasPattern,
1187    ) {
1188        match value {
1189            StringOrCanvasGradientOrCanvasPattern::String(string) => {
1190                if let Ok(rgba) = parse_color(canvas, &string) {
1191                    self.state.borrow_mut().stroke_style = CanvasFillOrStrokeStyle::Color(rgba);
1192                }
1193            },
1194            StringOrCanvasGradientOrCanvasPattern::CanvasGradient(gradient) => {
1195                self.state.borrow_mut().stroke_style =
1196                    CanvasFillOrStrokeStyle::Gradient(Dom::from_ref(&*gradient));
1197            },
1198            StringOrCanvasGradientOrCanvasPattern::CanvasPattern(pattern) => {
1199                self.state.borrow_mut().stroke_style =
1200                    CanvasFillOrStrokeStyle::Pattern(Dom::from_ref(&*pattern));
1201                if !pattern.origin_is_clean() {
1202                    self.set_origin_clean(false);
1203                }
1204            },
1205        }
1206    }
1207
1208    // https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle
1209    pub(super) fn fill_style(&self) -> StringOrCanvasGradientOrCanvasPattern {
1210        match self.state.borrow().fill_style {
1211            CanvasFillOrStrokeStyle::Color(ref rgba) => {
1212                let mut result = String::new();
1213                serialize(rgba, &mut result).unwrap();
1214                StringOrCanvasGradientOrCanvasPattern::String(DOMString::from(result))
1215            },
1216            CanvasFillOrStrokeStyle::Gradient(ref gradient) => {
1217                StringOrCanvasGradientOrCanvasPattern::CanvasGradient(DomRoot::from_ref(gradient))
1218            },
1219            CanvasFillOrStrokeStyle::Pattern(ref pattern) => {
1220                StringOrCanvasGradientOrCanvasPattern::CanvasPattern(DomRoot::from_ref(pattern))
1221            },
1222        }
1223    }
1224
1225    // https://html.spec.whatwg.org/multipage/#dom-context-2d-strokestyle
1226    pub(super) fn set_fill_style(
1227        &self,
1228        canvas: Option<&HTMLCanvasElement>,
1229        value: StringOrCanvasGradientOrCanvasPattern,
1230    ) {
1231        match value {
1232            StringOrCanvasGradientOrCanvasPattern::String(string) => {
1233                if let Ok(rgba) = parse_color(canvas, &string) {
1234                    self.state.borrow_mut().fill_style = CanvasFillOrStrokeStyle::Color(rgba);
1235                }
1236            },
1237            StringOrCanvasGradientOrCanvasPattern::CanvasGradient(gradient) => {
1238                self.state.borrow_mut().fill_style =
1239                    CanvasFillOrStrokeStyle::Gradient(Dom::from_ref(&*gradient));
1240            },
1241            StringOrCanvasGradientOrCanvasPattern::CanvasPattern(pattern) => {
1242                self.state.borrow_mut().fill_style =
1243                    CanvasFillOrStrokeStyle::Pattern(Dom::from_ref(&*pattern));
1244                if !pattern.origin_is_clean() {
1245                    self.set_origin_clean(false);
1246                }
1247            },
1248        }
1249    }
1250
1251    // https://html.spec.whatwg.org/multipage/#dom-context-2d-createlineargradient
1252    pub(super) fn create_linear_gradient(
1253        &self,
1254        global: &GlobalScope,
1255        cx: &mut JSContext,
1256        x0: Finite<f64>,
1257        y0: Finite<f64>,
1258        x1: Finite<f64>,
1259        y1: Finite<f64>,
1260    ) -> DomRoot<CanvasGradient> {
1261        CanvasGradient::new(
1262            global,
1263            cx,
1264            CanvasGradientStyle::Linear(LinearGradientStyle::new(*x0, *y0, *x1, *y1, Vec::new())),
1265        )
1266    }
1267
1268    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createradialgradient>
1269    #[expect(clippy::too_many_arguments)]
1270    pub(super) fn create_radial_gradient(
1271        &self,
1272        global: &GlobalScope,
1273        cx: &mut JSContext,
1274        x0: Finite<f64>,
1275        y0: Finite<f64>,
1276        r0: Finite<f64>,
1277        x1: Finite<f64>,
1278        y1: Finite<f64>,
1279        r1: Finite<f64>,
1280    ) -> Fallible<DomRoot<CanvasGradient>> {
1281        if *r0 < 0. || *r1 < 0. {
1282            return Err(Error::IndexSize(None));
1283        }
1284
1285        Ok(CanvasGradient::new(
1286            global,
1287            cx,
1288            CanvasGradientStyle::Radial(RadialGradientStyle::new(
1289                *x0,
1290                *y0,
1291                *r0,
1292                *x1,
1293                *y1,
1294                *r1,
1295                Vec::new(),
1296            )),
1297        ))
1298    }
1299
1300    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-createpattern>
1301    pub(super) fn create_pattern(
1302        &self,
1303        global: &GlobalScope,
1304        cx: &mut JSContext,
1305        image: CanvasImageSource,
1306        mut repetition: DOMString,
1307    ) -> Fallible<Option<DomRoot<CanvasPattern>>> {
1308        let snapshot = match image {
1309            CanvasImageSource::HTMLImageElement(ref image) => {
1310                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1311                if !image.is_usable()? {
1312                    return Ok(None);
1313                }
1314
1315                image
1316                    .get_raster_image_data()
1317                    .ok_or(Error::InvalidState(None))?
1318            },
1319            CanvasImageSource::HTMLVideoElement(ref video) => {
1320                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1321                if !video.is_usable() {
1322                    return Ok(None);
1323                }
1324
1325                video
1326                    .get_current_frame_data()
1327                    .ok_or(Error::InvalidState(None))?
1328            },
1329            CanvasImageSource::HTMLCanvasElement(ref canvas) => {
1330                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1331                if canvas.get_size().is_empty() {
1332                    return Err(Error::InvalidState(None));
1333                }
1334
1335                canvas.get_image_data().ok_or(Error::InvalidState(None))?
1336            },
1337            CanvasImageSource::ImageBitmap(ref bitmap) => {
1338                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1339                if bitmap.is_detached() {
1340                    return Err(Error::InvalidState(None));
1341                }
1342
1343                bitmap
1344                    .bitmap_data()
1345                    .clone()
1346                    .ok_or(Error::InvalidState(None))?
1347            },
1348            CanvasImageSource::OffscreenCanvas(ref canvas) => {
1349                // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument>
1350                if canvas.get_size().is_empty() {
1351                    return Err(Error::InvalidState(None));
1352                }
1353
1354                canvas.get_image_data().ok_or(Error::InvalidState(None))?
1355            },
1356            CanvasImageSource::CSSStyleValue(ref value) => value
1357                .get_url(self.base_url.clone())
1358                .and_then(|url| self.fetch_image_data(url, None))
1359                .ok_or(Error::InvalidState(None))?,
1360        };
1361
1362        if repetition.is_empty() {
1363            repetition.push_str("repeat");
1364        }
1365
1366        if let Ok(rep) = RepetitionStyle::from_str(&repetition.str()) {
1367            let size = snapshot.size();
1368            Ok(Some(CanvasPattern::new(
1369                global,
1370                cx,
1371                snapshot,
1372                size.cast(),
1373                rep,
1374                self.is_origin_clean(image),
1375            )))
1376        } else {
1377            Err(Error::Syntax(None))
1378        }
1379    }
1380
1381    // https://html.spec.whatwg.org/multipage/#dom-context-2d-save
1382    pub(super) fn save(&self) {
1383        self.saved_states
1384            .borrow_mut()
1385            .push(self.state.borrow().clone());
1386    }
1387
1388    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
1389    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-restore>
1390    pub(super) fn restore(&self) {
1391        let mut saved_states = self.saved_states.borrow_mut();
1392        if let Some(state) = saved_states.pop() {
1393            let clips_to_pop = self.state.borrow().clips_pushed;
1394            if clips_to_pop != 0 {
1395                self.send_canvas_2d_msg(Canvas2dMsg::PopClips(clips_to_pop));
1396            }
1397            self.state.borrow_mut().clone_from(&state);
1398        }
1399    }
1400
1401    // https://html.spec.whatwg.org/multipage/#dom-context-2d-globalalpha
1402    pub(super) fn global_alpha(&self) -> f64 {
1403        self.state.borrow().global_alpha
1404    }
1405
1406    // https://html.spec.whatwg.org/multipage/#dom-context-2d-globalalpha
1407    pub(super) fn set_global_alpha(&self, alpha: f64) {
1408        if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
1409            return;
1410        }
1411
1412        self.state.borrow_mut().global_alpha = alpha;
1413    }
1414
1415    // https://html.spec.whatwg.org/multipage/#dom-context-2d-globalcompositeoperation
1416    pub(super) fn global_composite_operation(&self) -> DOMString {
1417        match self.state.borrow().global_composition {
1418            CompositionOrBlending::Composition(op) => DOMString::from(op.to_string()),
1419            CompositionOrBlending::Blending(op) => DOMString::from(op.to_string()),
1420        }
1421    }
1422
1423    // https://html.spec.whatwg.org/multipage/#dom-context-2d-globalcompositeoperation
1424    pub(super) fn set_global_composite_operation(&self, op_str: DOMString) {
1425        if let Ok(op) = CompositionOrBlending::from_str(&op_str.str()) {
1426            self.state.borrow_mut().global_composition = op;
1427        }
1428    }
1429
1430    // https://html.spec.whatwg.org/multipage/#dom-context-2d-imagesmoothingenabled
1431    pub(super) fn image_smoothing_enabled(&self) -> bool {
1432        self.state.borrow().image_smoothing_enabled
1433    }
1434
1435    // https://html.spec.whatwg.org/multipage/#dom-context-2d-imagesmoothingenabled
1436    pub(super) fn set_image_smoothing_enabled(&self, value: bool) {
1437        self.state.borrow_mut().image_smoothing_enabled = value;
1438    }
1439
1440    // https://html.spec.whatwg.org/multipage/#dom-context-2d-filltext
1441    pub(super) fn fill_text(
1442        &self,
1443        global_scope: &GlobalScope,
1444        canvas: Option<&HTMLCanvasElement>,
1445        text: DOMString,
1446        x: f64,
1447        y: f64,
1448        max_width: Option<f64>,
1449    ) {
1450        // Step 1: If any of the arguments are infinite or NaN, then return.
1451        if !x.is_finite() ||
1452            !y.is_finite() ||
1453            max_width.is_some_and(|max_width| !max_width.is_finite())
1454        {
1455            return;
1456        }
1457
1458        if self.state.borrow().font_style.is_none() {
1459            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into())
1460        }
1461        // This may be `None` if if this is offscreen canvas, in which case just use
1462        // the initial values for the text style.
1463        let size = self.font_style().font_size.computed_size().px() as f64;
1464
1465        let Some((bounds, text_run)) = self.text_with_size(
1466            global_scope,
1467            &text.str(),
1468            Point2D::new(x, y),
1469            size,
1470            max_width,
1471        ) else {
1472            return;
1473        };
1474        self.send_canvas_2d_msg(Canvas2dMsg::FillText(
1475            bounds,
1476            text_run,
1477            self.state.borrow().fill_style.to_fill_or_stroke_style(),
1478            self.state.borrow().shadow_options(),
1479            self.state.borrow().composition_options(),
1480            self.state.borrow().transform,
1481        ));
1482    }
1483
1484    // https://html.spec.whatwg.org/multipage/#dom-context-2d-stroketext
1485    pub(super) fn stroke_text(
1486        &self,
1487        global_scope: &GlobalScope,
1488        canvas: Option<&HTMLCanvasElement>,
1489        text: DOMString,
1490        x: f64,
1491        y: f64,
1492        max_width: Option<f64>,
1493    ) {
1494        // Step 1: If any of the arguments are infinite or NaN, then return.
1495        if !x.is_finite() ||
1496            !y.is_finite() ||
1497            max_width.is_some_and(|max_width| !max_width.is_finite())
1498        {
1499            return;
1500        }
1501
1502        if self.state.borrow().font_style.is_none() {
1503            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into())
1504        }
1505        // This may be `None` if if this is offscreen canvas, in which case just use
1506        // the initial values for the text style.
1507        let size = self.font_style().font_size.computed_size().px() as f64;
1508
1509        let Some((bounds, text_run)) = self.text_with_size(
1510            global_scope,
1511            &text.str(),
1512            Point2D::new(x, y),
1513            size,
1514            max_width,
1515        ) else {
1516            return;
1517        };
1518        self.send_canvas_2d_msg(Canvas2dMsg::StrokeText(
1519            bounds,
1520            text_run,
1521            self.state.borrow().stroke_style.to_fill_or_stroke_style(),
1522            self.state.borrow().line_options(),
1523            self.state.borrow().shadow_options(),
1524            self.state.borrow().composition_options(),
1525            self.state.borrow().transform,
1526        ));
1527    }
1528
1529    /// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
1530    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-measuretext>
1531    /// <https://html.spec.whatwg.org/multipage/#textmetrics>
1532    pub(super) fn measure_text(
1533        &self,
1534        global: &GlobalScope,
1535        canvas: Option<&HTMLCanvasElement>,
1536        text: DOMString,
1537        cx: &mut JSContext,
1538    ) -> DomRoot<TextMetrics> {
1539        // > Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.0
1540        // Max width is not provided for `measureText()`.
1541
1542        // > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
1543        let text = replace_ascii_whitespace(&text.str());
1544
1545        // > Step 3: Let font be the current font of target, as given by that object's font
1546        // > attribute.
1547        if self.state.borrow().font_style.is_none() {
1548            self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into());
1549        }
1550
1551        let Some(font_context) = global.font_context() else {
1552            warn!("Tried to paint to a canvas of GlobalScope without a FontContext.");
1553            return TextMetrics::default(global, cx);
1554        };
1555
1556        let font_style = self.font_style();
1557        let font_group = font_context.font_group(font_style);
1558        let font = font_group.first(font_context).expect("couldn't find font");
1559        let ascent = font.metrics.ascent.to_f64_px();
1560        let descent = font.metrics.descent.to_f64_px();
1561        let runs = self.build_unshaped_text_runs(font_context, &text, &font_group);
1562
1563        let mut total_advance = 0.0;
1564        let shaped_runs: Vec<_> = runs
1565            .into_iter()
1566            .filter_map(|unshaped_text_run| {
1567                let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
1568                total_advance += text_run.advance;
1569                Some(text_run)
1570            })
1571            .collect();
1572
1573        let bounding_box = shaped_runs
1574            .iter()
1575            .map(|text_run| text_run.bounds)
1576            .reduce(|a, b| a.union(&b))
1577            .unwrap_or_default();
1578
1579        let baseline = font.baseline().unwrap_or_else(|| FontBaseline {
1580            hanging_baseline: (ascent * HANGING_BASELINE_DEFAULT) as f32,
1581            ideographic_baseline: (-descent * IDEOGRAPHIC_BASELINE_DEFAULT) as f32,
1582            alphabetic_baseline: 0.,
1583        });
1584        let ideographic_baseline = baseline.ideographic_baseline as f64;
1585        let alphabetic_baseline = baseline.alphabetic_baseline as f64;
1586        let hanging_baseline = baseline.hanging_baseline as f64;
1587
1588        let state = self.state.borrow();
1589        let anchor_x = match state.text_align {
1590            CanvasTextAlign::End => total_advance,
1591            CanvasTextAlign::Center => total_advance / 2.,
1592            CanvasTextAlign::Right => total_advance,
1593            _ => 0.,
1594        } as f64;
1595        let anchor_y = match state.text_baseline {
1596            CanvasTextBaseline::Top => ascent,
1597            CanvasTextBaseline::Hanging => hanging_baseline,
1598            CanvasTextBaseline::Ideographic => ideographic_baseline,
1599            CanvasTextBaseline::Middle => (ascent - descent) / 2.,
1600            CanvasTextBaseline::Alphabetic => alphabetic_baseline,
1601            CanvasTextBaseline::Bottom => -descent,
1602        };
1603
1604        TextMetrics::new(
1605            global,
1606            cx,
1607            total_advance as f64,
1608            anchor_x - bounding_box.min_x(),
1609            bounding_box.max_x() - anchor_x,
1610            bounding_box.max_y() - anchor_y,
1611            anchor_y - bounding_box.min_y(),
1612            ascent - anchor_y,
1613            descent + anchor_y,
1614            ascent - anchor_y,
1615            descent + anchor_y,
1616            hanging_baseline - anchor_y,
1617            alphabetic_baseline - anchor_y,
1618            ideographic_baseline - anchor_y,
1619        )
1620    }
1621
1622    // https://html.spec.whatwg.org/multipage/#dom-context-2d-font
1623    pub(super) fn set_font(&self, canvas: Option<&HTMLCanvasElement>, value: DOMString) {
1624        let canvas = match canvas {
1625            Some(element) => element,
1626            None => return, // offscreen canvas doesn't have a placeholder canvas
1627        };
1628        let node = canvas.upcast::<Node>();
1629        let window = canvas.owner_window();
1630
1631        let Some(resolved_font_style) = window.resolved_font_style_query(node, value.to_string())
1632        else {
1633            // This will happen when there is a syntax error.
1634            return;
1635        };
1636        self.state.borrow_mut().font_style = Some(resolved_font_style);
1637    }
1638
1639    fn font_style(&self) -> ServoArc<Font> {
1640        self.state
1641            .borrow()
1642            .font_style
1643            .clone()
1644            .unwrap_or_else(|| ServoArc::new(Font::initial_values()))
1645    }
1646
1647    // https://html.spec.whatwg.org/multipage/#dom-context-2d-font
1648    pub(super) fn font(&self) -> DOMString {
1649        self.state.borrow().font_style.as_ref().map_or_else(
1650            || CanvasContextState::DEFAULT_FONT_STYLE.into(),
1651            |style| {
1652                let mut result = String::new();
1653                serialize_font(style, &mut result).unwrap();
1654                DOMString::from(result)
1655            },
1656        )
1657    }
1658
1659    // https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign
1660    pub(super) fn text_align(&self) -> CanvasTextAlign {
1661        self.state.borrow().text_align
1662    }
1663
1664    // https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign
1665    pub(super) fn set_text_align(&self, value: CanvasTextAlign) {
1666        self.state.borrow_mut().text_align = value;
1667    }
1668
1669    pub(super) fn text_baseline(&self) -> CanvasTextBaseline {
1670        self.state.borrow().text_baseline
1671    }
1672
1673    pub(super) fn set_text_baseline(&self, value: CanvasTextBaseline) {
1674        self.state.borrow_mut().text_baseline = value;
1675    }
1676
1677    // https://html.spec.whatwg.org/multipage/#dom-context-2d-direction
1678    pub(super) fn direction(&self) -> CanvasDirection {
1679        self.state.borrow().direction
1680    }
1681
1682    // https://html.spec.whatwg.org/multipage/#dom-context-2d-direction
1683    pub(super) fn set_direction(&self, value: CanvasDirection) {
1684        self.state.borrow_mut().direction = value;
1685    }
1686
1687    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linewidth
1688    pub(super) fn line_width(&self) -> f64 {
1689        self.state.borrow().line_width
1690    }
1691
1692    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linewidth
1693    pub(super) fn set_line_width(&self, width: f64) {
1694        if !width.is_finite() || width <= 0.0 {
1695            return;
1696        }
1697
1698        self.state.borrow_mut().line_width = width;
1699    }
1700
1701    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linecap
1702    pub(super) fn line_cap(&self) -> CanvasLineCap {
1703        match self.state.borrow().line_cap {
1704            LineCapStyle::Butt => CanvasLineCap::Butt,
1705            LineCapStyle::Round => CanvasLineCap::Round,
1706            LineCapStyle::Square => CanvasLineCap::Square,
1707        }
1708    }
1709
1710    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linecap
1711    pub(super) fn set_line_cap(&self, cap: CanvasLineCap) {
1712        let line_cap = match cap {
1713            CanvasLineCap::Butt => LineCapStyle::Butt,
1714            CanvasLineCap::Round => LineCapStyle::Round,
1715            CanvasLineCap::Square => LineCapStyle::Square,
1716        };
1717        self.state.borrow_mut().line_cap = line_cap;
1718    }
1719
1720    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linejoin
1721    pub(super) fn line_join(&self) -> CanvasLineJoin {
1722        match self.state.borrow().line_join {
1723            LineJoinStyle::Round => CanvasLineJoin::Round,
1724            LineJoinStyle::Bevel => CanvasLineJoin::Bevel,
1725            LineJoinStyle::Miter => CanvasLineJoin::Miter,
1726        }
1727    }
1728
1729    // https://html.spec.whatwg.org/multipage/#dom-context-2d-linejoin
1730    pub(super) fn set_line_join(&self, join: CanvasLineJoin) {
1731        let line_join = match join {
1732            CanvasLineJoin::Round => LineJoinStyle::Round,
1733            CanvasLineJoin::Bevel => LineJoinStyle::Bevel,
1734            CanvasLineJoin::Miter => LineJoinStyle::Miter,
1735        };
1736        self.state.borrow_mut().line_join = line_join;
1737    }
1738
1739    // https://html.spec.whatwg.org/multipage/#dom-context-2d-miterlimit
1740    pub(super) fn miter_limit(&self) -> f64 {
1741        self.state.borrow().miter_limit
1742    }
1743
1744    // https://html.spec.whatwg.org/multipage/#dom-context-2d-miterlimit
1745    pub(super) fn set_miter_limit(&self, limit: f64) {
1746        if !limit.is_finite() || limit <= 0.0 {
1747            return;
1748        }
1749
1750        self.state.borrow_mut().miter_limit = limit;
1751    }
1752
1753    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-getlinedash>
1754    pub(super) fn line_dash(&self) -> Vec<f64> {
1755        // > return a sequence whose values are the values of
1756        // > the object's dash list, in the same order.
1757        self.state.borrow().line_dash.clone()
1758    }
1759
1760    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-setlinedash>
1761    pub(super) fn set_line_dash(&self, segments: Vec<f64>) {
1762        // > If any value in segments is not finite (e.g. an Infinity or a NaN value),
1763        // > or if any value is negative (less than zero), then return (without throwing
1764        // > an exception; user agents could show a message on a developer console,
1765        // > though, as that would be helpful for debugging).
1766        if segments
1767            .iter()
1768            .any(|segment| !segment.is_finite() || *segment < 0.0)
1769        {
1770            return;
1771        }
1772
1773        // > If the number of elements in segments is odd, then let segments
1774        // > be the concatenation of two copies of segments.
1775        let mut line_dash: Vec<_> = segments.clone();
1776        if segments.len() & 1 == 1 {
1777            line_dash.extend(line_dash.clone());
1778        }
1779
1780        // > Let the object's dash list be segments.
1781        self.state.borrow_mut().line_dash = line_dash.clone();
1782    }
1783
1784    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linedashoffset>
1785    pub(super) fn line_dash_offset(&self) -> f64 {
1786        // > On getting, it must return the current value.
1787        self.state.borrow().line_dash_offset
1788    }
1789
1790    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-linedashoffset?
1791    pub(super) fn set_line_dash_offset(&self, offset: f64) {
1792        // > On setting, infinite and NaN values must be ignored,
1793        // > leaving the value unchanged;
1794        if !offset.is_finite() {
1795            return;
1796        }
1797
1798        // > other values must change the current value to the new value.
1799        self.state.borrow_mut().line_dash_offset = offset;
1800    }
1801
1802    // https://html.spec.whatwg.org/multipage/#dom-context-2d-createimagedata
1803    pub(super) fn create_image_data(
1804        &self,
1805        global: &GlobalScope,
1806        sw: i32,
1807        sh: i32,
1808        can_gc: CanGc,
1809    ) -> Fallible<DomRoot<ImageData>> {
1810        if sw == 0 || sh == 0 {
1811            return Err(Error::IndexSize(None));
1812        }
1813        ImageData::new(global, sw.unsigned_abs(), sh.unsigned_abs(), None, can_gc)
1814    }
1815
1816    // https://html.spec.whatwg.org/multipage/#dom-context-2d-createimagedata
1817    pub(super) fn create_image_data_(
1818        &self,
1819        global: &GlobalScope,
1820        imagedata: &ImageData,
1821        can_gc: CanGc,
1822    ) -> Fallible<DomRoot<ImageData>> {
1823        ImageData::new(global, imagedata.Width(), imagedata.Height(), None, can_gc)
1824    }
1825
1826    // https://html.spec.whatwg.org/multipage/#dom-context-2d-getimagedata
1827    #[expect(clippy::too_many_arguments)]
1828    pub(super) fn get_image_data(
1829        &self,
1830        canvas_size: Size2D<u32>,
1831        global: &GlobalScope,
1832        sx: i32,
1833        sy: i32,
1834        sw: i32,
1835        sh: i32,
1836        can_gc: CanGc,
1837    ) -> Fallible<DomRoot<ImageData>> {
1838        // FIXME(nox): There are many arithmetic operations here that can
1839        // overflow or underflow, this should probably be audited.
1840
1841        if sw == 0 || sh == 0 {
1842            return Err(Error::IndexSize(None));
1843        }
1844
1845        if !self.origin_is_clean() {
1846            return Err(Error::Security(None));
1847        }
1848
1849        let (origin, size) = adjust_size_sign(Point2D::new(sx, sy), Size2D::new(sw, sh));
1850        let read_rect = match pixels::clip(origin, size.to_u32(), canvas_size) {
1851            Some(rect) => rect,
1852            None => {
1853                // All the pixels are outside the canvas surface.
1854                return ImageData::new(global, size.width, size.height, None, can_gc);
1855            },
1856        };
1857
1858        let data = if self.is_paintable() {
1859            let (sender, receiver) = generic_channel::channel().unwrap();
1860            self.send_canvas_2d_msg(Canvas2dMsg::GetImageData(Some(read_rect), sender));
1861
1862            let mut snapshot = receiver.recv().unwrap().to_owned();
1863            snapshot.transform(
1864                SnapshotAlphaMode::Transparent {
1865                    premultiplied: false,
1866                },
1867                SnapshotPixelFormat::RGBA,
1868            );
1869            Some(snapshot.into())
1870        } else {
1871            None
1872        };
1873
1874        ImageData::new(global, size.width, size.height, data, can_gc)
1875    }
1876
1877    // https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata
1878    pub(super) fn put_image_data(
1879        &self,
1880        canvas_size: Size2D<u32>,
1881        imagedata: &ImageData,
1882        dx: i32,
1883        dy: i32,
1884    ) {
1885        self.put_image_data_(
1886            canvas_size,
1887            imagedata,
1888            dx,
1889            dy,
1890            0,
1891            0,
1892            imagedata.Width() as i32,
1893            imagedata.Height() as i32,
1894        )
1895    }
1896
1897    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata>
1898    #[expect(clippy::too_many_arguments)]
1899    pub(super) fn put_image_data_(
1900        &self,
1901        canvas_size: Size2D<u32>,
1902        imagedata: &ImageData,
1903        dx: i32,
1904        dy: i32,
1905        dirty_x: i32,
1906        dirty_y: i32,
1907        dirty_width: i32,
1908        dirty_height: i32,
1909    ) {
1910        if !self.is_paintable() {
1911            return;
1912        }
1913
1914        // FIXME(nox): There are many arithmetic operations here that can
1915        // overflow or underflow, this should probably be audited.
1916
1917        let imagedata_size = Size2D::new(imagedata.Width(), imagedata.Height());
1918        if imagedata_size.area() == 0 {
1919            return;
1920        }
1921
1922        // Step 1.
1923        // Done later.
1924
1925        // Step 2.
1926        // TODO: throw InvalidState if buffer is detached.
1927
1928        // Steps 3-6.
1929        let (src_origin, src_size) = adjust_size_sign(
1930            Point2D::new(dirty_x, dirty_y),
1931            Size2D::new(dirty_width, dirty_height),
1932        );
1933        let src_rect = match pixels::clip(src_origin, src_size.to_u32(), imagedata_size.to_u32()) {
1934            Some(rect) => rect,
1935            None => return,
1936        };
1937        let (dst_origin, _) = adjust_size_sign(
1938            Point2D::new(dirty_x.saturating_add(dx), dirty_y.saturating_add(dy)),
1939            Size2D::new(dirty_width, dirty_height),
1940        );
1941        // By clipping to the canvas surface, we avoid sending any pixel
1942        // that would fall outside it.
1943        let dst_rect = match pixels::clip(dst_origin, src_rect.size, canvas_size) {
1944            Some(rect) => rect,
1945            None => return,
1946        };
1947
1948        // Step 7.
1949        let snapshot = imagedata.get_snapshot_rect(Rect::new(src_rect.origin, dst_rect.size));
1950        self.send_canvas_2d_msg(Canvas2dMsg::PutImageData(dst_rect, snapshot.to_shared()));
1951    }
1952
1953    // https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
1954    pub(super) fn draw_image(
1955        &self,
1956        canvas: Option<&HTMLCanvasElement>,
1957        image: CanvasImageSource,
1958        dx: f64,
1959        dy: f64,
1960    ) -> ErrorResult {
1961        if !(dx.is_finite() && dy.is_finite()) {
1962            return Ok(());
1963        }
1964
1965        self.draw_image_internal(canvas, image, 0f64, 0f64, None, None, dx, dy, None, None)
1966    }
1967
1968    // https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage
1969    pub(super) fn draw_image_(
1970        &self,
1971        canvas: Option<&HTMLCanvasElement>,
1972        image: CanvasImageSource,
1973        dx: f64,
1974        dy: f64,
1975        dw: f64,
1976        dh: f64,
1977    ) -> ErrorResult {
1978        if !(dx.is_finite() && dy.is_finite() && dw.is_finite() && dh.is_finite()) {
1979            return Ok(());
1980        }
1981
1982        self.draw_image_internal(
1983            canvas,
1984            image,
1985            0f64,
1986            0f64,
1987            None,
1988            None,
1989            dx,
1990            dy,
1991            Some(dw),
1992            Some(dh),
1993        )
1994    }
1995
1996    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-drawimage>
1997    #[expect(clippy::too_many_arguments)]
1998    pub(super) fn draw_image__(
1999        &self,
2000        canvas: Option<&HTMLCanvasElement>,
2001        image: CanvasImageSource,
2002        sx: f64,
2003        sy: f64,
2004        sw: f64,
2005        sh: f64,
2006        dx: f64,
2007        dy: f64,
2008        dw: f64,
2009        dh: f64,
2010    ) -> ErrorResult {
2011        if !(sx.is_finite() &&
2012            sy.is_finite() &&
2013            sw.is_finite() &&
2014            sh.is_finite() &&
2015            dx.is_finite() &&
2016            dy.is_finite() &&
2017            dw.is_finite() &&
2018            dh.is_finite())
2019        {
2020            return Ok(());
2021        }
2022
2023        self.draw_image_internal(
2024            canvas,
2025            image,
2026            sx,
2027            sy,
2028            Some(sw),
2029            Some(sh),
2030            dx,
2031            dy,
2032            Some(dw),
2033            Some(dh),
2034        )
2035    }
2036
2037    // https://html.spec.whatwg.org/multipage/#dom-context-2d-beginpath
2038    pub(super) fn begin_path(&self) {
2039        *self.current_default_path.borrow_mut() = Path::new();
2040    }
2041
2042    // https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
2043    pub(super) fn fill(&self, fill_rule: CanvasFillRule) {
2044        let path = self.current_default_path.borrow().clone();
2045        self.fill_(path, fill_rule);
2046    }
2047
2048    // https://html.spec.whatwg.org/multipage/#dom-context-2d-fill
2049    pub(super) fn fill_(&self, path: Path, fill_rule: CanvasFillRule) {
2050        let style = self.state.borrow().fill_style.to_fill_or_stroke_style();
2051        self.send_canvas_2d_msg(Canvas2dMsg::FillPath(
2052            style,
2053            path,
2054            fill_rule.convert(),
2055            self.state.borrow().shadow_options(),
2056            self.state.borrow().composition_options(),
2057            self.state.borrow().transform,
2058        ));
2059    }
2060
2061    // https://html.spec.whatwg.org/multipage/#dom-context-2d-stroke
2062    pub(super) fn stroke(&self) {
2063        let path = self.current_default_path.borrow().clone();
2064        self.stroke_(path);
2065    }
2066
2067    pub(super) fn stroke_(&self, path: Path) {
2068        let style = self.state.borrow().stroke_style.to_fill_or_stroke_style();
2069        self.send_canvas_2d_msg(Canvas2dMsg::StrokePath(
2070            path,
2071            style,
2072            self.state.borrow().line_options(),
2073            self.state.borrow().shadow_options(),
2074            self.state.borrow().composition_options(),
2075            self.state.borrow().transform,
2076        ));
2077    }
2078
2079    // https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
2080    pub(super) fn clip(&self, fill_rule: CanvasFillRule) {
2081        let path = self.current_default_path.borrow().clone();
2082        self.clip_(path, fill_rule);
2083    }
2084
2085    // https://html.spec.whatwg.org/multipage/#dom-context-2d-clip
2086    pub(super) fn clip_(&self, path: Path, fill_rule: CanvasFillRule) {
2087        self.state.borrow_mut().clips_pushed += 1;
2088        self.send_canvas_2d_msg(Canvas2dMsg::ClipPath(
2089            path,
2090            fill_rule.convert(),
2091            self.state.borrow().transform,
2092        ));
2093    }
2094
2095    // https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
2096    pub(super) fn is_point_in_path(
2097        &self,
2098        global: &GlobalScope,
2099        x: f64,
2100        y: f64,
2101        fill_rule: CanvasFillRule,
2102    ) -> bool {
2103        let mut path = self.current_default_path.borrow().clone();
2104        path.transform(self.state.borrow().transform.cast());
2105        self.is_point_in_path_(global, path, x, y, fill_rule)
2106    }
2107
2108    // https://html.spec.whatwg.org/multipage/#dom-context-2d-ispointinpath
2109    pub(super) fn is_point_in_path_(
2110        &self,
2111        _global: &GlobalScope,
2112        path: Path,
2113        x: f64,
2114        y: f64,
2115        fill_rule: CanvasFillRule,
2116    ) -> bool {
2117        let fill_rule = match fill_rule {
2118            CanvasFillRule::Nonzero => FillRule::Nonzero,
2119            CanvasFillRule::Evenodd => FillRule::Evenodd,
2120        };
2121        path.is_point_in_path(x, y, fill_rule)
2122    }
2123
2124    // https://html.spec.whatwg.org/multipage/#dom-context-2d-scale
2125    pub(super) fn scale(&self, x: f64, y: f64) {
2126        if !(x.is_finite() && y.is_finite()) {
2127            return;
2128        }
2129
2130        let transform = self.state.borrow().transform;
2131        self.update_transform(transform.pre_scale(x, y))
2132    }
2133
2134    // https://html.spec.whatwg.org/multipage/#dom-context-2d-rotate
2135    pub(super) fn rotate(&self, angle: f64) {
2136        if angle == 0.0 || !angle.is_finite() {
2137            return;
2138        }
2139
2140        let (sin, cos) = (angle.sin(), angle.cos());
2141        let transform = self.state.borrow().transform;
2142        self.update_transform(Transform2D::new(cos, sin, -sin, cos, 0.0, 0.0).then(&transform))
2143    }
2144
2145    // https://html.spec.whatwg.org/multipage/#dom-context-2d-translate
2146    pub(super) fn translate(&self, x: f64, y: f64) {
2147        if !(x.is_finite() && y.is_finite()) {
2148            return;
2149        }
2150
2151        let transform = self.state.borrow().transform;
2152        self.update_transform(transform.pre_translate(vec2(x, y)))
2153    }
2154
2155    // https://html.spec.whatwg.org/multipage/#dom-context-2d-transform
2156    pub(super) fn transform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
2157        if !(a.is_finite() &&
2158            b.is_finite() &&
2159            c.is_finite() &&
2160            d.is_finite() &&
2161            e.is_finite() &&
2162            f.is_finite())
2163        {
2164            return;
2165        }
2166
2167        let transform = self.state.borrow().transform;
2168        self.update_transform(Transform2D::new(a, b, c, d, e, f).then(&transform))
2169    }
2170
2171    // https://html.spec.whatwg.org/multipage/#dom-context-2d-gettransform
2172    pub(super) fn get_transform(
2173        &self,
2174        global: &GlobalScope,
2175        cx: &mut JSContext,
2176    ) -> DomRoot<DOMMatrix> {
2177        let transform = self.state.borrow_mut().transform;
2178        DOMMatrix::new(global, true, transform.to_3d(), CanGc::from_cx(cx))
2179    }
2180
2181    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform>
2182    pub(super) fn set_transform(&self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) {
2183        // Step 1. If any of the arguments are infinite or NaN, then return.
2184        if !a.is_finite() ||
2185            !b.is_finite() ||
2186            !c.is_finite() ||
2187            !d.is_finite() ||
2188            !e.is_finite() ||
2189            !f.is_finite()
2190        {
2191            return;
2192        }
2193
2194        // Step 2. Reset the current transformation matrix to the matrix described by:
2195        self.update_transform(Transform2D::new(a, b, c, d, e, f))
2196    }
2197
2198    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-settransform-matrix>
2199    pub(super) fn set_transform_(&self, transform: &DOMMatrix2DInit) -> ErrorResult {
2200        // Step 1. Let matrix be the result of creating a DOMMatrix from the 2D
2201        // dictionary transform.
2202        let matrix = dommatrix2dinit_to_matrix(transform)?;
2203
2204        // Step 2. If one or more of matrix's m11 element, m12 element, m21
2205        // element, m22 element, m41 element, or m42 element are infinite or
2206        // NaN, then return.
2207        if !matrix.m11.is_finite() ||
2208            !matrix.m12.is_finite() ||
2209            !matrix.m21.is_finite() ||
2210            !matrix.m22.is_finite() ||
2211            !matrix.m31.is_finite() ||
2212            !matrix.m32.is_finite()
2213        {
2214            return Ok(());
2215        }
2216
2217        // Step 3. Reset the current transformation matrix to matrix.
2218        self.update_transform(matrix.cast());
2219        Ok(())
2220    }
2221
2222    // https://html.spec.whatwg.org/multipage/#dom-context-2d-resettransform
2223    pub(super) fn reset_transform(&self) {
2224        self.update_transform(Transform2D::identity())
2225    }
2226
2227    // https://html.spec.whatwg.org/multipage/#dom-context-2d-closepath
2228    pub(super) fn close_path(&self) {
2229        self.current_default_path.borrow_mut().close_path();
2230    }
2231
2232    // https://html.spec.whatwg.org/multipage/#dom-context-2d-moveto
2233    pub(super) fn move_to(&self, x: f64, y: f64) {
2234        self.current_default_path.borrow_mut().move_to(x, y);
2235    }
2236
2237    // https://html.spec.whatwg.org/multipage/#dom-context-2d-lineto
2238    pub(super) fn line_to(&self, x: f64, y: f64) {
2239        self.current_default_path.borrow_mut().line_to(x, y);
2240    }
2241
2242    // https://html.spec.whatwg.org/multipage/#dom-context-2d-rect
2243    pub(super) fn rect(&self, x: f64, y: f64, width: f64, height: f64) {
2244        self.current_default_path
2245            .borrow_mut()
2246            .rect(x, y, width, height);
2247    }
2248
2249    // https://html.spec.whatwg.org/multipage/#dom-context-2d-quadraticcurveto
2250    pub(super) fn quadratic_curve_to(&self, cpx: f64, cpy: f64, x: f64, y: f64) {
2251        self.current_default_path
2252            .borrow_mut()
2253            .quadratic_curve_to(cpx, cpy, x, y);
2254    }
2255
2256    // https://html.spec.whatwg.org/multipage/#dom-context-2d-beziercurveto
2257    pub(super) fn bezier_curve_to(
2258        &self,
2259        cp1x: f64,
2260        cp1y: f64,
2261        cp2x: f64,
2262        cp2y: f64,
2263        x: f64,
2264        y: f64,
2265    ) {
2266        self.current_default_path
2267            .borrow_mut()
2268            .bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y);
2269    }
2270
2271    // https://html.spec.whatwg.org/multipage/#dom-context-2d-arc
2272    pub(super) fn arc(
2273        &self,
2274        x: f64,
2275        y: f64,
2276        r: f64,
2277        start: f64,
2278        end: f64,
2279        ccw: bool,
2280    ) -> ErrorResult {
2281        self.current_default_path
2282            .borrow_mut()
2283            .arc(x, y, r, start, end, ccw)
2284            .map_err(|_| Error::IndexSize(None))
2285    }
2286
2287    // https://html.spec.whatwg.org/multipage/#dom-context-2d-arcto
2288    pub(super) fn arc_to(&self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, r: f64) -> ErrorResult {
2289        self.current_default_path
2290            .borrow_mut()
2291            .arc_to(cp1x, cp1y, cp2x, cp2y, r)
2292            .map_err(|_| Error::IndexSize(None))
2293    }
2294
2295    /// <https://html.spec.whatwg.org/multipage/#dom-context-2d-ellipse>
2296    #[expect(clippy::too_many_arguments)]
2297    pub(super) fn ellipse(
2298        &self,
2299        x: f64,
2300        y: f64,
2301        rx: f64,
2302        ry: f64,
2303        rotation: f64,
2304        start: f64,
2305        end: f64,
2306        ccw: bool,
2307    ) -> ErrorResult {
2308        self.current_default_path
2309            .borrow_mut()
2310            .ellipse(x, y, rx, ry, rotation, start, end, ccw)
2311            .map_err(|_| Error::IndexSize(None))
2312    }
2313
2314    fn text_with_size(
2315        &self,
2316        global_scope: &GlobalScope,
2317        text: &str,
2318        origin: Point2D<f64>,
2319        size: f64,
2320        max_width: Option<f64>,
2321    ) -> Option<(Rect<f64>, Vec<TextRun>)> {
2322        let Some(font_context) = global_scope.font_context() else {
2323            warn!("Tried to paint to a canvas of GlobalScope without a FontContext.");
2324            return None;
2325        };
2326
2327        // Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
2328        if max_width.is_some_and(|max_width| max_width.is_nan() || max_width <= 0.) {
2329            return None;
2330        }
2331
2332        // > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
2333        let text = replace_ascii_whitespace(text);
2334
2335        // > Step 3: Let font be the current font of target, as given by that object's font
2336        // > attribute.
2337        let font_style = self.font_style();
2338        let font_group = font_context.font_group_with_size(font_style, Au::from_f64_px(size));
2339        let Some(first_font) = font_group.first(font_context) else {
2340            warn!("Could not render canvas text, because there was no first font.");
2341            return None;
2342        };
2343
2344        let runs = self.build_unshaped_text_runs(font_context, &text, &font_group);
2345
2346        // TODO: This doesn't do any kind of line layout at all. In particular, there needs
2347        // to be some alignment along a baseline and also support for bidi text.
2348        let mut total_advance = 0.0;
2349        let mut shaped_runs: Vec<_> = runs
2350            .into_iter()
2351            .filter_map(|unshaped_text_run| {
2352                let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
2353                total_advance += text_run.advance;
2354                Some(text_run)
2355            })
2356            .collect();
2357
2358        // > Step 6: If maxWidth was provided and the hypothetical width of the inline box in the
2359        // > hypothetical line box is greater than maxWidth CSS pixels, then change font to have a
2360        // > more condensed font (if one is available or if a reasonably readable one can be
2361        // > synthesized by applying a horizontal scale factor to the font) or a smaller font, and
2362        // > return to the previous step.
2363        //
2364        // TODO: We only try decreasing the font size here. Eventually it would make sense to use
2365        // other methods to try to decrease the size, such as finding a narrower font or decreasing
2366        // spacing.
2367        let total_advance = total_advance as f64;
2368        if let Some(max_width) = max_width {
2369            let new_size = (max_width / total_advance * size).floor().max(5.);
2370            if total_advance > max_width && new_size != size {
2371                return self.text_with_size(global_scope, &text, origin, new_size, Some(max_width));
2372            }
2373        }
2374
2375        // > Step 7: Find the anchor point for the line of text.
2376        let start =
2377            self.find_anchor_point_for_line_of_text(origin, &first_font.metrics, total_advance);
2378
2379        // > Step 8: Let result be an array constructed by iterating over each glyph in the inline box
2380        // > from left to right (if any), adding to the array, for each glyph, the shape of the glyph
2381        // > as it is in the inline box, positioned on a coordinate space using CSS pixels with its
2382        // > origin is at the anchor point.
2383        let mut bounds = None;
2384        for text_run in shaped_runs.iter_mut() {
2385            for glyph_and_position in text_run.glyphs_and_positions.iter_mut() {
2386                glyph_and_position.point += Vector2D::new(start.x as f32, start.y as f32);
2387            }
2388            bounds
2389                .get_or_insert(text_run.bounds)
2390                .union(&text_run.bounds);
2391        }
2392
2393        Some((
2394            bounds
2395                .unwrap_or_default()
2396                .translate(start.to_vector().cast_unit()),
2397            shaped_runs,
2398        ))
2399    }
2400
2401    fn build_unshaped_text_runs<'text>(
2402        &self,
2403        font_context: &FontContext,
2404        text: &'text str,
2405        font_group: &FontGroup,
2406    ) -> Vec<UnshapedTextRun<'text>> {
2407        let mut runs = Vec::new();
2408        // TODO: canvas also has experimental `lang` attribute (https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lang),
2409        // which Servo doesn't support yet. When this attribute is supported, some changes may be needed here.
2410        let x_language = self.font_style()._x_lang.clone();
2411        let language = x_language.0.parse().unwrap_or(Language::UND);
2412        let mut current_text_run = UnshapedTextRun::new(language);
2413        let mut current_text_run_start_index = 0;
2414
2415        // Variation Selectors (U+FE00–U+FE0F) and Variation Selectors Supplement (U+E0100–U+E01EF)
2416        // are used to select specific glyph variants (e.g., text vs. emoji presentation).
2417        // They must be attached to the preceding base character and do not start new runs.
2418        // - VS1–VS16 (U+FE00..U+FE0F) in the Variation Selectors block.
2419        // - VS17–VS256 (U+E0100..U+E01EF) in the Variation Selectors Supplement block.
2420        fn is_variation_selector(c: char) -> bool {
2421            matches!(c as u32, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
2422        }
2423
2424        for (index, character) in text.char_indices() {
2425            let next_char = text[index + character.len_utf8()..].chars().next();
2426
2427            let script = Script::from(character);
2428
2429            let font = font_group.find_by_codepoint(font_context, character, next_char, language);
2430
2431            if !is_variation_selector(character) &&
2432                !current_text_run.script_and_font_compatible(script, &font)
2433            {
2434                let previous_text_run = std::mem::replace(
2435                    &mut current_text_run,
2436                    UnshapedTextRun {
2437                        font: font.clone(),
2438                        script,
2439                        string: Default::default(),
2440                        language,
2441                    },
2442                );
2443                current_text_run_start_index = index;
2444                if !previous_text_run.string.is_empty() && previous_text_run.font.is_some() {
2445                    runs.push(previous_text_run);
2446                }
2447            }
2448
2449            current_text_run.string =
2450                &text[current_text_run_start_index..index + character.len_utf8()];
2451        }
2452
2453        if !current_text_run.string.is_empty() && current_text_run.font.is_some() {
2454            runs.push(current_text_run);
2455        }
2456        runs
2457    }
2458
2459    /// Find the *anchor_point* for the given parameters of a line of text.
2460    /// See <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>.
2461    fn find_anchor_point_for_line_of_text(
2462        &self,
2463        origin: Point2D<f64>,
2464        metrics: &FontMetrics,
2465        width: f64,
2466    ) -> Point2D<f64> {
2467        let state = self.state.borrow();
2468        let is_rtl = match state.direction {
2469            CanvasDirection::Ltr => false,
2470            CanvasDirection::Rtl => true,
2471            CanvasDirection::Inherit => false, // TODO: resolve direction wrt to canvas element
2472        };
2473
2474        let text_align = match self.text_align() {
2475            CanvasTextAlign::Start if is_rtl => CanvasTextAlign::Right,
2476            CanvasTextAlign::Start => CanvasTextAlign::Left,
2477            CanvasTextAlign::End if is_rtl => CanvasTextAlign::Left,
2478            CanvasTextAlign::End => CanvasTextAlign::Right,
2479            text_align => text_align,
2480        };
2481        let anchor_x = match text_align {
2482            CanvasTextAlign::Center => -width / 2.,
2483            CanvasTextAlign::Right => -width,
2484            _ => 0.,
2485        };
2486
2487        let ascent = metrics.ascent.to_f64_px();
2488        let descent = metrics.descent.to_f64_px();
2489        let anchor_y = match self.text_baseline() {
2490            CanvasTextBaseline::Top => ascent,
2491            CanvasTextBaseline::Hanging => ascent * HANGING_BASELINE_DEFAULT,
2492            CanvasTextBaseline::Ideographic => -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
2493            CanvasTextBaseline::Middle => (ascent - descent) / 2.,
2494            CanvasTextBaseline::Alphabetic => 0.,
2495            CanvasTextBaseline::Bottom => -descent,
2496        };
2497
2498        origin + Vector2D::new(anchor_x, anchor_y)
2499    }
2500}
2501
2502impl Drop for CanvasState {
2503    fn drop(&mut self) {
2504        if let Err(err) = self
2505            .canvas_thread_sender
2506            .send(CanvasMsg::Close(self.canvas_id))
2507        {
2508            warn!("Could not close canvas: {}", err)
2509        }
2510    }
2511}
2512
2513struct UnshapedTextRun<'a> {
2514    font: Option<FontRef>,
2515    script: Script,
2516    string: &'a str,
2517    language: Language,
2518}
2519
2520impl UnshapedTextRun<'_> {
2521    fn new(language: Language) -> Self {
2522        Self {
2523            font: Default::default(),
2524            script: Default::default(),
2525            string: Default::default(),
2526            language,
2527        }
2528    }
2529
2530    fn script_and_font_compatible(&self, script: Script, other_font: &Option<FontRef>) -> bool {
2531        if self.script != script {
2532            return false;
2533        }
2534
2535        match (&self.font, other_font) {
2536            (Some(font_a), Some(font_b)) => font_a.identifier() == font_b.identifier(),
2537            (None, None) => true,
2538            _ => false,
2539        }
2540    }
2541
2542    fn into_shaped_text_run(self, previous_advance: f32) -> Option<TextRun> {
2543        debug_assert!(!self.string.is_empty() && self.font.is_some());
2544        let font = self.font?;
2545
2546        let options = ShapingOptions {
2547            letter_spacing: None,
2548            word_spacing: None,
2549            script: self.script,
2550            language: self.language,
2551            flags: ShapingFlags::empty(),
2552        };
2553
2554        let glyphs = font.shape_text(self.string, &options);
2555
2556        let mut advance = 0.0;
2557        let mut bounds = None;
2558        let glyphs_and_positions = glyphs
2559            .glyphs()
2560            .map(|glyph| {
2561                let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
2562                let glyph_and_position = GlyphAndPosition {
2563                    id: glyph.id(),
2564                    point: Point2D::new(previous_advance + advance, glyph_offset.y.to_f32_px()),
2565                };
2566
2567                let glyph_bounds = font
2568                    .typographic_bounds(glyph.id())
2569                    .translate(Vector2D::new(advance + previous_advance, 0.0));
2570                bounds = Some(bounds.get_or_insert(glyph_bounds).union(&glyph_bounds));
2571
2572                advance += glyph.advance().to_f32_px();
2573
2574                glyph_and_position
2575            })
2576            .collect();
2577
2578        let identifier = font.identifier();
2579        let font_data = match &identifier {
2580            FontIdentifier::Local(_) => None,
2581            FontIdentifier::Web(_) | FontIdentifier::ArrayBuffer(_) => {
2582                Some(font.font_data_and_index().ok()?)
2583            },
2584        }
2585        .cloned();
2586        let canvas_font = CanvasFont {
2587            identifier,
2588            data: font_data,
2589        };
2590
2591        Some(TextRun {
2592            font: canvas_font,
2593            pt_size: font.descriptor.pt_size.to_f32_px(),
2594            glyphs_and_positions,
2595            advance,
2596            bounds: bounds.unwrap_or_default().cast(),
2597        })
2598    }
2599}
2600
2601pub(super) fn parse_color(
2602    canvas: Option<&HTMLCanvasElement>,
2603    string: &DOMString,
2604) -> Result<AbsoluteColor, ()> {
2605    let string = string.str();
2606    let mut input = ParserInput::new(&string);
2607    let mut parser = Parser::new(&mut input);
2608    let url = Url::parse("about:blank").unwrap().into();
2609    let context =
2610        parser_context_for_anonymous_content(CssRuleType::Style, ParsingMode::DEFAULT, &url);
2611    match Color::parse_and_compute(&context, &mut parser, None) {
2612        Some(color) => {
2613            // TODO: https://github.com/whatwg/html/issues/1099
2614            // Reconsider how to calculate currentColor in a display:none canvas
2615
2616            // TODO: will need to check that the context bitmap mode is fixed
2617            // once we implement CanvasProxy
2618            let current_color = match canvas {
2619                // https://drafts.css-houdini.org/css-paint-api/#2d-rendering-context
2620                // Whenever "currentColor" is used as a color in the PaintRenderingContext2D API,
2621                // it is treated as opaque black.
2622                None => AbsoluteColor::BLACK,
2623                Some(canvas) => {
2624                    let canvas_element = canvas.upcast::<Element>();
2625                    match canvas_element.style() {
2626                        Some(ref s) if canvas_element.has_css_layout_box() => {
2627                            s.get_inherited_text().color
2628                        },
2629                        _ => AbsoluteColor::BLACK,
2630                    }
2631                },
2632            };
2633
2634            Ok(color.resolve_to_absolute(&current_color))
2635        },
2636        None => Err(()),
2637    }
2638}
2639
2640// Used by drawImage to determine if a source or destination rectangle is valid
2641// Origin coordinates and size cannot be negative. Size has to be greater than zero
2642pub(super) fn is_rect_valid(rect: Rect<f64>) -> bool {
2643    rect.size.width > 0.0 && rect.size.height > 0.0
2644}
2645
2646// https://html.spec.whatwg.org/multipage/#serialisation-of-a-color
2647pub(super) fn serialize<W>(color: &AbsoluteColor, dest: &mut W) -> fmt::Result
2648where
2649    W: fmt::Write,
2650{
2651    let srgb = match color.color_space {
2652        ColorSpace::Srgb if color.flags.contains(ColorFlags::IS_LEGACY_SRGB) => *color,
2653        ColorSpace::Hsl | ColorSpace::Hwb => color.into_srgb_legacy(),
2654        _ => return color.to_css(&mut CssWriter::new(dest)),
2655    };
2656    debug_assert!(srgb.flags.contains(ColorFlags::IS_LEGACY_SRGB));
2657    let red = clamp_unit_f32(srgb.components.0);
2658    let green = clamp_unit_f32(srgb.components.1);
2659    let blue = clamp_unit_f32(srgb.components.2);
2660    let alpha = srgb.alpha;
2661    if alpha == 1.0 {
2662        write!(
2663            dest,
2664            "#{:x}{:x}{:x}{:x}{:x}{:x}",
2665            red >> 4,
2666            red & 0xF,
2667            green >> 4,
2668            green & 0xF,
2669            blue >> 4,
2670            blue & 0xF
2671        )
2672    } else {
2673        write!(dest, "rgba({}, {}, {}, {})", red, green, blue, alpha)
2674    }
2675}
2676
2677pub(super) fn adjust_size_sign(
2678    mut origin: Point2D<i32>,
2679    mut size: Size2D<i32>,
2680) -> (Point2D<i32>, Size2D<u32>) {
2681    if size.width < 0 {
2682        size.width = -size.width;
2683        origin.x = origin.x.saturating_sub(size.width);
2684    }
2685    if size.height < 0 {
2686        size.height = -size.height;
2687        origin.y = origin.y.saturating_sub(size.height);
2688    }
2689    (origin, size.to_u32())
2690}
2691
2692fn serialize_font<W>(style: &Font, dest: &mut W) -> fmt::Result
2693where
2694    W: fmt::Write,
2695{
2696    if style.font_style == FontStyle::ITALIC {
2697        write!(dest, "{} ", style.font_style.to_css_string())?;
2698    }
2699    if style.font_weight.is_bold() {
2700        write!(dest, "{} ", style.font_weight.to_css_string())?;
2701    }
2702    if style.font_variant_caps == FontVariantCaps::SmallCaps {
2703        write!(dest, "{} ", style.font_variant_caps.to_css_string())?;
2704    }
2705    write!(
2706        dest,
2707        "{} {}",
2708        style.font_size.to_css_string(),
2709        style.font_family.to_css_string()
2710    )
2711}
2712
2713fn adjust_canvas_size(size: Size2D<u64>) -> Size2D<u64> {
2714    // Firefox limits width/height to 32767 pixels and Chromium to 65535 pixels,
2715    // but slows down dramatically before it reaches that limit.
2716    // We limit by area instead, giving us larger maximum dimensions,
2717    // in exchange for a smaller maximum canvas size.
2718    const MAX_CANVAS_AREA: u64 = 32768 * 8192;
2719    // Max width/height to 65535 in CSS pixels.
2720    const MAX_CANVAS_SIZE: u64 = 65535;
2721
2722    if !size.is_empty() &&
2723        size.greater_than(Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE))
2724            .none() &&
2725        size.area() < MAX_CANVAS_AREA
2726    {
2727        size
2728    } else {
2729        Size2D::zero()
2730    }
2731}
2732
2733impl Convert<FillRule> for CanvasFillRule {
2734    fn convert(self) -> FillRule {
2735        match self {
2736            CanvasFillRule::Nonzero => FillRule::Nonzero,
2737            CanvasFillRule::Evenodd => FillRule::Evenodd,
2738        }
2739    }
2740}
2741
2742fn replace_ascii_whitespace(text: &str) -> String {
2743    text.chars()
2744        .map(|c| match c {
2745            ' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
2746            _ => c,
2747        })
2748        .collect()
2749}