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