mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
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:
parent
36e4886da1
commit
559ba4b3ee
3 changed files with 100 additions and 40 deletions
|
@ -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(())
|
||||
|
|
7
tests/wpt/mozilla/meta/MANIFEST.json
vendored
7
tests/wpt/mozilla/meta/MANIFEST.json
vendored
|
@ -12850,6 +12850,13 @@
|
|||
]
|
||||
]
|
||||
},
|
||||
"canvas-oversize-serialization.html": [
|
||||
"3330ee2b8c4e33a18a3e17151fd7c398c9a5d024",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
]
|
||||
],
|
||||
"canvas.initial.reset.2dstate.html": [
|
||||
"e276ed09ffcf16eff16b784c622b93665c4109ee",
|
||||
[
|
||||
|
|
31
tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html
vendored
Normal file
31
tests/wpt/mozilla/tests/mozilla/canvas-oversize-serialization.html
vendored
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue