WebP options

This commit is contained in:
Jake Archibald
2018-10-19 12:46:19 +01:00
parent 0ba3c57f10
commit 49c47e32dc
4 changed files with 275 additions and 234 deletions

View File

@@ -2,13 +2,21 @@ import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util'; import { bind } from '../../lib/initial-util';
import { inputFieldCheckedAsNumber, inputFieldValueAsNumber } from '../../lib/util'; import { inputFieldCheckedAsNumber, inputFieldValueAsNumber } from '../../lib/util';
import { EncodeOptions, WebPImageHint } from './encoder-meta'; import { EncodeOptions, WebPImageHint } from './encoder-meta';
import * as styles from './styles.scss'; import * as style from '../../components/options/style.scss';
import '../../custom-els/RangeInput'; import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';
import Range from '../../components/range';
import linkState from 'linkstate';
type Props = { interface Props {
options: EncodeOptions, options: EncodeOptions;
onChange(newOptions: EncodeOptions): void, onChange(newOptions: EncodeOptions): void;
}; }
interface State {
showAdvanced: boolean;
}
// From kLosslessPresets in config_enc.c // From kLosslessPresets in config_enc.c
// The format is [method, quality]. // The format is [method, quality].
@@ -26,249 +34,281 @@ function determineLosslessQuality(quality: number): number {
return losslessPresetDefault; return losslessPresetDefault;
} }
export default class WebPEncoderOptions extends Component<Props, {}> { export default class WebPEncoderOptions extends Component<Props, State> {
state: State = {
showAdvanced: false,
};
@bind @bind
onChange(event: Event) { onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const lossless = inputFieldCheckedAsNumber(form.lossless); const lossless = inputFieldCheckedAsNumber(form.lossless);
const losslessPresetInput = (form.lossless_preset as HTMLInputElement); const { options } = this.props;
const losslessPresetValue = inputFieldValueAsNumber(
form.lossless_preset, determineLosslessQuality(options.quality),
);
const options: EncodeOptions = { const newOptions: EncodeOptions = {
// Copy over options the form doesn't care about, eg emulate_jpeg_size // Copy over options the form doesn't care about, eg emulate_jpeg_size
...this.props.options, ...options,
// And now stuff from the form: // And now stuff from the form:
lossless, lossless,
// Special-cased inputs: // Special-cased inputs:
// In lossless mode, the quality is derived from the preset. // In lossless mode, the quality is derived from the preset.
quality: lossless ? quality: lossless ?
losslessPresets[Number(losslessPresetInput.value)][1] : losslessPresets[losslessPresetValue][1] :
inputFieldValueAsNumber(form.quality), inputFieldValueAsNumber(form.quality, options.quality),
// In lossless mode, the method is derived from the preset. // In lossless mode, the method is derived from the preset.
method: lossless ? method: lossless ?
losslessPresets[Number(losslessPresetInput.value)][0] : losslessPresets[losslessPresetValue][0] :
inputFieldValueAsNumber(form.method_input), inputFieldValueAsNumber(form.method_input, options.method),
image_hint: (form.image_hint as HTMLInputElement).checked ? image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint) ?
WebPImageHint.WEBP_HINT_GRAPH : WebPImageHint.WEBP_HINT_GRAPH :
WebPImageHint.WEBP_HINT_DEFAULT, WebPImageHint.WEBP_HINT_DEFAULT,
// .checked // .checked
exact: inputFieldCheckedAsNumber(form.exact), exact: inputFieldCheckedAsNumber(form.exact, options.exact),
alpha_compression: inputFieldCheckedAsNumber(form.alpha_compression), alpha_compression: inputFieldCheckedAsNumber(
autofilter: inputFieldCheckedAsNumber(form.autofilter), form.alpha_compression, options.alpha_compression,
filter_type: inputFieldCheckedAsNumber(form.filter_type), ),
use_sharp_yuv: inputFieldCheckedAsNumber(form.use_sharp_yuv), 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 // .value
near_lossless: 100 - inputFieldValueAsNumber(form.near_lossless), near_lossless: 100 - inputFieldValueAsNumber(form.near_lossless, 100 - options.near_lossless),
alpha_quality: inputFieldValueAsNumber(form.alpha_quality), alpha_quality: inputFieldValueAsNumber(form.alpha_quality, options.alpha_quality),
alpha_filtering: inputFieldValueAsNumber(form.alpha_filtering), alpha_filtering: inputFieldValueAsNumber(form.alpha_filtering, options.alpha_filtering),
sns_strength: inputFieldValueAsNumber(form.sns_strength), sns_strength: inputFieldValueAsNumber(form.sns_strength, options.sns_strength),
filter_strength: inputFieldValueAsNumber(form.filter_strength), filter_strength: inputFieldValueAsNumber(form.filter_strength, options.filter_strength),
filter_sharpness: 7 - inputFieldValueAsNumber(form.filter_sharpness), filter_sharpness:
pass: inputFieldValueAsNumber(form.pass), 7 - inputFieldValueAsNumber(form.filter_sharpness, 7 - options.filter_sharpness),
preprocessing: inputFieldValueAsNumber(form.preprocessing), pass: inputFieldValueAsNumber(form.pass, options.pass),
segments: inputFieldValueAsNumber(form.segments), preprocessing: inputFieldValueAsNumber(form.preprocessing, options.preprocessing),
partitions: inputFieldValueAsNumber(form.partitions), segments: inputFieldValueAsNumber(form.segments, options.segments),
partitions: inputFieldValueAsNumber(form.partitions, options.partitions),
}; };
this.props.onChange(options); this.props.onChange(newOptions);
} }
private _losslessSpecificOptions(options: EncodeOptions) { private _losslessSpecificOptions(options: EncodeOptions) {
return ( return (
<div> <div key="lossless">
<label> <div class={style.optionOneCell}>
Effort: <Range
<range-input
name="lossless_preset" name="lossless_preset"
min="0" min="0"
max="9" max="9"
value={'' + determineLosslessQuality(options.quality)} value={determineLosslessQuality(options.quality)}
onChange={this.onChange} onInput={this.onChange}
/> >
</label> Effort:
<label> </Range>
Slight loss: </div>
<range-input <div class={style.optionOneCell}>
<Range
name="near_lossless" name="near_lossless"
min="0" min="0"
max="100" max="100"
value={'' + (100 - options.near_lossless)} value={'' + (100 - options.near_lossless)}
onChange={this.onChange} onInput={this.onChange}
/> >
</label> Slight loss:
<label> </Range>
</div>
<label class={style.optionInputFirst}>
{/* {/*
Although there are 3 different kinds of image hint, webp only Although there are 3 different kinds of image hint, webp only
seems to do something with the 'graph' type, and I don't really seems to do something with the 'graph' type, and I don't really
understand what it does. understand what it does.
*/} */}
<input <Checkbox
name="image_hint" name="image_hint"
type="checkbox"
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH} checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
value={'' + WebPImageHint.WEBP_HINT_GRAPH}
onChange={this.onChange} onChange={this.onChange}
/> />
<span>Discrete tone image (graph, map-tile etc)</span> Discrete tone image
</label> </label>
</div> </div>
); );
} }
private _lossySpecificOptions(options: EncodeOptions) { private _lossySpecificOptions(options: EncodeOptions) {
const { showAdvanced } = this.state;
return ( return (
<div> <div key="lossy">
<label> <div class={style.optionOneCell}>
Effort: <Range
<range-input
name="method_input" name="method_input"
min="0" min="0"
max="6" max="6"
value={'' + options.method} value={options.method}
onChange={this.onChange} onInput={this.onChange}
/> >
</label> Effort:
<label> </Range>
Quality: </div>
<range-input <div class={style.optionOneCell}>
<Range
name="quality" name="quality"
min="0" min="0"
max="100" max="100"
step="0.01" step="0.01"
value={'' + options.quality} value={options.quality}
onChange={this.onChange} onInput={this.onChange}
/>
</label>
<hr />
<label>
<input
name="alpha_compression"
type="checkbox"
checked={!!options.alpha_compression}
onChange={this.onChange}
/>
Compress alpha
</label>
<label>
Alpha quality:
<range-input
name="alpha_quality"
min="0"
max="100"
value={'' + options.alpha_quality}
onChange={this.onChange}
/>
</label>
<label>
Alpha filter quality:
<range-input
name="alpha_filtering"
min="0"
max="2"
value={'' + options.alpha_filtering}
onChange={this.onChange}
/>
</label>
<hr />
<label>
<input
name="autofilter"
type="checkbox"
checked={!!options.autofilter}
onChange={this.onChange}
/>
<span>Auto adjust filter strength</span>
</label>
<label>
Filter strength:
<range-input
name="filter_strength"
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:
<range-input
name="filter_sharpness"
min="0"
max="7"
value={'' + (7 - 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>
<hr />
<label>
Passes:
<range-input
name="pass"
min="1"
max="10"
value={'' + options.pass}
onChange={this.onChange}
/>
</label>
<label>
Spacial noise shaping:
<range-input
name="sns_strength"
min="0"
max="100"
value={'' + options.sns_strength}
onChange={this.onChange}
/>
</label>
<label>
Preprocessing type:
<select
name="preprocessing"
value={'' + options.preprocessing}
onChange={this.onChange}
> >
<option value="0">None</option> Quality:
<option value="1">Segment smooth</option> </Range>
<option value="2">Pseudo-random dithering</option> </div>
</select> <label class={style.optionInputFirst}>
</label> <Checkbox
<label> checked={showAdvanced}
Segments: onChange={linkState(this, 'showAdvanced')}
<range-input
name="segments"
min="1"
max="4"
value={'' + options.segments}
onChange={this.onChange}
/>
</label>
<label>
Partitions:
<range-input
name="partitions"
min="0"
max="3"
value={'' + options.partitions}
onChange={this.onChange}
/> />
Show advanced settings
</label> </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> </div>
); );
} }
@@ -277,32 +317,26 @@ export default class WebPEncoderOptions extends Component<Props, {}> {
// I'm rendering both lossy and lossless forms, as it becomes much easier when // I'm rendering both lossy and lossless forms, as it becomes much easier when
// gathering the data. // gathering the data.
return ( return (
<form> <form class={style.optionsSection}>
<label> <label class={style.optionInputFirst}>
<input <Checkbox
name="lossless" name="lossless"
type="checkbox"
checked={!!options.lossless} checked={!!options.lossless}
onChange={this.onChange} onChange={this.onChange}
/> />
Lossless Lossless
</label> </label>
<div class={options.lossless ? '' : styles.hide}> {options.lossless
{this._losslessSpecificOptions(options)} ? this._losslessSpecificOptions(options)
</div> : this._lossySpecificOptions(options)
<div class={options.lossless ? styles.hide : ''}> }
{this._lossySpecificOptions(options)} <label class={style.optionInputFirst}>
</div> <Checkbox
<label>
<input
name="exact" name="exact"
type="checkbox"
checked={!!options.exact} checked={!!options.exact}
onChange={this.onChange} onChange={this.onChange}
/> />
<span> Preserve transparent data
Preserve transparent data. Otherwise, pixels with zero alpha will have RGB also zeroed.
</span>
</label> </label>
</form> </form>
); );

View File

@@ -1,3 +0,0 @@
.hide {
display: none;
}

View File

@@ -180,28 +180,30 @@ export default class Options extends Component<Props, State> {
<h2 class={style.optionsTitle}>Compress</h2> <h2 class={style.optionsTitle}>Compress</h2>
<section class={`${style.optionOneCell} ${style.optionsSection}`}> <div class={style.optionsScroller}>
{encoderSupportMap ? <section class={`${style.optionOneCell} ${style.optionsSection}`}>
<Select value={encoderState.type} onChange={this.onEncoderTypeChange} large> {encoderSupportMap ?
{encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => ( <Select value={encoderState.type} onChange={this.onEncoderTypeChange} large>
<option value={encoder.type}>{encoder.label}</option> {encoders.filter(encoder => encoderSupportMap[encoder.type]).map(encoder => (
))} <option value={encoder.type}>{encoder.label}</option>
</Select> ))}
: </Select>
<Select large><option>Loading</option></Select> :
} <Select large><option>Loading</option></Select>
</section>
{EncoderOptionComponent &&
<EncoderOptionComponent
options={
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures
// the correct type, but typescript isn't smart enough.
encoderState.options as any
} }
onChange={onEncoderOptionsChange} </section>
/>
} {EncoderOptionComponent &&
<EncoderOptionComponent
options={
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures
// the correct type, but typescript isn't smart enough.
encoderState.options as any
}
onChange={onEncoderOptionsChange}
/>
}
</div>
{/* {/*

View File

@@ -5,6 +5,9 @@ $horizontalPadding: 15px;
width: 300px; width: 300px;
opacity: 0.8; opacity: 0.8;
font-size: 1.2rem; font-size: 1.2rem;
max-height: 100%;
display: flex;
flex-flow: column;
} }
.options-title { .options-title {
@@ -55,3 +58,8 @@ $horizontalPadding: 15px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
} }
.options-scroller {
overflow-x: hidden;
overflow-y: auto;
}