From b9fe5e8e37c517e55472b55ddfe0fad7f4900a4a Mon Sep 17 00:00:00 2001 From: Hajime-san <41257923+Hajime-san@users.noreply.github.com> Date: Wed, 21 Aug 2024 05:47:41 +0900 Subject: [PATCH] support 8bit JPG --- Cargo.lock | 55 ++++++++++++++---- ext/canvas/01_image.js | 38 ++++++++++--- ext/canvas/Cargo.toml | 2 +- ext/canvas/lib.rs | 88 +++++++++++++++++------------ tests/testdata/image/1x1-red8.jpeg | Bin 0 -> 631 bytes tests/unit/image_bitmap_test.ts | 7 +++ 6 files changed, 136 insertions(+), 54 deletions(-) create mode 100644 tests/testdata/image/1x1-red8.jpeg diff --git a/Cargo.lock b/Cargo.lock index 0aa9c78c82..31ccf7c41b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,6 +598,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.6.0" @@ -3782,17 +3788,29 @@ dependencies = [ [[package]] name = "image" -version = "0.24.9" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", "gif", - "jpeg-decoder", + "image-webp", "num-traits", "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", ] [[package]] @@ -3973,12 +3991,6 @@ 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" @@ -5504,6 +5516,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-junit" version = "0.3.6" @@ -5826,7 +5844,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" dependencies = [ "hostname", - "quick-error", + "quick-error 1.2.3", ] [[package]] @@ -8766,3 +8784,18 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/ext/canvas/01_image.js b/ext/canvas/01_image.js index d69c853e41..2c74584d43 100644 --- a/ext/canvas/01_image.js +++ b/ext/canvas/01_image.js @@ -18,6 +18,8 @@ const { PromiseResolve, PromiseReject, RangeError, + ObjectAssign, + ArrayPrototypeJoin, } = primordials; import { _data, @@ -164,6 +166,11 @@ function createImageBitmap( options = undefined, ) { const prefix = "Failed to execute 'createImageBitmap'"; + // Add the value when implementing to add support for ImageBitmapSource + const imageBitmapSources = [ + "Blob", + "ImageData", + ]; // Overload: createImageBitmap(image [, options ]) if (arguments.length < 3) { @@ -212,6 +219,16 @@ function createImageBitmap( const imageBitmap = webidl.createBranded(ImageBitmap); + // 6. Switch on image: + let imageBitmapSource; + if (ObjectPrototypeIsPrototypeOf(BlobPrototype, image)) { + imageBitmapSource = imageBitmapSources[0]; + } + if (ObjectPrototypeIsPrototypeOf(ImageDataPrototype, image)) { + imageBitmapSource = imageBitmapSources[1]; + } + const _options = ObjectAssign(options, { imageBitmapSource }); + if (ObjectPrototypeIsPrototypeOf(ImageDataPrototype, image)) { const processedImage = processImage( image[_data], @@ -221,7 +238,7 @@ function createImageBitmap( sy, sw, sh, - options, + _options, ); imageBitmap[_bitmapData] = processedImage.data; imageBitmap[_width] = processedImage.outputWidth; @@ -230,23 +247,23 @@ function createImageBitmap( } if (ObjectPrototypeIsPrototypeOf(BlobPrototype, image)) { return (async () => { - const data = await image.arrayBuffer(); + const data = new Uint8Array(await image.arrayBuffer()); const mimeType = sniffImage(image.type); - const { data: imageData, width, height } = op_image_decode( - new Uint8Array(data), + const { width, height } = op_image_decode( + data, { mimeType, }, ); const processedImage = processImage( - imageData, + data, width, height, sxOrOptions, sy, sw, sh, - options, + _options, ); imageBitmap[_bitmapData] = processedImage.data; imageBitmap[_width] = processedImage.outputWidth; @@ -254,7 +271,13 @@ function createImageBitmap( return imageBitmap; })(); } else { - return PromiseReject(new TypeError("Invalid or unsupported image value")); + return PromiseReject( + new TypeError( + `${prefix}: The provided value is not of type '(${ + ArrayPrototypeJoin(imageBitmapSources, " or ") + })'.`, + ), + ); } } @@ -332,6 +355,7 @@ function processImage(input, width, height, sx, sy, sw, sh, options) { premultiply: options.premultiplyAlpha === "default" ? null : (options.premultiplyAlpha === "premultiply"), + imageBitmapSource: options.imageBitmapSource, }, ); diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml index 7353dcc2bf..ff9662b418 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","jpeg","bmp","ico","webp","gif"] } +image = { version = "0.25.2", 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 25bf5fd225..8779634096 100644 --- a/ext/canvas/lib.rs +++ b/ext/canvas/lib.rs @@ -6,9 +6,9 @@ use deno_core::ToJsBuffer; use image::imageops::FilterType; use image::GenericImageView; use image::Pixel; -use image::RgbaImage; use serde::Deserialize; use serde::Serialize; +use std::io::Cursor; use std::path::PathBuf; #[derive(Debug, Deserialize)] @@ -34,6 +34,14 @@ struct ImageProcessArgs { resize_quality: ImageResizeQuality, flip_y: bool, premultiply: Option, + image_bitmap_source: ImageBitmapSource, +} + +#[derive(Debug, Deserialize)] +// Follow the cases defined in the spec +enum ImageBitmapSource { + Blob, + ImageData, } #[op2] @@ -42,16 +50,31 @@ fn op_image_process( #[buffer] buf: &[u8], #[serde] args: ImageProcessArgs, ) -> Result { - let view = - RgbaImage::from_vec(args.width, args.height, buf.to_vec()).unwrap(); + let view = match args.image_bitmap_source { + ImageBitmapSource::Blob => image::ImageReader::new(Cursor::new(buf)) + .with_guessed_format()? + .decode()?, + ImageBitmapSource::ImageData => { + // > 4.12.5.1.15 Pixel manipulation + // > imagedata.data + // > Returns the one-dimensional array containing the data in RGBA order, as integers in the range 0 to 255. + // https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation + let image: image::DynamicImage = + image::RgbaImage::from_raw(args.width, args.height, buf.into()) + .expect("Invalid ImageData.") + .into(); + image + } + }; + let color = view.color(); let surface = if !(args.width == args.surface_width && args.height == args.surface_height && args.input_x == 0 && args.input_y == 0) { - let mut surface = RgbaImage::new(args.surface_width, args.surface_height); - + let mut surface = + image::DynamicImage::new(args.surface_width, args.surface_height, color); image::imageops::overlay(&mut surface, &view, args.input_x, args.input_y); surface @@ -66,12 +89,10 @@ fn op_image_process( ImageResizeQuality::High => FilterType::Lanczos3, }; - let mut image_out = image::imageops::resize( - &surface, - args.output_width, - args.output_height, - filter_type, - ); + // should use resize_exact + // https://github.com/image-rs/image/issues/1220#issuecomment-632060015 + let mut image_out = + surface.resize_exact(args.output_width, args.output_height, filter_type); if args.flip_y { image::imageops::flip_vertical_in_place(&mut image_out); @@ -79,36 +100,37 @@ fn op_image_process( // ignore 9. - if let Some(premultiply) = args.premultiply { - let is_not_premultiplied = image_out.pixels().any(|pixel| { - (pixel.0[0].max(pixel.0[1]).max(pixel.0[2])) > (255 * pixel.0[3]) - }); + if color.has_alpha() { + if let Some(premultiply) = args.premultiply { + let is_not_premultiplied = image_out.pixels().any(|(_, _, pixel)| { + (pixel[0].max(pixel[1]).max(pixel[2])) > (255 * pixel[3]) + }); - if premultiply { - if is_not_premultiplied { - for pixel in image_out.pixels_mut() { - let alpha = pixel.0[3]; + if premultiply { + if is_not_premultiplied { + for (_, _, mut pixel) in &mut image_out.pixels() { + let alpha = pixel[3]; + pixel.apply_without_alpha(|channel| { + (channel as f32 * (alpha as f32 / 255.0)) as u8 + }) + } + } + } else if !is_not_premultiplied { + for (_, _, mut pixel) in &mut image_out.pixels() { + let alpha = pixel[3]; pixel.apply_without_alpha(|channel| { - (channel as f32 * (alpha as f32 / 255.0)) as u8 + (channel as f32 / (alpha as f32 / 255.0)) as u8 }) } } - } else if !is_not_premultiplied { - for pixel in image_out.pixels_mut() { - let alpha = pixel.0[3]; - pixel.apply_without_alpha(|channel| { - (channel as f32 / (alpha as f32 / 255.0)) as u8 - }) - } } } - Ok(image_out.to_vec().into()) + Ok(image_out.into_bytes().into()) } #[derive(Debug, Serialize)] struct DecodedImage { - data: ToJsBuffer, width: u32, height: u32, } @@ -125,7 +147,7 @@ fn op_image_decode( #[buffer] buf: &[u8], #[serde] options: ImageDecodeOptions, ) -> Result { - let reader = std::io::BufReader::new(std::io::Cursor::new(buf)); + let reader = std::io::BufReader::new(Cursor::new(buf)); let image = match &*options.mime_type { "image/png" => { let decoder = image::codecs::png::PngDecoder::new(reader)?; @@ -155,11 +177,7 @@ fn op_image_decode( }; let (width, height) = image.dimensions(); - Ok(DecodedImage { - data: image.into_bytes().into(), - width, - height, - }) + Ok(DecodedImage { width, height }) } deno_core::extension!( diff --git a/tests/testdata/image/1x1-red8.jpeg b/tests/testdata/image/1x1-red8.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3d042f466c62d3384ca70ecc08a3aaed255df6b9 GIT binary patch literal 631 zcmex=ECr+Nabot8F zYu9hwy!G(W<0ns_J%91?)yGetzkL1n{m0K=Ab&A3FhjfrBq1I{^A|8W7@1gDm|56C z{$gY*2V!PH7FI<=HX+AA_QXPAC8I_T5vPd@Hy-3vHV*nAnpAX=OH9S&q3TDF*T6m_ k&SOnv`3&wcguiYv@Gvt1lM%BZgFVBe*U$Lx`2W8N07nYW<^TWy literal 0 HcmV?d00001 diff --git a/tests/unit/image_bitmap_test.ts b/tests/unit/image_bitmap_test.ts index f1cf82de24..27f867236e 100644 --- a/tests/unit/image_bitmap_test.ts +++ b/tests/unit/image_bitmap_test.ts @@ -100,6 +100,13 @@ Deno.test(async function imageBitmapFromBlob() { // 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.jpeg`)], { type: "image/jpeg" }); + const imageBitmap = await createImageBitmap(imageData); + // @ts-ignore: Deno[Deno.internal].core allowed + // deno-fmt-ignore + assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([254, 0, 0])); + } { const imageData = new Blob([await Deno.readFile(`${prefix}/1x1-red8.bmp`)], { type: "image/bmp" }); const imageBitmap = await createImageBitmap(imageData);