Skip to content

Commit

Permalink
Basic webp integration (GoogleChromeLabs#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakearchibald authored Jul 17, 2018
1 parent 452900c commit 50a5a5d
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 5 deletions.
1 change: 1 addition & 0 deletions codecs/webp_enc/webp_enc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
8 changes: 5 additions & 3 deletions src/codecs/encoders.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions src/codecs/webp/Encoder.worker.ts
Original file line number Diff line number Diff line change
@@ -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<EmscriptenWasm.Module>;
private api: Promise<ModuleAPI>;

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<ArrayBuffer> {
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;
}
}
16 changes: 16 additions & 0 deletions src/codecs/webp/encoder.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions src/codecs/webp/options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import qualityOption from '../generic/quality-option';

export default qualityOption();
2 changes: 2 additions & 0 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 5 additions & 2 deletions src/components/Options/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down

0 comments on commit 50a5a5d

Please sign in to comment.