Start integrating quantizer in the main data flow

This commit is contained in:
Surma
2018-07-30 16:51:40 +01:00
parent 5fbf6b297f
commit 2165383da4
5 changed files with 85 additions and 346 deletions

View File

@@ -1,3 +1,4 @@
import { QuantizeOptions } from './quantizer';
import imagequant from '../../../codecs/imagequant/imagequant';
// Using require() so TypeScript doesnt complain about this not being a module.
const wasmBinaryUrl = require('../../../codecs/imagequant/imagequant.wasm');
@@ -56,13 +57,13 @@ export default class ImageQuant {
})();
}
async quantize(data: ImageData): Promise<ImageData> {
async quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
const m = await this.emscriptenModule;
const api = await this.api;
const p = api.create_buffer(data.width, data.height);
m.HEAP8.set(new Uint8Array(data.data), p);
api.quantize(p, data.width, data.height, 256, 1.0);
api.quantize(p, data.width, data.height, opts.maxNumColors, opts.dither);
const resultPointer = api.get_result_pointer();
const resultView = new Uint8Array(
m.HEAP8.buffer,

View File

@@ -1,330 +1,58 @@
// import { h, Component } from 'preact';
// import { bind } from '../../lib/util';
import { h, Component } from 'preact';
import { bind } from '../../lib/util';
// import * as styles from './styles.scss';
import { QuantizeOptions } from './quantizer';
// type Props = {
// options: EncodeOptions,
// onChange(newOptions: EncodeOptions): void,
// };
type Props = {
options: QuantizeOptions,
onChange(newOptions: QuantizeOptions): void,
};
// // From kLosslessPresets in config_enc.c
// // The format is [method, quality].
// const losslessPresets:[number, number][] = [
// [0, 0], [1, 20], [2, 25], [3, 30], [3, 50],
// [4, 50], [4, 75], [4, 90], [5, 90], [6, 100],
// ];
// const losslessPresetDefault = 6;
/**
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
*/
function fieldValueAsNumber(field: any): number {
return Number((field as HTMLInputElement).value);
}
// function determineLosslessQuality(quality: number): number {
// const index = losslessPresets.findIndex(item => item[1] === quality);
// if (index !== -1) return index;
// // Quality doesn't match one of the presets.
// // This can happen when toggling 'lossless'.
// return losslessPresetDefault;
// }
export default class QuantizerOptions extends Component<Props, {}> {
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
// /**
// * @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
// */
// function fieldCheckedAsNumber(field: any): number {
// return Number((field as HTMLInputElement).checked);
// }
const options: QuantizeOptions = {
maxNumColors: fieldValueAsNumber(form.maxNumColors),
dither: fieldValueAsNumber(form.dither),
};
this.props.onChange(options);
}
// /**
// * @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
// */
// function fieldValueAsNumber(field: any): number {
// return Number((field as HTMLInputElement).value);
// }
// export default class WebPEncoderOptions extends Component<Props, {}> {
// @bind
// onChange(event: Event) {
// const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
// const lossless = fieldCheckedAsNumber(form.lossless);
// const losslessPresetInput = (form.lossless_preset as HTMLInputElement);
// const options: EncodeOptions = {
// // Copy over options the form doesn't care about, eg emulate_jpeg_size
// ...this.props.options,
// // And now stuff from the form:
// lossless,
// // Special-cased inputs:
// // In lossless mode, the quality is derived from the preset.
// quality: lossless ?
// losslessPresets[Number(losslessPresetInput.value)][1] :
// fieldValueAsNumber(form.quality),
// // In lossless mode, the method is derived from the preset.
// method: lossless ?
// losslessPresets[Number(losslessPresetInput.value)][0] :
// fieldValueAsNumber(form.method_input),
// image_hint: (form.image_hint as HTMLInputElement).checked ?
// WebPImageHint.WEBP_HINT_GRAPH :
// WebPImageHint.WEBP_HINT_DEFAULT,
// // .checked
// exact: fieldCheckedAsNumber(form.exact),
// alpha_compression: fieldCheckedAsNumber(form.alpha_compression),
// autofilter: fieldCheckedAsNumber(form.autofilter),
// filter_type: fieldCheckedAsNumber(form.filter_type),
// use_sharp_yuv: fieldCheckedAsNumber(form.use_sharp_yuv),
// // .value
// near_lossless: fieldValueAsNumber(form.near_lossless),
// alpha_quality: fieldValueAsNumber(form.alpha_quality),
// alpha_filtering: fieldValueAsNumber(form.alpha_filtering),
// sns_strength: fieldValueAsNumber(form.sns_strength),
// filter_strength: fieldValueAsNumber(form.filter_strength),
// filter_sharpness: fieldValueAsNumber(form.filter_sharpness),
// pass: fieldValueAsNumber(form.pass),
// preprocessing: fieldValueAsNumber(form.preprocessing),
// segments: fieldValueAsNumber(form.segments),
// partitions: fieldValueAsNumber(form.partitions),
// };
// this.props.onChange(options);
// }
// private _losslessSpecificOptions(options: EncodeOptions) {
// return (
// <div>
// <label>
// Effort:
// <input
// name="lossless_preset"
// type="range"
// min="0"
// max="9"
// value={'' + determineLosslessQuality(options.quality)}
// onChange={this.onChange}
// />
// </label>
// <label>
// Slight loss:
// <input
// class={styles.flipRange}
// name="near_lossless"
// type="range"
// min="0"
// max="100"
// value={'' + options.near_lossless}
// onChange={this.onChange}
// />
// </label>
// <label>
// {/*
// Although there are 3 different kinds of image hint, webp only
// seems to do something with the 'graph' type, and I don't really
// understand what it does.
// */}
// <input
// name="image_hint"
// type="checkbox"
// checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
// value={'' + WebPImageHint.WEBP_HINT_GRAPH}
// onChange={this.onChange}
// />
// Discrete tone image (graph, map-tile etc)
// </label>
// </div>
// );
// }
// private _lossySpecificOptions(options: EncodeOptions) {
// return (
// <div>
// <label>
// Effort:
// <input
// name="method_input"
// type="range"
// min="0"
// max="6"
// value={'' + options.method}
// onChange={this.onChange}
// />
// </label>
// <label>
// Quality:
// <input
// name="quality"
// type="range"
// min="0"
// max="100"
// step="0.01"
// value={'' + options.quality}
// onChange={this.onChange}
// />
// </label>
// <label>
// <input
// name="alpha_compression"
// type="checkbox"
// checked={!!options.alpha_compression}
// onChange={this.onChange}
// />
// Compress alpha
// </label>
// <label>
// Alpha quality:
// <input
// name="alpha_quality"
// type="range"
// min="0"
// max="100"
// value={'' + options.alpha_quality}
// onChange={this.onChange}
// />
// </label>
// <label>
// Alpha filter quality:
// <input
// name="alpha_filtering"
// type="range"
// min="0"
// max="2"
// value={'' + options.alpha_filtering}
// onChange={this.onChange}
// />
// </label>
// <label>
// Spacial noise shaping:
// <input
// name="sns_strength"
// type="range"
// min="0"
// max="100"
// value={'' + options.sns_strength}
// onChange={this.onChange}
// />
// </label>
// <label>
// <input
// name="autofilter"
// type="checkbox"
// checked={!!options.autofilter}
// onChange={this.onChange}
// />
// Auto adjust filter strength
// </label>
// <label>
// Filter strength:
// <input
// name="filter_strength"
// type="range"
// min="0"
// max="100"
// disabled={!!options.autofilter}
// value={'' + options.filter_strength}
// onChange={this.onChange}
// />
// </label>
// <label>
// <input
// name="filter_type"
// type="checkbox"
// checked={!!options.filter_type}
// onChange={this.onChange}
// />
// Strong filter
// </label>
// <label>
// Filter sharpness:
// <input
// class={styles.flipRange}
// name="filter_sharpness"
// type="range"
// min="0"
// max="7"
// value={'' + options.filter_sharpness}
// onChange={this.onChange}
// />
// </label>
// <label>
// <input
// name="use_sharp_yuv"
// type="checkbox"
// checked={!!options.use_sharp_yuv}
// onChange={this.onChange}
// />
// Sharp RGB->YUV conversion
// </label>
// <label>
// Passes:
// <input
// name="pass"
// type="range"
// min="1"
// max="10"
// value={'' + options.pass}
// onChange={this.onChange}
// />
// </label>
// <label>
// Preprocessing type:
// <select
// name="preprocessing"
// value={'' + options.preprocessing}
// onChange={this.onChange}
// >
// <option value="0">None</option>
// <option value="1">Segment smooth</option>
// <option value="2">Pseudo-random dithering</option>
// </select>
// </label>
// <label>
// Segments:
// <input
// name="segments"
// type="range"
// min="1"
// max="4"
// value={'' + options.segments}
// onChange={this.onChange}
// />
// </label>
// <label>
// Partitions:
// <input
// name="partitions"
// type="range"
// min="0"
// max="3"
// value={'' + options.partitions}
// onChange={this.onChange}
// />
// </label>
// </div>
// );
// }
// render({ options }: Props) {
// // I'm rendering both lossy and lossless forms, as it becomes much easier when
// // gathering the data.
// return (
// <form>
// <label>
// <input
// name="lossless"
// type="checkbox"
// checked={!!options.lossless}
// onChange={this.onChange}
// />
// Lossless
// </label>
// <div class={options.lossless ? '' : styles.hide}>
// {this._losslessSpecificOptions(options)}
// </div>
// <div class={options.lossless ? styles.hide : ''}>
// {this._lossySpecificOptions(options)}
// </div>
// <label>
// <input
// name="exact"
// type="checkbox"
// checked={!!options.exact}
// onChange={this.onChange}
// />
// Preserve transparent data. Otherwise, pixels with zero alpha will have RGB also zeroed.
// </label>
// </form>
// );
// }
// }
render({ options }: Props) {
return (
<form>
<label>
Pallette Color:
<input
name="maxNumColors"
type="range"
min="2"
max="256"
value={'' + options.maxNumColors}
onChange={this.onChange}
/>
</label>
<label>
Dithering:
<input
name="dither"
type="range"
min="0"
max="1"
value={'' + options.dither}
onChange={this.onChange}
/>
</label>
</form>
);
}
}

View File

@@ -1,7 +1,18 @@
import QuantizerWorker from './Quantizer.worker';
export const name = 'Image Quanitzer';
export async function quantize(data: ImageData): Promise<ImageData> {
export async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
const quantizer = await new QuantizerWorker();
return quantizer.quantize(data);
return quantizer.quantize(data, opts);
}
export interface QuantizeOptions {
maxNumColors: number;
dither: number;
}
// These come from struct WebPConfig in encode.h.
export const defaultOptions: QuantizeOptions = {
maxNumColors: 256,
dither: 1.0,
};

View File

@@ -1,6 +0,0 @@
.flip-range {
transform: scaleX(-1);
}
.hide {
display: none;
}

View File

@@ -60,24 +60,28 @@ const filesize = partial({});
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;
const quantizedSource = await quantizer.quantize(source.data);
let sourceData = source.data;
if (quantizerOptions) {
sourceData = await quantizer.quantize(sourceData, quantizerOptions);
}
const compressedData = await (() => {
switch (encodeData.type) {
case mozJPEG.type: return mozJPEG.encode(quantizedSource, encodeData.options);
case webP.type: return webP.encode(quantizedSource, encodeData.options);
case browserPNG.type: return browserPNG.encode(quantizedSource, encodeData.options);
case browserJPEG.type: return browserJPEG.encode(quantizedSource, encodeData.options);
case browserWebP.type: return browserWebP.encode(quantizedSource, encodeData.options);
case browserGIF.type: return browserGIF.encode(quantizedSource, encodeData.options);
case browserTIFF.type: return browserTIFF.encode(quantizedSource, encodeData.options);
case browserJP2.type: return browserJP2.encode(quantizedSource, encodeData.options);
case browserBMP.type: return browserBMP.encode(quantizedSource, encodeData.options);
case browserPDF.type: return browserPDF.encode(quantizedSource, encodeData.options);
case mozJPEG.type: return mozJPEG.encode(sourceData, encodeData.options);
case webP.type: return webP.encode(sourceData, encodeData.options);
case browserPNG.type: return browserPNG.encode(sourceData, encodeData.options);
case browserJPEG.type: return browserJPEG.encode(sourceData, encodeData.options);
case browserWebP.type: return browserWebP.encode(sourceData, encodeData.options);
case browserGIF.type: return browserGIF.encode(sourceData, encodeData.options);
case browserTIFF.type: return browserTIFF.encode(sourceData, encodeData.options);
case browserJP2.type: return browserJP2.encode(sourceData, encodeData.options);
case browserBMP.type: return browserBMP.encode(sourceData, encodeData.options);
case browserPDF.type: return browserPDF.encode(sourceData, encodeData.options);
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
}
})();
@@ -216,7 +220,8 @@ export default class App extends Component<Props, State> {
let file;
try {
file = await compressImage(source, image.encoderState);
// FIXME (@surma): Somehow show a options window and get the values from there.
file = await compressImage(source, { maxNumColors: 8, dither: 0.5 }, image.encoderState);
} catch (err) {
this.setState({ error: `Encoding error (type=${image.encoderState.type}): ${err}` });
throw err;