diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-05-29 18:09:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-29 16:09:05 +0000 |
commit | 559ba4b3ee0a1d748c89e15a60424f7bd76c9e26 (patch) | |
tree | fb768f3b97e3316ef7d790ff0c7275c98cc7ee6c | |
parent | 36e4886da1c0b1ea04e9ddcfd266380ed9ea53d8 (diff) | |
download | servo-559ba4b3ee0a1d748c89e15a60424f7bd76c9e26.tar.gz servo-559ba4b3ee0a1d748c89e15a60424f7bd76c9e26.zip |
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 <mrobinson@igalia.com>
-rw-r--r-- | components/script/dom/htmlcanvaselement.rs | 102 | ||||
-rw-r--r-- | tests/wpt/mozilla/meta/MANIFEST.json | 7 | ||||
-rw-r--r-- | tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html | 31 |
3 files changed, 100 insertions, 40 deletions
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<f64>, 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<crate::DomTypeHolder> for HTMLCanvasElement { mime_type: DOMString, quality: HandleValue, ) -> Fallible<USVString> { - // 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<crate::DomTypeHolder> 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<crate::DomTypeHolder> 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<u8> = 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<u8> = 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 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Serializing a large canvas does not panic</title> + <link rel=help href="https://html.spec.whatwg.org/multipage/#dom-canvas-todataurl"> + <link rel=help href="https://html.spec.whatwg.org/multipage/#dom-canvas-toblob"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> +</head> + +<body> +<!-- This is not a standard WPT tests, because canvas size limits are specific + to browsers. For us, failure to serialize depends on both canvas size limits + and also whether or not the image library we use (image-rs) produces an error + when we attempt serialization. --> +<canvas id="canvas" width="2000000"></canvas> + +<script> + test(function() { + assert_equals(canvas.toDataURL("image/webp", 0.5), 'data:,'); + }, "Calling toDataURL on an oversized canvas results in an empty URL."); + + async_test(function(t) { + canvas.toBlob((blob) => { + assert_equals(blob, null); + t.done(); + }, "image/webp", 0.5); + }, "Calling toBlob() on an oversized canvas results in a null blob"); +</script> +</body> +</html> |