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