From c29006d59336be5b038a519b70d596b2831b85d9 Mon Sep 17 00:00:00 2001 From: Surma Date: Wed, 5 Feb 2020 21:52:10 -0800 Subject: [PATCH] Add AVIF encoder without options --- codecs/avif_dec/avif_dec.d.ts | 4 +- codecs/avif_enc/avif_enc.d.ts | 4 +- src/codecs/avif/decoder.ts | 4 +- src/codecs/avif/encoder-meta.ts | 76 +----- src/codecs/avif/encoder.ts | 10 +- src/codecs/avif/options.tsx | 332 +-------------------------- src/codecs/encoders.ts | 4 + src/codecs/processor-worker/index.ts | 10 + src/codecs/processor.ts | 6 + src/components/compress/index.tsx | 2 + 10 files changed, 50 insertions(+), 402 deletions(-) diff --git a/codecs/avif_dec/avif_dec.d.ts b/codecs/avif_dec/avif_dec.d.ts index a53187bb..5f685d06 100644 --- a/codecs/avif_dec/avif_dec.d.ts +++ b/codecs/avif_dec/avif_dec.d.ts @@ -4,10 +4,10 @@ interface RawImage { height: number; } -interface WebPModule extends EmscriptenWasm.Module { +interface AVIFModule extends EmscriptenWasm.Module { decode(data: BufferSource): RawImage; free_result(): void; } -export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule; +export default function(opts: EmscriptenWasm.ModuleOpts): AVIFModule; diff --git a/codecs/avif_enc/avif_enc.d.ts b/codecs/avif_enc/avif_enc.d.ts index 89e319d2..60785dc0 100644 --- a/codecs/avif_enc/avif_enc.d.ts +++ b/codecs/avif_enc/avif_enc.d.ts @@ -1,9 +1,9 @@ // import { EncodeOptions } from '../../src/codecs/webp/encoder-meta'; -interface WebPModule extends EmscriptenWasm.Module { +interface AVIFModule extends EmscriptenWasm.Module { encode(data: BufferSource, width: number, height: number): Uint8Array; free_result(): void; } -export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule; +export default function(opts: EmscriptenWasm.ModuleOpts): AVIFModule; diff --git a/src/codecs/avif/decoder.ts b/src/codecs/avif/decoder.ts index dcd8369a..247edb50 100644 --- a/src/codecs/avif/decoder.ts +++ b/src/codecs/avif/decoder.ts @@ -1,8 +1,8 @@ -import avif_dec, { WebPModule } from '../../../codecs/avif_dec/avif_dec'; +import avif_dec, { AVIFModule } from '../../../codecs/avif_dec/avif_dec'; import wasmUrl from '../../../codecs/avif_dec/avif_dec.wasm'; import { initEmscriptenModule } from '../util'; -let emscriptenModule: Promise; +let emscriptenModule: Promise; export async function decode(data: ArrayBuffer): Promise { if (!emscriptenModule) emscriptenModule = initEmscriptenModule(avif_dec, wasmUrl); diff --git a/src/codecs/avif/encoder-meta.ts b/src/codecs/avif/encoder-meta.ts index 330a2825..3d9c6d4e 100644 --- a/src/codecs/avif/encoder-meta.ts +++ b/src/codecs/avif/encoder-meta.ts @@ -1,72 +1,10 @@ -export enum WebPImageHint { - WEBP_HINT_DEFAULT, // default preset. - WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot - WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting - WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc). -} +export interface EncodeOptions { } -export interface EncodeOptions { - quality: number; - target_size: number; - target_PSNR: number; - method: number; - sns_strength: number; - filter_strength: number; - filter_sharpness: number; - filter_type: number; - partitions: number; - segments: number; - pass: number; - show_compressed: number; - preprocessing: number; - autofilter: number; - partition_limit: number; - alpha_compression: number; - alpha_filtering: number; - alpha_quality: number; - lossless: number; - exact: number; - image_hint: number; - emulate_jpeg_size: number; - thread_level: number; - low_memory: number; - near_lossless: number; - use_delta_palette: number; - use_sharp_yuv: number; -} -export interface EncoderState { type: typeof type; options: EncodeOptions; } - -export const type = 'webp'; -export const label = 'WebP'; -export const mimeType = 'image/webp'; -export const extension = 'webp'; -// These come from struct WebPConfig in encode.h. +export const type = 'avif'; +export const label = 'AVIF'; +export const mimeType = 'image/avif'; +export const extension = 'avif'; export const defaultOptions: EncodeOptions = { - quality: 75, - target_size: 0, - target_PSNR: 0, - method: 4, - sns_strength: 50, - filter_strength: 60, - filter_sharpness: 0, - filter_type: 1, - partitions: 0, - segments: 4, - pass: 1, - show_compressed: 0, - preprocessing: 0, - autofilter: 0, - partition_limit: 0, - alpha_compression: 1, - alpha_filtering: 1, - alpha_quality: 100, - lossless: 0, - exact: 0, - image_hint: 0, - emulate_jpeg_size: 0, - thread_level: 0, - low_memory: 0, - near_lossless: 100, - use_delta_palette: 0, - use_sharp_yuv: 0, }; + +export interface EncoderState { type: typeof type; options: EncodeOptions; } diff --git a/src/codecs/avif/encoder.ts b/src/codecs/avif/encoder.ts index 5fafb577..07a6b371 100644 --- a/src/codecs/avif/encoder.ts +++ b/src/codecs/avif/encoder.ts @@ -1,15 +1,15 @@ -import webp_enc, { WebPModule } from '../../../codecs/webp_enc/webp_enc'; -import wasmUrl from '../../../codecs/webp_enc/webp_enc.wasm'; +import avif_enc, { AVIFModule } from '../../../codecs/avif_enc/avif_enc'; +import wasmUrl from '../../../codecs/avif_enc/avif_enc.wasm'; import { EncodeOptions } from './encoder-meta'; import { initEmscriptenModule } from '../util'; -let emscriptenModule: Promise; +let emscriptenModule: Promise; export async function encode(data: ImageData, options: EncodeOptions): Promise { - if (!emscriptenModule) emscriptenModule = initEmscriptenModule(webp_enc, wasmUrl); + if (!emscriptenModule) emscriptenModule = initEmscriptenModule(avif_enc, wasmUrl); const module = await emscriptenModule; - const resultView = module.encode(data.data, data.width, data.height, options); + const resultView = module.encode(data.data, data.width, data.height); const result = new Uint8Array(resultView); module.free_result(); diff --git a/src/codecs/avif/options.tsx b/src/codecs/avif/options.tsx index 6d363ad3..db33c310 100644 --- a/src/codecs/avif/options.tsx +++ b/src/codecs/avif/options.tsx @@ -1,345 +1,33 @@ import { h, Component } from 'preact'; import { bind } from '../../lib/initial-util'; -import { inputFieldCheckedAsNumber, inputFieldValueAsNumber, preventDefault } from '../../lib/util'; -import { EncodeOptions, WebPImageHint } from './encoder-meta'; +import { /*inputFieldCheckedAsNumber, inputFieldValueAsNumber,*/ preventDefault } from '../../lib/util'; +import { EncodeOptions } from './encoder-meta'; import * as style from '../../components/Options/style.scss'; -import Checkbox from '../../components/checkbox'; -import Expander from '../../components/expander'; -import Select from '../../components/select'; -import Range from '../../components/range'; -import linkState from 'linkstate'; +// import Checkbox from '../../components/checkbox'; +// import Expander from '../../components/expander'; +// import Select from '../../components/select'; +// import Range from '../../components/range'; +// import linkState from 'linkstate'; interface Props { options: EncodeOptions; onChange(newOptions: EncodeOptions): void; } -interface State { - showAdvanced: boolean; -} +interface State{} -// From kLosslessPresets in config_enc.c -// The format is [method, quality]. -const losslessPresets:[number, number][] = [ - [0, 0], [1, 20], [2, 25], [3, 30], [3, 50], - [4, 50], [4, 75], [4, 90], [5, 90], [6, 100], -]; -const losslessPresetDefault = 6; - -function determineLosslessQuality(quality: number, method: number): number { - const index = losslessPresets.findIndex( - ([presetMethod, presetQuality]) => presetMethod === method && presetQuality === quality, - ); - if (index !== -1) return index; - // Quality doesn't match one of the presets. - // This can happen when toggling 'lossless'. - return losslessPresetDefault; -} - -export default class WebPEncoderOptions extends Component { +export default class AVIFEncoderOptions extends Component { state: State = { - showAdvanced: false, }; @bind onChange(event: Event) { - const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement; - const lossless = inputFieldCheckedAsNumber(form.lossless); - const { options } = this.props; - const losslessPresetValue = inputFieldValueAsNumber( - form.lossless_preset, determineLosslessQuality(options.quality, options.method), - ); - - const newOptions: EncodeOptions = { - // Copy over options the form doesn't care about, eg emulate_jpeg_size - ...options, - // And now stuff from the form: - lossless, - // Special-cased inputs: - // In lossless mode, the quality is derived from the preset. - quality: lossless ? - losslessPresets[losslessPresetValue][1] : - inputFieldValueAsNumber(form.quality, options.quality), - // In lossless mode, the method is derived from the preset. - method: lossless ? - losslessPresets[losslessPresetValue][0] : - inputFieldValueAsNumber(form.method_input, options.method), - image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint) ? - WebPImageHint.WEBP_HINT_GRAPH : - WebPImageHint.WEBP_HINT_DEFAULT, - // .checked - exact: inputFieldCheckedAsNumber(form.exact, options.exact), - alpha_compression: inputFieldCheckedAsNumber( - form.alpha_compression, options.alpha_compression, - ), - 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 - near_lossless: 100 - inputFieldValueAsNumber(form.near_lossless, 100 - options.near_lossless), - alpha_quality: inputFieldValueAsNumber(form.alpha_quality, options.alpha_quality), - alpha_filtering: inputFieldValueAsNumber(form.alpha_filtering, options.alpha_filtering), - sns_strength: inputFieldValueAsNumber(form.sns_strength, options.sns_strength), - filter_strength: inputFieldValueAsNumber(form.filter_strength, options.filter_strength), - filter_sharpness: - 7 - inputFieldValueAsNumber(form.filter_sharpness, 7 - options.filter_sharpness), - pass: inputFieldValueAsNumber(form.pass, options.pass), - preprocessing: inputFieldValueAsNumber(form.preprocessing, options.preprocessing), - segments: inputFieldValueAsNumber(form.segments, options.segments), - partitions: inputFieldValueAsNumber(form.partitions, options.partitions), - }; - this.props.onChange(newOptions); - } - - private _losslessSpecificOptions(options: EncodeOptions) { - return ( -
-
- - Effort: - -
-
- - Slight loss: - -
- -
- ); - } - - private _lossySpecificOptions(options: EncodeOptions) { - const { showAdvanced } = this.state; - - return ( -
-
- - Effort: - -
-
- - Quality: - -
- - - {showAdvanced ? -
- -
- - Alpha quality: - -
-
- - Alpha filter quality: - -
- - - {options.autofilter ? null : -
- - Filter strength: - -
- } -
- -
- - Filter sharpness: - -
- -
- - Passes: - -
-
- - Spacial noise shaping: - -
- -
- - Segments: - -
-
- - Partitions: - -
-
- : null - } -
-
- ); } render({ options }: Props) { - // I'm rendering both lossy and lossless forms, as it becomes much easier when - // gathering the data. return (
- - {options.lossless - ? this._losslessSpecificOptions(options) - : this._lossySpecificOptions(options) - } - + Lol
); } diff --git a/src/codecs/encoders.ts b/src/codecs/encoders.ts index aa925674..6997c555 100644 --- a/src/codecs/encoders.ts +++ b/src/codecs/encoders.ts @@ -2,6 +2,7 @@ import * as identity from './identity/encoder-meta'; import * as oxiPNG from './oxipng/encoder-meta'; import * as mozJPEG from './mozjpeg/encoder-meta'; import * as webP from './webp/encoder-meta'; +import * as avif from './avif/encoder-meta'; import * as browserPNG from './browser-png/encoder-meta'; import * as browserJPEG from './browser-jpeg/encoder-meta'; import * as browserWebP from './browser-webp/encoder-meta'; @@ -20,6 +21,7 @@ export type EncoderState = oxiPNG.EncoderState | mozJPEG.EncoderState | webP.EncoderState | + avif.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState | browserWebP.EncoderState | @@ -34,6 +36,7 @@ export type EncoderOptions = oxiPNG.EncodeOptions | mozJPEG.EncodeOptions | webP.EncodeOptions | + avif.EncodeOptions | browserPNG.EncodeOptions | browserJPEG.EncodeOptions | browserWebP.EncodeOptions | @@ -50,6 +53,7 @@ export const encoderMap = { [oxiPNG.type]: oxiPNG, [mozJPEG.type]: mozJPEG, [webP.type]: webP, + [avif.type]: avif, [browserPNG.type]: browserPNG, [browserJPEG.type]: browserJPEG, [browserWebP.type]: browserWebP, diff --git a/src/codecs/processor-worker/index.ts b/src/codecs/processor-worker/index.ts index d1982c77..191717d2 100644 --- a/src/codecs/processor-worker/index.ts +++ b/src/codecs/processor-worker/index.ts @@ -82,6 +82,15 @@ async function webpDecode(data: ArrayBuffer): Promise { return timed('webpDecode', () => decode(data)); } +async function avifEncode( + data: ImageData, options: import('../avif/encoder-meta').EncodeOptions, +): Promise { + const { encode } = await import( + /* webpackChunkName: "process-avif-enc" */ + '../avif/encoder'); + return encode(data, options); +} + async function avifDecode(data: ArrayBuffer): Promise { const { decode } = await import( /* webpackChunkName: "process-avif-dec" */ @@ -97,6 +106,7 @@ const exports = { oxiPngEncode, webpEncode, webpDecode, + avifEncode, avifDecode, }; export type ProcessorWorkerApi = typeof exports; diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts index f67dc41e..41f6ddfa 100644 --- a/src/codecs/processor.ts +++ b/src/codecs/processor.ts @@ -4,6 +4,7 @@ import { canvasEncode, blobToArrayBuffer } from '../lib/util'; import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta'; import { EncodeOptions as OxiPNGEncoderOptions } from './oxipng/encoder-meta'; import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; +import { EncodeOptions as AvifEncoderOptions } from './avif/encoder-meta'; import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta'; import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta'; import { BrowserResizeOptions, VectorResizeOptions } from './resize/processor-meta'; @@ -169,6 +170,11 @@ export default class Processor { return this._workerApi!.avifDecode(data); } + @Processor._processingJob({ needsWorker: true }) + avifEncode(data: ImageData, opts: AvifEncoderOptions): Promise { + return this._workerApi!.avifEncode(data, opts); + } + // Not-worker jobs: @Processor._processingJob() diff --git a/src/components/compress/index.tsx b/src/components/compress/index.tsx index ea91dbda..f48d0173 100644 --- a/src/components/compress/index.tsx +++ b/src/components/compress/index.tsx @@ -10,6 +10,7 @@ import * as identity from '../../codecs/identity/encoder-meta'; import * as oxiPNG from '../../codecs/oxipng/encoder-meta'; import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta'; import * as webP from '../../codecs/webp/encoder-meta'; +import * as avif from '../../codecs/avif/encoder-meta'; import * as browserPNG from '../../codecs/browser-png/encoder-meta'; import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta'; import * as browserWebP from '../../codecs/browser-webp/encoder-meta'; @@ -136,6 +137,7 @@ async function compressImage( case oxiPNG.type: return processor.oxiPngEncode(image, encodeData.options); case mozJPEG.type: return processor.mozjpegEncode(image, encodeData.options); case webP.type: return processor.webpEncode(image, encodeData.options); + case avif.type: return processor.avifEncode(image, encodeData.options); case browserPNG.type: return processor.browserPngEncode(image); case browserJPEG.type: return processor.browserJpegEncode(image, encodeData.options); case browserWebP.type: return processor.browserWebpEncode(image, encodeData.options);