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 if (is_resource($this->image)) { 34 imagedestroy($this->image); 35 } 36 } 37 38 /** @inheritDoc 39 * @throws Exception 40 * @link https://gist.github.com/EionRobb/8e0c76178522bc963c75caa6a77d3d37#file-imagecreatefromstring_autorotate-php-L15 41 */ 42 public function autorotate() 43 { 44 if ($this->extension !== 'jpeg') { 45 return $this; 46 } 47 48 $orientation = 1; 49 50 if (function_exists('exif_read_data')) { 51 // use PHP's exif capablities 52 $exif = exif_read_data($this->imagepath); 53 if (!empty($exif['Orientation'])) { 54 $orientation = $exif['Orientation']; 55 } 56 } else { 57 // grep the exif info from the raw contents 58 // we read only the first 70k bytes 59 $data = file_get_contents($this->imagepath, false, null, 0, 70000); 60 if (preg_match('@\x12\x01\x03\x00\x01\x00\x00\x00(.)\x00\x00\x00@', $data, $matches)) { 61 // Little endian EXIF 62 $orientation = ord($matches[1]); 63 } else if (preg_match('@\x01\x12\x00\x03\x00\x00\x00\x01\x00(.)\x00\x00@', $data, $matches)) { 64 // Big endian EXIF 65 $orientation = ord($matches[1]); 66 } 67 } 68 69 return $this->rotate($orientation); 70 } 71 72 /** 73 * @inheritDoc 74 * @throws Exception 75 */ 76 public function rotate($orientation) 77 { 78 $orientation = (int)$orientation; 79 if ($orientation < 0 || $orientation > 8) { 80 throw new Exception('Unknown rotation given'); 81 } 82 83 if ($orientation <= 1) { 84 // no rotation wanted 85 return $this; 86 } 87 88 // fill color 89 $transparency = imagecolorallocatealpha($this->image, 0, 0, 0, 127); 90 91 // rotate 92 if (in_array($orientation, [3, 4])) { 93 $image = imagerotate($this->image, 180, $transparency); 94 } 95 if (in_array($orientation, [5, 6])) { 96 $image = imagerotate($this->image, -90, $transparency); 97 list($this->width, $this->height) = [$this->height, $this->width]; 98 } elseif (in_array($orientation, [7, 8])) { 99 $image = imagerotate($this->image, 90, $transparency); 100 list($this->width, $this->height) = [$this->height, $this->width]; 101 } 102 /** @var resource $image is now defined */ 103 104 // additionally flip 105 if (in_array($orientation, [2, 5, 7, 4])) { 106 imageflip($image, IMG_FLIP_HORIZONTAL); 107 } 108 109 imagedestroy($this->image); 110 $this->image = $image; 111 112 //keep png alpha channel if possible 113 if ($this->extension == 'png' && function_exists('imagesavealpha')) { 114 imagealphablending($this->image, false); 115 imagesavealpha($this->image, true); 116 } 117 118 return $this; 119 } 120 121 /** 122 * @inheritDoc 123 * @throws Exception 124 */ 125 public function resize($width, $height) 126 { 127 list($width, $height) = $this->boundingBox($width, $height); 128 $this->resizeOperation($width, $height); 129 return $this; 130 } 131 132 /** 133 * @inheritDoc 134 * @throws Exception 135 */ 136 public function crop($width, $height) 137 { 138 list($this->width, $this->height, $offsetX, $offsetY) = $this->cropPosition($width, $height); 139 $this->resizeOperation($width, $height, $offsetX, $offsetY); 140 return $this; 141 } 142 143 /** 144 * @inheritDoc 145 * @throws Exception 146 */ 147 public function save($path, $extension = '') 148 { 149 if ($extension === 'jpg') { 150 $extension = 'jpeg'; 151 } 152 if ($extension === '') { 153 $extension = $this->extension; 154 } 155 $saver = 'image' . $extension; 156 if (!function_exists($saver)) { 157 throw new Exception('Can not save image format ' . $extension); 158 } 159 160 if ($extension == 'jpeg') { 161 imagejpeg($this->image, $path, $this->options['quality']); 162 } else { 163 $saver($this->image, $path); 164 } 165 166 imagedestroy($this->image); 167 } 168 169 /** 170 * Initialize libGD on the given image 171 * 172 * @param string $path 173 * @return resource 174 * @throws Exception 175 */ 176 protected function loadImage($path) 177 { 178 // Figure out the file info 179 $info = getimagesize($path); 180 if ($info === false) { 181 throw new Exception('Failed to read image information'); 182 } 183 $this->width = $info[0]; 184 $this->height = $info[1]; 185 186 // what type of image is it? 187 $this->extension = image_type_to_extension($info[2], false); 188 $creator = 'imagecreatefrom' . $this->extension; 189 if (!function_exists($creator)) { 190 throw new Exception('Can not work with image format ' . $this->extension); 191 } 192 193 // create the GD instance 194 $image = @$creator($path); 195 196 if ($image === false) { 197 throw new Exception('Failed to load image wiht libGD'); 198 } 199 200 return $image; 201 } 202 203 /** 204 * Creates a new blank image to which we can copy 205 * 206 * Tries to set up alpha/transparency stuff correctly 207 * 208 * @param int $width 209 * @param int $height 210 * @return resource 211 * @throws Exception 212 */ 213 protected function createImage($width, $height) 214 { 215 // create a canvas to copy to, use truecolor if possible (except for gif) 216 $canvas = false; 217 if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') { 218 $canvas = @imagecreatetruecolor($width, $height); 219 } 220 if (!$canvas) { 221 $canvas = @imagecreate($width, $height); 222 } 223 if (!$canvas) { 224 throw new Exception('Failed to create new canvas'); 225 } 226 227 //keep png alpha channel if possible 228 if ($this->extension == 'png' && function_exists('imagesavealpha')) { 229 imagealphablending($canvas, false); 230 imagesavealpha($canvas, true); 231 } 232 233 //keep gif transparent color if possible 234 if ($this->extension == 'gif') { 235 $this->keepGifTransparency($this->image, $canvas); 236 } 237 238 return $canvas; 239 } 240 241 /** 242 * Copy transparency from gif to gif 243 * 244 * If no transparency is found or the PHP does not support it, the canvas is filled with white 245 * 246 * @param resource $image Original image 247 * @param resource $canvas New, empty image 248 * @return void 249 */ 250 protected function keepGifTransparency($image, $canvas) 251 { 252 if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) { 253 return; 254 } 255 256 try { 257 if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) { 258 throw new \Exception('missing alpha methods'); 259 } 260 261 $transcolorindex = @imagecolortransparent($image); 262 $transcolor = @imagecolorsforindex($image, $transcolorindex); 263 if (!$transcolor) { 264 // pre-PHP8 false is returned, in PHP8 an exception is thrown 265 throw new \ValueError('no valid alpha color'); 266 } 267 268 $transcolorindex = @imagecolorallocate( 269 $canvas, 270 $transcolor['red'], 271 $transcolor['green'], 272 $transcolor['blue'] 273 ); 274 @imagefill($canvas, 0, 0, $transcolorindex); 275 @imagecolortransparent($canvas, $transcolorindex); 276 277 } catch (\Throwable $ignored) { 278 //filling with white 279 $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255); 280 @imagefill($canvas, 0, 0, $whitecolorindex); 281 } 282 } 283 284 /** 285 * Calculate new size 286 * 287 * If widht and height are given, the new size will be fit within this bounding box. 288 * If only one value is given the other is adjusted to match according to the aspect ratio 289 * 290 * @param int $width width of the bounding box 291 * @param int $height height of the bounding box 292 * @return array (width, height) 293 * @throws Exception 294 */ 295 protected function boundingBox($width, $height) 296 { 297 $width = $this->cleanDimension($width, $this->width); 298 $height = $this->cleanDimension($height, $this->height); 299 300 if ($width == 0 && $height == 0) { 301 throw new Exception('You can not resize to 0x0'); 302 } 303 304 if (!$height) { 305 // adjust to match width 306 $height = round(($width * $this->height) / $this->width); 307 } else if (!$width) { 308 // adjust to match height 309 $width = round(($height * $this->width) / $this->height); 310 } else { 311 // fit into bounding box 312 $scale = min($width / $this->width, $height / $this->height); 313 $width = $this->width * $scale; 314 $height = $this->height * $scale; 315 } 316 317 return [$width, $height]; 318 } 319 320 /** 321 * Ensure the given Dimension is a proper pixel value 322 * 323 * When a percentage is given, the value is calculated based on the given original dimension 324 * 325 * @param int|string $dim New Dimension 326 * @param int $orig Original dimension 327 * @return int 328 */ 329 protected function cleanDimension($dim, $orig) 330 { 331 if ($dim && substr($dim, -1) == '%') { 332 $dim = round($orig * ((float)$dim / 100)); 333 } else { 334 $dim = (int)$dim; 335 } 336 337 return $dim; 338 } 339 340 /** 341 * Calculates crop position 342 * 343 * Given the wanted final size, this calculates which exact area needs to be cut 344 * from the original image to be then resized to the wanted dimensions. 345 * 346 * @param int $width 347 * @param int $height 348 * @return array (cropWidth, cropHeight, offsetX, offsetY) 349 * @throws Exception 350 */ 351 protected function cropPosition($width, $height) 352 { 353 if ($width == 0 && $height == 0) { 354 throw new Exception('You can not crop to 0x0'); 355 } 356 357 if (!$height) { 358 $height = $width; 359 } 360 361 if (!$width) { 362 $width = $height; 363 } 364 365 // calculate ratios 366 $oldRatio = $this->width / $this->height; 367 $newRatio = $width / $height; 368 369 // calulate new size 370 if ($newRatio >= 1) { 371 if ($newRatio > $oldRatio) { 372 $cropWidth = $this->width; 373 $cropHeight = (int)($this->width / $newRatio); 374 } else { 375 $cropWidth = (int)($this->height * $newRatio); 376 $cropHeight = $this->height; 377 } 378 } else { 379 if ($newRatio < $oldRatio) { 380 $cropWidth = (int)($this->height * $newRatio); 381 $cropHeight = $this->height; 382 } else { 383 $cropWidth = $this->width; 384 $cropHeight = (int)($this->width / $newRatio); 385 } 386 } 387 388 // calculate crop offset 389 $offsetX = (int)(($this->width - $cropWidth) / 2); 390 $offsetY = (int)(($this->height - $cropHeight) / 2); 391 392 return [$cropWidth, $cropHeight, $offsetX, $offsetY]; 393 } 394 395 /** 396 * resize or crop images using PHP's libGD support 397 * 398 * @param int $toWidth desired width 399 * @param int $toHeight desired height 400 * @param int $offsetX offset of crop centre 401 * @param int $offsetY offset of crop centre 402 * @throws Exception 403 */ 404 protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0) 405 { 406 $newimg = $this->createImage($toWidth, $toHeight); 407 408 //try resampling first, fall back to resizing 409 if ( 410 !function_exists('imagecopyresampled') || 411 !@imagecopyresampled( 412 $newimg, 413 $this->image, 414 0, 415 0, 416 $offsetX, 417 $offsetY, 418 $toWidth, 419 $toHeight, 420 $this->width, 421 $this->height 422 ) 423 ) { 424 imagecopyresized( 425 $newimg, 426 $this->image, 427 0, 428 0, 429 $offsetX, 430 $offsetY, 431 $toWidth, 432 $toHeight, 433 $this->width, 434 $this->height 435 ); 436 } 437 438 // destroy original GD image ressource and replace with new one 439 imagedestroy($this->image); 440 $this->image = $newimg; 441 $this->width = $toWidth; 442 $this->height = $toHeight; 443 } 444 445} 446