3 * @file src/Object/Image.php
4 * @brief This file contains the Image class for image processing
6 namespace Friendica\Object;
9 use Friendica\Core\Config;
10 use Friendica\Core\System;
11 use Friendica\Util\Images;
16 * Class to handle images
20 /** @var Imagick|resource */
24 * Put back gd stuff, not everybody have Imagick
36 * @param boolean $type optional, default null
37 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
38 * @throws \ImagickException
40 public function __construct($data, $type = null)
42 $this->imagick = class_exists('Imagick');
43 $this->types = Images::supportedTypes();
44 if (!array_key_exists($type, $this->types)) {
49 if ($this->isImagick() && $this->loadData($data)) {
52 // Failed to load with Imagick, fallback
53 $this->imagick = false;
55 return $this->loadData($data);
62 public function __destruct()
65 if ($this->isImagick()) {
66 $this->image->clear();
67 $this->image->destroy();
70 if (is_resource($this->image)) {
71 imagedestroy($this->image);
79 public function isImagick()
81 return $this->imagick;
85 * @param string $data data
87 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
88 * @throws \ImagickException
90 private function loadData($data)
92 if ($this->isImagick()) {
93 $this->image = new Imagick();
95 $this->image->readImageBlob($data);
96 } catch (Exception $e) {
97 // Imagick couldn't use the data
102 * Setup the image to the format it will be saved to
104 $map = Images::getFormatsMap();
105 $format = $map[$this->type];
106 $this->image->setFormat($format);
108 // Always coalesce, if it is not a multi-frame image it won't hurt anyway
109 $this->image = $this->image->coalesceImages();
112 * setup the compression here, so we'll do it only once
114 switch ($this->getType()) {
116 $quality = Config::get('system', 'png_quality');
117 if ((! $quality) || ($quality > 9)) {
118 $quality = PNG_QUALITY;
121 * From http://www.imagemagick.org/script/command-line-options.php#quality:
123 * 'For the MNG and PNG image formats, the quality value sets
124 * the zlib compression level (quality / 10) and filter-type (quality % 10).
125 * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering,
126 * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
128 $quality = $quality * 10;
129 $this->image->setCompressionQuality($quality);
132 $quality = Config::get('system', 'jpeg_quality');
133 if ((! $quality) || ($quality > 100)) {
134 $quality = JPEG_QUALITY;
136 $this->image->setCompressionQuality($quality);
139 // The 'width' and 'height' properties are only used by non-Imagick routines.
140 $this->width = $this->image->getImageWidth();
141 $this->height = $this->image->getImageHeight();
147 $this->valid = false;
148 $this->image = @imagecreatefromstring($data);
149 if ($this->image !== false) {
150 $this->width = imagesx($this->image);
151 $this->height = imagesy($this->image);
153 imagealphablending($this->image, false);
154 imagesavealpha($this->image, true);
165 public function isValid()
167 if ($this->isImagick()) {
168 return ($this->image !== false);
176 public function getWidth()
178 if (!$this->isValid()) {
182 if ($this->isImagick()) {
183 return $this->image->getImageWidth();
191 public function getHeight()
193 if (!$this->isValid()) {
197 if ($this->isImagick()) {
198 return $this->image->getImageHeight();
200 return $this->height;
206 public function getImage()
208 if (!$this->isValid()) {
212 if ($this->isImagick()) {
214 $this->image = $this->image->deconstructImages();
223 public function getType()
225 if (!$this->isValid()) {
235 public function getExt()
237 if (!$this->isValid()) {
241 return $this->types[$this->getType()];
245 * @param integer $max max dimension
248 public function scaleDown($max)
250 if (!$this->isValid()) {
254 $width = $this->getWidth();
255 $height = $this->getHeight();
257 if ((! $width)|| (! $height)) {
261 if ($width > $max && $height > $max) {
262 // very tall image (greater than 16:9)
263 // constrain the width - let the height float.
265 if ((($height * 9) / 16) > $width) {
267 $dest_height = intval(($height * $max) / $width);
268 } elseif ($width > $height) {
269 // else constrain both dimensions
271 $dest_height = intval(($height * $max) / $width);
273 $dest_width = intval(($width * $max) / $height);
279 $dest_height = intval(($height * $max) / $width);
281 if ($height > $max) {
282 // very tall image (greater than 16:9)
283 // but width is OK - don't do anything
285 if ((($height * 9) / 16) > $width) {
286 $dest_width = $width;
287 $dest_height = $height;
289 $dest_width = intval(($width * $max) / $height);
293 $dest_width = $width;
294 $dest_height = $height;
299 return $this->scale($dest_width, $dest_height);
303 * @param integer $degrees degrees to rotate image
306 public function rotate($degrees)
308 if (!$this->isValid()) {
312 if ($this->isImagick()) {
313 $this->image->setFirstIterator();
315 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
316 } while ($this->image->nextImage());
320 // if script dies at this point check memory_limit setting in php.ini
321 $this->image = imagerotate($this->image, $degrees, 0);
322 $this->width = imagesx($this->image);
323 $this->height = imagesy($this->image);
327 * @param boolean $horiz optional, default true
328 * @param boolean $vert optional, default false
331 public function flip($horiz = true, $vert = false)
333 if (!$this->isValid()) {
337 if ($this->isImagick()) {
338 $this->image->setFirstIterator();
341 $this->image->flipImage();
344 $this->image->flopImage();
346 } while ($this->image->nextImage());
350 $w = imagesx($this->image);
351 $h = imagesy($this->image);
352 $flipped = imagecreate($w, $h);
354 for ($x = 0; $x < $w; $x++) {
355 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
359 for ($y = 0; $y < $h; $y++) {
360 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
363 $this->image = $flipped;
367 * @param string $filename filename
370 public function orient($filename)
372 if ($this->isImagick()) {
373 // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
374 $orientation = $this->image->getImageOrientation();
375 switch ($orientation) {
376 case Imagick::ORIENTATION_BOTTOMRIGHT:
377 $this->image->rotateimage("#000", 180);
379 case Imagick::ORIENTATION_RIGHTTOP:
380 $this->image->rotateimage("#000", 90);
382 case Imagick::ORIENTATION_LEFTBOTTOM:
383 $this->image->rotateimage("#000", -90);
387 $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
390 // based off comment on http://php.net/manual/en/function.imagerotate.php
392 if (!$this->isValid()) {
396 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
400 $exif = @exif_read_data($filename, null, true);
405 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
411 case 2: // horizontal flip
415 case 3: // 180 rotate left
419 case 4: // vertical flip
420 $this->flip(false, true);
423 case 5: // vertical flip + 90 rotate right
424 $this->flip(false, true);
428 case 6: // 90 rotate right
432 case 7: // horizontal flip + 90 rotate right
437 case 8: // 90 rotate left
442 // Logger::log('exif: ' . print_r($exif,true));
447 * @param integer $min minimum dimension
450 public function scaleUp($min)
452 if (!$this->isValid()) {
456 $width = $this->getWidth();
457 $height = $this->getHeight();
459 if ((!$width)|| (!$height)) {
463 if ($width < $min && $height < $min) {
464 if ($width > $height) {
466 $dest_height = intval(($height * $min) / $width);
468 $dest_width = intval(($width * $min) / $height);
474 $dest_height = intval(($height * $min) / $width);
476 if ($height < $min) {
477 $dest_width = intval(($width * $min) / $height);
480 $dest_width = $width;
481 $dest_height = $height;
486 return $this->scale($dest_width, $dest_height);
490 * @param integer $dim dimension
493 public function scaleToSquare($dim)
495 if (!$this->isValid()) {
499 return $this->scale($dim, $dim);
503 * @brief Scale image to target dimensions
505 * @param int $dest_width
506 * @param int $dest_height
509 private function scale($dest_width, $dest_height)
511 if (!$this->isValid()) {
515 if ($this->isImagick()) {
517 * If it is not animated, there will be only one iteration here,
518 * so don't bother checking
520 // Don't forget to go back to the first frame
521 $this->image->setFirstIterator();
523 // FIXME - implement horizontal bias for scaling as in following GD functions
524 // to allow very tall images to be constrained only horizontally.
525 $this->image->scaleImage($dest_width, $dest_height);
526 } while ($this->image->nextImage());
528 // These may not be necessary anymore
529 $this->width = $this->image->getImageWidth();
530 $this->height = $this->image->getImageHeight();
532 $dest = imagecreatetruecolor($dest_width, $dest_height);
533 imagealphablending($dest, false);
534 imagesavealpha($dest, true);
536 if ($this->type=='image/png') {
537 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
540 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
543 imagedestroy($this->image);
546 $this->image = $dest;
547 $this->width = imagesx($this->image);
548 $this->height = imagesy($this->image);
555 * @param integer $max maximum
556 * @param integer $x x coordinate
557 * @param integer $y y coordinate
558 * @param integer $w width
559 * @param integer $h height
562 public function crop($max, $x, $y, $w, $h)
564 if (!$this->isValid()) {
568 if ($this->isImagick()) {
569 $this->image->setFirstIterator();
571 $this->image->cropImage($w, $h, $x, $y);
573 * We need to remove the canva,
574 * or the image is not resized to the crop:
575 * http://php.net/manual/en/imagick.cropimage.php#97232
577 $this->image->setImagePage(0, 0, 0, 0);
578 } while ($this->image->nextImage());
579 return $this->scaleDown($max);
582 $dest = imagecreatetruecolor($max, $max);
583 imagealphablending($dest, false);
584 imagesavealpha($dest, true);
585 if ($this->type=='image/png') {
586 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
588 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
590 imagedestroy($this->image);
592 $this->image = $dest;
593 $this->width = imagesx($this->image);
594 $this->height = imagesy($this->image);
598 * @param string $path file path
600 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
602 public function saveToFilePath($path)
604 if (!$this->isValid()) {
608 $string = $this->asString();
612 $stamp1 = microtime(true);
613 file_put_contents($path, $string);
614 $a->getProfiler()->saveTimestamp($stamp1, "file", System::callstack());
618 * @brief Magic method allowing string casting of an Image object
620 * Ex: $data = $Image->asString();
622 * $data = (string) $Image;
625 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
627 public function __toString() {
628 return $this->asString();
633 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
635 public function asString()
637 if (!$this->isValid()) {
641 if ($this->isImagick()) {
643 $this->image = $this->image->deconstructImages();
644 $string = $this->image->getImagesBlob();
650 // Enable interlacing
651 imageinterlace($this->image, true);
653 switch ($this->getType()) {
655 $quality = Config::get('system', 'png_quality');
656 if ((!$quality) || ($quality > 9)) {
657 $quality = PNG_QUALITY;
659 imagepng($this->image, null, $quality);
662 $quality = Config::get('system', 'jpeg_quality');
663 if ((!$quality) || ($quality > 100)) {
664 $quality = JPEG_QUALITY;
666 imagejpeg($this->image, null, $quality);
668 $string = ob_get_contents();
675 * @brief supported mimetypes and corresponding file extensions
677 * @deprecated in version 2019.12 please use Util\Images::supportedTypes() instead.
679 public static function supportedTypes()
681 return Images::supportedTypes();
685 * @brief Maps Mime types to Imagick formats
686 * @return array With with image formats (mime type as key)
687 * @deprecated in version 2019.12 please use Util\Images::getFormatsMap() instead.
689 public static function getFormatsMap()
691 return Images::getFormatsMap();
695 * Guess image mimetype from filename or from Content-Type header
697 * @param string $filename Image filename
698 * @param boolean $fromcurl Check Content-Type header from curl request
699 * @param string $header passed headers to take into account
701 * @return string|null
703 * @deprecated in version 2019.12 please use Util\Images::guessType() instead.
705 public static function guessType($filename, $fromcurl = false, $header = '')
707 return Images::guessType($filename, $fromcurl, $header);
711 * @param string $url url
713 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
714 * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
716 public static function getInfoFromURL($url)
718 return Images::getInfoFromURLCached($url);
722 * @param integer $width width
723 * @param integer $height height
724 * @param integer $max max
726 * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
728 public static function getScalingDimensions($width, $height, $max)
730 return Images::getScalingDimensions($width, $height, $max);