diff --git a/src/codecs/browser-jpeg/decoder.ts b/src/codecs/browser-jpeg/decoder.ts index d6107bdd..6321d3a1 100644 --- a/src/codecs/browser-jpeg/decoder.ts +++ b/src/codecs/browser-jpeg/decoder.ts @@ -1,7 +1,6 @@ import { canDecodeImage, fileToBitmap } from '../../lib/util'; export const name = 'Browser JPEG Decoder'; -export const supportedExtensions = ['jpg', 'jpeg']; export const supportedMimeTypes = ['image/jpeg']; export async function decode(file: File): Promise { return fileToBitmap(file); @@ -13,3 +12,7 @@ const jpegFile = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgI export function isSupported(): Promise { return canDecodeImage(jpegFile); } + +export function canHandleMimeType(mimeType: string): boolean { + return supportedMimeTypes.includes(mimeType); +} diff --git a/src/codecs/browser-png/decoder.ts b/src/codecs/browser-png/decoder.ts index 357b0ab9..47ecd6b8 100644 --- a/src/codecs/browser-png/decoder.ts +++ b/src/codecs/browser-png/decoder.ts @@ -1,7 +1,6 @@ import { canDecodeImage, fileToBitmap } from '../../lib/util'; export const name = 'Browser PNG Decoder'; -export const supportedExtensions = ['png']; export const supportedMimeTypes = ['image/png']; export async function decode(file: File): Promise { return fileToBitmap(file); @@ -13,3 +12,7 @@ const pngFile = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfF export function isSupported(): Promise { return canDecodeImage(pngFile); } + +export function canHandleMimeType(mimeType: string): boolean { + return supportedMimeTypes.includes(mimeType); +} diff --git a/src/codecs/browser-webp/decoder.ts b/src/codecs/browser-webp/decoder.ts index baa962fd..9a570685 100644 --- a/src/codecs/browser-webp/decoder.ts +++ b/src/codecs/browser-webp/decoder.ts @@ -1,7 +1,6 @@ import { canDecodeImage, fileToBitmap } from '../../lib/util'; export const name = 'Browser WebP Decoder'; -export const supportedExtensions = ['webp']; export const supportedMimeTypes = ['image/webp']; export async function decode(file: File): Promise { return fileToBitmap(file); @@ -13,3 +12,7 @@ const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//7 export function isSupported(): Promise { return canDecodeImage(webpFile); } + +export function canHandleMimeType(mimeType: string): boolean { + return supportedMimeTypes.includes(mimeType); +} diff --git a/src/codecs/decoders.ts b/src/codecs/decoders.ts index 44c0b977..b49a2495 100644 --- a/src/codecs/decoders.ts +++ b/src/codecs/decoders.ts @@ -3,12 +3,13 @@ import * as browserPNG from './browser-png/decoder'; import * as browserWebP from './browser-webp/decoder'; import * as wasmWebP from './webp/decoder'; +import { sniffMimeType } from '../lib/util'; + export interface Decoder { name: string; decode(file: File): Promise; isSupported(): Promise; - supportedMimeTypes: string[]; - supportedExtensions: string[]; + canHandleMimeType(mimeType: string): boolean; } // We load all decoders and filter out the unsupported ones. @@ -19,9 +20,9 @@ export const decodersPromise: Promise = Promise.all( wasmWebP, browserWebP, ] - .map(async (encoder) => { - if (await encoder.isSupported()) { - return encoder; + .map(async (decoder) => { + if (await decoder.isSupported()) { + return decoder; } return null; }), @@ -31,12 +32,9 @@ export const decodersPromise: Promise = Promise.all( export async function findDecoder(file: File): Promise { const decoders = await decodersPromise; - // Prefer a match on mime type over a match on file extension - const decoder = decoders.find(decoder => decoder.supportedMimeTypes.includes(file.type)); - if (decoder) { - return decoder; + const mimeType = await sniffMimeType(file); + if (!mimeType) { + return; } - return decoders.find(decoder => - decoder.supportedExtensions.some(extension => - file.name.endsWith(`.${extension}`))); + return decoders.find(decoder => decoder.canHandleMimeType(mimeType)); } diff --git a/src/codecs/webp/decoder.ts b/src/codecs/webp/decoder.ts index 685a2d7e..497d3eab 100644 --- a/src/codecs/webp/decoder.ts +++ b/src/codecs/webp/decoder.ts @@ -2,7 +2,6 @@ import { blobToArrayBuffer, imageDataToBitmap } from '../../lib/util'; import DecoderWorker from './Decoder.worker'; export const name = 'WASM WebP Decoder'; -export const supportedExtensions = ['webp']; export const supportedMimeTypes = ['image/webp']; export async function decode(file: File): Promise { const decoder = await new DecoderWorker(); @@ -13,3 +12,7 @@ export async function decode(file: File): Promise { export async function isSupported(): Promise { return true; } + +export function canHandleMimeType(mimeType: string): boolean { + return supportedMimeTypes.includes(mimeType); +} diff --git a/src/lib/util.ts b/src/lib/util.ts index 1c731b09..1a35eb02 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -118,3 +118,28 @@ export function blobToArrayBuffer(blob: Blob): Promise { fileReader.readAsArrayBuffer(blob); }); } + +const magicNumberToMimeType = new Map([ + [/^%PDF-/, 'application/pdf'], + [/^GIF87a/, 'image/gif'], + [/^GIF89a/, 'image/gif'], + [/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'], + [/^\xFF\xD8\xFF/, 'image/jpeg'], + [/^BM/, 'image/bmp'], + [/^I I/, 'image/tiff'], + [/^II*/, 'image/tiff'], + [/^MM\x00*/, 'image/tiff'], + [/^RIFF....WEBPVP8 /, 'image/webp'], +]); + +export async function sniffMimeType(blob: Blob): Promise { + const firstChunk = await blobToArrayBuffer(blob.slice(0, 1024)); + const firstChunkString = Array.from( + new Uint8Array(firstChunk)).map(v => String.fromCodePoint(v), + ).join(''); + for (const [detector, mimeType] of magicNumberToMimeType.entries()) { + if (detector.test(firstChunkString)) { + return mimeType; + } + } +}