Repository: denoland/deno
Pull request: fix(ext/canvas): `createImageBitmap` must ignore the `Blob.type` value

fixes https://github.com/denoland/deno/issues/28723

I might misunderstood the spec. We should always sniff the MIME type from the byte sequence when the input is `Blob`.


Changed files:
filename: ext/canvas/01_image.js
patch: @@ -250,7 +250,9 @@ function createImageBitmap(
     if (isBlob) {
       imageBitmapSource = 0;
       buf = new Uint8Array(await image.arrayBuffer());
-      const mimeTypeString = sniffImage(image.type);
+      // NOTE: The MIME type of image/svg+xml can't support
+      // https://github.com/whatwg/html/pull/10172
+      const mimeTypeString = sniffImage(null, buf);
 
       if (mimeTypeString === "image/png") {
         mimeType = 1;
@@ -280,17 +282,6 @@ function createImageBitmap(
             "InvalidStateError",
           ),
         );
-      } else if (mimeTypeString === "") {
-        return PromiseReject(
-          new DOMException(
-            `The MIME type of source image is not specified\n
-hint: When you want to get a "Blob" from "fetch", make sure to go through a file server that returns the appropriate content-type response header,
-      and specify the URL to the file server like "await(await fetch('http://localhost:8000/sample.png').blob()".
-      Alternatively, if you are reading a local file using 'Deno.readFile' etc.,
-      set the appropriate MIME type like "new Blob([await Deno.readFile('sample.png')], { type: 'image/png' })".\n`,
-            "InvalidStateError",
-          ),
-        );
       } else {
         return PromiseReject(
           new DOMException(

filename: ext/web/01_mimesniff.js
patch: @@ -421,22 +421,19 @@ function imageTypePatternMatchingAlgorithm(input) {
 
 /**
  * Ref: https://mimesniff.spec.whatwg.org/#rules-for-sniffing-images-specifically
- * @param {string} mimeTypeString
- * @returns {string}
+ * @param {string | null} mimeTypeString
+ * @param {Uint8Array} byteSequence
+ * @returns {string | null}
  */
-function sniffImage(mimeTypeString) {
-  const mimeType = parseMimeType(mimeTypeString);
-  if (mimeType === null) {
-    return mimeTypeString;
-  }
+function sniffImage(mimeTypeString, byteSequence) {
+  // NOTE: Do we need to implement the "supplied MIME type" detection exactly?
+  // https://mimesniff.spec.whatwg.org/#supplied-mime-type-detection-algorithm
 
-  if (isXML(mimeType)) {
+  if (mimeTypeString !== null && isXML(mimeTypeString)) {
     return mimeTypeString;
   }
 
-  const imageTypeMatched = imageTypePatternMatchingAlgorithm(
-    new TextEncoder().encode(mimeTypeString),
-  );
+  const imageTypeMatched = imageTypePatternMatchingAlgorithm(byteSequence);
   if (imageTypeMatched !== undefined) {
     return imageTypeMatched;
   }

filename: tests/unit/image_bitmap_test.ts
patch: @@ -24,7 +24,6 @@ Deno.test(async function imageBitmapDirect() {
 Deno.test(async function imageBitmapRecivesImageBitmap() {
   const imageData = new Blob(
     [await Deno.readFile(`${prefix}/1x1-red16.png`)],
-    { type: "image/png" },
   );
   const imageBitmap1 = await createImageBitmap(imageData);
   const imageBitmap2 = await createImageBitmap(imageBitmap1);
@@ -111,7 +110,6 @@ Deno.test("imageOrientation", async (t) => {
 
   const imageData = new Blob(
     [await Deno.readFile(`${prefix}/squares_6.jpg`)],
-    { type: "image/jpeg" },
   );
   const WIDTH = 320;
   const CHANNELS = 3;
@@ -188,7 +186,6 @@ Deno.test("imageBitmapPremultiplyAlpha", async (t) => {
   await t.step('"Blob" premultiplyAlpha: "none"', async () => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/2x2-transparent8.png`)],
-      { type: "image/png" },
     );
     const imageBitmap = await createImageBitmap(imageData, {
       premultiplyAlpha: "none",
@@ -206,7 +203,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit png", async () => {
     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
@@ -216,7 +212,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("16-bit png", async () => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/1x1-red16.png`)],
-      { type: "image/png" },
     );
     const imageBitmap = await createImageBitmap(imageData);
     // @ts-ignore: Deno[Deno.internal].core allowed
@@ -236,7 +231,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit jpeg", async () => {
     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
@@ -246,7 +240,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit bmp", async () => {
     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
@@ -256,7 +249,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit gif", async () => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/1x1-red8.gif`)],
-      { type: "image/gif" },
     );
     await assertRejects(() => createImageBitmap(imageData), DOMException);
     // TODO(Hajime-san): remove the comment out when the implementation is ready
@@ -268,7 +260,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit webp", async () => {
     const imageData = new Blob(
       [await Deno.readFile(`${prefix}/1x1-red8.webp`)],
-      { type: "image/webp" },
     );
     await assertRejects(() => createImageBitmap(imageData), DOMException);
     // TODO(Hajime-san): remove the comment out when the implementation is ready
@@ -280,7 +271,6 @@ Deno.test("imageBitmapFromBlob", async (t) => {
   await t.step("8-bit ico", async () => {
     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
@@ -292,7 +282,7 @@ Deno.test("imageBitmapFromBlob", async (t) => {
     // 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);
   });
 });
@@ -304,7 +294,7 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
     //   0, 0, 255, 255 ]
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/1x1-2f-animated-has-def.png`),
-    ], { type: "image/png" });
+    ]);
     const imageBitmap = await createImageBitmap(imageData);
     // @ts-ignore: Deno[Deno.internal].core allowed
     // deno-fmt-ignore
@@ -317,7 +307,7 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
     //   0, 0, 255, 255 ]
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/1x1-3f-animated-no-def.png`),
-    ], { type: "image/png" });
+    ]);
     const imageBitmap = await createImageBitmap(imageData);
     // @ts-ignore: Deno[Deno.internal].core allowed
     // deno-fmt-ignore
@@ -333,7 +323,7 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
       await Deno.readFile(
         `${prefix}/1x1-3f-lossless-animated-semi-transparent.webp`,
       ),
-    ], { type: "image/webp" });
+    ]);
     await assertRejects(() => createImageBitmap(imageData), DOMException);
     // TODO(Hajime-san): remove the comment out when the implementation is ready
     // const imageBitmap = await createImageBitmap(imageData);
@@ -348,7 +338,7 @@ Deno.test("imageBitmapFromBlobAnimatedImage", async (t) => {
     //   0, 0, 255, 255 ]
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/1x1-3f-animated.gif`),
-    ], { type: "image/gif" });
+    ]);
     await assertRejects(() => createImageBitmap(imageData), DOMException);
     // TODO(Hajime-san): remove the comment out when the implementation is ready
     // const imageBitmap = await createImageBitmap(imageData);
@@ -376,7 +366,7 @@ Deno.test("imageBitmapFromBlobColorspaceConversion", async (t) => {
   await t.step('"Blob" colorSpaceConversion: "none"', async () => {
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/wide-gamut-pattern.png`),
-    ], { type: "image/png" });
+    ]);
     const imageBitmap = await createImageBitmap(imageData, {
       colorSpaceConversion: "none",
     });
@@ -389,7 +379,7 @@ Deno.test("imageBitmapFromBlobColorspaceConversion", async (t) => {
   await t.step('"Blob" colorSpaceConversion: "default"', async () => {
     const imageData = new Blob([
       await Deno.readFile(`${prefix}/wide-gamut-pattern.png`),
-    ], { type: "image/png" });
+    ]);
     const imageBitmap = await createImageBitmap(imageData, {
       colorSpaceConversion: "default",
     });
@@ -400,3 +390,22 @@ Deno.test("imageBitmapFromBlobColorspaceConversion", async (t) => {
     assertEquals(firstPixel, new Uint8Array([255, 0, 0, 255]));
   });
 });
+
+// reference:
+// https://github.com/web-platform-tests/wpt/blob/ea49709e5880c8133249d919c72d67798afc31ec/html/canvas/element/manual/imagebitmap/createImageBitmap-blob-invalidtype.html
+Deno.test("imageBitmapFromBlobInvalidtype", async () => {
+  const IMAGE = atob(
+    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
+      "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=",
+  );
+
+  const bytes = new Array(IMAGE.length);
+  for (let i = 0; i < IMAGE.length; i++) {
+    bytes[i] = IMAGE.charCodeAt(i);
+  }
+
+  const blob = new Blob([new Uint8Array(bytes)], { type: "text/html" });
+  const imageBitmap = await createImageBitmap(blob);
+  assertEquals(imageBitmap.width, 1);
+  assertEquals(imageBitmap.height, 1);
+});