fix(ext/canvas): createImageBitmap must ignore the Blob.type value (#28741)

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`.
This commit is contained in:
Hajime-san 2025-05-08 22:57:28 +09:00 committed by GitHub
parent f334f903ef
commit 53b51113f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 41 deletions

View file

@ -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(

View file

@ -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) {
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 (mimeTypeString !== null && isXML(mimeTypeString)) {
return mimeTypeString;
}
if (isXML(mimeType)) {
return mimeTypeString;
}
const imageTypeMatched = imageTypePatternMatchingAlgorithm(
new TextEncoder().encode(mimeTypeString),
);
const imageTypeMatched = imageTypePatternMatchingAlgorithm(byteSequence);
if (imageTypeMatched !== undefined) {
return imageTypeMatched;
}

View file

@ -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);
});