192a8473aSAndreas Gohr<?php /** @noinspection PhpComposerExtensionStubsInspection */ 292a8473aSAndreas Gohr 392a8473aSAndreas Gohr 492a8473aSAndreas Gohrnamespace splitbrain\slika; 592a8473aSAndreas Gohr 65c25a071SAndreas Gohr/** 75c25a071SAndreas Gohr * Image processing adapter for PHP's libGD 85c25a071SAndreas Gohr */ 992a8473aSAndreas Gohrclass GdAdapter extends Adapter 1092a8473aSAndreas Gohr{ 1192a8473aSAndreas Gohr /** @var resource libGD image */ 1292a8473aSAndreas Gohr protected $image; 1392a8473aSAndreas Gohr /** @var int width of the current image */ 1492a8473aSAndreas Gohr protected $width = 0; 1592a8473aSAndreas Gohr /** @var int height of the current image */ 1692a8473aSAndreas Gohr protected $height = 0; 1792a8473aSAndreas Gohr /** @var string the extension of the file we're working with */ 1892a8473aSAndreas Gohr protected $extension; 1992a8473aSAndreas Gohr 2092a8473aSAndreas Gohr 2192a8473aSAndreas Gohr /** @inheritDoc */ 2292a8473aSAndreas Gohr public function __construct($imagepath, $options = []) 2392a8473aSAndreas Gohr { 2492a8473aSAndreas Gohr parent::__construct($imagepath, $options); 2592a8473aSAndreas Gohr $this->image = $this->loadImage($imagepath); 2692a8473aSAndreas Gohr } 2792a8473aSAndreas Gohr 2892a8473aSAndreas Gohr /** 2992a8473aSAndreas Gohr * Clean up 3092a8473aSAndreas Gohr */ 3192a8473aSAndreas Gohr public function __destruct() 3292a8473aSAndreas Gohr { 3362c98cd6SAndreas Gohr // destroy the GD image resource (only needed on PHP < 8.0) 3492a8473aSAndreas Gohr if (is_resource($this->image)) { 3592a8473aSAndreas Gohr imagedestroy($this->image); 3692a8473aSAndreas Gohr } 3792a8473aSAndreas Gohr } 3892a8473aSAndreas Gohr 3992a8473aSAndreas Gohr /** @inheritDoc 4092a8473aSAndreas Gohr * @throws Exception 4192a8473aSAndreas Gohr */ 4292a8473aSAndreas Gohr public function autorotate() 4392a8473aSAndreas Gohr { 4492a8473aSAndreas Gohr if ($this->extension !== 'jpeg') { 4592a8473aSAndreas Gohr return $this; 4692a8473aSAndreas Gohr } 47*dd9e8e5eSAndreas Gohr return $this->rotate(ImageInfo::readExifOrientation($this->imagepath)); 4892a8473aSAndreas Gohr } 4992a8473aSAndreas Gohr 5092a8473aSAndreas Gohr /** 5192a8473aSAndreas Gohr * @inheritDoc 5292a8473aSAndreas Gohr * @throws Exception 5392a8473aSAndreas Gohr */ 5492a8473aSAndreas Gohr public function rotate($orientation) 5592a8473aSAndreas Gohr { 5692a8473aSAndreas Gohr $orientation = (int)$orientation; 5792a8473aSAndreas Gohr if ($orientation < 0 || $orientation > 8) { 5892a8473aSAndreas Gohr throw new Exception('Unknown rotation given'); 5992a8473aSAndreas Gohr } 6092a8473aSAndreas Gohr 6192a8473aSAndreas Gohr if ($orientation <= 1) { 6292a8473aSAndreas Gohr // no rotation wanted 6392a8473aSAndreas Gohr return $this; 6492a8473aSAndreas Gohr } 6592a8473aSAndreas Gohr 6692a8473aSAndreas Gohr // fill color 6792a8473aSAndreas Gohr $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127); 6892a8473aSAndreas Gohr 69*dd9e8e5eSAndreas Gohr // rotate (orientation 2 is a flip-only case and keeps $this->image) 70*dd9e8e5eSAndreas Gohr $image = $this->image; 7192a8473aSAndreas Gohr if (in_array($orientation, [3, 4])) { 72c13ef3baSAndreas Gohr $image = imagerotate($this->image, 180, $transparency); 73*dd9e8e5eSAndreas Gohr } elseif (in_array($orientation, [5, 6])) { 74c13ef3baSAndreas Gohr $image = imagerotate($this->image, -90, $transparency); 7592a8473aSAndreas Gohr list($this->width, $this->height) = [$this->height, $this->width]; 7692a8473aSAndreas Gohr } elseif (in_array($orientation, [7, 8])) { 77c13ef3baSAndreas Gohr $image = imagerotate($this->image, 90, $transparency); 7892a8473aSAndreas Gohr list($this->width, $this->height) = [$this->height, $this->width]; 7992a8473aSAndreas Gohr } 8092a8473aSAndreas Gohr 8192a8473aSAndreas Gohr // additionally flip 8292a8473aSAndreas Gohr if (in_array($orientation, [2, 5, 7, 4])) { 8392a8473aSAndreas Gohr imageflip($image, IMG_FLIP_HORIZONTAL); 8492a8473aSAndreas Gohr } 8592a8473aSAndreas Gohr 86*dd9e8e5eSAndreas Gohr if ($image !== $this->image) { 8762c98cd6SAndreas Gohr $this->__destruct(); // destroy old image 8892a8473aSAndreas Gohr $this->image = $image; 89*dd9e8e5eSAndreas Gohr } 9092a8473aSAndreas Gohr 9192a8473aSAndreas Gohr //keep png alpha channel if possible 9292a8473aSAndreas Gohr if ($this->extension == 'png' && function_exists('imagesavealpha')) { 9392a8473aSAndreas Gohr imagealphablending($this->image, false); 9492a8473aSAndreas Gohr imagesavealpha($this->image, true); 9592a8473aSAndreas Gohr } 9692a8473aSAndreas Gohr 9792a8473aSAndreas Gohr return $this; 9892a8473aSAndreas Gohr } 9992a8473aSAndreas Gohr 10092a8473aSAndreas Gohr /** 10192a8473aSAndreas Gohr * @inheritDoc 10292a8473aSAndreas Gohr * @throws Exception 10392a8473aSAndreas Gohr */ 10492a8473aSAndreas Gohr public function resize($width, $height) 10592a8473aSAndreas Gohr { 106*dd9e8e5eSAndreas Gohr list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height); 10792a8473aSAndreas Gohr $this->resizeOperation($width, $height); 10892a8473aSAndreas Gohr return $this; 10992a8473aSAndreas Gohr } 11092a8473aSAndreas Gohr 11192a8473aSAndreas Gohr /** 11292a8473aSAndreas Gohr * @inheritDoc 11392a8473aSAndreas Gohr * @throws Exception 11492a8473aSAndreas Gohr */ 11592a8473aSAndreas Gohr public function crop($width, $height) 11692a8473aSAndreas Gohr { 11792a8473aSAndreas Gohr list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height); 11892a8473aSAndreas Gohr $this->resizeOperation($width, $height, $offsetX, $offsetY); 11992a8473aSAndreas Gohr return $this; 12092a8473aSAndreas Gohr } 12192a8473aSAndreas Gohr 12292a8473aSAndreas Gohr /** 12392a8473aSAndreas Gohr * @inheritDoc 12492a8473aSAndreas Gohr * @throws Exception 12592a8473aSAndreas Gohr */ 12692a8473aSAndreas Gohr public function save($path, $extension = '') 12792a8473aSAndreas Gohr { 12892a8473aSAndreas Gohr if ($extension === 'jpg') { 12992a8473aSAndreas Gohr $extension = 'jpeg'; 13092a8473aSAndreas Gohr } 13192a8473aSAndreas Gohr if ($extension === '') { 13292a8473aSAndreas Gohr $extension = $this->extension; 13392a8473aSAndreas Gohr } 13492a8473aSAndreas Gohr $saver = 'image' . $extension; 13592a8473aSAndreas Gohr if (!function_exists($saver)) { 13692a8473aSAndreas Gohr throw new Exception('Can not save image format ' . $extension); 13792a8473aSAndreas Gohr } 13892a8473aSAndreas Gohr 13992a8473aSAndreas Gohr if ($extension == 'jpeg') { 14092a8473aSAndreas Gohr imagejpeg($this->image, $path, $this->options['quality']); 14192a8473aSAndreas Gohr } else { 14292a8473aSAndreas Gohr $saver($this->image, $path); 14392a8473aSAndreas Gohr } 14492a8473aSAndreas Gohr 14562c98cd6SAndreas Gohr $this->__destruct(); 14692a8473aSAndreas Gohr } 14792a8473aSAndreas Gohr 14892a8473aSAndreas Gohr /** 14992a8473aSAndreas Gohr * Initialize libGD on the given image 15092a8473aSAndreas Gohr * 15192a8473aSAndreas Gohr * @param string $path 15292a8473aSAndreas Gohr * @return resource 15392a8473aSAndreas Gohr * @throws Exception 15492a8473aSAndreas Gohr */ 15592a8473aSAndreas Gohr protected function loadImage($path) 15692a8473aSAndreas Gohr { 15792a8473aSAndreas Gohr // Figure out the file info 15892a8473aSAndreas Gohr $info = getimagesize($path); 15992a8473aSAndreas Gohr if ($info === false) { 16092a8473aSAndreas Gohr throw new Exception('Failed to read image information'); 16192a8473aSAndreas Gohr } 16292a8473aSAndreas Gohr $this->width = $info[0]; 16392a8473aSAndreas Gohr $this->height = $info[1]; 16492a8473aSAndreas Gohr 16592a8473aSAndreas Gohr // what type of image is it? 16692a8473aSAndreas Gohr $this->extension = image_type_to_extension($info[2], false); 16792a8473aSAndreas Gohr $creator = 'imagecreatefrom' . $this->extension; 16892a8473aSAndreas Gohr if (!function_exists($creator)) { 16992a8473aSAndreas Gohr throw new Exception('Can not work with image format ' . $this->extension); 17092a8473aSAndreas Gohr } 17192a8473aSAndreas Gohr 17292a8473aSAndreas Gohr // create the GD instance 17392a8473aSAndreas Gohr $image = @$creator($path); 17492a8473aSAndreas Gohr 17592a8473aSAndreas Gohr if ($image === false) { 17692a8473aSAndreas Gohr throw new Exception('Failed to load image wiht libGD'); 17792a8473aSAndreas Gohr } 17892a8473aSAndreas Gohr 17992a8473aSAndreas Gohr return $image; 18092a8473aSAndreas Gohr } 18192a8473aSAndreas Gohr 18292a8473aSAndreas Gohr /** 18392a8473aSAndreas Gohr * Creates a new blank image to which we can copy 18492a8473aSAndreas Gohr * 18592a8473aSAndreas Gohr * Tries to set up alpha/transparency stuff correctly 18692a8473aSAndreas Gohr * 18792a8473aSAndreas Gohr * @param int $width 18892a8473aSAndreas Gohr * @param int $height 18992a8473aSAndreas Gohr * @return resource 19092a8473aSAndreas Gohr * @throws Exception 19192a8473aSAndreas Gohr */ 19292a8473aSAndreas Gohr protected function createImage($width, $height) 19392a8473aSAndreas Gohr { 19492a8473aSAndreas Gohr // create a canvas to copy to, use truecolor if possible (except for gif) 19592a8473aSAndreas Gohr $canvas = false; 19692a8473aSAndreas Gohr if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') { 19792a8473aSAndreas Gohr $canvas = @imagecreatetruecolor($width, $height); 19892a8473aSAndreas Gohr } 19992a8473aSAndreas Gohr if (!$canvas) { 20092a8473aSAndreas Gohr $canvas = @imagecreate($width, $height); 20192a8473aSAndreas Gohr } 20292a8473aSAndreas Gohr if (!$canvas) { 20392a8473aSAndreas Gohr throw new Exception('Failed to create new canvas'); 20492a8473aSAndreas Gohr } 20592a8473aSAndreas Gohr 20692a8473aSAndreas Gohr //keep png alpha channel if possible 20792a8473aSAndreas Gohr if ($this->extension == 'png' && function_exists('imagesavealpha')) { 20892a8473aSAndreas Gohr imagealphablending($canvas, false); 20992a8473aSAndreas Gohr imagesavealpha($canvas, true); 21092a8473aSAndreas Gohr } 21192a8473aSAndreas Gohr 21292a8473aSAndreas Gohr //keep gif transparent color if possible 2132cadabe7SAndreas Gohr if ($this->extension == 'gif') { 2142cadabe7SAndreas Gohr $this->keepGifTransparency($this->image, $canvas); 2152cadabe7SAndreas Gohr } 2162cadabe7SAndreas Gohr 2172cadabe7SAndreas Gohr return $canvas; 2182cadabe7SAndreas Gohr } 2192cadabe7SAndreas Gohr 2202cadabe7SAndreas Gohr /** 2212cadabe7SAndreas Gohr * Copy transparency from gif to gif 2222cadabe7SAndreas Gohr * 2232cadabe7SAndreas Gohr * If no transparency is found or the PHP does not support it, the canvas is filled with white 2242cadabe7SAndreas Gohr * 2252cadabe7SAndreas Gohr * @param resource $image Original image 2262cadabe7SAndreas Gohr * @param resource $canvas New, empty image 2272cadabe7SAndreas Gohr * @return void 2282cadabe7SAndreas Gohr */ 2292cadabe7SAndreas Gohr protected function keepGifTransparency($image, $canvas) 2302cadabe7SAndreas Gohr { 2312cadabe7SAndreas Gohr if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) { 2322cadabe7SAndreas Gohr return; 2332cadabe7SAndreas Gohr } 2342cadabe7SAndreas Gohr 2352cadabe7SAndreas Gohr try { 2362cadabe7SAndreas Gohr if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) { 2372cadabe7SAndreas Gohr throw new \Exception('missing alpha methods'); 2382cadabe7SAndreas Gohr } 2392cadabe7SAndreas Gohr 2402cadabe7SAndreas Gohr $transcolorindex = @imagecolortransparent($image); 2412cadabe7SAndreas Gohr $transcolor = @imagecolorsforindex($image, $transcolorindex); 2422cadabe7SAndreas Gohr if (!$transcolor) { 2432cadabe7SAndreas Gohr // pre-PHP8 false is returned, in PHP8 an exception is thrown 2442cadabe7SAndreas Gohr throw new \ValueError('no valid alpha color'); 2452cadabe7SAndreas Gohr } 2462cadabe7SAndreas Gohr 24792a8473aSAndreas Gohr $transcolorindex = @imagecolorallocate( 24892a8473aSAndreas Gohr $canvas, 24992a8473aSAndreas Gohr $transcolor['red'], 25092a8473aSAndreas Gohr $transcolor['green'], 25192a8473aSAndreas Gohr $transcolor['blue'] 25292a8473aSAndreas Gohr ); 25392a8473aSAndreas Gohr @imagefill($canvas, 0, 0, $transcolorindex); 25492a8473aSAndreas Gohr @imagecolortransparent($canvas, $transcolorindex); 25592a8473aSAndreas Gohr 2562cadabe7SAndreas Gohr } catch (\Throwable $ignored) { 2572cadabe7SAndreas Gohr //filling with white 2582cadabe7SAndreas Gohr $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255); 2592cadabe7SAndreas Gohr @imagefill($canvas, 0, 0, $whitecolorindex); 2602cadabe7SAndreas Gohr } 26192a8473aSAndreas Gohr } 26292a8473aSAndreas Gohr 26392a8473aSAndreas Gohr /** 26492a8473aSAndreas Gohr * Calculates crop position 26592a8473aSAndreas Gohr * 26692a8473aSAndreas Gohr * Given the wanted final size, this calculates which exact area needs to be cut 26792a8473aSAndreas Gohr * from the original image to be then resized to the wanted dimensions. 26892a8473aSAndreas Gohr * 26992a8473aSAndreas Gohr * @param int $width 27092a8473aSAndreas Gohr * @param int $height 27192a8473aSAndreas Gohr * @return array (cropWidth, cropHeight, offsetX, offsetY) 27292a8473aSAndreas Gohr * @throws Exception 27392a8473aSAndreas Gohr */ 27492a8473aSAndreas Gohr protected function cropPosition($width, $height) 27592a8473aSAndreas Gohr { 27692a8473aSAndreas Gohr if ($width == 0 && $height == 0) { 27792a8473aSAndreas Gohr throw new Exception('You can not crop to 0x0'); 27892a8473aSAndreas Gohr } 27992a8473aSAndreas Gohr 28092a8473aSAndreas Gohr if (!$height) { 28192a8473aSAndreas Gohr $height = $width; 28292a8473aSAndreas Gohr } 28392a8473aSAndreas Gohr 28492a8473aSAndreas Gohr if (!$width) { 28592a8473aSAndreas Gohr $width = $height; 28692a8473aSAndreas Gohr } 28792a8473aSAndreas Gohr 28892a8473aSAndreas Gohr // calculate ratios 28992a8473aSAndreas Gohr $oldRatio = $this->width / $this->height; 29092a8473aSAndreas Gohr $newRatio = $width / $height; 29192a8473aSAndreas Gohr 29292a8473aSAndreas Gohr // calulate new size 29392a8473aSAndreas Gohr if ($newRatio >= 1) { 29492a8473aSAndreas Gohr if ($newRatio > $oldRatio) { 29592a8473aSAndreas Gohr $cropWidth = $this->width; 29692a8473aSAndreas Gohr $cropHeight = (int)($this->width / $newRatio); 29792a8473aSAndreas Gohr } else { 29892a8473aSAndreas Gohr $cropWidth = (int)($this->height * $newRatio); 29992a8473aSAndreas Gohr $cropHeight = $this->height; 30092a8473aSAndreas Gohr } 30192a8473aSAndreas Gohr } else { 30292a8473aSAndreas Gohr if ($newRatio < $oldRatio) { 30392a8473aSAndreas Gohr $cropWidth = (int)($this->height * $newRatio); 30492a8473aSAndreas Gohr $cropHeight = $this->height; 30592a8473aSAndreas Gohr } else { 30692a8473aSAndreas Gohr $cropWidth = $this->width; 30792a8473aSAndreas Gohr $cropHeight = (int)($this->width / $newRatio); 30892a8473aSAndreas Gohr } 30992a8473aSAndreas Gohr } 31092a8473aSAndreas Gohr 31192a8473aSAndreas Gohr // calculate crop offset 31292a8473aSAndreas Gohr $offsetX = (int)(($this->width - $cropWidth) / 2); 31392a8473aSAndreas Gohr $offsetY = (int)(($this->height - $cropHeight) / 2); 31492a8473aSAndreas Gohr 31592a8473aSAndreas Gohr return [$cropWidth, $cropHeight, $offsetX, $offsetY]; 31692a8473aSAndreas Gohr } 31792a8473aSAndreas Gohr 31892a8473aSAndreas Gohr /** 31992a8473aSAndreas Gohr * resize or crop images using PHP's libGD support 32092a8473aSAndreas Gohr * 32192a8473aSAndreas Gohr * @param int $toWidth desired width 32292a8473aSAndreas Gohr * @param int $toHeight desired height 32392a8473aSAndreas Gohr * @param int $offsetX offset of crop centre 32492a8473aSAndreas Gohr * @param int $offsetY offset of crop centre 32592a8473aSAndreas Gohr * @throws Exception 32692a8473aSAndreas Gohr */ 32792a8473aSAndreas Gohr protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0) 32892a8473aSAndreas Gohr { 32992a8473aSAndreas Gohr $newimg = $this->createImage($toWidth, $toHeight); 33092a8473aSAndreas Gohr 33192a8473aSAndreas Gohr //try resampling first, fall back to resizing 33292a8473aSAndreas Gohr if ( 33392a8473aSAndreas Gohr !function_exists('imagecopyresampled') || 33492a8473aSAndreas Gohr !@imagecopyresampled( 33592a8473aSAndreas Gohr $newimg, 33692a8473aSAndreas Gohr $this->image, 33792a8473aSAndreas Gohr 0, 33892a8473aSAndreas Gohr 0, 33992a8473aSAndreas Gohr $offsetX, 34092a8473aSAndreas Gohr $offsetY, 34192a8473aSAndreas Gohr $toWidth, 34292a8473aSAndreas Gohr $toHeight, 34392a8473aSAndreas Gohr $this->width, 34492a8473aSAndreas Gohr $this->height 34592a8473aSAndreas Gohr ) 34692a8473aSAndreas Gohr ) { 34792a8473aSAndreas Gohr imagecopyresized( 34892a8473aSAndreas Gohr $newimg, 34992a8473aSAndreas Gohr $this->image, 35092a8473aSAndreas Gohr 0, 35192a8473aSAndreas Gohr 0, 35292a8473aSAndreas Gohr $offsetX, 35392a8473aSAndreas Gohr $offsetY, 35492a8473aSAndreas Gohr $toWidth, 35592a8473aSAndreas Gohr $toHeight, 35692a8473aSAndreas Gohr $this->width, 35792a8473aSAndreas Gohr $this->height 35892a8473aSAndreas Gohr ); 35992a8473aSAndreas Gohr } 36092a8473aSAndreas Gohr 36192a8473aSAndreas Gohr // destroy original GD image ressource and replace with new one 36262c98cd6SAndreas Gohr $this->__destruct(); 36392a8473aSAndreas Gohr $this->image = $newimg; 36492a8473aSAndreas Gohr $this->width = $toWidth; 36592a8473aSAndreas Gohr $this->height = $toHeight; 36692a8473aSAndreas Gohr } 36792a8473aSAndreas Gohr 36892a8473aSAndreas Gohr} 369