<?php
/**
 * Image resize and compress class
 * Simple PHP class for resizing and compressing images in PNG, JPEG, GIF and WEBP format.
 * @uses PHP GD extension.
 * @see ImageUpload()
 *
 * Can use PNGQuant:
 * @see $PNGQuantPath optional configuration variable.
 *
 * @since 3.3
 * @since 12.2 Add WebP image format support
 *
 * @package RosarioSIS
 * @subpackage classes
 */

// WebP constants may not be defined, even in cases where the format is supported.
if ( ! defined( 'IMAGETYPE_WEBP' ) ) {
	define( 'IMAGETYPE_WEBP', 18 );
}
if ( ! defined( 'IMG_WEBP' ) ) {
	define( 'IMG_WEBP', IMAGETYPE_WEBP );
}

/*!
 * hi@j0hn.dk
 * No copyrights. Feel free to use this the way you like.
 *
 * @link https://github.com/michaube/image-resize-gd
 */
class ImageResizeGD {

	/**
	 * @var resource
	 */
	private $image;
	/**
	 * @var resource
	 */
	private $imageModified;

	/**
	 * @var int
	 */
	private $sourceWidth;
	/**
	 * @var int
	 */
	private $sourceHeight;

	/**
	 * @var int
	 */
	private $sourceType;

	/**
	 * @var int
	 */
	private $newWidth;
	/**
	 * @var int
	 */
	private $newHeight;

	/**
	 * @var int
	 */
	private $defaultJPEGCompression;

	/**
	 * @var int
	 */
	private $defaultPNGCompression;

	/**
	 * @var int
	 */
	private $defaultWEBPCompression;

	/**
	 * @var string
	 */
	private $PNGQuantPath;

	/**
	 * @var array
	 */
	private $allowedImageTypes = array(IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG, IMAGETYPE_WEBP);


	public function __destruct() {
		if ( is_resource( $this->image ) ) {
			// We don't need the original in memory anymore.
			if ( PHP_VERSION_ID >= 80000 ) {
				// FJ Fix PHP8.5 deprecated imagedestroy() function
				$this->image = null;
			} else {
				imagedestroy( $this->image );
			}
		}
	}


	/**
	 * Checks to see if current environment supports GD.
	 *
	 * @author Wordpress
	 * @see class-wp-image-editor-gd.php file.
	 *
	 * @static
	 * @access public
	 *
	 * @return bool
	 */
	public static function test() {
		if ( ! extension_loaded('gd') || ! function_exists('gd_info') )
			return false;

		return true;
	}

	/**
	 * Image resizing and compressing utility.
	 *
	 * @author hi@j0hn.dk
	 * @author François Jacquet
	 *
	 * @param string $imagePathOrString Path to the source image or base64 string image.
	 * @param int    $defaultJPEGCompression integer <0; 100>
	 * @param int    $defaultPNGCompression integer <0; 9>
	 * @param string $PNGQuantPath PNGQuant path, for PNG compression.
	 *
	 */
	function __construct($imagePathOrString, $defaultJPEGCompression = 80, $defaultPNGCompression = 9, $PNGQuantPath = '') {

		if ( strpos( $imagePathOrString, 'data:image' ) === 0 ) {

			$this->image = $this->openBase64Image( $imagePathOrString );
		} else {

			$this->image = $this->openImageFile( $imagePathOrString );
		}

		$this->setDefaultJPEGCompression($defaultJPEGCompression);
		$this->setDefaultPNGCompression($defaultPNGCompression);
		$this->setDefaultWEBPCompression(-1);
		$this->setPNGQuantPath($PNGQuantPath);

		$this->sourceWidth = imagesx($this->image);
		$this->sourceHeight = imagesy($this->image);

		if($this->sourceWidth === false || $this->sourceHeight === false) {
			throw new \InvalidArgumentException('Image type is not supported or file is corrupted.');
		}
	}

