Hook up options UI to quantizer

This commit is contained in:
Surma
2018-07-31 12:32:37 +01:00
parent 2165383da4
commit a002b376af
5 changed files with 121 additions and 20 deletions

View File

@@ -10,6 +10,8 @@ import * as browserJP2 from './browser-jp2/encoder';
import * as browserBMP from './browser-bmp/encoder'; import * as browserBMP from './browser-bmp/encoder';
import * as browserPDF from './browser-pdf/encoder'; import * as browserPDF from './browser-pdf/encoder';
import * as quantizer from './imagequant/quantizer';
export interface EncoderSupportMap { export interface EncoderSupportMap {
[key: string]: boolean; [key: string]: boolean;
} }
@@ -26,6 +28,13 @@ export type EncoderOptions =
browserPDF.EncodeOptions; browserPDF.EncodeOptions;
export type EncoderType = keyof typeof encoderMap; export type EncoderType = keyof typeof encoderMap;
export interface Enableable {
enabled: boolean;
}
export interface PreprocessorState {
quantizer: Enableable & quantizer.QuantizeOptions;
}
export const encoderMap = { export const encoderMap = {
[identity.type]: identity, [identity.type]: identity,
[mozJPEG.type]: mozJPEG, [mozJPEG.type]: mozJPEG,

View File

@@ -31,7 +31,7 @@ export default class QuantizerOptions extends Component<Props, {}> {
return ( return (
<form> <form>
<label> <label>
Pallette Color: Pallette Colors:
<input <input
name="maxNumColors" name="maxNumColors"
type="range" type="range"

View File

@@ -11,7 +11,6 @@ export interface QuantizeOptions {
dither: number; dither: number;
} }
// These come from struct WebPConfig in encode.h.
export const defaultOptions: QuantizeOptions = { export const defaultOptions: QuantizeOptions = {
maxNumColors: 256, maxNumColors: 256,
dither: 1.0, dither: 1.0,

View File

@@ -25,6 +25,7 @@ import {
EncoderType, EncoderType,
EncoderOptions, EncoderOptions,
encoderMap, encoderMap,
PreprocessorState,
} from '../../codecs/encoders'; } from '../../codecs/encoders';
import { decodeImage } from '../../codecs/decoders'; import { decodeImage } from '../../codecs/decoders';
@@ -33,12 +34,14 @@ interface SourceImage {
file: File; file: File;
bmp: ImageBitmap; bmp: ImageBitmap;
data: ImageData; data: ImageData;
preprocessed?: ImageData;
} }
interface EncodedImage { interface EncodedImage {
bmp?: ImageBitmap; bmp?: ImageBitmap;
file?: File; file?: File;
downloadUrl?: string; downloadUrl?: string;
preprocessorState: PreprocessorState;
encoderState: EncoderState; encoderState: EncoderState;
loading: boolean; loading: boolean;
/** Counter of the latest bmp currently encoding */ /** Counter of the latest bmp currently encoding */
@@ -58,17 +61,26 @@ interface State {
const filesize = partial({}); const filesize = partial({});
async function preprocessImage(
source: SourceImage,
preprocessData: PreprocessorState,
): Promise<ImageData> {
let result = source.data;
if (preprocessData.quantizer.enabled) {
result = await quantizer.quantize(result, preprocessData.quantizer);
}
return result;
}
async function compressImage( async function compressImage(
source: SourceImage, source: SourceImage,
quantizerOptions: quantizer.QuantizeOptions | null,
encodeData: EncoderState, encodeData: EncoderState,
): Promise<File> { ): Promise<File> {
// Special case for identity // Special case for identity
if (encodeData.type === identity.type) return source.file; if (encodeData.type === identity.type) return source.file;
let sourceData = source.data; let sourceData = source.data;
if (quantizerOptions) { if (source.preprocessed) {
sourceData = await quantizer.quantize(sourceData, quantizerOptions); sourceData = source.preprocessed;
} }
const compressedData = await (() => { const compressedData = await (() => {
switch (encodeData.type) { switch (encodeData.type) {
@@ -100,12 +112,24 @@ export default class App extends Component<Props, State> {
loading: false, loading: false,
images: [ images: [
{ {
preprocessorState: {
quantizer: {
enabled: false,
...quantizer.defaultOptions,
},
},
encoderState: { type: identity.type, options: identity.defaultOptions }, encoderState: { type: identity.type, options: identity.defaultOptions },
loadingCounter: 0, loadingCounter: 0,
loadedCounter: 0, loadedCounter: 0,
loading: false, loading: false,
}, },
{ {
preprocessorState: {
quantizer: {
enabled: false,
...quantizer.defaultOptions,
},
},
encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions }, encoderState: { type: mozJPEG.type, options: mozJPEG.defaultOptions },
loadingCounter: 0, loadingCounter: 0,
loadedCounter: 0, loadedCounter: 0,
@@ -127,7 +151,12 @@ export default class App extends Component<Props, State> {
} }
} }
onEncoderChange(index: 0 | 1, type: EncoderType, options?: EncoderOptions): void { onChange(
index: 0 | 1,
preprocessorState: PreprocessorState,
type: EncoderType,
options?: EncoderOptions,
): void {
const images = this.state.images.slice() as [EncodedImage, EncodedImage]; const images = this.state.images.slice() as [EncodedImage, EncodedImage];
const oldImage = images[index]; const oldImage = images[index];
@@ -142,13 +171,32 @@ export default class App extends Component<Props, State> {
images[index] = { images[index] = {
...oldImage, ...oldImage,
encoderState, encoderState,
preprocessorState,
}; };
this.setState({ images }); this.setState({ images });
} }
onOptionsChange(index: 0 | 1, options: EncoderOptions): void { onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
this.onEncoderChange(index, this.state.images[index].encoderState.type, options); this.onChange(index, this.state.images[index].preprocessorState, newType);
}
onPreprocessorOptionsChange(index: 0 | 1, options: PreprocessorState): void {
this.onChange(
index,
options,
this.state.images[index].encoderState.type,
this.state.images[index].encoderState.options,
);
}
onEncoderOptionsChange(index: 0 | 1, options: EncoderOptions): void {
this.onChange(
index,
this.state.images[index].preprocessorState,
this.state.images[index].encoderState.type,
options,
);
} }
componentDidUpdate(prevProps: Props, prevState: State): void { componentDidUpdate(prevProps: Props, prevState: State): void {
@@ -218,10 +266,9 @@ export default class App extends Component<Props, State> {
this.setState({ images }); this.setState({ images });
let file; let file;
try { try {
// FIXME (@surma): Somehow show a options window and get the values from there. source.preprocessed = await preprocessImage(source, image.preprocessorState);
file = await compressImage(source, { maxNumColors: 8, dither: 0.5 }, image.encoderState); file = await compressImage(source, image.encoderState);
} catch (err) { } catch (err) {
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` }); this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err; throw err;
@@ -283,9 +330,11 @@ export default class App extends Component<Props, State> {
{images.map((image, index) => ( {images.map((image, index) => (
<Options <Options
class={index ? style.rightOptions : style.leftOptions} class={index ? style.rightOptions : style.leftOptions}
preprocessorState={image.preprocessorState}
encoderState={image.encoderState} encoderState={image.encoderState}
onTypeChange={this.onEncoderChange.bind(this, index)} onEncoderTypeChange={this.onEncoderTypeChange.bind(this, index)}
onOptionsChange={this.onOptionsChange.bind(this, index)} onEncoderOptionsChange={this.onEncoderOptionsChange.bind(this, index)}
onPreprocessorOptionsChange={this.onPreprocessorOptionsChange.bind(this, index)}
/> />
))} ))}
{anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>} {anyLoading && <span style={{ position: 'fixed', top: 0, left: 0 }}>Loading...</span>}

View File

@@ -6,6 +6,8 @@ import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
import WebPEncoderOptions from '../../codecs/webp/options'; import WebPEncoderOptions from '../../codecs/webp/options';
import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options'; import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
import QuantizerOptions from '../../codecs/imagequant/options';
import * as identity from '../../codecs/identity/encoder'; import * as identity from '../../codecs/identity/encoder';
import * as mozJPEG from '../../codecs/mozjpeg/encoder'; import * as mozJPEG from '../../codecs/mozjpeg/encoder';
import * as webP from '../../codecs/webp/encoder'; import * as webP from '../../codecs/webp/encoder';
@@ -24,6 +26,7 @@ import {
encoders, encoders,
encodersSupported, encodersSupported,
EncoderSupportMap, EncoderSupportMap,
PreprocessorState,
} from '../../codecs/encoders'; } from '../../codecs/encoders';
const encoderOptionsComponentMap = { const encoderOptionsComponentMap = {
@@ -44,8 +47,10 @@ const encoderOptionsComponentMap = {
interface Props { interface Props {
class?: string; class?: string;
encoderState: EncoderState; encoderState: EncoderState;
onTypeChange(newType: EncoderType): void; preprocessorState: PreprocessorState;
onOptionsChange(newOptions: EncoderOptions): void; onEncoderTypeChange(newType: EncoderType): void;
onEncoderOptionsChange(newOptions: EncoderOptions): void;
onPreprocessorOptionsChange(newOptions: PreprocessorState): void;
} }
interface State { interface State {
@@ -61,25 +66,64 @@ export default class Options extends Component<Props, State> {
} }
@bind @bind
onTypeChange(event: Event) { onEncoderTypeChange(event: Event) {
const el = event.currentTarget as HTMLSelectElement; const el = event.currentTarget as HTMLSelectElement;
// The select element only has values matching encoder types, // The select element only has values matching encoder types,
// so 'as' is safe here. // so 'as' is safe here.
const type = el.value as EncoderType; const type = el.value as EncoderType;
this.props.onTypeChange(type); this.props.onEncoderTypeChange(type);
} }
render({ class: className, encoderState, onOptionsChange }: Props, { encoderSupportMap }: State) { @bind
onPreprocessorEnabledChange(event: Event) {
const el = event.currentTarget as HTMLInputElement;
const preprocessorState = this.props.preprocessorState;
const preprocessor = el.name.split('.')[0] as keyof typeof preprocessorState;
preprocessorState[preprocessor].enabled = el.checked;
this.props.onPreprocessorOptionsChange(preprocessorState);
}
render(
{ class: className, encoderState, preprocessorState, onEncoderOptionsChange }: Props,
{ encoderSupportMap }: State,
) {
// tslint:disable variable-name // tslint:disable variable-name
const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type]; const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type];
return ( return (
<div class={`${style.options}${className ? (' ' + className) : ''}`}> <div class={`${style.options}${className ? (' ' + className) : ''}`}>
<p>Quantization</p>
<label>
<input
name="quantizer.enable"
type="checkbox"
checked={!!preprocessorState.quantizer.enabled}
onChange={this.onPreprocessorEnabledChange}
/>
Enable
</label>
{preprocessorState.quantizer.enabled ? (
<QuantizerOptions
options={preprocessorState.quantizer}
// tslint:disable-next-line:jsx-no-lambda
onChange={quantizeOpts => this.props.onPreprocessorOptionsChange({
...preprocessorState,
quantizer: {
...quantizeOpts,
enabled: preprocessorState.quantizer.enabled,
},
})}
/>
) : (
<div/>
)}
<hr/>
<label> <label>
Mode: Mode:
{encoderSupportMap ? {encoderSupportMap ?
<select value={encoderState.type} onChange={this.onTypeChange}> <select value={encoderState.type} onChange={this.onEncoderTypeChange}>
{encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => ( {encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => (
<option value={encoder.type}>{encoder.label}</option> <option value={encoder.type}>{encoder.label}</option>
))} ))}
@@ -95,7 +139,7 @@ export default class Options extends Component<Props, State> {
// type, but typescript isn't smart enough. // type, but typescript isn't smart enough.
encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options'] encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options']
} }
onChange={onOptionsChange} onChange={onEncoderOptionsChange}
/> />
} }
</div> </div>