1<?php /** @noinspection PhpComposerExtensionStubsInspection */ 2 3 4namespace splitbrain\slika; 5 6/** 7 * Image processing adapter for PHP's libGD 8 */ 9class GdAdapter extends Adapter 10{ 11 /** @var resource libGD image */ 12 protected $image; 13 /** @var int width of the current image */ 14 protected $width = 0; 15 /** @var int height of the current image */ 16 protected $height = 0; 17 /** @var string the extension of the file we're working with */ 18 protected $extension; 19 20 21 /** @inheritDoc */ 22 public function __construct($imagepath, $options = []) 23 { 24 parent::__construct($imagepath, $options); 25 $this->image = $this->loadImage($imagepath); 26 } 27 28 /** 29 * Clean up 30 */ 31 public function __destruct() 32 { 33 // destroy the GD image resource (only needed on PHP < 8.0) 34 if (is_resource($this->image)) { 35 imagedestroy($this->image); 36 } 37 } 38 39 /** @inheritDoc 40 * @throws Exception 41 */ 42 public function autorotate() 43 { 44 if ($this->extension !== 'jpeg') { 45 return $this; 46 } 47 return $this->rotate(ImageInfo::readExifOrientation($this->imagepath)); 48 } 49 50 /** 51 * @inheritDoc 52 * @throws Exception 53 */ 54 public function rotate($orientation) 55 { 56 $orientation = (int)$orientation; 57 if ($orientation < 0 || $orientation > 8) { 58 throw new Exception('Unknown rotation given'); 59 } 60 61 if ($orientation <= 1) { 62 // no rotation wanted 63 return $this; 64 } 65 66 // fill color 67 $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127); 68 69 // rotate (orientation 2 is a flip-only case and keeps $this->image) 70 $image = $this->image; 71 if (in_array($orientation, [3, 4])) { 72 $image = imagerotate($this->image, 180, $transparency); 73 } elseif (in_array($orientation, [5, 6])) { 74 $image = imagerotate($this->image, -90, $transparency); 75 list($this->width, $this->height) = [$this->height, $this->width]; 76 } elseif (in_array($orientation, [7, 8])) { 77 $image = imagerotate($this->image, 90, $transparency); 78 list($this->width, $this->height) = [$this->height, $this->width]; 79 } 80 81 // additionally flip 82 if (in_array($orientation, [2, 5, 7, 4])) { 83 imageflip($image, IMG_FLIP_HORIZONTAL); 84 } 85 86 if ($image !== $this->image) { 87 $this->__destruct(); // destroy old image 88 $this->image = $image; 89 } 90 91 //keep png alpha channel if possible 92 if ($this->extension == 'png' && function_exists('imagesavealpha')) { 93 imagealphablending($this->image, false); 94 imagesavealpha($this->image, true); 95 } 96 97 return $this; 98 } 99 100 /** 101 * @inheritDoc 102 * @throws Exception 103 */ 104 public function resize($width, $height) 105 { 106 list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height); 107 $this->resizeOperation($width, $height); 108 return $this; 109 } 110 111 /** 112 * @inheritDoc 113 * @throws Exception 114 */ 115 public function crop($width, $height) 116 { 117 list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height); 118 $this->resizeOperation($width, $height, $offsetX, $offsetY); 119 return $this; 120 } 121 122 /** 123 * @inheritDoc 124 * @throws Exception 125 */ 126 public function save($path, $extension = '') 127 { 128 if ($extension === 'jpg') { 129 $extension = 'jpeg'; 130 } 131 if ($extension === '') { 132 $extension = $this->extension; 133 } 134 $saver = 'image' . $extension; 135 if (!function_exists($saver)) { 136 throw new Exception('Can not save image format ' . $extension); 137 } 138 139 if ($extension == 'jpeg') { 140 imagejpeg($this->image, $path, $this->options['quality']); 141 } else { 142 $saver($this->image, $path); 143 } 144 145 $this->__destruct(); 146 } 147 148 /** 149 * Initialize libGD on the given image 150 * 151 * @param string $path 152 * @return resource 153 * @throws Exception 154 */ 155 protected function loadImage($path) 156 { 157 // Figure out the file info 158 $info = getimagesize($path); 159 if ($info === false) { 160 throw new Exception('Failed to read image information'); 161 } 162 $this->width = $info[0]; 163 $this->height = $info[1]; 164 165 // what type of image is it? 166 $this->extension = image_type_to_extension($info[2], false); 167 $creator = 'imagecreatefrom' . $this->extension; 168 if (!function_exists($creator)) { 169 throw new Exception('Can not work with image format ' . $this->extension); 170 } 171 172 // create the GD instance 173 $image = @$creator($path); 174 175 if ($image === false) { 176 throw new Exception('Failed to load image wiht libGD'); 177 } 178 179 return $image; 180 } 181 182 /** 183 * Creates a new blank image to which we can copy 184 * 185 * Tries to set up alpha/transparency stuff correctly 186 * 187 * @param int $width 188 * @param int $height 189 * @return resource 190 * @throws Exception 191 */ 192 protected function createImage($width, $height) 193 { 194 // create a canvas to copy to, use truecolor if possible (except for gif) 195 $canvas = false; 196 if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') { 197 $canvas = @imagecreatetruecolor($width, $height); 198 } 199 if (!$canvas) { 200 $canvas = @imagecreate($width, $height); 201 } 202 if (!$canvas) { 203 throw new Exception('Failed to create new canvas'); 204 } 205 206 //keep png alpha channel if possible 207 if ($this->extension == 'png' && function_exists('imagesavealpha')) { 208 imagealphablending($canvas, false); 209 imagesavealpha($canvas, true); 210 } 211 212 //keep gif transparent color if possible 213 if ($this->extension == 'gif') { 214 $this->keepGifTransparency($this->image, $canvas); 215 } 216 217 return $canvas; 218 } 219 220 /** 221 * Copy transparency from gif to gif 222 * 223 * If no transparency is found or the PHP does not support it, the canvas is filled with white 224 * 225 * @param resource $image Original image 226 * @param resource $canvas New, empty image 227 * @return void 228 */ 229 protected function keepGifTransparency($image, $canvas) 230 { 231 if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) { 232 return; 233 } 234 235 try { 236 if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) { 237 throw new \Exception('missing alpha methods'); 238 } 239 240 $transcolorindex = @imagecolortransparent($image); 241 $transcolor = @imagecolorsforindex($image, $transcolorindex); 242 if (!$transcolor) { 243 // pre-PHP8 false is returned, in PHP8 an exception is thrown 244 throw new \ValueError('no valid alpha color'); 245 } 246 247 $transcolorindex = @imagecolorallocate( 248 $canvas, 249 $transcolor['red'], 250 $transcolor['green'], 251 $transcolor['blue'] 252 ); 253 @imagefill($canvas, 0, 0, $transcolorindex); 254 @imagecolortransparent($canvas, $transcolorindex); 255 256 } catch (\Throwable $ignored) { 257 //filling with white 258 $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255); 259 @imagefill($canvas, 0, 0, $whitecolorindex); 260 } 261 } 262 263 /** 264 * Calculates crop position 265 * 266 * Given the wanted final size, this calculates which exact area needs to be cut 267 * from the original image to be then resized to the wanted dimensions. 268 * 269 * @param int $width 270 * @param int $height 271 * @return array (cropWidth, cropHeight, offsetX, offsetY) 272 * @throws Exception 273 */ 274 protected function cropPosition($width, $height) 275 { 276 if ($width == 0 && $height == 0) { 277 throw new Exception('You can not crop to 0x0'); 278 } 279 280 if (!$height) { 281 $height = $width; 282 } 283 284 if (!$width) { 285 $width = $height; 286 } 287 288 // calculate ratios 289 $oldRatio = $this->width / $this->height; 290 $newRatio = $width / $height; 291 292 // calulate new size 293 if ($newRatio >= 1) { 294 if ($newRatio > $oldRatio) { 295 $cropWidth = $this->width; 296 $cropHeight = (int)($this->width / $newRatio); 297 } else { 298 $cropWidth = (int)($this->height * $newRatio); 299 $cropHeight = $this->height; 300 } 301 } else { 302 if ($newRatio < $oldRatio) { 303 $cropWidth = (int)($this->height * $newRatio); 304 $cropHeight = $this->height; 305 } else { 306 $cropWidth = $this->width; 307 $cropHeight = (int)($this->width / $newRatio); 308 } 309 } 310 311 // calculate crop offset 312 $offsetX = (int)(($this->width - $cropWidth) / 2); 313 $offsetY = (int)(($this->height - $cropHeight) / 2); 314 315 return [$cropWidth, $cropHeight, $offsetX, $offsetY]; 316 } 317 318 /** 319 * resize or crop images using PHP's libGD support 320 * 321 * @param int $toWidth desired width 322 * @param int $toHeight desired height 323 * @param int $offsetX offset of crop centre 324 * @param int $offsetY offset of crop centre 325 * @throws Exception 326 */ 327 protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0) 328 { 329 $newimg = $this->createImage($toWidth, $toHeight); 330 331 //try resampling first, fall back to resizing 332 if ( 333 !function_exists('imagecopyresampled') || 334 !@imagecopyresampled( 335 $newimg, 336 $this->image, 337 0, 338 0, 339 $offsetX, 340 $offsetY, 341 $toWidth, 342 $toHeight, 343 $this->width, 344 $this->height 345 ) 346 ) { 347 imagecopyresized( 348 $newimg, 349 $this->image, 350 0, 351 0, 352 $offsetX, 353 $offsetY, 354 $toWidth, 355 $toHeight, 356 $this->width, 357 $this->height 358 ); 359 } 360 361 // destroy original GD image ressource and replace with new one 362 $this->__destruct(); 363 $this->image = $newimg; 364 $this->width = $toWidth; 365 $this->height = $toHeight; 366 } 367 368} 369