diff --git a/src/codecs/browser-webp/encoder.ts b/src/codecs/browser-webp/encoder.ts new file mode 100644 index 000000000..aae422259 --- /dev/null +++ b/src/codecs/browser-webp/encoder.ts @@ -0,0 +1,23 @@ +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); + // 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; +} + +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..51a7e5a9d 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -2,12 +2,18 @@ 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 interface EncoderSupportMap { + [key: string]: boolean; +} 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 +21,20 @@ export const encoderMap = { [mozJPEG.type]: mozJPEG, [browserPNG.type]: browserPNG, [browserJPEG.type]: browserJPEG, + [browserWebP.type]: browserWebP, }; 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 db302f63b..047dfdf49 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -12,7 +12,13 @@ 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 { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders'; +import * as browserWebP from '../../codecs/browser-webp/encoder'; +import { + EncoderState, + EncoderType, + EncoderOptions, + encoderMap, +} from '../../codecs/encoders'; interface SourceImage { file: File; @@ -54,7 +60,8 @@ 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); - default: throw Error(`Unexpected encoder name`); + case browserWebP.type: return browserWebP.encode(source.data, encodeData.options); + default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`); } })(); diff --git a/src/components/options/index.tsx b/src/components/options/index.tsx index 2b0099abc..ef59272e6 100644 --- a/src/components/options/index.tsx +++ b/src/components/options/index.tsx @@ -3,18 +3,28 @@ 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 { EncoderState, EncoderType, EncoderOptions, encoders } from '../../codecs/encoders'; +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, + encodersSupported, + EncoderSupportMap, +} 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 { @@ -24,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; @@ -39,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 &&