From 559ba4b3ee0a1d748c89e15a60424f7bd76c9e26 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 29 May 2025 18:09:05 +0200 Subject: [PATCH] script: Let canvas serialization to image fail gracefully (#37184) Instead of panicking when serialization of canvas to image data (whether through `toBlob()` or via `toDataURL()`), properly handle failed serialization. This is an implementation of the appropriate error handling from the specification text. Testing: This change includes a new Serov-specific test, because it is impossible to know what the canvas size limits are of all browsers. Fixes: #36840. Signed-off-by: Martin Robinson --- components/script/dom/htmlcanvaselement.rs | 102 +++++++++++------- tests/wpt/mozilla/meta/MANIFEST.json | 7 ++ .../canvas-oversize-serialization.html | 31 ++++++ 3 files changed, 100 insertions(+), 40 deletions(-) create mode 100644 tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html diff --git a/components/script/dom/htmlcanvaselement.rs b/components/script/dom/htmlcanvaselement.rs index f287e6877e6..025328dd78c 100644 --- a/components/script/dom/htmlcanvaselement.rs +++ b/components/script/dom/htmlcanvaselement.rs @@ -16,7 +16,7 @@ use html5ever::{LocalName, Prefix, local_name, ns}; use image::codecs::jpeg::JpegEncoder; use image::codecs::png::PngEncoder; use image::codecs::webp::WebPEncoder; -use image::{ColorType, ImageEncoder}; +use image::{ColorType, ImageEncoder, ImageError}; #[cfg(feature = "webgpu")] use ipc_channel::ipc::{self as ipcchan}; use js::error::throw_type_error; @@ -391,7 +391,7 @@ impl HTMLCanvasElement { quality: Option, snapshot: &Snapshot, encoder: &mut W, - ) { + ) -> Result<(), ImageError> { // We can't use self.Width() or self.Height() here, since the size of the canvas // may have changed since the snapshot was created. Truncating the dimensions to a // u32 can't panic, since the data comes from a canvas which is always smaller than @@ -404,9 +404,7 @@ impl HTMLCanvasElement { EncodedImageType::Png => { // FIXME(nox): https://github.com/image-rs/image-png/issues/86 // FIXME(nox): https://github.com/image-rs/image-png/issues/87 - PngEncoder::new(encoder) - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + PngEncoder::new(encoder).write_image(canvas_data, width, height, ColorType::Rgba8) }, EncodedImageType::Jpeg => { let jpeg_encoder = if let Some(quality) = quality { @@ -424,16 +422,16 @@ impl HTMLCanvasElement { JpegEncoder::new(encoder) }; - jpeg_encoder - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + jpeg_encoder.write_image(canvas_data, width, height, ColorType::Rgba8) }, - EncodedImageType::Webp => { // No quality support because of https://github.com/image-rs/image/issues/1984 - WebPEncoder::new_lossless(encoder) - .write_image(canvas_data, width, height, ColorType::Rgba8) - .unwrap(); + WebPEncoder::new_lossless(encoder).write_image( + canvas_data, + width, + height, + ColorType::Rgba8, + ) }, } } @@ -522,17 +520,22 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { mime_type: DOMString, quality: HandleValue, ) -> Fallible { - // Step 1. + // Step 1: If this canvas element's bitmap's origin-clean flag is set to false, + // then throw a "SecurityError" DOMException. if !self.origin_is_clean() { return Err(Error::Security); } - // Step 2. + // Step 2: If this canvas element's bitmap has no pixels (i.e. either its + // horizontal dimension or its vertical dimension is zero), then return the string + // "data:,". (This is the shortest data: URL; it represents the empty string in a + // text/plain resource.) if self.Width() == 0 || self.Height() == 0 { return Ok(USVString("data:,".into())); } - // Step 3. + // Step 3: Let file be a serialization of this canvas element's bitmap as a file, + // passing type and quality if given. let Some(mut snapshot) = self.get_image_data() else { return Ok(USVString("data:,".into())); }; @@ -557,12 +560,20 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { &base64::engine::general_purpose::STANDARD, ); - self.encode_for_mime_type( - &image_type, - Self::maybe_quality(quality), - &snapshot, - &mut encoder, - ); + if self + .encode_for_mime_type( + &image_type, + Self::maybe_quality(quality), + &snapshot, + &mut encoder, + ) + .is_err() + { + // Step 4. If file is null, then return "data:,". + return Ok(USVString("data:,".into())); + } + + // Step 5. Return a data: URL representing file. [RFC2397] encoder.into_inner(); Ok(USVString(url)) } @@ -610,26 +621,37 @@ impl HTMLCanvasElementMethods for HTMLCanvasElement { return error!("Expected blob callback, but found none!"); }; - if let Some(mut snapshot) = result { - snapshot.transform( - snapshot::AlphaMode::Transparent{ premultiplied: false }, - snapshot::PixelFormat::RGBA - ); - // Step 4.1 - // If result is non-null, then set result to a serialization of result as a file with - // type and quality if given. - let mut encoded: Vec = vec![]; - - this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded); - let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); - // Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element - let blob = Blob::new(&this.global(), blob_impl, CanGc::note()); - - // Step 4.2.2 Invoke callback with « result » and "report". - let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note()); - } else { + let Some(mut snapshot) = result else { let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note()); - } + return; + }; + + snapshot.transform( + snapshot::AlphaMode::Transparent{ premultiplied: false }, + snapshot::PixelFormat::RGBA + ); + + // Step 4.1: If result is non-null, then set result to a serialization of + // result as a file with type and quality if given. + // Step 4.2: Queue an element task on the canvas blob serialization task + // source given the canvas element to run these steps: + let mut encoded: Vec = vec![]; + let blob_impl; + let blob; + let result = match this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded) { + Ok(..) => { + // Step 4.2.1: If result is non-null, then set result to a new Blob + // object, created in the relevant realm of this canvas element, + // representing result. [FILEAPI] + blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); + blob = Blob::new(&this.global(), blob_impl, CanGc::note()); + Some(&*blob) + } + Err(..) => None, + }; + + // Step 4.2.2: Invoke callback with « result » and "report". + let _ = callback.Call__(result, ExceptionHandling::Report, CanGc::note()); })); Ok(()) diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index bf19c365d17..cce286bb674 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12850,6 +12850,13 @@ ] ] }, + "canvas-oversize-serialization.html": [ + "3330ee2b8c4e33a18a3e17151fd7c398c9a5d024", + [ + null, + {} + ] + ], "canvas.initial.reset.2dstate.html": [ "e276ed09ffcf16eff16b784c622b93665c4109ee", [ diff --git a/tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html b/tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html new file mode 100644 index 00000000000..3330ee2b8c4 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html @@ -0,0 +1,31 @@ + + + + Serializing a large canvas does not panic + + + + + + + + + + + + +