import { EncodeOptions, defaultOptions } 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 Checkbox from 'client/lazy-app/Compress/Options/Checkbox'; import Expander from 'client/lazy-app/Compress/Options/Expander'; import Select from 'client/lazy-app/Compress/Options/Select'; import Range from 'client/lazy-app/Compress/Options/Range'; import linkState from 'linkstate'; import Revealer from 'client/lazy-app/Compress/Options/Revealer'; export const encode = ( signal: AbortSignal, workerBridge: WorkerBridge, imageData: ImageData, options: EncodeOptions, ) => workerBridge.avifEncode(signal, imageData, options); interface Props { options: EncodeOptions; onChange(newOptions: EncodeOptions): void; } interface State { options: EncodeOptions; lossless: boolean; maxQuality: number; minQuality: number; separateAlpha: boolean; losslessAlpha: boolean; maxAlphaQuality: number; minAlphaQuality: number; showAdvanced: boolean; grayscale: boolean; subsample: number; tileRows: number; tileCols: number; effort: number; } const maxQuant = 63; const maxSpeed = 10; export class Options extends Component { static getDerivedStateFromProps( props: Props, state: State, ): Partial | null { if (state.options && shallowEqual(state.options, props.options)) { return null; } const { options } = props; const lossless = options.maxQuantizer === 0 && options.minQuantizer === 0; const minQuantizerValue = lossless ? defaultOptions.minQuantizer : options.minQuantizer; const maxQuantizerValue = lossless ? defaultOptions.maxQuantizer : options.maxQuantizer; const losslessAlpha = options.maxQuantizerAlpha === 0 && options.minQuantizerAlpha === 0; const minQuantizerAlphaValue = losslessAlpha ? defaultOptions.minQuantizerAlpha : options.minQuantizerAlpha; const maxQuantizerAlphaValue = losslessAlpha ? defaultOptions.maxQuantizerAlpha : options.maxQuantizerAlpha; // Create default form state from options return { options, lossless, losslessAlpha, maxQuality: maxQuant - minQuantizerValue, minQuality: maxQuant - maxQuantizerValue, separateAlpha: options.maxQuantizer !== options.maxQuantizerAlpha || options.minQuantizer !== options.minQuantizerAlpha, maxAlphaQuality: maxQuant - minQuantizerAlphaValue, minAlphaQuality: maxQuant - maxQuantizerAlphaValue, grayscale: options.subsample === 0, subsample: options.subsample === 0 || lossless ? defaultOptions.subsample : options.subsample, tileRows: options.tileRowsLog2, tileCols: options.tileColsLog2, effort: maxSpeed - options.speed, }; } // The rest of the defaults are set in getDerivedStateFromProps state: State = { 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, }; // Ensure that min cannot be greater than max switch (prop) { case 'maxQuality': if (newVal < this.state.minQuality) { newState.minQuality = newVal as number; } break; case 'minQuality': if (newVal > this.state.maxQuality) { newState.maxQuality = newVal as number; } break; case 'maxAlphaQuality': if (newVal < this.state.minAlphaQuality) { newState.minAlphaQuality = newVal as number; } break; case 'minAlphaQuality': if (newVal > this.state.maxAlphaQuality) { newState.maxAlphaQuality = newVal as number; } break; } const optionState = { ...this.state, ...newState, }; const maxQuantizer = optionState.lossless ? 0 : maxQuant - optionState.minQuality; const minQuantizer = optionState.lossless ? 0 : maxQuant - optionState.maxQuality; const newOptions: EncodeOptions = { maxQuantizer, minQuantizer, maxQuantizerAlpha: optionState.separateAlpha ? optionState.losslessAlpha ? 0 : maxQuant - optionState.minAlphaQuality : maxQuantizer, minQuantizerAlpha: optionState.separateAlpha ? optionState.losslessAlpha ? 0 : maxQuant - optionState.maxAlphaQuality : minQuantizer, // Always set to 4:4:4 if lossless subsample: optionState.grayscale ? 0 : optionState.lossless ? 3 : optionState.subsample, tileColsLog2: optionState.tileCols, tileRowsLog2: optionState.tileRows, speed: maxSpeed - optionState.effort, }; // Updating options, so we don't recalculate in getDerivedStateFromProps. newState.options = newOptions; this.setState( // It isn't clear to me why I have to cast this :) newState as State, ); this.props.onChange(newOptions); }); } return this._inputChangeCallbacks.get(prop)!; }; render( _: Props, { effort, grayscale, lossless, losslessAlpha, maxAlphaQuality, maxQuality, minAlphaQuality, minQuality, separateAlpha, showAdvanced, subsample, tileCols, tileRows, }: State, ) { return (
{!lossless && (
Max quality:
Min quality:
)}
{separateAlpha && (
{!losslessAlpha && (
Max alpha quality:
Min alpha quality:
)}
)}
{showAdvanced && (
{/**/} {!grayscale && !lossless && ( )}
Log2 of tile rows:
Log2 of tile cols:
)}
Effort:
); } }