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, $upscale = true) 105 { 106 list($width, $height) = ImageInfo::boundingBox($this->width, $this->height, $width, $height, $upscale); 107 $this->resizeOperation($width, $height); 108 return $this; 109 } 110 111 /** 112 * @inheritDoc 113 * @throws Exception 114 */ 115 public function crop($width, $height, $upscale = true) 116 { 117 $width = (int)$width; 118 $height = (int)$height; 119 if ($width == 0 && $height == 0) { 120 throw new Exception('You can not crop to 0x0'); 121 } 122 // an omitted dimension results in a square 123 if (!$height) $height = $width; 124 if (!$width) $width = $height; 125 126 if (!$upscale && ($width > $this->width || $height > $this->height)) { 127 // never enlarge: crop the oversized dimension(s) centered, without scaling 128 $cropWidth = min($width, $this->width); 129 $cropHeight = min($height, $this->height); 130 list($offsetX, $offsetY) = $this->centerOffset($cropWidth, $cropHeight); 131 // the extracted region equals the output, so nothing is scaled 132 $width = $cropWidth; 133 $height = $cropHeight; 134 } else { 135 list($cropWidth, $cropHeight, $offsetX, $offsetY) = $this->cropPosition($width, $height); 136 } 137 138 $this->width = $cropWidth; 139 $this->height = $cropHeight; 140 $this->resizeOperation($width, $height, $offsetX, $offsetY); 141 return $this; 142 } 143 144 /** 145 * @inheritDoc 146 * @throws Exception 147 */ 148 public function save($path, $extension = '') 149 { 150 if ($extension === 'jpg') { 151 $extension = 'jpeg'; 152 } 153 if ($extension === '') { 154 $extension = $this->extension; 155 } 156 $saver = 'image' . $extension; 157 if (!function_exists($saver)) { 158 throw new Exception('Can not save image format ' . $extension); 159 } 160 161 if ($extension == 'jpeg') { 162 imagejpeg($this->image, $path, $this->options['quality']); 163 } else { 164 $saver($this->image, $path); 165 } 166 167 $this->__destruct(); 168 } 169 170 /** 171 * Initialize libGD on the given image 172 * 173 * @param string $path 174 * @return resource 175 * @throws Exception 176 */ 177 protected function loadImage($path) 178 { 179 // Figure out the file info 180 $info = getimagesize($path); 181 if ($info === false) { 182 throw new Exception('Failed to read image information'); 183 } 184 $this->width = $info[0]; 185 $this->height = $info[1]; 186 187 // what type of image is it? 188 $this->extension = image_type_to_extension($info[2], false); 189 $creator = 'imagecreatefrom' . $this->extension; 190 if (!function_exists($creator)) { 191 throw new Exception('Can not work with image format ' . $this->extension); 192 } 193 194 // create the GD instance 195 $image = @$creator($path); 196 197 if ($image === false) { 198 throw new Exception('Failed to load image wiht libGD'); 199 } 200 201 return $image; 202 } 203 204 /** 205 * Creates a new blank image to which we can copy 206 * 207 * Tries to set up alpha/transparency stuff correctly 208 * 209 * @param int $width 210 * @param int $height 211 * @return resource 212 * @throws Exception 213 */ 214 protected function createImage($width, $height) 215 { 216 // create a canvas to copy to, use truecolor if possible (except for gif) 217 $canvas = false; 218 if (function_exists('imagecreatetruecolor') && $this->extension != 'gif') { 219 $canvas = @imagecreatetruecolor($width, $height); 220 } 221 if (!$canvas) { 222 $canvas = @imagecreate($width, $height); 223 } 224 if (!$canvas) { 225 throw new Exception('Failed to create new canvas'); 226 } 227 228 //keep png alpha channel if possible 229 if ($this->extension == 'png' && function_exists('imagesavealpha')) { 230 imagealphablending($canvas, false); 231 imagesavealpha($canvas, true); 232 } 233 234 //keep gif transparent color if possible 235 if ($this->extension == 'gif') { 236 $this->keepGifTransparency($this->image, $canvas); 237 } 238 239 return $canvas; 240 } 241 242 /** 243 * Copy transparency from gif to gif 244 * 245 * If no transparency is found or the PHP does not support it, the canvas is filled with white 246 * 247 * @param resource $image Original image 248 * @param resource $canvas New, empty image 249 * @return void 250 */ 251 protected function keepGifTransparency($image, $canvas) 252 { 253 if (!function_exists('imagefill') || !function_exists('imagecolorallocate')) { 254 return; 255 } 256 257 try { 258 if (!function_exists('imagecolorsforindex') || !function_exists('imagecolortransparent')) { 259 throw new \Exception('missing alpha methods'); 260 } 261 262 $transcolorindex = @imagecolortransparent($image); 263 $transcolor = @imagecolorsforindex($image, $transcolorindex); 264 if (!$transcolor) { 265 // pre-PHP8 false is returned, in PHP8 an exception is thrown 266 throw new \ValueError('no valid alpha color'); 267 } 268 269 $transcolorindex = @imagecolorallocate( 270 $canvas, 271 $transcolor['red'], 272 $transcolor['green'], 273 $transcolor['blue'] 274 ); 275 @imagefill($canvas, 0, 0, $transcolorindex); 276 @imagecolortransparent($canvas, $transcolorindex); 277 278 } catch (\Throwable $ignored) { 279 //filling with white 280 $whitecolorindex = @imagecolorallocate($canvas, 255, 255, 255); 281 @imagefill($canvas, 0, 0, $whitecolorindex); 282 } 283 } 284 285 /** 286 * Calculates crop position 287 * 288 * Given the wanted final size, this calculates which exact area needs to be cut 289 * from the original image to be then resized to the wanted dimensions. 290 * 291 * The dimensions are expected to be already resolved to non-zero pixel values. 292 * 293 * @param int $width resolved target width 294 * @param int $height resolved target height 295 * @return array (cropWidth, cropHeight, offsetX, offsetY) 296 */ 297 protected function cropPosition($width, $height) 298 { 299 // calculate ratios 300 $oldRatio = $this->width / $this->height; 301 $newRatio = $width / $height; 302 303 // calulate new size 304 if ($newRatio >= 1) { 305 if ($newRatio > $oldRatio) { 306 $cropWidth = $this->width; 307 $cropHeight = (int)($this->width / $newRatio); 308 } else { 309 $cropWidth = (int)($this->height * $newRatio); 310 $cropHeight = $this->height; 311 } 312 } else { 313 if ($newRatio < $oldRatio) { 314 $cropWidth = (int)($this->height * $newRatio); 315 $cropHeight = $this->height; 316 } else { 317 $cropWidth = $this->width; 318 $cropHeight = (int)($this->width / $newRatio); 319 } 320 } 321 322 list($offsetX, $offsetY) = $this->centerOffset($cropWidth, $cropHeight); 323 324 return [$cropWidth, $cropHeight, $offsetX, $offsetY]; 325 } 326 327 /** 328 * Calculate the top left offset to extract a centered region of the given size 329 * 330 * @param int $cropWidth width of the region to extract 331 * @param int $cropHeight height of the region to extract 332 * @return array (offsetX, offsetY) 333 */ 334 protected function centerOffset($cropWidth, $cropHeight) 335 { 336 return [ 337 (int)(($this->width - $cropWidth) / 2), 338 (int)(($this->height - $cropHeight) / 2), 339 ]; 340 } 341 342 /** 343 * resize or crop images using PHP's libGD support 344 * 345 * @param int $toWidth desired width 346 * @param int $toHeight desired height 347 * @param int $offsetX offset of crop centre 348 * @param int $offsetY offset of crop centre 349 * @throws Exception 350 */ 351 protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0) 352 { 353 $newimg = $this->createImage($toWidth, $toHeight); 354 355 //try resampling first, fall back to resizing 356 if ( 357 !function_exists('imagecopyresampled') || 358 !@imagecopyresampled( 359 $newimg, 360 $this->image, 361 0, 362 0, 363 $offsetX, 364 $offsetY, 365 $toWidth, 366 $toHeight, 367 $this->width, 368 $this->height 369 ) 370 ) { 371 imagecopyresized( 372 $newimg, 373 $this->image, 374 0, 375 0, 376 $offsetX, 377 $offsetY, 378 $toWidth, 379 $toHeight, 380 $this->width, 381 $this->height 382 ); 383 } 384 385 // destroy original GD image ressource and replace with new one 386 $this->__destruct(); 387 $this->image = $newimg; 388 $this->width = $toWidth; 389 $this->height = $toHeight; 390 } 391 392} 393