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