	/**
	 * Resize image, so the output does not exceed given width and height.
	 * Smaller image will be upscaled.
	 * Method maintains the aspect ratio.
	 *
	 * @param int $newWidth Desired width of the output image
	 * @param int $newHeight Desired height of the output image
	 * @return void
	 */
	public function resizeWithinDimensions($newWidth, $newHeight) {
		if ($newWidth === $this->sourceWidth && $newHeight === $this->sourceHeight) {
			$this->copyImageDataWithoutResampling();
			$this->newWidth = $this->sourceWidth;
			$this->newHeight = $this->sourceHeight;
			return;
		}
		$widthRatio  = $this->sourceWidth / $newWidth;
		$heightRatio = $this->sourceHeight / $newHeight;

		if ($widthRatio > $heightRatio) {
			$this->resizeByWidth($newWidth);
		} else {
			$this->resizeByHeight($newHeight);
		}
	}

	/**
	 * Resize image, so the output gets exactly given width.
	 * Height will be scaled maintaining the aspect ratio.
	 *
	 * @param int $newWidth Desired width of the output image
	 * @return void
	 */
	public function resizeByWidth($newWidth) {
		if ($newWidth === $this->sourceWidth) {
			$this->copyImageDataWithoutResampling();
			$this->newWidth = $this->sourceWidth;
			$this->newHeight = $this->sourceHeight;
			return;
		}
		$this->newWidth = $newWidth;

		$ratio = $this->sourceHeight / $this->sourceWidth;
		$this->newHeight = $ratio * $newWidth;

		$this->copyImageData();
	}

	/**
	 * Resize image, so the output gets exactly given height.
	 * Width will be scaled maintaining the aspect ratio.
	 *
	 * @param int $newHeight Desired height of the output image
	 * @return void
	 */
	public function resizeByHeight($newHeight) {
		if ($newHeight === $this->sourceHeight) {
			$this->copyImageDataWithoutResampling();
			$this->newWidth = $this->sourceWidth;
			$this->newHeight = $this->sourceHeight;
			return;
		}
		$this->newHeight = $newHeight;

		$ratio = $this->sourceWidth / $this->sourceHeight;
		$this->newWidth = $ratio * $newHeight;

		$this->copyImageData();
	}

	/**
	 * Resize image, so the output matches exactly given dimensions.
	 * Image is scaled, centered and cropped.
	 *
	 * @param int $newWidth Desired width of the output image
	 * @param int $newHeight Desired height of the output image
	 * @return void
	 */
	public function resizeToFillDimensionsExactly($newWidth, $newHeight) {
		if ($newWidth === $this->sourceWidth && $newHeight === $this->sourceHeight) {
			$this->copyImageDataWithoutResampling();
			$this->newWidth = $this->sourceWidth;
			$this->newHeight = $this->sourceHeight;
			return;
		}
		$widthRatio  = $this->sourceWidth / $newWidth;
		$heightRatio = $this->sourceHeight / $newHeight;

		if ($heightRatio < $widthRatio) {
			$optimalRatio = $heightRatio;
		} else {
			$optimalRatio = $widthRatio;
		}

		$this->newWidth  = $this->sourceWidth / $optimalRatio;
		$this->newHeight = $this->sourceHeight / $optimalRatio;

		$this->copyImageData();

		$cropStartX = ($this->newWidth / 2) - ($newWidth / 2);
		$cropStartY = ($this->newHeight / 2) - ($newHeight / 2);

		$imageCropped = $this->imageModified;

		$this->imageModified = imagecreatetruecolor((int) $newWidth , (int) $newHeight);

		imagecopy($this->imageModified, $imageCropped , 0, 0, $cropStartX, $cropStartY, (int) $newWidth, (int) $newHeight);

		$this->newWidth = $newWidth;
		$this->newHeight = $newHeight;
	}

