[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/splitbrain/slika/src/ -> GdAdapter.php (source)

   1  <?php /** @noinspection PhpComposerExtensionStubsInspection */
   2  
   3  
   4  namespace splitbrain\slika;
   5  
   6  /**
   7   * Image processing adapter for PHP's libGD
   8   */
   9  class 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  }