Hook up options UI to quantizer
This commit is contained in:
@ -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,
|
||||
|
@ -31,7 +31,7 @@ export default class QuantizerOptions extends Component<Props, {}> {
|
||||
return (
|
||||
<form>
|
||||
<label>
|
||||
Pallette Color:
|
||||
Pallette Colors:
|
||||
<input
|
||||
name="maxNumColors"
|
||||
type="range"
|
||||
|
@ -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,
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user