Extra Wp2 Options (#853)

* wip

* wip

* Add extra options

* Even more options!

* Update src/features/encoders/wp2/client/index.tsx

Co-authored-by: Surma <surma@surma.dev>

Co-authored-by: Surma <surma@surma.dev>
This commit is contained in:
Jake Archibald
2020-11-20 16:12:38 +00:00
committed by GitHub
parent f11e692d58
commit 13631f1cfc
7 changed files with 339 additions and 90 deletions

View File

@@ -7,7 +7,31 @@ using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array"); thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string image_in, int image_width, int image_height, WP2::EncoderConfig config) { struct WP2Options {
float quality;
float alpha_quality;
int speed;
int pass;
int uv_mode;
float sns;
int csp_type;
int error_diffusion;
bool use_random_matrix;
};
val encode(std::string image_in, int image_width, int image_height, WP2Options options) {
WP2::EncoderConfig config = {};
config.quality = options.quality;
config.alpha_quality = options.alpha_quality;
config.speed = options.speed;
config.pass = options.pass;
config.uv_mode = static_cast<WP2::EncoderConfig::UVMode>(options.uv_mode);
config.csp_type = static_cast<WP2::Csp>(options.csp_type);
config.sns = options.sns;
config.error_diffusion = options.error_diffusion;
config.use_random_matrix = options.use_random_matrix;
uint8_t* image_buffer = (uint8_t*)image_in.c_str(); uint8_t* image_buffer = (uint8_t*)image_in.c_str();
WP2::ArgbBuffer src = WP2::ArgbBuffer(); WP2::ArgbBuffer src = WP2::ArgbBuffer();
WP2Status status = WP2Status status =
@@ -27,12 +51,16 @@ val encode(std::string image_in, int image_width, int image_height, WP2::Encoder
} }
EMSCRIPTEN_BINDINGS(my_module) { EMSCRIPTEN_BINDINGS(my_module) {
value_object<WP2::EncoderConfig>("WP2EncoderConfig") value_object<WP2Options>("WP2Options")
.field("quality", &WP2::EncoderConfig::quality) .field("quality", &WP2Options::quality)
.field("alpha_quality", &WP2::EncoderConfig::alpha_quality) .field("alpha_quality", &WP2Options::alpha_quality)
.field("speed", &WP2::EncoderConfig::speed) .field("speed", &WP2Options::speed)
.field("pass", &WP2::EncoderConfig::pass) .field("pass", &WP2Options::pass)
.field("sns", &WP2::EncoderConfig::sns); .field("uv_mode", &WP2Options::uv_mode)
.field("csp_type", &WP2Options::csp_type)
.field("error_diffusion", &WP2Options::error_diffusion)
.field("use_random_matrix", &WP2Options::use_random_matrix)
.field("sns", &WP2Options::sns);
function("encode", &encode); function("encode", &encode);
} }

View File

