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==",
|
"integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==",
|
||||||
"dev": true
|
"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": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
@@ -4529,6 +4535,24 @@
|
|||||||
"homedir-polyfill": "^1.0.1"
|
"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": {
|
"express": {
|
||||||
"version": "4.16.2",
|
"version": "4.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz",
|
||||||
@@ -4712,6 +4736,16 @@
|
|||||||
"object-assign": "^4.0.1"
|
"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": {
|
"filename-regex": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "apache-2.0",
|
"license": "apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:mozjpeg_enc": "cd codecs/mozjpeg_enc && npm run build",
|
||||||
|
"build:codecs": "npm run build:mozjpeg_enc",
|
||||||
"start": "webpack serve --hot",
|
"start": "webpack serve --hot",
|
||||||
"build": "webpack -p",
|
"build": "webpack -p",
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
@@ -30,6 +32,7 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^9.4.7",
|
"@types/node": "^9.4.7",
|
||||||
|
"@types/webassembly-js-api": "0.0.1",
|
||||||
"babel-loader": "^7.1.4",
|
"babel-loader": "^7.1.4",
|
||||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
@@ -52,6 +55,8 @@
|
|||||||
"eslint-plugin-promise": "^3.7.0",
|
"eslint-plugin-promise": "^3.7.0",
|
||||||
"eslint-plugin-react": "^7.7.0",
|
"eslint-plugin-react": "^7.7.0",
|
||||||
"eslint-plugin-standard": "^3.0.1",
|
"eslint-plugin-standard": "^3.0.1",
|
||||||
|
"exports-loader": "^0.7.0",
|
||||||
|
"file-loader": "^1.1.11",
|
||||||
"html-webpack-plugin": "^3.0.6",
|
"html-webpack-plugin": "^3.0.6",
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"loader-utils": "^1.1.0",
|
"loader-utils": "^1.1.0",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { bind } from '../../lib/util';
|
import { bind, bitmapToImageData } from '../../lib/util';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import Output from '../output';
|
import Output from '../output';
|
||||||
|
|
||||||
|
import {MozJpegEncoder} from '../../lib/codec-wrappers/mozjpeg-enc';
|
||||||
|
|
||||||
type Props = {};
|
type Props = {};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -28,8 +30,13 @@ export default class App extends Component<Props, State> {
|
|||||||
const fileInput = event.target as HTMLInputElement;
|
const fileInput = event.target as HTMLInputElement;
|
||||||
if (!fileInput.files || !fileInput.files[0]) return;
|
if (!fileInput.files || !fileInput.files[0]) return;
|
||||||
// TODO: handle decode error
|
// TODO: handle decode error
|
||||||
const img = await createImageBitmap(fileInput.files[0]);
|
const bitmap = await createImageBitmap(fileInput.files[0]);
|
||||||
this.setState({ img });
|
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) {
|
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',
|
loader: 'babel-loader',
|
||||||
// Don't respect any Babel RC files found on the filesystem:
|
// Don't respect any Babel RC files found on the filesystem:
|
||||||
options: Object.assign(readJson('.babelrc'), { babelrc: false })
|
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: [
|
plugins: [
|
||||||
|
new webpack.IgnorePlugin(/(fs)/, /\/codecs\//),
|
||||||
// Pretty progressbar showing build progress:
|
// Pretty progressbar showing build progress:
|
||||||
new ProgressBarPlugin({
|
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',
|
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