Implement mime type sniffing

This commit is contained in:
Surma
2018-07-16 14:54:48 +01:00
parent b15545402a
commit 0f08121596
6 changed files with 51 additions and 16 deletions

View File

@@ -1,7 +1,6 @@
import { canDecodeImage, fileToBitmap } from '../../lib/util'; import { canDecodeImage, fileToBitmap } from '../../lib/util';
export const name = 'Browser JPEG Decoder'; export const name = 'Browser JPEG Decoder';
export const supportedExtensions = ['jpg', 'jpeg'];
export const supportedMimeTypes = ['image/jpeg']; export const supportedMimeTypes = ['image/jpeg'];
export async function decode(file: File): Promise<ImageBitmap> { export async function decode(file: File): Promise<ImageBitmap> {
return fileToBitmap(file); return fileToBitmap(file);
@@ -13,3 +12,7 @@ const jpegFile = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgI
export function isSupported(): Promise<boolean> { export function isSupported(): Promise<boolean> {
return canDecodeImage(jpegFile); return canDecodeImage(jpegFile);
} }
export function canHandleMimeType(mimeType: string): boolean {
return supportedMimeTypes.includes(mimeType);
}

View File

@@ -1,7 +1,6 @@
import { canDecodeImage, fileToBitmap } from '../../lib/util'; import { canDecodeImage, fileToBitmap } from '../../lib/util';
export const name = 'Browser PNG Decoder'; export const name = 'Browser PNG Decoder';
export const supportedExtensions = ['png'];
export const supportedMimeTypes = ['image/png']; export const supportedMimeTypes = ['image/png'];
export async function decode(file: File): Promise<ImageBitmap> { export async function decode(file: File): Promise<ImageBitmap> {
return fileToBitmap(file); return fileToBitmap(file);
@@ -13,3 +12,7 @@ const pngFile = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfF
export function isSupported(): Promise<boolean> { export function isSupported(): Promise<boolean> {
return canDecodeImage(pngFile); return canDecodeImage(pngFile);
} }
export function canHandleMimeType(mimeType: string): boolean {
return supportedMimeTypes.includes(mimeType);
}

View File

@@ -1,7 +1,6 @@
import { canDecodeImage, fileToBitmap } from '../../lib/util'; import { canDecodeImage, fileToBitmap } from '../../lib/util';
export const name = 'Browser WebP Decoder'; export const name = 'Browser WebP Decoder';
export const supportedExtensions = ['webp'];
export const supportedMimeTypes = ['image/webp']; export const supportedMimeTypes = ['image/webp'];
export async function decode(file: File): Promise<ImageBitmap> { export async function decode(file: File): Promise<ImageBitmap> {
return fileToBitmap(file); return fileToBitmap(file);
@@ -13,3 +12,7 @@ const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//7
export function isSupported(): Promise<boolean> { export function isSupported(): Promise<boolean> {
return canDecodeImage(webpFile); return canDecodeImage(webpFile);
} }
export function canHandleMimeType(mimeType: string): boolean {
return supportedMimeTypes.includes(mimeType);
}

View File

@@ -3,12 +3,13 @@ import * as browserPNG from './browser-png/decoder';
import * as browserWebP from './browser-webp/decoder'; import * as browserWebP from './browser-webp/decoder';
import * as wasmWebP from './webp/decoder'; import * as wasmWebP from './webp/decoder';
import { sniffMimeType } from '../lib/util';
export interface Decoder { export interface Decoder {
name: string; name: string;
decode(file: File): Promise<ImageBitmap>; decode(file: File): Promise<ImageBitmap>;
isSupported(): Promise<boolean>; isSupported(): Promise<boolean>;
supportedMimeTypes: string[]; canHandleMimeType(mimeType: string): boolean;
supportedExtensions: string[];
} }
// We load all decoders and filter out the unsupported ones. // We load all decoders and filter out the unsupported ones.
@@ -19,9 +20,9 @@ export const decodersPromise: Promise<Decoder[]> = Promise.all(
wasmWebP, wasmWebP,
browserWebP, browserWebP,
] ]
.map(async (encoder) => { .map(async (decoder) => {
if (await encoder.isSupported()) { if (await decoder.isSupported()) {
return encoder; return decoder;
} }
return null; return null;
}), }),
@@ -31,12 +32,9 @@ export const decodersPromise: Promise<Decoder[]> = Promise.all(
export async function findDecoder(file: File): Promise<Decoder | undefined> { export async function findDecoder(file: File): Promise<Decoder | undefined> {
const decoders = await decodersPromise; const decoders = await decodersPromise;
// Prefer a match on mime type over a match on file extension const mimeType = await sniffMimeType(file);
const decoder = decoders.find(decoder => decoder.supportedMimeTypes.includes(file.type)); if (!mimeType) {
if (decoder) { return;
return decoder;
} }
return decoders.find(decoder => return decoders.find(decoder => decoder.canHandleMimeType(mimeType));
decoder.supportedExtensions.some(extension =>
file.name.endsWith(`.${extension}`)));
} }

View File

@@ -2,7 +2,6 @@ import { blobToArrayBuffer, imageDataToBitmap } from '../../lib/util';
import DecoderWorker from './Decoder.worker'; import DecoderWorker from './Decoder.worker';
export const name = 'WASM WebP Decoder'; export const name = 'WASM WebP Decoder';
export const supportedExtensions = ['webp'];
export const supportedMimeTypes = ['image/webp']; export const supportedMimeTypes = ['image/webp'];
export async function decode(file: File): Promise<ImageBitmap> { export async function decode(file: File): Promise<ImageBitmap> {
const decoder = await new DecoderWorker(); const decoder = await new DecoderWorker();
@@ -13,3 +12,7 @@ export async function decode(file: File): Promise<ImageBitmap> {
export async function isSupported(): Promise<boolean> { export async function isSupported(): Promise<boolean> {
return true; return true;
} }
export function canHandleMimeType(mimeType: string): boolean {
return supportedMimeTypes.includes(mimeType);
}

View File

@@ -118,3 +118,28 @@ export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
fileReader.readAsArrayBuffer(blob); fileReader.readAsArrayBuffer(blob);
}); });
} }
const magicNumberToMimeType = new Map<RegExp, string>([
[/^%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<string | undefined> {
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;
}
}
}