@@ -4,6 +4,24 @@ export interface EncodeOptions {
speed: number; speed: number;
pass: number; pass: number;
sns: number; sns: number;
uv_mode: UVMode;
csp_type: Csp;
error_diffusion: number;
use_random_matrix: boolean;
}
export const enum UVMode {
UVModeAdapt = 0, // Mix of 420 and 444 (per block)
UVMode420, // All blocks 420
UVMode444, // All blocks 444
UVModeAuto, // Choose any of the above automatically
}
export const enum Csp {
kYCoCg,
kYCbCr,
kCustom,
kYIQ,
} }
export interface WP2Module extends EmscriptenWasm.Module { export interface WP2Module extends EmscriptenWasm.Module {

View File

@@ -576,7 +576,7 @@ var wp2_enc = (function () {
}, },
}); });
var ob = { var ob = {
p: function (a, b, c, d) { t: function (a, b, c, d) {
A( A(
'Assertion failed: ' + 'Assertion failed: ' +
C(a) + C(a) +
@@ -807,7 +807,7 @@ var wp2_enc = (function () {
return []; return [];
}); });
}, },
c: function (a, b, c, d, e) { d: function (a, b, c, d, e) {
function g(l) { function g(l) {
return l; return l;
} }
@@ -1000,10 +1000,10 @@ var wp2_enc = (function () {
}, },
}); });
}, },
q: function (a, b, c, d, e, g) { p: function (a, b, c, d, e, g) {
Fa[a] = { name: U(b), da: Y(c, d), ea: Y(e, g), U: [] }; Fa[a] = { name: U(b), da: Y(c, d), ea: Y(e, g), U: [] };
}, },
e: function (a, b, c, d, e, g, m, h, k, l) { c: function (a, b, c, d, e, g, m, h, k, l) {
Fa[a].U.push({ Fa[a].U.push({
X: U(b), X: U(b),
aa: c, aa: c,
@@ -1034,7 +1034,7 @@ var wp2_enc = (function () {
m: function (a) { m: function (a) {
4 < a && (X[a].T += 1); 4 < a && (X[a].T += 1);
}, },
t: function (a, b, c, d) { q: function (a, b, c, d) {
a || W('Cannot use deleted val. handle = ' + a); a || W('Cannot use deleted val. handle = ' + a);
a = X[a].value; a = X[a].value;
var e = ib[b]; var e = ib[b];
@@ -1079,7 +1079,7 @@ var wp2_enc = (function () {
u: function (a, b, c) { u: function (a, b, c) {
D.copyWithin(a, b, b + c); D.copyWithin(a, b, b + c);
}, },
d: function (a) { e: function (a) {
a >>>= 0; a >>>= 0;
var b = D.length; var b = D.length;
if (2147483648 < a) return !1; if (2147483648 < a) return !1;

Binary file not shown.

View File

@@ -315,9 +315,9 @@ export class Options extends Component<Props, State> {
value={subsample} value={subsample}
onChange={this._inputChange('subsample', 'number')} onChange={this._inputChange('subsample', 'number')}
> >
<option value="1">4:2:0</option> <option value="1">Half</option>
{/*<option value="2">4:2:2</option>*/} {/*<option value="2">4:2:2</option>*/}
<option value="3">4:4:4</option> <option value="3">Off</option>
</Select> </Select>
</label> </label>
)} )}

View File

@@ -1,9 +1,14 @@
import { EncodeOptions } from '../shared/meta'; import { EncodeOptions, UVMode, Csp } from '../shared/meta';
import { defaultOptions } from '../shared/meta';
import type WorkerBridge from 'client/lazy-app/worker-bridge'; import type WorkerBridge from 'client/lazy-app/worker-bridge';
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util'; import { preventDefault, shallowEqual } from 'client/lazy-app/util';
import * as style from 'client/lazy-app/Compress/Options/style.css'; import * as style from 'client/lazy-app/Compress/Options/style.css';
import Range from 'client/lazy-app/Compress/Options/Range'; import Range from 'client/lazy-app/Compress/Options/Range';
import Select from 'client/lazy-app/Compress/Options/Select';
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
import Expander from 'client/lazy-app/Compress/Options/Expander';
import linkState from 'linkstate';
export const encode = ( export const encode = (
signal: AbortSignal, signal: AbortSignal,
@@ -18,93 +23,286 @@ interface Props {
} }
interface State { interface State {
options: EncodeOptions;
effort: number;
quality: number;
alphaQuality: number;
passes: number;
sns: number;
uvMode: number;
lossless: boolean;
slightLoss: number;
colorSpace: number;
errorDiffusion: number;
useRandomMatrix: boolean;
showAdvanced: boolean; showAdvanced: boolean;
separateAlpha: boolean;
} }
export class Options extends Component<Props, State> { export class Options extends Component<Props, State> {
state: State = { static getDerivedStateFromProps(
showAdvanced: false, props: Props,
}; state: State,
): Partial<State> | null {
if (state.options && shallowEqual(state.options, props.options)) {
return null;
}
private onChange = (event: Event) => { const { options } = props;
const form = (event.currentTarget as HTMLInputElement).closest(
'form', const modifyState: Partial<State> = {
) as HTMLFormElement; options,
const { options } = this.props; effort: options.speed,
const newOptions: EncodeOptions = { alphaQuality: options.alpha_quality,
quality: inputFieldValueAsNumber(form.quality, options.quality), passes: options.pass,
alpha_quality: inputFieldValueAsNumber( sns: options.sns,
form.alpha_quality, uvMode: options.uv_mode,
options.alpha_quality, colorSpace: options.csp_type,
), errorDiffusion: options.error_diffusion,
speed: inputFieldValueAsNumber(form.speed, options.speed), useRandomMatrix: options.use_random_matrix,
pass: inputFieldValueAsNumber(form.pass, options.pass), separateAlpha: options.quality !== options.alpha_quality,
sns: inputFieldValueAsNumber(form.sns, options.sns),
}; };
this.props.onChange(newOptions);
// If quality is > 95, it's lossless with slight loss
if (options.quality > 95) {
modifyState.lossless = true;
modifyState.slightLoss = 100 - options.quality;
} else {
modifyState.quality = options.quality;
modifyState.lossless = false;
}
return modifyState;
}
// Other state is set in getDerivedStateFromProps
state: State = {
lossless: false,
slightLoss: 0,
quality: defaultOptions.quality,
showAdvanced: 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 = {
speed: optionState.effort,
quality: optionState.lossless
? 100 - optionState.slightLoss
: optionState.quality,
alpha_quality: optionState.separateAlpha
? optionState.alphaQuality
: optionState.quality,
pass: optionState.passes,
sns: optionState.sns,
uv_mode: optionState.uvMode,
csp_type: optionState.colorSpace,
error_diffusion: optionState.errorDiffusion,
use_random_matrix: optionState.useRandomMatrix,
};
// 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({ options }: Props) { render(
{}: Props,
{
effort,
alphaQuality,
passes,
quality,
sns,
uvMode,
lossless,
slightLoss,
colorSpace,
errorDiffusion,
useRandomMatrix,
separateAlpha,
showAdvanced,
}: State,
) {
return ( return (
<form class={style.optionsSection} onSubmit={preventDefault}> <form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionInputFirst}>
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
/>
Lossless
</label>
<Expander>
{lossless && (
<div class={style.optionOneCell}>
<Range
min="0"
max="5"
step="0.1"
value={slightLoss}
onInput={this._inputChange('slightLoss', 'number')}
>
Slight loss:
</Range>
</div>
)}
</Expander>
<Expander>
{!lossless && (
<div>
<div class={style.optionOneCell}>
<Range
min="0"
max="95"
step="0.1"
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
/>
Separate alpha quality
</label>
<Expander>
{separateAlpha && (
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={alphaQuality}
onInput={this._inputChange('alphaQuality', 'number')}
>
Alpha Quality:
</Range>
</div>
)}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
</label>
<Expander>
{showAdvanced && (
<div>
<div class={style.optionOneCell}>
<Range
min="1"
max="10"
step="1"
value={passes}
onInput={this._inputChange('passes', 'number')}
>
Passes:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={sns}
onInput={this._inputChange('sns', 'number')}
>
Spatial noise shaping:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="100"
step="1"
value={errorDiffusion}
onInput={this._inputChange('errorDiffusion', 'number')}
>
Error diffusion:
</Range>
</div>
<label class={style.optionTextFirst}>
Subsample chroma:
<Select
value={uvMode}
onInput={this._inputChange('uvMode', 'number')}
>
<option value={UVMode.UVModeAuto}>Auto</option>
<option value={UVMode.UVModeAdapt}>Vary</option>
<option value={UVMode.UVMode420}>Half</option>
<option value={UVMode.UVMode444}>Off</option>
</Select>
</label>
<label class={style.optionTextFirst}>
Color space:
<Select
value={colorSpace}
onInput={this._inputChange('colorSpace', 'number')}
>
<option value={Csp.kYCoCg}>YCoCg</option>
<option value={Csp.kYCbCr}>YCbCr</option>
<option value={Csp.kYIQ}>YIQ</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<Checkbox
checked={useRandomMatrix}
onChange={this._inputChange(
'useRandomMatrix',
'boolean',
)}
/>
Random matrix
</label>
</div>
)}
</Expander>
</div>
)}
</Expander>
<div class={style.optionOneCell}> <div class={style.optionOneCell}>
<Range <Range
name="quality"
min="0"
max="100"
step="1"
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="alpha_quality"
min="0"
max="100"
step="1"
value={options.alpha_quality}
onInput={this.onChange}
>
Alpha Quality:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="speed"
min="0" min="0"
max="9" max="9"
step="1" step="1"
value={options.speed} value={effort}
onInput={this.onChange} onInput={this._inputChange('effort', 'number')}
> >
Speed: Effort:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="pass"
min="1"
max="10"
step="1"
value={options.pass}
onInput={this.onChange}
>
Pass:
</Range>
</div>
<div class={style.optionOneCell}>
<Range
name="sns"
min="0"
max="100"
step="1"
value={options.sns}
onInput={this.onChange}
>
Spatial noise shaping:
</Range> </Range>
</div> </div>
</form> </form>

View File

@@ -11,16 +11,21 @@
* limitations under the License. * limitations under the License.
*/ */
import type { EncodeOptions } from 'codecs/wp2/enc/wp2_enc'; import type { EncodeOptions } from 'codecs/wp2/enc/wp2_enc';
import { UVMode, Csp } from 'codecs/wp2/enc/wp2_enc';
export { EncodeOptions }; export { EncodeOptions, UVMode, Csp };
export const label = 'WebP v2 (unstable)'; export const label = 'WebP v2 (unstable)';
export const mimeType = 'image/webp2'; export const mimeType = 'image/webp2';
export const extension = 'wp2'; export const extension = 'wp2';
export const defaultOptions: EncodeOptions = { export const defaultOptions: EncodeOptions = {
quality: 75, quality: 75,
alpha_quality: 100, alpha_quality: 75,
speed: 5, speed: 5,
pass: 1, pass: 1,
sns: 50, sns: 50,
uv_mode: UVMode.UVModeAuto,
csp_type: Csp.kYCoCg,
error_diffusion: 0,
use_random_matrix: false,
}; };