diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 8888bdf561..fc07ad4965 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 1fafbe0c55..e193032dfa 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 0000000000..5da0ca907a --- /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 b7670c28a0..f894cf3d48 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 293dde6cb9..b397cf829b 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 fc8b3f0f8f..4a52043c63 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 46e78c2e19..4515efb5fb 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 0000000000..c9c3a380ff --- /dev/null +++ b/indra/llimage/llimageavif.cpp @@ -0,0 +1,320 @@ +/* + * @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); + } + + // 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); + + 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"); + 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 = (lossless && !grayscale) ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT601; + + avifRGBImage rgb; + avifRGBImageSetDefaults(&rgb, image); + 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; + + 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 0000000000..9727267a5e --- /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 3ad911f986..5f2399fda3 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 2fbc28bfea..9c9e6a721e 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 45f235eac9..d270364a45 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 7c50fe3b10..f2e2bdce3c 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 6e72443dcc..8bad6e6232 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 f997f2be97..cd526e50cb 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 dcca539d4b..d05c08e100 100644 --- a/indra/newview/llpanelsnapshotlocal.cpp +++ b/indra/newview/llpanelsnapshotlocal.cpp @@ -121,6 +121,14 @@ LLSnapshotModel::ESnapshotFormat LLPanelSnapshotLocal::getImageFormat() const { fmt = LLSnapshotModel::SNAPSHOT_FORMAT_WEBP; } + else if (id == "AVIF") + { + fmt = LLSnapshotModel::SNAPSHOT_FORMAT_AVIF; + } + else if (id == "AVIF_LOSSLESS") + { + fmt = LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS; + } return fmt; } @@ -132,7 +140,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 4bfdcde612..e28103721b 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 3ad1f786bc..b0af3fc4d9 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,12 @@ 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; + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: + ratio = 3.0; // Lossless AVIF, roughly PNG-class sizes + break; } } mDataSize = (S32)((F32)mPreviewImage->getDataSize() / ratio); @@ -990,6 +997,12 @@ LLPointer LLSnapshotLivePreview::getFormattedImage() case LLSnapshotModel::SNAPSHOT_FORMAT_WEBP: mFormattedImage = new LLImageWebP(); break; + 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 6e2858ff8a..500a4bf9b8 100644 --- a/indra/newview/llsnapshotmodel.h +++ b/indra/newview/llsnapshotmodel.h @@ -43,7 +43,9 @@ class LLSnapshotModel SNAPSHOT_FORMAT_PNG, SNAPSHOT_FORMAT_JPEG, SNAPSHOT_FORMAT_BMP, - SNAPSHOT_FORMAT_WEBP + SNAPSHOT_FORMAT_WEBP, + SNAPSHOT_FORMAT_AVIF, + SNAPSHOT_FORMAT_AVIF_LOSSLESS } ESnapshotFormat; typedef enum diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp index 81bf0a4438..e5930ba767 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,12 @@ 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; + 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 a30c13eb90..c4ab278d09 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,13 @@ 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: + case LLSnapshotModel::SNAPSHOT_FORMAT_AVIF_LOSSLESS: + 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 da81ca5220..2a3c2faea8 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,14 @@ 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 e48785d252..84858ae776 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": [