diff --git a/.cirrus.yml b/.cirrus.yml index 7547bd18cb74d..917859dad2b81 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,10 +11,10 @@ task: #- sed -i -e 's/quarterly/latest/g' /etc/pkg/FreeBSD.conf #- pkg upgrade -y - kldload accf_http - - pkg install -y autoconf bison gmake re2c icu libiconv png freetype2 enchant2 bzip2 krb5 t1lib gmp tidyp libsodium libzip libxml2 libxslt openssl oniguruma pkgconf webp + - pkg install -y autoconf bison gmake re2c icu libiconv png freetype2 enchant2 bzip2 krb5 t1lib gmp tidyp libsodium libzip libxml2 libxslt openssl oniguruma pkgconf webp libavif script: - ./buildconf -f - - ./configure --prefix=/usr/local --enable-debug --enable-option-checking=fatal --enable-fpm --with-pdo-sqlite --without-pear --with-bz2 --with-jpeg --with-webp --with-freetype --enable-gd --enable-exif --with-zip --with-zlib --enable-soap --enable-xmlreader --with-xsl --with-libxml --enable-shmop --enable-pcntl --enable-mbstring --with-curl --enable-sockets --with-openssl --with-iconv=/usr/local --enable-bcmath --enable-calendar --enable-ftp --with-kerberos --with-ffi --enable-zend-test --enable-intl --with-mhash --with-sodium --enable-werror --with-config-file-path=/etc --with-config-file-scan-dir=/etc/php.d + - ./configure --prefix=/usr/local --enable-debug --enable-option-checking=fatal --enable-fpm --with-pdo-sqlite --without-pear --with-bz2 --with-avif --with-jpeg --with-webp --with-freetype --enable-gd --enable-exif --with-zip --with-zlib --enable-soap --enable-xmlreader --with-xsl --with-libxml --enable-shmop --enable-pcntl --enable-mbstring --with-curl --enable-sockets --with-openssl --with-iconv=/usr/local --enable-bcmath --enable-calendar --enable-ftp --with-kerberos --with-ffi --enable-zend-test --enable-intl --with-mhash --with-sodium --enable-werror --with-config-file-path=/etc --with-config-file-scan-dir=/etc/php.d - gmake -j2 - mkdir /etc/php.d - gmake install diff --git a/Zend/Optimizer/zend_func_info.c b/Zend/Optimizer/zend_func_info.c index d236d0d02c007..b4b40ecd72848 100644 --- a/Zend/Optimizer/zend_func_info.c +++ b/Zend/Optimizer/zend_func_info.c @@ -760,6 +760,9 @@ static const func_info_t func_infos[] = { #ifdef HAVE_GD_JPG F1("imagecreatefromjpeg", MAY_BE_FALSE | MAY_BE_OBJECT), #endif +#ifdef HAVE_GD_AVIF + F1("imagecreatefromavif", MAY_BE_FALSE | MAY_BE_OBJECT), +#endif #ifdef HAVE_GD_PNG F1("imagecreatefrompng", MAY_BE_FALSE | MAY_BE_OBJECT), #endif diff --git a/appveyor/build_task.bat b/appveyor/build_task.bat index ce199fa0f35a5..93ed1bccf364b 100644 --- a/appveyor/build_task.bat +++ b/appveyor/build_task.bat @@ -45,6 +45,15 @@ if not exist "%DEPS_DIR%" ( ) if %errorlevel% neq 0 exit /b 3 +rem install libavif into dependency directory +rem temporary workaround for libavif not being part of the official dependencies +if not exist "%DEPS_DIR%\libavif-0.9.0-%PHP_SDK_VS%-%PHP_SDK_ARCH%.zip" ( + curl -fsSL -o %DEPS_DIR%\libavif-0.9.0-%PHP_SDK_VS%-%PHP_SDK_ARCH%.zip https://windows.php.net/downloads/pecl/deps/libavif-0.9.0-%PHP_SDK_VS%-%PHP_SDK_ARCH%.zip +) +if not exist "%DEPS_DIR%\lib\avif.lib" ( + 7z x %DEPS_DIR%\libavif-0.9.0-%PHP_SDK_VS%-%PHP_SDK_ARCH%.zip -o%DEPS_DIR% +) + cmd /c buildconf.bat --force if %errorlevel% neq 0 exit /b 3 diff --git a/ext/gd/config.m4 b/ext/gd/config.m4 index 03c61032aa2d0..4c24e1f5842f5 100644 --- a/ext/gd/config.m4 +++ b/ext/gd/config.m4 @@ -13,6 +13,15 @@ PHP_ARG_WITH([external-gd], [no], [no]) +if test -z "$PHP_AVIF"; then + PHP_ARG_WITH([avif], + [for libavif], + [AS_HELP_STRING([--with-avif], + [GD: Enable AVIF support (only for bundled libgd)])], + [no], + [no]) +fi + if test -z "$PHP_WEBP"; then PHP_ARG_WITH([webp], [for libwebp], @@ -71,6 +80,16 @@ AC_DEFUN([PHP_GD_PNG],[ AC_DEFINE(HAVE_LIBPNG, 1, [ ]) ]) +AC_DEFUN([PHP_GD_AVIF],[ + if test "$PHP_AVIF" != "no"; then + PKG_CHECK_MODULES([AVIF], [libavif]) + PHP_EVAL_LIBLINE($AVIF_LIBS, GD_SHARED_LIBADD) + PHP_EVAL_INCLINE($AVIF_CFLAGS) + AC_DEFINE(HAVE_LIBAVIF, 1, [ ]) + AC_DEFINE(HAVE_GD_AVIF, 1, [ ]) + fi +]) + AC_DEFUN([PHP_GD_WEBP],[ if test "$PHP_WEBP" != "no"; then PKG_CHECK_MODULES([WEBP], [libwebp]) @@ -121,6 +140,7 @@ AC_DEFUN([PHP_GD_JISX0208],[ AC_DEFUN([PHP_GD_CHECK_VERSION],[ PHP_CHECK_LIBRARY(gd, gdImageCreateFromPng, [AC_DEFINE(HAVE_GD_PNG, 1, [ ])], [], [ $GD_SHARED_LIBADD ]) + PHP_CHECK_LIBRARY(gd, gdImageCreateFromAvif, [AC_DEFINE(HAVE_GD_AVIF, 1, [ ])], [], [ $GD_SHARED_LIBADD ]) PHP_CHECK_LIBRARY(gd, gdImageCreateFromWebp, [AC_DEFINE(HAVE_GD_WEBP, 1, [ ])], [], [ $GD_SHARED_LIBADD ]) PHP_CHECK_LIBRARY(gd, gdImageCreateFromJpeg, [AC_DEFINE(HAVE_GD_JPG, 1, [ ])], [], [ $GD_SHARED_LIBADD ]) PHP_CHECK_LIBRARY(gd, gdImageCreateFromXpm, [AC_DEFINE(HAVE_GD_XPM, 1, [ ])], [], [ $GD_SHARED_LIBADD ]) @@ -141,7 +161,7 @@ if test "$PHP_GD" != "no"; then dnl Disable strict prototypes as GD takes advantages of variadic function signatures for function pointers. GD_CFLAGS="-Wno-strict-prototypes" extra_sources="libgd/gd.c libgd/gd_gd.c libgd/gd_gd2.c libgd/gd_io.c libgd/gd_io_dp.c \ - libgd/gd_io_file.c libgd/gd_ss.c libgd/gd_io_ss.c libgd/gd_webp.c \ + libgd/gd_io_file.c libgd/gd_ss.c libgd/gd_io_ss.c libgd/gd_webp.c libgd/gd_avif.c \ libgd/gd_png.c libgd/gd_jpeg.c libgd/gdxpm.c libgd/gdfontt.c libgd/gdfonts.c \ libgd/gdfontmb.c libgd/gdfontl.c libgd/gdfontg.c libgd/gdtables.c libgd/gdft.c \ libgd/gdcache.c libgd/gdkanji.c libgd/wbmp.c libgd/gd_wbmp.c libgd/gdhelpers.c \ @@ -162,6 +182,7 @@ dnl These are always available with bundled library dnl Various checks for GD features PHP_GD_ZLIB PHP_GD_PNG + PHP_GD_AVIF PHP_GD_WEBP PHP_GD_JPEG PHP_GD_XPM diff --git a/ext/gd/config.w32 b/ext/gd/config.w32 index 541d5f51e0e2b..8ed0205287069 100644 --- a/ext/gd/config.w32 +++ b/ext/gd/config.w32 @@ -2,6 +2,7 @@ ARG_WITH("gd", "Bundled GD support", "yes,shared"); ARG_WITH("libwebp", "webp support", "yes"); +ARG_WITH("libavif", "avif support", "yes"); if (PHP_GD != "no") { if ( @@ -30,6 +31,14 @@ if (PHP_GD != "no") { WARNING("libwebp not enabled; libraries and headers not found"); } } + if (PHP_LIBAVIF != "no") { + if (CHECK_LIB("avif.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("avif.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\avif")) { + ADD_FLAG("CFLAGS_GD", "/D HAVE_LIBAVIF /D HAVE_GD_AVIF"); + } else { + WARNING("libavif not enabled; libraries and headers not found"); + } + } CHECK_LIB("User32.lib", "gd", PHP_GD); CHECK_LIB("Gdi32.lib", "gd", PHP_GD); @@ -39,7 +48,7 @@ if (PHP_GD != "no") { gdft.c gd_gd2.c gd_gd.c gd_gif_in.c gd_gif_out.c gdhelpers.c gd_io.c gd_io_dp.c \ gd_io_file.c gd_io_ss.c gd_jpeg.c gdkanji.c gd_png.c gd_ss.c \ gdtables.c gd_topal.c gd_wbmp.c gdxpm.c wbmp.c gd_xbm.c gd_security.c gd_transform.c \ - gd_filter.c gd_pixelate.c gd_rotate.c gd_color_match.c gd_webp.c \ + gd_filter.c gd_pixelate.c gd_rotate.c gd_color_match.c gd_webp.c gd_avif.c \ gd_crop.c gd_interpolation.c gd_matrix.c gd_bmp.c gd_tga.c", "gd"); AC_DEFINE('HAVE_LIBGD', 1, 'GD support'); ADD_FLAG("CFLAGS_GD", " \ diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 39fea969f0385..08974023a65d3 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -54,7 +54,6 @@ # include #endif - #include "gd_compat.h" #ifdef HAVE_GD_BUNDLED @@ -371,6 +370,7 @@ PHP_MINIT_FUNCTION(gd) REGISTER_INI_ENTRIES(); + REGISTER_LONG_CONSTANT("IMG_AVIF", PHP_IMG_AVIF, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_GIF", PHP_IMG_GIF, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_JPG", PHP_IMG_JPG, CONST_CS | CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_JPEG", PHP_IMG_JPEG, CONST_CS | CONST_PERSISTENT); @@ -601,6 +601,9 @@ PHP_MINFO_FUNCTION(gd) #ifdef HAVE_GD_BMP php_info_print_table_row(2, "BMP Support", "enabled"); #endif +#ifdef HAVE_GD_AVIF + php_info_print_table_row(2, "AVIF Support", "enabled"); +#endif #ifdef HAVE_GD_TGA php_info_print_table_row(2, "TGA Read Support", "enabled"); #endif @@ -655,6 +658,11 @@ PHP_FUNCTION(gd_info) #else add_assoc_bool(return_value, "BMP Support", 0); #endif +#ifdef HAVE_GD_AVIF + add_assoc_bool(return_value, "AVIF Support", 1); +#else + add_assoc_bool(return_value, "AVIF Support", 0); +#endif #ifdef HAVE_GD_TGA add_assoc_bool(return_value, "TGA Read Support", 1); #else @@ -1407,7 +1415,7 @@ PHP_FUNCTION(imagecreate) } /* }}} */ -/* {{{ Return the types of images supported in a bitfield - 1=GIF, 2=JPEG, 4=PNG, 8=WBMP, 16=XPM */ +/* {{{ Return the types of images supported in a bitfield - 1=GIF, 2=JPEG, 4=PNG, 8=WBMP, 16=XPM, etc */ PHP_FUNCTION(imagetypes) { int ret = 0; @@ -1431,6 +1439,9 @@ PHP_FUNCTION(imagetypes) #ifdef HAVE_GD_TGA ret |= PHP_IMG_TGA; #endif +#ifdef HAVE_GD_AVIF + ret |= PHP_IMG_AVIF; +#endif if (zend_parse_parameters_none() == FAILURE) { RETURN_THROWS(); @@ -1589,6 +1600,15 @@ PHP_FUNCTION(imagecreatefromstring) RETURN_FALSE; #endif + case PHP_GDIMG_TYPE_AVIF: +#ifdef HAVE_GD_AVIF + im = _php_image_create_from_string(data, "AVIF", gdImageCreateFromAvifCtx); + break; +#else + php_error_docref(NULL, E_WARNING, "No AVIF support in this PHP build"); + RETURN_FALSE; +#endif + default: php_error_docref(NULL, E_WARNING, "Data is not in a recognized format"); RETURN_FALSE; @@ -1769,6 +1789,15 @@ PHP_FUNCTION(imagecreatefromxbm) } /* }}} */ +#ifdef HAVE_GD_AVIF +/* {{{ Create a new image from AVIF file or URL */ +PHP_FUNCTION(imagecreatefromavif) +{ + _php_image_create_from(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_AVIF, "AVIF", gdImageCreateFromAvif, gdImageCreateFromAvifCtx); +} +/* }}} */ +#endif /* HAVE_GD_AVIF */ + #ifdef HAVE_GD_XPM /* {{{ Create a new image from XPM file or URL */ PHP_FUNCTION(imagecreatefromxpm) @@ -1996,6 +2025,15 @@ PHP_FUNCTION(imagewebp) /* }}} */ #endif /* HAVE_GD_WEBP */ +#ifdef HAVE_GD_AVIF +/* {{{ Output AVIF image to browser or file */ +PHP_FUNCTION(imageavif) +{ + _php_image_output_ctx(INTERNAL_FUNCTION_PARAM_PASSTHRU, PHP_GDIMG_TYPE_AVIF, "AVIF", gdImageAvifCtx); +} +/* }}} */ +#endif /* HAVE_GD_AVIF */ + #ifdef HAVE_GD_JPG /* {{{ Output JPEG image to browser or file */ PHP_FUNCTION(imagejpeg) @@ -4178,7 +4216,7 @@ static gdIOCtx *create_output_context() { static void _php_image_output_ctx(INTERNAL_FUNCTION_PARAMETERS, int image_type, char *tn, void (*func_p)()) { zval *imgind; - zend_long quality = -1, basefilter = -1; + zend_long quality = -1, basefilter = -1, speed = -1; gdImagePtr im; gdIOCtx *ctx = NULL; zval *to_zval = NULL; @@ -4191,6 +4229,10 @@ static void _php_image_output_ctx(INTERNAL_FUNCTION_PARAMETERS, int image_type, if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|z!ll", &imgind, gd_image_ce, &to_zval, &quality, &basefilter) == FAILURE) { RETURN_THROWS(); } + } else if (image_type == PHP_GDIMG_TYPE_AVIF) { + if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|z!ll", &imgind, gd_image_ce, &to_zval, &quality, &speed) == FAILURE) { + RETURN_THROWS(); + } } else { if (zend_parse_parameters(ZEND_NUM_ARGS(), "O|z!l", &imgind, gd_image_ce, &to_zval, &quality) == FAILURE) { RETURN_THROWS(); @@ -4218,6 +4260,12 @@ static void _php_image_output_ctx(INTERNAL_FUNCTION_PARAMETERS, int image_type, } (*func_p)(im, ctx, (int) quality); break; + case PHP_GDIMG_TYPE_AVIF: + if (speed == -1) { + speed = 6; + } + (*func_p)(im, ctx, (int) quality, (int) speed); + break; case PHP_GDIMG_TYPE_PNG: (*func_p)(im, ctx, (int) quality, (int) basefilter); break; diff --git a/ext/gd/gd.stub.php b/ext/gd/gd.stub.php index 068cc8797ed53..5e39ef83cd571 100644 --- a/ext/gd/gd.stub.php +++ b/ext/gd/gd.stub.php @@ -66,6 +66,10 @@ function imagetypes(): int {} function imagecreatefromstring(string $data): GdImage|false {} +#ifdef HAVE_GD_AVIF +function imagecreatefromavif(string $filename): GdImage|false {} +#endif + function imagecreatefromgif(string $filename): GdImage|false {} #ifdef HAVE_GD_JPG @@ -104,6 +108,11 @@ function imagecreatefromtga(string $filename): GdImage|false {} function imagexbm(GdImage $image, ?string $filename, ?int $foreground_color = null): bool {} +#ifdef HAVE_GD_AVIF +/** @param resource|string|null $file */ +function imageavif(GdImage $image, $file = null, int $quality = -1, int $speed = -1): bool {} +#endif + /** @param resource|string|null $file */ function imagegif(GdImage $image, $file = null): bool {} diff --git a/ext/gd/gd_arginfo.h b/ext/gd/gd_arginfo.h index 83b72fbdee1c9..dad8f3a3eac03 100644 --- a/ext/gd/gd_arginfo.h +++ b/ext/gd/gd_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 4db5a04f57436fffff4d34f4e44db7b7fdc39874 */ + * Stub hash: 6c091ec5dc43771d26c3dac479ede7b38aa6e574 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_gd_info, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -144,6 +144,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_imagecreatefromstring, 0, 1, ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) ZEND_END_ARG_INFO() +#if defined(HAVE_GD_AVIF) +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_imagecreatefromavif, 0, 1, GdImage, MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) +ZEND_END_ARG_INFO() +#endif + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_imagecreatefromgif, 0, 1, GdImage, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -206,6 +212,15 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagexbm, 0, 2, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, foreground_color, IS_LONG, 1, "null") ZEND_END_ARG_INFO() +#if defined(HAVE_GD_AVIF) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imageavif, 0, 1, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, image, GdImage, 0) + ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, file, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, quality, IS_LONG, 0, "-1") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, speed, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() +#endif + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagegif, 0, 1, _IS_BOOL, 0) ZEND_ARG_OBJ_INFO(0, image, GdImage, 0) ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, file, "null") @@ -593,6 +608,9 @@ ZEND_FUNCTION(imagesetbrush); ZEND_FUNCTION(imagecreate); ZEND_FUNCTION(imagetypes); ZEND_FUNCTION(imagecreatefromstring); +#if defined(HAVE_GD_AVIF) +ZEND_FUNCTION(imagecreatefromavif); +#endif ZEND_FUNCTION(imagecreatefromgif); #if defined(HAVE_GD_JPG) ZEND_FUNCTION(imagecreatefromjpeg); @@ -618,6 +636,9 @@ ZEND_FUNCTION(imagecreatefrombmp); ZEND_FUNCTION(imagecreatefromtga); #endif ZEND_FUNCTION(imagexbm); +#if defined(HAVE_GD_AVIF) +ZEND_FUNCTION(imageavif); +#endif ZEND_FUNCTION(imagegif); #if defined(HAVE_GD_PNG) ZEND_FUNCTION(imagepng); @@ -728,6 +749,9 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(imagecreate, arginfo_imagecreate) ZEND_FE(imagetypes, arginfo_imagetypes) ZEND_FE(imagecreatefromstring, arginfo_imagecreatefromstring) +#if defined(HAVE_GD_AVIF) + ZEND_FE(imagecreatefromavif, arginfo_imagecreatefromavif) +#endif ZEND_FE(imagecreatefromgif, arginfo_imagecreatefromgif) #if defined(HAVE_GD_JPG) ZEND_FE(imagecreatefromjpeg, arginfo_imagecreatefromjpeg) @@ -753,6 +777,9 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(imagecreatefromtga, arginfo_imagecreatefromtga) #endif ZEND_FE(imagexbm, arginfo_imagexbm) +#if defined(HAVE_GD_AVIF) + ZEND_FE(imageavif, arginfo_imageavif) +#endif ZEND_FE(imagegif, arginfo_imagegif) #if defined(HAVE_GD_PNG) ZEND_FE(imagepng, arginfo_imagepng) diff --git a/ext/gd/libgd/gd.h b/ext/gd/libgd/gd.h index f274b3aebad73..163254f75f379 100644 --- a/ext/gd/libgd/gd.h +++ b/ext/gd/libgd/gd.h @@ -364,6 +364,10 @@ gdImagePtr gdImageCreateFromWebp(FILE *fd); gdImagePtr gdImageCreateFromWebpCtx(gdIOCtxPtr in); gdImagePtr gdImageCreateFromWebpPtr (int size, void *data); +gdImagePtr gdImageCreateFromAvif(FILE *infile); +gdImagePtr gdImageCreateFromAvifPtr(int size, void *data); +gdImagePtr gdImageCreateFromAvifCtx(gdIOCtx *infile); + gdImagePtr gdImageCreateFromTga( FILE * fp ); gdImagePtr gdImageCreateFromTgaCtx(gdIOCtx* ctx); gdImagePtr gdImageCreateFromTgaPtr(int size, void *data); @@ -624,6 +628,13 @@ gdImagePtr gdImageCreateFromGif(FILE *fd); gdImagePtr gdImageCreateFromGifCtx(gdIOCtxPtr in); gdImagePtr gdImageCreateFromGifSource(gdSourcePtr in); +//TODO: we may not need all of these +void gdImageAvif(gdImagePtr im, FILE *outfile); +void gdImageAvifEx(gdImagePtr im, FILE *outfile, int quality, int speed); +void *gdImageAvifPtr(gdImagePtr im, int *size); +void *gdImageAvifPtrEx(gdImagePtr im, int *size, int quality, int speed); +void gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed); + /* A custom data sink. For backwards compatibility. Use gdIOCtx instead. */ /* The sink function must return -1 on error, otherwise the number diff --git a/ext/gd/libgd/gd_avif.c b/ext/gd/libgd/gd_avif.c new file mode 100644 index 0000000000000..01bcbf14abdb8 --- /dev/null +++ b/ext/gd/libgd/gd_avif.c @@ -0,0 +1,603 @@ +#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 simply the default encoding speed of the AV1 codec. + This could be as slow as 0. So we use 6, which is currently considered to be a fine default. +*/ + +#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 6 + +// 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 = avifAlloc(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) { + avifFree(dataBuf); + return AVIF_RESULT_IO_ERROR; + } + + out->data = dataBuf; + out->size = charsRead; + return AVIF_RESULT_OK; +} + +// avif.h says this is optional, but it seemed easy to implement. +static void destroyAvifIO(struct avifIO *io) { + gdFree(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. +*/ +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. +*/ +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 +*/ +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(GD_NOTICE, "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, + and qualities or speeds that are lower than the maximum value get clamped to the maxmum value. + Note that AVIF_SPEED_DEFAULT is -1. If we ever set SPEED_DEFAULT = AVIF_SPEED_DEFAULT, + we'd want to add a conditional to ensure that value doesn't get clamped. + + + Returns: + + * for , , and , nothing. + * for and , a pointer to the image in memory. +*/ + +/* + If we're passed the QUALITY_DEFAULT of -1, set the quantizer params to QUANTIZER_DEFAULT. +*/ +void gdImageAvifCtx(gdImagePtr im, gdIOCtx *outfile, int quality, int speed) +{ + avifResult result; + avifRGBImage rgb; + avifRWData avifOutput = AVIF_DATA_EMPTY; + avifBool lossless = quality == 100; + avifEncoder *encoder = NULL; + + uint32_t val; + uint8_t *p; + int x, y; + + if (im == NULL) + return; + + if (!gdImageTrueColor(im)) { + gd_error("avif error - avif doesn't support palette images"); + return; + } + + if (!gdImageSX(im) || !gdImageSY(im)) { + gd_error("avif error - image dimensions must not be zero"); + return; + } + + if (overflow2(gdImageSX(im), gdImageSY(im))) { + gd_error("avif error - image dimensions are too large"); + return; + } + + 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); + if (isAvifError(result, "Could not convert image to YUV")) + 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; + + if (!setEncoderTilesAndThreads(encoder, &rgb)) + goto cleanup; + + //TODO: is there a reason to use timeSscales != 1? + result = avifEncoderAddImage(encoder, avifIm, 1, AVIF_ADD_IMAGE_FLAG_SINGLE); + if (isAvifError(result, "Could not encode image")) + goto cleanup; + + result = avifEncoderFinish(encoder, &avifOutput); + if (isAvifError(result, "Could not finish encoding")) + 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); +} + +void gdImageAvifEx(gdImagePtr im, FILE *outFile, int quality, int speed) +{ + gdIOCtx *out = gdNewFileCtx(outFile); + + if (out != NULL) { + gdImageAvifCtx(im, out, quality, speed); + out->gd_free(out); + } +} + +void gdImageAvif(gdImagePtr im, FILE *outFile) +{ + gdImageAvifEx(im, outFile, QUALITY_DEFAULT, SPEED_DEFAULT); +} + +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; + } + + gdImageAvifCtx(im, out, quality, speed); + rv = gdDPExtractData(out, size); + + out->gd_free(out); + return rv; +} + +void * gdImageAvifPtr(gdImagePtr im, int *size) +{ + return gdImageAvifPtrEx(im, size, QUALITY_DEFAULT, AVIF_SPEED_DEFAULT); +} + +#endif /* HAVE_LIBAVIF */ diff --git a/ext/gd/libgd/gd_intern.h b/ext/gd/libgd/gd_intern.h index 27f3d10badfdd..ce72bf1cc1f93 100644 --- a/ext/gd/libgd/gd_intern.h +++ b/ext/gd/libgd/gd_intern.h @@ -8,6 +8,7 @@ #define MAX(a,b) ((a)<(b)?(b):(a)) #endif #define MAX3(a,b,c) ((a)<(b)?(MAX(b,c)):(MAX(a,c))) +#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) #endif diff --git a/ext/gd/libgd/gd_interpolation.c b/ext/gd/libgd/gd_interpolation.c index 3fce0100e8d94..cf8821372d923 100644 --- a/ext/gd/libgd/gd_interpolation.c +++ b/ext/gd/libgd/gd_interpolation.c @@ -60,6 +60,7 @@ #include "gd.h" #include "gdhelpers.h" +#include "gd_intern.h" #ifdef _MSC_VER # pragma optimize("t", on) @@ -85,8 +86,6 @@ #endif #define MAX3(a,b,c) ((a)<(b)?(MAX(b,c)):(MAX(a,c))) -#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) - /* only used here, let do a generic fixed point integers later if required by other part of GD */ typedef long gdFixed; diff --git a/ext/gd/php_gd.h b/ext/gd/php_gd.h index 1c09bc33d8950..e804e748d2e88 100644 --- a/ext/gd/php_gd.h +++ b/ext/gd/php_gd.h @@ -39,6 +39,7 @@ #define PHP_GDIMG_TYPE_WEBP 11 #define PHP_GDIMG_TYPE_BMP 12 #define PHP_GDIMG_TYPE_TGA 13 +#define PHP_GDIMG_TYPE_AVIF 14 #define PHP_IMG_GIF 1 #define PHP_IMG_JPG 2 @@ -49,6 +50,7 @@ #define PHP_IMG_WEBP 32 #define PHP_IMG_BMP 64 #define PHP_IMG_TGA 128 +#define PHP_IMG_AVIF 256 #ifdef PHP_WIN32 # ifdef PHP_GD_EXPORTS @@ -68,6 +70,7 @@ PHPAPI extern const char php_sig_png[8]; PHPAPI extern const char php_sig_bmp[2]; PHPAPI extern const char php_sig_riff[4]; PHPAPI extern const char php_sig_webp[4]; +PHPAPI extern const char php_sig_avif[4]; extern zend_module_entry gd_module_entry; #define phpext_gd_ptr &gd_module_entry diff --git a/ext/gd/tests/avif_decode_encode.phpt b/ext/gd/tests/avif_decode_encode.phpt new file mode 100644 index 0000000000000..f823f6c9967ee --- /dev/null +++ b/ext/gd/tests/avif_decode_encode.phpt @@ -0,0 +1,73 @@ +--TEST-- +avif decoding/encoding tests +--EXTENSIONS-- +gd +--SKIPIF-- + +--FILE-- + + +--EXPECT-- +Decoding AVIF image: ok +Default AVIF encoding: ok +Encoding AVIF at quality 70: ok +Encoding AVIF at quality 70 with speed 5: ok +Encoding AVIF with default quality: ok +Encoding AVIF with illegal quality: ok +Encoding AVIF with illegal speed: ok +Encoding AVIF losslessly... ok +Decoding the AVIF we just wrote... +How many pixels are different in the two images? 0 diff --git a/ext/gd/tests/girl.avif b/ext/gd/tests/girl.avif new file mode 100644 index 0000000000000..761e1fa19a94c Binary files /dev/null and b/ext/gd/tests/girl.avif differ