forked from external-repos/squoosh
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 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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user