Skip to main content

script/dom/canvas/
offscreencanvas.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::rc::Rc;
7
8use dom_struct::dom_struct;
9use euclid::default::Size2D;
10#[cfg(feature = "webgl")]
11use js::error::throw_type_error;
12use js::realm::CurrentRealm;
13use js::rust::{HandleObject, HandleValue};
14use pixels::{EncodedImageType, Snapshot};
15use rustc_hash::FxHashMap;
16use script_bindings::cell::{DomRefCell, Ref};
17#[cfg(feature = "webgl")]
18use script_bindings::inheritance::Castable;
19#[cfg(feature = "webgl")]
20use script_bindings::reflector::DomObject;
21use script_bindings::reflector::reflect_dom_object_with_proto;
22use script_bindings::weakref::WeakRef;
23use servo_base::id::{OffscreenCanvasId, OffscreenCanvasIndex};
24#[cfg(feature = "webgl")]
25use servo_canvas_traits::webgl::{GLContextAttributes, WebGLVersion};
26use servo_constellation_traits::{BlobImpl, TransferableOffscreenCanvas};
27
28use crate::canvas_context::{CanvasContext, OffscreenRenderingContext};
29#[cfg(feature = "webgl")]
30use crate::conversions::Convert;
31use crate::dom::bindings::codegen::Bindings::OffscreenCanvasBinding::{
32    ImageEncodeOptions, OffscreenCanvasMethods,
33    OffscreenRenderingContext as RootedOffscreenRenderingContext, OffscreenRenderingContextId,
34};
35#[cfg(feature = "webgl")]
36use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLContextAttributes;
37use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas as RootedHTMLCanvasElementOrOffscreenCanvas;
38#[cfg(feature = "webgl")]
39use crate::dom::bindings::conversions::ConversionResult;
40use crate::dom::bindings::error::{Error, Fallible};
41use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
42use crate::dom::bindings::reflector::DomGlobal;
43use crate::dom::bindings::root::{Dom, DomRoot};
44use crate::dom::bindings::structuredclone::StructuredData;
45use crate::dom::bindings::transferable::Transferable;
46use crate::dom::blob::Blob;
47use crate::dom::eventtarget::EventTarget;
48use crate::dom::globalscope::GlobalScope;
49use crate::dom::html::htmlcanvaselement::HTMLCanvasElement;
50use crate::dom::imagebitmap::ImageBitmap;
51use crate::dom::imagebitmaprenderingcontext::ImageBitmapRenderingContext;
52use crate::dom::offscreencanvasrenderingcontext2d::OffscreenCanvasRenderingContext2D;
53use crate::dom::promise::Promise;
54#[cfg(feature = "webgl")]
55use crate::dom::types::{WebGLRenderingContext, Window};
56#[cfg(feature = "webgl")]
57use crate::dom::webgl::webgl2renderingcontext::WebGL2RenderingContext;
58
59/// <https://html.spec.whatwg.org/multipage/#offscreencanvas>
60#[dom_struct]
61pub(crate) struct OffscreenCanvas {
62    eventtarget: EventTarget,
63    width: Cell<u64>,
64    height: Cell<u64>,
65
66    /// Represents both the [bitmap] and the [context mode] of the canvas.
67    ///
68    /// [bitmap]: https://html.spec.whatwg.org/multipage/#offscreencanvas-bitmap
69    /// [context mode]: https://html.spec.whatwg.org/multipage/#offscreencanvas-context-mode
70    context: DomRefCell<Option<OffscreenRenderingContext>>,
71
72    /// <https://html.spec.whatwg.org/multipage/#offscreencanvas-placeholder>
73    placeholder: Option<WeakRef<HTMLCanvasElement>>,
74}
75
76impl OffscreenCanvas {
77    pub(crate) fn new_inherited(
78        width: u64,
79        height: u64,
80        placeholder: Option<WeakRef<HTMLCanvasElement>>,
81    ) -> OffscreenCanvas {
82        OffscreenCanvas {
83            eventtarget: EventTarget::new_inherited(),
84            width: Cell::new(width),
85            height: Cell::new(height),
86            context: DomRefCell::new(None),
87            placeholder,
88        }
89    }
90
91    pub(crate) fn new(
92        cx: &mut js::context::JSContext,
93        global: &GlobalScope,
94        proto: Option<HandleObject>,
95        width: u64,
96        height: u64,
97        placeholder: Option<WeakRef<HTMLCanvasElement>>,
98    ) -> DomRoot<OffscreenCanvas> {
99        reflect_dom_object_with_proto(
100            cx,
101            Box::new(OffscreenCanvas::new_inherited(width, height, placeholder)),
102            global,
103            proto,
104        )
105    }
106
107    pub(crate) fn get_size(&self) -> Size2D<u32> {
108        Size2D::new(
109            self.Width().try_into().unwrap_or(u32::MAX),
110            self.Height().try_into().unwrap_or(u32::MAX),
111        )
112    }
113
114    #[cfg(feature = "webgl")]
115    #[expect(unsafe_code)]
116    fn get_gl_attributes(
117        cx: &mut js::context::JSContext,
118        options: HandleValue,
119    ) -> Option<GLContextAttributes> {
120        unsafe {
121            match WebGLContextAttributes::new(cx, options) {
122                Ok(ConversionResult::Success(attrs)) => Some(attrs.convert()),
123                Ok(ConversionResult::Failure(error)) => {
124                    throw_type_error(cx.raw_cx(), &error);
125                    None
126                },
127                _ => {
128                    debug!("Unexpected error on conversion of WebGLContextAttributes");
129                    None
130                },
131            }
132        }
133    }
134
135    pub(crate) fn origin_is_clean(&self) -> bool {
136        match *self.context.borrow() {
137            Some(ref context) => context.origin_is_clean(),
138            _ => true,
139        }
140    }
141
142    pub(crate) fn context(&self) -> Option<Ref<'_, OffscreenRenderingContext>> {
143        Ref::filter_map(self.context.borrow(), |ctx| ctx.as_ref()).ok()
144    }
145
146    pub(crate) fn get_image_data(&self) -> Option<Snapshot> {
147        match self.context.borrow().as_ref() {
148            Some(context) => context.get_image_data(),
149            None => {
150                let size = self.get_size();
151                if size.is_empty() ||
152                    pixels::compute_rgba8_byte_length_if_within_limit(
153                        size.width as usize,
154                        size.height as usize,
155                    )
156                    .is_none()
157                {
158                    None
159                } else {
160                    Some(Snapshot::cleared(size))
161                }
162            },
163        }
164    }
165
166    pub(crate) fn get_or_init_2d_context(
167        &self,
168        cx: &mut js::context::JSContext,
169    ) -> Option<DomRoot<OffscreenCanvasRenderingContext2D>> {
170        if let Some(ctx) = self.context() {
171            return match *ctx {
172                OffscreenRenderingContext::Context2d(ref ctx) => Some(DomRoot::from_ref(ctx)),
173                _ => None,
174            };
175        }
176        let context =
177            OffscreenCanvasRenderingContext2D::new(cx, &self.global(), self, self.get_size())?;
178        *self.context.safe_borrow_mut(cx.no_gc()) = Some(OffscreenRenderingContext::Context2d(
179            Dom::from_ref(&*context),
180        ));
181        Some(context)
182    }
183
184    /// <https://html.spec.whatwg.org/multipage/#offscreen-context-type-bitmaprenderer>
185    pub(crate) fn get_or_init_bitmaprenderer_context(
186        &self,
187        cx: &mut js::context::JSContext,
188    ) -> Option<DomRoot<ImageBitmapRenderingContext>> {
189        // Return the same object as was returned the last time the method was
190        // invoked with this same first argument.
191        if let Some(ctx) = self.context() {
192            return match *ctx {
193                OffscreenRenderingContext::BitmapRenderer(ref ctx) => Some(DomRoot::from_ref(ctx)),
194                _ => None,
195            };
196        }
197
198        // Step 1. Let context be the result of running the
199        // ImageBitmapRenderingContext creation algorithm given this and
200        // options.
201        let canvas =
202            RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
203
204        let context = ImageBitmapRenderingContext::new(cx, &self.global(), &canvas);
205
206        // Step 2. Set this's context mode to bitmaprenderer.
207        *self.context.safe_borrow_mut(cx.no_gc()) = Some(
208            OffscreenRenderingContext::BitmapRenderer(Dom::from_ref(&*context)),
209        );
210
211        // Step 3. Return context.
212        Some(context)
213    }
214
215    #[cfg(feature = "webgl")]
216    // <https://html.spec.whatwg.org/multipage/#offscreen-context-type-webgl>
217    pub(crate) fn get_or_init_webgl_context(
218        &self,
219        cx: &mut js::context::JSContext,
220        options: HandleValue,
221    ) -> Option<DomRoot<WebGLRenderingContext>> {
222        if let Some(ctx) = self.context() {
223            return match *ctx {
224                OffscreenRenderingContext::WebGL(ref ctx) => Some(DomRoot::from_ref(ctx)),
225                _ => None,
226            };
227        }
228
229        // 1. Let context be the result of following the instructions given in the
230        // WebGL specifications' Context Creation sections.
231        let canvas =
232            RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
233        let size = self.get_size();
234        let attrs = Self::get_gl_attributes(cx, options)?;
235        self.global()
236            .downcast::<Window>()
237            .and_then(|window| {
238                WebGLRenderingContext::new(cx, window, &canvas, WebGLVersion::WebGL1, size, attrs)
239            })
240            .map(|context| {
241                // Step 2. If context is null, then return null;
242                // otherwise set this's context mode to webgl or webgl2.
243                *self.context.safe_borrow_mut(cx.no_gc()) =
244                    Some(OffscreenRenderingContext::WebGL(Dom::from_ref(&*context)));
245
246                // Step 3. Return context.
247                context
248            })
249    }
250
251    #[cfg(feature = "webgl")]
252    // <https://html.spec.whatwg.org/multipage/#offscreen-context-type-webgl>
253    fn get_or_init_webgl2_context(
254        &self,
255        cx: &mut js::context::JSContext,
256        options: HandleValue,
257    ) -> Option<DomRoot<WebGL2RenderingContext>> {
258        if !WebGL2RenderingContext::is_webgl2_enabled(cx, self.global().reflector().get_jsobject())
259        {
260            return None;
261        }
262        if let Some(ctx) = self.context() {
263            return match *ctx {
264                OffscreenRenderingContext::WebGL2(ref ctx) => Some(DomRoot::from_ref(ctx)),
265                _ => None,
266            };
267        }
268
269        // 1. Let context be the result of following the instructions given in the
270        // WebGL specifications' Context Creation sections.
271        let canvas =
272            RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
273        let size = self.get_size();
274        let attrs = Self::get_gl_attributes(cx, options)?;
275        self.global()
276            .downcast::<Window>()
277            .and_then(|window| WebGL2RenderingContext::new(cx, window, &canvas, size, attrs))
278            .map(|context| {
279                // Step 2. If context is null, then return null;
280                // otherwise set this's context mode to webgl or webgl2.
281                *self.context.safe_borrow_mut(cx.no_gc()) =
282                    Some(OffscreenRenderingContext::WebGL2(Dom::from_ref(&*context)));
283
284                // Step 3. Return context.
285                context
286            })
287    }
288
289    pub(crate) fn placeholder(&self) -> Option<DomRoot<HTMLCanvasElement>> {
290        self.placeholder
291            .as_ref()
292            .and_then(|placeholder| placeholder.root())
293    }
294}
295
296impl Transferable for OffscreenCanvas {
297    type Index = OffscreenCanvasIndex;
298    type Data = TransferableOffscreenCanvas;
299
300    /// <https://html.spec.whatwg.org/multipage/#the-offscreencanvas-interface:transfer-steps>
301    fn transfer(
302        &self,
303        cx: &mut js::context::JSContext,
304    ) -> Fallible<(OffscreenCanvasId, TransferableOffscreenCanvas)> {
305        // <https://html.spec.whatwg.org/multipage/#structuredserializewithtransfer>
306        // Step 5.2. If transferable has a [[Detached]] internal slot and
307        // transferable.[[Detached]] is true, then throw a "DataCloneError"
308        // DOMException.
309        if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
310            return Err(Error::DataClone(None));
311        }
312
313        // Step 1. If value's context mode is not equal to none, then throw an
314        // "InvalidStateError" DOMException.
315        if !self.context.borrow().is_none() {
316            return Err(Error::InvalidState(None));
317        }
318
319        // TODO(#37882): Allow to transfer with a placeholder canvas element.
320        if self.placeholder.is_some() {
321            return Err(Error::InvalidState(None));
322        }
323
324        // Step 2. Set value's context mode to detached.
325        *self.context.safe_borrow_mut(cx.no_gc()) = Some(OffscreenRenderingContext::Detached);
326
327        // Step 3. Let width and height be the dimensions of value's bitmap.
328        // Step 5. Unset value's bitmap.
329        let width = self.width.replace(0);
330        let height = self.height.replace(0);
331
332        // TODO(#37918) Step 4. Let language and direction be the values of
333        // value's inherited language and inherited direction.
334
335        // Step 6. Set dataHolder.[[Width]] to width and dataHolder.[[Height]]
336        // to height.
337
338        // TODO(#37918) Step 7. Set dataHolder.[[Language]] to language and
339        // dataHolder.[[Direction]] to direction.
340
341        // TODO(#37882) Step 8. Set dataHolder.[[PlaceholderCanvas]] to be a
342        // weak reference to value's placeholder canvas element, if value has
343        // one, or null if it does not.
344        let transferred = TransferableOffscreenCanvas { width, height };
345
346        Ok((OffscreenCanvasId::new(), transferred))
347    }
348
349    /// <https://html.spec.whatwg.org/multipage/#the-offscreencanvas-interface:transfer-receiving-steps>
350    fn transfer_receive(
351        cx: &mut js::context::JSContext,
352        owner: &GlobalScope,
353        _: OffscreenCanvasId,
354        transferred: TransferableOffscreenCanvas,
355    ) -> Result<DomRoot<Self>, ()> {
356        // Step 1. Initialize value's bitmap to a rectangular array of
357        // transparent black pixels with width given by dataHolder.[[Width]] and
358        // height given by dataHolder.[[Height]].
359
360        // TODO(#37918) Step 2. Set value's inherited language to
361        // dataHolder.[[Language]] and its inherited direction to
362        // dataHolder.[[Direction]].
363
364        // TODO(#37882) Step 3. If dataHolder.[[PlaceholderCanvas]] is not null,
365        // set value's placeholder canvas element to
366        // dataHolder.[[PlaceholderCanvas]] (while maintaining the weak
367        // reference semantics).
368        Ok(OffscreenCanvas::new(
369            cx,
370            owner,
371            None,
372            transferred.width,
373            transferred.height,
374            None,
375        ))
376    }
377
378    fn serialized_storage<'a>(
379        data: StructuredData<'a, '_>,
380    ) -> &'a mut Option<FxHashMap<OffscreenCanvasId, Self::Data>> {
381        match data {
382            StructuredData::Reader(r) => &mut r.offscreen_canvases,
383            StructuredData::Writer(w) => &mut w.offscreen_canvases,
384        }
385    }
386}
387
388impl OffscreenCanvasMethods<crate::DomTypeHolder> for OffscreenCanvas {
389    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas>
390    fn Constructor(
391        cx: &mut js::context::JSContext,
392        global: &GlobalScope,
393        proto: Option<HandleObject>,
394        width: u64,
395        height: u64,
396    ) -> Fallible<DomRoot<OffscreenCanvas>> {
397        Ok(OffscreenCanvas::new(cx, global, proto, width, height, None))
398    }
399
400    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-getcontext>
401    fn GetContext(
402        &self,
403        cx: &mut js::context::JSContext,
404        id: OffscreenRenderingContextId,
405        options: HandleValue,
406    ) -> Fallible<Option<RootedOffscreenRenderingContext>> {
407        // Step 3. Throw an "InvalidStateError" DOMException if the
408        // OffscreenCanvas object's context mode is detached.
409        if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
410            return Err(Error::InvalidState(None));
411        }
412
413        match id {
414            OffscreenRenderingContextId::_2d => Ok(self
415                .get_or_init_2d_context(cx)
416                .map(RootedOffscreenRenderingContext::OffscreenCanvasRenderingContext2D)),
417            OffscreenRenderingContextId::Bitmaprenderer => Ok(self
418                .get_or_init_bitmaprenderer_context(cx)
419                .map(RootedOffscreenRenderingContext::ImageBitmapRenderingContext)),
420            #[cfg(feature = "webgl")]
421            OffscreenRenderingContextId::Webgl => Ok(self
422                .get_or_init_webgl_context(cx, options)
423                .map(RootedOffscreenRenderingContext::WebGLRenderingContext)),
424            #[cfg(feature = "webgl")]
425            OffscreenRenderingContextId::Experimental_webgl => Ok(self
426                .get_or_init_webgl_context(cx, options)
427                .map(RootedOffscreenRenderingContext::WebGLRenderingContext)),
428            #[cfg(feature = "webgl")]
429            OffscreenRenderingContextId::Webgl2 => Ok(self
430                .get_or_init_webgl2_context(cx, options)
431                .map(RootedOffscreenRenderingContext::WebGL2RenderingContext)),
432            #[cfg(feature = "webgl")]
433            OffscreenRenderingContextId::Experimental_webgl2 => Ok(self
434                .get_or_init_webgl2_context(cx, options)
435                .map(RootedOffscreenRenderingContext::WebGL2RenderingContext)),
436        }
437    }
438
439    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-width>
440    fn Width(&self) -> u64 {
441        self.width.get()
442    }
443
444    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-width>
445    fn SetWidth(&self, cx: &mut js::context::JSContext, value: u64) {
446        self.width.set(value);
447
448        if let Some(canvas_context) = self.context() {
449            canvas_context.resize();
450        }
451
452        if let Some(canvas) = self.placeholder() {
453            canvas.set_natural_width(cx, value as _)
454        }
455    }
456
457    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-height>
458    fn Height(&self) -> u64 {
459        self.height.get()
460    }
461
462    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-height>
463    fn SetHeight(&self, cx: &mut js::context::JSContext, value: u64) {
464        self.height.set(value);
465
466        if let Some(canvas_context) = self.context() {
467            canvas_context.resize();
468        }
469
470        if let Some(canvas) = self.placeholder() {
471            canvas.set_natural_height(cx, value as _)
472        }
473    }
474
475    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-transfertoimagebitmap>
476    fn TransferToImageBitmap(
477        &self,
478        cx: &mut js::context::JSContext,
479    ) -> Fallible<DomRoot<ImageBitmap>> {
480        // Step 1. If the value of this OffscreenCanvas object's [[Detached]]
481        // internal slot is set to true, then throw an "InvalidStateError"
482        // DOMException.
483        if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
484            return Err(Error::InvalidState(None));
485        }
486
487        // Step 2. If this OffscreenCanvas object's context mode is set to none,
488        // then throw an "InvalidStateError" DOMException.
489        if self.context.borrow().is_none() {
490            return Err(Error::InvalidState(None));
491        }
492
493        // Step 3. Let image be a newly created ImageBitmap object that
494        // references the same underlying bitmap data as this OffscreenCanvas
495        // object's bitmap.
496        let Some(snapshot) = self.get_image_data() else {
497            return Err(Error::InvalidState(None));
498        };
499
500        let image_bitmap = ImageBitmap::new(cx, &self.global(), snapshot);
501        image_bitmap.set_origin_clean(self.origin_is_clean());
502
503        // Step 4. Set this OffscreenCanvas object's bitmap to reference a newly
504        // created bitmap of the same dimensions and color space as the previous
505        // bitmap, and with its pixels initialized to transparent black, or
506        // opaque black if the rendering context's alpha is false.
507        if let Some(canvas_context) = self.context() {
508            canvas_context.reset_bitmap();
509        }
510
511        // Step 5. Return image.
512        Ok(image_bitmap)
513    }
514
515    /// <https://html.spec.whatwg.org/multipage/#dom-offscreencanvas-converttoblob>
516    fn ConvertToBlob(
517        &self,
518        cx: &mut js::context::JSContext,
519        options: &ImageEncodeOptions,
520    ) -> Rc<Promise> {
521        // Step 5. Let result be a new promise object.
522        let mut realm = CurrentRealm::assert(cx);
523        let promise = Promise::new_in_realm(&mut realm);
524
525        // Step 1. If the value of this's [[Detached]] internal slot is true,
526        // then return a promise rejected with an "InvalidStateError"
527        // DOMException.
528        if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
529            promise.reject_error(cx, Error::InvalidState(None));
530            return promise;
531        }
532
533        // Step 2. If this's context mode is 2d and the rendering context's
534        // output bitmap's origin-clean flag is set to false, then return a
535        // promise rejected with a "SecurityError" DOMException.
536        if !self.origin_is_clean() {
537            promise.reject_error(cx, Error::Security(None));
538            return promise;
539        }
540
541        // Step 3. If this's bitmap has no pixels (i.e., either its horizontal
542        // dimension or its vertical dimension is zero), then return a promise
543        // rejected with an "IndexSizeError" DOMException.
544        if self.Width() == 0 || self.Height() == 0 {
545            promise.reject_error(cx, Error::IndexSize(None));
546            return promise;
547        }
548
549        // Step 4. Let bitmap be a copy of this's bitmap.
550        let Some(mut snapshot) = self.get_image_data() else {
551            promise.reject_error(cx, Error::InvalidState(None));
552            return promise;
553        };
554
555        // Step 7. Run these steps in parallel:
556        // Step 7.1. Let file be a serialization of bitmap as a file, with
557        // options's type and quality if present.
558        // Step 7.2. Queue a global task on the canvas blob serialization task
559        // source given global to run these steps:
560        let trusted_this = Trusted::new(self);
561        let trusted_promise = TrustedPromise::new(promise.clone());
562
563        let image_type = EncodedImageType::from(&options.type_.str() as &str);
564        let quality = options.quality;
565
566        self.global()
567            .task_manager()
568            .canvas_blob_task_source()
569            .queue(task!(convert_to_blob: move |cx| {
570                let this = trusted_this.root();
571                let promise = trusted_promise.root();
572
573                let mut encoded: Vec<u8> = vec![];
574
575                if snapshot.encode_for_mime_type(&image_type, quality, &mut encoded).is_err() {
576                    // Step 7.2.1. If file is null, then reject result with an
577                    // "EncodingError" DOMException.
578                    promise.reject_error(cx, Error::Encoding(None));
579                    return;
580                };
581
582                // Step 7.2.2. Otherwise, resolve result with a new Blob object,
583                // created in global's relevant realm, representing file.
584                let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
585                let blob = Blob::new(cx, &this.global(), blob_impl);
586
587                promise.resolve_native(cx, &blob);
588            }));
589
590        // Step 8. Return result.
591        promise
592    }
593}