* @version 2.8.2 (last revision: January 25, 2023) * @copyright © 2006 - 2023 Stefan Gabos * @license https://www.gnu.org/licenses/lgpl-3.0.txt GNU LESSER GENERAL PUBLIC LICENSE * @package Zebra_Image */ class Zebra_Image { /** * If set to `true`, JPEG images will be auto-rotated according to the {@link http://keyj.emphy.de/exif-orientation-rant/ Exif Orientation Tag} * so that they are always shown correctly. * * > If set to `true` you must also enable exif-support with `--enable-exif`.
* Windows users must enable both the `php_mbstring.dll` and `php_exif.dll` DLL's in `php.ini`.
* `php_mbstring.dll` must be loaded before `php_exif.dll`, so adjust your php.ini accordingly. * See {@link https://www.php.net/manual/en/exif.installation.php the manual}. * * Default is `false` * * @since 2.2.4 * * @var boolean */ public $auto_handle_exif_orientation; /** * Indicates whether BMP images should be compressed with run-length encoding (RLE), or not. * * > Used only if PHP version is `7.2.0+` and the file at {@link target_path} is a `BMP` image, or it will be * ignored otherwise. * * Default is `TRUE` * * @since 2.8.1 * * @var boolean */ public $bmp_compressed; /** * Indicates the file system permissions to be set for newly created images. * * Better is to leave this setting as it is. * * If you know what you are doing, here is how you can calculate the permission levels: * * - 400 Owner Read * - 200 Owner Write * - 100 Owner Execute * - 40 Group Read * - 20 Group Write * - 10 Group Execute * - 4 Global Read * - 2 Global Write * - 1 Global Execute * * Default is `0755` * * @var integer */ public $chmod_value; /** * If set to `false`, images having both width and height smaller than the required width and height will be left * untouched. {@link jpeg_quality} and {@link png_compression} will still apply! * * Available only for the {@link resize()} method. * * Default is `true` * * @var boolean */ public $enlarge_smaller_images; /** * In case of an error read this property's value to see the error's code. * * Possible error codes are: * * - `1` - source file could not be found * - `2` - source file is not readable * - `3` - could not write target file * - `4` - unsupported source file type *(note that you will also get this for animated WEBP images!)* * - `5` - unsupported target file type * - `6` - GD library version does not support target file format * - `7` - GD library is not installed! * - `8` - "chmod" command is disabled via configuration * - `9` - "exif_read_data" function is not available * * Default is `0` (no error) * * @var integer */ public $error; /** * Indicates whether the created image should be saved as a progressive JPEG. * * > Used only if the file at {@link target_path} is a `JPG/JPEG` image, or will be ignored otherwise. * * Default is `false` * * @since 2.5.0 * * @var boolean */ public $jpeg_interlace; /** * Indicates the quality of the output image (better quality means bigger file size). * * > Used only if the file at {@link target_path} is a `JPG/JPEG` image, or it will be ignored otherwise. * * Range is `0` - `100`. * * Default is `85` * * @var integer */ public $jpeg_quality; /** * Indicates the compression level of the output image (lower compression means bigger file size). * * > Used only if PHP version is `5.1.2+` and the file at {@link target_path} is a `PNG` image, or it will be * ignored otherwise. * * Range is `0` - `9`. * * Default is `9` * * @since 2.2 * * @var integer */ public $png_compression; /** * Specifies whether upon resizing images should preserve their aspect ratio. * * > Available only for the {@link resize()} method. * * Default is `true` * * @var boolean */ public $preserve_aspect_ratio; /** * Indicates whether a target file should preserve the source file's date/time. * * Default is `true` * * @since 1.0.4 * * @var boolean */ public $preserve_time; /** * Indicates whether the target image should have a `sharpen` filter applied to it. * * Can be very useful when creating thumbnails and should be used only when creating thumbnails. * * > The sharpen filter relies on the {@link https://www.php.net/manual/en/function.imageconvolution.php imageconvolution} * function which is available for PHP version `5.1.0+`, and will leave the images unaltered for older versions! * * Default is `false` * * @since 2.2 * * @var boolean */ public $sharpen_images; /** * Path to an image file to apply the transformations to. * * Supported file types are `BMP`, `GIF`, `JPEG`, `PNG` and `WEBP`. * * > `WEBP` support is available for PHP version `7.0.0+`.

* Note that even though `WEBP` support was added to PHP in version `5.5.0`, it only started working with version * `5.5.1`, while support for transparency was added with version `7.0.0`. As a result, I decided to make it * available only if PHP version is at least `7.0.0`

* Animated `WEBP` images are not currently supported by GD. * See {@link https://github.com/libgd/libgd/issues/648 here} and {@link https://bugs.php.net/bug.php?id=79809&thanks=4 here}. * * > `BMP` support is available for PHP version `7.2.0+` * * @var string */ public $source_path; /** * Path (including file name) to where to save the transformed image. * * > Can be a different format than the file at {@link source_path}. The format of the transformed image will be * determined by the file's extension. Supported file types are `BMP`, `GIF`, `JPEG`, `PNG` and `WEBP`. * * > `WEBP` support is available for PHP version `7.0.0+`.

* Note that even though `WEBP` support was added to PHP in version `5.5.0`, it only started working with version * `5.5.1`, while support for transparency was added with version `7.0.0`. As a result, I decided to make it * available only if PHP version is at least `7.0.0`

* Animated `WEBP` images are not currently supported by GD. * See {@link https://github.com/libgd/libgd/issues/648 here} and {@link https://bugs.php.net/bug.php?id=79809&thanks=4 here}. * * > `BMP` support is available for PHP version `7.2.0+` * * @var string */ public $target_path; /** * Indicates the quality level of the output image. * * > Used only if PHP version is `7.0.0+` and the file at {@link target_path} is a `WEBP` image, or it will be * ignored otherwise. * * Range is `0` - `100` * * Default is `80` * * @since 2.6.0 * * @var integer */ public $webp_quality; /** * @var resource */ private $source_identifier; /** * @var mixed */ private $source_type; /** * @var int */ private $source_width; /** * @var int */ private $source_height; /** * @var array */ private $source_transparent_color; /** * @var int */ private $source_transparent_color_index; /** * @var int */ private $source_time; /** * @var string */ private $target_type; /** * Constructor of the class. * * Initializes the class and the default properties. * * @return void */ public function __construct() { // set default values for properties $this->chmod_value = 0755; $this->error = 0; $this->jpeg_quality = 85; $this->png_compression = 9; $this->webp_quality = 80; $this->preserve_aspect_ratio = $this->preserve_time = $this->enlarge_smaller_images = $this->bmp_compressed = true; $this->sharpen_images = $this->auto_handle_exif_orientation = $this->jpeg_interlace = false; $this->source_path = $this->target_path = ''; } /** * Applies one or more filters to the image given as {@link source_path} and outputs it as the file specified as * {@link target_path}. * * > This method is available only if the {@link https://www.php.net/manual/en/function.imagefilter.php imagefilter} * function is available (available from `PHP 5+`), and will leave images unaltered otherwise. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // apply the "grayscale" filter * $img->apply_filter('grayscale'); * * // apply the "contrast" filter * $img->apply_filter('contrast', -20); * * * You can also apply multiple filters at once. In this case, the method requires a single argument, an array of * arrays, containing the filters and associated arguments, where applicable: * * * // create a sepia effect * // note how we're applying multiple filters at once * // each filter is in its own array * $img->apply_filter(array( * * // first we apply the "grayscale" filter * array('grayscale'), * * // then we apply the "colorize" filter with 90, 60, 40 as * // the values for red, green and blue * array('colorize', 90, 60, 40), * * )); * * * @param mixed $filter The case-insensitive name of the filter to apply. Can be one of the following: * * - **brightness** - changes the brightness of the image; use `arg1` to set * the level of brightness; the range of brightness is * `-255` - `255` * - **colorize** - adds specified RGB values to each pixel; use `arg1`, * `arg2` and `arg3` in the form of `red`, `green` and * `blue`, and `arg4` for the `alpha` channel; the range * for each color is `-255` to `255` and `0` to `127` for * the `alpha` where `0` indicates completely opaque * while `127` indicates completely transparent; *alpha * support is available for PHP 5.2.5+* * - **contrast** - changes the contrast of the image; use `arg1` to set * the level of contrast; the range of contrast is `-100` * to `100` * - **edgedetect** - uses edge detection to highlight the edges in the image * - **emboss** - embosses the image * - **gaussian_blur** - blurs the image using the Gaussian method * - **grayscale** - converts the image into grayscale by changing the red, * green and blue components to their weighted sum using * the same coefficients as the REC.601 luma (Y') calculation; * the alpha components are retained; for palette images * the result may differ due to palette limitations * - **mean_removal** - uses mean removal to achieve a *"sketchy"* effect * - **negate** - reverses all the colors of the image * - **pixelate** - applies pixelation effect to the image; use `arg1` to * set the block size and `arg2` to set the pixelation * effect mode; *this filter is available only for PHP * 5.3.0+* * - **selective_blur** - blurs the image * - **scatter** - applies scatter effect to the image; use `arg1` and * `arg2` to define the effect strength and additionally * `arg3` to only apply the on select pixel colors * - **smooth** - makes the image smoother; use `arg1` to set the level * of smoothness; applies a 9-cell convolution matrix * where center pixel has the weight of `arg1` and others * weight of 1.0; the result is normalized by dividing * the sum with `arg1` + 8.0 (sum of the matrix); any * float is accepted, large value (in practice: 2048 or) * more) = no change * * @param mixed $arg1 Used by the following filters: * - **brightness** - sets the brightness level (`-255` to `255`) * - **contrast** - sets the contrast level (`-100` to `100`) * - **colorize** - sets the value of the red component (`-255` to `255`) * - **pixelate** - sets the block size, in pixels * - **scatter** - effect subtraction level; this must not be higher or * equal to the addition level set with `arg3` * - **smooth** - sets the smoothness level * * @param mixed $arg2 Used by the following filters: * - **colorize** - sets the value of the green component (`-255` to `255`) * - **pixelate** - whether to use advanced pixelation effect or not (defaults to `false`) * - **scatter** - effect addition level * * @param mixed $arg3 Used by the following filters: * - **colorize** - sets the value of the blue component (`-255` to `255`) * - **scatter** - optional array indexed color values to apply effect at * * @param mixed $arg4 Used by the following filters: * - **colorize** - alpha channel; a value between `0` and `127`. `0` indicates * completely opaque while `127` indicates completely * transparent * * @since 2.2.2 * * @return boolean Returns `true` on success or false on error. * * If {@link https://www.php.net/manual/en/function.imagefilter.php imagefilter} is not * available, the method will return `false` without setting an {@link error} code. * * If the requested filter doesn't exist, or invalid arguments are passed, the method * will trigger a warning. * * If `false` is returned and you are sure that * {@link https://www.php.net/manual/en/function.imagefilter.php imagefilter} exists and that * the requested filter is valid, check the {@link error} property to see the error code. */ public function apply_filter($filter, $arg1 = '', $arg2 = '', $arg3 = '', $arg4 = '') { // if "imagefilter" function exists and the requested filter exists if (function_exists('imagefilter')) { // if image resource was successfully created if ($this->_create_from_source()) { // prepare the target image $target_identifier = $this->_prepare_image($this->source_width, $this->source_height, -1); // copy the original image imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, 0, 0, $this->source_width, $this->source_height, $this->source_width, $this->source_height ); // if multiple filters are to be applied at once if (is_array($filter)) { // iterate through the filters foreach ($filter as $arguments) { // if filter exists if (defined('IMG_FILTER_' . strtoupper($arguments[0]))) { // try to apply the filter and trigger an error if the filter could not be applied if (!@call_user_func_array('imagefilter', array_merge(array($target_identifier, constant('IMG_FILTER_' . strtoupper($arguments[0]))), array_slice($arguments, 1)))) { trigger_error('Invalid arguments used for "' . strtoupper($arguments[0]) . '" filter', E_USER_WARNING); } // if filter doesn't exists, trigger an error } else { trigger_error('Filter "' . strtoupper($arguments[0]) . '" is not available', E_USER_WARNING); } } // if a single filter is to be applied and it is available } elseif (defined('IMG_FILTER_' . strtoupper($filter))) { // get all the arguments passed to the method $arguments = func_get_args(); // try to apply the filter and trigger an error if the filter could not be applied if (!@call_user_func_array('imagefilter', array_merge(array($target_identifier, constant('IMG_FILTER_' . strtoupper($filter))), array_slice($arguments, 1)))) { trigger_error('Invalid arguments used for "' . strtoupper($arguments[0]) . '" filter', E_USER_WARNING); } // if filter doesn't exists, trigger an error } else { trigger_error('Filter "' . strtoupper($filter) . '" is not available', E_USER_WARNING); } // write image return $this->_write_image($target_identifier); } } // if script gets this far, return false // note that we do not set the error level as it has been already set // by the _create_from_source() method earlier, if the case return false; } /** * Crops a portion of the image given as {@link source_path} and outputs it as the file specified as {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // crop a rectangle of 100x100 pixels, starting from the top-left corner * $img->crop(0, 0, 100, 100); * * * @param integer $start_x x coordinate to start cropping from * * @param integer $start_y y coordinate to start cropping from * * @param integer $end_x x coordinate where to end the cropping * * @param integer $end_y y coordinate where to end the cropping * * @param mixed $background_color (Optional) A hexadecimal color value (like `#FFFFFF` or `#FFF`) used when * the cropping coordinates are off-scale (negative values and/or values * greater than the image's size) to fill the remaining space. * * When set to `-1` the script will preserve transparency for transparent `GIF` * and `PNG` images. For non-transparent images the background will be black * (`#000000`) in this case. * * Default is `-1` * * @since 1.0.4 * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error code. */ public function crop($start_x, $start_y, $end_x, $end_y, $background_color = -1) { // this method might be also called internally // in this case, there's a sixth argument that points to an already existing image identifier $args = func_get_args(); // if a sixth argument exists // for PHP 8.0.0+ GD functions return and accept \GdImage objects instead of resources (https://php.watch/versions/8.0/gdimage) if (isset($args[5]) && (is_resource($args[5]) || (version_compare(PHP_VERSION, '8.0.0', '>=') && $args[5] instanceof \GdImage))) { // that it is the image identifier that we'll be using further on $this->source_identifier = $args[5]; // set this to true so that the script will continue to execute at the next IF $result = true; // we need to make sure these are integers or PHP 8.1+ will show a warning // https://php.watch/versions/8.1/deprecate-implicit-conversion-incompatible-float-string $start_x = (int)$start_x; $start_y = (int)$start_y; $end_x = (int)$end_x; $end_y = (int)$end_y; // if method is called as usually // try to create an image resource from source path } else { $result = $this->_create_from_source(); } // if image resource was successfully created if ($result !== false) { // compute width and height $width = $end_x - $start_x; $height = $end_y - $start_y; // prepare the target image $target_identifier = $this->_prepare_image($width, $height, $background_color); $dest_x = 0; $dest_y = 0; // if starting x is negative if ($start_x < 0) { // we are adjusting the position where we place the cropped image on the target image $dest_x = abs($start_x); // and crop starting from 0 $start_x = 0; } // if ending x is larger than the image's width, adjust the width we're showing if ($end_x > ($image_width = imagesx($this->source_identifier))) { $width = $image_width - $start_x; } // if starting y is negative if ($start_y < 0) { // we are adjusting the position where we place the cropped image on the target image $dest_y = abs($start_y); // and crop starting from 0 $start_y = 0; } // if ending y is larger than the image's height, adjust the height we're showing if ($end_y > ($image_height = imagesy($this->source_identifier))) { $height = $image_height - $start_y; } // crop the image imagecopyresampled( $target_identifier, $this->source_identifier, $dest_x, $dest_y, $start_x, $start_y, $width, $height, $width, $height ); // write image return $this->_write_image($target_identifier); } // if script gets this far, return false // note that we do not set the error level as it has been already set // by the _create_from_source() method earlier return false; } /** * Flips both horizontally and vertically the image given as {@link source_path} and outputs the resulted image as * {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // flip the image both horizontally and vertically * $img->flip_both(); * * * @since 2.1 * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error code. */ public function flip_both() { return $this->_flip('both'); } /** * Flips horizontally the image given as {@link source_path} and outputs the resulted image as {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // flip the image horizontally * $img->flip_horizontal(); * * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error code. */ public function flip_horizontal() { return $this->_flip('horizontal'); } /** * Flips vertically the image given as {@link source_path} and outputs the resulted image as {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // flip the image vertically * $img->flip_vertical(); * * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error code. */ public function flip_vertical() { return $this->_flip('vertical'); } /** * Resizes the image given as {@link source_path} and outputs the resulted image as {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // apply a "sharpen" filter to the resulting images * $img->sharpen_images = true; * * // resize the image to exactly 150x150 pixels, without altering * // aspect ratio, by using the CROP_CENTER method * $img->resize(150, 150, ZEBRA_IMAGE_CROP_CENTER); * * * @param integer $width The width to resize the image to. * * If set to `0`, the width will be automatically adjusted, depending on the * value of the `height` argument so that the image preserves its aspect ratio. * * If {@link preserve_aspect_ratio} is set to `true` and both this and the * `height` arguments are values greater than `0`, the image will be resized * to the exact required width and height and the aspect ratio will be * preserved (see also the description for the `method` argument below on * how can this be done). * * If {@link preserve_aspect_ratio} is set to `false`, the image will be * resized to the required width and the aspect ratio will be ignored. * * If both `width` and `height` are set to `0`, a copy of the source image * will be created. {@link jpeg_quality} and {@link png_compression} will * still apply! * * If either `width` or `height` are set to `0`, the script will consider * the value of {@link preserve_aspect_ratio} to bet set to `true` regardless * of its actual value! * * @param integer $height The height to resize the image to. * * If set to `0`, the height will be automatically adjusted, depending on * the value of the `width` argument so that the image preserves its aspect * ratio. * * If {@link preserve_aspect_ratio} is set to `true` and both this and the * `width` arguments are values greater than `0`, the image will be resized * to the exact required width and height and the aspect ratio will be * preserved (see also the description for the `method` argument below on * how can this be done). * * If {@link preserve_aspect_ratio} is set to `false`, the image will be * resized to the required height and the aspect ratio will be ignored. * * If both `width` and `height` are set to `0`, a copy of the source image * will be created. {@link jpeg_quality} and {@link png_compression} will * * If either `width` or `height` are set to `0`, the script will consider * the value of {@link preserve_aspect_ratio} to bet set to `true` regardless * of its actual value! * * @param int $method (Optional) Method to use when resizing images to exact width and height * while preserving aspect ratio. * * If the {@link preserve_aspect_ratio} property is set to `true` and both * the `width` and `height` arguments are values greater than `0`, the image * will be resized to the exact given width and height and the aspect ratio * will be preserved by using on of the following methods: * * - **ZEBRA_IMAGE_BOXED** - the image will be scaled so that it will fit * in a box with the given width and height (both width/height will be * smaller or equal to the required width/height) and then it will * be centered both horizontally and vertically; the blank area will be * filled with the color specified by the `bgcolor` argument. (the blank * area will be filled only if the image is not transparent!) * * - **ZEBRA_IMAGE_NOT_BOXED** - the image will be scaled so that it * *could* fit in a box with the given width and height but will not be * enclosed in a box with given width and height. The new width/height * will be both smaller or equal to the required width/height. * * - **ZEBRA_IMAGE_CROP_TOPLEFT** * - **ZEBRA_IMAGE_CROP_TOPCENTER** * - **ZEBRA_IMAGE_CROP_TOPRIGHT** * - **ZEBRA_IMAGE_CROP_MIDDLELEFT** * - **ZEBRA_IMAGE_CROP_CENTER** * - **ZEBRA_IMAGE_CROP_MIDDLERIGHT** * - **ZEBRA_IMAGE_CROP_BOTTOMLEFT** * - **ZEBRA_IMAGE_CROP_BOTTOMCENTER** * - **ZEBRA_IMAGE_CROP_BOTTOMRIGHT** * * For the methods involving crop, first the image is scaled so that both * its sides are equal or greater than the respective sizes of the bounding * box; next, a region of required width and height will be cropped from * indicated region of the resulted image. * * Default is `ZEBRA_IMAGE_CROP_CENTER` * * @param mixed $background_color (Optional) The hexadecimal color (like `#FFFFFF` or `#FFF`) of the blank * area. See the `method` argument. * * When set to `-1` the script will preserve transparency for transparent `GIF` * and `PNG` images. For non-transparent images the background will be white * (`#FFFFFF`) in this case. * * Default is `-1` * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see what went * wrong. */ public function resize($width = 0, $height = 0, $method = ZEBRA_IMAGE_CROP_CENTER, $background_color = -1) { // we need to make sure these are integers or PHP 8.1+ will show a warning // https://php.watch/versions/8.1/deprecate-implicit-conversion-incompatible-float-string $width = (int)$width; $height = (int)$height; // if image resource was successfully created if ($this->_create_from_source()) { // if either width or height is to be adjusted automatically // set a flag telling the script that, even if $preserve_aspect_ratio is set to false // treat everything as if it was set to true if ($width == 0 || $height == 0) { $auto_preserve_aspect_ratio = true; } // if aspect ratio needs to be preserved if ($this->preserve_aspect_ratio || isset($auto_preserve_aspect_ratio)) { // if height is given and width is to be computed accordingly if ($width == 0 && $height > 0) { // get the original image's aspect ratio $aspect_ratio = $this->source_width / $this->source_height; // the target image's height is as given as argument to the method $target_height = $height; // compute the target image's width, preserving the aspect ratio $target_width = round($height * $aspect_ratio); // if width is given and height is to be computed accordingly } elseif ($width > 0 && $height == 0) { // get the original image's aspect ratio $aspect_ratio = $this->source_height / $this->source_width; // the target image's width is as given as argument to the method $target_width = $width; // compute the target image's height, preserving the aspect ratio $target_height = round($width * $aspect_ratio); // if both width and height are given and ZEBRA_IMAGE_BOXED or ZEBRA_IMAGE_NOT_BOXED methods are to be used } elseif ($width > 0 && $height > 0 && ($method == 0 || $method == 1)) { // compute the horizontal and vertical aspect ratios $vertical_aspect_ratio = $height / $this->source_height; $horizontal_aspect_ratio = $width / $this->source_width; // if the image's newly computed height would be inside the bounding box if (round($horizontal_aspect_ratio * $this->source_height) < $height) { // the target image's width is as given as argument to the method $target_width = $width; // compute the target image's height so that the image will stay inside the bounding box $target_height = round($horizontal_aspect_ratio * $this->source_height); // otherwise } else { // the target image's height is as given as argument to the method $target_height = $height; // compute the target image's width so that the image will stay inside the bounding box $target_width = round($vertical_aspect_ratio * $this->source_width); } // if both width and height are given and image is to be cropped in order to get to the required size } elseif ($width > 0 && $height > 0 && $method > 1 && $method < 11) { // compute the horizontal and vertical aspect ratios $vertical_aspect_ratio = $this->source_height / $height; $horizontal_aspect_ratio = $this->source_width / $width; // we'll use one of the two $aspect_ratio = $vertical_aspect_ratio < $horizontal_aspect_ratio ? $vertical_aspect_ratio : $horizontal_aspect_ratio; // compute the target image's width, preserving the aspect ratio $target_width = round($this->source_width / $aspect_ratio); // compute the target image's height, preserving the aspect ratio $target_height = round($this->source_height / $aspect_ratio); // for any other case } else { // we will create a copy of the source image $target_width = $this->source_width; $target_height = $this->source_height; } // if aspect ratio does not need to be preserved } else { // compute the target image's width $target_width = ($width > 0 ? $width : $this->source_width); // compute the target image's height $target_height = ($height > 0 ? $height : $this->source_height); } // if if ( // all images are to be resized - including images that are smaller than the given width/height $this->enlarge_smaller_images || // smaller images than the given width/height are to be left untouched // but current image has at leas one side that is larger than the required width/height ($width > 0 && $height > 0 ? ($this->source_width > $width || $this->source_height > $height) : ($this->source_width > $target_width || $this->source_height > $target_height) ) ) { // if if ( // aspect ratio needs to be preserved AND ($this->preserve_aspect_ratio || isset($auto_preserve_aspect_ratio)) && // both width and height are given ($width > 0 && $height > 0) && // images are to be cropped ($method > 1 && $method < 11) ) { // prepare the target image $target_identifier = $this->_prepare_image($target_width, $target_height, $background_color); imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, 0, 0, $target_width, $target_height, $this->source_width, $this->source_height ); // do the crop according to the required method switch ($method) { // if image needs to be cropped from the top-left corner case ZEBRA_IMAGE_CROP_TOPLEFT: // crop accordingly return $this->crop( 0, 0, $width, $height, $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the top-center case ZEBRA_IMAGE_CROP_TOPCENTER: // crop accordingly return $this->crop( intval(floor(($target_width - $width) / 2)), 0, intval(floor(($target_width - $width) / 2) + $width), $height, $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the top-right corner case ZEBRA_IMAGE_CROP_TOPRIGHT: // crop accordingly return $this->crop( $target_width - $width, 0, $target_width, $height, $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the middle-left case ZEBRA_IMAGE_CROP_MIDDLELEFT: // crop accordingly return $this->crop( 0, intval(floor(($target_height - $height) / 2)), $width, intval(floor(($target_height - $height) / 2) + $height), $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the center of the image case ZEBRA_IMAGE_CROP_CENTER: // crop accordingly return $this->crop( intval(floor(($target_width - $width) / 2)), intval(floor(($target_height - $height) / 2)), intval(floor(($target_width - $width) / 2) + $width), intval(floor(($target_height - $height) / 2) + $height), $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the middle-right case ZEBRA_IMAGE_CROP_MIDDLERIGHT: // crop accordingly return $this->crop( $target_width - $width, intval(floor(($target_height - $height) / 2)), $target_width, intval(floor(($target_height - $height) / 2) + $height), $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the bottom-left corner case ZEBRA_IMAGE_CROP_BOTTOMLEFT: // crop accordingly return $this->crop( 0, $target_height - $height, $width, $target_height, $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the bottom-center case ZEBRA_IMAGE_CROP_BOTTOMCENTER: // crop accordingly return $this->crop( intval(floor(($target_width - $width) / 2)), $target_height - $height, intval(floor(($target_width - $width) / 2) + $width), $target_height, $background_color, $target_identifier // crop this resource instead ); // if image needs to be cropped from the bottom-right corner case ZEBRA_IMAGE_CROP_BOTTOMRIGHT: // crop accordingly return $this->crop( $target_width - $width, $target_height - $height, $target_width, $target_height, $background_color, $target_identifier // crop this resource instead ); } // if aspect ratio doesn't need to be preserved or // it needs to be preserved and method is ZEBRA_IMAGE_BOXED or ZEBRA_IMAGE_NOT_BOXED } else { // prepare the target image $target_identifier = $this->_prepare_image( ($width > 0 && $height > 0 && $method !== ZEBRA_IMAGE_NOT_BOXED ? $width : $target_width), ($width > 0 && $height > 0 && $method !== ZEBRA_IMAGE_NOT_BOXED ? $height : $target_height), $background_color ); imagecopyresampled( $target_identifier, $this->source_identifier, ($width > 0 && $height > 0 && $method !== ZEBRA_IMAGE_NOT_BOXED ? ($width - $target_width) / 2 : 0), ($width > 0 && $height > 0 && $method !== ZEBRA_IMAGE_NOT_BOXED ? ($height - $target_height) / 2 : 0), 0, 0, $target_width, $target_height, $this->source_width, $this->source_height ); // if script gets this far, write the image to disk return $this->_write_image($target_identifier); } // if we get here it means that // smaller images than the given width/height are to be left untouched // therefore, we save the image as it is } else { // prepare the target image $target_identifier = $this->_prepare_image($this->source_width, $this->source_height, $background_color); imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, 0, 0, $this->source_width, $this->source_height, $this->source_width, $this->source_height ); // previously to 2.2.7 I was simply calling the _write_images() method without the code from above this // comment and therefore, when resizing transparent images to a format which doesn't support transparency // and the "enlarge_smaller_images" property being set to FALSE, the "background_color" argument was not // applied and lead to unexpected background colors for the resulting images return $this->_write_image($target_identifier); } } // if script gets this far return false // note that we do not set the error level as it has been already set // by the _create_from_source() method earlier return false; } /** * Rotates the image given as {@link source_path} and outputs the resulted image as {@link target_path}. * * * // include the Zebra_Image library * // (you don't need this if you installed using composer) * require 'path/to/Zebra_Image.php'; * * // instantiate the class * $img = new Zebra_Image(); * * // a source image * // (where "ext" is one of the supported file types extension) * $img->source_path = 'path/to/source.ext'; * * // path to where should the resulting image be saved * // note that by simply setting a different extension to the file will * // instruct the script to create an image of that particular type * $img->target_path = 'path/to/target.ext'; * * // rotate the image 45 degrees, clockwise * $img->rotate(45); * * * @param double $angle Angle by which to rotate the image clockwise. * * Between `0` and `360`. * * @param mixed $background_color (Optional) The hexadecimal color (like `#FFFFFF` or `#FFF`) of the * uncovered zone after the rotation. * * When set to `-1` the script will preserve transparency for transparent `GIF` * and `PNG` images. For non-transparent images the background will be white * (`#FFFFFF`) in this case. * * Default is `-1`. * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error * code. */ public function rotate($angle, $background_color = -1) { // don't do anything if no angle is given if ($angle == 0 || $angle == 360) { return true; } // get function arguments $arguments = func_get_args(); // if a third argument exists $use_existing_source = (isset($arguments[2]) && $arguments[2] === false); // if we came here just to fix orientation or if image resource was successfully created if ($use_existing_source || $this->_create_from_source()) { // there is a bug in GD when angle is 90, 180, 270 // transparency is not preserved if ($angle % 90 === 0) { $angle += 0.001; } // angles are given clockwise but imagerotate works counterclockwise so we need to negate our value $angle = -$angle; // if the uncovered zone after the rotation is to be transparent if ($background_color == -1) { // if target image is a PNG or an WEBP if ($this->target_type === 'png' || $this->target_type === 'webp') { // allocate a transparent color $background_color = imagecolorallocatealpha($this->source_identifier, 0, 0, 0, 127); // if target image is a GIF } elseif ($this->target_type === 'gif') { // if source image was a GIF and a transparent color existed if ($this->source_type == IMAGETYPE_GIF && $this->source_transparent_color_index >= 0) { // use that color $background_color = imagecolorallocate( $this->source_identifier, $this->source_transparent_color['red'], $this->source_transparent_color['green'], $this->source_transparent_color['blue'] ); // if image had no transparent color } else { // allocate a transparent color $background_color = imagecolorallocate($this->source_identifier, 255, 255, 255); // make color transparent // (imagecolorallocate may return FALSE, that's why the elvis operator) imagecolortransparent($this->source_identifier, $background_color ? : null); } // for other image types } else { // use white as the color of uncovered zone after the rotation $background_color = imagecolorallocate($this->source_identifier, 255, 255, 255); } // if a background color is given } else { // convert the color to RGB values $background_color = $this->_hex2rgb($background_color); // allocate the color to the image identifier $background_color = imagecolorallocate( $this->source_identifier, $background_color['r'], $background_color['g'], $background_color['b'] ); } // rotate the image $target_identifier = imagerotate($this->source_identifier, $angle, $background_color ?: 0); // if we called this method from the _create_from_source() method // because we are fixing orientation if ($use_existing_source) { // make any further method work on the rotated image $this->source_identifier = $target_identifier; // update the width and height of the image to the values // of the rotated image $this->source_width = imagesx($target_identifier); $this->source_height = imagesy($target_identifier); return true; // write image otherwise } else { return $this->_write_image($target_identifier); } } // if script gets this far return false // note that we do not set the error level as it has been already set // by the _create_from_source() method earlier return false; } /** * Returns an array containing the image identifier representing the image obtained from {@link $source_path}, the * image's width and height and the image's type. * * @return mixed * * @access private */ private function _create_from_source() { // perform some error checking first // if the GD library is not installed if (!function_exists('gd_info')) { // save the error level and stop the execution of the script $this->error = 7; return false; // if source file does not exist } elseif (!is_file($this->source_path)) { // save the error level and stop the execution of the script $this->error = 1; return false; // if source file is not readable } elseif (!is_readable($this->source_path)) { // save the error level and stop the execution of the script $this->error = 2; return false; // if target file is same as source file and source file is not writable } elseif ($this->target_path == $this->source_path && !is_writable($this->source_path)) { // save the error level and stop the execution of the script $this->error = 3; return false; // try to get source file width, height and type // and if it founds an unsupported file type } elseif ( ($this->source_type = strtolower(substr($this->source_path, strrpos($this->source_path, '.') + 1))) && !(version_compare(PHP_VERSION, '7.0.0') < 0 && $this->source_type === 'webp') && !(version_compare(PHP_VERSION, '7.2.0') < 0 && $this->source_type === 'bmp') && // getimagesize() doesn't support WEBP until 7.1.0 so we will handle that differently ($this->source_path !== 'webp' && !list($this->source_width, $this->source_height, $this->source_type) = @getimagesize($this->source_path)) ) { // save the error level and stop the execution of the script $this->error = 4; return false; // if no errors so far } else { // get target file's type based on the file extension $this->target_type = strtolower(substr($this->target_path, strrpos($this->target_path, '.') + 1)); // if we are working with WEBP images if ($this->source_type === 'webp') { // define this constant which is not available until PHP 7.1.0 if (!defined('IMAGETYPE_WEBP')) { define('IMAGETYPE_WEBP', 18); } // if PHP version is less than 7.1.0 if (version_compare(PHP_VERSION, '7.1.0') < 0) { // flag these so we compute them later on $this->source_width = -1; $this->source_height = -1; } // set value to newly created constant $this->source_type = IMAGETYPE_WEBP; } // create an image from file using extension dependant function // checks for file extension switch ($this->source_type) { // if BMP case IMAGETYPE_BMP: // create an image from file $identifier = imagecreatefrombmp($this->source_path); break; // if GIF case IMAGETYPE_GIF: // create an image from file $identifier = imagecreatefromgif($this->source_path); // get the index of the transparent color (if any) if (($this->source_transparent_color_index = imagecolortransparent($identifier)) >= 0) { // get the transparent color's RGB values // there are GIF images which *are* transparent and everything works as expected, but // imagecolortransparent() returns a color that is outside the range of colors in the image's pallette... // therefore, we check first if the index is in range if ($this->source_transparent_color_index < imagecolorstotal($identifier)) { // if transparent color index is in range, get the transparent color's RGB values $this->source_transparent_color = @imagecolorsforindex($identifier, $this->source_transparent_color_index); // if transparent color index is outside the range of colors in the image's pallette } else { // get RGB values for color at index 0 // (so that we don't have error further in the code) $this->source_transparent_color = @imagecolorsforindex($identifier, 0); } } break; // if PNG case IMAGETYPE_PNG: // create an image from file $identifier = imagecreatefrompng($this->source_path); // disable blending imagealphablending($identifier, false); // save full alpha channel information imagesavealpha($identifier, true); break; // if JPEG case IMAGETYPE_JPEG: // create an image from file $identifier = imagecreatefromjpeg($this->source_path); break; // if WEBP case IMAGETYPE_WEBP: // because animated WEBP images are not supported // we need to check if this is such a file // WEBP file header https://developers.google.com/speed/webp/docs/riff_container // solution from by Sven Liivak https://stackoverflow.com/questions/45190469/how-to-identify-whether-webp-image-is-static-or-animated#answer-52333192 $fh = fopen($this->source_path, 'rb'); // let's see if this is the "Extended" file format fseek($fh, 12); // if this is the extended file format if (fread($fh, 4) === 'VP8X') { // look for the "Animation (A)" bit fseek($fh, 20); // is this is an animated WEBP? $is_animated = ((ord(fread($fh, 1)) >> 1) & 1); } fclose($fh); // if this is an animated WEBP if (isset($is_animated) && $is_animated) { // flag as unsupported file type $this->error = 4; return false; } // create an image from file $identifier = imagecreatefromwebp($this->source_path); // if we are working with WEBP images but PHP version is less than 7.1.0 if ($this->source_width === -1) { // use these to get image's width and height as support for WEBP in getimagesize() was added only // beginning with PHP 7.1.0 $this->source_width = imagesx($identifier); $this->source_height = imagesy($identifier); } // disable blending imagealphablending($identifier, false); // save full alpha channel information imagesavealpha($identifier, true); break; default: // if unsupported file type // note that we call this if the file is not BMP, GIF, JPG, PNG or WEBP even though the getimagesize function // might handle more image types $this->error = 4; return false; } } // if target file has to have the same timestamp as the source image // save it as a global property of the class if ($this->preserve_time) { $this->source_time = filemtime($this->source_path); } // make available the source image's identifier $this->source_identifier = $identifier; // for JPEG files, if we need to handle exif orientation automatically if ($this->auto_handle_exif_orientation && $this->source_type === IMAGETYPE_JPEG) { // if "exif_read_data" function is not available, return false if (!function_exists('exif_read_data')) { // save the error level and stop the execution of the script $this->error = 9; return false; // if "exif_read_data" function is available, EXIF information is available, orientation information is available and orientation needs fixing } elseif (($exif = @exif_read_data($this->source_path)) && isset($exif['Orientation']) && in_array($exif['Orientation'], array(3, 6, 8))) { // fix the orientation switch ($exif['Orientation']) { case 3: // 180 rotate left $this->rotate(180, -1, false); break; case 6: // 90 rotate right $this->rotate(90, -1, false); break; case 8: // 90 rotate left $this->rotate(-90, -1, false); break; } } } return true; } /** * Flips horizontally, vertically or both ways the image given as {@link source_path}. * * @param string $orientation How to flip the image. * * Allowed values are `horizontal`, `vertical` and `both`. * * @since 2.1 * * @return boolean Returns TRUE on success or FALSE on error. * * If FALSE is returned, check the {@link error} property to see the error code. * @access private * */ private function _flip($orientation) { // if image resource was successfully created if ($this->_create_from_source()) { // prepare the target image $target_identifier = $this->_prepare_image($this->source_width, $this->source_height, -1); // flip according to $orientation switch ($orientation) { case 'horizontal': imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, ($this->source_width - 1), 0, $this->source_width, $this->source_height, -$this->source_width, $this->source_height ); break; case 'vertical': imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, 0, ($this->source_height - 1), $this->source_width, $this->source_height, $this->source_width, -$this->source_height ); break; case 'both': imagecopyresampled( $target_identifier, $this->source_identifier, 0, 0, ($this->source_width - 1), ($this->source_height - 1), $this->source_width, $this->source_height, -$this->source_width, -$this->source_height ); break; } // write image return $this->_write_image($target_identifier); } // if script gets this far, return false // note that we do not set the error level as it has been already set // by the _create_from_source() method earlier return false; } /** * Converts a hexadecimal representation of a color (i.e. `#123456` or `#AAA`) to a RGB representation. * * The RGB values will be a value between `0` and `255` each. * * @param string $color Hexadecimal representation of a color (i.e. `#123456` or `#AAA`). * * @param string $default_on_error (Optional) Hexadecimal representation of a color to be used in case `$color` * is not recognized as a hexadecimal color. * * Default is `#FFFFFF` * * @return array Returns an associative array with the values of (R)ed, (G)reen and (B)lue * * @access private */ private function _hex2rgb($color, $default_on_error = '#FFFFFF') { // if color is not formatted correctly // use the default color if (preg_match('/^#?([a-f]|[0-9]){3}(([a-f]|[0-9]){3})?$/i', $color) == 0) { $color = $default_on_error; } // trim off the "#" prefix from $background_color $color = ltrim($color, '#'); // if color is given using the shorthand (i.e. "FFF" instead of "FFFFFF") if (strlen($color) == 3) { $tmp = ''; // take each value // and duplicate it for ($i = 0; $i < 3; $i++) { $tmp .= str_repeat($color[$i], 2); } // the color in it's full, 6 characters length notation $color = $tmp; } // decimal representation of the color $int = hexdec($color); // extract and return the RGB values return array( 'r' => 0xFF & ($int >> 0x10), 'g' => 0xFF & ($int >> 0x8), 'b' => 0xFF & $int ); } /** * Creates a blank image of given width, height and background color. * * @param integer $width Width of the new image. * * @param integer $height Height of the new image. * * @param mixed $background_color (Optional) The hexadecimal color of the background. * * Can also be `-1` case in which the script will try to create a transparent * image, if possible. * * Default is `#FFFFFF`. * * @return resource Returns the identifier of the newly created image. * * @access private */ private function _prepare_image($width, $height, $background_color = '#FFFFFF') { // create a blank image $identifier = imagecreatetruecolor((int)$width <= 0 ? 1 : (int)$width, (int)$height <= 0 ? 1 : (int)$height); // if we are creating a transparent image, and image type supports transparency if ($background_color === -1 && $this->target_type !== 'jpg') { // disable blending imagealphablending($identifier, false); // allocate a transparent color $background_color = imagecolorallocatealpha($identifier, 0, 0, 0, 127); // we also need to set this for saving GIFs imagecolortransparent($identifier, $background_color); // save full alpha channel information imagesavealpha($identifier, true); // if we are not creating a transparent image } else { // convert hex color to rgb $background_color = $this->_hex2rgb($background_color); // prepare the background color $background_color = imagecolorallocate($identifier, $background_color['r'], $background_color['g'], $background_color['b']); } // fill the image with the background color imagefill($identifier, 0, 0, $background_color); // return the image's identifier return $identifier; } /** * Sharpens images. Useful when creating thumbnails. * * Code taken from the comments at {@link https://www.php.net/manual/en/function.imageconvolution.php}. * * > This function will yield a result only for PHP version 5.1.0+ and will leave the image unaltered for older * versions! * * @param resource $image An image identifier * * @return resource Returns the sharpened image * * @access private */ private function _sharpen_image($image) { // if the "sharpen_images" is set to true and we're running an appropriate version of PHP // (the "imageconvolution" is available only for PHP 5.1.0+) if ($this->sharpen_images && version_compare(PHP_VERSION, '5.1.0') >= 0) { // the convolution matrix as an array of three arrays of three floats $matrix = array( array(-1.2, -1, -1.2), array(-1, 20, -1), array(-1.2, -1, -1.2), ); // the divisor of the matrix $divisor = array_sum(array_map('array_sum', $matrix)); // color offset $offset = 0; // sharpen image imageconvolution($image, $matrix, $divisor, $offset); } // return the image's identifier return $image; } /** * Creates a new image from given image identifier having the extension as specified by {@link target_path}. * * @param resource $identifier An image identifier * * @return boolean Returns `true` on success or `false` on error. * * If `false` is returned, check the {@link error} property to see the error code. * * @access private */ private function _write_image($identifier) { // sharpen image if it's required $this->_sharpen_image($identifier); // save interlaced JPEG images if required if (in_array($this->target_type, array('jpg', 'jpeg')) && $this->jpeg_interlace) { imageinterlace($identifier, 1); } // image saving process goes according to required extension switch ($this->target_type) { // if BMP case 'bmp': // if GD support for this file type is not available if (!function_exists('imagebmp')) { // save the error level and stop the execution of the script $this->error = 6; return false; // if, for some reason, file could not be created } elseif (@!imagebmp($identifier, $this->target_path, $this->bmp_compressed)) { // save the error level and stop the execution of the script $this->error = 3; return false; } break; // if GIF case 'gif': // if GD support for this file type is not available // in version 1.6 of GD the support for GIF files was dropped see // https://www.php.net/manual/en/function.imagegif.php#function.imagegif.notes if (!function_exists('imagegif')) { // save the error level and stop the execution of the script $this->error = 6; return false; // if, for some reason, file could not be created } elseif (@!imagegif($identifier, $this->target_path)) { // save the error level and stop the execution of the script $this->error = 3; return false; } break; // if JPEG case 'jpg': case 'jpeg': // if GD support for this file type is not available if (!function_exists('imagejpeg')) { // save the error level and stop the execution of the script $this->error = 6; return false; // if, for some reason, file could not be created } elseif (@!imagejpeg($identifier, $this->target_path, $this->jpeg_quality)) { // save the error level and stop the execution of the script $this->error = 3; return false; } break; // if PNG case 'png': // if GD support for this file type is not available if (!function_exists('imagepng')) { // save the error level and stop the execution of the script $this->error = 6; return false; // if, for some reason, file could not be created } elseif (@!imagepng($identifier, $this->target_path, $this->png_compression)) { // save the error level and stop the execution of the script $this->error = 3; return false; } break; // if WEBP case 'webp': // if GD support for this file type is not available if (!function_exists('imagewebp')) { // save the error level and stop the execution of the script $this->error = 6; return false; // if, for some reason, file could not be created } elseif (@!imagewebp($identifier, $this->target_path, $this->webp_quality)) { // save the error level and stop the execution of the script $this->error = 3; return false; } break; // if not a supported file extension default: // save the error level and stop the execution of the script $this->error = 5; return false; } // get a list of functions disabled via configuration $disabled_functions = @ini_get('disable_functions'); // if the 'chmod' function is not disabled via configuration if ($disabled_functions === '' || strpos('chmod', $disabled_functions) === false) { // chmod the file chmod($this->target_path, intval($this->chmod_value, 8)); // save the error level } else { $this->error = 8; } // if target file has to have the same timestamp as the source image if ($this->preserve_time) { // touch the newly created file @touch($this->target_path, $this->source_time); } // free memory imagedestroy($this->source_identifier); imagedestroy($identifier); // for PHP 8.0.0+ imagedestroy is no-op (https://php.watch/versions/8.0/gdimage) // and we have to use unset() if (version_compare(PHP_VERSION, '8.0.0', '>=')) { unset($this->source_identifier); unset($identifier); } // return true return true; } }