	/**
	 * Saves image after modifications, or a copy if no modifications were done.
	 * Allows adding a solid color background to transparent images.
	 * By default output will be saved as the same type as source.
	 * By default, the name of the output file is null:
	 * the image will be output to browser.
	 *
	 * @param string $saveImageName Name of the output file. Do not use extension here, it will be added based on $extension parameter.
	 * @param int $quality Should be value <0; 100> for IMAGETYPE_JPEG, <0; 9> for IMAGETYPE_PNG
	 * @param int $extension Desired output format. Should be one of php predefined constants: IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG
	 * @param string|null $backgroundColor Should be a hexadecimal RGB color value without hash, like FF0000 or 090909
	 * @return string Name of saved output file with extension. For example foo.jpg
	 * @throws Exception When installed gd library does not support desired $extension
	 * @throws Exception When image could not be saved
	 * @throws InvalidArgumentException When $extension is not a value of any of those constants: IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG
	 *
	 */
	public function saveImageFile($saveImageName = null, $extension = null, $quality = null, $backgroundColor = null) {
		if($this->imageModified === null) {
			$this->copyImageDataWithoutResampling();
		}

		if($extension !== null) {
			if(!in_array($extension, $this->allowedImageTypes)) {
				throw new \InvalidArgumentException('This image type is not supported.');
			}
		} else {
			$extension = $this->sourceType;
		}

		if($backgroundColor !== null) {
			$this->addBackgroundColor($backgroundColor);
		}

		if($quality < 0) {
			$quality = 0;
		}

		if($quality > 100 && $extension === IMAGETYPE_JPEG) {
			$quality = 100;
		}

		if($quality > 9 && $extension === IMAGETYPE_PNG) {
			$quality = 9;
		}

		if($quality > 100 && $extension === IMAGETYPE_WEBP) {
			$quality = 100;
		}

		switch($extension) {
			case IMAGETYPE_GIF:
				if (imagetypes() & IMG_GIF) {

					$imageFile = is_null( $saveImageName ) ? null :
						$saveImageName . ( substr( $saveImageName, -4 ) === '.gif' ? '' : '.gif' );

					$imageResult = imagegif($this->imageModified, $imageFile);
				} else {
					throw new \Exception('Your GD library does not support gif image types.');
				}
				break;
			case IMAGETYPE_JPEG:
				if (imagetypes() & IMG_JPG) {
					if($quality === null) {
						$quality = $this->getDefaultJPEGCompression();
					}

					$imageFile = is_null( $saveImageName ) ? null :
						$saveImageName . ( substr( $saveImageName, -4 ) === '.jpg' ? '' :
							( substr( $saveImageName, -5 ) === '.jpeg' ? '' : '.jpg' ) );

					$imageResult = imagejpeg($this->imageModified, $imageFile, $quality);
				} else {
					throw new \Exception('Your GD library does not support jpg image types.');
				}
				break;
			case IMAGETYPE_PNG:
				if (imagetypes() & IMG_PNG) {
					if($quality === null) {
						$quality = $this->getDefaultPNGCompression();
					}

					$imageFile = is_null( $saveImageName ) ? null :
						$saveImageName . ( substr( $saveImageName, -4 ) === '.png' ? '' : '.png' );

					$imageResult = imagepng($this->imageModified, $imageFile, $quality);

					$this->compressPNGQuant( $imageFile );
				} else {
					throw new \Exception('Your GD library does not support png image types.');
				}
				break;
			case IMAGETYPE_WEBP:
				if (function_exists('imagewebp')) {
					if($quality === null) {
						$quality = $this->getDefaultWEBPCompression();
					}

					$imageFile = is_null( $saveImageName ) ? null :
						$saveImageName . ( substr( $saveImageName, -5 ) === '.webp' ? '' : '.webp' );

					$imageResult = imagewebp($this->imageModified, $imageFile, $quality);
				} else {
					throw new \Exception('Your GD library does not support webp image types.');
				}
				break;
			default:
				break;
		}

		if($imageResult === false) {
			throw new \Exception('Image could not be saved.');
		}

		if ( PHP_VERSION_ID < 80000 ) {
			// FJ Fix PHP8.5 deprecated imagedestroy() function
			imagedestroy($this->imageModified);
		}

		$this->imageModified = null;

		return $imageFile;
	}

	/**
	 * @return int
	 */
	public function getDefaultJPEGCompression()
	{
		return $this->defaultJPEGCompression;
	}

	/**
	 * @param int $defaultJPEGCompression
	 */
	public function setDefaultJPEGCompression($defaultJPEGCompression)
	{
		$this->defaultJPEGCompression = $defaultJPEGCompression;
	}

	/**
	 * @return int
	 */
	public function getDefaultPNGCompression()
	{
		return $this->defaultPNGCompression;
	}

