Skip to main content

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

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