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 if ($width == 0 && $height == 0) { 272 throw new Exception('You can not resize to 0x0'); 273 } 274 275 if (!$height) { 276 // adjust to match width 277 $height = round(($width * $this->height) / $this->width); 278 } else if (!$width) { 279 // adjust to match height 280 $width = round(($height * $this->width) / $this->height); 281 } else { 282 // fit into bounding box 283 $scale = min($width / $this->width, $height / $this->height); 284 $width = $this->width * $scale; 285 $height = $this->height * $scale; 286 } 287 288 return [$width, $height]; 289 } 290 291 /** 292 * Calculates crop position 293 * 294 * Given the wanted final size, this calculates which exact area needs to be cut 295 * from the original image to be then resized to the wanted dimensions. 296 * 297 * @param int $width 298 * @param int $height 299 * @return array (cropWidth, cropHeight, offsetX, offsetY) 300 * @throws Exception 301 */ 302 protected function cropPosition($width, $height) 303 { 304 if ($width == 0 && $height == 0) { 305 throw new Exception('You can not crop to 0x0'); 306 } 307 308 if (!$height) { 309 $height = $width; 310 } 311 312 if (!$width) { 313 $width = $height; 314 } 315 316 // calculate ratios 317 $oldRatio = $this->width / $this->height; 318 $newRatio = $width / $height; 319 320 // calulate new size 321 if ($newRatio >= 1) { 322 if ($newRatio > $oldRatio) { 323 $cropWidth = $this->width; 324 $cropHeight = (int)($this->width / $newRatio); 325 } else { 326 $cropWidth = (int)($this->height * $newRatio); 327 $cropHeight = $this->height; 328 } 329 } else { 330 if ($newRatio < $oldRatio) { 331 $cropWidth = (int)($this->height * $newRatio); 332 $cropHeight = $this->height; 333 } else { 334 $cropWidth = $this->width; 335 $cropHeight = (int)($this->width / $newRatio); 336 } 337 } 338 339 // calculate crop offset 340 $offsetX = (int)(($this->width - $cropWidth) / 2); 341 $offsetY = (int)(($this->height - $cropHeight) / 2); 342 343 return [$cropWidth, $cropHeight, $offsetX, $offsetY]; 344 } 345 346 /** 347 * resize or crop images using PHP's libGD support 348 * 349 * @param int $toWidth desired width 350 * @param int $toHeight desired height 351 * @param int $offsetX offset of crop centre 352 * @param int $offsetY offset of crop centre 353 * @throws Exception 354 */ 355 protected function resizeOperation($toWidth, $toHeight, $offsetX = 0, $offsetY = 0) 356 { 357 $newimg = $this->createImage($toWidth, $toHeight); 358 359 //try resampling first, fall back to resizing 360 if ( 361 !function_exists('imagecopyresampled') || 362 !@imagecopyresampled( 363 $newimg, 364 $this->image, 365 0, 366 0, 367 $offsetX, 368 $offsetY, 369 $toWidth, 370 $toHeight, 371 $this->width, 372 $this->height 373 ) 374 ) { 375 imagecopyresized( 376 $newimg, 377 $this->image, 378 0, 379 0, 380 $offsetX, 381 $offsetY, 382 $toWidth, 383 $toHeight, 384 $this->width, 385 $this->height 386 ); 387 } 388 389 // destroy original GD image ressource and replace with new one 390 imagedestroy($this->image); 391 $this->image = $newimg; 392 $this->width = $toWidth; 393 $this->height = $toHeight; 394 } 395 396} 397