Mozjpeg opts (#140)

* Switching to embind

* Adding options to mozjpeg wasm

* Updating packages

* Ditching enum - causing more problems than it's worth

* Adding mozjpeg options UI

* Forgot about this enum

* Bools just work
This commit is contained in:
Jake Archibald
2018-08-17 16:25:28 +01:00
committed by GitHub
parent bff515b63f
commit 1ae65dd4a1
13 changed files with 426 additions and 392 deletions

View File

@@ -1,22 +1,10 @@
import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
// Using require() so TypeScript doesnt complain about this not being a module.
import { EncodeOptions } from './encoder';
const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm');
// API exposed by wasm module. Details in the codecs 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 default class MozJpegEncoder {
private emscriptenModule: Promise<EmscriptenWasm.Module>;
private api: Promise<ModuleAPI>;
private emscriptenModule: Promise<MozJPEGModule>;
constructor() {
this.emscriptenModule = new Promise((resolve) => {
@@ -41,37 +29,20 @@ export default class MozJpegEncoder {
},
});
});
this.api = (async () => {
// Not sure why, but TypeScript complains that I am using
// `emscriptenModule` before its 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, options: EncodeOptions): Promise<ArrayBuffer> {
const m = await this.emscriptenModule;
const api = await this.api;
const module = await this.emscriptenModule;
const p = api.create_buffer(data.width, data.height);
m.HEAP8.set(data.data, p);
api.encode(p, data.width, data.height, options.quality);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize);
const p = module.create_buffer(data.width, data.height);
module.HEAP8.set(data.data, p);
module.encode(p, data.width, data.height, options);
const resultPointer = module.get_result_pointer();
const resultSize = module.get_result_size();
const resultView = new Uint8Array(module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result();
api.destroy_buffer(p);
module.free_result();
module.destroy_buffer(p);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;

View File

@@ -1,13 +1,46 @@
import EncoderWorker from './Encoder.worker';
export interface EncodeOptions { quality: number; }
export enum MozJpegColorSpace {
GRAYSCALE = 1,
RGB,
YCbCr,
}
export interface EncodeOptions {
quality: number;
baseline: boolean;
arithmetic: boolean;
progressive: boolean;
optimize_coding: boolean;
smoothing: number;
color_space: MozJpegColorSpace;
quant_table: number;
trellis_multipass: boolean;
trellis_opt_zero: boolean;
trellis_opt_table: boolean;
trellis_loops: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'mozjpeg';
export const label = 'MozJPEG';
export const mimeType = 'image/jpeg';
export const extension = 'jpg';
export const defaultOptions: EncodeOptions = { quality: 7 };
export const defaultOptions: EncodeOptions = {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: MozJpegColorSpace.YCbCr,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
};
export async function encode(data: ImageData, options: EncodeOptions) {
// We need to await this because it's been comlinked.

View File

@@ -1,3 +1,161 @@
import qualityOption from '../generic/quality-option';
import { h, Component } from 'preact';
import { bind, inputFieldChecked, inputFieldValueAsNumber } from '../../lib/util';
import { EncodeOptions, MozJpegColorSpace } from './encoder';
export default qualityOption();
type Props = {
options: EncodeOptions,
onChange(newOptions: EncodeOptions): void,
};
export default class MozJPEGEncoderOptions extends Component<Props, {}> {
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const options: EncodeOptions = {
// Copy over options the form doesn't currently care about, eg arithmetic
...this.props.options,
// And now stuff from the form:
// .checked
baseline: inputFieldChecked(form.baseline),
progressive: inputFieldChecked(form.progressive),
optimize_coding: inputFieldChecked(form.optimize_coding),
trellis_multipass: inputFieldChecked(form.trellis_multipass),
trellis_opt_zero: inputFieldChecked(form.trellis_opt_zero),
trellis_opt_table: inputFieldChecked(form.trellis_opt_table),
// .value
quality: inputFieldValueAsNumber(form.quality),
smoothing: inputFieldValueAsNumber(form.smoothing),
color_space: inputFieldValueAsNumber(form.color_space),
quant_table: inputFieldValueAsNumber(form.quant_table),
trellis_loops: inputFieldValueAsNumber(form.trellis_loops),
};
this.props.onChange(options);
}
render({ options }: Props) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
// gathering the data.
return (
<form>
<label>
Quality:
<input
name="quality"
type="range"
min="0"
max="100"
value={'' + options.quality}
onChange={this.onChange}
/>
</label>
<label>
<input
name="baseline"
type="checkbox"
checked={options.baseline}
onChange={this.onChange}
/>
Baseline (worse but legacy-compatible)
</label>
<label style={{ display: options.baseline ? 'none' : '' }}>
<input
name="progressive"
type="checkbox"
checked={options.progressive}
onChange={this.onChange}
/>
Progressive multi-pass rendering
</label>
<label style={{ display: options.baseline ? '' : 'none' }}>
<input
name="optimize_coding"
type="checkbox"
checked={options.optimize_coding}
onChange={this.onChange}
/>
Optimize Huffman table
</label>
<label>
Smoothing:
<input
name="smoothing"
type="range"
min="0"
max="100"
value={'' + options.smoothing}
onChange={this.onChange}
/>
</label>
<label>
Output color space:
<select
name="color_space"
value={'' + options.color_space}
onChange={this.onChange}
>
<option value={MozJpegColorSpace.GRAYSCALE}>Grayscale</option>
<option value={MozJpegColorSpace.RGB}>RGB (sub-optimal)</option>
<option value={MozJpegColorSpace.YCbCr}>YCbCr (optimized for color)</option>
</select>
</label>
<label>
Quantization table:
<select
name="quant_table"
value={'' + options.quant_table}
onChange={this.onChange}
>
<option value="0">JPEG Annex K</option>
<option value="1">Flat</option>
<option value="2">MSSIM-tuned Kodak</option>
<option value="3">ImageMagick</option>
<option value="4">PSNR-HVS-M-tuned Kodak</option>
<option value="5">Klein et al</option>
<option value="6">Watson et al</option>
<option value="7">Ahumada et al</option>
<option value="8">Peterson et al</option>
</select>
</label>
<label>
<input
name="trellis_multipass"
type="checkbox"
checked={options.trellis_multipass}
onChange={this.onChange}
/>
Consider multiple scans during trellis quantization
</label>
<label style={{ display: options.trellis_multipass ? '' : 'none' }}>
<input
name="trellis_opt_zero"
type="checkbox"
checked={options.trellis_opt_zero}
onChange={this.onChange}
/>
Optimize runs of zero blocks
</label>
<label>
<input
name="trellis_opt_table"
type="checkbox"
checked={options.trellis_opt_table}
onChange={this.onChange}
/>
Optimize after trellis quantization
</label>
<label>
Trellis quantization passes:
<input
name="trellis_loops"
type="range"
min="1"
max="50"
value={'' + options.trellis_loops}
onChange={this.onChange}
/>
</label>
</form>
);
}
}

View File

@@ -157,7 +157,14 @@ export function inputFieldValueAsNumber(field: any): number {
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
*/
export function inputFieldCheckedAsNumber(field: any): number {
return Number((field as HTMLInputElement).checked);
return Number(inputFieldChecked(field));
}
/**
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
*/
export function inputFieldChecked(field: any): boolean {
return (field as HTMLInputElement).checked;
}
/**