From f09dec137579d981c0f885aa170bc8d7ff8c6596 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Mon, 30 Dec 2024 17:16:13 +0100 Subject: [PATCH] Implement slow GD test helpers in C There are two test helpers which need to traverse all pixels of the given images, what is obviously rather slow in PHP. To avoid that performance penalty, we implement the functions in C and make them available as internal PHP functions, but only if `GD_TEST_HELPERS` is defined when building. We still keep the PHP implementations as fall- back in case someone wants to run the tests without the internal functions being defined. We also improve the userland functions by traversing the pixels in Z order what is more cache efficient for the libgd implementation. --- .github/actions/configure-x32/action.yml | 2 +- .github/scripts/windows/build_task.bat | 2 +- ext/gd/gd.c | 67 ++++++++++++++++++++++++ ext/gd/gd.stub.php | 6 +++ ext/gd/gd_arginfo.h | 22 +++++++- ext/gd/tests/func.inc | 28 ++++++---- ext/gd/tests/similarity.inc | 64 ++++++++++------------ 7 files changed, 143 insertions(+), 48 deletions(-) diff --git a/.github/actions/configure-x32/action.yml b/.github/actions/configure-x32/action.yml index a5c5df4f7971d..aa43ed2c1e7bb 100644 --- a/.github/actions/configure-x32/action.yml +++ b/.github/actions/configure-x32/action.yml @@ -12,7 +12,7 @@ runs: export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:/usr/lib/i386-linux-gnu/pkgconfig" ./buildconf --force - export CFLAGS="-m32 -msse2" + export CFLAGS="-m32 -msse2 -DGD_TEST_HELPERS" export CXXFLAGS="-m32 -msse2" export LDFLAGS=-L/usr/lib/i386-linux-gnu ./configure ${{ inputs.configurationParameters }} \ diff --git a/.github/scripts/windows/build_task.bat b/.github/scripts/windows/build_task.bat index fd9a956bd38fb..f499b50fb2141 100644 --- a/.github/scripts/windows/build_task.bat +++ b/.github/scripts/windows/build_task.bat @@ -32,7 +32,7 @@ if "%THREAD_SAFE%" equ "0" set ADD_CONF=%ADD_CONF% --disable-zts if "%INTRINSICS%" neq "" set ADD_CONF=%ADD_CONF% --enable-native-intrinsics=%INTRINSICS% if "%ASAN%" equ "1" set ADD_CONF=%ADD_CONF% --enable-sanitizer --enable-debug-pack -set CFLAGS=/W1 /WX /w14013 +set CFLAGS=/W1 /WX /w14013 /DGD_TEST_HELPERS cmd /c configure.bat ^ --enable-snapshot-build ^ diff --git a/ext/gd/gd.c b/ext/gd/gd.c index 17bda3d65e2dc..f64fd465481cc 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -4326,6 +4326,73 @@ PHP_FUNCTION(imageresolution) } /* }}} */ +#ifdef GD_TEST_HELPERS +static PHP_FUNCTION(imagechangedpixels) +{ + zval *IM1, *IM2; + gdImagePtr im1, im2; + int i, j, result = 0; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(IM1, gd_image_ce) + Z_PARAM_OBJECT_OF_CLASS(IM2, gd_image_ce) + ZEND_PARSE_PARAMETERS_END(); + + im1 = php_gd_libgdimageptr_from_zval_p(IM1); + im2 = php_gd_libgdimageptr_from_zval_p(IM2); + + ZEND_ASSERT(gdImageTrueColor(im1) && gdImageTrueColor(im2)); + ZEND_ASSERT(gdImageSX(im1) == gdImageSX(im2) && gdImageSY(im1) == gdImageSY(im2)); + + for (j = gdImageSY(im1) - 1; j >= 0; --j) { + for (i = gdImageSX(im1) - 1; i >= 0; --i) { + if (gdImageTrueColorPixel(im1, i, j) != gdImageTrueColorPixel(im2, i, j)) { + result++; + } + } + } + + RETURN_LONG(result); +} + +static double calc_pixel_distance(gdImagePtr im1, gdImagePtr im2, int x, int y) +{ + int c1 = gdImageGetPixel(im1, x, y); + int c2 = gdImageGetPixel(im2, x, y); +# define SQR(a) ((a) * (a)) + return sqrt( + SQR(gdImageRed(im1, c1) - gdImageRed(im2, c2)) + + SQR(gdImageGreen(im1, c1) - gdImageGreen(im2, c2)) + + SQR(gdImageBlue(im1, c1) - gdImageBlue(im2, c2))); +# undef SQR +} + +static PHP_FUNCTION(calc_image_dissimilarity) +{ + zval *IM1, *IM2; + gdImagePtr im1, im2; + int i, j; + double result = 0; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(IM1, gd_image_ce) + Z_PARAM_OBJECT_OF_CLASS(IM2, gd_image_ce) + ZEND_PARSE_PARAMETERS_END(); + + im1 = php_gd_libgdimageptr_from_zval_p(IM1); + im2 = php_gd_libgdimageptr_from_zval_p(IM2); + + ZEND_ASSERT(gdImageSX(im1) == gdImageSX(im2) && gdImageSY(im1) == gdImageSY(im2)); + + for (j = gdImageSY(im2) - 1; j >= 0; --j) { + for (i = gdImageSX(im1) - 1; i >= 0; --i) { + result += calc_pixel_distance(im1, im2, i, j); + } + } + + RETURN_DOUBLE(result); +} +#endif /********************************************************* * diff --git a/ext/gd/gd.stub.php b/ext/gd/gd.stub.php index 347e43e728b87..e387b19f12f18 100644 --- a/ext/gd/gd.stub.php +++ b/ext/gd/gd.stub.php @@ -794,3 +794,9 @@ function imagesetinterpolation(GdImage $image, int $method = IMG_BILINEAR_FIXED) * @refcount 1 */ function imageresolution(GdImage $image, ?int $resolution_x = null, ?int $resolution_y = null): array|bool {} + +#ifdef GD_TEST_HELPERS +function imagechangedpixels(GdImage $im1, GdImage $im2): int {} + +function calc_image_dissimilarity(GdImage $im1, GdImage $im2): float {} +#endif diff --git a/ext/gd/gd_arginfo.h b/ext/gd/gd_arginfo.h index 02f57e52ba940..061ed64f43e1b 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: d80b05c9e734f5690e5cf302dc190cef7d9ce702 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_gd_info, 0, 0, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -567,6 +567,18 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_imageresolution, 0, 1, MAY_BE_AR ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, resolution_y, IS_LONG, 1, "null") ZEND_END_ARG_INFO() +#if defined(GD_TEST_HELPERS) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_imagechangedpixels, 0, 2, IS_LONG, 0) + ZEND_ARG_OBJ_INFO(0, im1, GdImage, 0) + ZEND_ARG_OBJ_INFO(0, im2, GdImage, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_calc_image_dissimilarity, 0, 2, IS_DOUBLE, 0) + ZEND_ARG_OBJ_INFO(0, im1, GdImage, 0) + ZEND_ARG_OBJ_INFO(0, im2, GdImage, 0) +ZEND_END_ARG_INFO() +#endif + ZEND_FUNCTION(gd_info); ZEND_FUNCTION(imageloadfont); ZEND_FUNCTION(imagesetstyle); @@ -701,6 +713,10 @@ ZEND_FUNCTION(imageaffinematrixconcat); ZEND_FUNCTION(imagegetinterpolation); ZEND_FUNCTION(imagesetinterpolation); ZEND_FUNCTION(imageresolution); +#if defined(GD_TEST_HELPERS) +ZEND_FUNCTION(imagechangedpixels); +ZEND_FUNCTION(calc_image_dissimilarity); +#endif static const zend_function_entry ext_functions[] = { ZEND_FE(gd_info, arginfo_gd_info) @@ -839,6 +855,10 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(imagegetinterpolation, arginfo_imagegetinterpolation) ZEND_FE(imagesetinterpolation, arginfo_imagesetinterpolation) ZEND_FE(imageresolution, arginfo_imageresolution) +#if defined(GD_TEST_HELPERS) + ZEND_FE(imagechangedpixels, arginfo_imagechangedpixels) + ZEND_FE(calc_image_dissimilarity, arginfo_calc_image_dissimilarity) +#endif ZEND_FE_END }; diff --git a/ext/gd/tests/func.inc b/ext/gd/tests/func.inc index 0f10aa7d83dee..cb62adf148578 100644 --- a/ext/gd/tests/func.inc +++ b/ext/gd/tests/func.inc @@ -59,6 +59,23 @@ function get_libxpm_version() return $version; } +if (!function_exists("imagechangedpixels")) { + function imagechangedpixels(GdImage $im1, GdImage $im2): int + { + $pixels_changed = 0; + for ($y = imagesy($im1) - 1; $y >= 0; --$y) { + for ($x = imagesx($im1) - 1; $x >= 0; --$x) { + $c1 = imagecolorat($im1, $x, $y); + $c2 = imagecolorat($im2, $x, $y); + if ($c1 != $c2) { + $pixels_changed++; + } + } + } + return $pixels_changed; + } +} + /** * Tests that an in-memory image equals a PNG file. * @@ -106,16 +123,7 @@ function test_image_equals_image(GdImage $expected, GdImage $actual, bool $save_ } return; } - $pixels_changed = 0; - for ($y = 0; $y < $exp_y; $y++) { - for ($x = 0; $x < $exp_x; $x ++) { - $exp_c = imagecolorat($expected, $x, $y); - $act_c = imagecolorat($actual, $x, $y); - if ($exp_c != $act_c) { - $pixels_changed++; - } - } - } + $pixels_changed = imagechangedpixels($expected, $actual); if (!$pixels_changed) { echo "The images are equal.\n"; } else { diff --git a/ext/gd/tests/similarity.inc b/ext/gd/tests/similarity.inc index cb0dba77f2064..41fc5629a5d89 100644 --- a/ext/gd/tests/similarity.inc +++ b/ext/gd/tests/similarity.inc @@ -23,42 +23,36 @@ function get_rgb($color, &$red, &$green, &$blue) $blue = $color & 0xFF; } -/** - * Calculates the euclidean distance of two RGB values. - * - * @param int $color1 - * @param int $color2 - * - * @return int - */ -function calc_pixel_distance($color1, $color2) -{ - get_rgb($color1, $red1, $green1, $blue1); - get_rgb($color2, $red2, $green2, $blue2); - return sqrt( - pow($red1 - $red2, 2) + pow($green1 - $green2, 2) + pow($blue1 - $blue2, 2) - ); -} +if (!function_exists("calc_image_dissimilarity")) { + /** + * Calculates the euclidean distance of two RGB values. + */ + function calc_pixel_distance(int $color1, int $color2): float + { + get_rgb($color1, $red1, $green1, $blue1); + get_rgb($color2, $red2, $green2, $blue2); + return sqrt( + pow($red1 - $red2, 2) + pow($green1 - $green2, 2) + pow($blue1 - $blue2, 2) + ); + } -/** - * Calculates dissimilarity of two images. - * - * @param resource $image1 - * @param resource $image2 - * - * @return int The dissimilarity. 0 means the images are identical. The higher - * the value, the more dissimilar are the images. - */ -function calc_image_dissimilarity($image1, $image2) -{ - // assumes image1 and image2 have same width and height - $dissimilarity = 0; - for ($i = 0, $n = imagesx($image1); $i < $n; $i++) { - for ($j = 0, $m = imagesy($image1); $j < $m; $j++) { - $color1 = imagecolorat($image1, $i, $j); - $color2 = imagecolorat($image2, $i, $j); - $dissimilarity += calc_pixel_distance($color1, $color2); + /** + * Calculates dissimilarity of two images. + * + * 0 means the images are identical. The higher + * the value, the more dissimilar are the images. + */ + function calc_image_dissimilarity(GdImage $im1, GdImage $im2): float + { + // assumes image1 and image2 have same width and height + $dissimilarity = 0; + for ($j = imagesy($im1) - 1; $j >= 0; --$j) { + for ($i = imagesx($im1) - 1; $i >= 0; --$i) { + $color1 = imagecolorat($im1, $i, $j); + $color2 = imagecolorat($im2, $i, $j); + $dissimilarity += calc_pixel_distance($color1, $color2); + } } + return $dissimilarity; } - return $dissimilarity; }