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