Skip to main content

script/dom/html/embedded_content/
htmlcanvaselement.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, RefCell};
6use std::rc::Rc;
7
8use dom_struct::dom_struct;
9use euclid::default::Size2D;
10use html5ever::{LocalName, Prefix, local_name, ns};
11use js::context::NoGC;
12use js::error::throw_type_error;
13use js::rust::{HandleObject, HandleValue};
14use layout_api::HTMLCanvasData;
15use pixels::{EncodedImageType, Snapshot};
16use rustc_hash::FxHashMap;
17use script_bindings::cell::{DomRefCell, Ref};
18#[cfg(feature = "webgl")]
19use script_bindings::reflector::DomObject;
20use script_bindings::weakref::WeakRef;
21use servo_base::Epoch;
22#[cfg(feature = "webgl")]
23use servo_canvas_traits::webgl::{GLContextAttributes, WebGLVersion};
24use servo_constellation_traits::BlobImpl;
25#[cfg(feature = "webgpu")]
26use servo_constellation_traits::ScriptToConstellationMessage;
27use servo_media::streams::MediaStreamType;
28use servo_media::streams::registry::MediaStreamId;
29use style::attr::AttrValue;
30use webrender_api::ImageKey;
31
32use crate::canvas_context::{CanvasContext, RenderingContext};
33#[cfg(feature = "webgl")]
34use crate::conversions::Convert;
35use crate::dom::bindings::callback::ExceptionHandling;
36use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::CanvasRenderingContext2DSettings;
37use crate::dom::bindings::codegen::Bindings::HTMLCanvasElementBinding::{
38    BlobCallback, HTMLCanvasElementMethods, RenderingContext as RootedRenderingContext,
39};
40use crate::dom::bindings::codegen::Bindings::MediaStreamBinding::MediaStreamMethods;
41#[cfg(feature = "webgl")]
42use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLContextAttributes;
43use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas as RootedHTMLCanvasElementOrOffscreenCanvas;
44use crate::dom::bindings::conversions::ConversionResult;
45use crate::dom::bindings::error::{Error, Fallible};
46use crate::dom::bindings::inheritance::Castable;
47use crate::dom::bindings::num::Finite;
48use crate::dom::bindings::refcounted::Trusted;
49use crate::dom::bindings::reflector::DomGlobal;
50use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom};
51use crate::dom::bindings::str::{DOMString, USVString};
52use crate::dom::blob::Blob;
53use crate::dom::canvasrenderingcontext2d::CanvasRenderingContext2D;
54use crate::dom::document::Document;
55use crate::dom::element::attributes::storage::AttrRef;
56use crate::dom::element::{AttributeMutation, Element};
57#[cfg(not(feature = "webgpu"))]
58use crate::dom::gpucanvascontext::GPUCanvasContext;
59use crate::dom::html::htmlelement::HTMLElement;
60use crate::dom::imagebitmaprenderingcontext::ImageBitmapRenderingContext;
61use crate::dom::mediastream::MediaStream;
62use crate::dom::mediastreamtrack::MediaStreamTrack;
63use crate::dom::node::virtualmethods::VirtualMethods;
64use crate::dom::node::{Node, NodeDamage, NodeTraits};
65use crate::dom::offscreencanvas::OffscreenCanvas;
66use crate::dom::values::UNSIGNED_LONG_MAX;
67#[cfg(feature = "webgl")]
68use crate::dom::webgl::webgl2renderingcontext::WebGL2RenderingContext;
69#[cfg(feature = "webgl")]
70use crate::dom::webgl::webglrenderingcontext::WebGLRenderingContext;
71#[cfg(feature = "webgpu")]
72use crate::dom::webgpu::gpucanvascontext::GPUCanvasContext;
73
74const DEFAULT_WIDTH: u32 = 300;
75const DEFAULT_HEIGHT: u32 = 150;
76
77/// <https://html.spec.whatwg.org/multipage/#htmlcanvaselement>
78#[dom_struct]
79pub(crate) struct HTMLCanvasElement {
80    htmlelement: HTMLElement,
81
82    /// <https://html.spec.whatwg.org/multipage/#concept-canvas-context-mode>
83    context_mode: DomRefCell<Option<RenderingContext>>,
84
85    /// This id along with [`Self::blob_callbacks`] are used to keep track of ongoing toBlob() calls.
86    callback_id: Cell<u32>,
87
88    /// This hashmap along with [`Self::callback_id`] are used to keep track of ongoing toBlob() calls.
89    #[conditional_malloc_size_of]
90    blob_callbacks: RefCell<FxHashMap<u32, Rc<BlobCallback>>>,
91
92    /// The [`ImageKey`] used to render this [`HTMLCanvasElement`] to the WebRender scene, if it
93    /// has a `RenderingContext`, otherwise `None`. Note that this key is owned by the `RenderingContext`
94    /// itself which will take care of cleaning it up.
95    #[no_trace]
96    image_key: Cell<Option<ImageKey>>,
97}
98
99impl HTMLCanvasElement {
100    fn new_inherited(
101        local_name: LocalName,
102        prefix: Option<Prefix>,
103        document: &Document,
104    ) -> HTMLCanvasElement {
105        HTMLCanvasElement {
106            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
107            context_mode: DomRefCell::new(None),
108            callback_id: Cell::new(0),
109            blob_callbacks: RefCell::new(FxHashMap::default()),
110            image_key: Default::default(),
111        }
112    }
113
114    pub(crate) fn new(
115        cx: &mut js::context::JSContext,
116        local_name: LocalName,
117        prefix: Option<Prefix>,
118        document: &Document,
119        proto: Option<HandleObject>,
120    ) -> DomRoot<HTMLCanvasElement> {
121        Node::reflect_weak_referenceable_node_with_proto(
122            cx,
123            Rc::new(HTMLCanvasElement::new_inherited(
124                local_name, prefix, document,
125            )),
126            document,
127            proto,
128        )
129    }
130
131    fn recreate_contexts_after_resize(&self) {
132        if let Some(ref context) = *self.context_mode.borrow() {
133            context.resize()
134        }
135    }
136
137    pub(crate) fn get_size(&self) -> Size2D<u32> {
138        Size2D::new(self.Width(), self.Height())
139    }
140
141    pub(crate) fn origin_is_clean(&self) -> bool {
142        match *self.context_mode.borrow() {
143            Some(ref context) => context.origin_is_clean(),
144            _ => true,
145        }
146    }
147
148    pub(crate) fn mark_as_dirty(&self) {
149        if let Some(ref context) = *self.context_mode.borrow() {
150            context.mark_as_dirty()
151        }
152    }
153
154    pub(crate) fn set_natural_width(&self, cx: &mut js::context::JSContext, value: u32) {
155        let value = if value > UNSIGNED_LONG_MAX {
156            DEFAULT_WIDTH
157        } else {
158            value
159        };
160        self.upcast::<Element>()
161            .set_attribute(cx, &html5ever::local_name!("width"), value.into());
162    }
163
164    pub(crate) fn set_natural_height(&self, cx: &mut js::context::JSContext, value: u32) {
165        let value = if value > UNSIGNED_LONG_MAX {
166            DEFAULT_HEIGHT
167        } else {
168            value
169        };
170        self.upcast::<Element>()
171            .set_attribute(cx, &html5ever::local_name!("height"), value.into());
172    }
173}
174
175impl LayoutDom<'_, HTMLCanvasElement> {
176    pub(crate) fn data(self) -> HTMLCanvasData {
177        let width_attr = self
178            .upcast::<Element>()
179            .get_attr_for_layout(&ns!(), &local_name!("width"));
180        let height_attr = self
181            .upcast::<Element>()
182            .get_attr_for_layout(&ns!(), &local_name!("height"));
183        HTMLCanvasData {
184            image_key: self.unsafe_get().image_key.get(),
185            width: width_attr.map_or(DEFAULT_WIDTH, |val| val.as_uint()),
186            height: height_attr.map_or(DEFAULT_HEIGHT, |val| val.as_uint()),
187        }
188    }
189}
190
191impl HTMLCanvasElement {
192    pub(crate) fn context(&self) -> Option<Ref<'_, RenderingContext>> {
193        Ref::filter_map(self.context_mode.borrow(), |ctx| ctx.as_ref()).ok()
194    }
195
196    fn set_rendering_context(
197        &self,
198        no_gc: &NoGC,
199        make_rendering_context: impl FnOnce() -> RenderingContext,
200    ) {
201        self.upcast::<Node>()
202            .dirty(no_gc, NodeDamage::ContentOrHeritage);
203        self.context_mode
204            .borrow_mut()
205            .replace(make_rendering_context());
206
207        let Some(rendering_context) = &*self.context_mode.borrow() else {
208            return;
209        };
210
211        let get_image_key = || self.owner_window().image_cache().get_image_key();
212        let image_key = match rendering_context {
213            RenderingContext::Placeholder(..) => None,
214            RenderingContext::Context2d(..) => get_image_key(),
215            RenderingContext::BitmapRenderer(..) => get_image_key(),
216            #[cfg(feature = "webgl")]
217            RenderingContext::WebGL(..) => get_image_key(),
218            #[cfg(feature = "webgl")]
219            RenderingContext::WebGL2(..) => get_image_key(),
220            #[cfg(feature = "webgpu")]
221            RenderingContext::WebGPU(..) => get_image_key(),
222        };
223        self.image_key.set(image_key);
224        if let Some(image_key) = image_key {
225            rendering_context.set_image_key(image_key);
226        }
227    }
228
229    /// <https://html.spec.whatwg.org/multipage/#2d-context-creation-algorithm>
230    fn get_or_init_2d_context(
231        &self,
232        cx: &mut js::context::JSContext,
233        options: HandleValue,
234    ) -> Option<DomRoot<CanvasRenderingContext2D>> {
235        if let Some(ctx) = self.context() {
236            return match *ctx {
237                RenderingContext::Context2d(ref ctx) => Some(DomRoot::from_ref(ctx)),
238                _ => None,
239            };
240        }
241
242        let window = self.owner_window();
243        let size = self.get_size();
244
245        // Step 1. Let settings be the result of converting options to the dictionary type
246        // CanvasRenderingContext2DSettings. (This can throw an exception.)
247        let settings = match CanvasRenderingContext2DSettings::new(cx, options) {
248            Ok(ConversionResult::Success(settings)) => settings,
249            Ok(ConversionResult::Failure(error)) => {
250                throw_type_error(cx, &error);
251                return None;
252            },
253            Err(()) => return None,
254        };
255        let context =
256            CanvasRenderingContext2D::new(cx, window.as_global_scope(), self, size, &settings)?;
257        self.set_rendering_context(cx.no_gc(), || {
258            RenderingContext::Context2d(Dom::from_ref(&*context))
259        });
260        Some(context)
261    }
262
263    /// <https://html.spec.whatwg.org/multipage/#canvas-context-bitmaprenderer>
264    fn get_or_init_bitmaprenderer_context(
265        &self,
266        cx: &mut js::context::JSContext,
267    ) -> Option<DomRoot<ImageBitmapRenderingContext>> {
268        // Return the same object as was returned the last time the method was
269        // invoked with this same first argument.
270        if let Some(ctx) = self.context() {
271            return match *ctx {
272                RenderingContext::BitmapRenderer(ref ctx) => Some(DomRoot::from_ref(ctx)),
273                _ => None,
274            };
275        }
276
277        // Step 1. Let context be the result of running the
278        // ImageBitmapRenderingContext creation algorithm given this and
279        // options.
280        let canvas =
281            RootedHTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(self));
282
283        // Step 2. Set this's context mode to bitmaprenderer.
284        let context = ImageBitmapRenderingContext::new(cx, &self.owner_global(), &canvas);
285        self.set_rendering_context(cx.no_gc(), || {
286            RenderingContext::BitmapRenderer(Dom::from_ref(&*context))
287        });
288
289        // Step 3. Return context.
290        Some(context)
291    }
292
293    #[cfg(feature = "webgl")]
294    fn get_or_init_webgl_context(
295        &self,
296        cx: &mut js::context::JSContext,
297        options: HandleValue,
298    ) -> Option<DomRoot<WebGLRenderingContext>> {
299        if let Some(ctx) = self.context() {
300            return match *ctx {
301                RenderingContext::WebGL(ref ctx) => Some(DomRoot::from_ref(ctx)),
302                _ => None,
303            };
304        }
305        let window = self.owner_window();
306        let canvas =
307            RootedHTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(self));
308        let size = self.get_size();
309        let attrs = Self::get_gl_attributes(cx, options)?;
310        let context =
311            WebGLRenderingContext::new(cx, &window, &canvas, WebGLVersion::WebGL1, size, attrs)?;
312        self.set_rendering_context(cx.no_gc(), || {
313            RenderingContext::WebGL(Dom::from_ref(&*context))
314        });
315        Some(context)
316    }
317
318    #[cfg(feature = "webgl")]
319    fn get_or_init_webgl2_context(
320        &self,
321        cx: &mut js::context::JSContext,
322        options: HandleValue,
323    ) -> Option<DomRoot<WebGL2RenderingContext>> {
324        if !WebGL2RenderingContext::is_webgl2_enabled(cx, self.global().reflector().get_jsobject())
325        {
326            return None;
327        }
328        if let Some(ctx) = self.context() {
329            return match *ctx {
330                RenderingContext::WebGL2(ref ctx) => Some(DomRoot::from_ref(ctx)),
331                _ => None,
332            };
333        }
334        let window = self.owner_window();
335        let canvas =
336            RootedHTMLCanvasElementOrOffscreenCanvas::HTMLCanvasElement(DomRoot::from_ref(self));
337        let size = self.get_size();
338        let attrs = Self::get_gl_attributes(cx, options)?;
339        let context = WebGL2RenderingContext::new(cx, &window, &canvas, size, attrs)?;
340        self.set_rendering_context(cx.no_gc(), || {
341            RenderingContext::WebGL2(Dom::from_ref(&*context))
342        });
343        Some(context)
344    }
345
346    #[cfg(not(feature = "webgpu"))]
347    fn get_or_init_webgpu_context(&self) -> Option<DomRoot<GPUCanvasContext>> {
348        None
349    }
350
351    #[cfg(feature = "webgpu")]
352    fn get_or_init_webgpu_context(
353        &self,
354        cx: &mut js::context::JSContext,
355    ) -> Option<DomRoot<GPUCanvasContext>> {
356        use servo_base::generic_channel;
357
358        if let Some(ctx) = self.context() {
359            return match *ctx {
360                RenderingContext::WebGPU(ref ctx) => Some(DomRoot::from_ref(ctx)),
361                _ => None,
362            };
363        }
364        let (sender, receiver) = generic_channel::channel().unwrap();
365        let global_scope = self.owner_global();
366        let _ = global_scope
367            .script_to_constellation_chan()
368            .send(ScriptToConstellationMessage::GetWebGPUChan(sender));
369        receiver
370            .recv()
371            .expect("Failed to get WebGPU channel")
372            .map(|channel| {
373                let context = GPUCanvasContext::new(cx, &global_scope, self, channel);
374                self.set_rendering_context(cx.no_gc(), || {
375                    RenderingContext::WebGPU(Dom::from_ref(&*context))
376                });
377                context
378            })
379    }
380
381    #[cfg(feature = "webgl")]
382    fn get_gl_attributes(
383        cx: &mut js::context::JSContext,
384        options: HandleValue,
385    ) -> Option<GLContextAttributes> {
386        match WebGLContextAttributes::new(cx, options) {
387            Ok(ConversionResult::Success(attrs)) => Some(attrs.convert()),
388            Ok(ConversionResult::Failure(error)) => {
389                throw_type_error(cx, &error);
390                None
391            },
392            _ => {
393                debug!("Unexpected error on conversion of WebGLContextAttributes");
394                None
395            },
396        }
397    }
398
399    pub(crate) fn is_valid(&self) -> bool {
400        self.Height() != 0 && self.Width() != 0
401    }
402
403    pub(crate) fn get_image_data(&self) -> Option<Snapshot> {
404        match self.context_mode.borrow().as_ref() {
405            Some(context) => context.get_image_data(),
406            None => {
407                let size = self.get_size();
408                if size.is_empty() ||
409                    pixels::compute_rgba8_byte_length_if_within_limit(
410                        size.width as usize,
411                        size.height as usize,
412                    )
413                    .is_none()
414                {
415                    None
416                } else {
417                    Some(Snapshot::cleared(size.cast()))
418                }
419            },
420        }
421    }
422
423    fn maybe_quality(quality: HandleValue) -> Option<f64> {
424        if quality.is_number() {
425            Some(quality.to_number())
426        } else {
427            None
428        }
429    }
430
431    pub(crate) fn update_rendering(&self, epoch: Epoch) -> Option<ImageKey> {
432        let context = self.context()?;
433        let image_key = self.image_key.get()?;
434        let pending = match &*context {
435            RenderingContext::Placeholder(..) => false,
436            RenderingContext::Context2d(context) => context.update_rendering(epoch),
437            RenderingContext::BitmapRenderer(context) => context.update_rendering(epoch),
438            #[cfg(feature = "webgl")]
439            RenderingContext::WebGL(context) => context.update_rendering(epoch),
440            #[cfg(feature = "webgl")]
441            RenderingContext::WebGL2(context) => context.base_context().update_rendering(epoch),
442            #[cfg(feature = "webgpu")]
443            RenderingContext::WebGPU(context) => context.update_rendering(epoch),
444        };
445
446        if pending {
447            return Some(image_key);
448        }
449        None
450    }
451}
452
453impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
454    // https://html.spec.whatwg.org/multipage/#dom-canvas-width
455    make_uint_getter!(Width, "width", DEFAULT_WIDTH);
456
457    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-width>
458    fn SetWidth(&self, cx: &mut js::context::JSContext, value: u32) -> Fallible<()> {
459        // > When setting the value of the width or height attribute, if the context mode of the canvas element
460        // > is set to placeholder, the user agent must throw an "InvalidStateError" DOMException and leave the
461        // > attribute's value unchanged.
462        if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
463            return Err(Error::InvalidState(Some(
464                "Canvas element's context mode is set to placeholder: Cannot set width".into(),
465            )));
466        }
467
468        let value = if value > UNSIGNED_LONG_MAX {
469            DEFAULT_WIDTH
470        } else {
471            value
472        };
473        self.upcast::<Element>()
474            .set_attribute(cx, &html5ever::local_name!("width"), value.into());
475        Ok(())
476    }
477
478    // https://html.spec.whatwg.org/multipage/#dom-canvas-height
479    make_uint_getter!(Height, "height", DEFAULT_HEIGHT);
480
481    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-height>
482    fn SetHeight(&self, cx: &mut js::context::JSContext, value: u32) -> Fallible<()> {
483        // > When setting the value of the width or height attribute, if the context mode of the canvas element
484        // > is set to placeholder, the user agent must throw an "InvalidStateError" DOMException and leave the
485        // > attribute's value unchanged.
486        if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
487            return Err(Error::InvalidState(Some(
488                "Canvas element's context mode is set to placeholder: Cannot set height".into(),
489            )));
490        }
491
492        let value = if value > UNSIGNED_LONG_MAX {
493            DEFAULT_HEIGHT
494        } else {
495            value
496        };
497        self.upcast::<Element>()
498            .set_attribute(cx, &html5ever::local_name!("height"), value.into());
499        Ok(())
500    }
501
502    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-getcontext>
503    fn GetContext(
504        &self,
505        cx: &mut js::context::JSContext,
506        id: DOMString,
507        options: HandleValue,
508    ) -> Fallible<Option<RootedRenderingContext>> {
509        // Step 1. If options is not an object, then set options to null.
510        let options = if options.get().is_object() {
511            options
512        } else {
513            HandleValue::null()
514        };
515
516        // Always throw an InvalidState exception when the canvas is in Placeholder mode (See table in the spec).
517        if let Some(RenderingContext::Placeholder(_)) = *self.context_mode.borrow() {
518            return Err(Error::InvalidState(Some(
519                "Canvas element's context mode is set to placeholder: Cannot get context".into(),
520            )));
521        }
522
523        Ok(match &*id.str() {
524            "2d" => self
525                .get_or_init_2d_context(cx, options)
526                .map(RootedRenderingContext::CanvasRenderingContext2D),
527            "bitmaprenderer" => self
528                .get_or_init_bitmaprenderer_context(cx)
529                .map(RootedRenderingContext::ImageBitmapRenderingContext),
530            #[cfg(feature = "webgl")]
531            "webgl" | "experimental-webgl" => self
532                .get_or_init_webgl_context(cx, options)
533                .map(RootedRenderingContext::WebGLRenderingContext),
534            #[cfg(feature = "webgl")]
535            "webgl2" | "experimental-webgl2" => self
536                .get_or_init_webgl2_context(cx, options)
537                .map(RootedRenderingContext::WebGL2RenderingContext),
538            #[cfg(feature = "webgpu")]
539            "webgpu" => self
540                .get_or_init_webgpu_context(cx)
541                .map(RootedRenderingContext::GPUCanvasContext),
542            _ => None,
543        })
544    }
545
546    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl>
547    fn ToDataURL(&self, mime_type: DOMString, quality: HandleValue) -> Fallible<USVString> {
548        // Step 1: If this canvas element's bitmap's origin-clean flag is set to false,
549        // then throw a "SecurityError" DOMException.
550        if !self.origin_is_clean() {
551            return Err(Error::Security(Some("Canvas bitmap is not clean".into())));
552        }
553
554        // Step 2: If this canvas element's bitmap has no pixels (i.e. either its
555        // horizontal dimension or its vertical dimension is zero), then return the string
556        // "data:,". (This is the shortest data: URL; it represents the empty string in a
557        // text/plain resource.)
558        if self.Width() == 0 || self.Height() == 0 {
559            return Ok(USVString("data:,".into()));
560        }
561
562        // Step 3: Let file be a serialization of this canvas element's bitmap as a file,
563        // passing type and quality if given.
564        let Some(mut snapshot) = self.get_image_data() else {
565            return Ok(USVString("data:,".into()));
566        };
567
568        let image_type = EncodedImageType::from(&mime_type.str() as &str);
569
570        let mut url = format!("data:{};base64,", image_type.as_mime_type());
571
572        let mut encoder = base64::write::EncoderStringWriter::from_consumer(
573            &mut url,
574            &base64::engine::general_purpose::STANDARD,
575        );
576
577        if snapshot
578            .encode_for_mime_type(&image_type, Self::maybe_quality(quality), &mut encoder)
579            .is_err()
580        {
581            // Step 4. If file is null, then return "data:,".
582            return Ok(USVString("data:,".into()));
583        }
584
585        // Step 5. Return a data: URL representing file. [RFC2397]
586        encoder.into_inner();
587        Ok(USVString(url))
588    }
589
590    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-toblob>
591    fn ToBlob(
592        &self,
593        callback: Rc<BlobCallback>,
594        mime_type: DOMString,
595        quality: HandleValue,
596    ) -> Fallible<()> {
597        // Step 1.
598        // If this canvas element's bitmap's origin-clean flag is set to false, then throw a
599        // "SecurityError" DOMException.
600        if !self.origin_is_clean() {
601            return Err(Error::Security(Some("Canvas bitmap is not clean".into())));
602        }
603
604        // Step 2. Let result be null.
605        // Step 3. If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension
606        // nor its vertical dimension is zero),
607        // then set result to a copy of this canvas element's bitmap.
608        let result = if self.Width() == 0 || self.Height() == 0 {
609            None
610        } else {
611            self.get_image_data()
612        };
613
614        let this = Trusted::new(self);
615        let callback_id = self.callback_id.get().wrapping_add(1);
616        self.callback_id.set(callback_id);
617
618        self.blob_callbacks
619            .borrow_mut()
620            .insert(callback_id, callback);
621        let quality = Self::maybe_quality(quality);
622        let image_type = EncodedImageType::from(&mime_type.str() as &str);
623
624        self.global()
625            .task_manager()
626            .canvas_blob_task_source()
627            .queue(task!(to_blob: move |cx| {
628                let this = this.root();
629                let Some(callback) = &this.blob_callbacks.borrow_mut().remove(&callback_id) else {
630                    return error!("Expected blob callback, but found none!");
631                };
632
633                let Some(mut snapshot) = result else {
634                    let _ = callback.Call__(cx, None, ExceptionHandling::Report);
635                    return;
636                };
637
638                // Step 4.1: If result is non-null, then set result to a serialization of
639                // result as a file with type and quality if given.
640                // Step 4.2: Queue an element task on the canvas blob serialization task
641                // source given the canvas element to run these steps:
642                let mut encoded: Vec<u8> = vec![];
643                let blob_impl;
644                let blob;
645                let result = match snapshot.encode_for_mime_type(&image_type, quality, &mut encoded) {
646                   Ok(..) => {
647                       // Step 4.2.1: If result is non-null, then set result to a new Blob
648                       // object, created in the relevant realm of this canvas element,
649                       // representing result. [FILEAPI]
650                       blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type().to_owned());
651                       blob = Blob::new(cx, &this.global(), blob_impl);
652                       Some(&*blob)
653                   }
654                   Err(..) => None,
655                };
656
657                // Step 4.2.2: Invoke callback with « result » and "report".
658                let _ = callback.Call__(cx, result, ExceptionHandling::Report);
659            }));
660
661        Ok(())
662    }
663
664    /// <https://html.spec.whatwg.org/multipage/#dom-canvas-transfercontroltooffscreen>
665    fn TransferControlToOffscreen(
666        &self,
667        cx: &mut js::context::JSContext,
668    ) -> Fallible<DomRoot<OffscreenCanvas>> {
669        if self.context_mode.borrow().is_some() {
670            // Step 1.
671            // If this canvas element's context mode is not set to none, throw an "InvalidStateError" DOMException.
672            return Err(Error::InvalidState(Some("Canvas element's context mode must not be set when transferring control to an offscreen canvas".into())));
673        };
674
675        // Step 2.
676        // Let offscreenCanvas be a new OffscreenCanvas object with its width and height equal to the values of
677        // the width and height content attributes of this canvas element.
678        // Step 3.
679        // Set the placeholder canvas element of offscreenCanvas to a weak reference to this canvas element.
680        let offscreen_canvas = OffscreenCanvas::new(
681            cx,
682            &self.global(),
683            None,
684            self.Width().into(),
685            self.Height().into(),
686            Some(WeakRef::new(self)),
687        );
688
689        // Step 4. Set this canvas element's context mode to placeholder.
690        self.set_rendering_context(cx.no_gc(), || {
691            RenderingContext::Placeholder(offscreen_canvas.as_traced())
692        });
693
694        // Step 5. Return offscreenCanvas.
695        Ok(offscreen_canvas)
696    }
697
698    /// <https://w3c.github.io/mediacapture-fromelement/#dom-htmlcanvaselement-capturestream>
699    fn CaptureStream(
700        &self,
701        cx: &mut js::context::JSContext,
702        _frame_request_rate: Option<Finite<f64>>,
703    ) -> DomRoot<MediaStream> {
704        let global = self.global();
705        let stream = MediaStream::new(cx, &global);
706        let track =
707            MediaStreamTrack::new(cx, &global, MediaStreamId::new(), MediaStreamType::Video);
708        stream.AddTrack(&track);
709        stream
710    }
711}
712
713impl VirtualMethods for HTMLCanvasElement {
714    fn super_type(&self) -> Option<&dyn VirtualMethods> {
715        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
716    }
717
718    fn attribute_mutated(
719        &self,
720        cx: &mut js::context::JSContext,
721        attr: AttrRef<'_>,
722        mutation: AttributeMutation,
723    ) {
724        self.super_type()
725            .unwrap()
726            .attribute_mutated(cx, attr, mutation);
727        match attr.local_name() {
728            &local_name!("width") | &local_name!("height") => {
729                self.recreate_contexts_after_resize();
730                self.upcast::<Node>().dirty(cx.no_gc(), NodeDamage::Other);
731            },
732            _ => {},
733        };
734    }
735
736    fn attribute_affects_presentational_hints(&self, attr: AttrRef<'_>) -> bool {
737        match attr.local_name() {
738            &local_name!("width") | &local_name!("height") => true,
739            _ => self
740                .super_type()
741                .unwrap()
742                .attribute_affects_presentational_hints(attr),
743        }
744    }
745
746    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
747        match *name {
748            local_name!("width") => AttrValue::from_u32(value.into(), DEFAULT_WIDTH),
749            local_name!("height") => AttrValue::from_u32(value.into(), DEFAULT_HEIGHT),
750            _ => self
751                .super_type()
752                .unwrap()
753                .parse_plain_attribute(name, value),
754        }
755    }
756}
757
758#[cfg(feature = "webgl")]
759impl Convert<GLContextAttributes> for WebGLContextAttributes {
760    fn convert(self) -> GLContextAttributes {
761        GLContextAttributes {
762            alpha: self.alpha,
763            depth: self.depth,
764            stencil: self.stencil,
765            antialias: self.antialias,
766            premultiplied_alpha: self.premultipliedAlpha,
767            preserve_drawing_buffer: self.preserveDrawingBuffer,
768        }
769    }
770}