From 7edb7f0de8cff573765ec6bd78c9809270106425 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 11:24:40 +0100 Subject: [PATCH 01/12] Wrangling TypeScript and webpack to work with Emscripten wasm stuff --- codecs/mozjpeg_enc/mozjpeg_enc.d.ts | 1 + emscripten-wasm.d.ts | 99 +++++++++++++++++++++++++++ package-lock.json | 24 +++++++ package.json | 4 ++ src/components/app/index.tsx | 8 ++- src/lib/codec-wrappers/codec.ts | 7 ++ src/lib/codec-wrappers/mozjpeg-enc.ts | 21 ++++++ webpack.config.js | 7 ++ 8 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 codecs/mozjpeg_enc/mozjpeg_enc.d.ts create mode 100644 emscripten-wasm.d.ts create mode 100644 src/lib/codec-wrappers/codec.ts create mode 100644 src/lib/codec-wrappers/mozjpeg-enc.ts diff --git a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts new file mode 100644 index 00000000..5e680ea9 --- /dev/null +++ b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts @@ -0,0 +1 @@ +export default function(): EmscriptenWasm.Module; diff --git a/emscripten-wasm.d.ts b/emscripten-wasm.d.ts new file mode 100644 index 00000000..f8988008 --- /dev/null +++ b/emscripten-wasm.d.ts @@ -0,0 +1,99 @@ +// 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"; + + 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; + } +} + diff --git a/package-lock.json b/package-lock.json index 15be382d..84533abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -385,6 +385,12 @@ "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==", "dev": true }, + "@types/webassembly-js-api": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/webassembly-js-api/-/webassembly-js-api-0.0.1.tgz", + "integrity": "sha1-YtULIBB319TMEJuxytoi/f1FI/s=", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4529,6 +4535,24 @@ "homedir-polyfill": "^1.0.1" } }, + "exports-loader": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/exports-loader/-/exports-loader-0.7.0.tgz", + "integrity": "sha512-RKwCrO4A6IiKm0pG3c9V46JxIHcDplwwGJn6+JJ1RcVnh/WSGJa0xkmk5cRVtgOPzCAtTMGj2F7nluh9L0vpSA==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "source-map": "0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.0.tgz", + "integrity": "sha1-D+llA6yGpa213mP05BKuSHLNvoY=", + "dev": true + } + } + }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", diff --git a/package.json b/package.json index c0a6a53b..372a8749 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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", @@ -52,6 +55,7 @@ "eslint-plugin-promise": "^3.7.0", "eslint-plugin-react": "^7.7.0", "eslint-plugin-standard": "^3.0.1", + "exports-loader": "^0.7.0", "html-webpack-plugin": "^3.0.6", "if-env": "^1.0.4", "loader-utils": "^1.1.0", diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index a41b8e20..8b3abae7 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -3,6 +3,8 @@ import { bind } 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 = { @@ -29,7 +31,11 @@ export default class App extends Component { if (!fileInput.files || !fileInput.files[0]) return; // TODO: handle decode error const img = await createImageBitmap(fileInput.files[0]); - this.setState({ img }); + const encoder = new MozJpegEncoder(); + const compressedData = await encoder.encode(img); + const blob = new Blob([compressedData], {type: 'image/jpeg'}); + const compressedImage = await createImageBitmap(blob); + this.setState({ img: compressedImage }); } render({ }: Props, { img }: State) { diff --git a/src/lib/codec-wrappers/codec.ts b/src/lib/codec-wrappers/codec.ts new file mode 100644 index 00000000..88483854 --- /dev/null +++ b/src/lib/codec-wrappers/codec.ts @@ -0,0 +1,7 @@ +export interface Encoder { + encode(image: ImageBitmap): Promise; +} + +export interface Decoder { + decode(data: ArrayBuffer): Promise; +} diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts new file mode 100644 index 00000000..47a3420d --- /dev/null +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -0,0 +1,21 @@ +import {Encoder} from './codec'; + +import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc'; + +export class MozJpegEncoder implements Encoder { + private emscriptenModule: Promise; + constructor() { + this.emscriptenModule = new Promise(resolve => { + console.log(mozjpeg_enc); + const m = mozjpeg_enc(); + m.onRuntimeInitialized = () => resolve(m); + }); + } + + async encode(bitmap: ImageBitmap): Promise { + console.log('encoding!'); + const m = await this.emscriptenModule; + console.log(m); + return Promise.resolve(new Uint8Array([1,2,3]).buffer); + } +} diff --git a/webpack.config.js b/webpack.config.js index a7de24f4..312166cf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -107,10 +107,17 @@ 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: /\/codec\/.*\.js$/, + loader: 'exports-loader', } ] }, plugins: [ + // Ignore some of the native Node modules for any of the codecs. These files are generated by Emscripten and are supposed to also work in Node, which we don‘t care about. + 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', From c2e2a1a0b65681f646b21b67efd270461f268417 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 16:04:56 +0100 Subject: [PATCH 02/12] Succesfully load wasm file via webpack --- codecs/mozjpeg_enc/mozjpeg_enc.d.ts | 2 +- package-lock.json | 10 ++++++++++ package.json | 1 + src/lib/codec-wrappers/mozjpeg-enc.ts | 22 ++++++++++++++++++---- webpack.config.js | 18 +++++++++++++++++- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts index 5e680ea9..c3a5a4bb 100644 --- a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts +++ b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts @@ -1 +1 @@ -export default function(): EmscriptenWasm.Module; +export default function(opts: {}): EmscriptenWasm.Module; diff --git a/package-lock.json b/package-lock.json index 84533abe..673baa70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4736,6 +4736,16 @@ "object-assign": "^4.0.1" } }, + "file-loader": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.1.11.tgz", + "integrity": "sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==", + "dev": true, + "requires": { + "loader-utils": "^1.0.2", + "schema-utils": "^0.4.5" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", diff --git a/package.json b/package.json index 372a8749..472385fc 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "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", diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index 47a3420d..732b4a47 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -1,19 +1,33 @@ 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'); export class MozJpegEncoder implements Encoder { private emscriptenModule: Promise; constructor() { this.emscriptenModule = new Promise(resolve => { - console.log(mozjpeg_enc); - const m = mozjpeg_enc(); - m.onRuntimeInitialized = () => resolve(m); + 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() { + resolve(m); + } + }); }); } async encode(bitmap: ImageBitmap): Promise { - console.log('encoding!'); + console.log('awaiting module'); + debugger; const m = await this.emscriptenModule; console.log(m); return Promise.resolve(new Uint8Array([1,2,3]).buffer); diff --git a/webpack.config.js b/webpack.config.js index 312166cf..5b4e43a3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,6 +48,18 @@ module.exports = function (_, env) { } }, module: { + // This is needed to make webpack NOT process wasm files. + // See https://github.com/webpack/webpack/issues/6725 + defaultRules: [ + { + type: "javascript/auto", + resolve: {} + }, + { + test: /\.json$/i, + type: "json" + }, + ], rules: [ { test: /\.(scss|sass)$/, @@ -110,8 +122,12 @@ module.exports = function (_, env) { }, { // All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`. - test: /\/codec\/.*\.js$/, + test: /\/codecs\/.*\.js$/, loader: 'exports-loader', + }, + { + test: /\/codecs\/.*\.wasm$/, + loader: 'file-loader', } ] }, From 8daaea5768220e8792daf9082c4f6b6a2eeb1c72 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 16:19:16 +0100 Subject: [PATCH 03/12] Fixed the freeze bug thing --- src/lib/codec-wrappers/mozjpeg-enc.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index 732b4a47..6977eeaa 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -19,6 +19,8 @@ export class MozJpegEncoder implements Encoder { return url; }, onRuntimeInitialized() { + // This is a bug I discovered. To be filed. + delete (m as any).then; resolve(m); } }); @@ -26,8 +28,6 @@ export class MozJpegEncoder implements Encoder { } async encode(bitmap: ImageBitmap): Promise { - console.log('awaiting module'); - debugger; const m = await this.emscriptenModule; console.log(m); return Promise.resolve(new Uint8Array([1,2,3]).buffer); From 49db0de05f61e4e73dc926d56cb95d9a1069203d Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 22:27:24 +0100 Subject: [PATCH 04/12] Actually piping the data through the compressor --- src/components/app/index.tsx | 19 +++++++++++++-- src/lib/codec-wrappers/codec.ts | 2 +- src/lib/codec-wrappers/mozjpeg-enc.ts | 33 ++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index 8b3abae7..80103d7b 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -25,14 +25,28 @@ export default class App extends Component { } } + private async getImageData(bitmap: ImageBitmap): Promise { + // Make canvas same size as image + const canvas = document.createElement('canvas'); + [canvas.width, canvas.height] = [bitmap.width, bitmap.height]; + // Draw image onto canvas + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error("Could not create canvas contex"); + } + ctx.drawImage(bitmap, 0, 0); + return ctx.getImageData(0, 0, bitmap.width, bitmap.height); + } + @bind async onFileChange(event: Event) { const fileInput = event.target as HTMLInputElement; if (!fileInput.files || !fileInput.files[0]) return; // TODO: handle decode error - const img = await createImageBitmap(fileInput.files[0]); + const bitmap = await createImageBitmap(fileInput.files[0]); + const data = await this.getImageData(bitmap); const encoder = new MozJpegEncoder(); - const compressedData = await encoder.encode(img); + const compressedData = await encoder.encode(data); const blob = new Blob([compressedData], {type: 'image/jpeg'}); const compressedImage = await createImageBitmap(blob); this.setState({ img: compressedImage }); @@ -53,3 +67,4 @@ export default class App extends Component { ); } } + diff --git a/src/lib/codec-wrappers/codec.ts b/src/lib/codec-wrappers/codec.ts index 88483854..c99ea3a1 100644 --- a/src/lib/codec-wrappers/codec.ts +++ b/src/lib/codec-wrappers/codec.ts @@ -1,5 +1,5 @@ export interface Encoder { - encode(image: ImageBitmap): Promise; + encode(data: ImageData): Promise; } export interface Decoder { diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index 6977eeaa..da5a0f4e 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -6,8 +6,10 @@ const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm'); export class MozJpegEncoder implements Encoder { private emscriptenModule: Promise; + private api: any; constructor() { this.emscriptenModule = new Promise(resolve => { + // TODO: See if I can just use m.then()? const m = mozjpeg_enc({ // Just to be safe, don’t automatically invoke any wasm functions // noInitialRun: false, @@ -25,11 +27,36 @@ export class MozJpegEncoder implements Encoder { } }); }); + + 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', '', ['number']), + get_result_pointer: m.cwrap('get_result_pointer', 'number', []), + get_result_size: m.cwrap('get_result_size', 'number', []), + }; + })(); } - async encode(bitmap: ImageBitmap): Promise { + async encode(data: ImageData): Promise { const m = await this.emscriptenModule; - console.log(m); - return Promise.resolve(new Uint8Array([1,2,3]).buffer); + 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(resultPointer); + api.destroy_buffer(p); + + return result.buffer; } } From 7a5c8f5d6bb24379e77952a1e4a91f2d49251015 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 22:31:20 +0100 Subject: [PATCH 05/12] Typings for cwrap API --- src/lib/codec-wrappers/mozjpeg-enc.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index da5a0f4e..f74aeed2 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -4,9 +4,20 @@ 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; - private api: any; + private api: Promise; constructor() { this.emscriptenModule = new Promise(resolve => { // TODO: See if I can just use m.then()? @@ -36,7 +47,7 @@ export class MozJpegEncoder implements Encoder { 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', '', ['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', []), }; @@ -54,7 +65,7 @@ export class MozJpegEncoder implements Encoder { const resultSize = api.get_result_size(); const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize); const result = new Uint8Array(resultView); - api.free_result(resultPointer); + api.free_result(); api.destroy_buffer(p); return result.buffer; From e38e7154a6c214cf3691cc0f428859294a5d4332 Mon Sep 17 00:00:00 2001 From: Surma Date: Thu, 17 May 2018 22:33:21 +0100 Subject: [PATCH 06/12] Disable auto-run just to be safe --- src/lib/codec-wrappers/mozjpeg-enc.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index f74aeed2..3077bad1 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -20,10 +20,9 @@ export class MozJpegEncoder implements Encoder { private api: Promise; constructor() { this.emscriptenModule = new Promise(resolve => { - // TODO: See if I can just use m.then()? const m = mozjpeg_enc({ // Just to be safe, don’t automatically invoke any wasm functions - // noInitialRun: false, + noInitialRun: false, locateFile(url: string): string { // Redirect the request for the wasm binary to whatever webpack gave us. if(url.endsWith('.wasm')) { From d4a616713af1906704f4ca2f095b051bff3b0b7e Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:29:24 +0100 Subject: [PATCH 07/12] Simplify webpack config --- webpack.config.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 5b4e43a3..7d002571 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -48,18 +48,6 @@ module.exports = function (_, env) { } }, module: { - // This is needed to make webpack NOT process wasm files. - // See https://github.com/webpack/webpack/issues/6725 - defaultRules: [ - { - type: "javascript/auto", - resolve: {} - }, - { - test: /\.json$/i, - type: "json" - }, - ], rules: [ { test: /\.(scss|sass)$/, @@ -127,12 +115,14 @@ module.exports = function (_, env) { }, { 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: [ - // Ignore some of the native Node modules for any of the codecs. These files are generated by Emscripten and are supposed to also work in Node, which we don‘t care about. new webpack.IgnorePlugin(/(fs)/, /\/codecs\//), // Pretty progressbar showing build progress: new ProgressBarPlugin({ From 1533728f595362d945a2a699605fc1233e723a57 Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:34:42 +0100 Subject: [PATCH 08/12] Add types to module initialize func --- codecs/mozjpeg_enc/mozjpeg_enc.d.ts | 2 +- emscripten-wasm.d.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts index c3a5a4bb..9f2e9ca1 100644 --- a/codecs/mozjpeg_enc/mozjpeg_enc.d.ts +++ b/codecs/mozjpeg_enc/mozjpeg_enc.d.ts @@ -1 +1 @@ -export default function(opts: {}): EmscriptenWasm.Module; +export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module; diff --git a/emscripten-wasm.d.ts b/emscripten-wasm.d.ts index f8988008..2d56197e 100644 --- a/emscripten-wasm.d.ts +++ b/emscripten-wasm.d.ts @@ -1,8 +1,16 @@ // 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? +// 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; @@ -92,7 +100,7 @@ declare namespace EmscriptenWasm { _malloc(size: number): number; _free(ptr: number): void; - // Augmentations below by surma@ + // Augmentations below by @surma. onRuntimeInitialized: () => void | null; } } From a9e1c38971f608a1e4a169cfb6734af253249598 Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:36:05 +0100 Subject: [PATCH 09/12] Style nitz --- src/components/app/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index 80103d7b..3588cac5 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -28,11 +28,12 @@ export default class App extends Component { private async getImageData(bitmap: ImageBitmap): Promise { // Make canvas same size as image const canvas = document.createElement('canvas'); - [canvas.width, canvas.height] = [bitmap.width, bitmap.height]; + 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 contex"); + throw new Error("Could not create canvas context"); } ctx.drawImage(bitmap, 0, 0); return ctx.getImageData(0, 0, bitmap.width, bitmap.height); From 19342208d284b4f2b315dc2526876ef8b7932f31 Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:38:13 +0100 Subject: [PATCH 10/12] Add explanation on infinite loop bug --- src/lib/codec-wrappers/mozjpeg-enc.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/codec-wrappers/mozjpeg-enc.ts b/src/lib/codec-wrappers/mozjpeg-enc.ts index 3077bad1..f39fdaa6 100644 --- a/src/lib/codec-wrappers/mozjpeg-enc.ts +++ b/src/lib/codec-wrappers/mozjpeg-enc.ts @@ -31,7 +31,11 @@ export class MozJpegEncoder implements Encoder { return url; }, onRuntimeInitialized() { - // This is a bug I discovered. To be filed. + // 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); } From 5245c5ca6e3947c8bafe205b4620dd98f712f9b5 Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:46:29 +0100 Subject: [PATCH 11/12] Put bitmapToImageData into utils module --- src/components/app/index.tsx | 18 ++---------------- src/lib/util.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/components/app/index.tsx b/src/components/app/index.tsx index 3588cac5..7fade86c 100644 --- a/src/components/app/index.tsx +++ b/src/components/app/index.tsx @@ -1,5 +1,5 @@ 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'; @@ -25,27 +25,13 @@ export default class App extends Component { } } - private async getImageData(bitmap: ImageBitmap): Promise { - // Make canvas same size as image - 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); - } - @bind async onFileChange(event: Event) { const fileInput = event.target as HTMLInputElement; if (!fileInput.files || !fileInput.files[0]) return; // TODO: handle decode error const bitmap = await createImageBitmap(fileInput.files[0]); - const data = await this.getImageData(bitmap); + const data = await bitmapToImageData(bitmap); const encoder = new MozJpegEncoder(); const compressedData = await encoder.encode(data); const blob = new Blob([compressedData], {type: 'image/jpeg'}); diff --git a/src/lib/util.ts b/src/lib/util.ts index 3ceadea5..3d0e09b7 100644 --- a/src/lib/util.ts +++ b/src/lib/util.ts @@ -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 { + // 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); +} + From 9d8f88555608145be312980dbb39dd0847a691bf Mon Sep 17 00:00:00 2001 From: Surma Date: Mon, 21 May 2018 13:49:26 +0100 Subject: [PATCH 12/12] Remove `SharedArrayBuffer` as an option --- src/lib/codec-wrappers/codec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/codec-wrappers/codec.ts b/src/lib/codec-wrappers/codec.ts index c99ea3a1..d0e632fb 100644 --- a/src/lib/codec-wrappers/codec.ts +++ b/src/lib/codec-wrappers/codec.ts @@ -1,5 +1,5 @@ export interface Encoder { - encode(data: ImageData): Promise; + encode(data: ImageData): Promise; } export interface Decoder {