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