Files
squoosh/src/features/encoders/jxl/client/index.tsx
Ziemowit Zabawa 18b53e2b8a JPEG XL: Add lossy Modular option (#1136)
* Improve .gitattributes

* Add disabled checkbox style

* Update Makefile

* Update jxl_enc.cpp

* add -O3 flag to skia compilation for optimization's sake

* Bump libjxl revision to 9f544641ec83f6abd9da598bdd08178ee8a003e0

Change use of EncodeFile from `EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*aux=*/nullptr, pool_ptr)` to `EncodeFile(cparams, &io, &passes_enc_state, &bytes, /*cms=*/nullptr, /*aux=*/jxl::GetJxlCms(), pool_ptr)`

* JPEG XL: Add lossy Modular option

Co-authored-by: CanadianBaconBoi <beamconnor@gmail.com>
Co-authored-by: CanadianBaconBoi <bc.bacon.bits@gmail.com>
2022-01-21 12:42:27 +00:00

250 lines
7.5 KiB
TypeScript

import { EncodeOptions } from '../shared/meta';
import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact';
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
export const encode = (
signal: AbortSignal,
workerBridge: WorkerBridge,
imageData: ImageData,
options: EncodeOptions,
) => workerBridge.jxlEncode(signal, imageData, options);
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
interface State {
options: EncodeOptions;
effort: number;
quality: number;
progressive: boolean;
edgePreservingFilter: number;
lossless: boolean;
slightLoss: boolean;
autoEdgePreservingFilter: boolean;
decodingSpeedTier: number;
photonNoiseIso: number;
alternativeLossy: boolean;
}
export class Options extends Component<Props, State> {
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | null {
if (state.options && shallowEqual(state.options, props.options)) {
return null;
}
const { options } = props;
// Create default form state from options
return {
options,
effort: options.effort,
quality: options.quality,
progressive: options.progressive,
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
lossless: options.quality === 100,
slightLoss: options.lossyPalette,
autoEdgePreservingFilter: options.epf === -1,
decodingSpeedTier: options.decodingSpeedTier,
photonNoiseIso: options.photonNoiseIso,
alternativeLossy: options.lossyModular,
};
}
// The rest of the defaults are set in getDerivedStateFromProps
state: State = {
lossless: false,
} as State;
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
// Cache the callback for performance
if (!this._inputChangeCallbacks.has(prop)) {
this._inputChangeCallbacks.set(prop, (event: Event) => {
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
const newVal =
type === 'boolean'
? 'checked' in formEl
? formEl.checked
: !!formEl.value
: Number(formEl.value);
const newState: Partial<State> = {
[prop]: newVal,
};
const optionState = {
...this.state,
...newState,
};
const newOptions: EncodeOptions = {
effort: optionState.effort,
quality: optionState.lossless ? 100 : optionState.quality,
progressive: optionState.progressive,
epf: optionState.autoEdgePreservingFilter
? -1
: optionState.edgePreservingFilter,
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
decodingSpeedTier: optionState.decodingSpeedTier,
photonNoiseIso: optionState.photonNoiseIso,
lossyModular: optionState.quality < 7 ? true : optionState.alternativeLossy,
};
// Updating options, so we don't recalculate in getDerivedStateFromProps.
newState.options = newOptions;
this.setState(newState);
this.props.onChange(newOptions);
});
}
return this._inputChangeCallbacks.get(prop)!;
};
render(
{}: Props,
{
effort,
quality,
progressive,
edgePreservingFilter,
lossless,
slightLoss,
autoEdgePreservingFilter,
decodingSpeedTier,
photonNoiseIso,
alternativeLossy,
}: State,
) {
// 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.optionToggle}>
Lossless
<Checkbox
name="lossless"
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
</label>
<Expander>
{lossless && (
<label class={style.optionToggle}>
Slight loss
<Checkbox
name="slightLoss"
checked={slightLoss}
onChange={this._inputChange('slightLoss', 'boolean')}
/>
</label>
)}
</Expander>
<Expander>
{!lossless && (
<div>
<div class={style.optionOneCell}>
<Range
min="0"
max="99.9"
step="0.1"
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
</Range>
</div>
<label class={style.optionToggle}>
Alternative lossy mode
<Checkbox
checked={quality < 7 ? true : alternativeLossy}
disabled={quality < 7}
onChange={this._inputChange('alternativeLossy', 'boolean')}
/>
</label>
<label class={style.optionToggle}>
Auto edge filter
<Checkbox
checked={autoEdgePreservingFilter}
onChange={this._inputChange(
'autoEdgePreservingFilter',
'boolean',
)}
/>
</label>
<Expander>
{!autoEdgePreservingFilter && (
<div class={style.optionOneCell}>
<Range
min="0"
max="3"
value={edgePreservingFilter}
onInput={this._inputChange(
'edgePreservingFilter',
'number',
)}
>
Edge preserving filter:
</Range>
</div>
)}
</Expander>
<div class={style.optionOneCell}>
<Range
min="0"
max="4"
value={decodingSpeedTier}
onInput={this._inputChange('decodingSpeedTier', 'number')}
>
Optimise for decoding speed (worse compression):
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="50000"
step="100"
value={photonNoiseIso}
onInput={this._inputChange('photonNoiseIso', 'number')}
>
Noise equivalent to ISO:
</Range>
</div>
</div>
)}
</Expander>
<label class={style.optionToggle}>
Progressive rendering
<Checkbox
name="progressive"
checked={progressive}
onChange={this._inputChange('progressive', 'boolean')}
/>
</label>
<div class={style.optionOneCell}>
<Range
min="1"
max="9"
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
</Range>
</div>
</form>
);
}
}