3 * @file src/Object/Image.php
4 * This file contains the Image class for image processing
6 namespace Friendica\Object;
9 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);
63 public function __destruct()
66 if ($this->isImagick()) {
67 $this->image->clear();
68 $this->image->destroy();
71 if (is_resource($this->image)) {
72 imagedestroy($this->image);
80 public function isImagick()
82 return $this->imagick;
86 * @param string $data data
88 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
89 * @throws \ImagickException
91 private function loadData($data)
93 if ($this->isImagick()) {
94 $this->image = new Imagick();
96 $this->image->readImageBlob($data);
97 } catch (Exception $e) {
98 // Imagick couldn't use the data
103 * Setup the image to the format it will be saved to
105 $map = Images::getFormatsMap();
106 $format = $map[$this->type];
107 $this->image->setFormat($format);
109 // Always coalesce, if it is not a multi-frame image it won't hurt anyway
110 $this->image = $this->image->coalesceImages();
113 * setup the compression here, so we'll do it only once
115 switch ($this->getType()) {
117 $quality = DI::config()->get('system', 'png_quality');
118 if ((! $quality) || ($quality > 9)) {
119 $quality = PNG_QUALITY;
122 * From http://www.imagemagick.org/script/command-line-options.php#quality:
124 * 'For the MNG and PNG image formats, the quality value sets
125 * the zlib compression level (quality / 10) and filter-type (quality % 10).
126 * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering,
127 * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
129 $quality = $quality * 10;
130 $this->image->setCompressionQuality($quality);
133 $quality = DI::config()->get('system', 'jpeg_quality');
134 if ((! $quality) || ($quality > 100)) {
135 $quality = JPEG_QUALITY;
137 $this->image->setCompressionQuality($quality);
140 // The 'width' and 'height' properties are only used by non-Imagick routines.
141 $this->width = $this->image->getImageWidth();
142 $this->height = $this->image->getImageHeight();
148 $this->valid = false;
149 $this->image = @imagecreatefromstring($data);
150 if ($this->image !== false) {
151 $this->width = imagesx($this->image);
152 $this->height = imagesy($this->image);
154 imagealphablending($this->image, false);
155 imagesavealpha($this->image, true);
166 public function isValid()
168 if ($this->isImagick()) {
169 return ($this->image !== false);
177 public function getWidth()
179 if (!$this->isValid()) {
183 if ($this->isImagick()) {
184 return $this->image->getImageWidth();
192 public function getHeight()
194 if (!$this->isValid()) {
198 if ($this->isImagick()) {
199 return $this->image->getImageHeight();
201 return $this->height;
207 public function getImage()
209 if (!$this->isValid()) {
213 if ($this->isImagick()) {
215 $this->image = $this->image->deconstructImages();
224 public function getType()
226 if (!$this->isValid()) {
236 public function getExt()
238 if (!$this->isValid()) {
242 return $this->types[$this->getType()];
246 * @param integer $max max dimension
249 public function scaleDown($max)
251 if (!$this->isValid()) {
255 $width = $this->getWidth();
256 $height = $this->getHeight();
258 if ((! $width)|| (! $height)) {
262 if ($width > $max && $height > $max) {
263 // very tall image (greater than 16:9)
264 // constrain the width - let the height float.
266 if ((($height * 9) / 16) > $width) {
268 $dest_height = intval(($height * $max) / $width);
269 } elseif ($width > $height) {
270 // else constrain both dimensions
272 $dest_height = intval(($height * $max) / $width);
274 $dest_width = intval(($width * $max) / $height);
280 $dest_height = intval(($height * $max) / $width);
282 if ($height > $max) {
283 // very tall image (greater than 16:9)
284 // but width is OK - don't do anything
286 if ((($height * 9) / 16) > $width) {
287 $dest_width = $width;
288 $dest_height = $height;
290 $dest_width = intval(($width * $max) / $height);
294 $dest_width = $width;
295 $dest_height = $height;
300 return $this->scale($dest_width, $dest_height);
304 * @param integer $degrees degrees to rotate image
307 public function rotate($degrees)
309 if (!$this->isValid()) {
313 if ($this->isImagick()) {
314 $this->image->setFirstIterator();
316 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
317 } while ($this->image->nextImage());
321 // if script dies at this point check memory_limit setting in php.ini
322 $this->image = imagerotate($this->image, $degrees, 0);
323 $this->width = imagesx($this->image);
324 $this->height = imagesy($this->image);
328 * @param boolean $horiz optional, default true
329 * @param boolean $vert optional, default false
332 public function flip($horiz = true, $vert = false)
334 if (!$this->isValid()) {
338 if ($this->isImagick()) {
339 $this->image->setFirstIterator();
342 $this->image->flipImage();
345 $this->image->flopImage();
347 } while ($this->image->nextImage());
351 $w = imagesx($this->image);
352 $h = imagesy($this->image);
353 $flipped = imagecreate($w, $h);
355 for ($x = 0; $x < $w; $x++) {
356 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
360 for ($y = 0; $y < $h; $y++) {
361 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
364 $this->image = $flipped;
368 * @param string $filename filename
371 public function orient($filename)
373 if ($this->isImagick()) {
374 // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
375 $orientation = $this->image->getImageOrientation();
376 switch ($orientation) {
377 case Imagick::ORIENTATION_BOTTOMRIGHT:
378 $this->image->rotateimage("#000", 180);
380 case Imagick::ORIENTATION_RIGHTTOP:
381 $this->image->rotateimage("#000", 90);
383 case Imagick::ORIENTATION_LEFTBOTTOM:
384 $this->image->rotateimage("#000", -90);
388 $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
391 // based off comment on http://php.net/manual/en/function.imagerotate.php
393 if (!$this->isValid()) {
397 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
401 $exif = @exif_read_data($filename, null, true);
406 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
412 case 2: // horizontal flip
416 case 3: // 180 rotate left
420 case 4: // vertical flip
421 $this->flip(false, true);
424 case 5: // vertical flip + 90 rotate right
425 $this->flip(false, true);
429 case 6: // 90 rotate right
433 case 7: // horizontal flip + 90 rotate right
438 case 8: // 90 rotate left
443 // Logger::log('exif: ' . print_r($exif,true));
448 * @param integer $min minimum dimension
451 public function scaleUp($min)
453 if (!$this->isValid()) {
457 $width = $this->getWidth();
458 $height = $this->getHeight();
460 if ((!$width)|| (!$height)) {
464 if ($width < $min && $height < $min) {
465 if ($width > $height) {
467 $dest_height = intval(($height * $min) / $width);
469 $dest_width = intval(($width * $min) / $height);
475 $dest_height = intval(($height * $min) / $width);
477 if ($height < $min) {
478 $dest_width = intval(($width * $min) / $height);
481 $dest_width = $width;
482 $dest_height = $height;
487 return $this->scale($dest_width, $dest_height);
491 * @param integer $dim dimension
494 public function scaleToSquare($dim)
496 if (!$this->isValid()) {
500 return $this->scale($dim, $dim);
504 * Scale image to target dimensions
506 * @param int $dest_width
507 * @param int $dest_height
510 private function scale($dest_width, $dest_height)
512 if (!$this->isValid()) {
516 if ($this->isImagick()) {
518 * If it is not animated, there will be only one iteration here,
519 * so don't bother checking
521 // Don't forget to go back to the first frame
522 $this->image->setFirstIterator();
524 // FIXME - implement horizontal bias for scaling as in following GD functions
525 // to allow very tall images to be constrained only horizontally.
526 $this->image->scaleImage($dest_width, $dest_height);
527 } while ($this->image->nextImage());
529 // These may not be necessary anymore
530 $this->width = $this->image->getImageWidth();
531 $this->height = $this->image->getImageHeight();
533 $dest = imagecreatetruecolor($dest_width, $dest_height);
534 imagealphablending($dest, false);
535 imagesavealpha($dest, true);
537 if ($this->type=='image/png') {
538 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
541 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
544 imagedestroy($this->image);
547 $this->image = $dest;
548 $this->width = imagesx($this->image);
549 $this->height = imagesy($this->image);
556 * @param integer $max maximum
557 * @param integer $x x coordinate
558 * @param integer $y y coordinate
559 * @param integer $w width
560 * @param integer $h height
563 public function crop($max, $x, $y, $w, $h)
565 if (!$this->isValid()) {
569 if ($this->isImagick()) {
570 $this->image->setFirstIterator();
572 $this->image->cropImage($w, $h, $x, $y);
574 * We need to remove the canva,
575 * or the image is not resized to the crop:
576 * http://php.net/manual/en/imagick.cropimage.php#97232
578 $this->image->setImagePage(0, 0, 0, 0);
579 } while ($this->image->nextImage());
580 return $this->scaleDown($max);
583 $dest = imagecreatetruecolor($max, $max);
584 imagealphablending($dest, false);
585 imagesavealpha($dest, true);
586 if ($this->type=='image/png') {
587 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
589 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
591 imagedestroy($this->image);
593 $this->image = $dest;
594 $this->width = imagesx($this->image);
595 $this->height = imagesy($this->image);
599 * @param string $path file path
601 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
603 public function saveToFilePath($path)
605 if (!$this->isValid()) {
609 $string = $this->asString();
611 $stamp1 = microtime(true);
612 file_put_contents($path, $string);
613 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
617 * Magic method allowing string casting of an Image object
619 * Ex: $data = $Image->asString();
621 * $data = (string) $Image;
624 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
626 public function __toString() {
627 return $this->asString();
632 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
634 public function asString()
636 if (!$this->isValid()) {
640 if ($this->isImagick()) {
642 $this->image = $this->image->deconstructImages();
643 $string = $this->image->getImagesBlob();
649 // Enable interlacing
650 imageinterlace($this->image, true);
652 switch ($this->getType()) {
654 $quality = DI::config()->get('system', 'png_quality');
655 if ((!$quality) || ($quality > 9)) {
656 $quality = PNG_QUALITY;
658 imagepng($this->image, null, $quality);
661 $quality = DI::config()->get('system', 'jpeg_quality');
662 if ((!$quality) || ($quality > 100)) {
663 $quality = JPEG_QUALITY;
665 imagejpeg($this->image, null, $quality);
667 $string = ob_get_contents();
674 * 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 * Maps Mime types to Imagick formats
687 * @return array With with image formats (mime type as key)
688 * @deprecated in version 2019.12 please use Util\Images::getFormatsMap() instead.
690 public static function getFormatsMap()
692 return Images::getFormatsMap();
696 * Guess image mimetype from filename or from Content-Type header
698 * @param string $filename Image filename
699 * @param boolean $fromcurl Check Content-Type header from curl request
700 * @param string $header passed headers to take into account
702 * @return string|null
704 * @deprecated in version 2019.12 please use Util\Images::guessType() instead.
706 public static function guessType($filename, $fromcurl = false, $header = '')
708 return Images::guessType($filename, $fromcurl, $header);
712 * @param string $url url
714 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
715 * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
717 public static function getInfoFromURL($url)
719 return Images::getInfoFromURLCached($url);
723 * @param integer $width width
724 * @param integer $height height
725 * @param integer $max max
727 * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
729 public static function getScalingDimensions($width, $height, $max)
731 return Images::getScalingDimensions($width, $height, $max);