Update libavif (#1381)

* Update libavif (v1.0.0-main)

* Update libavif for improved compression and speed
* v1.0.0 deprecates usage of min and max-quantizers; we use `quality` and `qualityAlpha` instead
* Renamed `maxSpeed` to `MAX_EFFORT` for better readability

* Update libavif (v1.0.1-main)

* Refactor variable names for clarity

* Update libaom (v3.7.0)

* Add checks for API return values

* Rename variables for readability

Changes `cqlevel` to `quality`, and `cqAlphaLevel` to `qualityAlpha`

* Minor patches in logic

* Minor patches in lossless calculation

* Add chroma subsampling options to AVIF

* Add skeleton for sharp downsampling param

* Try to use libsharpyuv

* Encoder working, decoder isnt

* Make sure sharpyuv is disabled for decoder

* Add AVIF_LOCAL flags for sharp yuv

* Get AVIF sharp YUV working

* Clean up AVIF makefiles

* AVIF: Make sharpyuv conditional on subsample

* AVIF: Flags to speed up sharpyuv build

* AVIF: Minor refactoring in enc.cpp

* AVIF: Minor refactoring & renaming

* AVIF: Use smart pointers to prevent memory leaks

* AVIF: Minor refactoring

* AVIF: Revert defaultoptions logic change

---------

Co-authored-by: Surma <surma@surma.dev>
Co-authored-by: Jake Archibald <jaffathecake@gmail.com>
This commit is contained in:
Aryan Pingle
2024-01-24 13:34:33 +05:30
committed by GitHub
parent d87eff7645
commit e217740e53
19 changed files with 196 additions and 100 deletions

View File

@@ -1,9 +1,9 @@
# libavif and libaom versions are from
# google3/third_party/libavif/METADATA
CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/647c3c208cf152395d777c1bf7240d2ecf7df5a9.tar.gz
CODEC_PACKAGE = node_modules/libavif.tar.gz
# using libavif from https://github.com/AOMediaCodec/libavif
LIBAVIF_URL = https://github.com/AOMediaCodec/libavif/archive/refs/tags/v1.0.1.tar.gz
LIBAVIF_PACKAGE = node_modules/libavif.tar.gz
LIBAOM_URL = https://aomedia.googlesource.com/aom/+archive/v3.6.0.tar.gz
# using libaom from https://aomedia.googlesource.com/aom
LIBAOM_URL = https://aomedia.googlesource.com/aom/+archive/v3.7.0.tar.gz
LIBAOM_PACKAGE = node_modules/libaom.tar.gz
export CODEC_DIR = node_modules/libavif
@@ -11,7 +11,12 @@ export BUILD_DIR = node_modules/build
export LIBAOM_DIR = node_modules/libaom
override CFLAGS += "-Wno-unused-macros"
export
# We must build libsharpyuv from a specific libwebp commit
# See libavif/ext/libsharpyuv.cmd for more detail
LIBWEBP_URL_WITH_SHARPYUV = https://chromium.googlesource.com/webm/libwebp/+archive/e2c85878f6a33f29948b43d3492d9cdaf801aa54.tar.gz
LIBWEBP_DIR := $(CODEC_DIR)/ext/libwebp
export LIBSHARPYUV := $(LIBWEBP_DIR)/build/libsharpyuv.a
OUT_ENC_JS = enc/avif_enc.js
OUT_NODE_ENC_JS = enc/avif_node_enc.js
@@ -28,10 +33,10 @@ HELPER_MAKEFLAGS := -f helper.Makefile
.PHONY: all clean
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS) $(OUT_NODE_DEC_JS)
all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_JS)
$(OUT_NODE_ENC_JS) $(OUT_NODE_ENC_MT_JS): ENVIRONMENT=node
$(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
# ST-Encoding
$(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(LIBSHARPYUV)
$(MAKE) \
$(HELPER_MAKEFLAGS) \
OUT_JS=$@ \
@@ -42,9 +47,10 @@ $(OUT_NODE_ENC_JS) $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(L
-DCONFIG_AV1_HIGHBITDEPTH=0 \
" \
ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0"
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0 -DAVIF_LOCAL_LIBSHARPYUV=ON"
$(OUT_ENC_MT_JS) $(OUT_NODE_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
# MT-Encoding
$(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(LIBSHARPYUV)
$(MAKE) \
$(HELPER_MAKEFLAGS) \
OUT_JS=$@ \
@@ -54,11 +60,11 @@ $(OUT_ENC_MT_JS) $(OUT_NODE_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.t
-DCONFIG_AV1_HIGHBITDEPTH=0 \
" \
ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0" \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_DECODE=0 -DAVIF_LOCAL_LIBSHARPYUV=ON" \
OUT_FLAGS="-pthread"
$(OUT_NODE_DEC_JS): ENVIRONMENT=node
$(OUT_NODE_DEC_JS) $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
# Decoding
$(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt
$(MAKE) \
$(HELPER_MAKEFLAGS) \
OUT_JS=$@ \
@@ -70,22 +76,56 @@ $(OUT_NODE_DEC_JS) $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(L
ENVIRONMENT=$(ENVIRONMENT) \
LIBAVIF_FLAGS="-DAVIF_CODEC_AOM_ENCODE=0"
$(CODEC_PACKAGE):
mkdir -p $(@D)
curl -sL $(CODEC_URL) -o $@
# LIBAOM EXTRACTION SECTION
# Download the libaom tarball
$(LIBAOM_PACKAGE):
mkdir -p $(@D)
curl -sL $(LIBAOM_URL) -o $@
$(CODEC_DIR)/CMakeLists.txt: $(CODEC_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(CODEC_PACKAGE)
# Extract libaom from the tarball
$(LIBAOM_DIR)/CMakeLists.txt: $(LIBAOM_PACKAGE)
mkdir -p $(@D)
tar xzm -C $(@D) -f $(LIBAOM_PACKAGE)
# LIBAVIF EXTRACTION SECTION
# Download the libavif tarball
$(LIBAVIF_PACKAGE):
mkdir -p $(@D)
curl -sL $(LIBAVIF_URL) -o $@
# Extract libavif from the tarball
$(CODEC_DIR)/CMakeLists.txt: $(LIBAVIF_PACKAGE)
mkdir -p $(@D)
tar xzm --strip 1 -C $(@D) -f $(LIBAVIF_PACKAGE)
# Create libavif/ext/libwebp
$(LIBWEBP_DIR)/CMakeLists.txt: $(CODEC_DIR)/CMakeLists.txt
mkdir -p $(LIBWEBP_DIR)
curl -sL $(LIBWEBP_URL_WITH_SHARPYUV) \
| tar xzm -C $(LIBWEBP_DIR)
# Make libsharpyuv.a
$(LIBSHARPYUV): $(LIBWEBP_DIR)/CMakeLists.txt
mkdir -p $(@D)
emcmake cmake \
-DWEBP_BUILD_ANIM_UTILS=OFF \
-DWEBP_BUILD_CWEBP=OFF \
-DWEBP_BUILD_DWEBP=OFF \
-DWEBP_BUILD_GIF2WEBP=OFF \
-DWEBP_BUILD_IMG2WEBP=OFF \
-DWEBP_BUILD_VWEBP=OFF \
-DWEBP_BUILD_WEBPINFO=OFF \
-DWEBP_BUILD_LIBWEBPMUX=OFF \
-DWEBP_BUILD_WEBPMUX=OFF \
-DWEBP_BUILD_EXTRAS=OFF \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_BUILD_TYPE=Release \
-S $(LIBWEBP_DIR) \
-B $(@D)
$(MAKE) -C $(@D) sharpyuv
clean:
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_JS) clean
$(MAKE) $(HELPER_MAKEFLAGS) OUT_JS=$(OUT_ENC_MT_JS) clean

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

View File

@@ -3,15 +3,27 @@
#include <emscripten/val.h>
#include "avif/avif.h"
#include <memory>
#include <string>
#define RETURN_NULL_IF(expression) \
do { \
if (expression) \
return val::null(); \
} while (false)
using namespace emscripten;
using AvifImagePtr = std::unique_ptr<avifImage, decltype(&avifImageDestroy)>;
using AvifEncoderPtr = std::unique_ptr<avifEncoder, decltype(&avifEncoderDestroy)>;
struct AvifOptions {
// [0 - 63]
// 0 = lossless
// 63 = worst quality
int cqLevel;
// As above, but -1 means 'use cqLevel'
int cqAlphaLevel;
// [0 - 100]
// 0 = worst quality
// 100 = lossless
int quality;
// As above, but -1 means 'use quality'
int qualityAlpha;
// [0 - 6]
// Creates 2^n tiles in that dimension
int tileRowsLog2;
@@ -35,12 +47,15 @@ struct AvifOptions {
int tune;
// 0-50
int denoiseLevel;
// toggles AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV
bool enableSharpYUV;
};
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string buffer, int width, int height, AvifOptions options) {
avifRWData output = AVIF_DATA_EMPTY;
avifResult status; // To check the return status for avif API's
int depth = 8;
avifPixelFormat format;
switch (options.subsample) {
@@ -58,11 +73,13 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
break;
}
bool lossless = options.cqLevel == AVIF_QUANTIZER_LOSSLESS &&
options.cqAlphaLevel <= AVIF_QUANTIZER_LOSSLESS &&
bool lossless = options.quality == AVIF_QUALITY_LOSSLESS &&
(options.qualityAlpha == -1 || options.qualityAlpha == AVIF_QUALITY_LOSSLESS) &&
format == AVIF_PIXEL_FORMAT_YUV444;
avifImage* image = avifImageCreate(width, height, depth, format);
// Smart pointer for the input image in YUV format
AvifImagePtr image(avifImageCreate(width, height, depth, format), avifImageDestroy);
RETURN_NULL_IF(image == nullptr);
if (lossless) {
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
@@ -73,43 +90,49 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
uint8_t* rgba = reinterpret_cast<uint8_t*>(const_cast<char*>(buffer.data()));
avifRGBImage srcRGB;
avifRGBImageSetDefaults(&srcRGB, image);
avifRGBImageSetDefaults(&srcRGB, image.get());
srcRGB.pixels = rgba;
srcRGB.rowBytes = width * 4;
avifImageRGBToYUV(image, &srcRGB);
if (options.enableSharpYUV) {
srcRGB.chromaDownsampling = AVIF_CHROMA_DOWNSAMPLING_SHARP_YUV;
}
status = avifImageRGBToYUV(image.get(), &srcRGB);
RETURN_NULL_IF(status != AVIF_RESULT_OK);
avifEncoder* encoder = avifEncoderCreate();
// Create a smart pointer for the encoder
AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
RETURN_NULL_IF(encoder == nullptr);
if (lossless) {
encoder->minQuantizer = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizer = AVIF_QUANTIZER_LOSSLESS;
encoder->minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->quality = AVIF_QUALITY_LOSSLESS;
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
} else {
encoder->minQuantizer = AVIF_QUANTIZER_BEST_QUALITY;
encoder->maxQuantizer = AVIF_QUANTIZER_WORST_QUALITY;
encoder->minQuantizerAlpha = AVIF_QUANTIZER_BEST_QUALITY;
encoder->maxQuantizerAlpha = AVIF_QUANTIZER_WORST_QUALITY;
avifEncoderSetCodecSpecificOption(encoder, "end-usage", "q");
avifEncoderSetCodecSpecificOption(encoder, "cq-level", std::to_string(options.cqLevel).c_str());
avifEncoderSetCodecSpecificOption(encoder, "sharpness",
std::to_string(options.sharpness).c_str());
status = avifEncoderSetCodecSpecificOption(encoder.get(), "sharpness",
std::to_string(options.sharpness).c_str());
RETURN_NULL_IF(status != AVIF_RESULT_OK);
if (options.cqAlphaLevel != -1) {
avifEncoderSetCodecSpecificOption(encoder, "alpha:cq-level",
std::to_string(options.cqAlphaLevel).c_str());
// Set base quality
encoder->quality = options.quality;
// Conditionally set alpha quality
if (options.qualityAlpha == -1) {
encoder->qualityAlpha = options.quality;
} else {
encoder->qualityAlpha = options.qualityAlpha;
}
if (options.tune == 2 || (options.tune == 0 && options.cqLevel <= 32)) {
avifEncoderSetCodecSpecificOption(encoder, "tune", "ssim");
if (options.tune == 2 || (options.tune == 0 && options.quality >= 50)) {
status = avifEncoderSetCodecSpecificOption(encoder.get(), "tune", "ssim");
RETURN_NULL_IF(status != AVIF_RESULT_OK);
}
if (options.chromaDeltaQ) {
avifEncoderSetCodecSpecificOption(encoder, "enable-chroma-deltaq", "1");
status = avifEncoderSetCodecSpecificOption(encoder.get(), "color:enable-chroma-deltaq", "1");
RETURN_NULL_IF(status != AVIF_RESULT_OK);
}
avifEncoderSetCodecSpecificOption(encoder, "color:denoise-noise-level",
std::to_string(options.denoiseLevel).c_str());
status = avifEncoderSetCodecSpecificOption(encoder.get(), "color:denoise-noise-level",
std::to_string(options.denoiseLevel).c_str());
RETURN_NULL_IF(status != AVIF_RESULT_OK);
}
encoder->maxThreads = emscripten_num_logical_cores();
@@ -117,22 +140,21 @@ val encode(std::string buffer, int width, int height, AvifOptions options) {
encoder->tileColsLog2 = options.tileColsLog2;
encoder->speed = options.speed;
avifResult encodeResult = avifEncoderWrite(encoder, image, &output);
avifRWData output = AVIF_DATA_EMPTY;
avifResult encodeResult = avifEncoderWrite(encoder.get(), image.get(), &output);
auto js_result = val::null();
if (encodeResult == AVIF_RESULT_OK) {
js_result = Uint8Array.new_(typed_memory_view(output.size, output.data));
}
avifImageDestroy(image);
avifEncoderDestroy(encoder);
avifRWDataFree(&output);
return js_result;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<AvifOptions>("AvifOptions")
.field("cqLevel", &AvifOptions::cqLevel)
.field("cqAlphaLevel", &AvifOptions::cqAlphaLevel)
.field("quality", &AvifOptions::quality)
.field("qualityAlpha", &AvifOptions::qualityAlpha)
.field("tileRowsLog2", &AvifOptions::tileRowsLog2)
.field("tileColsLog2", &AvifOptions::tileColsLog2)
.field("speed", &AvifOptions::speed)
@@ -140,7 +162,8 @@ EMSCRIPTEN_BINDINGS(my_module) {
.field("sharpness", &AvifOptions::sharpness)
.field("tune", &AvifOptions::tune)
.field("denoiseLevel", &AvifOptions::denoiseLevel)
.field("subsample", &AvifOptions::subsample);
.field("subsample", &AvifOptions::subsample)
.field("enableSharpYUV", &AvifOptions::enableSharpYUV);
function("encode", &encode);
}

View File

@@ -5,15 +5,16 @@ export const enum AVIFTune {
}
export interface EncodeOptions {
cqLevel: number;
quality: number;
qualityAlpha: number;
denoiseLevel: number;
cqAlphaLevel: number;
tileRowsLog2: number;
tileColsLog2: number;
speed: number;
subsample: number;
chromaDeltaQ: boolean;
sharpness: number;
enableSharpYUV: boolean;
tune: AVIFTune;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1 +1 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./avif_enc_mt.js")).then(function(exports){return exports.default(Module)}).then(function(instance){Module=instance})}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["__emscripten_thread_exit"](result)}}catch(ex){if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["__emscripten_thread_exit"](ex.status)}}else{throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}postMessage({"cmd":"cancelDone"})}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -10,8 +10,11 @@
# $(LIBAVIF_FLAGS)
# $(ENVIRONMENT)
# $(OUT_JS) is something like "enc/avif_enc.js" or "enc/avif_enc_mt.js"
# so $(OUT_BUILD_DIR) will be "node_modules/build/enc/avif_enc[_mt]"
OUT_BUILD_DIR := $(BUILD_DIR)/$(basename $(OUT_JS))
# We're making libavif and libaom for every node_modules/[enc|dec]/
CODEC_BUILD_DIR := $(OUT_BUILD_DIR)/libavif
CODEC_OUT := $(CODEC_BUILD_DIR)/libavif.a
@@ -25,6 +28,13 @@ OUT_WORKER=$(OUT_JS:.js=.worker.js)
all: $(OUT_JS)
# Only add libsharpyuv as a dependency for encoders.
# Yes, that if statement is true for encoders.
ifneq (,$(findstring enc/, $(OUT_JS)))
$(OUT_JS): $(LIBSHARPYUV)
$(CODEC_OUT): $(LIBSHARPYUV)
endif
$(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(CXX) \
-I $(CODEC_DIR)/include \
@@ -32,6 +42,7 @@ $(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(LDFLAGS) \
$(OUT_FLAGS) \
--bind \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-o $@ \
@@ -39,6 +50,7 @@ $(OUT_JS): $(OUT_CPP) $(LIBAOM_OUT) $(CODEC_OUT)
$(CODEC_OUT): $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_OUT)
emcmake cmake \
-DCMAKE_LIBRARY_PATH=$(LIBSHARPYUV_BUILD_DIR) \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=0 \
-DAVIF_CODEC_AOM=1 \

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:2.0.23
FROM emscripten/emsdk:2.0.34
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
ENV CFLAGS "-O3 -flto"
ENV CXXFLAGS "${CFLAGS} -std=c++17"

View File

@@ -1,4 +1,4 @@
import { EncodeOptions, defaultOptions, AVIFTune } from '../shared/meta';
import { EncodeOptions, AVIFTune, 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';
@@ -36,12 +36,20 @@ interface State {
effort: number;
sharpness: number;
denoiseLevel: number;
aqMode: number;
tune: AVIFTune;
enableSharpYUV: boolean;
}
const maxQuant = 63;
const maxSpeed = 10;
/**
* AVIF quality ranges from 0 (worst) to 100 (lossless).
* Since lossless is a separate checkbox, we cap user-inputted quality at 99
*
* AVIF speed ranges from 0 (slowest) to 10 (fastest).
* We display it as 'effort' to the user since it conveys the speed-size tradeoff
* much better: speed = 10 - effort
*/
const MAX_QUALITY = 100;
const MAX_EFFORT = 10;
export class Options extends Component<Props, State> {
static getDerivedStateFromProps(
@@ -55,34 +63,28 @@ export class Options extends Component<Props, State> {
const { options } = props;
const lossless =
options.cqLevel === 0 &&
options.cqAlphaLevel <= 0 &&
options.quality === MAX_QUALITY &&
(options.qualityAlpha == -1 || options.qualityAlpha == MAX_QUALITY) &&
options.subsample == 3;
const separateAlpha = options.cqAlphaLevel !== -1;
const cqLevel = lossless ? defaultOptions.cqLevel : options.cqLevel;
const separateAlpha = options.qualityAlpha !== -1;
// Create default form state from options
return {
options,
lossless,
quality: maxQuant - cqLevel,
quality: lossless ? defaultOptions.quality : options.quality,
separateAlpha,
alphaQuality:
maxQuant -
(separateAlpha ? options.cqAlphaLevel : defaultOptions.cqLevel),
subsample:
options.subsample === 0 || lossless
? defaultOptions.subsample
: options.subsample,
alphaQuality: separateAlpha ? options.qualityAlpha : options.quality,
subsample: options.subsample,
tileRows: options.tileRowsLog2,
tileCols: options.tileColsLog2,
effort: maxSpeed - options.speed,
effort: MAX_EFFORT - options.speed,
chromaDeltaQ: options.chromaDeltaQ,
sharpness: options.sharpness,
denoiseLevel: options.denoiseLevel,
tune: options.tune,
enableSharpYUV: options.enableSharpYUV,
};
}
@@ -120,20 +122,21 @@ export class Options extends Component<Props, State> {
};
const newOptions: EncodeOptions = {
cqLevel: optionState.lossless ? 0 : maxQuant - optionState.quality,
cqAlphaLevel:
quality: optionState.lossless ? MAX_QUALITY : optionState.quality,
qualityAlpha:
optionState.lossless || !optionState.separateAlpha
? -1
: maxQuant - optionState.alphaQuality,
? -1 // Set qualityAlpha to quality
: optionState.alphaQuality,
// Always set to 4:4:4 if lossless
subsample: optionState.lossless ? 3 : optionState.subsample,
tileColsLog2: optionState.tileCols,
tileRowsLog2: optionState.tileRows,
speed: maxSpeed - optionState.effort,
speed: MAX_EFFORT - optionState.effort,
chromaDeltaQ: optionState.chromaDeltaQ,
sharpness: optionState.sharpness,
denoiseLevel: optionState.denoiseLevel,
tune: optionState.tune,
enableSharpYUV: optionState.enableSharpYUV,
};
// Updating options, so we don't recalculate in getDerivedStateFromProps.
@@ -167,6 +170,7 @@ export class Options extends Component<Props, State> {
sharpness,
denoiseLevel,
tune,
enableSharpYUV,
}: State,
) {
return (
@@ -183,7 +187,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}>
<Range
min="0"
max="63"
max={MAX_QUALITY - 1} // MAX_QUALITY would mean lossless
value={quality}
onInput={this._inputChange('quality', 'number')}
>
@@ -211,11 +215,26 @@ export class Options extends Component<Props, State> {
value={subsample}
onChange={this._inputChange('subsample', 'number')}
>
<option value="1">Half</option>
{/*<option value="2">4:2:2</option>*/}
<option value="3">Off</option>
<option value="0">4:0:0</option>
<option value="1">4:2:0</option>
<option value="2">4:2:2</option>
<option value="3">4:4:4</option>
</Select>
</label>
<Expander>
{subsample === 1 && (
<label class={style.optionToggle}>
Sharp YUV Downsampling
<Checkbox
checked={enableSharpYUV}
onChange={this._inputChange(
'enableSharpYUV',
'boolean',
)}
/>
</label>
)}
</Expander>
<label class={style.optionToggle}>
Separate alpha quality
<Checkbox
@@ -228,7 +247,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}>
<Range
min="0"
max="63"
max={MAX_QUALITY - 1} // MAX_QUALITY would mean lossless
value={alphaQuality}
onInput={this._inputChange(
'alphaQuality',
@@ -307,7 +326,7 @@ export class Options extends Component<Props, State> {
<div class={style.optionOneCell}>
<Range
min="0"
max="10"
max={MAX_EFFORT}
value={effort}
onInput={this._inputChange('effort', 'number')}
>

View File

@@ -18,8 +18,8 @@ export const label = 'AVIF';
export const mimeType = 'image/avif';
export const extension = 'avif';
export const defaultOptions: EncodeOptions = {
cqLevel: 33,
cqAlphaLevel: -1,
quality: 50,
qualityAlpha: -1,
denoiseLevel: 0,
tileColsLog2: 0,
tileRowsLog2: 0,
@@ -28,4 +28,5 @@ export const defaultOptions: EncodeOptions = {
chromaDeltaQ: false,
sharpness: 0,
tune: AVIFTune.auto,
enableSharpYUV: false,
};