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