diff --git a/codecs/wp2/enc/wp2_enc.cpp b/codecs/wp2/enc/wp2_enc.cpp index 3066101b..117f6784 100644 --- a/codecs/wp2/enc/wp2_enc.cpp +++ b/codecs/wp2/enc/wp2_enc.cpp @@ -7,7 +7,31 @@ using namespace emscripten; 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(options.uv_mode); + config.csp_type = static_cast(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(); WP2::ArgbBuffer src = WP2::ArgbBuffer(); WP2Status status = @@ -27,12 +51,16 @@ val encode(std::string image_in, int image_width, int image_height, WP2::Encoder } EMSCRIPTEN_BINDINGS(my_module) { - value_object("WP2EncoderConfig") - .field("quality", &WP2::EncoderConfig::quality) - .field("alpha_quality", &WP2::EncoderConfig::alpha_quality) - .field("speed", &WP2::EncoderConfig::speed) - .field("pass", &WP2::EncoderConfig::pass) - .field("sns", &WP2::EncoderConfig::sns); + value_object("WP2Options") + .field("quality", &WP2Options::quality) + .field("alpha_quality", &WP2Options::alpha_quality) + .field("speed", &WP2Options::speed) + .field("pass", &WP2Options::pass) + .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); } diff --git a/codecs/wp2/enc/wp2_enc.d.ts b/codecs/wp2/enc/wp2_enc.d.ts index 8e9fef9e..507aa594 100644 --- a/codecs/wp2/enc/wp2_enc.d.ts +++ b/codecs/wp2/enc/wp2_enc.d.ts @@ -4,6 +4,24 @@ export interface EncodeOptions { speed: number; pass: 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 { diff --git a/codecs/wp2/enc/wp2_enc.js b/codecs/wp2/enc/wp2_enc.js index ba9f9140..98433ea4 100644 --- a/codecs/wp2/enc/wp2_enc.js +++ b/codecs/wp2/enc/wp2_enc.js @@ -576,7 +576,7 @@ var wp2_enc = (function () { }, }); var ob = { - p: function (a, b, c, d) { + t: function (a, b, c, d) { A( 'Assertion failed: ' + C(a) + @@ -807,7 +807,7 @@ var wp2_enc = (function () { return []; }); }, - c: function (a, b, c, d, e) { + d: function (a, b, c, d, e) { function g(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: [] }; }, - 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({ X: U(b), aa: c, @@ -1034,7 +1034,7 @@ var wp2_enc = (function () { m: function (a) { 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 = X[a].value; var e = ib[b]; @@ -1079,7 +1079,7 @@ var wp2_enc = (function () { u: function (a, b, c) { D.copyWithin(a, b, b + c); }, - d: function (a) { + e: function (a) { a >>>= 0; var b = D.length; if (2147483648 < a) return !1; diff --git a/codecs/wp2/enc/wp2_enc.wasm b/codecs/wp2/enc/wp2_enc.wasm index 0157b60f..cd13fcc9 100755 Binary files a/codecs/wp2/enc/wp2_enc.wasm and b/codecs/wp2/enc/wp2_enc.wasm differ diff --git a/src/features/encoders/avif/client/index.tsx b/src/features/encoders/avif/client/index.tsx index cd0990f2..892c1248 100644 --- a/src/features/encoders/avif/client/index.tsx +++ b/src/features/encoders/avif/client/index.tsx @@ -315,9 +315,9 @@ export class Options extends Component { value={subsample} onChange={this._inputChange('subsample', 'number')} > - + {/**/} - + )} diff --git a/src/features/encoders/wp2/client/index.tsx b/src/features/encoders/wp2/client/index.tsx index 503f3672..ff6b03ef 100644 --- a/src/features/encoders/wp2/client/index.tsx +++ b/src/features/encoders/wp2/client/index.tsx @@ -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 { 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 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 = ( signal: AbortSignal, @@ -18,93 +23,286 @@ interface Props { } 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; + separateAlpha: boolean; } export class Options extends Component { - state: State = { - showAdvanced: false, - }; + static getDerivedStateFromProps( + props: Props, + state: State, + ): Partial | null { + if (state.options && shallowEqual(state.options, props.options)) { + return null; + } - private onChange = (event: Event) => { - const form = (event.currentTarget as HTMLInputElement).closest( - 'form', - ) as HTMLFormElement; - const { options } = this.props; - const newOptions: EncodeOptions = { - quality: inputFieldValueAsNumber(form.quality, options.quality), - alpha_quality: inputFieldValueAsNumber( - form.alpha_quality, - options.alpha_quality, - ), - speed: inputFieldValueAsNumber(form.speed, options.speed), - pass: inputFieldValueAsNumber(form.pass, options.pass), - sns: inputFieldValueAsNumber(form.sns, options.sns), + const { options } = props; + + const modifyState: Partial = { + options, + effort: options.speed, + alphaQuality: options.alpha_quality, + passes: options.pass, + sns: options.sns, + uvMode: options.uv_mode, + colorSpace: options.csp_type, + errorDiffusion: options.error_diffusion, + useRandomMatrix: options.use_random_matrix, + separateAlpha: options.quality !== options.alpha_quality, }; - 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 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 = { + [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 (
+ + + {lossless && ( +
+ + Slight loss: + +
+ )} +
+ + {!lossless && ( +
+
+ + Quality: + +
+ + + {separateAlpha && ( +
+ + Alpha Quality: + +
+ )} +
+ + + {showAdvanced && ( +
+
+ + Passes: + +
+
+ + Spatial noise shaping: + +
+
+ + Error diffusion: + +
+ + + +
+ )} +
+
+ )} +
- Quality: - -
-
- - Alpha Quality: - -
-
- - Speed: - -
-
- - Pass: - -
-
- - Spatial noise shaping: + Effort:
diff --git a/src/features/encoders/wp2/shared/meta.ts b/src/features/encoders/wp2/shared/meta.ts index e9d652f1..9b5abe45 100644 --- a/src/features/encoders/wp2/shared/meta.ts +++ b/src/features/encoders/wp2/shared/meta.ts @@ -11,16 +11,21 @@ * limitations under the License. */ 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 mimeType = 'image/webp2'; export const extension = 'wp2'; export const defaultOptions: EncodeOptions = { quality: 75, - alpha_quality: 100, + alpha_quality: 75, speed: 5, pass: 1, sns: 50, + uv_mode: UVMode.UVModeAuto, + csp_type: Csp.kYCoCg, + error_diffusion: 0, + use_random_matrix: false, };