mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 02:29:50 +00:00
Merge pull request #35 from GoogleChromeLabs/load-codec
Load mozjpeg codec and encode image
This commit is contained in:
1
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
Normal file
1
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
|
||||
107
emscripten-wasm.d.ts
vendored
Normal file
107
emscripten-wasm.d.ts
vendored
Normal file
@@ -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
package-lock.json
generated
34
package-lock.json
generated
@@ -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",
|
||||
@@ -4712,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",
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 = {
|
||||
@@ -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) {
|
||||
@@ -47,3 +54,4 @@ export default class App extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
src/lib/codec-wrappers/codec.ts
Normal file
7
src/lib/codec-wrappers/codec.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Encoder {
|
||||
encode(data: ImageData): Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
export interface Decoder {
|
||||
decode(data: ArrayBuffer): Promise<ImageBitmap>;
|
||||
}
|
||||
76
src/lib/codec-wrappers/mozjpeg-enc.ts
Normal file
76
src/lib/codec-wrappers/mozjpeg-enc.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user