Integrate encoder

This commit is contained in:
Surma
2020-02-06 15:58:38 -08:00
committed by Ingvar Stepanyan
parent 98bc151907
commit e1dd2796c1
8 changed files with 51 additions and 389 deletions

View File

@@ -3,6 +3,7 @@ import * as oxiPNG from './oxipng/encoder-meta';
import * as mozJPEG from './mozjpeg/encoder-meta';
import * as webP from './webp/encoder-meta';
import * as avif from './avif/encoder-meta';
import * as jxl from './jxl/encoder-meta';
import * as browserPNG from './browser-png/encoder-meta';
import * as browserJPEG from './browser-jpeg/encoder-meta';
import * as browserWebP from './browser-webp/encoder-meta';
@@ -22,6 +23,7 @@ export type EncoderState =
mozJPEG.EncoderState |
webP.EncoderState |
avif.EncoderState |
jxl.EncoderState |
browserPNG.EncoderState |
browserJPEG.EncoderState |
browserWebP.EncoderState |
@@ -37,6 +39,7 @@ export type EncoderOptions =
mozJPEG.EncodeOptions |
webP.EncodeOptions |
avif.EncodeOptions |
jxl.EncodeOptions |
browserPNG.EncodeOptions |
browserJPEG.EncodeOptions |
browserWebP.EncodeOptions |
@@ -54,6 +57,7 @@ export const encoderMap = {
[mozJPEG.type]: mozJPEG,
[webP.type]: webP,
[avif.type]: avif,
[jxl.type]: jxl,
[browserPNG.type]: browserPNG,
[browserJPEG.type]: browserJPEG,
[browserWebP.type]: browserWebP,

View File

@@ -1,72 +1,12 @@
export enum WebPImageHint {
WEBP_HINT_DEFAULT, // default preset.
WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot
WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting
WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc).
export interface EncodeOptions {
}
export interface EncodeOptions {
quality: number;
target_size: number;
target_PSNR: number;
method: number;
sns_strength: number;
filter_strength: number;
filter_sharpness: number;
filter_type: number;
partitions: number;
segments: number;
pass: number;
show_compressed: number;
preprocessing: number;
autofilter: number;
partition_limit: number;
alpha_compression: number;
alpha_filtering: number;
alpha_quality: number;
lossless: number;
exact: number;
image_hint: number;
emulate_jpeg_size: number;
thread_level: number;
low_memory: number;
near_lossless: number;
use_delta_palette: number;
use_sharp_yuv: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'webp';
export const label = 'WebP';
export const mimeType = 'image/webp';
export const extension = 'webp';
export const type = 'jxl';
export const label = 'JPEG XL';
export const mimeType = 'image/jpegxl';
export const extension = 'jxl';
// These come from struct WebPConfig in encode.h.
export const defaultOptions: EncodeOptions = {
quality: 75,
target_size: 0,
target_PSNR: 0,
method: 4,
sns_strength: 50,
filter_strength: 60,
filter_sharpness: 0,
filter_type: 1,
partitions: 0,
segments: 4,
pass: 1,
show_compressed: 0,
preprocessing: 0,
autofilter: 0,
partition_limit: 0,
alpha_compression: 1,
alpha_filtering: 1,
alpha_quality: 100,
lossless: 0,
exact: 0,
image_hint: 0,
emulate_jpeg_size: 0,
thread_level: 0,
low_memory: 0,
near_lossless: 100,
use_delta_palette: 0,
use_sharp_yuv: 0,
};

View File

@@ -1,15 +1,15 @@
import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc';
import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm';
import jxl_enc, { JXLModule } from '../../../codecs/jxl_enc/jxl_enc';
import wasmUrl from '../../../codecs/jxl_enc/jxl_enc.wasm';
import { EncodeOptions } from './encoder-meta';
import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<WebPModule>;
let emscriptenModule: Promise<JXLModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl);
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(jxl_enc, wasmUrl);
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);
const resultView = module.encode(data.data, data.width, data.height);
const result = new Uint8Array(resultView);
module.free_result();

View File

