Skip to content

Commit

Permalink
Merge pull request GoogleChromeLabs#35 from GoogleChromeLabs/load-codec
Browse files Browse the repository at this point in the history
Load mozjpeg codec and encode image
  • Loading branch information
surma authored May 22, 2018
2 parents 1dcb5e4 + cdcedee commit 11690ab
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 4 deletions.
1 change: 1 addition & 0 deletions codecs/mozjpeg_enc/mozjpeg_enc.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
107 changes: 107 additions & 0 deletions emscripten-wasm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
// TODO(@surma): Upstream this?
declare namespace EmscriptenWasm {
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER";

// Options object for modularized Emscripten files. Shoe-horned by @surma.
// FIXME: This an incomplete definition!
interface ModuleOpts {
noInitialRun?: boolean;
locateFile?: (url: string) => string;
onRuntimeInitialized?: () => void;
}

interface Module {
print(str: string): void;
printErr(str: string): void;
arguments: string[];
environment: EnvironmentType;
preInit: { (): void }[];
preRun: { (): void }[];
postRun: { (): void }[];
preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean;
noExitRuntime: boolean;
logReadFiles: boolean;
filePackagePrefixURL: string;
wasmBinary: ArrayBuffer;

destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
instantiateWasm(
imports: WebAssembly.Imports,
successCallback: (module: WebAssembly.Module) => void
): WebAssembly.Exports;
locateFile(url: string): string;
onCustomMessage(event: MessageEvent): void;

Runtime: any;

ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any;
cwrap(ident: string, returnType: string | null, argTypes: string[]): any;

setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
getValue(ptr: number, type: string, noSafe?: boolean): number;

ALLOC_NORMAL: number;
ALLOC_STACK: number;
ALLOC_STATIC: number;
ALLOC_DYNAMIC: number;
ALLOC_NONE: number;

allocate(slab: any, types: string, allocator: number, ptr: number): number;
allocate(slab: any, types: string[], allocator: number, ptr: number): number;

Pointer_stringify(ptr: number, length?: number): string;
UTF16ToString(ptr: number): string;
stringToUTF16(str: string, outPtr: number): void;
UTF32ToString(ptr: number): string;
stringToUTF32(str: string, outPtr: number): void;

// USE_TYPED_ARRAYS == 1
HEAP: Int32Array;
IHEAP: Int32Array;
FHEAP: Float64Array;

// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;

TOTAL_STACK: number;
TOTAL_MEMORY: number;
FAST_MEMORY: number;

addOnPreRun(cb: () => any): void;
addOnInit(cb: () => any): void;
addOnPreMain(cb: () => any): void;
addOnExit(cb: () => any): void;
addOnPostRun(cb: () => any): void;

// Tools
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
intArrayToString(array: number[]): string;
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
writeArrayToMemory(array: number[], buffer: number): void;
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;

addRunDependency(id: any): void;
removeRunDependency(id: any): void;


preloadedImages: any;
preloadedAudios: any;

_malloc(size: number): number;
_free(ptr: number): void;

// Augmentations below by @surma.
onRuntimeInitialized: () => void | null;
}
}

34 changes: 34 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"version": "0.0.0",
"license": "apache-2.0",
"scripts": {
"build:mozjpeg_enc": "cd codecs/mozjpeg_enc && npm run build",
"build:codecs": "npm run build:mozjpeg_enc",
"start": "webpack serve --hot",
"build": "webpack -p",
"lint": "eslint src"
Expand All @@ -30,6 +32,7 @@
],
"devDependencies": {
"@types/node": "^9.4.7",
"@types/webassembly-js-api": "0.0.1",
"babel-loader": "^7.1.4",
"babel-plugin-jsx-pragmatic": "^1.0.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
Expand All @@ -52,6 +55,8 @@
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-standard": "^3.0.1",
"exports-loader": "^0.7.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.0.6",
"if-env": "^1.0.4",
"loader-utils": "^1.1.0",
Expand Down
14 changes: 11 additions & 3 deletions src/components/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/util';
import { bind, bitmapToImageData } from '../../lib/util';
import * as style from './style.scss';
import Output from '../output';

import {MozJpegEncoder} from '../../lib/codec-wrappers/mozjpeg-enc';

type Props = {};

type State = {
Expand All @@ -28,8 +30,13 @@ export default class App extends Component<Props, State> {
const fileInput = event.target as HTMLInputElement;
if (!fileInput.files || !fileInput.files[0]) return;
// TODO: handle decode error
const img = await createImageBitmap(fileInput.files[0]);
this.setState({ img });
const bitmap = await createImageBitmap(fileInput.files[0]);
const data = await bitmapToImageData(bitmap);
const encoder = new MozJpegEncoder();
const compressedData = await encoder.encode(data);
const blob = new Blob([compressedData], {type: 'image/jpeg'});
const compressedImage = await createImageBitmap(blob);
this.setState({ img: compressedImage });
}

render({ }: Props, { img }: State) {
Expand All @@ -47,3 +54,4 @@ export default class App extends Component<Props, State> {
);
}
}

7 changes: 7 additions & 0 deletions src/lib/codec-wrappers/codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Encoder {
encode(data: ImageData): Promise<ArrayBuffer>;
}

export interface Decoder {
decode(data: ArrayBuffer): Promise<ImageBitmap>;
}
76 changes: 76 additions & 0 deletions src/lib/codec-wrappers/mozjpeg-enc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {Encoder} from './codec';

import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
// Using require() so TypeScript doesn’t complain about this not being a module.
const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_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 class MozJpegEncoder implements Encoder {
private emscriptenModule: Promise<EmscriptenWasm.Module>;
private api: Promise<ModuleAPI>;
constructor() {
this.emscriptenModule = new Promise(resolve => {
const m = mozjpeg_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 m = await (this as any).emscriptenModule;
return {
version: m.cwrap('version', 'number', []),
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
encode: m.cwrap('encode', '', ['number', 'number', 'number', 'number']),
free_result: m.cwrap('free_result', '', []),
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
get_result_size: m.cwrap('get_result_size', 'number', []),
};
})();
}

async encode(data: ImageData): Promise<ArrayBuffer | SharedArrayBuffer> {
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, 2);
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);

return result.buffer;
}
}
19 changes: 19 additions & 0 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,22 @@ export function bind(target: any, propertyKey: string, descriptor: PropertyDescr
}
};
}

/**
* Turns a given `ImageBitmap` into `ImageData`.
*/
export async function bitmapToImageData(bitmap: ImageBitmap): Promise<ImageData> {
// Make canvas same size as image
// TODO: Move this off-thread if possible with `OffscreenCanvas` or iFrames?
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error("Could not create canvas context");
}
ctx.drawImage(bitmap, 0, 0);
return ctx.getImageData(0, 0, bitmap.width, bitmap.height);
}

15 changes: 14 additions & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,23 @@ module.exports = function (_, env) {
loader: 'babel-loader',
// Don't respect any Babel RC files found on the filesystem:
options: Object.assign(readJson('.babelrc'), { babelrc: false })
},
{
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
test: /\/codecs\/.*\.js$/,
loader: 'exports-loader',
},
{
test: /\/codecs\/.*\.wasm$/,
// This is needed to make webpack NOT process wasm files.
// See https://github.com/webpack/webpack/issues/6725
type: 'javascript/auto',
loader: 'file-loader',
}
]
],
},
plugins: [
new webpack.IgnorePlugin(/(fs)/, /\/codecs\//),
// Pretty progressbar showing build progress:
new ProgressBarPlugin({
format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m\r',
Expand Down

0 comments on commit 11690ab

Please sign in to comment.