	/**
	 * @param int $defaultPNGCompression
	 */
	public function setDefaultPNGCompression($defaultPNGCompression)
	{
		$this->defaultPNGCompression = $defaultPNGCompression;
	}

	/**
	 * @return string
	 */
	public function getPNGQuantPath()
	{
		return $this->PNGQuantPath;
	}

	/**
	 * @param string $PNGQuantPath
	 */
	public function setPNGQuantPath($PNGQuantPath)
	{
		$this->PNGQuantPath = (string) $PNGQuantPath;
	}

	/**
	 * @return int
	 */
	public function getDefaultWEBPCompression()
	{
		return $this->defaultWEBPCompression;
	}

	/**
	 * @param int $defaultJPEGCompression
	 */
	public function setDefaultWEBPCompression($defaultWEBPCompression)
	{
		$this->defaultWEBPCompression = $defaultWEBPCompression;
	}

	/**
	 * @return string
	 */
	public function getSourceType()
	{
		return $this->sourceType;
	}

	/**
	 * @return integer
	 */
	public function getSourceWidth()
	{
		return $this->sourceWidth;
	}

	/**
	 * @return integer
	 */
	public function getSourceHeight()
	{
		return $this->sourceHeight;
	}

	/**
	 * Adds solid background to image
	 *
	 * @param $backgroundColor
	 * @return void
	 */
	protected function addBackgroundColor($backgroundColor) {
		if(strlen($backgroundColor) !== 6) {
			throw new \InvalidArgumentException('Argument has to be a hexadecimal RGB color value, without hash. For example FFFFFF.');
		}

		$decRed = hexdec(substr($backgroundColor, 0, 2));
		$decGreen = hexdec(substr($backgroundColor, 2, 2));
		$decBlue = hexdec(substr($backgroundColor, 4, 2));

		$imageSolidBackground = imagecreatetruecolor((int) $this->newWidth, (int) $this->newHeight);
		$solidColor = imagecolorallocate($imageSolidBackground, $decRed, $decGreen, $decBlue);
		imagefilledrectangle($imageSolidBackground, 0, 0, (int) $this->newWidth, (int) $this->newHeight, $solidColor);
		imagecopy($imageSolidBackground, $this->imageModified, 0, 0, 0, 0, (int) $this->newWidth, (int) $this->newHeight);

		$this->imageModified = $imageSolidBackground;
	}

	/**
	 * @return void
	 */
	protected function copyImageDataWithoutResampling() {
		$this->imageModified = $this->image;

		$this->newWidth = $this->sourceWidth;
		$this->newHeight = $this->sourceHeight;
	}

	/**
	 * @return void
	 */
	protected function copyImageData() {
		$this->imageModified = imagecreatetruecolor((int) $this->newWidth, (int) $this->newHeight);

		imagealphablending($this->imageModified, false);
		imagesavealpha($this->imageModified, true);

		imagecopyresampled($this->imageModified, $this->image, 0, 0, 0, 0, (int) $this->newWidth, (int) $this->newHeight, $this->sourceWidth, $this->sourceHeight);
	}

