aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2025-05-29 18:09:05 +0200
committerGitHub <noreply@github.com>2025-05-29 16:09:05 +0000
commit559ba4b3ee0a1d748c89e15a60424f7bd76c9e26 (patch)
treefb768f3b97e3316ef7d790ff0c7275c98cc7ee6c
parent36e4886da1c0b1ea04e9ddcfd266380ed9ea53d8 (diff)
downloadservo-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.rs102
-rw-r--r--tests/wpt/mozilla/meta/MANIFEST.json7
-rw-r--r--tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html31
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>