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