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