diff --git a/packages/avif/codec/Makefile b/packages/avif/codec/Makefile index bc0f148..3a7a79c 100644 --- a/packages/avif/codec/Makefile +++ b/packages/avif/codec/Makefile @@ -1,9 +1,9 @@ -# libavif and libaom versions are from -# https://docs.google.com/document/d/1wEEA5rRU7wT54k41u3qyZIZHDCJArIMzLuzsrLAwaK8/edit -CODEC_URL = https://github.com/AOMediaCodec/libavif/archive/1c39e772c2c0d687691dd4b589a12c323f5f767d.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.1.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_ENC_MT_JS = enc/avif_enc_mt.js @@ -27,7 +32,8 @@ HELPER_MAKEFLAGS := -f helper.Makefile all: $(OUT_ENC_JS) $(OUT_DEC_JS) $(OUT_ENC_MT_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=$@ \ @@ -38,9 +44,10 @@ $(OUT_ENC_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLis -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_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=$@ \ @@ -50,9 +57,10 @@ $(OUT_ENC_MT_JS): $(OUT_ENC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMake -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" +# Decoding $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLists.txt $(MAKE) \ $(HELPER_MAKEFLAGS) \ @@ -65,22 +73,56 @@ $(OUT_DEC_JS): $(OUT_DEC_CPP) $(CODEC_DIR)/CMakeLists.txt $(LIBAOM_DIR)/CMakeLis 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 diff --git a/packages/avif/codec/enc/avif_enc.cpp b/packages/avif/codec/enc/avif_enc.cpp index e7f63b4..fa88a60 100644 --- a/packages/avif/codec/enc/avif_enc.cpp +++ b/packages/avif/codec/enc/avif_enc.cpp @@ -3,15 +3,27 @@ #include #include "avif/avif.h" +#include +#include + +#define RETURN_NULL_IF(expression) \ + do { \ + if (expression) \ + return val::null(); \ + } while (false) + using namespace emscripten; +using AvifImagePtr = std::unique_ptr; +using AvifEncoderPtr = std::unique_ptr; + 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(const_cast(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()); - - if (options.cqAlphaLevel != -1) { - avifEncoderSetCodecSpecificOption(encoder, "alpha:cq-level", - std::to_string(options.cqAlphaLevel).c_str()); + status = avifEncoderSetCodecSpecificOption(encoder.get(), "sharpness", + std::to_string(options.sharpness).c_str()); + RETURN_NULL_IF(status != AVIF_RESULT_OK); + + // 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") - .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); } diff --git a/packages/avif/codec/helper.Makefile b/packages/avif/codec/helper.Makefile index ad305ad..8f946e2 100644 --- a/packages/avif/codec/helper.Makefile +++ b/packages/avif/codec/helper.Makefile @@ -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 \ -s DYNAMIC_EXECUTION=0 \ @@ -41,6 +52,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 \ @@ -72,4 +84,4 @@ $(LIBAOM_OUT): $(LIBAOM_DIR)/CMakeLists.txt clean: $(RM) $(OUT_JS) $(OUT_WASM) $(OUT_WORKER) $(MAKE) -C $(CODEC_BUILD_DIR) clean - $(MAKE) -C $(LIBAOM_BUILD_DIR) clean + $(MAKE) -C $(LIBAOM_BUILD_DIR) clean \ No newline at end of file diff --git a/packages/avif/meta.ts b/packages/avif/meta.ts index da63cac..487d169 100644 --- a/packages/avif/meta.ts +++ b/packages/avif/meta.ts @@ -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, diff --git a/tools/cpp.dockerfile b/tools/cpp.dockerfile index ca07076..0208322 100644 --- a/tools/cpp.dockerfile +++ b/tools/cpp.dockerfile @@ -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"