1650 lines
55 KiB
C
1650 lines
55 KiB
C
/*
|
|
SDL_image: An example image loading library for use with SDL
|
|
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
|
|
|
|
This software is provided 'as-is', without any express or implied
|
|
warranty. In no event will the authors be held liable for any damages
|
|
arising from the use of this software.
|
|
|
|
Permission is granted to anyone to use this software for any purpose,
|
|
including commercial applications, and to alter it and redistribute it
|
|
freely, subject to the following restrictions:
|
|
|
|
1. The origin of this software must not be misrepresented; you must not
|
|
claim that you wrote the original software. If you use this software
|
|
in a product, an acknowledgment in the product documentation would be
|
|
appreciated but is not required.
|
|
2. Altered source versions must be plainly marked as such, and must not be
|
|
misrepresented as being the original software.
|
|
3. This notice may not be removed or altered from any source distribution.
|
|
*/
|
|
|
|
/* This is a AVIF image file loading framework */
|
|
|
|
#include <SDL3_image/SDL_image.h>
|
|
|
|
#include "IMG.h"
|
|
#include "IMG_avif.h"
|
|
#include "IMG_anim_encoder.h"
|
|
#include "IMG_anim_decoder.h"
|
|
#include "xmlman.h"
|
|
|
|
/* We'll have AVIF save support by default */
|
|
#if !defined(SAVE_AVIF)
|
|
#define SAVE_AVIF 1
|
|
#endif
|
|
|
|
#ifdef LOAD_AVIF
|
|
|
|
#include <avif/avif.h>
|
|
|
|
#if defined(LOAD_AVIF_DYNAMIC) && defined(SDL_ELF_NOTE_DLOPEN)
|
|
SDL_ELF_NOTE_DLOPEN(
|
|
"avif",
|
|
"Support for AVIF images using libavif",
|
|
SDL_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
|
|
LOAD_AVIF_DYNAMIC
|
|
)
|
|
#endif
|
|
|
|
|
|
/*
|
|
* - `SDL_PROP_SURFACE_MAXCLL_NUMBER`: MaxCLL (Maximum Content Light Level)
|
|
* indicates the maximum light level of any single pixel (in cd/m2 or nits)
|
|
* of the content. MaxCLL is usually measured off the final delivered
|
|
* content after mastering. If one uses the full light level of the HDR
|
|
* mastering display and adds a hard clip at its maximum value, MaxCLL would
|
|
* be equal to the peak luminance of the mastering monitor.
|
|
* - `SDL_PROP_SURFACE_MAXFALL_NUMBER`: MaxFALL (Maximum Frame Average Light
|
|
* Level) indicates the maximum value of the frame average light level (in
|
|
* cd/m2 or nits) of the content. MaxFALL is calculated by averaging the
|
|
* decoded luminance values of all the pixels within a frame. MaxFALL is
|
|
* usually much lower than MaxCLL.
|
|
*/
|
|
#define SDL_PROP_SURFACE_MAXCLL_NUMBER "SDL.surface.maxCLL"
|
|
#define SDL_PROP_SURFACE_MAXFALL_NUMBER "SDL.surface.maxFALL"
|
|
|
|
|
|
static struct {
|
|
int loaded;
|
|
void *handle;
|
|
avifDecoder * (*avifDecoderCreate)(void);
|
|
void (*avifDecoderDestroy)(avifDecoder * decoder);
|
|
avifResult (*avifDecoderNextImage)(avifDecoder * decoder);
|
|
avifResult (*avifDecoderParse)(avifDecoder * decoder);
|
|
void (*avifDecoderSetIO)(avifDecoder * decoder, avifIO * io);
|
|
avifResult (*avifEncoderAddImage)(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags);
|
|
avifEncoder * (*avifEncoderCreate)(void);
|
|
void (*avifEncoderDestroy)(avifEncoder * encoder);
|
|
avifResult (*avifEncoderFinish)(avifEncoder * encoder, avifRWData * output);
|
|
avifImage * (*avifImageCreate)(uint32_t width, uint32_t height, uint32_t depth, avifPixelFormat yuvFormat);
|
|
void (*avifImageDestroy)(avifImage * image);
|
|
avifResult (*avifImageRGBToYUV)(avifImage * image, const avifRGBImage * rgb);
|
|
avifResult (*avifImageYUVToRGB)(const avifImage * image, avifRGBImage * rgb);
|
|
avifBool (*avifPeekCompatibleFileType)(const avifROData * input);
|
|
void (*avifRGBImageSetDefaults)(avifRGBImage * rgb, const avifImage * image);
|
|
void (*avifRWDataFree)(avifRWData * raw);
|
|
const char * (*avifResultToString)(avifResult res);
|
|
|
|
// XMP metadata support
|
|
avifResult (*avifImageSetMetadataXMP)(avifImage * image, const uint8_t * xmp, size_t xmpSize);
|
|
} lib;
|
|
|
|
#ifdef LOAD_AVIF_DYNAMIC
|
|
#define FUNCTION_LOADER(FUNC, SIG) \
|
|
lib.FUNC = (SIG) SDL_LoadFunction(lib.handle, #FUNC); \
|
|
if (lib.FUNC == NULL) { SDL_UnloadObject(lib.handle); return false; }
|
|
#else
|
|
#define FUNCTION_LOADER(FUNC, SIG) \
|
|
lib.FUNC = FUNC; \
|
|
if (lib.FUNC == NULL) { return SDL_SetError("Missing avif.framework"); }
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
/* Need to turn off optimizations so weak framework load check works */
|
|
__attribute__ ((optnone))
|
|
#endif
|
|
static bool IMG_InitAVIF(void)
|
|
{
|
|
if ( lib.loaded == 0 ) {
|
|
#ifdef LOAD_AVIF_DYNAMIC
|
|
lib.handle = SDL_LoadObject(LOAD_AVIF_DYNAMIC);
|
|
if ( lib.handle == NULL ) {
|
|
return false;
|
|
}
|
|
#endif
|
|
FUNCTION_LOADER(avifDecoderCreate, avifDecoder * (*)(void))
|
|
FUNCTION_LOADER(avifDecoderDestroy, void (*)(avifDecoder * decoder))
|
|
FUNCTION_LOADER(avifDecoderNextImage, avifResult (*)(avifDecoder * decoder))
|
|
FUNCTION_LOADER(avifDecoderParse, avifResult (*)(avifDecoder * decoder))
|
|
FUNCTION_LOADER(avifDecoderSetIO, void (*)(avifDecoder * decoder, avifIO * io))
|
|
FUNCTION_LOADER(avifEncoderAddImage, avifResult (*)(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags))
|
|
FUNCTION_LOADER(avifEncoderCreate, avifEncoder * (*)(void))
|
|
FUNCTION_LOADER(avifEncoderDestroy, void (*)(avifEncoder * encoder))
|
|
FUNCTION_LOADER(avifEncoderFinish, avifResult (*)(avifEncoder * encoder, avifRWData * output))
|
|
FUNCTION_LOADER(avifImageCreate, avifImage * (*)(uint32_t width, uint32_t height, uint32_t depth, avifPixelFormat yuvFormat))
|
|
FUNCTION_LOADER(avifImageDestroy, void (*)(avifImage * image))
|
|
FUNCTION_LOADER(avifImageRGBToYUV, avifResult (*)(avifImage * image, const avifRGBImage * rgb))
|
|
FUNCTION_LOADER(avifImageYUVToRGB, avifResult (*)(const avifImage * image, avifRGBImage * rgb))
|
|
FUNCTION_LOADER(avifPeekCompatibleFileType, avifBool (*)(const avifROData * input))
|
|
FUNCTION_LOADER(avifRGBImageSetDefaults, void (*)(avifRGBImage * rgb, const avifImage * image))
|
|
FUNCTION_LOADER(avifRWDataFree, void (*)(avifRWData * raw))
|
|
FUNCTION_LOADER(avifResultToString, const char * (*)(avifResult res))
|
|
|
|
// XMP metadata support
|
|
FUNCTION_LOADER(avifImageSetMetadataXMP, avifResult (*)(avifImage * image, const uint8_t * xmp, size_t xmpSize))
|
|
}
|
|
++lib.loaded;
|
|
|
|
return true;
|
|
}
|
|
#if 0
|
|
void IMG_QuitAVIF(void)
|
|
{
|
|
if ( lib.loaded == 0 ) {
|
|
return;
|
|
}
|
|
if ( lib.loaded == 1 ) {
|
|
#ifdef LOAD_AVIF_DYNAMIC
|
|
SDL_UnloadObject(lib.handle);
|
|
#endif
|
|
}
|
|
--lib.loaded;
|
|
}
|
|
#endif // 0
|
|
|
|
static bool ReadAVIFHeader(SDL_IOStream *src, Uint8 **header_data, size_t *header_size)
|
|
{
|
|
Uint8 magic[16];
|
|
Uint64 size;
|
|
Uint64 read = 0;
|
|
Uint8 *data;
|
|
|
|
*header_data = NULL;
|
|
*header_size = 0;
|
|
|
|
if (SDL_ReadIO(src, magic, 8) != 8) {
|
|
return false;
|
|
}
|
|
read += 8;
|
|
|
|
if (SDL_memcmp(&magic[4], "ftyp", 4) != 0) {
|
|
return false;
|
|
}
|
|
|
|
size = (((Uint64)magic[0] << 24) |
|
|
((Uint64)magic[1] << 16) |
|
|
((Uint64)magic[2] << 8) |
|
|
((Uint64)magic[3] << 0));
|
|
if (size == 1) {
|
|
/* 64-bit header size */
|
|
if (SDL_ReadIO(src, &magic[8], 8) != 8) {
|
|
return false;
|
|
}
|
|
read += 8;
|
|
|
|
size = (((Uint64)magic[8] << 56) |
|
|
((Uint64)magic[9] << 48) |
|
|
((Uint64)magic[10] << 40) |
|
|
((Uint64)magic[11] << 32) |
|
|
((Uint64)magic[12] << 24) |
|
|
((Uint64)magic[13] << 16) |
|
|
((Uint64)magic[14] << 8) |
|
|
((Uint64)magic[15] << 0));
|
|
}
|
|
|
|
if (size > SDL_SIZE_MAX) {
|
|
return false;
|
|
}
|
|
if (size <= read) {
|
|
return false;
|
|
}
|
|
|
|
/* Read in the header */
|
|
data = (Uint8 *)SDL_malloc((size_t)size);
|
|
if (!data) {
|
|
return false;
|
|
}
|
|
SDL_memcpy(data, magic, (size_t)read);
|
|
|
|
if (SDL_ReadIO(src, &data[read], (size_t)(size - read)) != (size_t)(size - read)) {
|
|
SDL_free(data);
|
|
return false;
|
|
}
|
|
*header_data = data;
|
|
*header_size = (size_t)size;
|
|
return true;
|
|
}
|
|
|
|
/* See if an image is contained in a data source */
|
|
bool IMG_isAVIF(SDL_IOStream *src)
|
|
{
|
|
Sint64 start;
|
|
bool is_AVIF;
|
|
Uint8 *data;
|
|
size_t size;
|
|
|
|
if (!src) {
|
|
return false;
|
|
}
|
|
|
|
start = SDL_TellIO(src);
|
|
is_AVIF = false;
|
|
if (ReadAVIFHeader(src, &data, &size)) {
|
|
/* This might be AVIF, do more thorough checks */
|
|
if (IMG_InitAVIF()) {
|
|
avifROData header;
|
|
|
|
header.data = data;
|
|
header.size = size;
|
|
is_AVIF = lib.avifPeekCompatibleFileType(&header);
|
|
}
|
|
SDL_free(data);
|
|
}
|
|
SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
|
|
return is_AVIF;
|
|
}
|
|
|
|
/* Context for AFIF I/O operations */
|
|
typedef struct
|
|
{
|
|
SDL_IOStream *src;
|
|
Uint64 start;
|
|
uint8_t *data;
|
|
Sint64 size;
|
|
} avifIOContext;
|
|
|
|
static avifResult ReadAVIFIO(struct avifIO * io, uint32_t readFlags, uint64_t offset, size_t size, avifROData * out)
|
|
{
|
|
avifIOContext *context = (avifIOContext *)io->data;
|
|
|
|
/* The AVIF reader bounces all over, so always seek to the correct offset */
|
|
if (SDL_SeekIO(context->src, context->start + offset, SDL_IO_SEEK_SET) < 0) {
|
|
return AVIF_RESULT_IO_ERROR;
|
|
}
|
|
|
|
if (size > (Uint64)context->size) {
|
|
uint8_t *data = (uint8_t *)SDL_realloc(context->data, size);
|
|
if (!data) {
|
|
return AVIF_RESULT_IO_ERROR;
|
|
}
|
|
context->data = data;
|
|
context->size = (Sint64)size;
|
|
}
|
|
|
|
out->data = context->data;
|
|
out->size = SDL_ReadIO(context->src, context->data, size);
|
|
if (out->size == 0) {
|
|
if (SDL_GetIOStatus(context->src) == SDL_IO_STATUS_NOT_READY) {
|
|
return AVIF_RESULT_WAITING_ON_IO;
|
|
} else {
|
|
return AVIF_RESULT_IO_ERROR;
|
|
}
|
|
}
|
|
|
|
return AVIF_RESULT_OK;
|
|
}
|
|
|
|
static void DestroyAVIFIO(struct avifIO * io)
|
|
{
|
|
avifIOContext *context = (avifIOContext *)io->data;
|
|
|
|
if (context->data) {
|
|
SDL_free(context->data);
|
|
context->data = NULL;
|
|
}
|
|
}
|
|
|
|
static int ConvertGBR444toXBGR2101010(avifImage *image, SDL_Surface *surface)
|
|
{
|
|
const Uint16 *srcR, *srcG, *srcB;
|
|
int srcskipR, srcskipG, srcskipB;
|
|
int sR, sG, sB;
|
|
Uint32 *dst;
|
|
int dstskip;
|
|
int width, height;
|
|
|
|
srcR = (Uint16 *)image->yuvPlanes[2];
|
|
srcskipR = (image->yuvRowBytes[2] - image->width * sizeof(Uint16)) / sizeof(Uint16);
|
|
srcG = (Uint16 *)image->yuvPlanes[0];
|
|
srcskipG = (image->yuvRowBytes[0] - image->width * sizeof(Uint16)) / sizeof(Uint16);
|
|
srcB = (Uint16 *)image->yuvPlanes[1];
|
|
srcskipB = (image->yuvRowBytes[1] - image->width * sizeof(Uint16)) / sizeof(Uint16);
|
|
if (srcskipR < 0 || srcskipG < 0 || srcskipB < 0) {
|
|
return -1;
|
|
}
|
|
|
|
dst = (Uint32 *)surface->pixels;
|
|
dstskip = (surface->pitch - image->width * sizeof(Uint32)) / sizeof(Uint32);
|
|
|
|
height = image->height;
|
|
while (height--) {
|
|
width = image->width;
|
|
while (width--) {
|
|
sR = *srcR++;
|
|
sG = *srcG++;
|
|
sB = *srcB++;
|
|
sR = SDL_min(sR, 1023);
|
|
sG = SDL_min(sG, 1023);
|
|
sB = SDL_min(sB, 1023);
|
|
*dst = (0x03 << 30) | (sB << 20) | (sG << 10) | sR;
|
|
++dst;
|
|
}
|
|
srcR += srcskipR;
|
|
srcG += srcskipG;
|
|
srcB += srcskipB;
|
|
dst += dstskip;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void ConvertRGB16toXBGR2101010(avifRGBImage *image, SDL_Surface *surface)
|
|
{
|
|
const Uint16 *src;
|
|
Uint32 sR, sG, sB;
|
|
Uint32 *dst;
|
|
int dstskip;
|
|
int width, height;
|
|
|
|
src = (Uint16 *)image->pixels;
|
|
dst = (Uint32 *)surface->pixels;
|
|
dstskip = (surface->pitch - image->width * sizeof(Uint32)) / sizeof(Uint32);
|
|
|
|
height = image->height;
|
|
while (height--) {
|
|
width = image->width;
|
|
while (width--) {
|
|
sR = *src++;
|
|
sR >>= 6;
|
|
sG = *src++;
|
|
sG >>= 6;
|
|
sB = *src++;
|
|
sB >>= 6;
|
|
*dst = (0x03 << 30) | (sB << 20) | (sG << 10) | sR;
|
|
++dst;
|
|
}
|
|
dst += dstskip;
|
|
}
|
|
}
|
|
|
|
/* Load a AVIF type image from an SDL datasource */
|
|
SDL_Surface *IMG_LoadAVIF_IO(SDL_IOStream *src)
|
|
{
|
|
Sint64 start;
|
|
avifDecoder *decoder = NULL;
|
|
avifImage *image;
|
|
avifIO io;
|
|
avifIOContext context;
|
|
avifResult result;
|
|
SDL_Surface *surface = NULL;
|
|
|
|
if (!src) {
|
|
/* The error message has been set in SDL_IOFromFile */
|
|
return NULL;
|
|
}
|
|
start = SDL_TellIO(src);
|
|
|
|
if (!IMG_InitAVIF()) {
|
|
return NULL;
|
|
}
|
|
|
|
SDL_zero(context);
|
|
SDL_zero(io);
|
|
|
|
decoder = lib.avifDecoderCreate();
|
|
if (!decoder) {
|
|
SDL_SetError("Couldn't create AVIF decoder");
|
|
goto done;
|
|
}
|
|
|
|
/* Be permissive so we can load as many images as possible */
|
|
decoder->strictFlags = AVIF_STRICT_DISABLED;
|
|
|
|
context.src = src;
|
|
context.start = start;
|
|
io.destroy = DestroyAVIFIO;
|
|
io.read = ReadAVIFIO;
|
|
io.data = &context;
|
|
lib.avifDecoderSetIO(decoder, &io);
|
|
|
|
result = lib.avifDecoderParse(decoder);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't parse AVIF image: %s", lib.avifResultToString(result));
|
|
goto done;
|
|
}
|
|
|
|
result = lib.avifDecoderNextImage(decoder);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't get AVIF image: %s", lib.avifResultToString(result));
|
|
goto done;
|
|
}
|
|
|
|
image = decoder->image;
|
|
if (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084) {
|
|
// This is an HDR PQ image
|
|
|
|
if (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
|
|
image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) {
|
|
// This image uses identity GBR channel ordering
|
|
if (image->depth == 10) {
|
|
surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_XBGR2101010);
|
|
if (surface) {
|
|
if (ConvertGBR444toXBGR2101010(image, surface) < 0) {
|
|
// Invalid image, let avif take care of it
|
|
SDL_free(surface);
|
|
surface = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!surface) {
|
|
avifRGBImage rgb;
|
|
|
|
// Convert the YUV image to 10-bit RGB
|
|
SDL_zero(rgb);
|
|
rgb.width = image->width;
|
|
rgb.height = image->height;
|
|
rgb.depth = 16;
|
|
rgb.format = AVIF_RGB_FORMAT_RGB;
|
|
rgb.rowBytes = (uint32_t)image->width * 3 * sizeof(Uint16);
|
|
rgb.pixels = (uint8_t *)SDL_malloc(image->height * rgb.rowBytes);
|
|
if (!rgb.pixels) {
|
|
goto done;
|
|
}
|
|
result = lib.avifImageYUVToRGB(image, &rgb);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't convert AVIF image to RGB: %s", lib.avifResultToString(result));
|
|
SDL_free(rgb.pixels);
|
|
goto done;
|
|
}
|
|
|
|
surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_XBGR2101010);
|
|
if (surface) {
|
|
ConvertRGB16toXBGR2101010(&rgb, surface);
|
|
}
|
|
|
|
SDL_free(rgb.pixels);
|
|
}
|
|
|
|
if (surface) {
|
|
// Set HDR properties
|
|
|
|
// The older standards use an SDR white point of 100 nits.
|
|
// ITU-R BT.2408-6 recommends using an SDR white point of 203 nits.
|
|
// This is the default Chrome uses, and what a lot of game content
|
|
// assumes, so we'll go with that.
|
|
const float DEFAULT_PQ_SDR_WHITE_POINT = 203.0f;
|
|
|
|
// The official definition is 10000, but PQ game content is often mastered for 400 or 1000 nits
|
|
const uint16_t DEFAULT_PQ_MAXCLL = 1000;
|
|
uint16_t maxCLL = DEFAULT_PQ_MAXCLL;
|
|
|
|
SDL_PropertiesID props = SDL_GetSurfaceProperties(surface);
|
|
SDL_Colorspace colorspace = SDL_DEFINE_COLORSPACE(SDL_COLOR_TYPE_RGB,
|
|
SDL_COLOR_RANGE_FULL,
|
|
image->colorPrimaries,
|
|
image->transferCharacteristics,
|
|
SDL_MATRIX_COEFFICIENTS_IDENTITY,
|
|
SDL_CHROMA_LOCATION_NONE);
|
|
SDL_SetSurfaceColorspace(surface, colorspace);
|
|
if (image->clli.maxCLL > 0) {
|
|
maxCLL = image->clli.maxCLL;
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXCLL_NUMBER, image->clli.maxCLL);
|
|
}
|
|
if (image->clli.maxPALL > 0) {
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXFALL_NUMBER, image->clli.maxPALL);
|
|
}
|
|
SDL_SetFloatProperty(props, SDL_PROP_SURFACE_SDR_WHITE_POINT_FLOAT, DEFAULT_PQ_SDR_WHITE_POINT);
|
|
SDL_SetFloatProperty(props, SDL_PROP_SURFACE_HDR_HEADROOM_FLOAT, (float)maxCLL / DEFAULT_PQ_SDR_WHITE_POINT);
|
|
}
|
|
}
|
|
|
|
if (!surface) {
|
|
avifRGBImage rgb;
|
|
|
|
surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_ARGB8888);
|
|
if (!surface) {
|
|
goto done;
|
|
}
|
|
|
|
/* Convert the YUV image to RGB */
|
|
SDL_zero(rgb);
|
|
rgb.width = surface->w;
|
|
rgb.height = surface->h;
|
|
rgb.depth = 8;
|
|
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
|
|
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
|
#else
|
|
rgb.format = AVIF_RGB_FORMAT_ARGB;
|
|
#endif
|
|
rgb.pixels = (uint8_t *)surface->pixels;
|
|
rgb.rowBytes = (uint32_t)surface->pitch;
|
|
result = lib.avifImageYUVToRGB(image, &rgb);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't convert AVIF image to RGB: %s", lib.avifResultToString(result));
|
|
SDL_DestroySurface(surface);
|
|
surface = NULL;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
done:
|
|
if (decoder) {
|
|
lib.avifDecoderDestroy(decoder);
|
|
}
|
|
if (!surface) {
|
|
SDL_SeekIO(src, start, SDL_IO_SEEK_SET);
|
|
}
|
|
return surface;
|
|
}
|
|
|
|
static bool IMG_SaveAVIF_IO_libavif(SDL_Surface *surface, SDL_IOStream *dst, int quality)
|
|
{
|
|
avifImage *image = NULL;
|
|
avifRGBImage rgb;
|
|
avifEncoder *encoder = NULL;
|
|
avifRWData avifOutput = AVIF_DATA_EMPTY;
|
|
avifResult rc;
|
|
SDL_Colorspace colorspace;
|
|
Uint16 maxCLL, maxFALL;
|
|
SDL_PropertiesID props;
|
|
bool result = false;
|
|
|
|
if (!IMG_InitAVIF()) {
|
|
return false;
|
|
}
|
|
|
|
/* Get the colorspace and light level properties, if any */
|
|
colorspace = SDL_GetSurfaceColorspace(surface);
|
|
props = SDL_GetSurfaceProperties(surface);
|
|
maxCLL = (Uint16)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_MAXCLL_NUMBER, 0);
|
|
maxFALL = (Uint16)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_MAXFALL_NUMBER, 0);
|
|
|
|
SDL_zero(rgb);
|
|
image = lib.avifImageCreate(surface->w, surface->h, 10, AVIF_PIXEL_FORMAT_YUV444);
|
|
if (!image) {
|
|
SDL_SetError("Couldn't create AVIF YUV image");
|
|
goto done;
|
|
}
|
|
image->yuvRange = AVIF_RANGE_FULL;
|
|
image->colorPrimaries = (avifColorPrimaries)SDL_COLORSPACEPRIMARIES(colorspace);
|
|
image->transferCharacteristics = (avifTransferCharacteristics)SDL_COLORSPACETRANSFER(colorspace);
|
|
image->clli.maxCLL = maxCLL;
|
|
image->clli.maxPALL = maxFALL;
|
|
|
|
lib.avifRGBImageSetDefaults(&rgb, image);
|
|
|
|
if (SDL_ISPIXELFORMAT_10BIT(surface->format)) {
|
|
const Uint16 expand_alpha[] = {
|
|
0, 0x155, 0x2aa, 0x3ff
|
|
};
|
|
int width, height;
|
|
Uint16 *dst16;
|
|
Uint32 *src;
|
|
int srcskip;
|
|
|
|
/* We are not actually using YUV, but storing raw GBR (yes not RGB) data */
|
|
image->yuvFormat = AVIF_PIXEL_FORMAT_YUV444;
|
|
image->depth = 10;
|
|
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
|
|
|
|
if (SDL_PIXELORDER(surface->format) == SDL_PACKEDORDER_XRGB ||
|
|
SDL_PIXELORDER(surface->format) == SDL_PACKEDORDER_ARGB) {
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
} else {
|
|
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
|
}
|
|
rgb.ignoreAlpha = SDL_ISPIXELFORMAT_ALPHA(surface->format) ? false : true;
|
|
rgb.depth = 10;
|
|
rgb.rowBytes = (uint32_t)image->width * 4 * sizeof(Uint16);
|
|
rgb.pixels = (uint8_t *)SDL_malloc(image->height * rgb.rowBytes);
|
|
if (!rgb.pixels) {
|
|
goto done;
|
|
}
|
|
|
|
src = (Uint32 *)surface->pixels;
|
|
srcskip = surface->pitch - (surface->w * sizeof(Uint32));
|
|
dst16 = (Uint16 *)rgb.pixels;
|
|
height = image->height;
|
|
while (height--) {
|
|
width = image->width;
|
|
while (width--) {
|
|
Uint32 pixelvalue = *src++;
|
|
|
|
*dst16++ = (pixelvalue >> 20) & 0x3FF;
|
|
*dst16++ = (pixelvalue >> 10) & 0x3FF;
|
|
*dst16++ = (pixelvalue >> 0) & 0x3FF;
|
|
*dst16++ = expand_alpha[(pixelvalue >> 30) & 0x3];
|
|
}
|
|
src = (Uint32 *)(((Uint8 *)src) + srcskip);
|
|
}
|
|
|
|
rc = lib.avifImageRGBToYUV(image, &rgb);
|
|
if (rc != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't convert to YUV: %s", lib.avifResultToString(rc));
|
|
goto done;
|
|
}
|
|
|
|
} else {
|
|
SDL_Surface *temp = NULL;
|
|
|
|
rgb.depth = 8;
|
|
switch (surface->format) {
|
|
case SDL_PIXELFORMAT_RGBX32:
|
|
case SDL_PIXELFORMAT_RGBA32:
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
temp = surface;
|
|
break;
|
|
case SDL_PIXELFORMAT_XRGB32:
|
|
case SDL_PIXELFORMAT_ARGB32:
|
|
rgb.format = AVIF_RGB_FORMAT_ARGB;
|
|
temp = surface;
|
|
break;
|
|
case SDL_PIXELFORMAT_BGRX32:
|
|
case SDL_PIXELFORMAT_BGRA32:
|
|
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
|
temp = surface;
|
|
break;
|
|
case SDL_PIXELFORMAT_XBGR32:
|
|
case SDL_PIXELFORMAT_ABGR32:
|
|
rgb.format = AVIF_RGB_FORMAT_ABGR;
|
|
temp = surface;
|
|
break;
|
|
default:
|
|
/* Need to convert to a format libavif understands */
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
if (SDL_ISPIXELFORMAT_ALPHA(surface->format)) {
|
|
temp = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
|
|
} else {
|
|
temp = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBX32);
|
|
}
|
|
if (!temp) {
|
|
goto done;
|
|
}
|
|
break;
|
|
}
|
|
rgb.ignoreAlpha = SDL_ISPIXELFORMAT_ALPHA(surface->format) ? false : true;
|
|
rgb.pixels = (uint8_t *)temp->pixels;
|
|
rgb.rowBytes = (uint32_t)temp->pitch;
|
|
|
|
/* Convert to YUV */
|
|
rc = lib.avifImageRGBToYUV(image, &rgb);
|
|
|
|
/* Do any cleanup */
|
|
if (temp != surface) {
|
|
SDL_free(temp);
|
|
}
|
|
rgb.pixels = NULL;
|
|
|
|
/* Check the result of the conversion */
|
|
if (rc != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't convert to YUV: %s", lib.avifResultToString(rc));
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
encoder = lib.avifEncoderCreate();
|
|
encoder->quality = quality;
|
|
encoder->qualityAlpha = AVIF_QUALITY_LOSSLESS;
|
|
encoder->speed = AVIF_SPEED_FASTEST;
|
|
|
|
rc = lib.avifEncoderAddImage(encoder, image, 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
|
|
if (rc != AVIF_RESULT_OK) {
|
|
SDL_SetError("Failed to add image to avif encoder: %s", lib.avifResultToString(rc));
|
|
goto done;
|
|
}
|
|
|
|
rc = lib.avifEncoderFinish(encoder, &avifOutput);
|
|
if (rc != AVIF_RESULT_OK) {
|
|
SDL_SetError("Failed to finish encoder: %s", lib.avifResultToString(rc));
|
|
goto done;
|
|
}
|
|
|
|
if (SDL_WriteIO(dst, avifOutput.data, avifOutput.size) == avifOutput.size) {
|
|
result = true;
|
|
}
|
|
|
|
done:
|
|
if (rgb.pixels) {
|
|
SDL_free(rgb.pixels);
|
|
}
|
|
if (image) {
|
|
lib.avifImageDestroy(image);
|
|
}
|
|
if (encoder) {
|
|
lib.avifEncoderDestroy(encoder);
|
|
}
|
|
lib.avifRWDataFree(&avifOutput);
|
|
|
|
return result;
|
|
}
|
|
|
|
#else
|
|
|
|
/* We don't have any way to save AVIF files */
|
|
#undef SAVE_AVIF
|
|
|
|
/* See if an image is contained in a data source */
|
|
bool IMG_isAVIF(SDL_IOStream *src)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/* Load a AVIF type image from an SDL datasource */
|
|
SDL_Surface *IMG_LoadAVIF_IO(SDL_IOStream *src)
|
|
{
|
|
SDL_SetError("SDL_image built without AVIF support");
|
|
return NULL;
|
|
}
|
|
|
|
#endif /* LOAD_AVIF */
|
|
|
|
#if SAVE_AVIF
|
|
|
|
bool IMG_SaveAVIF_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, int quality)
|
|
{
|
|
bool result = false;
|
|
|
|
if (!IMG_VerifyCanSaveSurface(surface)) {
|
|
goto done;
|
|
}
|
|
if (!dst) {
|
|
SDL_InvalidParamError("dst");
|
|
goto done;
|
|
}
|
|
|
|
result = IMG_SaveAVIF_IO_libavif(surface, dst, quality);
|
|
|
|
done:
|
|
if (closeio) {
|
|
result &= SDL_CloseIO(dst);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool IMG_SaveAVIF(SDL_Surface *surface, const char *file, int quality)
|
|
{
|
|
if (!IMG_VerifyCanSaveSurface(surface)) {
|
|
return false;
|
|
}
|
|
|
|
SDL_IOStream *dst = SDL_IOFromFile(file, "wb");
|
|
if (dst) {
|
|
return IMG_SaveAVIF_IO(surface, dst, true, quality);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
bool IMG_SaveAVIF_IO(SDL_Surface *surface, SDL_IOStream *dst, bool closeio, int quality)
|
|
{
|
|
return SDL_SetError("SDL_image built without AVIF save support");
|
|
}
|
|
|
|
bool IMG_SaveAVIF(SDL_Surface *surface, const char *file, int quality)
|
|
{
|
|
return SDL_SetError("SDL_image built without AVIF save support");
|
|
}
|
|
|
|
#endif // SAVE_AVIF
|
|
|
|
#ifdef LOAD_AVIF
|
|
|
|
static void SetHDRProperties(SDL_Surface *surface, const avifImage *image)
|
|
{
|
|
// Standard HDR constants
|
|
const float DEFAULT_PQ_SDR_WHITE_POINT = 203.0f;
|
|
const uint16_t DEFAULT_PQ_MAXCLL = 1000;
|
|
uint16_t maxCLL = DEFAULT_PQ_MAXCLL;
|
|
|
|
SDL_PropertiesID props = SDL_GetSurfaceProperties(surface);
|
|
|
|
// Set colorspace first
|
|
SDL_Colorspace colorspace = SDL_DEFINE_COLORSPACE(SDL_COLOR_TYPE_RGB,
|
|
SDL_COLOR_RANGE_FULL,
|
|
image->colorPrimaries,
|
|
image->transferCharacteristics,
|
|
image->matrixCoefficients,
|
|
SDL_CHROMA_LOCATION_NONE);
|
|
SDL_SetSurfaceColorspace(surface, colorspace);
|
|
|
|
// Check if this is an HDR image by transfer function
|
|
bool isHDR = (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084 ||
|
|
image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_HLG);
|
|
|
|
if (isHDR) {
|
|
// Use metadata if available
|
|
if (image->clli.maxCLL > 0) {
|
|
maxCLL = image->clli.maxCLL;
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXCLL_NUMBER, image->clli.maxCLL);
|
|
} else {
|
|
// Default values based on common HDR mastering practices
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXCLL_NUMBER, DEFAULT_PQ_MAXCLL);
|
|
}
|
|
|
|
if (image->clli.maxPALL > 0) {
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXFALL_NUMBER, image->clli.maxPALL);
|
|
} else {
|
|
// A reasonable default for MaxFALL
|
|
SDL_SetNumberProperty(props, SDL_PROP_SURFACE_MAXFALL_NUMBER, DEFAULT_PQ_MAXCLL / 4);
|
|
}
|
|
|
|
// HDR properties needed for proper tone mapping
|
|
SDL_SetFloatProperty(props, SDL_PROP_SURFACE_SDR_WHITE_POINT_FLOAT, DEFAULT_PQ_SDR_WHITE_POINT);
|
|
SDL_SetFloatProperty(props, SDL_PROP_SURFACE_HDR_HEADROOM_FLOAT, (float)maxCLL / DEFAULT_PQ_SDR_WHITE_POINT);
|
|
}
|
|
}
|
|
|
|
struct IMG_AnimationDecoderContext
|
|
{
|
|
avifDecoder *decoder; /* AVIF decoder instance */
|
|
avifIO io; /* IO context for the decoder */
|
|
avifIOContext ioContext; /* Context data for IO operations */
|
|
Sint64 start_pos; /* Starting position in the stream */
|
|
|
|
int current_frame; /* Current frame index */
|
|
int total_frames; /* Total number of frames in the animation */
|
|
|
|
int width; /* Width of the animation */
|
|
int height; /* Height of the animation */
|
|
};
|
|
|
|
static bool IMG_AnimationDecoderReset_Internal(IMG_AnimationDecoder *decoder)
|
|
{
|
|
IMG_AnimationDecoderContext *ctx = decoder->ctx;
|
|
|
|
// Reset the decoder
|
|
if (ctx->decoder) {
|
|
lib.avifDecoderDestroy(ctx->decoder);
|
|
ctx->decoder = NULL;
|
|
}
|
|
|
|
// Reset stream position
|
|
if (SDL_SeekIO(decoder->src, ctx->start_pos, SDL_IO_SEEK_SET) < 0) {
|
|
return SDL_SetError("Failed to seek to beginning of AVIF file");
|
|
}
|
|
|
|
// Recreate the decoder
|
|
ctx->decoder = lib.avifDecoderCreate();
|
|
if (!ctx->decoder) {
|
|
return SDL_SetError("Couldn't create AVIF decoder");
|
|
}
|
|
|
|
// Be permissive to decode as many frames as possible
|
|
ctx->decoder->strictFlags = AVIF_STRICT_DISABLED;
|
|
|
|
// Set up IO
|
|
if (ctx->ioContext.data) {
|
|
SDL_free(ctx->ioContext.data);
|
|
}
|
|
|
|
ctx->ioContext.src = decoder->src;
|
|
ctx->ioContext.start = ctx->start_pos;
|
|
ctx->ioContext.data = NULL;
|
|
ctx->ioContext.size = 0;
|
|
|
|
ctx->io.destroy = DestroyAVIFIO;
|
|
ctx->io.read = ReadAVIFIO;
|
|
ctx->io.data = &ctx->ioContext;
|
|
lib.avifDecoderSetIO(ctx->decoder, &ctx->io);
|
|
|
|
// Reset state
|
|
ctx->current_frame = 0;
|
|
|
|
// Need to re-parse the animation if we reset
|
|
avifResult result = lib.avifDecoderParse(ctx->decoder);
|
|
if (result != AVIF_RESULT_OK) {
|
|
return SDL_SetError("Couldn't re-parse AVIF animation after reset: %s", lib.avifResultToString(result));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool IMG_AnimationDecoderGetNextFrame_Internal(IMG_AnimationDecoder *decoder, SDL_Surface **frame, Uint64 *duration)
|
|
{
|
|
IMG_AnimationDecoderContext *ctx = decoder->ctx;
|
|
avifResult result;
|
|
|
|
if (ctx->total_frames - ctx->current_frame < 1) {
|
|
decoder->status = IMG_DECODER_STATUS_COMPLETE;
|
|
return false;
|
|
}
|
|
|
|
result = lib.avifDecoderNextImage(ctx->decoder);
|
|
if (result != AVIF_RESULT_OK) {
|
|
if (result == AVIF_RESULT_NO_IMAGES_REMAINING) {
|
|
// This shouldn't happen here, but handle it gracefully
|
|
decoder->status = IMG_DECODER_STATUS_COMPLETE;
|
|
return false;
|
|
}
|
|
|
|
return SDL_SetError("Couldn't get AVIF frame %d: %s", ctx->current_frame + 1, lib.avifResultToString(result));
|
|
}
|
|
|
|
avifImage *image = ctx->decoder->image;
|
|
SDL_Surface *frame_surface = NULL;
|
|
|
|
if (image->depth == 16) {
|
|
// Handle 16-bit depth
|
|
avifRGBImage rgb;
|
|
SDL_zero(rgb);
|
|
|
|
rgb.width = image->width;
|
|
rgb.height = image->height;
|
|
rgb.depth = 16;
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
|
|
frame_surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_RGBA64);
|
|
if (!frame_surface) {
|
|
return SDL_SetError("Couldn't create 16-bit surface for AVIF frame");
|
|
}
|
|
|
|
rgb.pixels = (uint8_t *)frame_surface->pixels;
|
|
rgb.rowBytes = (uint32_t)frame_surface->pitch;
|
|
rgb.ignoreAlpha = false;
|
|
|
|
result = lib.avifImageYUVToRGB(image, &rgb);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_DestroySurface(frame_surface);
|
|
return SDL_SetError("Couldn't convert 16-bit AVIF image to RGB: %s", lib.avifResultToString(result));
|
|
}
|
|
|
|
// Set HDR properties if needed
|
|
SetHDRProperties(frame_surface, image);
|
|
} else if (image->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SMPTE2084) {
|
|
// Handle HDR PQ image
|
|
if (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY &&
|
|
image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) {
|
|
if (image->depth == 10) {
|
|
frame_surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_XBGR2101010);
|
|
if (frame_surface) {
|
|
if (ConvertGBR444toXBGR2101010(image, frame_surface) < 0) {
|
|
SDL_DestroySurface(frame_surface);
|
|
frame_surface = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!frame_surface) {
|
|
avifRGBImage rgb;
|
|
SDL_zero(rgb);
|
|
|
|
rgb.width = image->width;
|
|
rgb.height = image->height;
|
|
rgb.depth = 16;
|
|
rgb.format = AVIF_RGB_FORMAT_RGB;
|
|
rgb.rowBytes = (uint32_t)image->width * 3 * sizeof(Uint16);
|
|
rgb.pixels = (uint8_t *)SDL_malloc(image->height * rgb.rowBytes);
|
|
if (!rgb.pixels) {
|
|
return SDL_SetError("Out of memory for AVIF RGB pixels");
|
|
}
|
|
|
|
result = lib.avifImageYUVToRGB(image, &rgb);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_free(rgb.pixels);
|
|
return SDL_SetError("Couldn't convert AVIF image to RGB: %s", lib.avifResultToString(result));
|
|
}
|
|
|
|
frame_surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_XBGR2101010);
|
|
if (frame_surface) {
|
|
ConvertRGB16toXBGR2101010(&rgb, frame_surface);
|
|
}
|
|
SDL_free(rgb.pixels);
|
|
}
|
|
|
|
if (frame_surface) {
|
|
SetHDRProperties(frame_surface, image);
|
|
}
|
|
} else {
|
|
frame_surface = SDL_CreateSurface(image->width, image->height, SDL_PIXELFORMAT_RGBA32);
|
|
if (!frame_surface) {
|
|
return SDL_SetError("Couldn't create surface for AVIF frame");
|
|
}
|
|
|
|
avifRGBImage rgb;
|
|
SDL_zero(rgb);
|
|
|
|
rgb.width = frame_surface->w;
|
|
rgb.height = frame_surface->h;
|
|
rgb.depth = 8;
|
|
|
|
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
#else
|
|
rgb.format = AVIF_RGB_FORMAT_ABGR;
|
|
#endif
|
|
|
|
rgb.ignoreAlpha = false;
|
|
rgb.pixels = (uint8_t *)frame_surface->pixels;
|
|
rgb.rowBytes = (uint32_t)frame_surface->pitch;
|
|
|
|
result = lib.avifImageYUVToRGB(image, &rgb);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_DestroySurface(frame_surface);
|
|
return SDL_SetError("Couldn't convert AVIF image to RGB: %s", lib.avifResultToString(result));
|
|
}
|
|
|
|
SDL_Colorspace colorspace = SDL_DEFINE_COLORSPACE(SDL_COLOR_TYPE_RGB,
|
|
SDL_COLOR_RANGE_FULL,
|
|
image->colorPrimaries,
|
|
image->transferCharacteristics,
|
|
image->matrixCoefficients,
|
|
SDL_CHROMA_LOCATION_NONE);
|
|
SDL_SetSurfaceColorspace(frame_surface, colorspace);
|
|
}
|
|
|
|
*duration = ctx->decoder->imageTiming.durationInTimescales * decoder->timebase_numerator;
|
|
|
|
ctx->current_frame++;
|
|
|
|
*frame = frame_surface;
|
|
return true;
|
|
}
|
|
|
|
static bool IMG_AnimationDecoderClose_Internal(IMG_AnimationDecoder *decoder)
|
|
{
|
|
IMG_AnimationDecoderContext *ctx = decoder->ctx;
|
|
|
|
if (ctx->decoder) {
|
|
lib.avifDecoderDestroy(ctx->decoder);
|
|
ctx->decoder = NULL;
|
|
}
|
|
|
|
if (ctx->ioContext.data) {
|
|
SDL_free(ctx->ioContext.data);
|
|
ctx->ioContext.data = NULL;
|
|
ctx->ioContext.size = 0;
|
|
}
|
|
|
|
SDL_free(ctx);
|
|
decoder->ctx = NULL;
|
|
return true;
|
|
}
|
|
|
|
bool IMG_CreateAVIFAnimationDecoder(IMG_AnimationDecoder *decoder, SDL_PropertiesID props)
|
|
{
|
|
if (!IMG_InitAVIF()) {
|
|
return false;
|
|
}
|
|
|
|
IMG_AnimationDecoderContext *ctx = (IMG_AnimationDecoderContext *)SDL_calloc(1, sizeof(*ctx));
|
|
if (!ctx) {
|
|
return false;
|
|
}
|
|
|
|
ctx->start_pos = SDL_TellIO(decoder->src);
|
|
if (ctx->start_pos < 0) {
|
|
SDL_free(ctx);
|
|
return SDL_SetError("Failed to get current stream position");
|
|
}
|
|
|
|
ctx->decoder = lib.avifDecoderCreate();
|
|
if (!ctx->decoder) {
|
|
SDL_free(ctx);
|
|
return SDL_SetError("Couldn't create AVIF decoder");
|
|
}
|
|
|
|
ctx->decoder->strictFlags = AVIF_STRICT_DISABLED;
|
|
ctx->decoder->timescale = decoder->timebase_denominator;
|
|
|
|
ctx->ioContext.src = decoder->src;
|
|
ctx->ioContext.start = ctx->start_pos;
|
|
ctx->ioContext.data = NULL;
|
|
ctx->ioContext.size = 0;
|
|
|
|
ctx->io.destroy = DestroyAVIFIO;
|
|
ctx->io.read = ReadAVIFIO;
|
|
ctx->io.data = &ctx->ioContext;
|
|
lib.avifDecoderSetIO(ctx->decoder, &ctx->io);
|
|
|
|
ctx->current_frame = 0;
|
|
|
|
int maxLCores = SDL_GetNumLogicalCPUCores();
|
|
int maxThreads = (int)SDL_GetNumberProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_AVIF_MAX_THREADS_NUMBER, maxLCores / 2);
|
|
maxThreads = SDL_clamp(maxThreads, 1, maxLCores);
|
|
ctx->decoder->maxThreads = maxThreads;
|
|
|
|
bool allowProgressive = SDL_GetBooleanProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_AVIF_ALLOW_PROGRESSIVE_BOOLEAN, true);
|
|
ctx->decoder->allowProgressive = allowProgressive ? AVIF_TRUE : AVIF_FALSE;
|
|
|
|
bool allowIncremental = SDL_GetBooleanProperty(props, IMG_PROP_ANIMATION_DECODER_CREATE_AVIF_ALLOW_PROGRESSIVE_BOOLEAN, false);
|
|
ctx->decoder->allowIncremental = allowIncremental ? AVIF_TRUE : AVIF_FALSE;
|
|
|
|
bool ignoreProps = SDL_GetBooleanProperty(props, IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN, false);
|
|
ctx->decoder->ignoreExif = ignoreProps ? AVIF_TRUE : AVIF_FALSE;
|
|
ctx->decoder->ignoreXMP = ignoreProps ? AVIF_TRUE : AVIF_FALSE;
|
|
|
|
decoder->ctx = ctx;
|
|
decoder->Reset = IMG_AnimationDecoderReset_Internal;
|
|
decoder->GetNextFrame = IMG_AnimationDecoderGetNextFrame_Internal;
|
|
decoder->Close = IMG_AnimationDecoderClose_Internal;
|
|
|
|
avifResult result = lib.avifDecoderParse(ctx->decoder);
|
|
if (result != AVIF_RESULT_OK) {
|
|
SDL_SetError("Couldn't parse AVIF animation: %s", lib.avifResultToString(result));
|
|
SDL_free(ctx);
|
|
return false;
|
|
}
|
|
|
|
if (ctx->decoder->imageCount < 1) {
|
|
SDL_SetError("No frames found in AVIF");
|
|
SDL_free(ctx);
|
|
return false;
|
|
}
|
|
|
|
ctx->width = ctx->decoder->image->width;
|
|
ctx->height = ctx->decoder->image->height;
|
|
ctx->total_frames = ctx->decoder->imageCount;
|
|
|
|
if (!ignoreProps) {
|
|
// Allow implicit properties to be set which are not globalized but specific to the decoder.
|
|
SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_FRAME_COUNT_NUMBER, ctx->total_frames);
|
|
|
|
// Set well-defined properties.
|
|
if (ctx->decoder->repetitionCount != AVIF_REPETITION_COUNT_UNKNOWN) {
|
|
SDL_SetNumberProperty(decoder->props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, ctx->decoder->repetitionCount + 1);
|
|
}
|
|
|
|
// Get other well-defined properties and set them in our props.
|
|
if (!ctx->decoder->ignoreXMP) {
|
|
uint8_t *data = ctx->decoder->image->xmp.data;
|
|
size_t len = ctx->decoder->image->xmp.size;
|
|
if (data && len > 0) {
|
|
char *desc = __xmlman_GetXMPDescription(data, len);
|
|
char *rights = __xmlman_GetXMPCopyright(data, len);
|
|
char *title = __xmlman_GetXMPTitle(data, len);
|
|
char *creator = __xmlman_GetXMPCreator(data, len);
|
|
char *createDate = __xmlman_GetXMPCreateDate(data, len);
|
|
if (desc) {
|
|
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_DESCRIPTION_STRING, desc);
|
|
SDL_free(desc);
|
|
}
|
|
if (rights) {
|
|
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_COPYRIGHT_STRING, rights);
|
|
SDL_free(rights);
|
|
}
|
|
if (title) {
|
|
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_TITLE_STRING, title);
|
|
SDL_free(title);
|
|
}
|
|
if (creator) {
|
|
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_AUTHOR_STRING, creator);
|
|
SDL_free(creator);
|
|
}
|
|
if (createDate) {
|
|
SDL_SetStringProperty(decoder->props, IMG_PROP_METADATA_CREATION_TIME_STRING, createDate);
|
|
SDL_free(createDate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#else
|
|
|
|
bool IMG_CreateAVIFAnimationDecoder(IMG_AnimationDecoder* decoder, SDL_PropertiesID props)
|
|
{
|
|
return SDL_SetError("SDL_image built without AVIF animation support");
|
|
}
|
|
|
|
#endif /* LOAD_AVIF */
|
|
|
|
#if SAVE_AVIF
|
|
|
|
struct IMG_AnimationEncoderContext
|
|
{
|
|
avifEncoder *encoder;
|
|
bool first_frame_added;
|
|
SDL_PropertiesID metadata;
|
|
};
|
|
|
|
static bool AnimationEncoder_AddFrame(IMG_AnimationEncoder *encoder, SDL_Surface *surface, Uint64 duration)
|
|
{
|
|
avifImage *image = NULL;
|
|
avifRGBImage rgb;
|
|
avifResult rc;
|
|
SDL_Colorspace colorspace;
|
|
Uint16 maxCLL, maxFALL;
|
|
SDL_PropertiesID props;
|
|
uint64_t durationInTimescales;
|
|
bool lockedSurf = false;
|
|
SDL_Surface *temp = NULL;
|
|
bool temp_is_copy = false;
|
|
bool isLossless = encoder->quality == 100;
|
|
bool is10bit = SDL_ISPIXELFORMAT_10BIT(surface->format);
|
|
bool is16bit = (surface->format == SDL_PIXELFORMAT_RGB48 ||
|
|
surface->format == SDL_PIXELFORMAT_BGR48 ||
|
|
surface->format == SDL_PIXELFORMAT_RGBA64 ||
|
|
surface->format == SDL_PIXELFORMAT_ARGB64 ||
|
|
surface->format == SDL_PIXELFORMAT_BGRA64 ||
|
|
surface->format == SDL_PIXELFORMAT_ABGR64);
|
|
bool hasAlpha = SDL_ISPIXELFORMAT_ALPHA(surface->format);
|
|
|
|
durationInTimescales = duration * encoder->timebase_numerator;
|
|
colorspace = SDL_GetSurfaceColorspace(surface);
|
|
props = SDL_GetSurfaceProperties(surface);
|
|
maxCLL = (Uint16)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_MAXCLL_NUMBER, 0);
|
|
maxFALL = (Uint16)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_MAXFALL_NUMBER, 0);
|
|
|
|
uint32_t depth = is16bit ? 16 : (is10bit ? 10 : 8);
|
|
avifPixelFormat pixelFormat = (isLossless) ? AVIF_PIXEL_FORMAT_NONE : AVIF_PIXEL_FORMAT_YUV444;
|
|
|
|
image = lib.avifImageCreate(surface->w, surface->h, depth, pixelFormat);
|
|
if (!image) {
|
|
return SDL_SetError("Couldn't create AVIF image");
|
|
}
|
|
|
|
if (!encoder->ctx->first_frame_added && IMG_HasMetadata(encoder->ctx->metadata)) {
|
|
size_t outlen = 0;
|
|
uint8_t *xmp_data = __xmlman_ConstructXMPWithRDFDescription(SDL_GetStringProperty(encoder->ctx->metadata, IMG_PROP_METADATA_TITLE_STRING, NULL), SDL_GetStringProperty(encoder->ctx->metadata, IMG_PROP_METADATA_AUTHOR_STRING, NULL), SDL_GetStringProperty(encoder->ctx->metadata, IMG_PROP_METADATA_DESCRIPTION_STRING, NULL), SDL_GetStringProperty(encoder->ctx->metadata, IMG_PROP_METADATA_COPYRIGHT_STRING, NULL), SDL_GetStringProperty(encoder->ctx->metadata, IMG_PROP_METADATA_CREATION_TIME_STRING, NULL), &outlen);
|
|
if (!xmp_data || outlen < 1) {
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't create XMP data for AVIF image");
|
|
}
|
|
avifResult ar = lib.avifImageSetMetadataXMP(image, xmp_data, outlen);
|
|
if (ar != AVIF_RESULT_OK) {
|
|
lib.avifImageDestroy(image);
|
|
SDL_free(xmp_data);
|
|
return SDL_SetError("Couldn't set XMP metadata for AVIF image: %s", lib.avifResultToString(ar));
|
|
}
|
|
SDL_free(xmp_data);
|
|
}
|
|
|
|
image->yuvRange = AVIF_RANGE_FULL;
|
|
image->colorPrimaries = (avifColorPrimaries)SDL_COLORSPACEPRIMARIES(colorspace);
|
|
image->transferCharacteristics = (avifTransferCharacteristics)SDL_COLORSPACETRANSFER(colorspace);
|
|
|
|
if (is10bit || is16bit || isLossless) {
|
|
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY;
|
|
} else {
|
|
image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT709;
|
|
}
|
|
|
|
image->clli.maxCLL = maxCLL;
|
|
image->clli.maxPALL = maxFALL;
|
|
|
|
SDL_zero(rgb);
|
|
lib.avifRGBImageSetDefaults(&rgb, image);
|
|
|
|
if (SDL_MUSTLOCK(surface)) {
|
|
if (!SDL_LockSurface(surface)) {
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't lock surface for reading");
|
|
}
|
|
lockedSurf = true;
|
|
}
|
|
|
|
if (is16bit) {
|
|
rgb.depth = 16;
|
|
|
|
switch (surface->format) {
|
|
case SDL_PIXELFORMAT_RGB48:
|
|
rgb.format = AVIF_RGB_FORMAT_RGB;
|
|
break;
|
|
case SDL_PIXELFORMAT_BGR48:
|
|
rgb.format = AVIF_RGB_FORMAT_BGR;
|
|
break;
|
|
case SDL_PIXELFORMAT_RGBA64:
|
|
rgb.format = AVIF_RGB_FORMAT_RGBA;
|
|
break;
|
|
case SDL_PIXELFORMAT_ARGB64:
|
|
rgb.format = AVIF_RGB_FORMAT_ARGB;
|
|
break;
|
|
case SDL_PIXELFORMAT_BGRA64:
|
|
rgb.format = AVIF_RGB_FORMAT_BGRA;
|
|
break;
|
|
case SDL_PIXELFORMAT_ABGR64:
|
|
rgb.format = AVIF_RGB_FORMAT_ABGR;
|
|
break;
|
|
default:
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Received a pixelformat that isn't 16 bit for 16 bit encoding");
|
|
}
|
|
|
|
rgb.ignoreAlpha = !hasAlpha;
|
|
rgb.pixels = (uint8_t *)surface->pixels;
|
|
rgb.rowBytes = (uint32_t)surface->pitch;
|
|
|
|
if (!isLossless) {
|
|
rc = lib.avifImageRGBToYUV(image, &rgb);
|
|
|
|
if (rc != AVIF_RESULT_OK) {
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't convert 16-bit RGB to YUV: %s", lib.avifResultToString(rc));
|
|
}
|
|
}
|
|
} else if (is10bit) {
|
|
const Uint16 expand_alpha[] = {
|
|
0, 0x155, 0x2aa, 0x3ff
|
|
};
|
|
|
|
rgb.depth = 10;
|
|
rgb.format = (SDL_PIXELORDER(surface->format) == SDL_PACKEDORDER_XRGB ||
|
|
SDL_PIXELORDER(surface->format) == SDL_PACKEDORDER_ARGB) ?
|
|
AVIF_RGB_FORMAT_RGBA : AVIF_RGB_FORMAT_BGRA;
|
|
rgb.ignoreAlpha = !hasAlpha;
|
|
rgb.rowBytes = (uint32_t)image->width * 4 * sizeof(Uint16);
|
|
rgb.pixels = (uint8_t *)SDL_malloc(image->height * rgb.rowBytes);
|
|
|
|
if (!rgb.pixels) {
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Out of memory for RGB pixels");
|
|
}
|
|
|
|
// Convert 10-bit packed format to planar format for libavif
|
|
int width, height;
|
|
Uint16 *dst16 = (Uint16 *)rgb.pixels;
|
|
Uint32 *src = (Uint32 *)surface->pixels;
|
|
int srcskip = surface->pitch - (surface->w * sizeof(Uint32));
|
|
|
|
height = image->height;
|
|
while (height--) {
|
|
width = image->width;
|
|
while (width--) {
|
|
Uint32 pixelvalue = *src++;
|
|
|
|
// Extract 10-bit components
|
|
*dst16++ = (pixelvalue >> 20) & 0x3FF; // R
|
|
*dst16++ = (pixelvalue >> 10) & 0x3FF; // G
|
|
*dst16++ = (pixelvalue >> 0) & 0x3FF; // B
|
|
|
|
// Handle alpha - use full opacity for non-alpha formats
|
|
if (hasAlpha) {
|
|
*dst16++ = expand_alpha[(pixelvalue >> 30) & 0x3]; // A
|
|
} else {
|
|
*dst16++ = 0x3FF; // Full opacity
|
|
}
|
|
}
|
|
src = (Uint32 *)(((Uint8 *)src) + srcskip);
|
|
}
|
|
|
|
if (!isLossless) {
|
|
rc = lib.avifImageRGBToYUV(image, &rgb);
|
|
|
|
SDL_free(rgb.pixels);
|
|
rgb.pixels = NULL;
|
|
|
|
if (rc != AVIF_RESULT_OK) {
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't convert to YUV: %s", lib.avifResultToString(rc));
|
|
}
|
|
}
|
|
} else {
|
|
avifRGBFormat format;
|
|
bool needsConversion = true;
|
|
|
|
switch (surface->format) {
|
|
case SDL_PIXELFORMAT_RGBA32:
|
|
format = AVIF_RGB_FORMAT_RGBA;
|
|
needsConversion = false;
|
|
break;
|
|
case SDL_PIXELFORMAT_ARGB32:
|
|
format = AVIF_RGB_FORMAT_ARGB;
|
|
needsConversion = false;
|
|
break;
|
|
case SDL_PIXELFORMAT_BGRA32:
|
|
format = AVIF_RGB_FORMAT_BGRA;
|
|
needsConversion = false;
|
|
break;
|
|
case SDL_PIXELFORMAT_ABGR32:
|
|
format = AVIF_RGB_FORMAT_ABGR;
|
|
needsConversion = false;
|
|
break;
|
|
case SDL_PIXELFORMAT_RGB24:
|
|
format = AVIF_RGB_FORMAT_RGB;
|
|
needsConversion = true;
|
|
break;
|
|
case SDL_PIXELFORMAT_BGR24:
|
|
format = AVIF_RGB_FORMAT_BGR;
|
|
needsConversion = true;
|
|
break;
|
|
case SDL_PIXELFORMAT_RGBX32:
|
|
case SDL_PIXELFORMAT_XRGB32:
|
|
case SDL_PIXELFORMAT_BGRX32:
|
|
case SDL_PIXELFORMAT_XBGR32:
|
|
needsConversion = true;
|
|
format = AVIF_RGB_FORMAT_RGBA;
|
|
break;
|
|
default:
|
|
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
|
|
format = AVIF_RGB_FORMAT_ABGR;
|
|
#else
|
|
format = AVIF_RGB_FORMAT_RGBA;
|
|
#endif
|
|
needsConversion = true;
|
|
break;
|
|
}
|
|
|
|
if (needsConversion) {
|
|
Uint32 target_format = hasAlpha ? SDL_PIXELFORMAT_RGBA32 : SDL_PIXELFORMAT_RGBX32;
|
|
temp = SDL_ConvertSurface(surface, target_format);
|
|
if (!temp) {
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't convert surface to compatible format");
|
|
}
|
|
temp_is_copy = true;
|
|
|
|
rgb.format = format;
|
|
rgb.pixels = (uint8_t *)temp->pixels;
|
|
rgb.rowBytes = (uint32_t)temp->pitch;
|
|
} else {
|
|
rgb.format = format;
|
|
rgb.pixels = (uint8_t *)surface->pixels;
|
|
rgb.rowBytes = (uint32_t)surface->pitch;
|
|
}
|
|
|
|
rgb.depth = 8;
|
|
rgb.ignoreAlpha = !hasAlpha;
|
|
rgb.alphaPremultiplied = AVIF_FALSE;
|
|
rgb.chromaUpsampling = AVIF_CHROMA_UPSAMPLING_AUTOMATIC;
|
|
|
|
if (!isLossless) {
|
|
rc = lib.avifImageRGBToYUV(image, &rgb);
|
|
|
|
if (rc != AVIF_RESULT_OK) {
|
|
if (temp_is_copy && temp) {
|
|
SDL_DestroySurface(temp);
|
|
}
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
lib.avifImageDestroy(image);
|
|
return SDL_SetError("Couldn't convert to YUV: %s", lib.avifResultToString(rc));
|
|
}
|
|
}
|
|
}
|
|
|
|
avifAddImageFlags addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
|
|
if (isLossless || !encoder->ctx->first_frame_added) {
|
|
addImageFlags = AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME;
|
|
}
|
|
|
|
if (isLossless) {
|
|
if (is10bit && rgb.pixels) {
|
|
// For 10-bit lossless that required conversion
|
|
rc = lib.avifEncoderAddImage(encoder->ctx->encoder, image, durationInTimescales, addImageFlags);
|
|
SDL_free(rgb.pixels);
|
|
rgb.pixels = NULL;
|
|
} else {
|
|
// For direct encoding (8-bit, 16-bit, or pre-converted 10-bit)
|
|
rc = lib.avifEncoderAddImage(encoder->ctx->encoder, image, durationInTimescales, addImageFlags);
|
|
}
|
|
} else {
|
|
// For lossy encoding (YUV conversion already done)
|
|
rc = lib.avifEncoderAddImage(encoder->ctx->encoder, image, durationInTimescales, addImageFlags);
|
|
}
|
|
|
|
if (temp_is_copy && temp) {
|
|
SDL_DestroySurface(temp);
|
|
}
|
|
|
|
if (lockedSurf) {
|
|
SDL_UnlockSurface(surface);
|
|
}
|
|
|
|
lib.avifImageDestroy(image);
|
|
|
|
if (rc != AVIF_RESULT_OK) {
|
|
return SDL_SetError("Failed to add image to avif encoder: %s", lib.avifResultToString(rc));
|
|
}
|
|
|
|
if (!encoder->ctx->first_frame_added) {
|
|
encoder->ctx->first_frame_added = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool AnimationEncoder_End(struct IMG_AnimationEncoder* encoder)
|
|
{
|
|
avifRWData avifOutput = AVIF_DATA_EMPTY;
|
|
avifResult rc;
|
|
bool result = false;
|
|
|
|
if (!encoder->ctx || !encoder->ctx->encoder) {
|
|
return SDL_SetError("Invalid context or encoder");
|
|
}
|
|
|
|
rc = lib.avifEncoderFinish(encoder->ctx->encoder, &avifOutput);
|
|
if (rc != AVIF_RESULT_OK) {
|
|
SDL_SetError("Failed to finish encoder: %s", lib.avifResultToString(rc));
|
|
goto done;
|
|
}
|
|
|
|
if (SDL_WriteIO(encoder->dst, avifOutput.data, avifOutput.size) == avifOutput.size) {
|
|
result = true;
|
|
} else {
|
|
SDL_SetError("Failed to write AVIF data to IOStream");
|
|
}
|
|
|
|
done:
|
|
if (encoder->ctx->metadata) {
|
|
SDL_DestroyProperties(encoder->ctx->metadata);
|
|
encoder->ctx->metadata = 0;
|
|
}
|
|
if (encoder->ctx->encoder) {
|
|
lib.avifEncoderDestroy(encoder->ctx->encoder);
|
|
encoder->ctx->encoder = NULL;
|
|
}
|
|
if (encoder->ctx) {
|
|
SDL_free(encoder->ctx);
|
|
encoder->ctx = NULL;
|
|
}
|
|
lib.avifRWDataFree(&avifOutput);
|
|
|
|
return result;
|
|
}
|
|
|
|
bool IMG_CreateAVIFAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
|
|
{
|
|
if (!IMG_InitAVIF()) {
|
|
return false;
|
|
}
|
|
|
|
IMG_AnimationEncoderContext *ctx = (IMG_AnimationEncoderContext *)SDL_calloc(1, sizeof(*ctx));
|
|
if (!ctx) {
|
|
return SDL_SetError("Out of memory for AVIF context");
|
|
}
|
|
|
|
ctx->encoder = lib.avifEncoderCreate();
|
|
if (!ctx->encoder) {
|
|
SDL_free(ctx);
|
|
return SDL_SetError("Couldn't create AVIF encoder");
|
|
}
|
|
|
|
if (encoder->quality < 0) {
|
|
encoder->quality = 75;
|
|
} else if (encoder->quality > 100) {
|
|
encoder->quality = 100;
|
|
}
|
|
|
|
int availableLCores = SDL_GetNumLogicalCPUCores();
|
|
int threads = (int)SDL_GetNumberProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_AVIF_MAX_THREADS_NUMBER, availableLCores / 2);
|
|
threads = SDL_clamp(threads, 1, availableLCores);
|
|
|
|
int keyFrameInterval = (int)SDL_GetNumberProperty(props, IMG_PROP_ANIMATION_ENCODER_CREATE_AVIF_KEYFRAME_INTERVAL_NUMBER, 0);
|
|
|
|
ctx->encoder->maxThreads = threads;
|
|
ctx->encoder->quality = encoder->quality;
|
|
|
|
ctx->encoder->qualityAlpha = AVIF_QUALITY_DEFAULT;
|
|
ctx->encoder->speed = AVIF_SPEED_FASTEST;
|
|
|
|
if (ctx->encoder->quality >= 100) {
|
|
ctx->encoder->keyframeInterval = SDL_min(1, keyFrameInterval);
|
|
ctx->encoder->minQuantizer = 0;
|
|
ctx->encoder->maxQuantizer = 0;
|
|
|
|
ctx->encoder->minQuantizerAlpha = 0;
|
|
ctx->encoder->maxQuantizerAlpha = 0;
|
|
ctx->encoder->autoTiling = AVIF_FALSE;
|
|
} else {
|
|
ctx->encoder->keyframeInterval = keyFrameInterval;
|
|
ctx->encoder->minQuantizer = 8;
|
|
ctx->encoder->maxQuantizer = 63;
|
|
|
|
ctx->encoder->minQuantizerAlpha = 0;
|
|
ctx->encoder->maxQuantizerAlpha = 63;
|
|
ctx->encoder->autoTiling = AVIF_TRUE;
|
|
}
|
|
|
|
ctx->encoder->tileRowsLog2 = 0;
|
|
ctx->encoder->tileColsLog2 = 0;
|
|
|
|
ctx->encoder->timescale = encoder->timebase_denominator;
|
|
|
|
bool ignoreProps = SDL_GetBooleanProperty(props, IMG_PROP_METADATA_IGNORE_PROPS_BOOLEAN, false);
|
|
if (!ignoreProps) {
|
|
ctx->encoder->repetitionCount = (int)SDL_max(SDL_GetNumberProperty(props, IMG_PROP_METADATA_LOOP_COUNT_NUMBER, 0), 0) - 1;
|
|
|
|
ctx->metadata = SDL_CreateProperties();
|
|
if (!ctx->metadata) {
|
|
lib.avifEncoderDestroy(ctx->encoder);
|
|
SDL_free(ctx);
|
|
return SDL_SetError("Couldn't create properties ID for AVIF encoder metadata");
|
|
}
|
|
|
|
if (!SDL_CopyProperties(props, ctx->metadata)) {
|
|
lib.avifEncoderDestroy(ctx->encoder);
|
|
SDL_DestroyProperties(ctx->metadata);
|
|
SDL_free(ctx);
|
|
return SDL_SetError("Couldn't copy properties to AVIF encoder metadata");
|
|
}
|
|
}
|
|
|
|
encoder->ctx = ctx;
|
|
encoder->AddFrame = AnimationEncoder_AddFrame;
|
|
encoder->Close = AnimationEncoder_End;
|
|
|
|
return true;
|
|
}
|
|
|
|
#else
|
|
|
|
bool IMG_CreateAVIFAnimationEncoder(IMG_AnimationEncoder *encoder, SDL_PropertiesID props)
|
|
{
|
|
return SDL_SetError("SDL_image built without AVIF animation save support");
|
|
}
|
|
|
|
#endif /* SAVE_AVIF */
|