mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 01:37:26 +00:00
Start integrating quantizer in the main data flow
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { QuantizeOptions } from './quantizer';
|
||||||
import imagequant from '../../../codecs/imagequant/imagequant';
|
import imagequant from '../../../codecs/imagequant/imagequant';
|
||||||
// Using require() so TypeScript doesn’t complain about this not being a module.
|
// Using require() so TypeScript doesn’t complain about this not being a module.
|
||||||
const wasmBinaryUrl = require('../../../codecs/imagequant/imagequant.wasm');
|
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 m = await this.emscriptenModule;
|
||||||
const api = await this.api;
|
const api = await this.api;
|
||||||
|
|
||||||
const p = api.create_buffer(data.width, data.height);
|
const p = api.create_buffer(data.width, data.height);
|
||||||
m.HEAP8.set(new Uint8Array(data.data), p);
|
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 resultPointer = api.get_result_pointer();
|
||||||
const resultView = new Uint8Array(
|
const resultView = new Uint8Array(
|
||||||
m.HEAP8.buffer,
|
m.HEAP8.buffer,
|
||||||
|
|||||||
@@ -1,330 +1,58 @@
|
|||||||
// import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
// import { bind } from '../../lib/util';
|
import { bind } from '../../lib/util';
|
||||||
// import * as styles from './styles.scss';
|
// import * as styles from './styles.scss';
|
||||||
|
import { QuantizeOptions } from './quantizer';
|
||||||
|
|
||||||
// type Props = {
|
type Props = {
|
||||||
// options: EncodeOptions,
|
options: QuantizeOptions,
|
||||||
// onChange(newOptions: EncodeOptions): void,
|
onChange(newOptions: QuantizeOptions): void,
|
||||||
// };
|
};
|
||||||
|
|
||||||
// // From kLosslessPresets in config_enc.c
|
/**
|
||||||
// // The format is [method, quality].
|
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||||
// const losslessPresets:[number, number][] = [
|
*/
|
||||||
// [0, 0], [1, 20], [2, 25], [3, 30], [3, 50],
|
function fieldValueAsNumber(field: any): number {
|
||||||
// [4, 50], [4, 75], [4, 90], [5, 90], [6, 100],
|
return Number((field as HTMLInputElement).value);
|
||||||
// ];
|
}
|
||||||
// const losslessPresetDefault = 6;
|
|
||||||
|
|
||||||
// function determineLosslessQuality(quality: number): number {
|
export default class QuantizerOptions extends Component<Props, {}> {
|
||||||
// const index = losslessPresets.findIndex(item => item[1] === quality);
|
@bind
|
||||||
// if (index !== -1) return index;
|
onChange(event: Event) {
|
||||||
// // Quality doesn't match one of the presets.
|
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||||
// // This can happen when toggling 'lossless'.
|
|
||||||
// return losslessPresetDefault;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
const options: QuantizeOptions = {
|
||||||
// * @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
maxNumColors: fieldValueAsNumber(form.maxNumColors),
|
||||||
// */
|
dither: fieldValueAsNumber(form.dither),
|
||||||
// function fieldCheckedAsNumber(field: any): number {
|
};
|
||||||
// return Number((field as HTMLInputElement).checked);
|
this.props.onChange(options);
|
||||||
// }
|
}
|
||||||
|
|
||||||
// /**
|
render({ options }: Props) {
|
||||||
// * @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
return (
|
||||||
// */
|
<form>
|
||||||
// function fieldValueAsNumber(field: any): number {
|
<label>
|
||||||
// return Number((field as HTMLInputElement).value);
|
Pallette Color:
|
||||||
// }
|
<input
|
||||||
|
name="maxNumColors"
|
||||||
// export default class WebPEncoderOptions extends Component<Props, {}> {
|
type="range"
|
||||||
// @bind
|
min="2"
|
||||||
// onChange(event: Event) {
|
max="256"
|
||||||
// const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
value={'' + options.maxNumColors}
|
||||||
// const lossless = fieldCheckedAsNumber(form.lossless);
|
onChange={this.onChange}
|
||||||
// const losslessPresetInput = (form.lossless_preset as HTMLInputElement);
|
/>
|
||||||
|
</label>
|
||||||
// const options: EncodeOptions = {
|
<label>
|
||||||
// // Copy over options the form doesn't care about, eg emulate_jpeg_size
|
Dithering:
|
||||||
// ...this.props.options,
|
<input
|
||||||
// // And now stuff from the form:
|
name="dither"
|
||||||
// lossless,
|
type="range"
|
||||||
// // Special-cased inputs:
|
min="0"
|
||||||
// // In lossless mode, the quality is derived from the preset.
|
max="1"
|
||||||
// quality: lossless ?
|
value={'' + options.dither}
|
||||||
// losslessPresets[Number(losslessPresetInput.value)][1] :
|
onChange={this.onChange}
|
||||||
// fieldValueAsNumber(form.quality),
|
/>
|
||||||
// // In lossless mode, the method is derived from the preset.
|
</label>
|
||||||
// method: lossless ?
|
</form>
|
||||||
// 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>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
import QuantizerWorker from './Quantizer.worker';
|
import QuantizerWorker from './Quantizer.worker';
|
||||||
|
|
||||||
export const name = 'Image Quanitzer';
|
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();
|
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,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
.flip-range {
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
.hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -60,24 +60,28 @@ const filesize = partial({});
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
const quantizedSource = await quantizer.quantize(source.data);
|
let sourceData = source.data;
|
||||||
|
if (quantizerOptions) {
|
||||||
|
sourceData = await quantizer.quantize(sourceData, quantizerOptions);
|
||||||
|
}
|
||||||
const compressedData = await (() => {
|
const compressedData = await (() => {
|
||||||
switch (encodeData.type) {
|
switch (encodeData.type) {
|
||||||
case mozJPEG.type: return mozJPEG.encode(quantizedSource, encodeData.options);
|
case mozJPEG.type: return mozJPEG.encode(sourceData, encodeData.options);
|
||||||
case webP.type: return webP.encode(quantizedSource, encodeData.options);
|
case webP.type: return webP.encode(sourceData, encodeData.options);
|
||||||
case browserPNG.type: return browserPNG.encode(quantizedSource, encodeData.options);
|
case browserPNG.type: return browserPNG.encode(sourceData, encodeData.options);
|
||||||
case browserJPEG.type: return browserJPEG.encode(quantizedSource, encodeData.options);
|
case browserJPEG.type: return browserJPEG.encode(sourceData, encodeData.options);
|
||||||
case browserWebP.type: return browserWebP.encode(quantizedSource, encodeData.options);
|
case browserWebP.type: return browserWebP.encode(sourceData, encodeData.options);
|
||||||
case browserGIF.type: return browserGIF.encode(quantizedSource, encodeData.options);
|
case browserGIF.type: return browserGIF.encode(sourceData, encodeData.options);
|
||||||
case browserTIFF.type: return browserTIFF.encode(quantizedSource, encodeData.options);
|
case browserTIFF.type: return browserTIFF.encode(sourceData, encodeData.options);
|
||||||
case browserJP2.type: return browserJP2.encode(quantizedSource, encodeData.options);
|
case browserJP2.type: return browserJP2.encode(sourceData, encodeData.options);
|
||||||
case browserBMP.type: return browserBMP.encode(quantizedSource, encodeData.options);
|
case browserBMP.type: return browserBMP.encode(sourceData, encodeData.options);
|
||||||
case browserPDF.type: return browserPDF.encode(quantizedSource, encodeData.options);
|
case browserPDF.type: return browserPDF.encode(sourceData, encodeData.options);
|
||||||
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -216,7 +220,8 @@ export default class App extends Component<Props, State> {
|
|||||||
let file;
|
let file;
|
||||||
|
|
||||||
try {
|
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) {
|
} 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user