From 67cdfdbf569deaa60c6e77b146fa819dd8e197c4 Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Mon, 2 Jul 2018 09:13:43 +0100 Subject: [PATCH 1/3] Adding WebP (without feature detect in place) --- src/codecs/browser-webp/encoder.ts | 21 +++++++++++++++++++++ src/codecs/browser-webp/options.ts | 3 +++ src/codecs/encoders.ts | 7 +++++-- src/components/app/index.tsx | 2 ++ src/components/options/index.tsx | 19 +++++++++++-------- 5 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 src/codecs/browser-webp/encoder.ts create mode 100644 src/codecs/browser-webp/options.ts diff --git a/src/codecs/browser-webp/encoder.ts b/src/codecs/browser-webp/encoder.ts new file mode 100644 index 000000000..98b8223dd --- /dev/null +++ b/src/codecs/browser-webp/encoder.ts @@ -0,0 +1,21 @@ +import { canvasEncode } from '../../lib/util'; + +export interface EncodeOptions { quality: number; } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'browser-webp'; +export const label = 'Browser WebP'; +export const mimeType = 'image/webp'; +export const extension = 'webp'; +export const defaultOptions: EncodeOptions = { quality: 0.5 }; + +export async function featureTest() { + const data = new ImageData(1, 1); + const blob = await encode(data, defaultOptions); + if (!blob) return false; + return blob.type === mimeType; +} + +export function encode(data: ImageData, { quality }: EncodeOptions) { + return canvasEncode(data, mimeType, quality); +} diff --git a/src/codecs/browser-webp/options.ts b/src/codecs/browser-webp/options.ts new file mode 100644 index 000000000..678e5a9e5 --- /dev/null +++ b/src/codecs/browser-webp/options.ts @@ -0,0 +1,3 @@ +import qualityOption from '../generic/quality-option'; + +export default qualityOption({ min: 0, max: 1, step: 0 }); diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index 294f9988b..1479d3e02 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -2,12 +2,14 @@ import * as mozJPEG from './mozjpeg/encoder'; import * as identity from './identity/encoder'; import * as browserPNG from './browser-png/encoder'; import * as browserJPEG from './browser-jpeg/encoder'; +import * as browserWebP from './browser-webp/encoder'; export type EncoderState = - identity.EncoderState | mozJPEG.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState; + identity.EncoderState | mozJPEG.EncoderState | browserPNG.EncoderState | + browserJPEG.EncoderState | browserWebP.EncoderState; export type EncoderOptions = identity.EncodeOptions | mozJPEG.EncodeOptions | browserPNG.EncodeOptions | - browserJPEG.EncodeOptions; + browserJPEG.EncodeOptions | browserWebP.EncodeOptions; export type EncoderType = keyof typeof encoderMap; export const encoderMap = { @@ -15,6 +17,7 @@ export const encoderMap = { [mozJPEG.type]: mozJPEG, [browserPNG.type]: browserPNG, [browserJPEG.type]: browserJPEG, + [browserWebP.type]: browserWebP, }; export const encoders = Array.from(Object.values(encoderMap)); diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index db302f63b..0973e21bd 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -12,6 +12,7 @@ import * as mozJPEG from '../../codecs/mozjpeg/encoder'; import * as identity from '../../codecs/identity/encoder'; import * as browserPNG from '../../codecs/browser-png/encoder'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; +import * as browserWebP from '../../codecs/browser-webp/encoder'; import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders'; interface SourceImage { @@ -54,6 +55,7 @@ async function compressImage( case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options); case browserPNG.type: return browserPNG.encode(source.data, encodeData.options); case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options); + case browserWebP.type: return browserWebP.encode(source.data, encodeData.options); default: throw Error(`Unexpected encoder name`); } })(); diff --git a/src/components/options/index.tsx b/src/components/options/index.tsx index 2b0099abc..bcc16ca58 100644 --- a/src/components/options/index.tsx +++ b/src/components/options/index.tsx @@ -3,18 +3,21 @@ import * as style from './style.scss'; import { bind } from '../../lib/util'; import MozJpegEncoderOptions from '../../codecs/mozjpeg/options'; import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options'; +import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options'; -import { type as mozJPEGType } from '../../codecs/mozjpeg/encoder'; -import { type as identityType } from '../../codecs/identity/encoder'; -import { type as browserPNGType } from '../../codecs/browser-png/encoder'; -import { type as browserJPEGType } from '../../codecs/browser-jpeg/encoder'; +import * as mozJPEG from '../../codecs/mozjpeg/encoder'; +import * as identity from '../../codecs/identity/encoder'; +import * as browserPNG from '../../codecs/browser-png/encoder'; +import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; +import * as browserWebP from '../../codecs/browser-webp/encoder'; import { EncoderState, EncoderType, EncoderOptions, encoders } from '../../codecs/encoders'; const encoderOptionsComponentMap = { - [mozJPEGType]: MozJpegEncoderOptions, - [identityType]: undefined, - [browserPNGType]: undefined, - [browserJPEGType]: BrowserJPEGEncoderOptions, + [mozJPEG.type]: MozJpegEncoderOptions, + [identity.type]: undefined, + [browserPNG.type]: undefined, + [browserJPEG.type]: BrowserJPEGEncoderOptions, + [browserWebP.type]: BrowserWebPEncoderOptions, }; interface Props { From 7385294d2054c9858cefca28a60fbf465c62275a Mon Sep 17 00:00:00 2001 From: Jake Archibald Date: Mon, 2 Jul 2018 12:24:36 +0100 Subject: [PATCH 2/3] Adding WebP check --- src/codecs/browser-webp/encoder.ts | 2 ++ src/codecs/encoders.ts | 17 +++++++++++++++ src/components/app/index.tsx | 10 +++++++-- src/components/options/index.tsx | 34 +++++++++++++++++++++++------- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/codecs/browser-webp/encoder.ts b/src/codecs/browser-webp/encoder.ts index 98b8223dd..aae422259 100644 --- a/src/codecs/browser-webp/encoder.ts +++ b/src/codecs/browser-webp/encoder.ts @@ -12,7 +12,9 @@ export const defaultOptions: EncodeOptions = { quality: 0.5 }; export async function featureTest() { const data = new ImageData(1, 1); const blob = await encode(data, defaultOptions); + // According to the spec, the blob should be null if the format isn't supported… if (!blob) return false; + // …but Safari falls back to PNG, so we need to check the mime type. return blob.type === mimeType; } diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index 1479d3e02..51a7e5a9d 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -4,6 +4,10 @@ import * as browserPNG from './browser-png/encoder'; import * as browserJPEG from './browser-jpeg/encoder'; import * as browserWebP from './browser-webp/encoder'; +export interface EncoderSupportMap { + [key: string]: boolean; +} + export type EncoderState = identity.EncoderState | mozJPEG.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState | browserWebP.EncoderState; @@ -21,3 +25,16 @@ export const encoderMap = { }; export const encoders = Array.from(Object.values(encoderMap)); + +/** Does this browser support a given encoder? Indexed by label */ +export const encodersSupported = Promise.resolve().then(async () => { + const encodersSupported: EncoderSupportMap = {}; + + await Promise.all(encoders.map(async (encoder) => { + // If the encoder provides a featureTest, call it, otherwise assume supported. + const isSupported = !('featureTest' in encoder) || await encoder.featureTest(); + encodersSupported[encoder.type] = isSupported; + })); + + return encodersSupported; +}); diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index 0973e21bd..2db904a2f 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -13,7 +13,13 @@ import * as identity from '../../codecs/identity/encoder'; import * as browserPNG from '../../codecs/browser-png/encoder'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; import * as browserWebP from '../../codecs/browser-webp/encoder'; -import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders'; +import { + EncoderState, + EncoderType, + EncoderOptions, + encoderMap, + encodersSupported, +} from '../../codecs/encoders'; interface SourceImage { file: File; @@ -56,7 +62,7 @@ async function compressImage( case browserPNG.type: return browserPNG.encode(source.data, encodeData.options); case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options); case browserWebP.type: return browserWebP.encode(source.data, encodeData.options); - default: throw Error(`Unexpected encoder name`); + default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`); } })(); diff --git a/src/components/options/index.tsx b/src/components/options/index.tsx index bcc16ca58..ef59272e6 100644 --- a/src/components/options/index.tsx +++ b/src/components/options/index.tsx @@ -10,7 +10,14 @@ import * as identity from '../../codecs/identity/encoder'; import * as browserPNG from '../../codecs/browser-png/encoder'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; import * as browserWebP from '../../codecs/browser-webp/encoder'; -import { EncoderState, EncoderType, EncoderOptions, encoders } from '../../codecs/encoders'; +import { + EncoderState, + EncoderType, + EncoderOptions, + encoders, + encodersSupported, + EncoderSupportMap, +} from '../../codecs/encoders'; const encoderOptionsComponentMap = { [mozJPEG.type]: MozJpegEncoderOptions, @@ -27,11 +34,18 @@ interface Props { onOptionsChange(newOptions: EncoderOptions): void; } -interface State {} +interface State { + encoderSupportMap?: EncoderSupportMap; +} export default class Options extends Component { typeSelect?: HTMLSelectElement; + constructor() { + super(); + encodersSupported.then(encoderSupportMap => this.setState({ encoderSupportMap })); + } + @bind onTypeChange(event: Event) { const el = event.currentTarget as HTMLSelectElement; @@ -42,18 +56,22 @@ export default class Options extends Component { this.props.onTypeChange(type); } - render({ class: className, encoderState, onOptionsChange }: Props) { + render({ class: className, encoderState, onOptionsChange }: Props, { encoderSupportMap }: State) { const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type]; return (
{EncoderOptionComponent && Date: Mon, 2 Jul 2018 12:29:44 +0100 Subject: [PATCH 3/3] Remove unused import --- src/components/app/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index 2db904a2f..047dfdf49 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -18,7 +18,6 @@ import { EncoderType, EncoderOptions, encoderMap, - encodersSupported, } from '../../codecs/encoders'; interface SourceImage {