]> git.mxchange.org Git - friendica.git/blob - src/Object/Image.php
Group selection: Respect "pubmail" and ignore atchived or blocked contacts
[friendica.git] / src / Object / Image.php
1 <?php
2 /**
3  * @file src/Object/Image.php
4  * @brief This file contains the Image class for image processing
5  */
6 namespace Friendica\Object;
7
8 use Exception;
9 use Friendica\Core\Config;
10 use Friendica\Core\System;
11 use Friendica\Util\Images;
12 use Imagick;
13 use ImagickPixel;
14
15 /**
16  * Class to handle images
17  */
18 class Image
19 {
20         /** @var Imagick|resource */
21         private $image;
22
23         /*
24          * Put back gd stuff, not everybody have Imagick
25          */
26         private $imagick;
27         private $width;
28         private $height;
29         private $valid;
30         private $type;
31         private $types;
32
33         /**
34          * @brief Constructor
35          * @param string  $data
36          * @param boolean $type optional, default null
37          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
38          * @throws \ImagickException
39          */
40         public function __construct($data, $type = null)
41         {
42                 $this->imagick = class_exists('Imagick');
43                 $this->types = Images::supportedTypes();
44                 if (!array_key_exists($type, $this->types)) {
45                         $type = 'image/jpeg';
46                 }
47                 $this->type = $type;
48
49                 if ($this->isImagick() && $this->loadData($data)) {
50                         return true;
51                 } else {
52                         // Failed to load with Imagick, fallback
53                         $this->imagick = false;
54                 }
55                 return $this->loadData($data);
56         }
57
58         /**
59          * @brief Destructor
60          * @return void
61          */
62         public function __destruct()
63         {
64                 if ($this->image) {
65                         if ($this->isImagick()) {
66                                 $this->image->clear();
67                                 $this->image->destroy();
68                                 return;
69                         }
70                         if (is_resource($this->image)) {
71                                 imagedestroy($this->image);
72                         }
73                 }
74         }
75
76         /**
77          * @return boolean
78          */
79         public function isImagick()
80         {
81                 return $this->imagick;
82         }
83
84         /**
85          * @param string $data data
86          * @return boolean
87          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
88          * @throws \ImagickException
89          */
90         private function loadData($data)
91         {
92                 if ($this->isImagick()) {
93                         $this->image = new Imagick();
94                         try {
95                                 $this->image->readImageBlob($data);
96                         } catch (Exception $e) {
97                                 // Imagick couldn't use the data
98                                 return false;
99                         }
100
101                         /*
102                          * Setup the image to the format it will be saved to
103                          */
104                         $map = Images::getFormatsMap();
105                         $format = $map[$this->type];
106                         $this->image->setFormat($format);
107
108                         // Always coalesce, if it is not a multi-frame image it won't hurt anyway
109                         $this->image = $this->image->coalesceImages();
110
111                         /*
112                          * setup the compression here, so we'll do it only once
113                          */
114                         switch ($this->getType()) {
115                                 case "image/png":
116                                         $quality = Config::get('system', 'png_quality');
117                                         if ((! $quality) || ($quality > 9)) {
118                                                 $quality = PNG_QUALITY;
119                                         }
120                                         /*
121                                          * From http://www.imagemagick.org/script/command-line-options.php#quality:
122                                          *
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'
127                                          */
128                                         $quality = $quality * 10;
129                                         $this->image->setCompressionQuality($quality);
130                                         break;
131                                 case "image/jpeg":
132                                         $quality = Config::get('system', 'jpeg_quality');
133                                         if ((! $quality) || ($quality > 100)) {
134                                                 $quality = JPEG_QUALITY;
135                                         }
136                                         $this->image->setCompressionQuality($quality);
137                         }
138
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();
142                         $this->valid  = true;
143
144                         return true;
145                 }
146
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);
152                         $this->valid  = true;
153                         imagealphablending($this->image, false);
154                         imagesavealpha($this->image, true);
155
156                         return true;
157                 }
158
159                 return false;
160         }
161
162         /**
163          * @return boolean
164          */
165         public function isValid()
166         {
167                 if ($this->isImagick()) {
168                         return ($this->image !== false);
169                 }
170                 return $this->valid;
171         }
172
173         /**
174          * @return mixed
175          */
176         public function getWidth()
177         {
178                 if (!$this->isValid()) {
179                         return false;
180                 }
181
182                 if ($this->isImagick()) {
183                         return $this->image->getImageWidth();
184                 }
185                 return $this->width;
186         }
187
188         /**
189          * @return mixed
190          */
191         public function getHeight()
192         {
193                 if (!$this->isValid()) {
194                         return false;
195                 }
196
197                 if ($this->isImagick()) {
198                         return $this->image->getImageHeight();
199                 }
200                 return $this->height;
201         }
202
203         /**
204          * @return mixed
205          */
206         public function getImage()
207         {
208                 if (!$this->isValid()) {
209                         return false;
210                 }
211
212                 if ($this->isImagick()) {
213                         /* Clean it */
214                         $this->image = $this->image->deconstructImages();
215                         return $this->image;
216                 }
217                 return $this->image;
218         }
219
220         /**
221          * @return mixed
222          */
223         public function getType()
224         {
225                 if (!$this->isValid()) {
226                         return false;
227                 }
228
229                 return $this->type;
230         }
231
232         /**
233          * @return mixed
234          */
235         public function getExt()
236         {
237                 if (!$this->isValid()) {
238                         return false;
239                 }
240
241                 return $this->types[$this->getType()];
242         }
243
244         /**
245          * @param integer $max max dimension
246          * @return mixed
247          */
248         public function scaleDown($max)
249         {
250                 if (!$this->isValid()) {
251                         return false;
252                 }
253
254                 $width = $this->getWidth();
255                 $height = $this->getHeight();
256
257                 if ((! $width)|| (! $height)) {
258                         return false;
259                 }
260
261                 if ($width > $max && $height > $max) {
262                         // very tall image (greater than 16:9)
263                         // constrain the width - let the height float.
264
265                         if ((($height * 9) / 16) > $width) {
266                                 $dest_width = $max;
267                                 $dest_height = intval(($height * $max) / $width);
268                         } elseif ($width > $height) {
269                                 // else constrain both dimensions
270                                 $dest_width = $max;
271                                 $dest_height = intval(($height * $max) / $width);
272                         } else {
273                                 $dest_width = intval(($width * $max) / $height);
274                                 $dest_height = $max;
275                         }
276                 } else {
277                         if ($width > $max) {
278                                 $dest_width = $max;
279                                 $dest_height = intval(($height * $max) / $width);
280                         } else {
281                                 if ($height > $max) {
282                                         // very tall image (greater than 16:9)
283                                         // but width is OK - don't do anything
284
285                                         if ((($height * 9) / 16) > $width) {
286                                                 $dest_width = $width;
287                                                 $dest_height = $height;
288                                         } else {
289                                                 $dest_width = intval(($width * $max) / $height);
290                                                 $dest_height = $max;
291                                         }
292                                 } else {
293                                         $dest_width = $width;
294                                         $dest_height = $height;
295                                 }
296                         }
297                 }
298
299                 return $this->scale($dest_width, $dest_height);
300         }
301
302         /**
303          * @param integer $degrees degrees to rotate image
304          * @return mixed
305          */
306         public function rotate($degrees)
307         {
308                 if (!$this->isValid()) {
309                         return false;
310                 }
311
312                 if ($this->isImagick()) {
313                         $this->image->setFirstIterator();
314                         do {
315                                 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
316                         } while ($this->image->nextImage());
317                         return;
318                 }
319
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);
324         }
325
326         /**
327          * @param boolean $horiz optional, default true
328          * @param boolean $vert  optional, default false
329          * @return mixed
330          */
331         public function flip($horiz = true, $vert = false)
332         {
333                 if (!$this->isValid()) {
334                         return false;
335                 }
336
337                 if ($this->isImagick()) {
338                         $this->image->setFirstIterator();
339                         do {
340                                 if ($horiz) {
341                                         $this->image->flipImage();
342                                 }
343                                 if ($vert) {
344                                         $this->image->flopImage();
345                                 }
346                         } while ($this->image->nextImage());
347                         return;
348                 }
349
350                 $w = imagesx($this->image);
351                 $h = imagesy($this->image);
352                 $flipped = imagecreate($w, $h);
353                 if ($horiz) {
354                         for ($x = 0; $x < $w; $x++) {
355                                 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
356                         }
357                 }
358                 if ($vert) {
359                         for ($y = 0; $y < $h; $y++) {
360                                 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
361                         }
362                 }
363                 $this->image = $flipped;
364         }
365
366         /**
367          * @param string $filename filename
368          * @return mixed
369          */
370         public function orient($filename)
371         {
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);
378                                         break;
379                                 case Imagick::ORIENTATION_RIGHTTOP:
380                                         $this->image->rotateimage("#000", 90);
381                                         break;
382                                 case Imagick::ORIENTATION_LEFTBOTTOM:
383                                         $this->image->rotateimage("#000", -90);
384                                         break;
385                         }
386
387                         $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
388                         return true;
389                 }
390                 // based off comment on http://php.net/manual/en/function.imagerotate.php
391
392                 if (!$this->isValid()) {
393                         return false;
394                 }
395
396                 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
397                         return;
398                 }
399
400                 $exif = @exif_read_data($filename, null, true);
401                 if (!$exif) {
402                         return;
403                 }
404
405                 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
406
407                 switch ($ort) {
408                         case 1: // nothing
409                                 break;
410
411                         case 2: // horizontal flip
412                                 $this->flip();
413                                 break;
414
415                         case 3: // 180 rotate left
416                                 $this->rotate(180);
417                                 break;
418
419                         case 4: // vertical flip
420                                 $this->flip(false, true);
421                                 break;
422
423                         case 5: // vertical flip + 90 rotate right
424                                 $this->flip(false, true);
425                                 $this->rotate(-90);
426                                 break;
427
428                         case 6: // 90 rotate right
429                                 $this->rotate(-90);
430                                 break;
431
432                         case 7: // horizontal flip + 90 rotate right
433                                 $this->flip();
434                                 $this->rotate(-90);
435                                 break;
436
437                         case 8: // 90 rotate left
438                                 $this->rotate(90);
439                                 break;
440                 }
441
442                 //      Logger::log('exif: ' . print_r($exif,true));
443                 return $exif;
444         }
445
446         /**
447          * @param integer $min minimum dimension
448          * @return mixed
449          */
450         public function scaleUp($min)
451         {
452                 if (!$this->isValid()) {
453                         return false;
454                 }
455
456                 $width = $this->getWidth();
457                 $height = $this->getHeight();
458
459                 if ((!$width)|| (!$height)) {
460                         return false;
461                 }
462
463                 if ($width < $min && $height < $min) {
464                         if ($width > $height) {
465                                 $dest_width = $min;
466                                 $dest_height = intval(($height * $min) / $width);
467                         } else {
468                                 $dest_width = intval(($width * $min) / $height);
469                                 $dest_height = $min;
470                         }
471                 } else {
472                         if ($width < $min) {
473                                 $dest_width = $min;
474                                 $dest_height = intval(($height * $min) / $width);
475                         } else {
476                                 if ($height < $min) {
477                                         $dest_width = intval(($width * $min) / $height);
478                                         $dest_height = $min;
479                                 } else {
480                                         $dest_width = $width;
481                                         $dest_height = $height;
482                                 }
483                         }
484                 }
485
486                 return $this->scale($dest_width, $dest_height);
487         }
488
489         /**
490          * @param integer $dim dimension
491          * @return mixed
492          */
493         public function scaleToSquare($dim)
494         {
495                 if (!$this->isValid()) {
496                         return false;
497                 }
498
499                 return $this->scale($dim, $dim);
500         }
501
502         /**
503          * @brief Scale image to target dimensions
504          *
505          * @param int $dest_width
506          * @param int $dest_height
507          * @return boolean
508          */
509         private function scale($dest_width, $dest_height)
510         {
511                 if (!$this->isValid()) {
512                         return false;
513                 }
514
515                 if ($this->isImagick()) {
516                         /*
517                          * If it is not animated, there will be only one iteration here,
518                          * so don't bother checking
519                          */
520                         // Don't forget to go back to the first frame
521                         $this->image->setFirstIterator();
522                         do {
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());
527
528                         // These may not be necessary anymore
529                         $this->width  = $this->image->getImageWidth();
530                         $this->height = $this->image->getImageHeight();
531                 } else {
532                         $dest = imagecreatetruecolor($dest_width, $dest_height);
533                         imagealphablending($dest, false);
534                         imagesavealpha($dest, true);
535
536                         if ($this->type=='image/png') {
537                                 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
538                         }
539
540                         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
541
542                         if ($this->image) {
543                                 imagedestroy($this->image);
544                         }
545
546                         $this->image = $dest;
547                         $this->width  = imagesx($this->image);
548                         $this->height = imagesy($this->image);
549                 }
550
551                 return true;
552         }
553
554         /**
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
560          * @return mixed
561          */
562         public function crop($max, $x, $y, $w, $h)
563         {
564                 if (!$this->isValid()) {
565                         return false;
566                 }
567
568                 if ($this->isImagick()) {
569                         $this->image->setFirstIterator();
570                         do {
571                                 $this->image->cropImage($w, $h, $x, $y);
572                                 /*
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
576                                  */
577                                 $this->image->setImagePage(0, 0, 0, 0);
578                         } while ($this->image->nextImage());
579                         return $this->scaleDown($max);
580                 }
581
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
587                 }
588                 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
589                 if ($this->image) {
590                         imagedestroy($this->image);
591                 }
592                 $this->image = $dest;
593                 $this->width  = imagesx($this->image);
594                 $this->height = imagesy($this->image);
595         }
596
597         /**
598          * @param string $path file path
599          * @return mixed
600          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
601          */
602         public function saveToFilePath($path)
603         {
604                 if (!$this->isValid()) {
605                         return false;
606                 }
607
608                 $string = $this->asString();
609
610                 $a = \get_app();
611
612                 $stamp1 = microtime(true);
613                 file_put_contents($path, $string);
614                 $a->getProfiler()->saveTimestamp($stamp1, "file", System::callstack());
615         }
616
617         /**
618          * @brief Magic method allowing string casting of an Image object
619          *
620          * Ex: $data = $Image->asString();
621          * can be replaced by
622          * $data = (string) $Image;
623          *
624          * @return string
625          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
626          */
627         public function __toString() {
628                 return $this->asString();
629         }
630
631         /**
632          * @return mixed
633          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
634          */
635         public function asString()
636         {
637                 if (!$this->isValid()) {
638                         return false;
639                 }
640
641                 if ($this->isImagick()) {
642                         /* Clean it */
643                         $this->image = $this->image->deconstructImages();
644                         $string = $this->image->getImagesBlob();
645                         return $string;
646                 }
647
648                 ob_start();
649
650                 // Enable interlacing
651                 imageinterlace($this->image, true);
652
653                 switch ($this->getType()) {
654                         case "image/png":
655                                 $quality = Config::get('system', 'png_quality');
656                                 if ((!$quality) || ($quality > 9)) {
657                                         $quality = PNG_QUALITY;
658                                 }
659                                 imagepng($this->image, null, $quality);
660                                 break;
661                         case "image/jpeg":
662                                 $quality = Config::get('system', 'jpeg_quality');
663                                 if ((!$quality) || ($quality > 100)) {
664                                         $quality = JPEG_QUALITY;
665                                 }
666                                 imagejpeg($this->image, null, $quality);
667                 }
668                 $string = ob_get_contents();
669                 ob_end_clean();
670
671                 return $string;
672         }
673
674         /**
675          * @brief supported mimetypes and corresponding file extensions
676          * @return array
677          * @deprecated in version 2019.12 please use Util\Images::supportedTypes() instead.
678          */
679         public static function supportedTypes()
680         {
681                 return Images::supportedTypes();
682         }
683
684         /**
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.
688          */
689         public static function getFormatsMap()
690         {
691                 return Images::getFormatsMap();
692         }
693
694         /**
695          * Guess image mimetype from filename or from Content-Type header
696          *
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
700          *
701          * @return string|null
702          * @throws Exception
703          * @deprecated in version 2019.12 please use Util\Images::guessType() instead.
704          */
705         public static function guessType($filename, $fromcurl = false, $header = '')
706         {
707                 return Images::guessType($filename, $fromcurl, $header);
708         }
709
710         /**
711          * @param string $url url
712          * @return array
713          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
714          * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
715          */
716         public static function getInfoFromURL($url)
717         {
718                 return Images::getInfoFromURLCached($url);
719         }
720
721         /**
722          * @param integer $width  width
723          * @param integer $height height
724          * @param integer $max    max
725          * @return array
726          * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
727          */
728         public static function getScalingDimensions($width, $height, $max)
729         {
730                 return Images::getScalingDimensions($width, $height, $max);
731         }
732 }