From 912ca5ed604bbccd52ca32a20e486c7ba93780a2 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Wed, 25 Dec 2024 20:03:16 +0100 Subject: [PATCH 1/5] Support alternative color quantization algorithms As is, ext/gd only supports the default color quantization algorithm, which is used for `imagetruecolortopalette()`. However, libgd supports alternative algorithms as of GD 2.1.0, namely NeuQuant (which is built in) and libimagequant (which is used by pngquant). We add support for these alternative color quantization algorithms as small wrappers around the two respective libgd functions, introducing also a couple of constants. We also add the nnquant module to our bundled libgd. --- ext/gd/config.m4 | 1 + ext/gd/config.w32 | 2 +- ext/gd/gd.c | 62 ++++ ext/gd/gd.stub.php | 28 ++ ext/gd/gd_arginfo.h | 22 +- ext/gd/libgd/gd.c | 5 + ext/gd/libgd/gd.h | 59 ++++ ext/gd/libgd/gd_nnquant.c | 613 ++++++++++++++++++++++++++++++++++++++ ext/gd/libgd/gd_nnquant.h | 16 + ext/gd/libgd/gd_topal.c | 179 +++++++++++ 10 files changed, 985 insertions(+), 2 deletions(-) create mode 100644 ext/gd/libgd/gd_nnquant.c create mode 100644 ext/gd/libgd/gd_nnquant.h diff --git a/ext/gd/config.m4 b/ext/gd/config.m4 index 7da5b8cd1b2ec..f4e8e05fd5c23 100644 --- a/ext/gd/config.m4 +++ b/ext/gd/config.m4 @@ -233,6 +233,7 @@ if test "$PHP_GD" != "no"; then libgd/gd_io.c libgd/gd_jpeg.c libgd/gd_matrix.c + libgd/gd_nnquant.c libgd/gd_pixelate.c libgd/gd_png.c libgd/gd_rotate.c diff --git a/ext/gd/config.w32 b/ext/gd/config.w32 index 4e168fc3474f6..673adacd65f99 100644 --- a/ext/gd/config.w32 +++ b/ext/gd/config.w32 @@ -58,7 +58,7 @@ if (PHP_GD != "no") { 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_avif.c \ - gd_crop.c gd_interpolation.c gd_matrix.c gd_bmp.c gd_tga.c", "gd"); + gd_crop.c gd_interpolation.c gd_matrix.c gd_bmp.c gd_tga.c gd_nnquant.c", "gd"); AC_DEFINE('HAVE_GD_BUNDLED', 1, "Define to 1 if gd extension uses GD library bundled in PHP."); AC_DEFINE('HAVE_GD_PNG', 1, "Define to 1 if gd extension has PNG support."); AC_DEFINE('HAVE_GD_BMP', 1, "Define to 1 if gd extension has BMP support."); diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 17bda3d65e2dc..35528edf9bf90 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -801,6 +801,68 @@ PHP_FUNCTION(imagecolormatch) } /* }}} */ +PHP_FUNCTION(imagetruecolortopalettesetmethod) +{ + zval *IM; + zend_long method; + zend_long speed = 0; + gdImagePtr im; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(IM, gd_image_ce) + Z_PARAM_LONG(method) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(speed) + ZEND_PARSE_PARAMETERS_END(); + + im = php_gd_libgdimageptr_from_zval_p(IM); + + if (method <= GD_QUANT_DEFAULT || method >= GD_QUANT_LIQ) { + zend_argument_value_error(2, "must be one of the GD_QUANT_* constants"); + RETURN_THROWS(); + } + + if (speed < 0 || speed > 10) { + zend_argument_value_error(3, "must be between 0 and 10"); + RETURN_THROWS(); + } + + if (gdImageTrueColorToPaletteSetMethod(im, method, speed)) { + RETURN_TRUE; + } else { + php_error_docref(NULL, E_WARNING, "Couldn't set quantization method"); + RETURN_FALSE; + } +} + +PHP_FUNCTION(imagetruecolortopalettesetquality) +{ + zval *IM; + zend_long min_quality; + zend_long max_quality; + gdImagePtr im; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT_OF_CLASS(IM, gd_image_ce) + Z_PARAM_LONG(min_quality) + Z_PARAM_LONG(max_quality) + ZEND_PARSE_PARAMETERS_END(); + + im = php_gd_libgdimageptr_from_zval_p(IM); + + if (min_quality < 1 || min_quality > 100) { + zend_argument_value_error(2, "must be between 1 and 100"); + RETURN_THROWS(); + } + + if (max_quality < 1 || max_quality > 100) { + zend_argument_value_error(3, "must be between 1 and 100"); + RETURN_THROWS(); + } + + gdImageTrueColorToPaletteSetQuality (im, min_quality, max_quality); +} + /* {{{ Set line thickness for drawing lines, ellipses, rectangles, polygons etc. */ PHP_FUNCTION(imagesetthickness) { diff --git a/ext/gd/gd.stub.php b/ext/gd/gd.stub.php index 347e43e728b87..c8a5dfaf03508 100644 --- a/ext/gd/gd.stub.php +++ b/ext/gd/gd.stub.php @@ -175,6 +175,30 @@ const IMG_EFFECT_MULTIPLY = UNKNOWN; #endif +/** + * @var int + * @cvalue GD_QUANT_DEFAULT + */ +const IMG_QUANT_DEFAULT = UNKNOWN; + +/** + * @var int + * @cvalue GD_QUANT_JQUANT + */ +const IMG_QUANT_JQUANT = UNKNOWN; + +/** + * @var int + * @cvalue GD_QUANT_NEUQUANT + */ +const IMG_QUANT_NEUQUANT = UNKNOWN; + +/** + * @var int + * @cvalue GD_QUANT_LIQ + */ +const IMG_QUANT_LIQ = UNKNOWN; + /** * @var int * @cvalue GD_CROP_DEFAULT @@ -498,6 +522,10 @@ function imagepalettetotruecolor(GdImage $image): bool {} function imagecolormatch(GdImage $image1, GdImage $image2): bool {} +function imagetruecolortopalettesetmethod(GdImage $image, int $method, int $speed = 0): bool {} + +function imagetruecolortopalettesetquality(GdImage $image, int $min_quality, int $max_quality): void {} + function imagesetthickness(GdImage $image, int $thickness): bool {} function imagefilledellipse(GdImage $image, int $center_x, int $center_y, int $width, int $height, int $color): bool {} diff --git a/ext/gd/gd_arginfo.h b/ext/gd/gd_arginfo.h index 02f57e52ba940..80cfdfd4c5efd 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: 0f8a22bff1d123313f37da400500e573baace837 */ + * Stub hash: 6df1e68080ceef570af08c72636be6428c0668eb */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_gd_info, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -35,6 +35,18 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagecolormatch, 0, 2, _IS_BOOL, ZEND_ARG_OBJ_INFO(0, image2, GdImage, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagetruecolortopalettesetmethod, 0, 2, _IS_BOOL, 0) + ZEND_ARG_OBJ_INFO(0, image, GdImage, 0) + ZEND_ARG_TYPE_INFO(0, method, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, speed, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagetruecolortopalettesetquality, 0, 3, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, image, GdImage, 0) + ZEND_ARG_TYPE_INFO(0, min_quality, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, max_quality, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagesetthickness, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_INFO(0, image, GdImage, 0) ZEND_ARG_TYPE_INFO(0, thickness, IS_LONG, 0) @@ -575,6 +587,8 @@ ZEND_FUNCTION(imageistruecolor); ZEND_FUNCTION(imagetruecolortopalette); ZEND_FUNCTION(imagepalettetotruecolor); ZEND_FUNCTION(imagecolormatch); +ZEND_FUNCTION(imagetruecolortopalettesetmethod); +ZEND_FUNCTION(imagetruecolortopalettesetquality); ZEND_FUNCTION(imagesetthickness); ZEND_FUNCTION(imagefilledellipse); ZEND_FUNCTION(imagefilledarc); @@ -711,6 +725,8 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(imagetruecolortopalette, arginfo_imagetruecolortopalette) ZEND_FE(imagepalettetotruecolor, arginfo_imagepalettetotruecolor) ZEND_FE(imagecolormatch, arginfo_imagecolormatch) + ZEND_FE(imagetruecolortopalettesetmethod, arginfo_imagetruecolortopalettesetmethod) + ZEND_FE(imagetruecolortopalettesetquality, arginfo_imagetruecolortopalettesetquality) ZEND_FE(imagesetthickness, arginfo_imagesetthickness) ZEND_FE(imagefilledellipse, arginfo_imagefilledellipse) ZEND_FE(imagefilledarc, arginfo_imagefilledarc) @@ -879,6 +895,10 @@ static void register_gd_symbols(int module_number) #if defined(gdEffectMultiply) REGISTER_LONG_CONSTANT("IMG_EFFECT_MULTIPLY", gdEffectMultiply, CONST_PERSISTENT); #endif + REGISTER_LONG_CONSTANT("IMG_QUANT_DEFAULT", GD_QUANT_DEFAULT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("IMG_QUANT_JQUANT", GD_QUANT_JQUANT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("IMG_QUANT_NEUQUANT", GD_QUANT_NEUQUANT, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("IMG_QUANT_LIQ", GD_QUANT_LIQ, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_CROP_DEFAULT", GD_CROP_DEFAULT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_CROP_TRANSPARENT", GD_CROP_TRANSPARENT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("IMG_CROP_BLACK", GD_CROP_BLACK, CONST_PERSISTENT); diff --git a/ext/gd/libgd/gd.c b/ext/gd/libgd/gd.c index 44a90773ee12d..ab75d35dabad3 100644 --- a/ext/gd/libgd/gd.c +++ b/ext/gd/libgd/gd.c @@ -1020,6 +1020,11 @@ gdImagePtr gdImageClone (gdImagePtr src) { dst->res_x = src->res_x; dst->res_y = src->res_y; + dst->paletteQuantizationMethod = src->paletteQuantizationMethod; + dst->paletteQuantizationSpeed = src->paletteQuantizationSpeed; + dst->paletteQuantizationMinQuality = src->paletteQuantizationMinQuality; + dst->paletteQuantizationMaxQuality = src->paletteQuantizationMaxQuality; + dst->interpolation_id = src->interpolation_id; dst->interpolation = src->interpolation; diff --git a/ext/gd/libgd/gd.h b/ext/gd/libgd/gd.h index 672ddf94837cd..1f59ad006a3eb 100644 --- a/ext/gd/libgd/gd.h +++ b/ext/gd/libgd/gd.h @@ -106,6 +106,33 @@ int gdAlphaBlend(int dest, int src); int gdLayerOverlay(int dst, int src); int gdLayerMultiply(int dest, int src); +/** + * Group: Color Quantization + * + * Enum: gdPaletteQuantizationMethod + * + * Constants: + * GD_QUANT_DEFAULT - GD_QUANT_LIQ if libimagequant is available, + * GD_QUANT_JQUANT otherwise. + * GD_QUANT_JQUANT - libjpeg's old median cut. Fast, but only uses 16-bit + * color. + * GD_QUANT_NEUQUANT - NeuQuant - approximation using Kohonen neural network. + * GD_QUANT_LIQ - A combination of algorithms used in libimagequant + * aiming for the highest quality at cost of speed. + * + * Note that GD_QUANT_JQUANT does not retain the alpha channel, and + * GD_QUANT_NEUQUANT does not support dithering. + * + * See also: + * - + */ +enum gdPaletteQuantizationMethod { + GD_QUANT_DEFAULT = 0, + GD_QUANT_JQUANT = 1, + GD_QUANT_NEUQUANT = 2, + GD_QUANT_LIQ = 3 +}; + /** * Group: Transform * @@ -241,6 +268,18 @@ typedef struct gdImageStruct { int cy2; unsigned int res_x; unsigned int res_y; + + /* Selects quantization method, see gdImageTrueColorToPaletteSetMethod() and gdPaletteQuantizationMethod enum. */ + int paletteQuantizationMethod; + /* speed/quality trade-off. 1 = best quality, 10 = best speed. 0 = method-specific default. + Applicable to GD_QUANT_LIQ and GD_QUANT_NEUQUANT. */ + int paletteQuantizationSpeed; + /* Image will remain true-color if conversion to palette cannot achieve given quality. + Value from 1 to 100, 1 = ugly, 100 = perfect. Applicable to GD_QUANT_LIQ.*/ + int paletteQuantizationMinQuality; + /* Image will use minimum number of palette colors needed to achieve given quality. Must be higher than paletteQuantizationMinQuality + Value from 1 to 100, 1 = ugly, 100 = perfect. Applicable to GD_QUANT_LIQ.*/ + int paletteQuantizationMaxQuality; gdInterpolationMethod interpolation_id; interpolation_method interpolation; } gdImage; @@ -575,6 +614,24 @@ int gdImagePaletteToTrueColor(gdImagePtr src); and im2 is the palette version */ int gdImageColorMatch(gdImagePtr im1, gdImagePtr im2); +/* Selects quantization method used for subsequent gdImageTrueColorToPalette calls. + See gdPaletteQuantizationMethod enum (e.g. GD_QUANT_NEUQUANT, GD_QUANT_LIQ). + Speed is from 1 (highest quality) to 10 (fastest). + Speed 0 selects method-specific default (recommended). + + Returns FALSE if the given method is invalid or not available. +*/ +int gdImageTrueColorToPaletteSetMethod (gdImagePtr im, int method, int speed); + +/* + Chooses quality range that subsequent call to gdImageTrueColorToPalette will aim for. + Min and max quality is in range 1-100 (1 = ugly, 100 = perfect). Max must be higher than min. + If palette cannot represent image with at least min_quality, then image will remain true-color. + If palette can represent image with quality better than max_quality, then lower number of colors will be used. + This function has effect only when GD_QUANT_LIQ method has been selected and the source image is true-color. +*/ +void gdImageTrueColorToPaletteSetQuality (gdImagePtr im, int min_quality, int max_quality); + /* Specifies a color index (if a palette image) or an RGB color (if a truecolor image) which should be considered 100% transparent. FOR TRUECOLOR IMAGES, @@ -739,6 +796,8 @@ void gdImageAlphaBlending(gdImagePtr im, int alphaBlendingArg); void gdImageAntialias(gdImagePtr im, int antialias); void gdImageSaveAlpha(gdImagePtr im, int saveAlphaArg); +gdImagePtr gdImageNeuQuant(gdImagePtr im, const int max_color, int sample_factor); + enum gdPixelateMode { GD_PIXELATE_UPPERLEFT, GD_PIXELATE_AVERAGE diff --git a/ext/gd/libgd/gd_nnquant.c b/ext/gd/libgd/gd_nnquant.c new file mode 100644 index 0000000000000..fbbadb2b2459a --- /dev/null +++ b/ext/gd/libgd/gd_nnquant.c @@ -0,0 +1,613 @@ +/* NeuQuant Neural-Net Quantization Algorithm + * ------------------------------------------ + * + * Copyright (c) 1994 Anthony Dekker + * + * NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. + * See "Kohonen neural networks for optimal colour quantization" + * in "Network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367. + * for a discussion of the algorithm. + * See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML + * + * Any party obtaining a copy of these files from the author, directly or + * indirectly, is granted, free of charge, a full and unrestricted irrevocable, + * world-wide, paid up, royalty-free, nonexclusive right and license to deal + * in this software and documentation files (the "Software"), including without + * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons who receive + * copies from any such party to do so, with the only requirement being + * that this copyright notice remain intact. + * + * + * Modified to process 32bit RGBA images. + * Stuart Coyle 2004-2007 + * From: http://pngnq.sourceforge.net/ + * + * Ported to libgd by Pierre A. Joye + * (and make it thread safety by droping static and global variables) + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include +#include +#include "gd.h" +#include "gdhelpers.h" +#include "gd_errors.h" + +#include "gd_nnquant.h" + +/* Network Definitions + ------------------- */ + +#define maxnetpos (MAXNETSIZE-1) +#define netbiasshift 4 /* bias for colour values */ +#define ncycles 100 /* no. of learning cycles */ + +/* defs for freq and bias */ +#define intbiasshift 16 /* bias for fractions */ +#define intbias (((int) 1)<>betashift) /* beta = 1/1024 */ +#define betagamma (intbias<<(gammashift-betashift)) + +/* defs for decreasing radius factor */ +#define initrad (MAXNETSIZE>>3) /* for 256 cols, radius starts */ +#define radiusbiasshift 6 /* at 32.0 biased by 6 bits */ +#define radiusbias (((int) 1)<network, 0, sizeof(nq_pixel)*MAXNETSIZE); + + nnq->thepicture = thepic; + nnq->lengthcount = len; + nnq->samplefac = sample; + nnq->netsize = colours; + + for (i=0; i < nnq->netsize; i++) { + p = nnq->network[i]; + p[0] = p[1] = p[2] = p[3] = (i << (netbiasshift+8)) / nnq->netsize; + nnq->freq[i] = intbias / nnq->netsize; /* 1/netsize */ + nnq->bias[i] = 0; + } +} + +/* -------------------------- */ + +/* Unbias network to give byte values 0..255 and record + * position i to prepare for sort + */ +/* -------------------------- */ + +static void unbiasnet(nn_quant *nnq) +{ + int i,j,temp; + + for (i=0; i < nnq->netsize; i++) { + for (j=0; j<4; j++) { + /* OLD CODE: network[i][j] >>= netbiasshift; */ + /* Fix based on bug report by Juergen Weigert jw@suse.de */ + temp = (nnq->network[i][j] + (1 << (netbiasshift - 1))) >> netbiasshift; + if (temp > 255) temp = 255; + nnq->network[i][j] = temp; + } + nnq->network[i][4] = i; /* record colour no */ + } +} + +/* Output colormap to unsigned char ptr in RGBA format */ +static void getcolormap(nn_quant *nnq, unsigned char *map) +{ + int i,j; + for(j=0; j < nnq->netsize; j++) { + for (i=3; i>=0; i--) { + *map = nnq->network[j][i]; + map++; + } + } +} + +/* Insertion sort of network and building of netindex[0..255] (to do after unbias) + ------------------------------------------------------------------------------- */ +static void inxbuild(nn_quant *nnq) +{ + register int i,j,smallpos,smallval; + register int *p,*q; + int previouscol,startpos; + + previouscol = 0; + startpos = 0; + for (i=0; i < nnq->netsize; i++) { + p = nnq->network[i]; + smallpos = i; + smallval = p[2]; /* index on g */ + /* find smallest in i..netsize-1 */ + for (j=i+1; j < nnq->netsize; j++) { + q = nnq->network[j]; + if (q[2] < smallval) { /* index on g */ + smallpos = j; + smallval = q[2]; /* index on g */ + } + } + q = nnq->network[smallpos]; + /* swap p (i) and q (smallpos) entries */ + if (i != smallpos) { + j = q[0]; + q[0] = p[0]; + p[0] = j; + j = q[1]; + q[1] = p[1]; + p[1] = j; + j = q[2]; + q[2] = p[2]; + p[2] = j; + j = q[3]; + q[3] = p[3]; + p[3] = j; + j = q[4]; + q[4] = p[4]; + p[4] = j; + } + /* smallval entry is now in position i */ + if (smallval != previouscol) { + nnq->netindex[previouscol] = (startpos+i)>>1; + for (j=previouscol+1; jnetindex[j] = i; + previouscol = smallval; + startpos = i; + } + } + nnq->netindex[previouscol] = (startpos+maxnetpos)>>1; + for (j=previouscol+1; j<256; j++) nnq->netindex[j] = maxnetpos; /* really 256 */ +} + + +/* Search for ABGR values 0..255 (after net is unbiased) and return colour index + ---------------------------------------------------------------------------- */ +static unsigned int inxsearch(nn_quant *nnq, int al, int b, int g, int r) +{ + register int i, j, dist, a, bestd; + register int *p; + unsigned int best; + + bestd = 1000; /* biggest possible dist is 256*3 */ + best = 0; + i = nnq->netindex[g]; /* index on g */ + j = i-1; /* start at netindex[g] and work outwards */ + + while ((inetsize) || (j>=0)) { + if (i< nnq->netsize) { + p = nnq->network[i]; + dist = p[2] - g; /* inx key */ + if (dist >= bestd) i = nnq->netsize; /* stop iter */ + else { + i++; + if (dist<0) dist = -dist; + a = p[1] - b; + if (a<0) a = -a; + dist += a; + if (dist=0) { + p = nnq->network[j]; + dist = g - p[2]; /* inx key - reverse dif */ + if (dist >= bestd) j = -1; /* stop iter */ + else { + j--; + if (dist<0) dist = -dist; + a = p[1] - b; + if (a<0) a = -a; + dist += a; + if (distbias; + f = nnq->freq; + + for (i=0; i< nnq->netsize; i++) { + n = nnq->network[i]; + dist = n[0] - al; + if (dist<0) dist = -dist; + a = n[1] - b; + if (a<0) a = -a; + dist += a; + a = n[2] - g; + if (a<0) a = -a; + dist += a; + a = n[3] - r; + if (a<0) a = -a; + dist += a; + if (dist>(intbiasshift-netbiasshift)); + if (biasdist> betashift); + *f++ -= betafreq; + *p++ += (betafreq<freq[bestpos] += beta; + nnq->bias[bestpos] -= betagamma; + return(bestbiaspos); +} + + +/* Move neuron i towards biased (a,b,g,r) by factor alpha + ---------------------------------------------------- */ + +static void altersingle(nn_quant *nnq, int alpha, int i, int al, int b, int g, int r) +{ + register int *n; + + n = nnq->network[i]; /* alter hit neuron */ + *n -= (alpha*(*n - al)) / initalpha; + n++; + *n -= (alpha*(*n - b)) / initalpha; + n++; + *n -= (alpha*(*n - g)) / initalpha; + n++; + *n -= (alpha*(*n - r)) / initalpha; +} + + +/* Move adjacent neurons by precomputed alpha*(1-((i-j)^2/[r]^2)) in radpower[|i-j|] + --------------------------------------------------------------------------------- */ + +static void alterneigh(nn_quant *nnq, int rad, int i, int al, int b,int g, int r) +{ + register int j,k,lo,hi,a; + register int *p, *q; + + lo = i-rad; + if (lo<-1) lo=-1; + hi = i+rad; + if (hi>nnq->netsize) hi=nnq->netsize; + + j = i+1; + k = i-1; + q = nnq->radpower; + while ((jlo)) { + a = (*(++q)); + if (jnetwork[j]; + *p -= (a*(*p - al)) / alpharadbias; + p++; + *p -= (a*(*p - b)) / alpharadbias; + p++; + *p -= (a*(*p - g)) / alpharadbias; + p++; + *p -= (a*(*p - r)) / alpharadbias; + j++; + } + if (k>lo) { + p = nnq->network[k]; + *p -= (a*(*p - al)) / alpharadbias; + p++; + *p -= (a*(*p - b)) / alpharadbias; + p++; + *p -= (a*(*p - g)) / alpharadbias; + p++; + *p -= (a*(*p - r)) / alpharadbias; + k--; + } + } +} + + +/* Main Learning Loop + ------------------ */ + +static void learn(nn_quant *nnq, int verbose) /* Stu: N.B. added parameter so that main() could control verbosity. */ +{ + register int i,j,al,b,g,r; + int radius,rad,alpha,step,delta,samplepixels; + register unsigned char *p; + unsigned char *lim; + + nnq->alphadec = 30 + ((nnq->samplefac-1)/3); + p = nnq->thepicture; + lim = nnq->thepicture + nnq->lengthcount; + samplepixels = nnq->lengthcount/(4 * nnq->samplefac); + /* here's a problem with small images: samplepixels < ncycles => delta = 0 */ + delta = samplepixels/ncycles; + /* kludge to fix */ + if(delta==0) delta = 1; + alpha = initalpha; + radius = initradius; + + rad = radius >> radiusbiasshift; + + for (i=0; iradpower[i] = alpha*(((rad*rad - i*i)*radbias)/(rad*rad)); + + if (verbose) gd_error_ex(GD_NOTICE, "beginning 1D learning: initial radius=%d\n", rad); + + if ((nnq->lengthcount%prime1) != 0) step = 4*prime1; + else { + if ((nnq->lengthcount%prime2) !=0) step = 4*prime2; + else { + if ((nnq->lengthcount%prime3) !=0) step = 4*prime3; + else step = 4*prime4; + } + } + + i = 0; + while (i < samplepixels) { + al = p[ALPHA] << netbiasshift; + b = p[BLUE] << netbiasshift; + g = p[GREEN] << netbiasshift; + r = p[RED] << netbiasshift; + j = contest(nnq, al,b,g,r); + + altersingle(nnq, alpha,j,al,b,g,r); + if (rad) alterneigh(nnq, rad,j,al,b,g,r); /* alter neighbours */ + + p += step; + while (p >= lim) p -= nnq->lengthcount; + + i++; + if (i%delta == 0) { /* FPE here if delta=0*/ + alpha -= alpha / nnq->alphadec; + radius -= radius / radiusdec; + rad = radius >> radiusbiasshift; + if (rad <= 1) rad = 0; + for (j=0; jradpower[j] = alpha*(((rad*rad - j*j)*radbias)/(rad*rad)); + } + } + if (verbose) gd_error_ex(GD_NOTICE, "finished 1D learning: final alpha=%f !\n",((float)alpha)/initalpha); +} + +/** + * Function: gdImageNeuQuant + * + * Creates a new palette image from a truecolor image + * + * This is the same as calling with the + * quantization method . + * + * Parameters: + * im - The image. + * max_color - The number of desired palette entries. + * sample_factor - The quantization precision between 1 (highest quality) and + * 10 (fastest). + * + * Returns: + * A newly create palette image; NULL on failure. + */ +gdImagePtr gdImageNeuQuant(gdImagePtr im, const int max_color, int sample_factor) +{ + const int newcolors = max_color; + const int verbose = 1; + + int bot_idx, top_idx; /* for remapping of indices */ + int remap[MAXNETSIZE]; + int i,x; + + unsigned char map[MAXNETSIZE][4]; + unsigned char *d; + + nn_quant *nnq = NULL; + + int row; + unsigned char *rgba = NULL; + gdImagePtr dst = NULL; + + /* Default it to 3 */ + if (sample_factor < 1) { + sample_factor = 3; + } + /* Start neuquant */ + /* Pierre: + * This implementation works with aligned contiguous buffer only + * Upcoming new buffers are contiguous and will be much faster. + * let don't bloat this code to support our good "old" 31bit format. + * It also lets us convert palette image, if one likes to reduce + * a palette + */ + if (overflow2(gdImageSX(im), gdImageSY(im)) + || overflow2(gdImageSX(im) * gdImageSY(im), 4)) { + goto done; + } + rgba = (unsigned char *) gdMalloc(gdImageSX(im) * gdImageSY(im) * 4); + if (!rgba) { + goto done; + } + + d = rgba; + for (row = 0; row < gdImageSY(im); row++) { + int *p = im->tpixels[row]; + register int c; + + for (i = 0; i < gdImageSX(im); i++) { + c = *p; + *d++ = gdImageAlpha(im, c); + *d++ = gdImageRed(im, c); + *d++ = gdImageBlue(im, c); + *d++ = gdImageGreen(im, c); + p++; + } + } + + nnq = (nn_quant *) gdMalloc(sizeof(nn_quant)); + if (!nnq) { + goto done; + } + + initnet(nnq, rgba, gdImageSY(im) * gdImageSX(im) * 4, sample_factor, newcolors); + + learn(nnq, verbose); + unbiasnet(nnq); + getcolormap(nnq, (unsigned char*)map); + inxbuild(nnq); + /* remapping colormap to eliminate opaque tRNS-chunk entries... */ + for (top_idx = newcolors-1, bot_idx = x = 0; x < newcolors; ++x) { + if (map[x][3] == 255) { /* maxval */ + remap[x] = top_idx--; + } else { + remap[x] = bot_idx++; + } + } + if (bot_idx != top_idx + 1) { + gd_error(" internal logic error: remapped bot_idx = %d, top_idx = %d\n", + bot_idx, top_idx); + goto done; + } + + dst = gdImageCreate(gdImageSX(im), gdImageSY(im)); + if (!dst) { + goto done; + } + + for (x = 0; x < newcolors; ++x) { + dst->red[remap[x]] = map[x][0]; + dst->green[remap[x]] = map[x][1]; + dst->blue[remap[x]] = map[x][2]; + dst->alpha[remap[x]] = map[x][3]; + dst->open[remap[x]] = 0; + dst->colorsTotal++; + } + + /* Do each image row */ + for ( row = 0; row < gdImageSY(im); ++row ) { + int offset; + unsigned char *p = dst->pixels[row]; + + /* Assign the new colors */ + offset = row * gdImageSX(im) * 4; + for(i=0; i < gdImageSX(im); i++) { + p[i] = remap[ + inxsearch(nnq, rgba[i * 4 + offset + ALPHA], + rgba[i * 4 + offset + BLUE], + rgba[i * 4 + offset + GREEN], + rgba[i * 4 + offset + RED]) + ]; + } + } + +done: + if (rgba) { + gdFree(rgba); + } + + if (nnq) { + gdFree(nnq); + } + return dst; +} diff --git a/ext/gd/libgd/gd_nnquant.h b/ext/gd/libgd/gd_nnquant.h new file mode 100644 index 0000000000000..11643b7d6222e --- /dev/null +++ b/ext/gd/libgd/gd_nnquant.h @@ -0,0 +1,16 @@ +/* maximum number of colours that can be used. + actual number is now passed to initcolors */ +#define MAXNETSIZE 256 + +/* For 256 colours, fixed arrays need 8kb, plus space for the image + ---------------------------------------------------------------- */ + + +/* four primes near 500 - assume no image has a length so large */ +/* that it is divisible by all four primes */ +#define prime1 499 +#define prime2 491 +#define prime3 487 +#define prime4 503 + +#define minpicturebytes (4*prime4) /* minimum size for input image */ diff --git a/ext/gd/libgd/gd_topal.c b/ext/gd/libgd/gd_topal.c index 2a9fb3d608dc8..c0abd8208bf8f 100644 --- a/ext/gd/libgd/gd_topal.c +++ b/ext/gd/libgd/gd_topal.c @@ -1426,6 +1426,73 @@ zeroHistogram (hist3d histogram) } } +/** + * Function: gdImageTrueColorToPaletteSetMethod + * + * Selects the quantization method + * + * That quantization method is used for all subsequent + * and calls. + * + * Parameters: + * im - The image. + * method - The quantization method, see . + * speed - The quantization speed between 1 (highest quality) and + * 10 (fastest). 0 selects a method-specific default (recommended). + * + * Returns: + * Zero if the given method is invalid or not available; non-zero otherwise. + * + * See also: + * - + */ +int gdImageTrueColorToPaletteSetMethod (gdImagePtr im, int method, int speed) +{ +#ifndef HAVE_LIBIMAGEQUANT + if (method == GD_QUANT_LIQ) { + return FALSE; + } +#endif + + if (method >= GD_QUANT_DEFAULT && method <= GD_QUANT_LIQ) { + im->paletteQuantizationMethod = method; + + if (speed < 0 || speed > 10) { + speed = 0; + } + im->paletteQuantizationSpeed = speed; + } + return TRUE; +} + +/** + * Function: gdImageTrueColorToPaletteSetQuality + * + * Chooses a quality range for quantization + * + * That quality range is used in all subsequent calls to + * and + * if the quantization method is . + * + * Parameters: + * im - The image. + * min_quality - The minimum quality in range 1-100 (1 = ugly, 100 = perfect). + * If the palette cannot represent the image with at least + * min_quality, then no conversion is done. + * max_quality - The maximum quality in range 1-100 (1 = ugly, 100 = perfect), + * which must be higher than the min_quality. If the palette can + * represent the image with a quality better than max_quality, + * then fewer colors than requested will be used. + */ +void gdImageTrueColorToPaletteSetQuality (gdImagePtr im, int min_quality, int max_quality) +{ + if (min_quality >= 0 && min_quality <= 100 && + max_quality >= 0 && max_quality <= 100 && min_quality <= max_quality) { + im->paletteQuantizationMinQuality = min_quality; + im->paletteQuantizationMaxQuality = max_quality; + } +} + static int gdImageTrueColorToPaletteBody (gdImagePtr oim, int dither, int colorsWanted, gdImagePtr *cimP); gdImagePtr gdImageCreatePaletteFromTrueColor (gdImagePtr im, int dither, int colorsWanted) @@ -1442,6 +1509,28 @@ int gdImageTrueColorToPalette (gdImagePtr im, int dither, int colorsWanted) return gdImageTrueColorToPaletteBody(im, dither, colorsWanted, 0); } +#ifdef HAVE_LIBIMAGEQUANT +/** + LIQ library needs pixels in RGBA order with alpha 0-255 (opaque 255). + This callback is run whenever source rows need to be converted from GD's format. +*/ +static void convert_gdpixel_to_rgba(liq_color output_row[], int y, int width, void *userinfo) +{ + gdImagePtr oim = userinfo; + int x; + for(x = 0; x < width; x++) { + output_row[x].r = gdTrueColorGetRed(input_buf[y][x]) * 255/gdRedMax; + output_row[x].g = gdTrueColorGetGreen(input_buf[y][x]) * 255/gdGreenMax; + output_row[x].b = gdTrueColorGetBlue(input_buf[y][x]) * 255/gdBlueMax; + int alpha = gdTrueColorGetAlpha(input_buf[y][x]); + if (gdAlphaOpaque < gdAlphaTransparent) { + alpha = gdAlphaTransparent - alpha; + } + output_row[x].a = alpha * 255/gdAlphaMax; + } +} +#endif + static void free_truecolor_image_data(gdImagePtr oim) { int i; @@ -1455,6 +1544,19 @@ static void free_truecolor_image_data(gdImagePtr oim) oim->tpixels = 0; } +#ifdef HAVE_LIBIMAGEQUANT +/* liq requires 16 byte aligned heap memory */ +static void *malloc16(size_t size) +{ +#ifndef _WIN32 + void *p; + return posix_memalign(&p, 16, size) == 0 ? p : NULL; +#else + return _aligned_malloc(16, size); +#endif +} +#endif + /* * Module initialization routine for 2-pass color quantization. */ @@ -1514,6 +1616,83 @@ static int gdImageTrueColorToPaletteBody (gdImagePtr oim, int dither, int colors } } + if (oim->paletteQuantizationMethod == GD_QUANT_NEUQUANT) { + if (cimP) { /* NeuQuant always creates a copy, so the new blank image can't be used */ + gdImageDestroy(nim); + } + nim = gdImageNeuQuant(oim, colorsWanted, oim->paletteQuantizationSpeed ? oim->paletteQuantizationSpeed : 2); + if (cimP) { + *cimP = nim; + } + if (!nim) { + return FALSE; + } else { + free_truecolor_image_data(oim); + gdImageCopy(oim, nim, 0, 0, 0, 0, oim->sx, oim->sy); + gdImageDestroy(nim); + } + return TRUE; + } + + +#ifdef HAVE_LIBIMAGEQUANT + if (oim->paletteQuantizationMethod == GD_QUANT_DEFAULT || + oim->paletteQuantizationMethod == GD_QUANT_LIQ) { + liq_attr *attr = liq_attr_create_with_allocator(malloc16, free); + liq_image *image; + liq_result *remap; + int remapped_ok = 0; + + liq_set_max_colors(attr, colorsWanted); + + /* by default make it fast to match speed of previous implementation */ + liq_set_speed(attr, oim->paletteQuantizationSpeed ? oim->paletteQuantizationSpeed : 9); + if (oim->paletteQuantizationMaxQuality) { + liq_set_quality(attr, oim->paletteQuantizationMinQuality, oim->paletteQuantizationMaxQuality); + } + image = liq_image_create_custom(attr, convert_gdpixel_to_rgba, oim, oim->sx, oim->sy, 0); + remap = liq_quantize_image(attr, image); + if (!remap) { /* minimum quality not met, leave image unmodified */ + liq_image_destroy(image); + liq_attr_destroy(attr); + goto outOfMemory; + } + + liq_set_dithering_level(remap, dither ? 1 : 0); + if (LIQ_OK == liq_write_remapped_image_rows(remap, image, output_buf)) { + remapped_ok = 1; + const liq_palette *pal = liq_get_palette(remap); + nim->transparent = -1; + unsigned int icolor; + for(icolor=0; icolor < pal->count; icolor++) { + nim->open[icolor] = 0; + nim->red[icolor] = pal->entries[icolor].r * gdRedMax/255; + nim->green[icolor] = pal->entries[icolor].g * gdGreenMax/255; + nim->blue[icolor] = pal->entries[icolor].b * gdBlueMax/255; + int alpha = pal->entries[icolor].a * gdAlphaMax/255; + if (gdAlphaOpaque < gdAlphaTransparent) { + alpha = gdAlphaTransparent - alpha; + } + nim->alpha[icolor] = alpha; + if (nim->transparent == -1 && alpha == gdAlphaTransparent) { + nim->transparent = icolor; + } + } + nim->colorsTotal = pal->count; + } + liq_result_destroy(remap); + liq_image_destroy(image); + liq_attr_destroy(attr); + + if (remapped_ok) { + if (!cimP) { + free_truecolor_image_data(oim); + } + return TRUE; + } + } +#endif + cquantize = (my_cquantize_ptr) gdCalloc (1, sizeof (my_cquantizer)); if (!cquantize) { From 6325cc61fd5cc9e9b9fc35ed5927cc4dcf86faeb Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Thu, 26 Dec 2024 13:46:14 +0100 Subject: [PATCH 2/5] Support building with libimagequant on Windows We might need to check for vcomp140.dll, too. --- ext/gd/config.w32 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ext/gd/config.w32 b/ext/gd/config.w32 index 673adacd65f99..d60e1621c2443 100644 --- a/ext/gd/config.w32 +++ b/ext/gd/config.w32 @@ -26,6 +26,11 @@ if (PHP_GD != "no") { AC_DEFINE('HAVE_XPM', 1, "Define to 1 if you have the xpm library."); AC_DEFINE('HAVE_GD_XPM', 1, "Define to 1 if gd extension has XPM support."); } + if (CHECK_LIB("imagequant.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("libimagequant.h", "CFLAGS_GD", PHP_GD) + ) { + AC_DEFINE('HAVE_LIBIMAGEQUANT', 1, "Define to 1 if you have the libimagequant library."); + } if (PHP_LIBWEBP != "no") { if ((CHECK_LIB("libwebp_a.lib", "gd", PHP_GD) || CHECK_LIB("libwebp.lib", "gd", PHP_GD)) && CHECK_HEADER_ADD_INCLUDE("decode.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp") && From fe279a5b7b6f1f49ef2be9d33e65f07c29c9efae Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Thu, 26 Dec 2024 13:49:47 +0100 Subject: [PATCH 3/5] Fixes * proper range check for $method * error message must refer to `IMG_QUANT_*` * gd_topal.c must include libimagequant.h (like upstream does) * fix _aligned_malloc() arguments --- ext/gd/gd.c | 4 ++-- ext/gd/libgd/gd_topal.c | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 35528edf9bf90..293235bfa6a02 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -817,8 +817,8 @@ PHP_FUNCTION(imagetruecolortopalettesetmethod) im = php_gd_libgdimageptr_from_zval_p(IM); - if (method <= GD_QUANT_DEFAULT || method >= GD_QUANT_LIQ) { - zend_argument_value_error(2, "must be one of the GD_QUANT_* constants"); + if (method < GD_QUANT_DEFAULT || method > GD_QUANT_LIQ) { + zend_argument_value_error(2, "must be one of the IMG_QUANT_* constants"); RETURN_THROWS(); } diff --git a/ext/gd/libgd/gd_topal.c b/ext/gd/libgd/gd_topal.c index c0abd8208bf8f..b36ca442f4d73 100644 --- a/ext/gd/libgd/gd_topal.c +++ b/ext/gd/libgd/gd_topal.c @@ -39,6 +39,10 @@ #include "gd.h" #include "gdhelpers.h" +#ifdef HAVE_LIBIMAGEQUANT +#include +#endif + /* (Re)define some defines known by libjpeg */ #define QUANT_2PASS_SUPPORTED @@ -1552,7 +1556,7 @@ static void *malloc16(size_t size) void *p; return posix_memalign(&p, 16, size) == 0 ? p : NULL; #else - return _aligned_malloc(16, size); + return _aligned_malloc(size, 16); #endif } #endif From 2f0c0826d51950cd064071982ae6b8c4195bac88 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Thu, 26 Dec 2024 19:49:53 +0100 Subject: [PATCH 4/5] Add new test and fix existing tests --- ext/gd/tests/bug67325.phpt | 1 + .../tests/imagetruecolortopalette_basic.phpt | 1 + .../imagetruecolortopalette_methods.phpt | 22 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 ext/gd/tests/imagetruecolortopalette_methods.phpt diff --git a/ext/gd/tests/bug67325.phpt b/ext/gd/tests/bug67325.phpt index b1e84242a4a3b..5cc2c6f5bc77f 100644 --- a/ext/gd/tests/bug67325.phpt +++ b/ext/gd/tests/bug67325.phpt @@ -14,6 +14,7 @@ if (!GD_BUNDLED && version_compare(GD_VERSION, '2.2.3', '<=')) { $filename = __DIR__ . DIRECTORY_SEPARATOR . 'bug67325.jpg'; $im = imagecreatefromjpeg($filename); +imagetruecolortopalettesetmethod($im, IMG_QUANT_JQUANT); imagetruecolortopalette($im, 0, 256); $white = 0; diff --git a/ext/gd/tests/imagetruecolortopalette_basic.phpt b/ext/gd/tests/imagetruecolortopalette_basic.phpt index 2f1c2961a603c..d63ff6da404a0 100644 --- a/ext/gd/tests/imagetruecolortopalette_basic.phpt +++ b/ext/gd/tests/imagetruecolortopalette_basic.phpt @@ -24,6 +24,7 @@ $b = imagecolorallocate($image,0,255,255); $half = imagefilledarc ( $image, 75, 75, 70, 70, 0, 180, $a, IMG_ARC_PIE ); $half2 = imagefilledarc ( $image, 75, 55, 80, 70, 0, -180, $b, IMG_ARC_PIE ); +imagetruecolortopalettesetmethod($image, IMG_QUANT_JQUANT); var_dump(imagetruecolortopalette($image, true, 2)); include_once __DIR__ . '/func.inc'; diff --git a/ext/gd/tests/imagetruecolortopalette_methods.phpt b/ext/gd/tests/imagetruecolortopalette_methods.phpt new file mode 100644 index 0000000000000..d3ce36a9285df --- /dev/null +++ b/ext/gd/tests/imagetruecolortopalette_methods.phpt @@ -0,0 +1,22 @@ +--TEST-- +Different quantization methods produce similar results +--EXTENSIONS-- +gd +--FILE-- + +--EXPECT-- +bool(true) From 86987c8c71a51ebbc09d97366c1c006c812092e6 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Fri, 27 Dec 2024 00:06:20 +0100 Subject: [PATCH 5/5] Fix UB See . --- ext/gd/libgd/gd_nnquant.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/gd/libgd/gd_nnquant.c b/ext/gd/libgd/gd_nnquant.c index fbbadb2b2459a..e25eeced187d5 100644 --- a/ext/gd/libgd/gd_nnquant.c +++ b/ext/gd/libgd/gd_nnquant.c @@ -309,7 +309,7 @@ static int contest(nn_quant *nnq, int al, int b, int g, int r) double bestd,bestbiasd; register int *p,*f, *n; - bestd = ~(((int) 1)<<31); + bestd = INT_MAX; bestbiasd = bestd; bestpos = 0; bestbiaspos = bestpos;