@@ -1,13 +1,13 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, WebPImageHint } from './encoder-meta';
import { /*inputFieldCheckedAsNumber, inputFieldValueAsNumber,*/ preventDefault } from '../../lib/util';
import { EncodeOptions } from './encoder-meta';
import * as style from '../../components/Options/style.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';
import Range from '../../components/range';
import linkState from 'linkstate';
// import Checkbox from '../../components/checkbox';
// import Expander from '../../components/expander';
// import Select from '../../components/select';
// import Range from '../../components/range';
// import linkState from 'linkstate';
interface Props {
options: EncodeOptions;
@@ -15,331 +15,28 @@ interface Props {
}
interface State {
showAdvanced: boolean;
}
// 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, method: number): number {
const index = losslessPresets.findIndex(
([presetMethod, presetQuality]) => presetMethod === method && presetQuality === 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 WebPEncoderOptions extends Component<Props, State> {
export default class JXLEncoderOptions extends Component<Props, State> {
state: State = {
showAdvanced: false,
};
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const lossless = inputFieldCheckedAsNumber(form.lossless);
const { options } = this.props;
const losslessPresetValue = inputFieldValueAsNumber(
form.lossless_preset, determineLosslessQuality(options.quality, options.method),
);
// const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
// const { options } = this.props;
const newOptions: EncodeOptions = {
// Copy over options the form doesn't care about, eg emulate_jpeg_size
...options,
// And now stuff from the form:
lossless,
// Special-cased inputs:
// In lossless mode, the quality is derived from the preset.
quality: lossless ?
losslessPresets[losslessPresetValue][1] :
inputFieldValueAsNumber(form.quality, options.quality),
// In lossless mode, the method is derived from the preset.
method: lossless ?
losslessPresets[losslessPresetValue][0] :
inputFieldValueAsNumber(form.method_input, options.method),
image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint) ?
WebPImageHint.WEBP_HINT_GRAPH :
WebPImageHint.WEBP_HINT_DEFAULT,
// .checked
exact: inputFieldCheckedAsNumber(form.exact, options.exact),
alpha_compression: inputFieldCheckedAsNumber(
form.alpha_compression, options.alpha_compression,
),
autofilter: inputFieldCheckedAsNumber(form.autofilter, options.autofilter),
filter_type: inputFieldCheckedAsNumber(form.filter_type, options.filter_type),
use_sharp_yuv: inputFieldCheckedAsNumber(form.use_sharp_yuv, options.use_sharp_yuv),
// .value
near_lossless: 100 - inputFieldValueAsNumber(form.near_lossless, 100 - options.near_lossless),
alpha_quality: inputFieldValueAsNumber(form.alpha_quality, options.alpha_quality),
alpha_filtering: inputFieldValueAsNumber(form.alpha_filtering, options.alpha_filtering),
sns_strength: inputFieldValueAsNumber(form.sns_strength, options.sns_strength),
filter_strength: inputFieldValueAsNumber(form.filter_strength, options.filter_strength),
filter_sharpness:
7 - inputFieldValueAsNumber(form.filter_sharpness, 7 - options.filter_sharpness),
pass: inputFieldValueAsNumber(form.pass, options.pass),
preprocessing: inputFieldValueAsNumber(form.preprocessing, options.preprocessing),
segments: inputFieldValueAsNumber(form.segments, options.segments),
partitions: inputFieldValueAsNumber(form.partitions, options.partitions),
};
this.props.onChange(newOptions);
}
private _losslessSpecificOptions(options: EncodeOptions) {
return (
<div key="lossless">
<div class={style.optionOneCell}>
<Range
name="lossless_preset"
min="0"
max="9"
value={determineLosslessQuality(options.quality, options.method)}
onInput={this.onChange}
>
Effort:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="near_lossless"
min="0"
max="100"
value={'' + (100 - options.near_lossless)}
onInput={this.onChange}
>
Slight loss:
</Range>
</div>
<label class={style.optionInputFirst}>
{/*
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.
*/}
<Checkbox
name="image_hint"
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
onChange={this.onChange}
/>
Discrete tone image
</label>
</div>
);
}
private _lossySpecificOptions(options: EncodeOptions) {
const { showAdvanced } = this.state;
return (
<div key="lossy">
<div class={style.optionOneCell}>
<Range
name="method_input"
min="0"
max="6"
value={options.method}
onInput={this.onChange}
>
Effort:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="quality"
min="0"
max="100"
step="0.1"
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
</label>
<Expander>
{showAdvanced ?
<div>
<label class={style.optionInputFirst}>
<Checkbox
name="alpha_compression"
checked={!!options.alpha_compression}
onChange={this.onChange}
/>
Compress alpha
</label>
<div class={style.optionOneCell}>
<Range
name="alpha_quality"
min="0"
max="100"
value={options.alpha_quality}
onInput={this.onChange}
>
Alpha quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="alpha_filtering"
min="0"
max="2"
value={options.alpha_filtering}
onInput={this.onChange}
>
Alpha filter quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
name="autofilter"
checked={!!options.autofilter}
onChange={this.onChange}
/>
Auto adjust filter strength
</label>
<Expander>
{options.autofilter ? null :
<div class={style.optionOneCell}>
<Range
name="filter_strength"
min="0"
max="100"
value={options.filter_strength}
onInput={this.onChange}
>
Filter strength:
</Range>
</div>
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="filter_type"
checked={!!options.filter_type}
onChange={this.onChange}
/>
Strong filter
</label>
<div class={style.optionOneCell}>
<Range
name="filter_sharpness"
min="0"
max="7"
value={7 - options.filter_sharpness}
onInput={this.onChange}
>
Filter sharpness:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
name="use_sharp_yuv"
checked={!!options.use_sharp_yuv}
onChange={this.onChange}
/>
Sharp RGBYUV conversion
</label>
<div class={style.optionOneCell}>
<Range
name="pass"
min="1"
max="10"
value={options.pass}
onInput={this.onChange}
>
Passes:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="sns_strength"
min="0"
max="100"
value={options.sns_strength}
onInput={this.onChange}
>
Spacial noise shaping:
</Range>
</div>
<label class={style.optionTextFirst}>
Preprocess:
<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>
<div class={style.optionOneCell}>
<Range
name="segments"
min="1"
max="4"
value={options.segments}
onInput={this.onChange}
>
Segments:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="partitions"
min="0"
max="3"
value={options.partitions}
onInput={this.onChange}
>
Partitions:
</Range>
</div>
</div>
: null
}
</Expander>
</div>
);
// const newOptions: EncodeOptions = {
// // Copy over options the form doesn't care about, eg emulate_jpeg_size
// ...options,
// };
// this.props.onChange(newOptions);
}
render({ options }: Props) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
// gathering the data.
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<Checkbox
name="lossless"
checked={!!options.lossless}
onChange={this.onChange}
/>
Lossless
</label>
{options.lossless
? this._losslessSpecificOptions(options)
: this._lossySpecificOptions(options)
}
<label class={style.optionInputFirst}>
<Checkbox
name="exact"
checked={!!options.exact}
onChange={this.onChange}
/>
Preserve transparent data
</label>
Lol
</form>
);
}

View File

@@ -98,6 +98,15 @@ async function avifDecode(data: ArrayBuffer): Promise<ImageData> {
return timed('avifDencode', () => decode(data));
}
async function jxlEncode(
data: ImageData, options: import('../jxl/encoder-meta').EncodeOptions,
): Promise<ArrayBuffer> {
const { encode } = await import(
/* webpackChunkName: "process-jxl-enc" */
'../jxl/encoder');
return encode(data, options);
}
async function jxlDecode(data: ArrayBuffer): Promise<ImageData> {
const { decode } = await import(
/* webpackChunkName: "process-jxl-dec" */
@@ -115,6 +124,7 @@ const exports = {
webpDecode,
avifEncode,
avifDecode,
jxlEncode,
jxlDecode,
};
export type ProcessorWorkerApi = typeof exports;

View File

@@ -5,6 +5,7 @@ import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
import { EncodeOptions as OxiPNGEncoderOptions } from './oxipng/encoder-meta';
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
import { EncodeOptions as AvifEncoderOptions } from './avif/encoder-meta';
import { EncodeOptions as JXLEncoderOptions } from './jxl/encoder-meta';
import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta';
import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta';
import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta';
@@ -173,6 +174,11 @@ export default class Processor {
return this._workerApi!.avifEncode(data, opts);
}
@Processor._processingJob({ needsWorker: true })
jxlEncode(data: ImageData, opts: JXLEncoderOptions): Promise<ArrayBuffer> {
return this._workerApi!.jxlEncode(data, opts);
}
@Processor._processingJob({ needsWorker: true })
async jxlDecode(blob: Blob): Promise<ImageData> {
const data = await blobToArrayBuffer(blob);

View File

@@ -8,6 +8,7 @@ import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
import WebPEncoderOptions from '../../codecs/webp/options';
import AvifEncoderOptions from '../../codecs/avif/options';
import JXLEncoderOptions from '../../codecs/jxl/options';
import BrowserWebPEncoderOptions from '../../codecs/browser-webp/options';
import QuantizerOptionsComponent from '../../codecs/imagequant/options';
@@ -18,6 +19,7 @@ import * as oxiPNG from '../../codecs/oxipng/encoder-meta';
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
import * as webP from '../../codecs/webp/encoder-meta';
import * as avif from '../../codecs/avif/encoder-meta';
import * as jxl from '../../codecs/jxl/encoder-meta';
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta';
import * as browserWebP from '../../codecs/browser-webp/encoder-meta';
@@ -50,6 +52,7 @@ const encoderOptionsComponentMap: {
[mozJPEG.type]: MozJpegEncoderOptions,
[webP.type]: WebPEncoderOptions,
[avif.type]: AvifEncoderOptions,
[jxl.type]: JXLEncoderOptions,
[browserPNG.type]: undefined,
[browserJPEG.type]: BrowserJPEGEncoderOptions,
[browserWebP.type]: BrowserWebPEncoderOptions,

View File

@@ -11,6 +11,7 @@ import * as oxiPNG from '../../codecs/oxipng/encoder-meta';
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
import * as webP from '../../codecs/webp/encoder-meta';
import * as avif from '../../codecs/avif/encoder-meta';
import * as jxl from '../../codecs/jxl/encoder-meta';
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta';
import * as browserWebP from '../../codecs/browser-webp/encoder-meta';
@@ -138,6 +139,7 @@ async function compressImage(
case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options);
case webP.type: return processor.webpEncode(image, encodeData.options);
case avif.type: return processor.avifEncode(image, encodeData.options);
case jxl.type: return processor.jxlEncode(image, encodeData.options);
case browserPNG.type: return processor.browserPngEncode(image);
case browserJPEG.type: return processor.browserJpegEncode(image, encodeData.options);
case browserWebP.type: return processor.browserWebpEncode(image, encodeData.options);