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..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") && @@ -58,7 +63,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..293235bfa6a02 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 IMG_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..e25eeced187d5 --- /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..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 @@ -1426,6 +1430,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 +1513,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 +1548,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(size, 16); +#endif +} +#endif + /* * Module initialization routine for 2-pass color quantization. */ @@ -1514,6 +1620,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) { 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)