script/dom/canvas/imagedata.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::borrow::Cow;
6use std::vec::Vec;
7
8use base::generic_channel::GenericSharedMemory;
9use base::id::{ImageDataId, ImageDataIndex};
10use constellation_traits::SerializableImageData;
11use dom_struct::dom_struct;
12use euclid::default::{Rect, Size2D};
13use js::gc::CustomAutoRooterGuard;
14use js::jsapi::JSObject;
15use js::rust::HandleObject;
16use js::typedarray::{ClampedU8, HeapUint8ClampedArray, TypedArray, Uint8ClampedArray};
17use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
18use rustc_hash::FxHashMap;
19use script_bindings::trace::RootedTraceableBox;
20
21use crate::dom::bindings::buffer_source::{
22 HeapBufferSource, create_buffer_source, create_heap_buffer_source_with_length,
23};
24use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{
25 ImageDataMethods, ImageDataPixelFormat, ImageDataSettings, PredefinedColorSpace,
26};
27use crate::dom::bindings::error::{Error, Fallible};
28use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto};
29use crate::dom::bindings::root::DomRoot;
30use crate::dom::bindings::serializable::Serializable;
31use crate::dom::bindings::structuredclone::StructuredData;
32use crate::dom::globalscope::GlobalScope;
33use crate::script_runtime::{CanGc, JSContext};
34
35#[dom_struct]
36pub(crate) struct ImageData {
37 reflector_: Reflector,
38 width: u32,
39 height: u32,
40 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-data>
41 #[ignore_malloc_size_of = "mozjs"]
42 data: HeapBufferSource<ClampedU8>,
43 pixel_format: ImageDataPixelFormat,
44 color_space: PredefinedColorSpace,
45}
46
47impl ImageData {
48 pub(crate) fn new(
49 global: &GlobalScope,
50 width: u32,
51 height: u32,
52 mut data: Option<Vec<u8>>,
53 can_gc: CanGc,
54 ) -> Fallible<DomRoot<ImageData>> {
55 let len =
56 pixels::compute_rgba8_byte_length_if_within_limit(width as usize, height as usize)
57 .ok_or(Error::Range(
58 "The requested image size exceeds the supported range".to_owned(),
59 ))?;
60
61 let settings = ImageDataSettings {
62 colorSpace: Some(PredefinedColorSpace::Srgb),
63 pixelFormat: ImageDataPixelFormat::Rgba_unorm8,
64 };
65
66 if let Some(ref mut d) = data {
67 d.resize(len as usize, 0);
68
69 let cx = GlobalScope::get_cx();
70 rooted!(in (*cx) let mut js_object = std::ptr::null_mut::<JSObject>());
71 let _buffer_source =
72 create_buffer_source::<ClampedU8>(cx, &d[..], js_object.handle_mut(), can_gc)
73 .map_err(|_| Error::JSFailed)?;
74 auto_root!(in(*cx) let data = TypedArray::<ClampedU8, *mut JSObject>::from(js_object.get()).map_err(|_| Error::JSFailed)?);
75
76 Self::Constructor_(global, None, can_gc, data, width, Some(height), &settings)
77 } else {
78 Self::Constructor(global, None, can_gc, width, height, &settings)
79 }
80 }
81
82 #[allow(clippy::too_many_arguments)]
83 /// <https://html.spec.whatwg.org/multipage/#initialize-an-imagedata-object>
84 fn initialize(
85 pixels_per_row: u32,
86 rows: u32,
87 settings: &ImageDataSettings,
88 source: Option<CustomAutoRooterGuard<Uint8ClampedArray>>,
89 default_color_space: Option<PredefinedColorSpace>,
90 global: &GlobalScope,
91 proto: Option<HandleObject>,
92 can_gc: CanGc,
93 ) -> Fallible<DomRoot<ImageData>> {
94 // 1. If source was given:
95 let data = if let Some(source) = source {
96 // 1. If settings["pixelFormat"] equals "rgba-unorm8" and source is not a Uint8ClampedArray,
97 // then throw an "InvalidStateError" DOMException.
98 // 2. If settings["pixelFormat"] is "rgba-float16" and source is not a Float16Array,
99 // then throw an "InvalidStateError" DOMException.
100 if !matches!(settings.pixelFormat, ImageDataPixelFormat::Rgba_unorm8) {
101 // we currently support only rgba-unorm8
102 return Err(Error::InvalidState(None));
103 }
104 // 3. Initialize the data attribute of imageData to source.
105 HeapBufferSource::<ClampedU8>::from_view(source)
106 } else {
107 // 2. Otherwise (source was not given):
108 match settings.pixelFormat {
109 ImageDataPixelFormat::Rgba_unorm8 => {
110 // 1. If settings["pixelFormat"] is "rgba-unorm8",
111 // then initialize the data attribute of imageData to a new Uint8ClampedArray object.
112 // The Uint8ClampedArray object must use a new ArrayBuffer for its storage,
113 // and must have a zero byte offset and byte length equal to the length of its storage, in bytes.
114 // The storage ArrayBuffer must have a length of 4 × rows × pixelsPerRow bytes.
115 // 3. If the storage ArrayBuffer could not be allocated,
116 // then rethrow the RangeError thrown by JavaScript, and return.
117 create_heap_buffer_source_with_length(
118 GlobalScope::get_cx(),
119 4 * rows * pixels_per_row,
120 can_gc,
121 )?
122 },
123 // 3. Otherwise, if settings["pixelFormat"] is "rgba-float16",
124 // then initialize the data attribute of imageData to a new Float16Array object.
125 // The Float16Array object must use a new ArrayBuffer for its storage,
126 // and must have a zero byte offset and byte length equal to the length of its storage, in bytes.
127 // The storage ArrayBuffer must have a length of 8 × rows × pixelsPerRow bytes.
128 // not implemented yet
129 }
130 };
131 // 3. Initialize the width attribute of imageData to pixelsPerRow.
132 let width = pixels_per_row;
133 // 4. Initialize the height attribute of imageData to rows.
134 let height = rows;
135 // 5. Initialize the pixelFormat attribute of imageData to settings["pixelFormat"].
136 let pixel_format = settings.pixelFormat;
137 // 6. If settings["colorSpace"] exists,
138 // then initialize the colorSpace attribute of imageData to settings["colorSpace"].
139 let color_space = settings
140 .colorSpace
141 // 7. Otherwise, if defaultColorSpace was given,
142 // then initialize the colorSpace attribute of imageData to defaultColorSpace.
143 .or(default_color_space)
144 // 8. Otherwise, initialize the colorSpace attribute of imageData to "srgb".
145 .unwrap_or(PredefinedColorSpace::Srgb);
146
147 Ok(reflect_dom_object_with_proto(
148 Box::new(ImageData {
149 reflector_: Reflector::new(),
150 width,
151 height,
152 data,
153 pixel_format,
154 color_space,
155 }),
156 global,
157 proto,
158 can_gc,
159 ))
160 }
161
162 pub(crate) fn is_detached(&self) -> bool {
163 self.data.is_detached_buffer(GlobalScope::get_cx())
164 }
165
166 pub(crate) fn get_size(&self) -> Size2D<u32> {
167 Size2D::new(self.Width(), self.Height())
168 }
169
170 /// Nothing must change the array on the JS side while the slice is live.
171 #[expect(unsafe_code)]
172 pub(crate) unsafe fn as_slice(&self) -> &[u8] {
173 assert!(self.data.is_initialized());
174 let internal_data = self
175 .data
176 .get_typed_array()
177 .expect("Failed to get Data from ImageData.");
178 // NOTE(nox): This is just as unsafe as `as_slice` itself even though we
179 // are extending the lifetime of the slice, because the data in
180 // this ImageData instance will never change. The method is thus unsafe
181 // because the array may be manipulated from JS while the reference
182 // is live.
183 unsafe {
184 let ptr: *const [u8] = internal_data.as_slice() as *const _;
185 &*ptr
186 }
187 }
188
189 /// Nothing must change the array on the JS side while the slice is live.
190 #[expect(unsafe_code)]
191 pub(crate) unsafe fn get_rect(&self, rect: Rect<u32>) -> Cow<'_, [u8]> {
192 pixels::rgba8_get_rect(unsafe { self.as_slice() }, self.get_size().to_u32(), rect)
193 }
194
195 #[expect(unsafe_code)]
196 pub(crate) fn get_snapshot_rect(&self, rect: Rect<u32>) -> Snapshot {
197 Snapshot::from_vec(
198 rect.size,
199 SnapshotPixelFormat::RGBA,
200 SnapshotAlphaMode::Transparent {
201 premultiplied: false,
202 },
203 unsafe { self.get_rect(rect).into_owned() },
204 )
205 }
206
207 #[expect(unsafe_code)]
208 pub(crate) fn to_shared_memory(&self) -> GenericSharedMemory {
209 // This is safe because we copy the slice content
210 GenericSharedMemory::from_bytes(unsafe { self.as_slice() })
211 }
212
213 #[expect(unsafe_code)]
214 pub(crate) fn to_vec(&self) -> Vec<u8> {
215 // This is safe because we copy the slice content
216 unsafe { self.as_slice() }.to_vec()
217 }
218}
219
220impl Serializable for ImageData {
221 type Index = ImageDataIndex;
222 type Data = SerializableImageData;
223
224 /// <https://html.spec.whatwg.org/multipage/#the-imagedata-interface:serializable-objects>
225 fn serialize(&self) -> Result<(ImageDataId, Self::Data), ()> {
226 // Step 1 Set serialized.[[Data]] to the sub-serialization of the value of value's data attribute.
227 let data = self.to_vec();
228
229 // Step 2 Set serialized.[[Width]] to the value of value's width attribute.
230 // Step 3 Set serialized.[[Height]] to the value of value's height attribute.
231 // Step 4 Set serialized.[[ColorSpace]] to the value of value's colorSpace attribute.
232 // Step 5 Set serialized.[[PixelFormat]] to the value of value's pixelFormat attribute.
233 // Note: Since we don't support Float16Array and display-p3 color space
234 // we don't need to serialize colorSpace and pixelFormat
235 let serialized = SerializableImageData {
236 data,
237 width: self.width,
238 height: self.height,
239 };
240 Ok((ImageDataId::new(), serialized))
241 }
242
243 /// <https://html.spec.whatwg.org/multipage/#the-imagedata-interface:deserialization-steps>
244 fn deserialize(
245 owner: &GlobalScope,
246 serialized: Self::Data,
247 can_gc: CanGc,
248 ) -> Result<DomRoot<Self>, ()> {
249 // Step 1 Initialize value's data attribute to the sub-deserialization of serialized.[[Data]].
250 // Step 2 Initialize value's width attribute to serialized.[[Width]].
251 // Step 3 Initialize value's height attribute to serialized.[[Height]].
252 // Step 4 Initialize value's colorSpace attribute to serialized.[[ColorSpace]].
253 // Step 5 Initialize value's pixelFormat attribute to serialized.[[PixelFormat]].
254 ImageData::new(
255 owner,
256 serialized.width,
257 serialized.height,
258 Some(serialized.data),
259 can_gc,
260 )
261 .map_err(|_| ())
262 }
263
264 fn serialized_storage<'a>(
265 reader: StructuredData<'a, '_>,
266 ) -> &'a mut Option<FxHashMap<ImageDataId, Self::Data>> {
267 match reader {
268 StructuredData::Reader(r) => &mut r.image_data,
269 StructuredData::Writer(w) => &mut w.image_data,
270 }
271 }
272}
273
274impl ImageDataMethods<crate::DomTypeHolder> for ImageData {
275 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata>
276 fn Constructor(
277 global: &GlobalScope,
278 proto: Option<HandleObject>,
279 can_gc: CanGc,
280 sw: u32,
281 sh: u32,
282 settings: &ImageDataSettings,
283 ) -> Fallible<DomRoot<Self>> {
284 // 1. If one or both of sw and sh are zero, then throw an "IndexSizeError" DOMException.
285 if sw == 0 || sh == 0 {
286 return Err(Error::IndexSize(None));
287 }
288
289 // When a constructor is called for an ImageData that is too large, other browsers throw
290 // IndexSizeError rather than RangeError here, so we do the same.
291 pixels::compute_rgba8_byte_length_if_within_limit(sw as usize, sh as usize)
292 .ok_or(Error::IndexSize(None))?;
293
294 // 2. Initialize this given sw, sh, and settings.
295 // 3. Initialize the image data of this to transparent black.
296 Self::initialize(sw, sh, settings, None, None, global, proto, can_gc)
297 }
298
299 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-with-data>
300 fn Constructor_(
301 global: &GlobalScope,
302 proto: Option<HandleObject>,
303 can_gc: CanGc,
304 data: CustomAutoRooterGuard<Uint8ClampedArray>,
305 sw: u32,
306 sh: Option<u32>,
307 settings: &ImageDataSettings,
308 ) -> Fallible<DomRoot<Self>> {
309 // 1. Let bytesPerPixel be 4 if settings["pixelFormat"] is "rgba-unorm8"; otherwise 8.
310 let bytes_per_pixel = match settings.pixelFormat {
311 ImageDataPixelFormat::Rgba_unorm8 => 4,
312 };
313 // 2. Let length be the buffer source byte length of data.
314 let length = data.len();
315 if length == 0 {
316 return Err(Error::InvalidState(None));
317 }
318 // 3. If length is not a nonzero integral multiple of bytesPerPixel,
319 // then throw an "InvalidStateError" DOMException.
320 if length % bytes_per_pixel != 0 {
321 return Err(Error::InvalidState(None));
322 }
323 // 4. Let length be length divided by bytesPerPixel.
324 let length = length / bytes_per_pixel;
325 // 5. If length is not an integral multiple of sw, then throw an "IndexSizeError" DOMException.
326 if sw == 0 || length % sw as usize != 0 {
327 return Err(Error::IndexSize(None));
328 }
329 // 6. Let height be length divided by sw.
330 let height = length / sw as usize;
331 // 7. If sh was given and its value is not equal to height, then throw an "IndexSizeError" DOMException.
332 if sh.is_some_and(|x| height != x as usize) {
333 return Err(Error::IndexSize(None));
334 }
335 // 8. Initialize this given sw, sh, settings, and source set to data.
336 Self::initialize(
337 sw,
338 height as u32,
339 settings,
340 Some(data),
341 None,
342 global,
343 proto,
344 can_gc,
345 )
346 }
347
348 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-width>
349 fn Width(&self) -> u32 {
350 self.width
351 }
352
353 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-height>
354 fn Height(&self) -> u32 {
355 self.height
356 }
357
358 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-data>
359 fn GetData(&self, _: JSContext) -> Fallible<RootedTraceableBox<HeapUint8ClampedArray>> {
360 self.data.get_typed_array().map_err(|_| Error::JSFailed)
361 }
362
363 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-pixelformat>
364 fn PixelFormat(&self) -> ImageDataPixelFormat {
365 self.pixel_format
366 }
367
368 /// <https://html.spec.whatwg.org/multipage/#dom-imagedata-colorspace>
369 fn ColorSpace(&self) -> PredefinedColorSpace {
370 self.color_space
371 }
372}