Repository: denoland/deno
Pull request: feat(ext/image): support gif, webp for `createImageBitmap`

# Summary

continues from https://github.com/denoland/deno/pull/25517

The PR(https://github.com/denoland/deno/pull/28470) gave us a chance to reconsider supporting gif and webp, which we had previously abandoned.
I can also confirm that there is demand for webp at least. https://github.com/denoland/deno/pull/25517#issuecomment-3571049154

## build size

on main at 1146973cd

```bash
% cargo build --release --target-dir "target/$(git rev-parse --abbrev-ref HEAD)-$(git rev-parse --short HEAD)"
...
% ls -l target/main-1146973cd/release/deno
-rwxr-xr-x  1 user  46682944  132048656 Nov 25 14:53 target/main-1146973cd/release/deno

-> 132.048656 mb
```

this PR

```bash
% cargo build --release --target-dir "target/$(git rev-parse --abbrev-ref HEAD)-$(git rev-parse --short HEAD)"
...
% ls -l target/feat-webp-gif-2485a575d/release/deno
-rwxr-xr-x  1 user  46682944  132311168 Nov 25 17:14 target/feat-webp-gif-2485a575d/release/deno

-> 132.311168 mb
```

about 263kb larger

Changed files:
filename: Cargo.lock
patch: @@ -1033,6 +1033,12 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
 [[package]]
 name = "colorchoice"
 version = "1.0.0"
@@ -4412,6 +4418,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.31.1"
@@ -5321,12 +5337,25 @@ checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
 dependencies = [
  "bytemuck",
  "byteorder-lite",
+ "color_quant",
+ "gif",
+ "image-webp",
  "num-traits",
  "png",
  "zune-core",
  "zune-jpeg",
 ]
 
+[[package]]
+name = "image-webp"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
+dependencies = [
+ "byteorder-lite",
+ "quick-error 2.0.1",
+]
+
 [[package]]
 name = "import_map"
 version = "0.24.0"
@@ -7253,6 +7282,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"
@@ -7644,7 +7679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
 dependencies = [
  "hostname",
- "quick-error",
+ "quick-error 1.2.3",
 ]
 
 [[package]]
@@ -10329,6 +10364,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 = "28.0.0"

filename: ext/image/01_image.js
patch: @@ -238,28 +238,12 @@ function createImageBitmap(
         mimeType = 2;
       } else if (mimeTypeString === "image/gif") {
         mimeType = 3;
-        // NOTE: Temporarily not supported due to build size concerns
-        // https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
-        return PromiseReject(
-          new DOMException(
-            "The MIME type of source image is not supported currently",
-            "InvalidStateError",
-          ),
-        );
       } else if (mimeTypeString === "image/bmp") {
         mimeType = 4;
       } else if (mimeTypeString === "image/x-icon") {
         mimeType = 5;
       } else if (mimeTypeString === "image/webp") {
         mimeType = 6;
-        // NOTE: Temporarily not supported due to build size concerns
-        // https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
-        return PromiseReject(
-          new DOMException(
-            "The MIME type of source image is not supported currently",
-            "InvalidStateError",
-          ),
-        );
       } else {
         return PromiseReject(
           new DOMException(

filename: ext/image/Cargo.toml
patch: @@ -17,7 +17,7 @@ path = "lib.rs"
 bytemuck.workspace = true
 deno_core.workspace = true
 deno_error.workspace = true
-image = { workspace = true, features = ["png", "jpeg", "bmp", "ico"] }
+image = { workspace = true, features = ["png", "jpeg", "bmp", "ico", "webp", "gif"] }
 # NOTE: The qcms is a color space conversion crate which parses ICC profiles that used in Gecko,
 # however it supports only 8-bit color depth currently.
 # https://searchfox.org/mozilla-central/rev/f09e3f9603a08b5b51bf504846091579bc2ff531/gfx/qcms/src/transform.rs#130-137

filename: ext/image/bitmap.rs
patch: @@ -9,15 +9,15 @@ use deno_core::GarbageCollected;
 use deno_core::JsBuffer;
 use deno_core::op2;
 use deno_core::webidl::WebIdlInterfaceConverter;
-// use image::codecs::webp::WebPDecoder;
 use image::DynamicImage;
 use image::ImageDecoder;
 use image::RgbaImage;
 use image::codecs::bmp::BmpDecoder;
-// use image::codecs::gif::GifDecoder;
+use image::codecs::gif::GifDecoder;
 use image::codecs::ico::IcoDecoder;
 use image::codecs::jpeg::JpegDecoder;
 use image::codecs::png::PngDecoder;
+use image::codecs::webp::WebPDecoder;
 use image::imageops::FilterType;
 use image::imageops::overlay;
 use image::metadata::Orientation;
@@ -125,20 +125,17 @@ fn decode_bitmap_data(
             )
           }
           MimeType::Gif => {
-            // NOTE: Temporarily not supported due to build size concerns
-            // https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
-            unimplemented!();
             // The GifDecoder decodes the first frame.
-            // let mut decoder = GifDecoder::new(BufReader::new(Cursor::new(buf)))
-            //   .map_err(CanvasError::image_error_to_invalid_image)?;
-            // let orientation = decoder.orientation()?;
-            // let icc_profile = decoder.icc_profile()?;
-            // (
-            //   DynamicImage::from_decoder(decoder)
-            //     .map_err(CanvasError::image_error_to_invalid_image)?,
-            //   orientation,
-            //   icc_profile,
-            // )
+            let mut decoder = GifDecoder::new(BufReader::new(Cursor::new(buf)))
+              .map_err(ImageError::image_error_to_invalid_image)?;
+            let orientation = decoder.orientation()?;
+            let icc_profile = decoder.icc_profile()?;
+            (
+              DynamicImage::from_decoder(decoder)
+                .map_err(ImageError::image_error_to_invalid_image)?,
+              orientation,
+              icc_profile,
+            )
           }
           MimeType::Bmp => {
             let mut decoder = BmpDecoder::new(BufReader::new(Cursor::new(buf)))
@@ -165,21 +162,18 @@ fn decode_bitmap_data(
             )
           }
           MimeType::Webp => {
-            // NOTE: Temporarily not supported due to build size concerns
-            // https://github.com/denoland/deno/pull/25517#issuecomment-2626044644
-            unimplemented!();
             // The WebPDecoder decodes the first frame.
-            // let mut decoder =
-            //   WebPDecoder::new(BufReader::new(Cursor::new(buf)))
-            //     .map_err(CanvasError::image_error_to_invalid_image)?;
-            // let orientation = decoder.orientation()?;
-            // let icc_profile = decoder.icc_profile()?;
-            // (
-            //   DynamicImage::from_decoder(decoder)
-            //     .map_err(CanvasError::image_error_to_invalid_image)?,
-            //   orientation,
-            //   icc_profile,
-            // )
+            let mut decoder =
+              WebPDecoder::new(BufReader::new(Cursor::new(buf)))
+                .map_err(ImageError::image_error_to_invalid_image)?;
+            let orientation = decoder.orientation()?;
+            let icc_profile = decoder.icc_profile()?;
+            (
+              DynamicImage::from_decoder(decoder)
+                .map_err(ImageError::image_error_to_invalid_image)?,
+              orientation,
+              icc_profile,
+            )
           }
           // This pattern is unreachable due to current block is already checked by the ImageBitmapSource above.
           MimeType::NoMatch => unreachable!(),

filename: tests/unit/image_bitmap_test.ts
patch: @@ -250,23 +250,19 @@ Deno.test("imageBitmapFromBlob", async (t) => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/1x1-red8.gif`)],
     );
-    await assertRejects(() => createImageBitmap(imageData), DOMException);
-    // TODO(Hajime-san): remove the comment out when the implementation is ready
-    // const imageBitmap = await createImageBitmap(imageData);
+    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]));
+    assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255]));
   });
   await t.step("8-bit webp", async () => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/1x1-red8.webp`)],
     );
-    await assertRejects(() => createImageBitmap(imageData), DOMException);
-    // TODO(Hajime-san): remove the comment out when the implementation is ready
-    // const imageBitmap = await createImageBitmap(imageData);
+    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]));
+    assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255]));
   });
   await t.step("8-bit ico", async () => {
     const imageData = new Blob(
@@ -324,12 +320,10 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
         `${prefix}/1x1-3f-lossless-animated-semi-transparent.webp`,
       ),
     ]);
-    await assertRejects(() => createImageBitmap(imageData), DOMException);
-    // TODO(Hajime-san): remove the comment out when the implementation is ready
-    // const imageBitmap = await createImageBitmap(imageData);
+    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, 127]));
+    assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 127]));
   });
   await t.step("animated gif", async () => {
     // the chunk of animated gif is below (3 frames, 1x1, 8-bit, RGBA)
@@ -339,12 +333,10 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/1x1-3f-animated.gif`),
     ]);
-    await assertRejects(() => createImageBitmap(imageData), DOMException);
-    // TODO(Hajime-san): remove the comment out when the implementation is ready
-    // const imageBitmap = await createImageBitmap(imageData);
+    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]));
+    assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([255, 0, 0, 255]));
   });
 });