diff --git a/CMakeLists.txt b/CMakeLists.txt index b13767d..57cd95d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ OPTION(ENABLE_FREETYPE "Enable Freetype2 support" 0) OPTION(ENABLE_FONTCONFIG "Enable FontConfig support" 0) OPTION(ENABLE_WEBP "Enable WebP support" 0) OPTION(ENABLE_HEIF "Enable HEIF support" 0) +OPTION(ENABLE_AVIF "Enable AVIF support" 0) OPTION(ENABLE_RAQM "Enable RAQM support" 0) if (BUILD_TEST) @@ -124,6 +125,13 @@ else (USE_EXT_GD) FIND_PACKAGE(HEIF REQUIRED) ENDIF (ENABLE_HEIF) + IF (ENABLE_AVIF) + FIND_PACKAGE(libavif 0.8.2 REQUIRED CONFIG) + SET(HAVE_LIBAVIF 1) + SET(AVIF_LIBRARIES avif) + SET(AVIF_FOUND 1) + ENDIF (ENABLE_AVIF) + IF (ENABLE_LIQ) FIND_PACKAGE(LIQ REQUIRED) ENDIF (ENABLE_LIQ) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 55f386f..cdb066a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,3 +1,4 @@ +Ben Morss (morsssss) chapg Chen Pingping (Wilson) Chris Reuter diff --git a/README.md b/README.md index 96268ea..aa8fa3f 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ GD has builtin support for: It also has optional support for more formats via external libraries: +* [AVIF](https://en.wikipedia.org/wiki/AV1#AV1_Image_File_Format_(AVIF)) via [libavif](https://github.com/AOMediaCodec/libavif) * [HEIF](https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format) via [libheif](https://github.com/strukturag/libheif/) - * Also it includes [AVIF](https://en.wikipedia.org/wiki/AV1#AV1_Image_File_Format_%28AVIF%29) read support if your system's `libheif` has AV1 decoding. + * This includes [AVIF](https://en.wikipedia.org/wiki/AV1#AV1_Image_File_Format_%28AVIF%29) read support if your system's `libheif` has AV1 decoding. * [JPEG](https://en.wikipedia.org/wiki/JPEG) via [IJG/libjpeg](http://www.ijg.org/) or [libjpeg-turbo](http://libjpeg-turbo.virtualgl.org/) * Does not include [JPEG 2000](https://en.wikipedia.org/wiki/JPEG_2000) * [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) via [libpng](http://www.libpng.org/) diff --git a/configure.ac b/configure.ac index 5a4c357..535db68 100644 --- a/configure.ac +++ b/configure.ac @@ -289,6 +289,13 @@ dnl Check for heif support. GD_LIB_PKG_CHECK([LIBHEIF], [HEIF], [heif], [libheif >= 1.7.0], [ AC_CHECK_LIB([heif], [heif_get_version], [dnl AS_VAR_APPEND([LIBHEIF_LIBS], [" -lheif"]) + ]) +]) + +dnl Check for avif support. +GD_LIB_PKG_CHECK([LIBAVIF], [AVIF], [avif], [libavif >= 0.8.2], [ + AC_CHECK_LIB([avif], [avifVersion], [dnl + AS_VAR_APPEND([LIBAVIF_LIBS], [" -lavif"]) gd_found_lib=yes ]) ]) @@ -323,6 +330,7 @@ AC_MSG_RESULT([ Support for JPEG library: $gd_with_LIBJPEG Support for WebP library: $gd_with_LIBWEBP Support for HEIF library: $gd_with_LIBHEIF + Support for AVIF library: $gd_with_LIBAVIF Support for TIFF library: $gd_with_LIBTIFF Support for Freetype 2.x library: $gd_with_LIBFREETYPE Support for Fontconfig library: $gd_with_LIBFONTCONFIG diff --git a/docs/README.CMAKE b/docs/README.CMAKE index 2be2045..c9356b2 100644 --- a/docs/README.CMAKE +++ b/docs/README.CMAKE @@ -20,6 +20,7 @@ ENABLE_FREETYPE=1 ENABLE_FONTCONFIG=1 ENABLE_XPM=1 ENABLE_WEBP=1 +ENABLE_AVIF=1 ENABLE_RAQM=1 You can optionally run our tests suite using: diff --git a/docs/naturaldocs/project/Menu.txt b/docs/naturaldocs/project/Menu.txt index 250d988..12df023 100644 --- a/docs/naturaldocs/project/Menu.txt +++ b/docs/naturaldocs/project/Menu.txt @@ -50,6 +50,7 @@ File: About LibGD 2.3.0-dev (no auto-title, preamble.txt) Group: Image Formats { + File: AVIF IO (no auto-title, gd_avif.c) File: BMP IO (no auto-title, gd_bmp.c) File: GD IO (no auto-title, gd_gd.c) File: GD2 IO (no auto-title, gd_gd2.c) diff --git a/examples/avif2jpeg.c b/examples/avif2jpeg.c new file mode 100644 index 0000000..6ced15d --- /dev/null +++ b/examples/avif2jpeg.c @@ -0,0 +1,55 @@ +/** + * A short program which converts a .avif file into a .jpg file - + * just to get a little practice with the basic functionality. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include +#include + +#include "gd.h" + +int main(int argc, char **argv) +{ + gdImagePtr im; + FILE *in, *out; + + if (argc != 3) { + fprintf(stderr, "Usage: avif2jpeg infile.avif outfile.jpg\n"); + exit(1); + } + + printf("Reading infile %s\n", argv[1]); + + in = fopen(argv[1], "rb"); + if (!in) { + fprintf(stderr, "\nError: input file %s does not exist.\n", argv[1]); + exit(1); + } + + im = gdImageCreateFromAvif(in); + fclose(in); + if (!im) { + fprintf(stderr, "\nError: input file %s is not in AVIF format.\n", argv[1]); + exit(1); + } + + out = fopen(argv[2], "wb"); + if (!out) { + fprintf(stderr, "\nError: can't write to output file %s\n", argv[2]); + gdImageDestroy(im); + exit(1); + } + + gdImageJpeg(im, out, 75); + + printf("Wrote outfile %s.\n", argv[2]); + + fclose(out); + gdImageDestroy(im); + + return 0; +} diff --git a/examples/jpeg2avif.c b/examples/jpeg2avif.c new file mode 100644 index 0000000..af55201 --- /dev/null +++ b/examples/jpeg2avif.c @@ -0,0 +1,57 @@ +/** + * A short program which converts a .jpg file into a .avif file - + * just to get a little practice with the basic functionality. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include +#include + +#include "gd.h" + +int main(int argc, char **argv) +{ + gdImagePtr im; + FILE *in, *out; + + if (argc != 3) { + fprintf(stderr, "Usage: jpeg2avif filename.jpg filename.avif\n"); + exit(1); + } + + printf("Reading infile %s\n", argv[1]); + + in = fopen(argv[1], "rb"); + if (!in) { + fprintf(stderr, "Error: input file %s does not exist.\n", argv[1]); + exit(1); + } + + im = gdImageCreateFromJpeg(in); + fclose(in); + if (!im) { + fprintf(stderr, "Error: input file %s is not in JPEG format.\n", argv[1]); + exit(1); + } + + out = fopen(argv[2], "wb"); + if (!out) { + fprintf(stderr, "Error: can't write to output file %s\n", argv[2]); + gdImageDestroy(im); + exit(1); + } + + fprintf(stderr, "Encoding...\n"); + + gdImageAvif(im, out); + + printf("Wrote outfile %s.\n", argv[2]); + + fclose(out); + gdImageDestroy(im); + + return 0; +} diff --git a/examples/jpeg2avifex.c b/examples/jpeg2avifex.c new file mode 100644 index 0000000..a8afb3b --- /dev/null +++ b/examples/jpeg2avifex.c @@ -0,0 +1,98 @@ +/** + * A short program which converts a .jpg file into a .avif file - + * just to get a little practice with the basic functionality. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include +#include +#include +#ifdef HAVE_UNISTD_H +#include +#endif + +#include "gd.h" + +static void usage() { + fprintf(stderr, "Usage: jpeg2avifex [-q quality] [-s speed] infile.jpg outfile.avif\n"); + exit(1); +} + +int main(int argc, char **argv) +{ + gdImagePtr im; + FILE *in, *out; + int c; + int speed = -1, quality = -1; // use default values if unspecified + char *infile, *outfile; + int failed = 0; + + if (argc < 3) { + usage(); + } + + while ((c = getopt(argc, argv, "q:s:")) != -1) { + switch (c) { + case 'q': + quality = atoi(optarg); + break; + + case 's': + speed = atoi(optarg); + break; + + default: + usage(); + } + } + + if (optind > argc - 2) + usage(); + + infile = strdup(argv[optind++]); + outfile = strdup(argv[optind]); + + printf("Reading infile %s\n", infile); + + in = fopen(infile, "rb"); + if (!in) { + fprintf(stderr, "\nError: input file %s does not exist.\n", infile); + failed = 1; + goto cleanup; + } + + im = gdImageCreateFromJpeg(in); + fclose(in); + if (!im) { + fprintf(stderr, "\nError: input file %s is not in JPEG format.\n", infile); + failed = 1; + goto cleanup; + } + + out = fopen(outfile, "wb"); + if (!out) { + fprintf(stderr, "\nError: can't write to output file %s\n", outfile); + failed = 1; + goto cleanup; + } + + fprintf(stderr, "Encoding...\n"); + + gdImageAvifEx(im, out, quality, speed); + + printf("Wrote outfile %s.\n", outfile); + + fclose(out); + +cleanup: + if (im) + gdImageDestroy(im); + + gdFree(infile); + gdFree(outfile); + + exit(failed); +} diff --git a/examples/png2avif.c b/examples/png2avif.c new file mode 100644 index 0000000..18463e7 --- /dev/null +++ b/examples/png2avif.c @@ -0,0 +1,58 @@ +/** + * A short program which converts a .png file into a .avif file - + * just to get a little practice with the basic functionality. + * We convert losslessly. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include +#include + +#include "gd.h" + +int main(int argc, char **argv) +{ + gdImagePtr im; + FILE *in, *out; + + if (argc != 3) { + fprintf(stderr, "Usage: png2avif infile.png outfile.avif\n"); + exit(1); + } + + printf("Reading infile %s\n", argv[1]); + + in = fopen(argv[1], "rb"); + if (!in) { + fprintf(stderr, "Error: input file %s does not exist.\n", argv[1]); + exit(1); + } + + im = gdImageCreateFromPng(in); + fclose(in); + if (!im) { + fprintf(stderr, "Error: input file %s is not in PNG format.\n", argv[1]); + exit(1); + } + + out = fopen(argv[2], "wb"); + if (!out) { + fprintf(stderr, "Error: can't write to output file %s\n", argv[2]); + gdImageDestroy(im); + exit(1); + } + + fprintf(stderr, "Encoding...\n"); + + gdImageAvifEx(im, out, 100, 0); + + printf("Wrote outfile %s.\n", argv[2]); + + fclose(out); + gdImageDestroy(im); + + return 0; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ccd76e9..509c422 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,6 +2,7 @@ SET (LIBGD_SRC_FILES bmp.h gd.c gd.h + gd_avif.c gd_bmp.c gd_color.c gd_color.h @@ -130,6 +131,7 @@ SET(LIBGD_DEP_LIBS ${XPM_LIBRARIES} ${FONTCONFIG_LIBRARY} ${WEBP_LIBRARIES} + ${AVIF_LIBRARIES} ${RAQM_LIBRARIES} ${HEIF_LIBRARIES} ) diff --git a/src/Makefile.am b/src/Makefile.am index 69a5c18..09bf192 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -60,6 +60,7 @@ libgd_la_SOURCES = \ bmp.h \ gd.c \ gd.h \ + gd_avif.c \ gd_bmp.c \ gd_color.c \ gd_color.h \ diff --git a/src/config.h.cmake b/src/config.h.cmake index fb86f3c..2b46a17 100644 --- a/src/config.h.cmake +++ b/src/config.h.cmake @@ -27,12 +27,18 @@ /* Define to 1 if you have the header file. */ #cmakedefine HAVE_INTTYPES_H +/* Define if you have avif */ +#cmakedefine HAVE_LIBAVIF + /* Define if you have fontconfig */ #cmakedefine HAVE_LIBFONTCONFIG /* Define if you have freetype */ #cmakedefine HAVE_LIBFREETYPE +/* Define if you have heif */ +#cmakedefine HAVE_LIBHEIF + /* Define if you have liq */ #cmakedefine HAVE_LIBIMAGEQUANT @@ -54,9 +60,6 @@ /* Define if you have webp */ #cmakedefine HAVE_LIBWEBP -/* Define if you have heif */ -#cmakedefine HAVE_LIBHEIF - /* Define if you have xpm */ #cmakedefine HAVE_LIBXPM diff --git a/src/gd.h b/src/gd.h index 55fa5b3..7b5870f 100644 --- a/src/gd.h +++ b/src/gd.h @@ -668,6 +668,10 @@ BGD_DECLARE(gdImagePtr) gdImageCreateFromHeif(FILE *inFile); BGD_DECLARE(gdImagePtr) gdImageCreateFromHeifPtr(int size, void *data); BGD_DECLARE(gdImagePtr) gdImageCreateFromHeifCtx(gdIOCtx *infile); +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvif(FILE *inFile); +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifPtr(int size, void *data); +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifCtx(gdIOCtx *infile); + BGD_DECLARE(gdImagePtr) gdImageCreateFromTiff(FILE *inFile); BGD_DECLARE(gdImagePtr) gdImageCreateFromTiffCtx(gdIOCtx *infile); BGD_DECLARE(gdImagePtr) gdImageCreateFromTiffPtr(int size, void *data); @@ -1131,6 +1135,11 @@ BGD_DECLARE(void *) gdImageHeifPtr(gdImagePtr im, int *size); BGD_DECLARE(void *) gdImageHeifPtrEx(gdImagePtr im, int *size, int quality, gdHeifCodec codec, gdHeifChroma chroma); BGD_DECLARE(void) gdImageHeifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, gdHeifCodec codec, gdHeifChroma chroma); +BGD_DECLARE(void) gdImageAvif(gdImagePtr im, FILE *outFile); +BGD_DECLARE(void) gdImageAvifEx(gdImagePtr im, FILE *outFile, int quality, int speed); +BGD_DECLARE(void *) gdImageAvifPtr(gdImagePtr im, int *size); +BGD_DECLARE(void *) gdImageAvifPtrEx(gdImagePtr im, int *size, int quality, int speed); +BGD_DECLARE(void) gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed); /** * Group: GifAnim diff --git a/src/gd_avif.c b/src/gd_avif.c new file mode 100644 index 0000000..d8f8284 --- /dev/null +++ b/src/gd_avif.c @@ -0,0 +1,680 @@ +/** + * File: AVIF IO + * + * Read and write AVIF images using libavif (https://github.com/AOMediaCodec/libavif) . + * Currently, the only ICC profile we support is sRGB. + * Since that's what web browsers use, it's sufficient for now. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include +#include + +#include "gd.h" +#include "gd_errors.h" +#include "gdhelpers.h" +#include "gd_intern.h" + +#ifdef HAVE_LIBAVIF +#include + +/* + Define defaults for encoding images: + CHROMA_SUBSAMPLING_DEFAULT: 4:2:0 is commonly used for Chroma subsampling. + CHROMA_SUBAMPLING_HIGH_QUALITY: Use 4:4:4, or no subsampling, when a sufficient high quality is requested. + SUBAMPLING_HIGH_QUALITY_THRESHOLD: At or above this value, use CHROMA_SUBAMPLING_HIGH_QUALITY + QUANTIZER_DEFAULT: + We need more testing to really know what quantizer settings are optimal, + but teams at Google have been using maximum=30 as a starting point. + QUALITY_DEFAULT: following gd conventions, -1 indicates the default. + SPEED_DEFAULT: AVIF_SPEED_DEFAULT is -1. This simply tells the AVIF encoder to use the default speed. +*/ + +#define CHROMA_SUBSAMPLING_DEFAULT AVIF_PIXEL_FORMAT_YUV420 +#define CHROMA_SUBAMPLING_HIGH_QUALITY AVIF_PIXEL_FORMAT_YUV444 +#define HIGH_QUALITY_SUBSAMPLING_THRESHOLD 90 +#define QUANTIZER_DEFAULT 30 +#define QUALITY_DEFAULT -1 +#define SPEED_DEFAULT AVIF_SPEED_DEFAULT + +// This initial size for the gdIOCtx is standard among GD image conversion functions. +#define NEW_DYNAMIC_CTX_SIZE 2048 + +// Our quality param ranges from 0 to 100. +// To calculate quality, we convert from AVIF's quantizer scale, which runs from 63 to 0. +#define MAX_QUALITY 100 + +// These constants are for computing the number of tiles and threads to use during encoding. +// Maximum threads are from libavif/contrib/gkd-pixbuf/loader.c. +#define MIN_TILE_AREA (512 * 512) +#define MAX_TILES 8 +#define MAX_THREADS 64 + +/*** Macros ***/ + +/* + From gd_png.c: + convert the 7-bit alpha channel to an 8-bit alpha channel. + We do a little bit-flipping magic, repeating the MSB + as the LSB, to ensure that 0 maps to 0 and + 127 maps to 255. We also have to invert to match + PNG's convention in which 255 is opaque. +*/ +#define alpha7BitTo8Bit(alpha7Bit) \ + (alpha7Bit == 127 ? \ + 0 : \ + 255 - ((alpha7Bit << 1) + (alpha7Bit >> 6))) + +#define alpha8BitTo7Bit(alpha8Bit) (gdAlphaMax - (alpha8Bit >> 1)) + + +/*** Helper functions ***/ + +/* Convert the quality param we expose to the quantity params used by libavif. + The *Quantizer* params values can range from 0 to 63, with 0 = highest quality and 63 = worst. + We make the scale 0-100, and we reverse this, so that 0 = worst quality and 100 = highest. + + Values below 0 are set to 0, and values below MAX_QUALITY are set to MAX_QUALITY. +*/ +static int quality2Quantizer(int quality) { + int clampedQuality = CLAMP(quality, 0, MAX_QUALITY); + + float scaleFactor = (float) AVIF_QUANTIZER_WORST_QUALITY / (float) MAX_QUALITY; + + return round(scaleFactor * (MAX_QUALITY - clampedQuality)); +} + +/* + As of February 2021, this algorithm reflects the latest research on how many tiles + and threads to include for a given image size. + This is subject to change as research continues. + + Returns false if there was an error, true if all was well. + */ +static avifBool setEncoderTilesAndThreads(avifEncoder *encoder, avifRGBImage *rgb) { + int imageArea, tiles, tilesLog2, encoderTiles; + + // _gdImageAvifCtx(), the calling function, checks this operation for overflow + imageArea = rgb->width * rgb->height; + + tiles = (int) ceil((double) imageArea / MIN_TILE_AREA); + tiles = MIN(tiles, MAX_TILES); + tiles = MIN(tiles, MAX_THREADS); + + // The number of tiles in any dimension will always be a power of 2. We can only specify log(2)tiles. + + tilesLog2 = floor(log2(tiles)); + + // If the image's width is greater than the height, use more tile columns + // than tile rows to make the tile size close to a square. + + if (rgb->width >= rgb->height) { + encoder->tileRowsLog2 = tilesLog2 / 2; + encoder->tileColsLog2 = tilesLog2 - encoder->tileRowsLog2; + } else { + encoder->tileColsLog2 = tilesLog2 / 2; + encoder->tileRowsLog2 = tilesLog2 - encoder->tileColsLog2; + } + + // It's good to have one thread per tile. + encoderTiles = (1 << encoder->tileRowsLog2) * (1 << encoder->tileColsLog2); + encoder->maxThreads = encoderTiles; + + return AVIF_TRUE; +} + +/* + We can handle AVIF images whose color profile is sRGB, or whose color profile isn't set. +*/ +static avifBool isAvifSrgbImage(avifImage *avifIm) { + return + (avifIm->colorPrimaries == AVIF_COLOR_PRIMARIES_BT709 || + avifIm->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) && + (avifIm->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_SRGB || + avifIm->transferCharacteristics == AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) + ; +} + +/* + Check the result from an Avif function to see if it's an error. + If so, decode the error and output it, and return true. + Otherwise, return false. +*/ +static avifBool isAvifError(avifResult result, const char *msg) { + if (result != AVIF_RESULT_OK) { + gd_error("avif error - %s: %s", msg, avifResultToString(result)); + return AVIF_TRUE; + } + + return AVIF_FALSE; +} + + +/* + implements the avifIOReadFunc interface by calling the relevant functions + in the gdIOCtx. Our logic is inspired by avifIOMemoryReaderRead() and avifIOFileReaderRead(). + We don't know whether we're reading from a file or from memory. We don't have to know, + since we rely on the helper functions in the gdIOCtx. + We assume we've stashed the gdIOCtx in io->data, as we do in createAvifIOFromCtx(). + + We ignore readFlags, just as the avifIO*ReaderRead() functions do. + + If there's a problem, this returns an avifResult error. + If things go well, return AVIF_RESULT_OK. + Of course these AVIF codes shouldn't be returned by any top-level GD function. +*/ +static avifResult readFromCtx(avifIO *io, uint32_t readFlags, uint64_t offset, size_t size, avifROData *out) +{ + void *dataBuf = NULL; + gdIOCtx *ctx = (gdIOCtx *) io->data; + + // TODO: if we set sizeHint, this will be more efficient. + + if (offset > LONG_MAX || size < 0) + return AVIF_RESULT_IO_ERROR; + + // Try to seek offset bytes forward. If we pass the end of the buffer, throw an error. + if (!ctx->seek(ctx, offset)) + return AVIF_RESULT_IO_ERROR; + + dataBuf = gdMalloc(size); + if (!dataBuf) { + gd_error("avif error - couldn't allocate memory"); + return AVIF_RESULT_UNKNOWN_ERROR; + } + + // Read the number of bytes requested. + // If getBuf() returns a negative value, that means there was an error. + int charsRead = ctx->getBuf(ctx, dataBuf, size); + if (charsRead < 0) { + gdFree(dataBuf); + return AVIF_RESULT_IO_ERROR; + } + + out->data = dataBuf; + out->size = charsRead; + return charsRead == size ? AVIF_RESULT_OK : AVIF_RESULT_TRUNCATED_DATA; +} + +// avif.h says this is optional, but it seemed easy to implement. +static void destroyAvifIO(struct avifIO *io) { + avifFree(io); +} + +/* Set up an avifIO object. + The functions in the gdIOCtx struct may point either to a file or a memory buffer. + To us, that's immaterial. + Our task is simply to assign avifIO functions to the proper functions from gdIOCtx. + The destroy function needs to destroy the avifIO object and anything else it uses. + + Returns NULL if memory for the object can't be allocated. +*/ + +// TODO: can we get sizeHint somehow? +static avifIO *createAvifIOFromCtx(gdIOCtx *ctx) { + avifIO *io; + + io = gdMalloc(sizeof(*io)); + if (io == NULL) + return NULL; + + // TODO: setting persistent=FALSE is safe, but it's less efficient. Is it necessary? + io->persistent = AVIF_FALSE; + io->read = readFromCtx; + io->write = NULL; // this function is currently unused; see avif.h + io->destroy = destroyAvifIO; + io->sizeHint = 0; // sadly, we don't get this information from the gdIOCtx. + io->data = ctx; + + return io; +} + + +/*** Decoding functions ***/ + +/* + Function: gdImageCreateFromAvif + + is called to load truecolor images from + AVIF format files. Invoke with an + already opened pointer to a file containing the desired + image. returns a to the new + truecolor image, or NULL if unable to load the image (most often + because the file is corrupt or does not contain a AVIF + image). does not close the file. + + This function creates a gdIOCtx struct from the file pointer it's passed. + And then it relies on to do the real decoding work. + If the file contains an image sequence, we simply read the first one, discarding the rest. + + Variants: + + creates an image from AVIF data + already in memory. + + reads data from the function + pointers in a structure. + + Parameters: + + infile - pointer to the input file + + Returns: + + A pointer to the new truecolor image. This will need to be + destroyed with once it is no longer needed. + + On error, returns 0. +*/ +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvif(FILE *infile) +{ + gdImagePtr im; + gdIOCtx *ctx = gdNewFileCtx(infile); + + if (!ctx) + return NULL; + + im = gdImageCreateFromAvifCtx(ctx); + ctx->gd_free(ctx); + + return im; +} + +/* + Function: gdImageCreateFromAvifPtr + + See . + + Parameters: + + size - size of Avif data in bytes. + data - pointer to Avif data. +*/ +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifPtr(int size, void *data) +{ + gdImagePtr im; + gdIOCtx *ctx = gdNewDynamicCtxEx(size, data, 0); + + if (!ctx) + return 0; + + im = gdImageCreateFromAvifCtx(ctx); + ctx->gd_free(ctx); + + return im; +} + +/* + Function: gdImageCreateFromAvifCtx + + See . + + Additional details: the AVIF library comes with functions to create an IO object from + a file and from a memory pointer. Of course, it doesn't have a way to create an IO object + from a gdIOCtx. So, here, we use our own helper function, . + + Otherwise, we create the image by calling AVIF library functions in order: + * avifDecoderCreate(), to create the decoder + * avifDecoderSetIO(), to tell libavif how to read from our data structure + * avifDecoderParse(), to parse the image + * avifDecoderNextImage(), to read the first image from the decoder + * avifRGBImageSetDefaults(), to create the avifRGBImage + * avifRGBImageAllocatePixels(), to allocate memory for the pixels + * avifImageYUVToRGB(), to convert YUV to RGB + + Finally, we create a new gd image and copy over the pixel data. + + Parameters: + + ctx - a gdIOCtx struct +*/ +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifCtx (gdIOCtx *ctx) +{ + int x, y; + gdImage *im = NULL; + avifResult result; + avifIO *io; + avifDecoder *decoder; + avifRGBImage rgb; + + // this lets us know that memory hasn't been allocated yet for the pixels + rgb.pixels = NULL; + + decoder = avifDecoderCreate(); + + io = createAvifIOFromCtx(ctx); + if (!io) { + gd_error("avif error - Could not allocate memory"); + goto cleanup; + } + + avifDecoderSetIO(decoder, io); + + result = avifDecoderParse(decoder); + if (isAvifError(result, "Could not parse image")) + goto cleanup; + + // Note again that, for an image sequence, we read only the first image, ignoring the rest. + result = avifDecoderNextImage(decoder); + if (isAvifError(result, "Could not decode image")) + goto cleanup; + + if (!isAvifSrgbImage(decoder->image)) + gd_error_ex(LOG_WARNING, "Image's color profile is not sRGB"); + + // Set up the avifRGBImage, and convert it from YUV to an 8-bit RGB image. + // (While AVIF image pixel depth can be 8, 10, or 12 bits, GD truecolor images are 8-bit.) + avifRGBImageSetDefaults(&rgb, decoder->image); + rgb.depth = 8; + avifRGBImageAllocatePixels(&rgb); + + result = avifImageYUVToRGB(decoder->image, &rgb); + if (isAvifError(result, "Conversion from YUV to RGB failed")) + goto cleanup; + + im = gdImageCreateTrueColor(decoder->image->width, decoder->image->height); + if (!im) { + gd_error("avif error - Could not create GD truecolor image"); + goto cleanup; + } + + im->saveAlphaFlag = 1; + + // Read the pixels from the AVIF image and copy them into the GD image. + + uint8_t *p = rgb.pixels; + + for (y = 0; y < decoder->image->height; y++) { + for (x = 0; x < decoder->image->width; x++) { + uint8_t r = *(p++); + uint8_t g = *(p++); + uint8_t b = *(p++); + uint8_t a = alpha8BitTo7Bit(*(p++)); + im->tpixels[y][x] = gdTrueColorAlpha(r, g, b, a); + } + } + +cleanup: + // if io has been allocated, this frees it + avifDecoderDestroy(decoder); + + if (rgb.pixels) + avifRGBImageFreePixels(&rgb); + + return im; +} + + +/*** Encoding functions ***/ + +/* + Function: gdImageAvifEx + + outputs the specified image to the specified file in + AVIF format. The file must be open for writing. Under MSDOS and + all versions of Windows, it is important to use "wb" as opposed to + simply "w" as the mode when opening the file, and under Unix there + is no penalty for doing so. does not close the file; + your code must do so. + + Variants: + + writes the image to a file, encoding with the default quality and speed. + + stores the image in RAM. + + stores the image in RAM, encoding with the default quality and speed. + + stores the image using a struct. + + Parameters: + + im - The image to save. + outFile - The FILE pointer to write to. + quality - Compression quality (0-100). 0 is lowest-quality, 100 is highest. + speed - The speed of compression (0-10). 0 is slowest, 10 is fastest. + + Notes on parameters: + quality - If quality = -1, we use a default quality as defined in QUALITY_DEFAULT. + For information on how we convert this quality to libavif's quantity param, see . + + speed - At slower speeds, encoding may be quite slow. Use judiciously. + + Qualities or speeds that are lower than the minimum value get clamped to the minimum value, + abd qualities or speeds that are lower than the maximum value get clamped to the maxmum value. + + + Returns: + + * for , , and , nothing. + * for and , a pointer to the image in memory. +*/ + +/* + Function: _gdImageAvifCtx + + We need this underscored function because gdImageAvifCtx() can't return anything. + And our functions that operate on a memory buffer need to know whether the encoding has succeeded. + + If we're passed the QUALITY_DEFAULT of -1, set the quantizer params to QUANTIZER_DEFAULT. + + This function returns 0 on success, or 1 on failure. + */ +static avifBool _gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed) +{ + avifResult result; + avifRGBImage rgb; + avifRWData avifOutput = AVIF_DATA_EMPTY; + avifBool failed = AVIF_FALSE; + avifBool lossless = quality == 100; + avifEncoder *encoder = NULL; + + uint32_t val; + uint8_t *p; + uint8_t a; + int x, y; + + if (im == NULL) + return 1; + + if (!gdImageTrueColor(im)) { + gd_error("avif doesn't support palette images"); + return 1; + } + + if (!gdImageSX(im) || !gdImageSY(im)) { + gd_error("image dimensions must not be zero"); + return 1; + } + + if (overflow2(gdImageSX(im), gdImageSY(im))) { + gd_error("image dimensions are too large"); + return 1; + } + + if (speed != AVIF_SPEED_DEFAULT) + speed = CLAMP(speed, AVIF_SPEED_SLOWEST, AVIF_SPEED_FASTEST); + + avifPixelFormat subsampling = quality >= HIGH_QUALITY_SUBSAMPLING_THRESHOLD ? + CHROMA_SUBAMPLING_HIGH_QUALITY : CHROMA_SUBSAMPLING_DEFAULT; + + // Create the AVIF image. + // Set the ICC to sRGB, as that's what gd supports right now. + // Note that MATRIX_COEFFICIENTS_IDENTITY enables lossless conversion from RGB to YUV. + + avifImage *avifIm = avifImageCreate(gdImageSX(im), gdImageSY(im), 8, subsampling); + + avifIm->colorPrimaries = AVIF_COLOR_PRIMARIES_BT709; + avifIm->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB; + avifIm->matrixCoefficients = lossless ? AVIF_MATRIX_COEFFICIENTS_IDENTITY : AVIF_MATRIX_COEFFICIENTS_BT709; + + avifRGBImageSetDefaults(&rgb, avifIm); + // this allocates memory, and sets rgb.rowBytes and rgb.pixels. + avifRGBImageAllocatePixels(&rgb); + + // Parse RGB data from the GD image, and copy it into the AVIF RGB image. + // Convert 7-bit GD alpha channel values to 8-bit AVIF values. + + p = rgb.pixels; + for (y = 0; y < rgb.height; y++) { + for (x = 0; x < rgb.width; x++) { + val = im->tpixels[y][x]; + + *(p++) = gdTrueColorGetRed(val); + *(p++) = gdTrueColorGetGreen(val); + *(p++) = gdTrueColorGetBlue(val); + *(p++) = alpha7BitTo8Bit(gdTrueColorGetAlpha(val)); + } + } + + // Convert the RGB image to YUV. + + result = avifImageRGBToYUV(avifIm, &rgb); + failed = isAvifError(result, "Could not convert image to YUV"); + if (failed) + goto cleanup; + + // Encode the image in AVIF format. + + encoder = avifEncoderCreate(); + int quantizerQuality = quality == QUALITY_DEFAULT ? + QUANTIZER_DEFAULT : quality2Quantizer(quality); + + encoder->minQuantizer = quantizerQuality; + encoder->maxQuantizer = quantizerQuality; + encoder->minQuantizerAlpha = quantizerQuality; + encoder->maxQuantizerAlpha = quantizerQuality; + encoder->speed = speed; + + failed = !setEncoderTilesAndThreads(encoder, &rgb); + if (failed) + goto cleanup; + + //TODO: is there a reason to use timeSscales != 1? + result = avifEncoderAddImage(encoder, avifIm, 1, AVIF_ADD_IMAGE_FLAG_SINGLE); + failed = isAvifError(result, "Could not encode image"); + if (failed) + goto cleanup; + + result = avifEncoderFinish(encoder, &avifOutput); + failed = isAvifError(result, "Could not finish encoding"); + if (failed) + goto cleanup; + + // Write the AVIF image bytes to the GD ctx. + + gdPutBuf(avifOutput.data, avifOutput.size, outfile); + +cleanup: + if (rgb.pixels) + avifRGBImageFreePixels(&rgb); + + if (encoder) + avifEncoderDestroy(encoder); + + if (avifOutput.data) + avifRWDataFree(&avifOutput); + + return failed; +} + +BGD_DECLARE(void) gdImageAvifEx(gdImagePtr im, FILE *outFile, int quality, int speed) +{ + gdIOCtx *out = gdNewFileCtx(outFile); + + if (out == NULL) + return; + + gdImageAvifCtx(im, out, quality, speed); + out->gd_free(out); +} + +BGD_DECLARE(void) gdImageAvif(gdImagePtr im, FILE *outFile) +{ + gdImageAvifEx(im, outFile, QUALITY_DEFAULT, AVIF_SPEED_DEFAULT); +} + +BGD_DECLARE(void *) gdImageAvifPtrEx(gdImagePtr im, int *size, int quality, int speed) +{ + void *rv; + gdIOCtx *out = gdNewDynamicCtx(NEW_DYNAMIC_CTX_SIZE, NULL); + + if (out == NULL) { + return NULL; + } + + if (_gdImageAvifCtx(im, out, quality, speed)) + rv = NULL; + else + rv = gdDPExtractData(out, size); + + out->gd_free(out); + return rv; +} + +BGD_DECLARE(void *) gdImageAvifPtr(gdImagePtr im, int *size) +{ + return gdImageAvifPtrEx(im, size, QUALITY_DEFAULT, AVIF_SPEED_DEFAULT); +} + + +BGD_DECLARE(void) gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed) +{ + _gdImageAvifCtx(im, outfile, quality, speed); +} + +#else /* !HAVE_LIBAVIF */ + +static void *_noAvifError(void) +{ + gd_error("AVIF image support has been disabled\n"); + return NULL; +} + +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvif(FILE *ctx) +{ + return _noAvifError(); +} + +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifPtr(int size, void *data) +{ + return _noAvifError(); +} + +BGD_DECLARE(gdImagePtr) gdImageCreateFromAvifCtx(gdIOCtx *ctx) +{ + return _noAvifError(); +} + +BGD_DECLARE(void) gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed) +{ + _noAvifError(); +} + +BGD_DECLARE(void) gdImageAvifEx(gdImagePtr im, FILE *outFile, int quality, int speed) +{ + _noAvifError(); +} + +BGD_DECLARE(void) gdImageAvif(gdImagePtr im, FILE *outFile) +{ + _noAvifError(); +} + +BGD_DECLARE(void *) gdImageAvifPtr(gdImagePtr im, int *size) +{ + return _noAvifError(); +} + +BGD_DECLARE(void *) gdImageAvifPtrEx(gdImagePtr im, int *size, int quality, int speed) +{ + return _noAvifError(); +} + +#endif /* HAVE_LIBAVIF */ diff --git a/src/gd_filename.c b/src/gd_filename.c index 6b67c3a..ddfaa02 100644 --- a/src/gd_filename.c +++ b/src/gd_filename.c @@ -51,6 +51,10 @@ static const struct FileType { {".xbm", gdImageCreateFromXbm, NULL, NULL}, {".tga", gdImageCreateFromTga, NULL, NULL}, +#ifdef HAVE_LIBAVIF + {".avif", gdImageCreateFromAvif, gdImageAvif, NULL}, +#endif + #ifdef HAVE_LIBPNG {".png", gdImageCreateFromPng, gdImagePng, NULL}, #endif diff --git a/src/gd_webp.c b/src/gd_webp.c index ab15109..a0b4787 100644 --- a/src/gd_webp.c +++ b/src/gd_webp.c @@ -42,10 +42,10 @@ Variants: - creates an image from WebP data + creates an image from WebP data already in memory. - reads its data via the function + reads its data via the function pointers in a structure. Parameters: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6775948..b279f63 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ if (BUILD_TEST) include_directories (BEFORE ${GD_INCLUDE_DIR} "${GDTEST_SOURCE_DIR}" "${CMAKE_BINARY_DIR}" "${CMAKE_BINARY_DIR}/tests/gdtest") SET(TESTS_DIRS + avif bmp fontconfig freetype diff --git a/tests/Makefile.am b/tests/Makefile.am index efbe26a..3630810 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -13,6 +13,7 @@ CLEANFILES = EXTRA_DIST = TESTS = +include avif/Makemodule.am include bmp/Makemodule.am include fontconfig/Makemodule.am include freetype/Makemodule.am @@ -99,6 +100,7 @@ EXTRA_DIST += \ # We don't keep any media files in the top dir ... just generated outputs. CLEANFILES += \ + *.avif \ *.bmp \ *.gd \ *.gd2 \ diff --git a/tests/avif/.gitignore b/tests/avif/.gitignore new file mode 100644 index 0000000..66233b0 --- /dev/null +++ b/tests/avif/.gitignore @@ -0,0 +1,5 @@ +/avif_im2im +/avif_null +/avif_ptr_double_free +/bad_input +/compare_avif_to_png diff --git a/tests/avif/CMakeLists.txt b/tests/avif/CMakeLists.txt new file mode 100644 index 0000000..e2df47b --- /dev/null +++ b/tests/avif/CMakeLists.txt @@ -0,0 +1,17 @@ +IF(AVIF_FOUND) +LIST(APPEND TESTS_FILES + avif_ptr_double_free + avif_im2im + avif_null + bad_input +) + +IF(PNG_FOUND) +LIST(APPEND TESTS_FILES + compare_avif_to_png +) +ENDIF(PNG_FOUND) + +ENDIF(AVIF_FOUND) + +ADD_GD_TESTS() diff --git a/tests/avif/Makemodule.am b/tests/avif/Makemodule.am new file mode 100644 index 0000000..b446a93 --- /dev/null +++ b/tests/avif/Makemodule.am @@ -0,0 +1,16 @@ +if HAVE_LIBAVIF +libgd_test_programs += \ + avif/avif_ptr_double_free + avif/avif_im2im + avif/avif_null + avif/bad_input + +if HAVE_LIBPNG +libgd_test_programs += \ + avif/compare_avif_to_png +endif + +endif + +EXTRA_DIST += \ + avif/CMakeLists.txt diff --git a/tests/avif/avif_im2im.c b/tests/avif/avif_im2im.c new file mode 100644 index 0000000..3a07ebd --- /dev/null +++ b/tests/avif/avif_im2im.c @@ -0,0 +1,67 @@ +/** + * File: avif_im2im + * + * Sanity check for AVIF encoding and decoding. + * We create a simple gd image, we encode it to AVIF, and we decode it back to gd. + * Then we make sure the image we started with and the image we finish with are the same. + * + */ + +#include "gd.h" +#include "gdtest.h" +#include + +int main() +{ + gdImagePtr srcGdIm, destGdIm; + void *avifImageDataPtr; + FILE *fp; + int r, g, b; + int size = 0; + CuTestImageResult result = {0, 0}; + + // Create new gd image and add some shapes to it. + srcGdIm = gdImageCreateTrueColor(100, 100); + gdTestAssertMsg(srcGdIm != NULL, "could not create source image\n"); + + r = gdImageColorAllocate(srcGdIm, 0xFF, 0, 0); + g = gdImageColorAllocate(srcGdIm, 0, 0xFF, 0); + b = gdImageColorAllocate(srcGdIm, 0, 0, 0xFF); + gdImageFilledRectangle(srcGdIm, 0, 0, 99, 99, r); + gdImageRectangle(srcGdIm, 20, 20, 79, 79, g); + gdImageEllipse(srcGdIm, 70, 25, 30, 20, b); + + // Encode the gd image to a test AVIF file. + fp = gdTestTempFp(); + gdImageAvif(srcGdIm, fp); + fclose(fp); + + // Encode the gd image to an AVIF image in memory. + avifImageDataPtr = gdImageAvifPtrEx(srcGdIm, &size, 100, 10); + gdTestAssertMsg(avifImageDataPtr != NULL, "gdImageAvifPtr() returned null\n"); + gdTestAssertMsg(size > 0, "gdImageAvifPtr() returned a non-positive size\n"); + + // Encode the AVIF image back into a gd image. + destGdIm = gdImageCreateFromAvifPtr(size, avifImageDataPtr); + gdTestAssertMsg(destGdIm != NULL, "gdImageAvifPtr() returned null\n"); + + // Encode that gd image to a test AVIF file. + fp = gdTestTempFp(); + gdImageAvif(destGdIm, fp); + fclose(fp); + + // Make sure the image we started with is the same as the image after two conversions. + gdTestImageDiff(srcGdIm, destGdIm, NULL, &result); + gdTestAssertMsg(result.pixels_changed == 0, "pixels changed: %d\n", result.pixels_changed); + + if (srcGdIm) + gdImageDestroy(srcGdIm); + + if (destGdIm) + gdImageDestroy(destGdIm); + + if (avifImageDataPtr) + gdFree(avifImageDataPtr); + + return gdNumFailures(); +} diff --git a/tests/avif/avif_null.c b/tests/avif/avif_null.c new file mode 100644 index 0000000..bbcc9c6 --- /dev/null +++ b/tests/avif/avif_null.c @@ -0,0 +1,23 @@ +/** + * File: avif_null.c + * + * Simple test case, confirming that if you try to create an AVIF image from a + * null file pointer, the creation will fail, and it will return NULL. + */ + +#include "gd.h" +#include "gdtest.h" + + +int main() +{ + gdImagePtr im; + + im = gdImageCreateFromAvif(NULL); + if (!gdTestAssert(im == NULL)) + gdImageDestroy(im); + + gdImageAvif(im, NULL); /* noop safely */ + + return gdNumFailures(); +} diff --git a/tests/avif/avif_ptr_double_free.c b/tests/avif/avif_ptr_double_free.c new file mode 100644 index 0000000..8160950 --- /dev/null +++ b/tests/avif/avif_ptr_double_free.c @@ -0,0 +1,34 @@ +/** + * Test that failure to convert to AVIF returns NULL + * + * We are creating an image, set its width to zero, and pass this image to + * gdImageAvifPtr(). + * This is supposed to fail, and as such should return NULL. + * + * See also + */ + +#include "gd.h" +#include "gdtest.h" + +int main() +{ + gdImagePtr src, dst; + int size; + + src = gdImageCreateTrueColor(1, 10); + gdTestAssert(src != NULL); + + src->sx = 0; // making the width 0 should cause gdImageAvifPtr() to fail + + dst = gdImageAvifPtr(src, &size); + gdTestAssert(dst == NULL); + + if (src) + gdImageDestroy(src); + + if (dst) + gdImageDestroy(dst); + + return gdNumFailures(); +} diff --git a/tests/avif/baboon.avif b/tests/avif/baboon.avif new file mode 100644 index 0000000..f5821db Binary files /dev/null and b/tests/avif/baboon.avif differ diff --git a/tests/avif/baboon.png b/tests/avif/baboon.png new file mode 100644 index 0000000..fdc5dcb Binary files /dev/null and b/tests/avif/baboon.png differ diff --git a/tests/avif/bad_input.c b/tests/avif/bad_input.c new file mode 100644 index 0000000..a9f976b --- /dev/null +++ b/tests/avif/bad_input.c @@ -0,0 +1,72 @@ +/** + * File: bad_input.c + * + * Make sure that the AVIF encoding and decoding functions handle bad input gracefully. + */ + +#include +#include +#include "gd.h" +#include "gdtest.h" + +#define PATH "avif/" +#define MAX_FILEPATH_LENGTH 50 + +#define NON_AVIF_FILE_NAME "sunset.png" +#define AVIF_FILE_NAME "sunset.avif" + +int main() { + FILE *fp; + int retval; + char nonAvifFilePath[MAX_FILEPATH_LENGTH], avifFilePath[MAX_FILEPATH_LENGTH]; + gdImagePtr realIm, badIm; + void *rv; + int size; + +// Create paths for our files. + strcpy(avifFilePath, PATH); + strcat(avifFilePath, AVIF_FILE_NAME); + + strcpy(nonAvifFilePath, PATH); + strcat(nonAvifFilePath, NON_AVIF_FILE_NAME); + +// Read in an AVIF image for testing. + + fp = gdTestFileOpen(avifFilePath); + realIm = gdImageCreateFromAvif(fp); + fclose(fp); + if (!gdTestAssertMsg(realIm != NULL, "gdImageCreateFromAvif() failed\n")) + return 1; + +// Try to decode a non-AVIF file. + + fp = gdTestFileOpen(nonAvifFilePath); + badIm = gdImageCreateFromAvif(fp); + fclose(fp); + gdTestAssertMsg(badIm == NULL, "gdImageCreateFromAvif() failed to return NULL when passed a non-AVIF file\n"); + + if (badIm) + gdImageDestroy(badIm); + + // Try to encode a valid image with bad quality parameters. This should still work. + + rv = gdImageAvifPtrEx(realIm, &size, 400, 10); + gdTestAssertMsg(rv != NULL, "gdImageAvifPtrEx() rejected an overly high quality param instead of clamping it to a valid value"); + gdFree(rv); + + rv = gdImageAvifPtrEx(realIm, &size, -4, 10); + gdTestAssertMsg(rv != NULL, "gdImageAvifPtrEx() rejected a negative quality param instead of clamping it to a valid value"); + gdFree(rv); + + rv = gdImageAvifPtrEx(realIm, &size, 30, 30); + gdTestAssertMsg(rv != NULL, "gdImageAvifPtrEx() rejected an overly high speed param instead of clamping it to a valid value"); + gdFree(rv); + + rv = gdImageAvifPtrEx(realIm, &size, 30, -4); + gdTestAssertMsg(rv != NULL, "gdImageAvifPtrEx() rejected a negative speed param instead of clamping it to a valid value"); + gdFree(rv); + + gdImageDestroy(realIm); + + return gdNumFailures(); +} diff --git a/tests/avif/compare_avif_to_png.c b/tests/avif/compare_avif_to_png.c new file mode 100644 index 0000000..716eae9 --- /dev/null +++ b/tests/avif/compare_avif_to_png.c @@ -0,0 +1,100 @@ +/** + * File: compare_avif_to_png + * + * Thorough check for AVIF encoding and decoding. + * This test reqiures a set of PNG images that have been losslessly encoded to AVIFs. + * For each such image, we encode the PNG into an AVIF, with the GD format as an intermediary, + * then compare the resulting AVIF with the original PNG. + * + * We then do this process in reverse, encoding the AVIF into a PNG, + * and compare the resulting PNG with the original AVIF. + * + * We report any discrepancies in the images, or any other errors that may occur. + */ + +#include +#include +#include "gd.h" +#include "gdtest.h" + +#define PATH "avif/" +#define MAX_FILEPATH_LENGTH 200 + +int main() { + FILE *fp; + gdImagePtr imFromPng = NULL, imFromAvif = NULL; + void *avifImDataPtr = NULL, *pngImDataPtr = NULL; + int size; + char filePath[MAX_FILEPATH_LENGTH], pngFilePath[MAX_FILEPATH_LENGTH], avifFilePath[MAX_FILEPATH_LENGTH]; + char errMsg[MAX_FILEPATH_LENGTH + 100]; + + const int filesCount = 4; + const char *filenames[filesCount] = {"baboon", "dice_with_alpha", "plum_blossom_12bit", "sunset"}; + + for (int i = 0; i < filesCount; i++) { + + // First, encode each PNG into an AVIF (with the GD format as an intermediary), + // then compare the result with the original PNG. + + strcpy(filePath, PATH); + strcat(filePath, filenames[i]); + strcat(strcpy(pngFilePath, filePath), ".png"); + strcat(strcpy(avifFilePath, filePath), ".avif"); + + fp = gdTestFileOpen(pngFilePath); + imFromPng = gdImageCreateFromPng(fp); + fclose(fp); + + strcat(strcpy(errMsg, filenames[i]), ".png: gdImageCreateFromPng failed\n"); + if (!gdTestAssertMsg(imFromPng != NULL, errMsg)) + goto avif2png; + + strcat(strcpy(errMsg, filenames[i]), ": gdImageAvifPtrEx failed\n"); + avifImDataPtr = gdImageAvifPtrEx(imFromPng, &size, 100, 0); + if (!gdTestAssertMsg(avifImDataPtr != NULL, errMsg)) + goto avif2png; + + strcat(strcpy(errMsg, filenames[i]), ": gdImageCreateFromAvifPtr failed\n"); + imFromAvif = gdImageCreateFromAvifPtr(size, avifImDataPtr); + if (!gdTestAssertMsg(imFromAvif != NULL, errMsg)) + goto avif2png; + + strcat(strcpy(errMsg, filenames[i]), ".png: Encoded AVIF image did not match original PNG\n"); + gdTestAssertMsg(gdAssertImageEquals(imFromPng, imFromAvif), errMsg); + + // Then, decode each AVIF into a GD format, and compare that with the orginal PNG. + +avif2png: + continue; + +/* Skip this reverse test for now, until we can find images that encode to PNGs + losslessly. + + fp = gdTestFileOpen(avifFilePath); + imFromAvif = gdImageCreateFromAvif(fp); + fclose(fp); + + strcat(strcpy(errMsg, filenames[i]), ".avif: gdImageCreateFromAvif failed\n"); + if (!gdTestAssertMsg(imFromAvif != NULL, errMsg)) + continue; + + strcat(strcpy(errMsg, filenames[i]), ".avif: Encoded PNG image did not match original AVIF\n"); + gdTestAssertMsg(gdAssertImageEqualsToFile(pngFilePath, imFromAvif), errMsg); +*/ + +} + + if (imFromPng) + gdImageDestroy(imFromPng); + + if (imFromAvif) + gdImageDestroy(imFromAvif); + + if (avifImDataPtr) + gdFree(avifImDataPtr); + + if (pngImDataPtr) + gdFree(pngImDataPtr); + + return gdNumFailures(); +} diff --git a/tests/avif/dice_with_alpha.avif b/tests/avif/dice_with_alpha.avif new file mode 100644 index 0000000..dce625b Binary files /dev/null and b/tests/avif/dice_with_alpha.avif differ diff --git a/tests/avif/dice_with_alpha.png b/tests/avif/dice_with_alpha.png new file mode 100644 index 0000000..d2809d1 Binary files /dev/null and b/tests/avif/dice_with_alpha.png differ diff --git a/tests/avif/plum_blossom_12bit.avif b/tests/avif/plum_blossom_12bit.avif new file mode 100644 index 0000000..7959d15 Binary files /dev/null and b/tests/avif/plum_blossom_12bit.avif differ diff --git a/tests/avif/plum_blossom_12bit.png b/tests/avif/plum_blossom_12bit.png new file mode 100644 index 0000000..e6901e2 Binary files /dev/null and b/tests/avif/plum_blossom_12bit.png differ diff --git a/tests/avif/sunset.avif b/tests/avif/sunset.avif new file mode 100644 index 0000000..4964558 Binary files /dev/null and b/tests/avif/sunset.avif differ diff --git a/tests/avif/sunset.png b/tests/avif/sunset.png new file mode 100644 index 0000000..21f45d5 Binary files /dev/null and b/tests/avif/sunset.png differ diff --git a/travis/main.sh b/travis/main.sh index 2d29b5d..9965c40 100755 --- a/travis/main.sh +++ b/travis/main.sh @@ -78,6 +78,9 @@ build_autotools() { m distclean } +# TODO: When we switch to Ubuntu 21+ (Hirsute), we can reenable libavif coverage, +# as Ubuntu 21+ supports libavif 0.8.2+. +# "-DENABLE_AVIF=1" cmake_args=( "-DBUILD_SHARED_LIBS=1" "-DBUILD_STATIC_LIBS=1" @@ -94,7 +97,7 @@ cmake_args=( ) # libxpm-dev is unavaible in brew repo -# Once it gets avaible, please modify this code block. +# Once it gets available, please modify this code block. if [[ ${TRAVIS_OS_NAME} == "linux" ]]; then cmake_args+=("-DENABLE_XPM=1") fi diff --git a/windows/Makefile.vc b/windows/Makefile.vc index d97b22f..3fefe80 100644 --- a/windows/Makefile.vc +++ b/windows/Makefile.vc @@ -95,10 +95,11 @@ LIB_OBJS= \ $(LIBGD_OBJ_DIR)\gd_crop.obj \ $(LIBGD_OBJ_DIR)\gd_color_map.obj \ $(LIBGD_OBJ_DIR)\gd_heif.obj \ + $(LIBGD_OBJ_DIR)\gd_avif.obj \ $(LIBGD_OBJ_DIR)\gd_webp.obj LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib Gdi32.Lib -LIBS_GD=libjpeg_a.lib freetype_a.lib libpng_a.lib libiconv_a.lib zlib_a.lib libheif_a.lib libwebp_a.lib libxpm_a.lib libtiff.lib +LIBS_GD=libjpeg_a.lib freetype_a.lib libpng_a.lib libiconv_a.lib zlib_a.lib libheif_a.lib libwebp_a.lib libxpm_a.lib libavif_a.lib libtiff.lib PROG_EXES= \ $(LIBGD_OBJ_DIR)\gdcmpgif.exe \ @@ -135,11 +136,13 @@ CFLAGS= $(CFLAGS) \ /DHAVE_FT2BUILD_H=1\ /DHAVE_GD_H=1\ /DHAVE_ICONV_H=1\ + /DHAVE_LIBAVIF=1\ /DHAVE_LIBFREETYPE=1\ /DHAVE_LIBJPEG=1\ /DHAVE_LIBPNG=1\ /DHAVE_LIBWEBP=1\ /DHAVE_LIBHEIF=1\ + /DHAVE_LIBAVIF=1\ /DHAVE_LIBZ=1\ /DHAVE_LIBXPM=1\ /DHAVE_LIBTIFF=1\ @@ -186,6 +189,9 @@ make_dirs: @echo #ifndef HAVE_ICONV_H>> $(GD_CONFIG_H) @echo #define HAVE_ICONV_H>> $(GD_CONFIG_H) @echo #endif>> $(GD_CONFIG_H) + @echo #ifndef HAVE_LIBAVIF>> $(GD_CONFIG_H) + @echo #define HAVE_LIBAVIF>> $(GD_CONFIG_H) + @echo #endif>> $(GD_CONFIG_H) @echo #ifndef HAVE_LIBFREETYPE>> $(GD_CONFIG_H) @echo #define HAVE_LIBFREETYPE>> $(GD_CONFIG_H) @echo #endif>> $(GD_CONFIG_H) @@ -201,6 +207,9 @@ make_dirs: @echo #ifndef HAVE_LIBHEIF>> $(GD_CONFIG_H) @echo #define HAVE_LIBHEIF>> $(GD_CONFIG_H) @echo #endif>> $(GD_CONFIG_H) + @echo #ifndef HAVE_LIBAVIF>> $(GD_CONFIG_H) + @echo #define HAVE_LIBAVIF>> $(GD_CONFIG_H) + @echo #endif>> $(GD_CONFIG_H) @echo #ifndef HAVE_LIBZ>> $(GD_CONFIG_H) @echo #define HAVE_LIBZ>> $(GD_CONFIG_H) @echo #endif>> $(GD_CONFIG_H) diff --git a/windows/Makefiletest.vc b/windows/Makefiletest.vc index 827a4e7..6e5d598 100644 --- a/windows/Makefiletest.vc +++ b/windows/Makefiletest.vc @@ -1,5 +1,10 @@ TESTS=bmp\bmp_im2im \ bmp\bmp_null \ +avif\avif_ptr_double_free \ +avif\avif_im2im \ +avif\avif_null \ +avif\compare_avif_to_png \ +avif\bad_input \ freetype\bug00132 \ gd\gd_im2im \ gd\gd_null \ diff --git a/windows/msys/Makefile b/windows/msys/Makefile index 8c5d380..d183ac2 100644 --- a/windows/msys/Makefile +++ b/windows/msys/Makefile @@ -98,7 +98,8 @@ gd_topal.c gd_wbmp.c gdcache.c gdfontg.c gdfontl.c gdfontmb.c \ gdfonts.c gdfontt.c gdft.c gdhelpers.c gdkanji.c gdtables.c gdxpm.c \ wbmp.c gd_filter.c gd_nnquant.c gd_rotate.c gd_matrix.c \ gd_interpolation.c gd_crop.c gd_webp.c gd_heif.c gd_tiff.c gd_tga.c \ -gd_bmp.c gd_xbm.c gd_color_match.c gd_version.c gd_filename.c +gd_bmp.c gd_xbm.c gd_color_match.c gd_version.c gd_filename.c \ +gd_avif.c OBJ=$(SRC:.c=.o)