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