From fc56e0801fabc19d514d8c877050d0feb00b7de5 Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 1 Jun 2026 15:22:59 -0400 Subject: [PATCH 1/3] Add AVIF image support via libavif Add an AVIF codec (LLImageAVIF) to llimage backed by libavif with dav1d (decode) and aom (encode), and wire it into the image upload and save/snapshot paths, mirroring the existing WebP integration. - llimage: IMG_CODEC_AVIF and LLImageAVIF (lossy quality-based encode, 8-bit RGB/RGBA decode with vertical flip); extension, MIME, and dimensions-info registration - build: libavif[aom,dav1d] vcpkg dependency and AVIF.cmake module - upload: file picker load filters, IMAGE_EXTENSIONS, local bitmaps - save: FFSAVE_AVIF, snapshot format (combo box, encode switches, quality slider) and texture save-as Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build.yaml | 4 +- indra/CMakeLists.txt | 1 + indra/cmake/AVIF.cmake | 6 + indra/cmake/CMakeLists.txt | 1 + indra/llimage/CMakeLists.txt | 3 + indra/llimage/llimage.cpp | 14 +- indra/llimage/llimage.h | 3 +- indra/llimage/llimageavif.cpp | 302 ++++++++++++++++++ indra/llimage/llimageavif.h | 51 +++ indra/llimage/llimagedimensionsinfo.cpp | 44 +++ indra/llimage/llimagedimensionsinfo.h | 1 + indra/newview/llfilepicker.cpp | 33 +- indra/newview/llfilepicker.h | 1 + indra/newview/lllocalbitmaps.cpp | 16 + indra/newview/lllocalbitmaps.h | 3 +- indra/newview/llpanelsnapshotlocal.cpp | 6 +- indra/newview/llpreviewtexture.cpp | 5 + indra/newview/llsnapshotlivepreview.cpp | 7 + indra/newview/llsnapshotmodel.h | 3 +- indra/newview/llviewermenufile.cpp | 6 +- indra/newview/llviewerwindow.cpp | 9 + .../default/xui/en/panel_snapshot_local.xml | 4 + .../newview/skins/default/xui/en/strings.xml | 3 +- indra/vcpkg.json | 7 + 24 files changed, 519 insertions(+), 14 deletions(-) create mode 100644 indra/cmake/AVIF.cmake create mode 100644 indra/llimage/llimageavif.cpp create mode 100644 indra/llimage/llimageavif.h diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8888bdf5615..fc07ad4965e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -215,7 +215,7 @@ jobs: libthai-dev libtool libudev-dev libunwind-dev liburing-dev libvlc-dev libwayland-dev \ libx11-dev libxcursor-dev libxext-dev libxfixes-dev libxft-dev libxi-dev libxinerama-dev \ libxkbcommon-dev libxrandr-dev libxss-dev libxtst-dev linux-libc-dev mono-complete \ - ninja-build pkgconf tar tex-common texinfo unzip zip + nasm ninja-build pkgconf tar tex-common texinfo unzip zip sudo locale-gen en_US.UTF-8 sudo locale-gen en_GB.UTF-8 @@ -232,7 +232,7 @@ jobs: - name: macOS Homebrew Dependency Install if: runner.os == 'macOS' run: | - brew install autoconf autoconf-archive automake libtool mono unzip zip + brew install autoconf autoconf-archive automake libtool mono nasm unzip zip - name: Checkout code uses: actions/checkout@v6 diff --git a/indra/CMakeLists.txt b/indra/CMakeLists.txt index 1fafbe0c554..e193032dfa7 100644 --- a/indra/CMakeLists.txt +++ b/indra/CMakeLists.txt @@ -243,6 +243,7 @@ endif () # Declare All Dependency Includes include(APR) +include(AVIF) include(Boost) include(bugsplat) include(CEFPlugin) diff --git a/indra/cmake/AVIF.cmake b/indra/cmake/AVIF.cmake new file mode 100644 index 00000000000..5da0ca907aa --- /dev/null +++ b/indra/cmake/AVIF.cmake @@ -0,0 +1,6 @@ +# -*- cmake -*- +include_guard() +add_library(ll::libavif INTERFACE IMPORTED) + +find_package(libavif CONFIG REQUIRED) +target_link_libraries(ll::libavif INTERFACE avif) diff --git a/indra/cmake/CMakeLists.txt b/indra/cmake/CMakeLists.txt index b7670c28a05..f894cf3d487 100644 --- a/indra/cmake/CMakeLists.txt +++ b/indra/cmake/CMakeLists.txt @@ -3,6 +3,7 @@ set(cmake_SOURCE_FILES 00-Common.cmake APR.cmake + AVIF.cmake Boost.cmake BootstrapVcpkg.cmake bugsplat.cmake diff --git a/indra/llimage/CMakeLists.txt b/indra/llimage/CMakeLists.txt index 293dde6cb9c..b397cf829bf 100644 --- a/indra/llimage/CMakeLists.txt +++ b/indra/llimage/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(llimage llimagepng.cpp llimagetga.cpp llimagewebp.cpp + llimageavif.cpp llimageworker.cpp llpngwrapper.cpp ) @@ -32,6 +33,7 @@ target_sources(llimage llimagepng.h llimagetga.h llimagewebp.h + llimageavif.h llimageworker.h llmapimagetype.h llpngwrapper.h @@ -52,6 +54,7 @@ target_link_libraries(llimage llmath llcommon ll::libwebp + ll::libavif ll::libpng ll::libjpeg ) diff --git a/indra/llimage/llimage.cpp b/indra/llimage/llimage.cpp index fc8b3f0f8f2..4a52043c637 100644 --- a/indra/llimage/llimage.cpp +++ b/indra/llimage/llimage.cpp @@ -39,6 +39,7 @@ #include "llimagejpeg.h" #include "llimagepng.h" #include "llimagewebp.h" +#include "llimageavif.h" #include "llimagedxt.h" #include "llmemory.h" @@ -2049,7 +2050,8 @@ file_extensions[] = { "mip", IMG_CODEC_DXT }, { "dxt", IMG_CODEC_DXT }, { "png", IMG_CODEC_PNG }, - { "webp", IMG_CODEC_WEBP } + { "webp", IMG_CODEC_WEBP }, + { "avif", IMG_CODEC_AVIF } }; static struct @@ -2069,7 +2071,8 @@ wide_file_extensions[] = { L"mip", IMG_CODEC_DXT }, { L"dxt", IMG_CODEC_DXT }, { L"png", IMG_CODEC_PNG }, - { L"webp", IMG_CODEC_WEBP } + { L"webp", IMG_CODEC_WEBP }, + { L"avif", IMG_CODEC_AVIF } }; #define NUM_FILE_EXTENSIONS LL_ARRAY_SIZE(file_extensions) #if 0 @@ -2270,6 +2273,9 @@ LLImageFormatted* LLImageFormatted::createFromType(S8 codec) case IMG_CODEC_WEBP: image = new LLImageWebP(); break; + case IMG_CODEC_AVIF: + image = new LLImageAVIF(); + break; case IMG_CODEC_J2C: image = new LLImageJ2C(); break; @@ -2314,6 +2320,10 @@ S8 LLImageFormatted::getCodecFromMimeType(std::string_view mimetype) { return IMG_CODEC_WEBP; } + else if (mimetype == "image/avif") + { + return IMG_CODEC_AVIF; + } return IMG_CODEC_INVALID; } diff --git a/indra/llimage/llimage.h b/indra/llimage/llimage.h index 46e78c2e195..4515efb5fb5 100644 --- a/indra/llimage/llimage.h +++ b/indra/llimage/llimage.h @@ -85,7 +85,8 @@ typedef enum e_image_codec IMG_CODEC_DXT = 6, IMG_CODEC_PNG = 7, IMG_CODEC_WEBP = 8, - IMG_CODEC_EOF = 9 + IMG_CODEC_AVIF = 9, + IMG_CODEC_EOF = 10 } EImageCodec; //============================================================================ diff --git a/indra/llimage/llimageavif.cpp b/indra/llimage/llimageavif.cpp new file mode 100644 index 00000000000..335003ba329 --- /dev/null +++ b/indra/llimage/llimageavif.cpp @@ -0,0 +1,302 @@ +/* + * @file llimageavif.cpp + * @brief LLImageFormatted glue to encode / decode AVIF files. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) Rye Mutt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * $/LicenseInfo$ + */ + +#include "linden_common.h" +#include "stdtypes.h" +#include "llerror.h" + +#include "llimage.h" +#include "llimageavif.h" + +#include + +#include +#include + +// libavif has no built-in vertical flip, but Second Life raw images are +// stored bottom-up (OpenGL convention), so we flip rows by hand. +namespace +{ + void flip_rows_in_place(U8* data, U32 height, size_t stride) + { + if (height < 2) + { + return; + } + std::vector row_tmp(stride); + for (U32 i = 0; i < height / 2; ++i) + { + U8* top = data + (size_t)i * stride; + U8* bottom = data + (size_t)(height - 1 - i) * stride; + memcpy(row_tmp.data(), top, stride); + memcpy(top, bottom, stride); + memcpy(bottom, row_tmp.data(), stride); + } + } +} + +// --------------------------------------------------------------------------- +// LLImageAVIF +// --------------------------------------------------------------------------- +LLImageAVIF::LLImageAVIF(S32 quality) + : LLImageFormatted(IMG_CODEC_AVIF) + , mEncodeQuality(quality) +{ +} + +// Virtual +// Parse AVIF image information and set the appropriate +// width, height and component (channel) information. +bool LLImageAVIF::updateData() +{ + resetLastError(); + + LLImageDataLock lock(this); + + // Check to make sure that this instance has been initialized with data + if (isBufferInvalid() || (0 == getDataSize())) + { + setLastError("Uninitialized instance of LLImageAVIF"); + return false; + } + + avifROData raw = { getData(), (size_t)getDataSize() }; + if (!avifPeekCompatibleFileType(&raw)) + { + setLastError("LLImageAVIF data does not have a valid AVIF header!"); + return false; + } + + avifDecoder* decoder = avifDecoderCreate(); + if (!decoder) + { + setLastError("LLImageAVIF could not create decoder"); + return false; + } + + bool success = false; + if (avifDecoderSetIOMemory(decoder, getData(), getDataSize()) == AVIF_RESULT_OK + && avifDecoderParse(decoder) == AVIF_RESULT_OK) + { + setSize(decoder->image->width, decoder->image->height, decoder->alphaPresent ? 4 : 3); + success = true; + } + else + { + setLastError("LLImageAVIF failed to parse AVIF header"); + } + + avifDecoderDestroy(decoder); + return success; +} + +// Virtual +// Decode an in-memory AVIF image into the raw RGB or RGBA format +// used within SecondLife. +bool LLImageAVIF::decode(LLImageRaw* raw_image, F32 decode_time) +{ + llassert_always(raw_image); + + LLImageDataSharedLock lockIn(this); + LLImageDataLock lockOut(raw_image); + + resetLastError(); + + // Check to make sure that this instance has been initialized with data + if (isBufferInvalid() || (0 == getDataSize())) + { + setLastError("LLImageAVIF trying to decode an image with no data!"); + return false; + } + + avifDecoder* decoder = avifDecoderCreate(); + if (!decoder) + { + setLastError("LLImageAVIF could not create decoder"); + return false; + } + + // RAII-ish cleanup for the decoder along every early-out below. + struct DecoderGuard + { + avifDecoder* d; + ~DecoderGuard() { if (d) avifDecoderDestroy(d); } + } guard{ decoder }; + + if (avifDecoderSetIOMemory(decoder, getData(), getDataSize()) != AVIF_RESULT_OK) + { + setLastError("LLImageAVIF failed to set decoder input"); + return false; + } + + if (avifDecoderParse(decoder) != AVIF_RESULT_OK) + { + setLastError("LLImageAVIF data does not have a valid AVIF header!"); + return false; + } + + if (avifDecoderNextImage(decoder) != AVIF_RESULT_OK) + { + setLastError("LLImageAVIF failed to decode AVIF image"); + return false; + } + + const bool has_alpha = decoder->alphaPresent; + + setSize(decoder->image->width, decoder->image->height, has_alpha ? 4 : 3); + + if (!raw_image->resize(getWidth(), getHeight(), getComponents())) + { + setLastError("LLImageAVIF failed to resize raw image output buffer"); + return false; + } + + // Convert the decoded YUV image straight into the raw image buffer as + // 8-bit RGB/RGBA. Higher bit depths / HDR are down-converted to 8-bit sRGB. + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, decoder->image); + rgb.format = has_alpha ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + rgb.depth = 8; + rgb.pixels = (uint8_t*)raw_image->getData(); + rgb.rowBytes = (uint32_t)(raw_image->getWidth() * raw_image->getComponents()); + + if (avifImageYUVToRGB(decoder->image, &rgb) != AVIF_RESULT_OK) + { + setLastError("LLImageAVIF failed to convert AVIF image to RGB"); + return false; + } + + // Flip the image because SL is opengl + flip_rows_in_place(raw_image->getData(), getHeight(), rgb.rowBytes); + + return true; +} + +// Virtual +// Encode the in memory RGB image into AVIF format. +bool LLImageAVIF::encode(const LLImageRaw* raw_image, F32 encode_time) +{ + llassert_always(raw_image); + + resetLastError(); + + LLImageDataSharedLock lockIn(raw_image); + LLImageDataLock lockOut(this); + + if (raw_image->isBufferInvalid() || (0 == raw_image->getDataSize())) + { + setLastError("LLImageAVIF trying to encode an image with no data!"); + return false; + } + + const U8* datap = raw_image->getData(); + const U32 height = raw_image->getHeight(); + const U32 width = raw_image->getWidth(); + const S8 components = raw_image->getComponents(); + const size_t stride = (size_t)width * components; + + setSize(width, height, components); + + // Flip image vertically into a temporary buffer for encode (SL is opengl). + std::unique_ptr tmp_buff; + try + { + tmp_buff = std::make_unique(height * stride); + } + catch (const std::bad_alloc&) + { + setLastError("LLImageAVIF::out of memory"); + return false; + } + + for (U32 i = 0; i < height; ++i) + { + const U8* row = &datap[(size_t)(height - 1 - i) * stride]; + memcpy(tmp_buff.get() + (size_t)i * stride, row, stride); + } + + avifImage* image = avifImageCreate(width, height, 8, AVIF_PIXEL_FORMAT_YUV420); + if (!image) + { + setLastError("LLImageAVIF::Failed to allocate image"); + return false; + } + + // Tag the bitstream as sRGB so decoders reproduce our 8-bit sRGB content. + image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, image); + rgb.format = (components == 4) ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + rgb.depth = 8; + rgb.pixels = tmp_buff.get(); + rgb.rowBytes = (uint32_t)stride; + + if (avifImageRGBToYUV(image, &rgb) != AVIF_RESULT_OK) + { + setLastError("LLImageAVIF::Failed to convert image to YUV"); + avifImageDestroy(image); + return false; + } + + avifEncoder* encoder = avifEncoderCreate(); + if (!encoder) + { + setLastError("LLImageAVIF::Failed to create encoder"); + avifImageDestroy(image); + return false; + } + + encoder->quality = mEncodeQuality; + encoder->qualityAlpha = mEncodeQuality; + encoder->speed = 6; // balance encode time against quality for snapshots + + avifRWData output = AVIF_DATA_EMPTY; + const avifResult result = avifEncoderWrite(encoder, image, &output); + + avifEncoderDestroy(encoder); + avifImageDestroy(image); + + if (result != AVIF_RESULT_OK || output.data == nullptr || output.size == 0) + { + setLastError("LLImageAVIF::Failed to encode image"); + avifRWDataFree(&output); + return false; + } + + if (!allocateData(narrow(output.size))) + { + setLastError("LLImageAVIF::Failed to allocate final buffer for image"); + avifRWDataFree(&output); + return false; + } + + memcpy(getData(), output.data, output.size); + avifRWDataFree(&output); + + return true; +} diff --git a/indra/llimage/llimageavif.h b/indra/llimage/llimageavif.h new file mode 100644 index 00000000000..9727267a5e9 --- /dev/null +++ b/indra/llimage/llimageavif.h @@ -0,0 +1,51 @@ +/* + * @file llimageavif.h + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) Rye Mutt + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * $/LicenseInfo$ + */ + +#ifndef AL_ALIMAGEAVIF_H +#define AL_ALIMAGEAVIF_H + +#include "stdtypes.h" +#include "llimage.h" + +class LLImageAVIF final : public LLImageFormatted +{ +protected: + ~LLImageAVIF() = default; + +public: + LLImageAVIF(S32 quality = 100); + + std::string getExtension() override { return std::string("avif"); } + bool updateData() override; + bool decode(LLImageRaw* raw_image, F32 decode_time) override; + bool encode(const LLImageRaw* raw_image, F32 encode_time) override; + + void setEncodeQuality(S32 q) { mEncodeQuality = q; } // 0 (worst) - 100 (lossless) + S32 getEncodeQuality() const { return mEncodeQuality; } + +protected: + S32 mEncodeQuality; // AVIF quality scale, 0 (worst) - 100 (lossless) +}; + +#endif diff --git a/indra/llimage/llimagedimensionsinfo.cpp b/indra/llimage/llimagedimensionsinfo.cpp index 3ad911f986b..5f2399fda37 100644 --- a/indra/llimage/llimagedimensionsinfo.cpp +++ b/indra/llimage/llimagedimensionsinfo.cpp @@ -31,6 +31,7 @@ #include "llimagedimensionsinfo.h" #include +#include // Value is true if one of Libjpeg's functions has encountered an error while working. static bool sJpegErrorEncountered = false; @@ -70,6 +71,8 @@ bool LLImageDimensionsInfo::load(const std::string& src_filename,U32 codec) return getImageDimensionsPng(); case IMG_CODEC_WEBP: return getImageDimensionsWebP(); + case IMG_CODEC_AVIF: + return getImageDimensionsAVIF(); default: return false; @@ -197,6 +200,47 @@ bool LLImageDimensionsInfo::getImageDimensionsWebP() return false; } +bool LLImageDimensionsInfo::getImageDimensionsAVIF() +{ + auto image_size = LLFile::size(mSrcFilename); + if (image_size > 0) + { + auto image_buf = std::make_unique(image_size); + + std::error_code ec; + mInfile.seek(0, LLFile::beg, ec); + mInfile.read(image_buf.get(), image_size, ec); + if (ec) + { + return false; + } + + avifDecoder* decoder = avifDecoderCreate(); + if (!decoder) + { + return false; + } + + bool success = false; + // Parsing alone (no frame decode) is enough to read the dimensions. + if (avifDecoderSetIOMemory(decoder, image_buf.get(), image_size) == AVIF_RESULT_OK + && avifDecoderParse(decoder) == AVIF_RESULT_OK) + { + mWidth = decoder->image->width; + mHeight = decoder->image->height; + success = true; + } + else + { + LL_WARNS() << "Not an AVIF" << LL_ENDL; + } + + avifDecoderDestroy(decoder); + return success; + } + return false; +} + // Called instead of exit() if Libjpeg encounters an error. void on_jpeg_error(j_common_ptr cinfo) { diff --git a/indra/llimage/llimagedimensionsinfo.h b/indra/llimage/llimagedimensionsinfo.h index 2fbc28bfea9..9c9e6a721eb 100644 --- a/indra/llimage/llimagedimensionsinfo.h +++ b/indra/llimage/llimagedimensionsinfo.h @@ -90,6 +90,7 @@ class LLImageDimensionsInfo bool getImageDimensionsPng(); bool getImageDimensionsJpeg(); bool getImageDimensionsWebP(); + bool getImageDimensionsAVIF(); S32 read_s32() { diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index 45f235eac96..d270364a451 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -53,7 +53,7 @@ LLFilePicker LLFilePicker::sInstance; #if LL_WINDOWS && !LL_SDL_WINDOW #define SOUND_FILTER L"Sounds (*.wav)\0*.wav\0" -#define IMAGE_FILTER L"Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp)\0*.tga;*.bmp;*.jpg;*.jpeg;*.png;*.webp\0" +#define IMAGE_FILTER L"Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp; *.avif)\0*.tga;*.bmp;*.jpg;*.jpeg;*.png;*.webp;*.avif\0" #define ANIM_FILTER L"Animations (*.bvh; *.anim)\0*.bvh;*.anim\0" #define COLLADA_FILTER L"Scene (*.dae)\0*.dae\0" #define GLTF_FILTER L"glTF (*.gltf; *.glb)\0*.gltf;*.glb\0" @@ -191,7 +191,7 @@ namespace filter_vec.push_back({ "Sounds (*.wav)", "wav" }); break; case LLFilePicker::FFLOAD_IMAGE: - filter_vec.push_back({ "Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp)", "tga;bmp;jpg;jpeg;png;webp" }); + filter_vec.push_back({ "Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp; *.avif)", "tga;bmp;jpg;jpeg;png;webp;avif" }); break; case LLFilePicker::FFLOAD_ANIM: filter_vec.push_back({ "Animations (*.bvh; *.anim)", "bvh;anim" }); @@ -222,7 +222,7 @@ namespace case LLFilePicker::FFLOAD_MATERIAL_TEXTURE: filter_vec.push_back({ "GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)", "gltf;glb;tga;bmp;jpg;jpeg;png" }); filter_vec.push_back({ "GLTF Files (*.gltf; *.glb)", "gltf;glb" }); - filter_vec.push_back({ "Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp)", "tga;bmp;jpg;jpeg;png;webp" }); + filter_vec.push_back({ "Images (*.tga; *.bmp; *.jpg; *.jpeg; *.png; *.webp; *.avif)", "tga;bmp;jpg;jpeg;png;webp;avif" }); break; case LLFilePicker::FFLOAD_HDRI: filter_vec.push_back({ "HDRI Files (*.exr)", "exr" }); @@ -468,6 +468,13 @@ bool LLFilePicker::getSaveFileModeless(ESaveFilter filter, } file_filters.push_back({ "WebP Images (*.webp)", "webp" }); break; + case FFSAVE_AVIF: + if (default_filename.empty()) + { + default_filename = "untitled.avif"; + } + file_filters.push_back({ "AVIF Images (*.avif)", "avif" }); + break; case FFSAVE_TGAPNG: if (default_filename.empty()) { @@ -477,6 +484,7 @@ bool LLFilePicker::getSaveFileModeless(ESaveFilter filter, file_filters.push_back({ "PNG Images (*.png)", "png" }); file_filters.push_back({ "Targa Images (*.tga)", "tga" }); file_filters.push_back({ "WebP Images (*.webp)", "webp" }); + file_filters.push_back({ "AVIF Images (*.avif)", "avif" }); break; case FFSAVE_JPEG: @@ -877,6 +885,16 @@ bool LLFilePicker::getSaveFile(ESaveFilter filter, const std::string& filename, L"WebP Images (*.webp)\0*.webp\0" \ L"\0"; break; + case FFSAVE_AVIF: + if (filename.empty()) + { + wcsncpy(mFilesW, L"untitled.avif", FILENAME_BUFFER_SIZE); /*Flawfinder: ignore*/ + } + mOFN.lpstrDefExt = L"avif"; + mOFN.lpstrFilter = + L"AVIF Images (*.avif)\0*.avif\0" \ + L"\0"; + break; case FFSAVE_TGAPNG: if (filename.empty()) { @@ -888,6 +906,7 @@ bool LLFilePicker::getSaveFile(ESaveFilter filter, const std::string& filename, L"PNG Images (*.png)\0*.png\0" \ L"Targa Images (*.tga)\0*.tga\0" \ L"WebP Images (*.webp)\0*.webp\0" \ + L"AVIF Images (*.avif)\0*.avif\0" \ L"\0"; break; @@ -1075,6 +1094,7 @@ std::unique_ptr> LLFilePicker::navOpenFilterProc(ELoadF allowedv->push_back("tpic"); allowedv->push_back("png"); allowedv->push_back("webp"); + allowedv->push_back("avif"); break; case FFLOAD_WAV: allowedv->push_back("wav"); @@ -1188,7 +1208,7 @@ void set_nav_save_data(LLFilePicker::ESaveFilter filter, std::string &extension, case LLFilePicker::FFSAVE_TGAPNG: type = "PNG"; creator = "prvw"; - extension = "png,tga,webp"; + extension = "png,tga,webp,avif"; break; case LLFilePicker::FFSAVE_BMP: type = "BMPf"; @@ -1210,6 +1230,11 @@ void set_nav_save_data(LLFilePicker::ESaveFilter filter, std::string &extension, type = "WEBP"; creator = "prvw"; break; + case LLFilePicker::FFSAVE_AVIF: + extension = "avif"; + type = "AVIF"; + creator = "prvw"; + break; case LLFilePicker::FFSAVE_AVI: type = "\?\?\?\?"; creator = "\?\?\?\?"; diff --git a/indra/newview/llfilepicker.h b/indra/newview/llfilepicker.h index 7c50fe3b103..f2e2bdce3cf 100644 --- a/indra/newview/llfilepicker.h +++ b/indra/newview/llfilepicker.h @@ -100,6 +100,7 @@ class LLFilePicker FFSAVE_JPEG = 14, FFSAVE_SCRIPT = 15, FFSAVE_WEBP, + FFSAVE_AVIF, FFSAVE_CSV, FFSAVE_TGAPNG diff --git a/indra/newview/lllocalbitmaps.cpp b/indra/newview/lllocalbitmaps.cpp index 6e72443dcca..8bad6e62327 100644 --- a/indra/newview/lllocalbitmaps.cpp +++ b/indra/newview/lllocalbitmaps.cpp @@ -38,6 +38,7 @@ #include "llimagejpeg.h" #include "llimagepng.h" #include "llimagewebp.h" +#include "llimageavif.h" /* misc headers */ #include "fsyspath.h" @@ -113,6 +114,10 @@ LLLocalBitmap::LLLocalBitmap(std::string filename) { mExtension = ET_IMG_WEBP; } + else if (temp_exten == "avif") + { + mExtension = ET_IMG_AVIF; + } else { LL_WARNS() << "File of no valid extension given, local bitmap creation aborted." << "\n" @@ -390,6 +395,17 @@ bool LLLocalBitmap::decodeBitmap(LLPointer rawimg) break; } + case ET_IMG_AVIF: + { + LLPointer avif_image = new LLImageAVIF; + if (avif_image->load(mFilename) && avif_image->decode(rawimg, 0.0f)) + { + rawimg->biasedScaleToPowerOfTwo(LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT); + decode_successful = true; + } + break; + } + default: { // separating this into -several- LL_WARNS() calls because in the extremely unlikely case that this happens diff --git a/indra/newview/lllocalbitmaps.h b/indra/newview/lllocalbitmaps.h index f997f2be97f..cd526e50cb9 100644 --- a/indra/newview/lllocalbitmaps.h +++ b/indra/newview/lllocalbitmaps.h @@ -92,7 +92,8 @@ class LLLocalBitmap ET_IMG_JPG, ET_IMG_J2C, ET_IMG_PNG, - ET_IMG_WEBP + ET_IMG_WEBP, + ET_IMG_AVIF }; private: /* members */ diff --git a/indra/newview/llpanelsnapshotlocal.cpp b/indra/newview/llpanelsnapshotlocal.cpp index dcca539d4bb..b087888e50a 100644 --- a/indra/newview/llpanelsnapshotlocal.cpp +++ b/indra/newview/llpanelsnapshotlocal.cpp @@ -121,6 +121,10 @@ LLSnapshotModel::ESnapshotFormat LLPanelSnapshotLocal::getImageFormat() const { fmt = LLSnapshotModel::SNAPSHOT_FORMAT_WEBP; } + else if (id == "AVIF") + { + fmt = LLSnapshotModel::SNAPSHOT_FORMAT_AVIF; + } return fmt; } @@ -132,7 +136,7 @@ void LLPanelSnapshotLocal::updateControls(const LLSD& info) (LLSnapshotModel::ESnapshotFormat) gSavedSettings.getS32("SnapshotFormat"); getChild("local_format_combo")->selectNthItem((S32) fmt); - const bool show_quality_ctrls = (fmt == LLSnapshotModel::SNAPSHOT_FORMAT_JPEG); + const bool show_quality_ctrls = (fmt == LLSnapshotModel::SNAPSHOT_FORMAT_JPEG || fmt == LLSnapshotModel::SNAPSHOT_FORMAT_AVIF); getChild("image_quality_slider")->setVisible(show_quality_ctrls); getChild("image_quality_level")->setVisible(show_quality_ctrls); diff --git a/indra/newview/llpreviewtexture.cpp b/indra/newview/llpreviewtexture.cpp index 4bfdcde612a..e28103721b3 100644 --- a/indra/newview/llpreviewtexture.cpp +++ b/indra/newview/llpreviewtexture.cpp @@ -41,6 +41,7 @@ #include "llimagetga.h" #include "llimagepng.h" #include "llimagewebp.h" +#include "llimageavif.h" #include "llinventory.h" #include "llinventorymodel.h" #include "llnotificationsutil.h" @@ -520,6 +521,10 @@ void LLPreviewTexture::onFileLoadedForSave(bool success, { image = new LLImageWebP; } + else if (extension == "avif") + { + image = new LLImageAVIF; + } if( image && !image->encode( src, 0 ) ) { diff --git a/indra/newview/llsnapshotlivepreview.cpp b/indra/newview/llsnapshotlivepreview.cpp index 3ad1f786bce..4df35ac0177 100644 --- a/indra/newview/llsnapshotlivepreview.cpp +++ b/indra/newview/llsnapshotlivepreview.cpp @@ -42,6 +42,7 @@ #include "llimagejpeg.h" #include "llimagepng.h" #include "llimagewebp.h" +#include "llimageavif.h" #include "lllandmarkactions.h" #include "lllocalcliprect.h" #include "llresmgr.h" @@ -948,6 +949,9 @@ void LLSnapshotLivePreview::estimateDataSize() case LLSnapshotModel::SNAPSHOT_FORMAT_WEBP: ratio = 4.0; // Average observed WebP compression ratio break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: + ratio = 8.0; // Lossy AVIF compresses substantially better + break; } } mDataSize = (S32)((F32)mPreviewImage->getDataSize() / ratio); @@ -990,6 +994,9 @@ LLPointer LLSnapshotLivePreview::getFormattedImage() case LLSnapshotModel::SNAPSHOT_FORMAT_WEBP: mFormattedImage = new LLImageWebP(); break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: + mFormattedImage = new LLImageAVIF(mSnapshotQuality); + break; } if (mFormattedImage->encode(mPreviewImage, 0)) { diff --git a/indra/newview/llsnapshotmodel.h b/indra/newview/llsnapshotmodel.h index 6e2858ff8aa..2ea9a08ca4f 100644 --- a/indra/newview/llsnapshotmodel.h +++ b/indra/newview/llsnapshotmodel.h @@ -43,7 +43,8 @@ class LLSnapshotModel SNAPSHOT_FORMAT_PNG, SNAPSHOT_FORMAT_JPEG, SNAPSHOT_FORMAT_BMP, - SNAPSHOT_FORMAT_WEBP + SNAPSHOT_FORMAT_WEBP, + SNAPSHOT_FORMAT_AVIF } ESnapshotFormat; typedef enum diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp index 81bf0a44384..0c5e2139518 100644 --- a/indra/newview/llviewermenufile.cpp +++ b/indra/newview/llviewermenufile.cpp @@ -48,6 +48,7 @@ #include "llimagejpeg.h" #include "llimagetga.h" #include "llimagewebp.h" +#include "llimageavif.h" #include "llinventorymodel.h" // gInventory #include "llpluginclassmedia.h" #include "llresourcedata.h" @@ -380,7 +381,7 @@ void LLMediaFilePicker::notify(const std::vector& filenames) #if LL_WINDOWS static std::string SOUND_EXTENSIONS = "wav"; -static std::string IMAGE_EXTENSIONS = "tga bmp jpg jpeg png webp"; +static std::string IMAGE_EXTENSIONS = "tga bmp jpg jpeg png webp avif"; static std::string ANIM_EXTENSIONS = "bvh anim"; static std::string XML_EXTENSIONS = "xml"; static std::string SLOBJECT_EXTENSIONS = "slobject"; @@ -1104,6 +1105,9 @@ class LLFileTakeSnapshotToDisk : public view_listener_t case LLSnapshotModel::SNAPSHOT_FORMAT_WEBP: formatted = new LLImageWebP; break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: + formatted = new LLImageAVIF(gSavedSettings.getS32("SnapshotQuality")); + break; } formatted->enableOverSize() ; formatted->encode(raw, 0); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index a30c13eb906..35202bb0c81 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -1422,6 +1422,7 @@ static dragdrop_type_lookup_t s_DragDropTypesLookup[] = { std::make_tuple("png", LLAssetType::AT_TEXTURE, LLFilePicker::FFLOAD_IMAGE), std::make_tuple("tga", LLAssetType::AT_TEXTURE, LLFilePicker::FFLOAD_IMAGE), std::make_tuple("webp", LLAssetType::AT_TEXTURE, LLFilePicker::FFLOAD_IMAGE), + std::make_tuple("avif", LLAssetType::AT_TEXTURE, LLFilePicker::FFLOAD_IMAGE), std::make_tuple("bvh", LLAssetType::AT_ANIMATION, LLFilePicker::FFLOAD_ANIM), std::make_tuple("anim", LLAssetType::AT_ANIMATION, LLFilePicker::FFLOAD_ANIM), std::make_tuple("wav", LLAssetType::AT_SOUND_WAV, LLFilePicker::FFLOAD_WAV), @@ -5102,6 +5103,8 @@ void LLViewerWindow::saveImageNumbered(LLImageFormatted *image, bool force_picke pick_type = LLFilePicker::FFSAVE_TGA; else if (extension == ".webp") pick_type = LLFilePicker::FFSAVE_WEBP; + else if (extension == ".avif") + pick_type = LLFilePicker::FFSAVE_AVIF; else pick_type = LLFilePicker::FFSAVE_ALL; @@ -5277,6 +5280,12 @@ bool LLViewerWindow::saveSnapshot(const std::string& filepath, S32 image_width, case LLSnapshotModel::SNAPSHOT_FORMAT_JPEG: image_codec = IMG_CODEC_JPEG; break; + case LLSnapshotModel::SNAPSHOT_FORMAT_WEBP: + image_codec = IMG_CODEC_WEBP; + break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: + image_codec = IMG_CODEC_AVIF; + break; default: image_codec = IMG_CODEC_BMP; break; diff --git a/indra/newview/skins/default/xui/en/panel_snapshot_local.xml b/indra/newview/skins/default/xui/en/panel_snapshot_local.xml index da81ca5220c..881f2b3d0c7 100644 --- a/indra/newview/skins/default/xui/en/panel_snapshot_local.xml +++ b/indra/newview/skins/default/xui/en/panel_snapshot_local.xml @@ -174,6 +174,10 @@ label="WebP (Lossless)" name="WEBP" value="WEBP" /> + Bitmap Images PNG Images WebP Images - Targa, PNG, WebP Images + AVIF Images + Targa, PNG, WebP, AVIF Images AVI Movie File XAF Anim File XML File diff --git a/indra/vcpkg.json b/indra/vcpkg.json index e48785d2523..84858ae7763 100644 --- a/indra/vcpkg.json +++ b/indra/vcpkg.json @@ -51,6 +51,13 @@ "glm", "harfbuzz", "hunspell", + { + "name": "libavif", + "features": [ + "aom", + "dav1d" + ] + }, { "name": "libjpeg-turbo", "features": [ From db70447521cd3aaa759f549103cea191b24d77bb Mon Sep 17 00:00:00 2001 From: Rye Date: Mon, 1 Jun 2026 19:09:52 -0400 Subject: [PATCH 2/3] Add lossless AVIF encode path for snapshots LLImageAVIF::encode() now produces mathematically lossless output at quality 100 (identity matrix + YUV 4:4:4); the lossy path keeps 4:2:0 with a BT.601 matrix. Expose a distinct "AVIF (Lossless)" snapshot format alongside the lossy "AVIF" option, mirroring the WebP (Lossless) treatment: - SNAPSHOT_FORMAT_AVIF_LOSSLESS enum and combo box item - encode, size-estimate, and save-dispatch switches - no quality slider for the lossless variant Co-Authored-By: Claude Opus 4.8 --- indra/llimage/llimageavif.cpp | 10 ++++++++-- indra/newview/llpanelsnapshotlocal.cpp | 4 ++++ indra/newview/llsnapshotlivepreview.cpp | 6 ++++++ indra/newview/llsnapshotmodel.h | 3 ++- indra/newview/llviewermenufile.cpp | 3 +++ indra/newview/llviewerwindow.cpp | 1 + .../skins/default/xui/en/panel_snapshot_local.xml | 4 ++++ 7 files changed, 28 insertions(+), 3 deletions(-) diff --git a/indra/llimage/llimageavif.cpp b/indra/llimage/llimageavif.cpp index 335003ba329..db479695caa 100644 --- a/indra/llimage/llimageavif.cpp +++ b/indra/llimage/llimageavif.cpp @@ -237,7 +237,13 @@ bool LLImageAVIF::encode(const LLImageRaw* raw_image, F32 encode_time) memcpy(tmp_buff.get() + (size_t)i * stride, row, stride); } - avifImage* image = avifImageCreate(width, height, 8, AVIF_PIXEL_FORMAT_YUV420); + // Quality 100 selects a mathematically lossless encode. True lossless requires + // the identity matrix (RGB stored without a lossy YUV conversion), which in turn + // requires 4:4:4 (no chroma subsampling). Lossy uses 4:2:0 with a BT.601 matrix. + const bool lossless = (mEncodeQuality >= AVIF_QUALITY_LOSSLESS); + + avifImage* image = avifImageCreate(width, height, 8, + lossless ? AVIF_PIXEL_FORMAT_YUV444 : AVIF_PIXEL_FORMAT_YUV420); if (!image) { setLastError("LLImageAVIF::Failed to allocate image"); @@ -247,7 +253,7 @@ bool LLImageAVIF::encode(const LLImageRaw* raw_image, F32 encode_time) // Tag the bitstream as sRGB so decoders reproduce our 8-bit sRGB content. image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; - image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; + image->matrixCoefficients = lossless ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT601; avifRGBImage rgb; avifRGBImageSetDefaults(&rgb, image); diff --git a/indra/newview/llpanelsnapshotlocal.cpp b/indra/newview/llpanelsnapshotlocal.cpp index b087888e50a..d05c08e100c 100644 --- a/indra/newview/llpanelsnapshotlocal.cpp +++ b/indra/newview/llpanelsnapshotlocal.cpp @@ -125,6 +125,10 @@ LLSnapshotModel::ESnapshotFormat LLPanelSnapshotLocal::getImageFormat() const { fmt = LLSnapshotModel::SNAPSHOT_FORMAT_AVIF; } + else if (id == "AVIF_LOSSLESS") + { + fmt = LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS; + } return fmt; } diff --git a/indra/newview/llsnapshotlivepreview.cpp b/indra/newview/llsnapshotlivepreview.cpp index 4df35ac0177..b0af3fc4d95 100644 --- a/indra/newview/llsnapshotlivepreview.cpp +++ b/indra/newview/llsnapshotlivepreview.cpp @@ -952,6 +952,9 @@ void LLSnapshotLivePreview::estimateDataSize() case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: ratio = 8.0; // Lossy AVIF compresses substantially better break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: + ratio = 3.0; // Lossless AVIF, roughly PNG-class sizes + break; } } mDataSize = (S32)((F32)mPreviewImage->getDataSize() / ratio); @@ -997,6 +1000,9 @@ LLPointer LLSnapshotLivePreview::getFormattedImage() case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: mFormattedImage = new LLImageAVIF(mSnapshotQuality); break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: + mFormattedImage = new LLImageAVIF(100); // quality 100 == lossless + break; } if (mFormattedImage->encode(mPreviewImage, 0)) { diff --git a/indra/newview/llsnapshotmodel.h b/indra/newview/llsnapshotmodel.h index 2ea9a08ca4f..500a4bf9b8e 100644 --- a/indra/newview/llsnapshotmodel.h +++ b/indra/newview/llsnapshotmodel.h @@ -44,7 +44,8 @@ class LLSnapshotModel SNAPSHOT_FORMAT_JPEG, SNAPSHOT_FORMAT_BMP, SNAPSHOT_FORMAT_WEBP, - SNAPSHOT_FORMAT_AVIF + SNAPSHOT_FORMAT_AVIF, + SNAPSHOT_FORMAT_AVIF_LOSSLESS } ESnapshotFormat; typedef enum diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp index 0c5e2139518..e5930ba7677 100644 --- a/indra/newview/llviewermenufile.cpp +++ b/indra/newview/llviewermenufile.cpp @@ -1108,6 +1108,9 @@ class LLFileTakeSnapshotToDisk : public view_listener_t case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: formatted = new LLImageAVIF(gSavedSettings.getS32("SnapshotQuality")); break; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: + formatted = new LLImageAVIF(100); // quality 100 == lossless + break; } formatted->enableOverSize() ; formatted->encode(raw, 0); diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index 35202bb0c81..c4ab278d090 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -5284,6 +5284,7 @@ bool LLViewerWindow::saveSnapshot(const std::string& filepath, S32 image_width, image_codec = IMG_CODEC_WEBP; break; case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF: + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: image_codec = IMG_CODEC_AVIF; break; default: diff --git a/indra/newview/skins/default/xui/en/panel_snapshot_local.xml b/indra/newview/skins/default/xui/en/panel_snapshot_local.xml index 881f2b3d0c7..2a3c2faea8f 100644 --- a/indra/newview/skins/default/xui/en/panel_snapshot_local.xml +++ b/indra/newview/skins/default/xui/en/panel_snapshot_local.xml @@ -178,6 +178,10 @@ label="AVIF" name="AVIF" value="AVIF" /> + Date: Mon, 1 Jun 2026 22:23:18 -0400 Subject: [PATCH 3/3] Support 1 and 2 component (grayscale) AVIF encode Encode 1- and 2-component raw images as monochrome (4:0:0) AVIF via AVIF_RGB_FORMAT_GRAY / AVIF_RGB_FORMAT_GRAYA, selecting the pixel and RGB format by component count instead of assuming RGB/RGBA. Monochrome uses a BT.601 matrix (identity requires three planes); luma maps directly to Y so quality 100 remains lossless. Co-Authored-By: Claude Opus 4.8 --- indra/llimage/llimageavif.cpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/indra/llimage/llimageavif.cpp b/indra/llimage/llimageavif.cpp index db479695caa..c9c3a380ff5 100644 --- a/indra/llimage/llimageavif.cpp +++ b/indra/llimage/llimageavif.cpp @@ -237,13 +237,19 @@ bool LLImageAVIF::encode(const LLImageRaw* raw_image, F32 encode_time) memcpy(tmp_buff.get() + (size_t)i * stride, row, stride); } - // Quality 100 selects a mathematically lossless encode. True lossless requires - // the identity matrix (RGB stored without a lossy YUV conversion), which in turn - // requires 4:4:4 (no chroma subsampling). Lossy uses 4:2:0 with a BT.601 matrix. + // 1/2 component images are grayscale (luma [+ alpha]); encode them as monochrome + // 4:0:0 so the single channel maps directly to luma. 3/4 component images are color. + // Quality 100 selects a mathematically lossless encode: for color that needs the + // identity matrix (RGB stored verbatim), which requires 4:4:4; lossy color uses + // 4:2:0. Monochrome stores luma directly, so a standard matrix is already exact and + // the identity matrix (which needs three planes) must not be used. + const bool grayscale = (components <= 2); const bool lossless = (mEncodeQuality >= AVIF_QUALITY_LOSSLESS); - avifImage* image = avifImageCreate(width, height, 8, - lossless ? AVIF_PIXEL_FORMAT_YUV444 : AVIF_PIXEL_FORMAT_YUV420); + const avifPixelFormat yuv_format = grayscale ? AVIF_PIXEL_FORMAT_YUV400 + : (lossless ? AVIF_PIXEL_FORMAT_YUV444 : AVIF_PIXEL_FORMAT_YUV420); + + avifImage* image = avifImageCreate(width, height, 8, yuv_format); if (!image) { setLastError("LLImageAVIF::Failed to allocate image"); @@ -253,11 +259,17 @@ bool LLImageAVIF::encode(const LLImageRaw* raw_image, F32 encode_time) // Tag the bitstream as sRGB so decoders reproduce our 8-bit sRGB content. image->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; - image->matrixCoefficients = lossless ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT601; + image->matrixCoefficients = (lossless && !grayscale) ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT601; avifRGBImage rgb; avifRGBImageSetDefaults(&rgb, image); - rgb.format = (components == 4) ? AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_RGB; + switch (components) + { + case 1: rgb.format = AVIF_RGB_FORMAT_GRAY; break; + case 2: rgb.format = AVIF_RGB_FORMAT_GRAYA; break; + case 4: rgb.format = AVIF_RGB_FORMAT_RGBA; break; + default: rgb.format = AVIF_RGB_FORMAT_RGB; break; // 3 components + } rgb.depth = 8; rgb.pixels = tmp_buff.get(); rgb.rowBytes = (uint32_t)stride;