-
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 all 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.ModuleOpts): EmscriptenWasm.Module; |
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; | ||
} | ||
} | ||
|
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 |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface Encoder { | ||
encode(data: ImageData): Promise<ArrayBuffer>; | ||
} | ||
|
||
export interface Decoder { | ||
decode(data: ArrayBuffer): Promise<ImageBitmap>; | ||
} |
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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? | ||
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. We could just cache this for now so it only runs when the source image changes. OffscreenCanvas is still behind a flag and iframe would have to be served from a different origin to have any benefit :( |
||
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); | ||
} | ||
|
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.
@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 comment
The 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 comment
The 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.