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 browserPDF from './browser-pdf/encoder';
import * as quantizer from './imagequant/quantizer';
export interface EncoderSupportMap {
[key: string]: boolean;
}
@ -26,6 +28,13 @@ export type EncoderOptions =
browserPDF.EncodeOptions;
export type EncoderType = keyof typeof encoderMap;
export interface Enableable {
enabled: boolean;
}
export interface PreprocessorState {
quantizer: Enableable & quantizer.QuantizeOptions;
}
export const encoderMap = {
[identity.type]: identity,
[mozJPEG.type]: mozJPEG,

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
import WebPEncoderOptions from '../../codecs/webp/options';
import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
import QuantizerOptions from '../../codecs/imagequant/options';
import * as identity from '../../codecs/identity/encoder';
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
import * as webP from '../../codecs/webp/encoder';
@ -24,6 +26,7 @@ import {
encoders,
encodersSupported,
EncoderSupportMap,
PreprocessorState,
} from '../../codecs/encoders';
const encoderOptionsComponentMap = {
@ -44,8 +47,10 @@ const encoderOptionsComponentMap = {
interface Props {
class?: string;
encoderState: EncoderState;
onTypeChange(newType: EncoderType): void;
onOptionsChange(newOptions: EncoderOptions): void;
preprocessorState: PreprocessorState;
onEncoderTypeChange(newType: EncoderType): void;
onEncoderOptionsChange(newOptions: EncoderOptions): void;
onPreprocessorOptionsChange(newOptions: PreprocessorState): void;
}
interface State {
@ -61,25 +66,64 @@ export default class Options extends Component<Props, State> {
}
@bind
onTypeChange(event: Event) {
onEncoderTypeChange(event: Event) {
const el = event.currentTarget as HTMLSelectElement;
// The select element only has values matching encoder types,
// so 'as' is safe here.
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
const EncoderOptionComponent = encoderOptionsComponentMap[encoderState.type];
return (
<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>
Mode:
{encoderSupportMap ?
<select value={encoderState.type} onChange={this.onTypeChange}>
<select value={encoderState.type} onChange={this.onEncoderTypeChange}>
{encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => (
<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.
encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options']
}
onChange={onOptionsChange}
onChange={onEncoderOptionsChange}
/>
}
</div>