1use 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::rust::{HandleObject, HandleValue};
12use pixels::{EncodedImageType, Snapshot};
13use rustc_hash::FxHashMap;
14use script_bindings::inheritance::Castable;
15use script_bindings::weakref::WeakRef;
16use servo_base::id::{OffscreenCanvasId, OffscreenCanvasIndex};
17use servo_canvas_traits::webgl::{GLContextAttributes, WebGLVersion};
18use servo_constellation_traits::{BlobImpl, TransferableOffscreenCanvas};
19
20use crate::canvas_context::{CanvasContext, OffscreenRenderingContext};
21use crate::conversions::Convert;
22use crate::dom::bindings::cell::{DomRefCell, Ref};
23use crate::dom::bindings::codegen::Bindings::OffscreenCanvasBinding::{
24 ImageEncodeOptions, OffscreenCanvasMethods,
25 OffscreenRenderingContext as RootedOffscreenRenderingContext, OffscreenRenderingContextId,
26};
27use crate::dom::bindings::codegen::Bindings::WebGLRenderingContextBinding::WebGLContextAttributes;
28use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas as RootedHTMLCanvasElementOrOffscreenCanvas;
29use crate::dom::bindings::conversions::ConversionResult;
30use crate::dom::bindings::error::{Error, Fallible};
31use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
32use crate::dom::bindings::reflector::{DomGlobal, DomObject, reflect_dom_object_with_proto};
33use crate::dom::bindings::root::{Dom, DomRoot};
34use crate::dom::bindings::structuredclone::StructuredData;
35use crate::dom::bindings::transferable::Transferable;
36use crate::dom::blob::Blob;
37use crate::dom::eventtarget::EventTarget;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::html::htmlcanvaselement::HTMLCanvasElement;
40use crate::dom::imagebitmap::ImageBitmap;
41use crate::dom::imagebitmaprenderingcontext::ImageBitmapRenderingContext;
42use crate::dom::offscreencanvasrenderingcontext2d::OffscreenCanvasRenderingContext2D;
43use crate::dom::promise::Promise;
44use crate::dom::types::{WebGLRenderingContext, Window};
45use crate::dom::webgl::webgl2renderingcontext::WebGL2RenderingContext;
46use crate::realms::{AlreadyInRealm, InRealm};
47use crate::script_runtime::{CanGc, JSContext};
48
49#[dom_struct]
51pub(crate) struct OffscreenCanvas {
52 eventtarget: EventTarget,
53 width: Cell<u64>,
54 height: Cell<u64>,
55
56 context: DomRefCell<Option<OffscreenRenderingContext>>,
61
62 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 global: &GlobalScope,
83 proto: Option<HandleObject>,
84 width: u64,
85 height: u64,
86 placeholder: Option<WeakRef<HTMLCanvasElement>>,
87 can_gc: CanGc,
88 ) -> DomRoot<OffscreenCanvas> {
89 reflect_dom_object_with_proto(
90 Box::new(OffscreenCanvas::new_inherited(width, height, placeholder)),
91 global,
92 proto,
93 can_gc,
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: JSContext,
107 options: HandleValue,
108 can_gc: CanGc,
109 ) -> Option<GLContextAttributes> {
110 unsafe {
111 match WebGLContextAttributes::new(cx, options, can_gc) {
112 Ok(ConversionResult::Success(attrs)) => Some(attrs.convert()),
113 Ok(ConversionResult::Failure(error)) => {
114 throw_type_error(*cx, &error);
115 None
116 },
117 _ => {
118 debug!("Unexpected error on conversion of WebGLContextAttributes");
119 None
120 },
121 }
122 }
123 }
124
125 pub(crate) fn origin_is_clean(&self) -> bool {
126 match *self.context.borrow() {
127 Some(ref context) => context.origin_is_clean(),
128 _ => true,
129 }
130 }
131
132 pub(crate) fn context(&self) -> Option<Ref<'_, OffscreenRenderingContext>> {
133 Ref::filter_map(self.context.borrow(), |ctx| ctx.as_ref()).ok()
134 }
135
136 pub(crate) fn get_image_data(&self) -> Option<Snapshot> {
137 match self.context.borrow().as_ref() {
138 Some(context) => context.get_image_data(),
139 None => {
140 let size = self.get_size();
141 if size.is_empty() ||
142 pixels::compute_rgba8_byte_length_if_within_limit(
143 size.width as usize,
144 size.height as usize,
145 )
146 .is_none()
147 {
148 None
149 } else {
150 Some(Snapshot::cleared(size))
151 }
152 },
153 }
154 }
155
156 pub(crate) fn get_or_init_2d_context(
157 &self,
158 can_gc: CanGc,
159 ) -> Option<DomRoot<OffscreenCanvasRenderingContext2D>> {
160 if let Some(ctx) = self.context() {
161 return match *ctx {
162 OffscreenRenderingContext::Context2d(ref ctx) => Some(DomRoot::from_ref(ctx)),
163 _ => None,
164 };
165 }
166 let context =
167 OffscreenCanvasRenderingContext2D::new(&self.global(), self, self.get_size(), can_gc)?;
168 *self.context.borrow_mut() = Some(OffscreenRenderingContext::Context2d(Dom::from_ref(
169 &*context,
170 )));
171 Some(context)
172 }
173
174 pub(crate) fn get_or_init_bitmaprenderer_context(
176 &self,
177 can_gc: CanGc,
178 ) -> Option<DomRoot<ImageBitmapRenderingContext>> {
179 if let Some(ctx) = self.context() {
182 return match *ctx {
183 OffscreenRenderingContext::BitmapRenderer(ref ctx) => Some(DomRoot::from_ref(ctx)),
184 _ => None,
185 };
186 }
187
188 let canvas =
192 RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
193
194 let context = ImageBitmapRenderingContext::new(&self.global(), &canvas, can_gc);
195
196 *self.context.borrow_mut() = Some(OffscreenRenderingContext::BitmapRenderer(
198 Dom::from_ref(&*context),
199 ));
200
201 Some(context)
203 }
204
205 pub(crate) fn get_or_init_webgl_context(
207 &self,
208 cx: JSContext,
209 options: HandleValue,
210 can_gc: CanGc,
211 ) -> Option<DomRoot<WebGLRenderingContext>> {
212 if let Some(ctx) = self.context() {
213 return match *ctx {
214 OffscreenRenderingContext::WebGL(ref ctx) => Some(DomRoot::from_ref(ctx)),
215 _ => None,
216 };
217 }
218
219 let canvas =
222 RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
223 let size = self.get_size();
224 let attrs = Self::get_gl_attributes(cx, options, can_gc)?;
225 self.global()
226 .downcast::<Window>()
227 .and_then(|window| {
228 WebGLRenderingContext::new(
229 window,
230 &canvas,
231 WebGLVersion::WebGL1,
232 size,
233 attrs,
234 can_gc,
235 )
236 })
237 .map(|context| {
238 *self.context.borrow_mut() =
241 Some(OffscreenRenderingContext::WebGL(Dom::from_ref(&*context)));
242
243 context
245 })
246 }
247
248 fn get_or_init_webgl2_context(
250 &self,
251 cx: JSContext,
252 options: HandleValue,
253 can_gc: CanGc,
254 ) -> Option<DomRoot<WebGL2RenderingContext>> {
255 if !WebGL2RenderingContext::is_webgl2_enabled(cx, self.global().reflector().get_jsobject())
256 {
257 return None;
258 }
259 if let Some(ctx) = self.context() {
260 return match *ctx {
261 OffscreenRenderingContext::WebGL2(ref ctx) => Some(DomRoot::from_ref(ctx)),
262 _ => None,
263 };
264 }
265
266 let canvas =
269 RootedHTMLCanvasElementOrOffscreenCanvas::OffscreenCanvas(DomRoot::from_ref(self));
270 let size = self.get_size();
271 let attrs = Self::get_gl_attributes(cx, options, can_gc)?;
272 self.global()
273 .downcast::<Window>()
274 .and_then(|window| WebGL2RenderingContext::new(window, &canvas, size, attrs, can_gc))
275 .map(|context| {
276 *self.context.borrow_mut() =
279 Some(OffscreenRenderingContext::WebGL2(Dom::from_ref(&*context)));
280
281 context
283 })
284 }
285
286 pub(crate) fn placeholder(&self) -> Option<DomRoot<HTMLCanvasElement>> {
287 self.placeholder
288 .as_ref()
289 .and_then(|placeholder| placeholder.root())
290 }
291}
292
293impl Transferable for OffscreenCanvas {
294 type Index = OffscreenCanvasIndex;
295 type Data = TransferableOffscreenCanvas;
296
297 fn transfer(
299 &self,
300 _cx: &mut js::context::JSContext,
301 ) -> Fallible<(OffscreenCanvasId, TransferableOffscreenCanvas)> {
302 if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
307 return Err(Error::DataClone(None));
308 }
309
310 if !self.context.borrow().is_none() {
313 return Err(Error::InvalidState(None));
314 }
315
316 if self.placeholder.is_some() {
318 return Err(Error::InvalidState(None));
319 }
320
321 *self.context.borrow_mut() = Some(OffscreenRenderingContext::Detached);
323
324 let width = self.width.replace(0);
327 let height = self.height.replace(0);
328
329 let transferred = TransferableOffscreenCanvas { width, height };
342
343 Ok((OffscreenCanvasId::new(), transferred))
344 }
345
346 fn transfer_receive(
348 cx: &mut js::context::JSContext,
349 owner: &GlobalScope,
350 _: OffscreenCanvasId,
351 transferred: TransferableOffscreenCanvas,
352 ) -> Result<DomRoot<Self>, ()> {
353 Ok(OffscreenCanvas::new(
366 owner,
367 None,
368 transferred.width,
369 transferred.height,
370 None,
371 CanGc::from_cx(cx),
372 ))
373 }
374
375 fn serialized_storage<'a>(
376 data: StructuredData<'a, '_>,
377 ) -> &'a mut Option<FxHashMap<OffscreenCanvasId, Self::Data>> {
378 match data {
379 StructuredData::Reader(r) => &mut r.offscreen_canvases,
380 StructuredData::Writer(w) => &mut w.offscreen_canvases,
381 }
382 }
383}
384
385impl OffscreenCanvasMethods<crate::DomTypeHolder> for OffscreenCanvas {
386 fn Constructor(
388 global: &GlobalScope,
389 proto: Option<HandleObject>,
390 can_gc: CanGc,
391 width: u64,
392 height: u64,
393 ) -> Fallible<DomRoot<OffscreenCanvas>> {
394 Ok(OffscreenCanvas::new(
395 global, proto, width, height, None, can_gc,
396 ))
397 }
398
399 fn GetContext(
401 &self,
402 cx: JSContext,
403 id: OffscreenRenderingContextId,
404 options: HandleValue,
405 can_gc: CanGc,
406 ) -> Fallible<Option<RootedOffscreenRenderingContext>> {
407 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(can_gc)
416 .map(RootedOffscreenRenderingContext::OffscreenCanvasRenderingContext2D)),
417 OffscreenRenderingContextId::Bitmaprenderer => Ok(self
418 .get_or_init_bitmaprenderer_context(can_gc)
419 .map(RootedOffscreenRenderingContext::ImageBitmapRenderingContext)),
420 OffscreenRenderingContextId::Webgl => Ok(self
421 .get_or_init_webgl_context(cx, options, can_gc)
422 .map(RootedOffscreenRenderingContext::WebGLRenderingContext)),
423 OffscreenRenderingContextId::Experimental_webgl => Ok(self
424 .get_or_init_webgl_context(cx, options, can_gc)
425 .map(RootedOffscreenRenderingContext::WebGLRenderingContext)),
426 OffscreenRenderingContextId::Webgl2 => Ok(self
427 .get_or_init_webgl2_context(cx, options, can_gc)
428 .map(RootedOffscreenRenderingContext::WebGL2RenderingContext)),
429 OffscreenRenderingContextId::Experimental_webgl2 => Ok(self
430 .get_or_init_webgl2_context(cx, options, can_gc)
431 .map(RootedOffscreenRenderingContext::WebGL2RenderingContext)),
432 }
433 }
434
435 fn Width(&self) -> u64 {
437 self.width.get()
438 }
439
440 fn SetWidth(&self, value: u64, can_gc: CanGc) {
442 self.width.set(value);
443
444 if let Some(canvas_context) = self.context() {
445 canvas_context.resize();
446 }
447
448 if let Some(canvas) = self.placeholder() {
449 canvas.set_natural_width(value as _, can_gc)
450 }
451 }
452
453 fn Height(&self) -> u64 {
455 self.height.get()
456 }
457
458 fn SetHeight(&self, value: u64, can_gc: CanGc) {
460 self.height.set(value);
461
462 if let Some(canvas_context) = self.context() {
463 canvas_context.resize();
464 }
465
466 if let Some(canvas) = self.placeholder() {
467 canvas.set_natural_height(value as _, can_gc)
468 }
469 }
470
471 fn TransferToImageBitmap(&self, can_gc: CanGc) -> Fallible<DomRoot<ImageBitmap>> {
473 if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
477 return Err(Error::InvalidState(None));
478 }
479
480 if self.context.borrow().is_none() {
483 return Err(Error::InvalidState(None));
484 }
485
486 let Some(snapshot) = self.get_image_data() else {
490 return Err(Error::InvalidState(None));
491 };
492
493 let image_bitmap = ImageBitmap::new(&self.global(), snapshot, can_gc);
494 image_bitmap.set_origin_clean(self.origin_is_clean());
495
496 if let Some(canvas_context) = self.context() {
501 canvas_context.reset_bitmap();
502 }
503
504 Ok(image_bitmap)
506 }
507
508 fn ConvertToBlob(&self, options: &ImageEncodeOptions, can_gc: CanGc) -> Rc<Promise> {
510 let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
512 let promise = Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), can_gc);
513
514 if let Some(OffscreenRenderingContext::Detached) = *self.context.borrow() {
518 promise.reject_error(Error::InvalidState(None), can_gc);
519 return promise;
520 }
521
522 if !self.origin_is_clean() {
526 promise.reject_error(Error::Security(None), can_gc);
527 return promise;
528 }
529
530 if self.Width() == 0 || self.Height() == 0 {
534 promise.reject_error(Error::IndexSize(None), can_gc);
535 return promise;
536 }
537
538 let Some(mut snapshot) = self.get_image_data() else {
540 promise.reject_error(Error::InvalidState(None), can_gc);
541 return promise;
542 };
543
544 let trusted_this = Trusted::new(self);
550 let trusted_promise = TrustedPromise::new(promise.clone());
551
552 let image_type = EncodedImageType::from(options.type_.to_string());
553 let quality = options.quality;
554
555 self.global()
556 .task_manager()
557 .canvas_blob_task_source()
558 .queue(task!(convert_to_blob: move |cx| {
559 let this = trusted_this.root();
560 let promise = trusted_promise.root();
561
562 let mut encoded: Vec<u8> = vec![];
563
564 if snapshot.encode_for_mime_type(&image_type, quality, &mut encoded).is_err() {
565 promise.reject_error(Error::Encoding(None), CanGc::from_cx(cx));
568 return;
569 };
570
571 let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type());
574 let blob = Blob::new(&this.global(), blob_impl, CanGc::from_cx(cx));
575
576 promise.resolve_native(&blob, CanGc::from_cx(cx));
577 }));
578
579 promise
581 }
582}