mirror of
https://github.com/servo/servo.git
synced 2025-07-23 07:13:52 +01: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::jpeg::JpegEncoder;
|
||||||
use image::codecs::png::PngEncoder;
|
use image::codecs::png::PngEncoder;
|
||||||
use image::codecs::webp::WebPEncoder;
|
use image::codecs::webp::WebPEncoder;
|
||||||
use image::{ColorType, ImageEncoder};
|
use image::{ColorType, ImageEncoder, ImageError};
|
||||||
#[cfg(feature = "webgpu")]
|
#[cfg(feature = "webgpu")]
|
||||||
use ipc_channel::ipc::{self as ipcchan};
|
use ipc_channel::ipc::{self as ipcchan};
|
||||||
use js::error::throw_type_error;
|
use js::error::throw_type_error;
|
||||||
|
@ -391,7 +391,7 @@ impl HTMLCanvasElement {
|
||||||
quality: Option<f64>,
|
quality: Option<f64>,
|
||||||
snapshot: &Snapshot,
|
snapshot: &Snapshot,
|
||||||
encoder: &mut W,
|
encoder: &mut W,
|
||||||
) {
|
) -> Result<(), ImageError> {
|
||||||
// We can't use self.Width() or self.Height() here, since the size of the canvas
|
// 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
|
// 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
|
// u32 can't panic, since the data comes from a canvas which is always smaller than
|
||||||
|
@ -404,9 +404,7 @@ impl HTMLCanvasElement {
|
||||||
EncodedImageType::Png => {
|
EncodedImageType::Png => {
|
||||||
// FIXME(nox): https://github.com/image-rs/image-png/issues/86
|
// FIXME(nox): https://github.com/image-rs/image-png/issues/86
|
||||||
// FIXME(nox): https://github.com/image-rs/image-png/issues/87
|
// FIXME(nox): https://github.com/image-rs/image-png/issues/87
|
||||||
PngEncoder::new(encoder)
|
PngEncoder::new(encoder).write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
|
||||||
.unwrap();
|
|
||||||
},
|
},
|
||||||
EncodedImageType::Jpeg => {
|
EncodedImageType::Jpeg => {
|
||||||
let jpeg_encoder = if let Some(quality) = quality {
|
let jpeg_encoder = if let Some(quality) = quality {
|
||||||
|
@ -424,16 +422,16 @@ impl HTMLCanvasElement {
|
||||||
JpegEncoder::new(encoder)
|
JpegEncoder::new(encoder)
|
||||||
};
|
};
|
||||||
|
|
||||||
jpeg_encoder
|
jpeg_encoder.write_image(canvas_data, width, height, ColorType::Rgba8)
|
||||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
|
||||||
.unwrap();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
EncodedImageType::Webp => {
|
EncodedImageType::Webp => {
|
||||||
// No quality support because of https://github.com/image-rs/image/issues/1984
|
// No quality support because of https://github.com/image-rs/image/issues/1984
|
||||||
WebPEncoder::new_lossless(encoder)
|
WebPEncoder::new_lossless(encoder).write_image(
|
||||||
.write_image(canvas_data, width, height, ColorType::Rgba8)
|
canvas_data,
|
||||||
.unwrap();
|
width,
|
||||||
|
height,
|
||||||
|
ColorType::Rgba8,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -522,17 +520,22 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
||||||
mime_type: DOMString,
|
mime_type: DOMString,
|
||||||
quality: HandleValue,
|
quality: HandleValue,
|
||||||
) -> Fallible<USVString> {
|
) -> 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() {
|
if !self.origin_is_clean() {
|
||||||
return Err(Error::Security);
|
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 {
|
if self.Width() == 0 || self.Height() == 0 {
|
||||||
return Ok(USVString("data:,".into()));
|
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 {
|
let Some(mut snapshot) = self.get_image_data() else {
|
||||||
return Ok(USVString("data:,".into()));
|
return Ok(USVString("data:,".into()));
|
||||||
};
|
};
|
||||||
|
@ -557,12 +560,20 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
||||||
&base64::engine::general_purpose::STANDARD,
|
&base64::engine::general_purpose::STANDARD,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.encode_for_mime_type(
|
if self
|
||||||
&image_type,
|
.encode_for_mime_type(
|
||||||
Self::maybe_quality(quality),
|
&image_type,
|
||||||
&snapshot,
|
Self::maybe_quality(quality),
|
||||||
&mut encoder,
|
&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();
|
encoder.into_inner();
|
||||||
Ok(USVString(url))
|
Ok(USVString(url))
|
||||||
}
|
}
|
||||||
|
@ -610,26 +621,37 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement {
|
||||||
return error!("Expected blob callback, but found none!");
|
return error!("Expected blob callback, but found none!");
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(mut snapshot) = result {
|
let Some(mut snapshot) = result else {
|
||||||
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 _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note());
|
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(())
|
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": [
|
"canvas.initial.reset.2dstate.html": [
|
||||||
"e276ed09ffcf16eff16b784c622b93665c4109ee",
|
"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