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>
This commit is contained in:
Martin Robinson 2025-05-29 18:09:05 +02:00 committed by GitHub
parent 36e4886da1
commit 559ba4b3ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 100 additions and 40 deletions

View file

@ -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(())

View file

@ -12850,6 +12850,13 @@
]
]
},
"canvas-oversize-serialization.html": [
"3330ee2b8c4e33a18a3e17151fd7c398c9a5d024",
[
null,
{}
]
],
"canvas.initial.reset.2dstate.html": [
"e276ed09ffcf16eff16b784c622b93665c4109ee",
[

View file

@ -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>