	/**
	 * @param string $imagePath
	 * @return resource
	 * @throws InvalidArgumentException Image type is not supported or file is corrupted
	 * @throws InvalidArgumentException Image type is not any of those IMAGETYPE_JPEG, IMAGETYPE_GIF, IMAGETYPE_PNG
	 * @throws Exception Image could not be opened
	 */
	protected function openImageFile($imagePath) {

		if ( ! function_exists( 'exif_imagetype' ) ) {
			/**
			 * Exif imagetype function
			 * Fix #171 Adding photos to users and students
			 * Provides function if PHP exif extension not installed.
			 *
			 * @since 3.5.2
			 *
			 * @link http://php.net/manual/en/function.exif-imagetype.php#80383
			 *
			 * @param  string $filename File name.
			 * @return mixed            Image type integer or false.
			 */
			function exif_imagetype ( $filename ) {
				if ( ( list($width, $height, $type, $attr) = getimagesize( $filename ) ) !== false ) {
					return $type;
				}

				return false;
			}
		}

		$imageType = @exif_imagetype($imagePath);

		if ($imageType === false) {
			if (version_compare( PHP_VERSION, '7.1', '<' )
				&& strstr(file_get_contents($imagePath), 'WEBPVP8') !== false) {
				/**
				 * Fix WebP image type detection on PHP < 7.1
				 *
				 * @link https://github.com/gumlet/php-image-resize/pull/144
				 */
				$imageType = IMAGETYPE_WEBP;
			} else {
				throw new \InvalidArgumentException('Image type is not supported or file is corrupted.');
			}
		}

		if (!$imageType || !in_array($imageType, $this->allowedImageTypes)) {
			// throw new \InvalidArgumentException('Image type is not supported.');
			// Default image type to PNG.
			$this->sourceType = IMAGETYPE_PNG;
		} else {
			$this->sourceType = $imageType;
		}

		if (function_exists('imagecreatefromwebp')
			&& $imageType === IMAGETYPE_WEBP) {
			// WebP may not work with imagecreatefromstring().
			// Note: animated WebP returns false.
			$image = @imagecreatefromwebp($imagePath);
		} else {
			$image = @imagecreatefromstring(file_get_contents($imagePath));
		}

		if($image === false) {
			throw new \Exception('Image could not be opened.');
		}

		$image = $this->fixTransparentBackground( $image );

		$image = $this->fixRotation( $image, $imagePath );

		return $image;
	}


	/**
	 * Open & check base64 encoded images.
	 *
	 * @author François Jacquet
	 *
	 * @param  string $imageData Base64 encoded image, src tag.
	 *
	 * @return resource
	 */
	protected function openBase64Image( $imageData )
	{
		$imageMimeType = '';

		if ( strpos( $imageData, 'data:image' ) === 0 ) {
			$imageMimeType = substr(
				$imageData,
				strpos( $imageData, 'data:' ) + 5,
				strpos( $imageData, ';base64' ) - 5
			);

			$imageData = substr( $imageData, ( strpos( $imageData, 'base64' ) + 7 ) );
		}

		$decodedData = base64_decode( $imageData );

		$allowedImageMimeTypes = array_map( 'image_type_to_mime_type', $this->allowedImageTypes );

		if (version_compare( PHP_VERSION, '7.1', '<' )) {
			// Fix WebP image type detection on PHP < 7.1
			$allowedImageMimeTypes[3] = 'image/webp';
		}

		if ( ! in_array( $imageMimeType, $allowedImageMimeTypes ) ) {
			if ( function_exists( 'getimagesizefromstring' ) )
			{
				$size = getimagesizefromstring( $decodedData );

				if ( ! $size
					|| $size[0] == 0
					|| $size[1] == 0
					|| ! $size['mime'] )
				{
					if (version_compare( PHP_VERSION, '7.1', '<' )
						&& strstr($decodedData, 'WEBPVP8') !== false) {
						/**
						 * Fix WebP image type detection on PHP < 7.1
						 *
						 * @link https://github.com/gumlet/php-image-resize/pull/144
						 */
						$imageMimeType = 'image/webp';
					} else {
						throw new \InvalidArgumentException('Image type is not supported or file is corrupted.');
					}
				}

				$imageMimeType = $size['mime'];
			}

			if (!in_array($imageMimeType, $allowedImageMimeTypes)) {
				// throw new \InvalidArgumentException('Image type is not supported.');
			}
		}

		foreach ( (array) $this->allowedImageTypes as $allowedImageType ) {
			if ( image_type_to_mime_type( $allowedImageType ) === $imageMimeType
				|| ( $allowedImageType === IMAGETYPE_WEBP && $imageMimeType === 'image/webp' ) ) {
				$this->sourceType = $allowedImageType;
				break;
			}
		}

		if ( ! $this->getSourceType() ) {
			// Default image type to PNG.
			$this->sourceType = IMAGETYPE_PNG;
		}

		if (function_exists('imagecreatefromwebp')
			&& $imageMimeType === 'image/webp') {
			// WebP may not work with imagecreatefromstring().
			// Note: animated WebP returns false.
			$imagePath = tempnam(sys_get_temp_dir(), 'ImageResizeGDWebP');

			file_put_contents($imagePath, $decodedData);

			$image = @imagecreatefromwebp($imagePath);

			@unlink($imagePath);
		} else {
			$image = @imagecreatefromstring( $decodedData );
		}

		if ($image === false) {
			throw new \Exception('Image type is not supported or file is corrupted.');
		}

		$image = $this->fixTransparentBackground( $image );

		return $image;
	}


