diff --git a/codecs/webp_enc/webp_enc.d.ts b/codecs/webp_enc/webp_enc.d.ts new file mode 100644 index 000000000..9f2e9ca16 --- /dev/null +++ b/codecs/webp_enc/webp_enc.d.ts @@ -0,0 +1 @@ +export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module; diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index 0b121816c..074d06cd0 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -1,5 +1,6 @@ -import * as mozJPEG from './mozjpeg/encoder'; import * as identity from './identity/encoder'; +import * as mozJPEG from './mozjpeg/encoder'; +import * as webP from './webp/encoder'; import * as browserPNG from './browser-png/encoder'; import * as browserJPEG from './browser-jpeg/encoder'; import * as browserWebP from './browser-webp/encoder'; @@ -14,12 +15,12 @@ export interface EncoderSupportMap { } export type EncoderState = - identity.EncoderState | mozJPEG.EncoderState | browserPNG.EncoderState | + identity.EncoderState | mozJPEG.EncoderState | webP.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState | browserWebP.EncoderState | browserGIF.EncoderState | browserTIFF.EncoderState | browserJP2.EncoderState | browserBMP.EncoderState | browserPDF.EncoderState; export type EncoderOptions = - identity.EncodeOptions | mozJPEG.EncodeOptions | browserPNG.EncodeOptions | + identity.EncodeOptions | mozJPEG.EncodeOptions | webP.EncodeOptions | browserPNG.EncodeOptions | browserJPEG.EncodeOptions | browserWebP.EncodeOptions | browserGIF.EncodeOptions | browserTIFF.EncodeOptions | browserJP2.EncodeOptions | browserBMP.EncodeOptions | browserPDF.EncodeOptions; @@ -28,6 +29,7 @@ export type EncoderType = keyof typeof encoderMap; export const encoderMap = { [identity.type]: identity, [mozJPEG.type]: mozJPEG, + [webP.type]: webP, [browserPNG.type]: browserPNG, [browserJPEG.type]: browserJPEG, [browserWebP.type]: browserWebP, diff --git a/src/codecs/webp/Encoder.worker.ts b/src/codecs/webp/Encoder.worker.ts new file mode 100644 index 000000000..d47f591bb --- /dev/null +++ b/src/codecs/webp/Encoder.worker.ts @@ -0,0 +1,80 @@ +import webp_enc from '../../../codecs/webp_enc/webp_enc'; +// Using require() so TypeScript doesn’t complain about this not being a module. +import { EncodeOptions } from './encoder'; +const wasmBinaryUrl = require('../../../codecs/webp_enc/webp_enc.wasm'); + +// API exposed by wasm module. Details in the codec’s README. +interface ModuleAPI { + version(): number; + create_buffer(width: number, height: number): number; + destroy_buffer(pointer: number): void; + encode(buffer: number, width: number, height: number, quality: number): void; + free_result(): void; + get_result_pointer(): number; + get_result_size(): number; +} + +export default class WebPEncoder { + private emscriptenModule: Promise; + private api: Promise; + + constructor() { + this.emscriptenModule = new Promise((resolve) => { + const m = webp_enc({ + // Just to be safe, don’t automatically invoke any wasm functions + noInitialRun: false, + locateFile(url: string): string { + // Redirect the request for the wasm binary to whatever webpack gave us. + if (url.endsWith('.wasm')) { + return wasmBinaryUrl; + } + return url; + }, + onRuntimeInitialized() { + // An Emscripten is a then-able that, for some reason, `then()`s itself, + // causing an infite loop when you wrap it in a real promise. Deleten the `then` + // prop solves this for now. + // See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129 + // TODO(surma@): File a bug with Emscripten on this. + delete (m as any).then; + resolve(m); + }, + }); + }); + + this.api = (async () => { + // Not sure why, but TypeScript complains that I am using + // `emscriptenModule` before it’s getting assigned, which is clearly not + // true :shrug: Using `any` + const module = await (this as any).emscriptenModule as EmscriptenWasm.Module; + + return { + version: module.cwrap('version', 'number', []), + create_buffer: module.cwrap('create_buffer', 'number', ['number', 'number']), + destroy_buffer: module.cwrap('destroy_buffer', '', ['number']), + encode: module.cwrap('encode', '', ['number', 'number', 'number', 'number']), + free_result: module.cwrap('free_result', '', []), + get_result_pointer: module.cwrap('get_result_pointer', 'number', []), + get_result_size: module.cwrap('get_result_size', 'number', []), + }; + })(); + } + + async encode(data: ImageData, options: EncodeOptions): Promise { + const m = await this.emscriptenModule; + const api = await this.api; + + const p = api.create_buffer(data.width, data.height); + m.HEAP8.set(data.data, p); + api.encode(p, data.width, data.height, options.quality); + const resultPointer = api.get_result_pointer(); + const resultSize = api.get_result_size(); + const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize); + const result = new Uint8Array(resultView); + api.free_result(); + api.destroy_buffer(p); + + // wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer. + return result.buffer as ArrayBuffer; + } +} diff --git a/src/codecs/webp/encoder.ts b/src/codecs/webp/encoder.ts new file mode 100644 index 000000000..e15905f96 --- /dev/null +++ b/src/codecs/webp/encoder.ts @@ -0,0 +1,16 @@ +import EncoderWorker from './Encoder.worker'; + +export interface EncodeOptions { quality: number; } +export interface EncoderState { type: typeof type; options: EncodeOptions; } + +export const type = 'webp'; +export const label = 'WebP'; +export const mimeType = 'image/webp'; +export const extension = 'webp'; +export const defaultOptions: EncodeOptions = { quality: 7 }; + +export async function encode(data: ImageData, options: EncodeOptions) { + // We need to await this because it's been comlinked. + const encoder = await new EncoderWorker(); + return encoder.encode(data, options); +} diff --git a/src/codecs/webp/options.tsx b/src/codecs/webp/options.tsx new file mode 100644 index 000000000..2878d8edd --- /dev/null +++ b/src/codecs/webp/options.tsx @@ -0,0 +1,3 @@ +import qualityOption from '../generic/quality-option'; + +export default qualityOption(); diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 4de9c8893..a17842c4c 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -9,6 +9,7 @@ import { FileDropEvent } from './custom-els/FileDrop'; import './custom-els/FileDrop'; import * as mozJPEG from '../../codecs/mozjpeg/encoder'; +import * as webP from '../../codecs/webp/encoder'; import * as identity from '../../codecs/identity/encoder'; import * as browserPNG from '../../codecs/browser-png/encoder'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder'; @@ -64,6 +65,7 @@ async function compressImage( const compressedData = await (() => { switch (encodeData.type) { case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options); + case webP.type: return webP.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); diff --git a/src/components/Options/index.tsx b/src/components/Options/index.tsx index a2990d112..377707c73 100644 --- a/src/components/Options/index.tsx +++ b/src/components/Options/index.tsx @@ -3,10 +3,12 @@ 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 WebPEncoderOptions from '../../codecs/webp/options'; import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options'; -import * as mozJPEG from '../../codecs/mozjpeg/encoder'; import * as identity from '../../codecs/identity/encoder'; +import * as mozJPEG from '../../codecs/mozjpeg/encoder'; +import * as webP from '../../codecs/webp/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'; @@ -25,8 +27,9 @@ import { } from '../../codecs/encoders'; const encoderOptionsComponentMap = { - [mozJPEG.type]: MozJpegEncoderOptions, [identity.type]: undefined, + [mozJPEG.type]: MozJpegEncoderOptions, + [webP.type]: WebPEncoderOptions, [browserPNG.type]: undefined, [browserJPEG.type]: BrowserJPEGEncoderOptions, [browserWebP.type]: BrowserWebPEncoderOptions,