diff --git a/Cargo.lock b/Cargo.lock index d97b04d4aa..0aa9c78c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3252,6 +3252,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.29.0" @@ -3779,6 +3789,8 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "gif", + "jpeg-decoder", "num-traits", "png", ] @@ -3961,6 +3973,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -8139,6 +8157,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "wgpu-core" version = "0.21.1" diff --git a/ext/canvas/01_image.js b/ext/canvas/01_image.js index 6fb1ee62fc..d69c853e41 100644 --- a/ext/canvas/01_image.js +++ b/ext/canvas/01_image.js @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { internals, primordials } from "ext:core/mod.js"; -import { op_image_decode_png, op_image_process } from "ext:core/ops"; +import { op_image_decode, op_image_process } from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { DOMException } from "ext:deno_web/01_dom_exception.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; @@ -204,7 +204,7 @@ function createImageBitmap( if (options.resizeHeight === 0) { return PromiseReject( new DOMException( - "options.resizeWidth has to be greater than 0", + "options.resizeHeight has to be greater than 0", "InvalidStateError", ), ); @@ -231,15 +231,12 @@ function createImageBitmap( if (ObjectPrototypeIsPrototypeOf(BlobPrototype, image)) { return (async () => { const data = await image.arrayBuffer(); - const mimetype = sniffImage(image.type); - if (mimetype !== "image/png") { - throw new DOMException( - `Unsupported type '${image.type}'`, - "InvalidStateError", - ); - } - const { data: imageData, width, height } = op_image_decode_png( + const mimeType = sniffImage(image.type); + const { data: imageData, width, height } = op_image_decode( new Uint8Array(data), + { + mimeType, + }, ); const processedImage = processImage( imageData, diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml index 6f4e613957..7353dcc2bf 100644 --- a/ext/canvas/Cargo.toml +++ b/ext/canvas/Cargo.toml @@ -16,5 +16,5 @@ path = "lib.rs" [dependencies] deno_core.workspace = true deno_webgpu.workspace = true -image = { version = "0.24.7", default-features = false, features = ["png"] } +image = { version = "0.24.7", default-features = false, features = ["png","jpeg","bmp","ico","webp","gif"] } serde = { workspace = true, features = ["derive"] } diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs index 72173f1331..25bf5fd225 100644 --- a/ext/canvas/lib.rs +++ b/ext/canvas/lib.rs @@ -1,12 +1,10 @@ // 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::ColorType; -use image::ImageDecoder; +use image::GenericImageView; use image::Pixel; use image::RgbaImage; use serde::Deserialize; @@ -109,34 +107,56 @@ fn op_image_process( } #[derive(Debug, Serialize)] -struct DecodedPng { +struct DecodedImage { data: ToJsBuffer, width: u32, height: u32, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ImageDecodeOptions { + mime_type: String, +} + #[op2] #[serde] -fn op_image_decode_png(#[buffer] buf: &[u8]) -> Result { - let png = image::codecs::png::PngDecoder::new(buf)?; +fn op_image_decode( + #[buffer] buf: &[u8], + #[serde] options: ImageDecodeOptions, +) -> Result { + let reader = std::io::BufReader::new(std::io::Cursor::new(buf)); + let image = match &*options.mime_type { + "image/png" => { + let decoder = image::codecs::png::PngDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + "image/jpeg" => { + let decoder = image::codecs::jpeg::JpegDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + "image/gif" => { + let decoder = image::codecs::gif::GifDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + "image/bmp" => { + let decoder = image::codecs::bmp::BmpDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + "image/x-icon" => { + let decoder = image::codecs::ico::IcoDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + "image/webp" => { + let decoder = image::codecs::webp::WebPDecoder::new(reader)?; + image::DynamicImage::from_decoder(decoder)? + } + _ => unreachable!(), + }; + let (width, height) = image.dimensions(); - let (width, height) = png.dimensions(); - - // TODO(@crowlKats): maybe use DynamicImage https://docs.rs/image/0.24.7/image/enum.DynamicImage.html ? - if png.color_type() != ColorType::Rgba8 { - return Err(type_error(format!( - "Color type '{:?}' not supported", - png.color_type() - ))); - } - - // read_image will assert that the buffer is the correct size, so we need to fill it with zeros - let mut png_data = vec![0_u8; png.total_bytes() as usize]; - - png.read_image(&mut png_data)?; - - Ok(DecodedPng { - data: png_data.into(), + Ok(DecodedImage { + data: image.into_bytes().into(), width, height, }) @@ -145,7 +165,7 @@ fn op_image_decode_png(#[buffer] buf: &[u8]) -> Result { deno_core::extension!( deno_canvas, deps = [deno_webidl, deno_web, deno_webgpu], - ops = [op_image_process, op_image_decode_png], + ops = [op_image_process, op_image_decode], lazy_loaded_esm = ["01_image.js"], ); diff --git a/tests/testdata/image/1x1-red8.bmp b/tests/testdata/image/1x1-red8.bmp new file mode 100644 index 0000000000..c28d7968f8 Binary files /dev/null and b/tests/testdata/image/1x1-red8.bmp differ diff --git a/tests/testdata/image/1x1-red8.gif b/tests/testdata/image/1x1-red8.gif new file mode 100644 index 0000000000..0e5a2d361d Binary files /dev/null and b/tests/testdata/image/1x1-red8.gif differ diff --git a/tests/testdata/image/1x1-red8.ico b/tests/testdata/image/1x1-red8.ico new file mode 100644 index 0000000000..4cdfe144bd Binary files /dev/null and b/tests/testdata/image/1x1-red8.ico differ diff --git a/tests/testdata/image/1x1-red8.png b/tests/testdata/image/1x1-red8.png new file mode 100644 index 0000000000..8783fe799a Binary files /dev/null and b/tests/testdata/image/1x1-red8.png differ diff --git a/tests/testdata/image/1x1-red8.webp b/tests/testdata/image/1x1-red8.webp new file mode 100644 index 0000000000..1c35f348fb Binary files /dev/null and b/tests/testdata/image/1x1-red8.webp differ diff --git a/tests/testdata/image/1x1-white.png b/tests/testdata/image/1x1-white.png deleted file mode 100644 index dd43faec54..0000000000 Binary files a/tests/testdata/image/1x1-white.png and /dev/null differ diff --git a/tests/unit/image_bitmap_test.ts b/tests/unit/image_bitmap_test.ts index 0066311820..f1cf82de24 100644 --- a/tests/unit/image_bitmap_test.ts +++ b/tests/unit/image_bitmap_test.ts @@ -92,12 +92,40 @@ Deno.test(async function imageBitmapFlipY() { }); Deno.test(async function imageBitmapFromBlob() { - const path = "tests/testdata/image/1x1-white.png"; - const imageData = new Blob([await Deno.readFile(path)], { - type: "image/png", - }); - const imageBitmap = await createImageBitmap(imageData); - // @ts-ignore: Deno[Deno.internal].core allowed - // deno-fmt-ignore - assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255,255,255,255])); + const prefix = "tests/testdata/image"; + { + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.png`)], { type: "image/png" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); + } + { + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.bmp`)], { type: "image/bmp" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); + } + { + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.gif`)], { type: "image/gif" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); + } + { + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.webp`)], { type: "image/webp" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); + } + { + const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.ico`)], { type: "image/x-icon" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255])); + } });