	/**
	 * Fix GD bug with transparent background PNG
	 * ending up having a black background
	 * when opened with imagecreatefromstring and then saved.
	 *
	 * @link http://stackoverflow.com/questions/2611852/imagecreatefrompng-makes-a-black-background-instead-of-transparent?rq=1
	 *
	 * @param  resource $image GD image resource.
	 * @return resource        GD image resource.
	 */
	protected function fixTransparentBackground( $image )
	{
		switch ( $this->sourceType ) {
			case IMAGETYPE_GIF:
			case IMAGETYPE_PNG:
				// Integer representation of the color black (rgb: 0,0,0).
				$background = imagecolorallocate($image , 0, 0, 0);
				// Removing the black from the placeholder.
				imagecolortransparent($image, $background);

			case IMAGETYPE_PNG:
				/**
				 * Turning off alpha blending (to ensure alpha channel information
				 * is preserved, rather than removed (blending with the rest of the
				 * image in the form of black))
				 */
				imagealphablending($image, false);

				/**
				 * turning on alpha channel information saving (to ensure the full range
				 * of transparency is preserved)
				 */
				imagesavealpha($image, true);
			break;
		}

		return $image;
	}


	/**
	 * Fix JPG image rotation
	 * Some cameras store the image orientation in the EXIF data.
	 * Prevent a portrait photo to be rotated to landscape when saved
	 *
	 * @since 10.6
	 *
	 * @link https://www.php.net/manual/en/function.exif-read-data.php#110894
	 * @link https://stackoverflow.com/questions/12774411/php-resizing-image-on-upload-rotates-the-image-when-i-dont-want-it-to
	 *
	 * @param  resource $image     GD image resource.
	 * @param  string   $imagePath Path to image file.
	 * @return resource            GD image resource.
	 */
	protected function fixRotation( $image, $imagePath )
	{
		if ( $this->getSourceType() !== IMAGETYPE_JPEG )
		{
			// Only fix rotation for JPG.
			return $image;
		}

		$orientation = 0;

		if ( function_exists( 'exif_read_data' ) )
		{
			// Suppress warning Incorrect APP1 Exif Identifier Code.
			$exif = @exif_read_data( $imagePath );

			if ( isset( $exif['Orientation'] ) )
			{
				$orientation = $exif['Orientation'];
			}
		}
		elseif ( preg_match(
			'@\x12\x01\x03\x00\x01\x00\x00\x00(.)\x00\x00\x00@',
			file_get_contents( $imagePath ),
			$matches ) )
		{
			$orientation = ord( $matches[1] );
		}

		switch ( $orientation )
		{
			case 8:
				$image = imagerotate( $image, 90, 0 );
				break;
			case 3:
				$image = imagerotate( $image, 180, 0 );
				break;
			case 6:
				$image = imagerotate( $image, -90, 0 );
				break;
		}

		return $image;
	}


	/**
	 * Compress a PNG image using PNGQuant
	 * Overwrites the original file.
	 *
	 * @author François Jacquet
	 *
	 * @link https://pngquant.org/
	 *
	 * @uses shell_exec
	 *
	 * @param  string $imageFile Image file path.
	 */
	public function compressPNGQuant( $imageFile )
	{
		if ( ! $this->getPNGQuantPath() )
		{
			return;
		}

		if ( ! file_exists( $imageFile ) )
		{
			throw new InvalidArgumentException( 'File does not exist: ' . $imageFile );
		}

		if ( in_array( 'shell_exec', explode( ',', ini_get( 'disable_functions' ) ) ) )
		{
			return;
		}

		shell_exec( escapeshellarg( $this->getPNGQuantPath() ) .
			' --quality=65-95 --force --ext .png --speed 5 ' . escapeshellarg( $imageFile ) );
	}
}
