-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Load mozjpeg codec and encode image #35
Changes from 6 commits
7edb7f0
c2e2a1a
8daaea5
49db0de
7a5c8f5
e38e715
d4a6167
1533728
a9e1c38
1934220
5245c5c
9d8f885
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default function(opts: {}): EmscriptenWasm.Module; | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @developit Currently I just have separate npm scripts to build all codecs and assume that these tasks have been run before building the app. WDYT? Do you have a better idea? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH that seems fine to me, unless we think the codecs will be changed as frequently as app code. Otherwise it seems nice to keep those things separate and keep the builds fast. Are the results from these builds committed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Build-speed was the main argument for me as well. And yes, build-artifacts are not commited. |
||
"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,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", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 = { | ||
|
@@ -23,13 +25,31 @@ export default class App extends Component<Props, State> { | |
} | ||
} | ||
|
||
private async getImageData(bitmap: ImageBitmap): Promise<ImageData> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't read/write any class state, so it'd be better as a function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, good call. I should probably start a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, we should do this on the worker side if we can (will be fun to play with offscreen canvas) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ... could we build an "offthread" canvas with an iframe? Cc @developit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we should see how much jank it causes us first. The iframe idea is interesting! OOPIFs are a thing now, so it could work. I wonder if transferrables lose some of their benefit when jumping across processes. |
||
// Make canvas same size as image | ||
const canvas = document.createElement('canvas'); | ||
[canvas.width, canvas.height] = [bitmap.width, bitmap.height]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think this technique is smarter than it is good, and it'd be better as two lines. Happy to be overridden by @developit or @kosamari though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nah, let’s keep it explicit. I don’t mind. I copy-pasted this from the |
||
// Draw image onto canvas | ||
const ctx = canvas.getContext('2d'); | ||
if (!ctx) { | ||
throw new Error("Could not create canvas contex"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "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 img = await createImageBitmap(fileInput.files[0]); | ||
this.setState({ img }); | ||
const bitmap = await createImageBitmap(fileInput.files[0]); | ||
const data = await this.getImageData(bitmap); | ||
const encoder = new MozJpegEncoder(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way it’s currently written, we should just be able to load these encoders in a worker and invoke |
||
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) { | ||
|
@@ -47,3 +67,4 @@ export default class App extends Component<Props, State> { | |
); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface Encoder { | ||
encode(data: ImageData): Promise<ArrayBuffer | SharedArrayBuffer>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What causes the return type to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, if this going to be used by anything? Won't they all have different options? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this because I thought it would be nice to have a consistent There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’ll re-check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, fair enough! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, |
||
} | ||
|
||
export interface Decoder { | ||
decode(data: ArrayBuffer): Promise<ImageBitmap>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
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() { | ||
// This is a bug I discovered. To be filed. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More info 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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need to give types to the options?