From 77703e8ba2caec1d58d652ea30f3db92826c3f3c Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:29:48 +0900 Subject: [PATCH] handling unsuportted situation - adding `DOMExceptionInvalidStateError` in global due to handling not supported image format - animation image is not supported currently --- ext/canvas/error.rs | 30 ++++++++++ ext/canvas/lib.rs | 56 +++++++++++++++--- runtime/errors.rs | 1 + runtime/js/99_main.js | 6 ++ tests/testdata/image/1x1-animation-rgba8.webp | Bin 0 -> 188 bytes tests/testdata/image/1x1-red32f.exr | Bin 0 -> 452 bytes tests/unit/image_bitmap_test.ts | 16 ++++- 7 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 ext/canvas/error.rs create mode 100644 tests/testdata/image/1x1-animation-rgba8.webp create mode 100644 tests/testdata/image/1x1-red32f.exr diff --git a/ext/canvas/error.rs b/ext/canvas/error.rs new file mode 100644 index 0000000000..bd08f9e8d6 --- /dev/null +++ b/ext/canvas/error.rs @@ -0,0 +1,30 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::AnyError; +use std::fmt; + +#[derive(Debug)] +pub struct DOMExceptionInvalidStateError { + pub msg: String, +} + +impl DOMExceptionInvalidStateError { + pub fn new(msg: &str) -> Self { + DOMExceptionInvalidStateError { + msg: msg.to_string(), + } + } +} + +impl fmt::Display for DOMExceptionInvalidStateError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } +} + +impl std::error::Error for DOMExceptionInvalidStateError {} + +pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionInvalidStateError") +} diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs index 8779634096..ef5516bef5 100644 --- a/ext/canvas/lib.rs +++ b/ext/canvas/lib.rs @@ -1,16 +1,22 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::op2; use deno_core::ToJsBuffer; use image::imageops::FilterType; +use image::AnimationDecoder; use image::GenericImageView; use image::Pixel; use serde::Deserialize; use serde::Serialize; +use std::io::BufReader; use std::io::Cursor; use std::path::PathBuf; +pub mod error; +use error::DOMExceptionInvalidStateError; + #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] enum ImageResizeQuality { @@ -20,6 +26,13 @@ enum ImageResizeQuality { High, } +#[derive(Debug, Deserialize)] +// Follow the cases defined in the spec +enum ImageBitmapSource { + Blob, + ImageData, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ImageProcessArgs { @@ -37,13 +50,6 @@ struct ImageProcessArgs { image_bitmap_source: ImageBitmapSource, } -#[derive(Debug, Deserialize)] -// Follow the cases defined in the spec -enum ImageBitmapSource { - Blob, - ImageData, -} - #[op2] #[serde] fn op_image_process( @@ -147,10 +153,25 @@ fn op_image_decode( #[buffer] buf: &[u8], #[serde] options: ImageDecodeOptions, ) -> Result { - let reader = std::io::BufReader::new(Cursor::new(buf)); + let reader = BufReader::new(Cursor::new(buf)); + // + // TODO: support animated images + // It's a little hard to implement animated images along spec because of the complexity. + // + // > If this is an animated image, imageBitmap's bitmap data must only be taken from + // > the default image of the animation (the one that the format defines is to be used when animation is + // > not supported or is disabled), or, if there is no such image, the first frame of the animation. + // https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html + // + // see also browser implementations: (The implementation of Gecko and WebKit is hard to read.) + // https://source.chromium.org/chromium/chromium/src/+/bdbc054a6cabbef991904b5df9066259505cc686:third_party/blink/renderer/platform/image-decoders/image_decoder.h;l=175-189 + // let image = match &*options.mime_type { "image/png" => { let decoder = image::codecs::png::PngDecoder::new(reader)?; + if decoder.is_apng()? { + return Err(type_error("Animation image is not supported.")); + } image::DynamicImage::from_decoder(decoder)? } "image/jpeg" => { @@ -158,6 +179,11 @@ fn op_image_decode( image::DynamicImage::from_decoder(decoder)? } "image/gif" => { + let decoder = image::codecs::gif::GifDecoder::new(reader)?; + if decoder.into_frames().count() > 1 { + return Err(type_error("Animation image is not supported.")); + } + let reader = BufReader::new(Cursor::new(buf)); let decoder = image::codecs::gif::GifDecoder::new(reader)?; image::DynamicImage::from_decoder(decoder)? } @@ -171,9 +197,21 @@ fn op_image_decode( } "image/webp" => { let decoder = image::codecs::webp::WebPDecoder::new(reader)?; + if decoder.has_animation() { + return Err(type_error("Animation image is not supported.")); + } image::DynamicImage::from_decoder(decoder)? } - _ => unreachable!(), + // return an error if the mime type is not supported in the variable list of ImageTypePatternTable below + // ext/web/01_mimesniff.js + _ => { + return Err( + DOMExceptionInvalidStateError::new( + "The source image is not a supported format.", + ) + .into(), + ) + } }; let (width, height) = image.dimensions(); diff --git a/runtime/errors.rs b/runtime/errors.rs index 694402773e..bd92b5cef4 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -159,6 +159,7 @@ pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { .or_else(|| deno_web::get_error_class_name(e)) .or_else(|| deno_webstorage::get_not_supported_error_class_name(e)) .or_else(|| deno_websocket::get_network_error_class_name(e)) + .or_else(|| deno_canvas::error::get_error_class_name(e)) .or_else(|| { e.downcast_ref::() .map(get_dlopen_error_class) diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 8b0d579ab5..8250ce59b2 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -453,6 +453,12 @@ core.registerErrorBuilder( return new DOMException(msg, "DataError"); }, ); +core.registerErrorBuilder( + "DOMExceptionInvalidStateError", + function DOMExceptionInvalidStateError(msg) { + return new DOMException(msg, "InvalidStateError"); + }, +); function runtimeStart( denoVersion, diff --git a/tests/testdata/image/1x1-animation-rgba8.webp b/tests/testdata/image/1x1-animation-rgba8.webp new file mode 100644 index 0000000000000000000000000000000000000000..3d237b7ecf74e693bd77917822f62f5597eda51d GIT binary patch literal 188 zcmWIYbaUInz`zjh>J$(bU=hIuWD5Z?1UUM6`mzC;|AByk!O_pxO#>zcrC%^JFaq`Y j@B{h!KrFy6@VEaTtJ0$Xj6|xx@V|p3^#T|FL)8NSf{Y@{ literal 0 HcmV?d00001 diff --git a/tests/testdata/image/1x1-red32f.exr b/tests/testdata/image/1x1-red32f.exr new file mode 100644 index 0000000000000000000000000000000000000000..23ab61731ed8d471a3feb33199be3d3c973d6305 GIT binary patch literal 452 zcmah_OHM;E44gi(09M=t(F;K70Zo5i847+;M1bZq{>Nt@!mGF&{GbS@xyAN3C`Sg%0B>R6I@~d literal 0 HcmV?d00001 diff --git a/tests/unit/image_bitmap_test.ts b/tests/unit/image_bitmap_test.ts index 27f867236e..4d6cee31ec 100644 --- a/tests/unit/image_bitmap_test.ts +++ b/tests/unit/image_bitmap_test.ts @@ -1,6 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { assertEquals } from "./test_util.ts"; +import { assertEquals, assertRejects } from "./test_util.ts"; function generateNumberedData(n: number): Uint8ClampedArray { return new Uint8ClampedArray( @@ -128,6 +128,14 @@ Deno.test(async function imageBitmapFromBlob() { // deno-fmt-ignore assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); } + { + // the chunk of animation webp is below (3 frames, 1x1, 8-bit, RGBA) + // [ 255, 0, 0, 127, + // 0, 255, 0, 127, + // 0, 0, 255, 127 ] + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-animation-rgba8.webp`)], { type: "image/webp" }); + await assertRejects(() => createImageBitmap(imageData), TypeError); + } { const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.ico`)], { type: "image/x-icon" }); const imageBitmap = await createImageBitmap(imageData); @@ -135,4 +143,10 @@ Deno.test(async function imageBitmapFromBlob() { // deno-fmt-ignore assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); } + { + // image/x-exr is a known mimetype for OpenEXR + // https://www.digipres.org/formats/sources/fdd/formats/#fdd000583 + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red32f.exr`)], { type: "image/x-exr" }); + await assertRejects(() => createImageBitmap(imageData), DOMException); + } });