mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 17:27:09 +00:00
Hardcode quantization pass
This commit is contained in:
78
src/codecs/imagequant/Quantizer.worker.ts
Normal file
78
src/codecs/imagequant/Quantizer.worker.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import imagequant from '../../../codecs/imagequant/imagequant';
|
||||||
|
// Using require() so TypeScript doesn’t complain about this not being a module.
|
||||||
|
const wasmBinaryUrl = require('../../../codecs/imagequant/imagequant.wasm');
|
||||||
|
|
||||||
|
// API exposed by wasm module. Details in the codec’s README.
|
||||||
|
interface ModuleAPI {
|
||||||
|
version(): number;
|
||||||
|
create_buffer(width: number, height: number): number;
|
||||||
|
destroy_buffer(pointer: number): void;
|
||||||
|
quantize(buffer: number, width: number, height: number, numColors: number, dither: number): void;
|
||||||
|
free_result(): void;
|
||||||
|
get_result_pointer(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ImageQuant {
|
||||||
|
private emscriptenModule: Promise<EmscriptenWasm.Module>;
|
||||||
|
private api: Promise<ModuleAPI>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.emscriptenModule = new Promise((resolve) => {
|
||||||
|
const m = imagequant({
|
||||||
|
// Just to be safe, don’t automatically invoke any wasm functions
|
||||||
|
noInitialRun: false,
|
||||||
|
locateFile(url: string): string {
|
||||||
|
// Redirect the request for the wasm binary to whatever webpack gave us.
|
||||||
|
if (url.endsWith('.wasm')) {
|
||||||
|
return wasmBinaryUrl;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
onRuntimeInitialized() {
|
||||||
|
// An Emscripten is a then-able that, for some reason, `then()`s itself,
|
||||||
|
// causing an infite loop when you wrap it in a real promise. Deleting the `then`
|
||||||
|
// prop solves this for now.
|
||||||
|
// See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129
|
||||||
|
// TODO(surma@): File a bug with Emscripten on this.
|
||||||
|
delete (m as any).then;
|
||||||
|
resolve(m);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api = (async () => {
|
||||||
|
// Not sure why, but TypeScript complains that I am using
|
||||||
|
// `emscriptenModule` before it’s getting assigned, which is clearly not
|
||||||
|
// true :shrug: Using `any`
|
||||||
|
const m = await (this as any).emscriptenModule;
|
||||||
|
return {
|
||||||
|
version: m.cwrap('version', 'number', []),
|
||||||
|
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||||
|
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
|
||||||
|
quantize: m.cwrap('quantize', '', ['number', 'number', 'number', 'number', 'number']),
|
||||||
|
free_result: m.cwrap('free_result', '', []),
|
||||||
|
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async quantize(data: ImageData): 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);
|
||||||
|
const resultPointer = api.get_result_pointer();
|
||||||
|
const resultView = new Uint8Array(
|
||||||
|
m.HEAP8.buffer,
|
||||||
|
resultPointer,
|
||||||
|
data.width * data.height * 4,
|
||||||
|
);
|
||||||
|
const result = new Uint8ClampedArray(resultView);
|
||||||
|
api.free_result();
|
||||||
|
api.destroy_buffer(p);
|
||||||
|
|
||||||
|
return new ImageData(result, data.width, data.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
330
src/codecs/imagequant/options.tsx
Normal file
330
src/codecs/imagequant/options.tsx
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
// import { h, Component } from 'preact';
|
||||||
|
// import { bind } from '../../lib/util';
|
||||||
|
// import * as styles from './styles.scss';
|
||||||
|
|
||||||
|
// type Props = {
|
||||||
|
// options: EncodeOptions,
|
||||||
|
// onChange(newOptions: EncodeOptions): 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @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);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * @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>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
7
src/codecs/imagequant/quantizer.ts
Normal file
7
src/codecs/imagequant/quantizer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import QuantizerWorker from './Quantizer.worker';
|
||||||
|
|
||||||
|
export const name = 'Image Quanitzer';
|
||||||
|
export async function quantize(data: ImageData): Promise<ImageData> {
|
||||||
|
const quantizer = await new QuantizerWorker();
|
||||||
|
return quantizer.quantize(data);
|
||||||
|
}
|
||||||
6
src/codecs/imagequant/styles.scss
Normal file
6
src/codecs/imagequant/styles.scss
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.flip-range {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import Options from '../Options';
|
|||||||
import { FileDropEvent } from './custom-els/FileDrop';
|
import { FileDropEvent } from './custom-els/FileDrop';
|
||||||
import './custom-els/FileDrop';
|
import './custom-els/FileDrop';
|
||||||
|
|
||||||
|
import * as quantizer from '../../codecs/imagequant/quantizer';
|
||||||
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';
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder';
|
||||||
@@ -64,18 +65,19 @@ async function compressImage(
|
|||||||
// 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);
|
||||||
const compressedData = await (() => {
|
const compressedData = await (() => {
|
||||||
switch (encodeData.type) {
|
switch (encodeData.type) {
|
||||||
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
|
case mozJPEG.type: return mozJPEG.encode(quantizedSource, encodeData.options);
|
||||||
case webP.type: return webP.encode(source.data, encodeData.options);
|
case webP.type: return webP.encode(quantizedSource, encodeData.options);
|
||||||
case browserPNG.type: return browserPNG.encode(source.data, encodeData.options);
|
case browserPNG.type: return browserPNG.encode(quantizedSource, encodeData.options);
|
||||||
case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options);
|
case browserJPEG.type: return browserJPEG.encode(quantizedSource, encodeData.options);
|
||||||
case browserWebP.type: return browserWebP.encode(source.data, encodeData.options);
|
case browserWebP.type: return browserWebP.encode(quantizedSource, encodeData.options);
|
||||||
case browserGIF.type: return browserGIF.encode(source.data, encodeData.options);
|
case browserGIF.type: return browserGIF.encode(quantizedSource, encodeData.options);
|
||||||
case browserTIFF.type: return browserTIFF.encode(source.data, encodeData.options);
|
case browserTIFF.type: return browserTIFF.encode(quantizedSource, encodeData.options);
|
||||||
case browserJP2.type: return browserJP2.encode(source.data, encodeData.options);
|
case browserJP2.type: return browserJP2.encode(quantizedSource, encodeData.options);
|
||||||
case browserBMP.type: return browserBMP.encode(source.data, encodeData.options);
|
case browserBMP.type: return browserBMP.encode(quantizedSource, encodeData.options);
|
||||||
case browserPDF.type: return browserPDF.encode(source.data, encodeData.options);
|
case browserPDF.type: return browserPDF.encode(quantizedSource, encodeData.options);
|
||||||
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user