forked from external-repos/squoosh
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 './custom-els/FileDrop';
|
||||
|
||||
import * as quantizer from '../../codecs/imagequant/quantizer';
|
||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
||||
import * as webP from '../../codecs/webp/encoder';
|
||||
import * as identity from '../../codecs/identity/encoder';
|
||||
@@ -64,18 +65,19 @@ async function compressImage(
|
||||
// Special case for identity
|
||||
if (encodeData.type === identity.type) return source.file;
|
||||
|
||||
const quantizedSource = await quantizer.quantize(source.data);
|
||||
const compressedData = await (() => {
|
||||
switch (encodeData.type) {
|
||||
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
|
||||
case webP.type: return webP.encode(source.data, encodeData.options);
|
||||
case browserPNG.type: return browserPNG.encode(source.data, encodeData.options);
|
||||
case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options);
|
||||
case browserWebP.type: return browserWebP.encode(source.data, encodeData.options);
|
||||
case browserGIF.type: return browserGIF.encode(source.data, encodeData.options);
|
||||
case browserTIFF.type: return browserTIFF.encode(source.data, encodeData.options);
|
||||
case browserJP2.type: return browserJP2.encode(source.data, encodeData.options);
|
||||
case browserBMP.type: return browserBMP.encode(source.data, encodeData.options);
|
||||
case browserPDF.type: return browserPDF.encode(source.data, encodeData.options);
|
||||
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);
|
||||
default: throw Error(`Unexpected encoder ${JSON.stringify(encodeData)}`);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user