AVIF in worker

This commit is contained in:
Jake Archibald
2020-09-16 11:48:13 +01:00
parent 2583d689b9
commit 7776134bc2
9 changed files with 171 additions and 42 deletions

View File

@@ -1,6 +1,7 @@
interface AVIFModule extends EmscriptenWasm.Module {
export interface AVIFModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}
export default function(opts: EmscriptenWasm.ModuleOpts): AVIFModule;
declare var moduleFactory: EmscriptenWasm.ModuleFactory<AVIFModule>;
export default moduleFactory;

View File

@@ -1,7 +1,14 @@
import { EncodeOptions } from '../../../src/codecs/avif/encoder-meta';
import { EncodeOptions } from 'image-worker/avifEncode';
interface AVIFModule extends EmscriptenWasm.Module {
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array | null;
export interface AVIFModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
}
export default function(opts: EmscriptenWasm.ModuleOpts): AVIFModule;
declare var moduleFactory: EmscriptenWasm.ModuleFactory<AVIFModule>;
export default moduleFactory;

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import avifDecoder, { AVIFModule } from 'codecs/avif/dec/avif_dec';
import wasmUrl from 'url:codecs/avif/dec/avif_dec.wasm';
import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<AVIFModule>;
export default async function decode(data: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(avifDecoder, wasmUrl);
}
const module = await emscriptenModule;
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View File

@@ -0,0 +1,45 @@
/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import avifEncoder, { AVIFModule } from 'codecs/avif/enc/avif_enc';
import wasmUrl from 'url:codecs/avif/enc/avif_enc.wasm';
import { initEmscriptenModule } from '../util';
export interface EncodeOptions {
minQuantizer: number;
maxQuantizer: number;
minQuantizerAlpha: number;
maxQuantizerAlpha: number;
tileRowsLog2: number;
tileColsLog2: number;
speed: number;
subsample: number;
}
let emscriptenModule: Promise<AVIFModule>;
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(avifEncoder, wasmUrl);
}
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, options);
if (!result) throw new Error('Encoding error');
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@@ -5,7 +5,8 @@ import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<AVIFModule>;
export async function decode(data: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(avif_dec, wasmUrl);
if (!emscriptenModule)
emscriptenModule = initEmscriptenModule(avif_dec, wasmUrl);
const module = await emscriptenModule;
const result = module.decode(data);

View File

@@ -24,4 +24,7 @@ export const defaultOptions: EncodeOptions = {
subsample: 1,
};
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}

View File

@@ -5,8 +5,12 @@ import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<AVIFModule>;
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(avif_enc, wasmUrl);
export async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule)
emscriptenModule = initEmscriptenModule(avif_enc, wasmUrl);
const module = await emscriptenModule;
const result = module.encode(data.data, data.width, data.height, options);

View File

@@ -34,18 +34,28 @@ const maxQuant = 63;
const maxSpeed = 10;
export default class AVIFEncoderOptions extends Component<Props, State> {
static getDerivedStateFromProps(props: Props, state: State): Partial<State> | undefined {
static getDerivedStateFromProps(
props: Props,
state: State,
): Partial<State> | undefined {
if (state.options && shallowEqual(state.options, props.options)) return;
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;
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 {
@@ -54,12 +64,16 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
losslessAlpha,
maxQuality: maxQuant - minQuantizerValue,
minQuality: maxQuant - maxQuantizerValue,
separateAlpha: options.maxQuantizer !== options.maxQuantizerAlpha ||
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,
subsample:
options.subsample === 0 || lossless
? defaultOptions.subsample
: options.subsample,
tileRows: options.tileRowsLog2,
tileCols: options.tileColsLog2,
effort: maxSpeed - options.speed,
@@ -78,9 +92,12 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
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 newVal =
type === 'boolean'
? 'checked' in formEl
? formEl.checked
: !!formEl.value
: Number(formEl.value);
const newState: Partial<State> = {
[prop]: newVal,
@@ -115,20 +132,32 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
...newState,
};
const maxQuantizer = optionState.lossless ? 0 : (maxQuant - optionState.minQuality);
const minQuantizer = optionState.lossless ? 0 : (maxQuant - optionState.maxQuality);
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,
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,
subsample: optionState.grayscale
? 0
: optionState.lossless
? 3
: optionState.subsample,
tileColsLog2: optionState.tileCols,
tileRowsLog2: optionState.tileRows,
speed: maxSpeed - optionState.effort,
@@ -147,13 +176,24 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
}
return this._inputChangeCallbacks.get(prop)!;
}
};
render(
_: Props,
{
effort, grayscale, lossless, losslessAlpha, maxAlphaQuality, maxQuality, minAlphaQuality,
minQuality, separateAlpha, showAdvanced, subsample, tileCols, tileRows,
effort,
grayscale,
lossless,
losslessAlpha,
maxAlphaQuality,
maxQuality,
minAlphaQuality,
minQuality,
separateAlpha,
showAdvanced,
subsample,
tileCols,
tileRows,
}: State,
) {
return (
@@ -209,7 +249,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
Lossless alpha
</label>
<Expander>
{!losslessAlpha &&
{!losslessAlpha && (
<div>
<div class={style.optionOneCell}>
<Range
@@ -232,7 +272,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
</Range>
</div>
</div>
}
)}
</Expander>
</div>
)}
@@ -245,7 +285,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
Show advanced settings
</label>
<Expander>
{showAdvanced &&
{showAdvanced && (
<div>
{/*<label class={style.optionInputFirst}>
<Checkbox
@@ -256,7 +296,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
Grayscale
</label>*/}
<Expander>
{!grayscale && !lossless &&
{!grayscale && !lossless && (
<label class={style.optionTextFirst}>
Subsample chroma:
<Select
@@ -269,7 +309,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
<option value="3">4:4:4</option>
</Select>
</label>
}
)}
</Expander>
<div class={style.optionOneCell}>
<Range
@@ -292,7 +332,7 @@ export default class AVIFEncoderOptions extends Component<Props, State> {
</Range>
</div>
</div>
}
)}
</Expander>
<div class={style.optionOneCell}>
<Range