]> git.mxchange.org Git - friendica.git/blob - src/Object/Image.php
Merge pull request #8832 from annando/fix-db-error
[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                 return $exif;
460         }
461
462         /**
463          * @param integer $min minimum dimension
464          * @return mixed
465          */
466         public function scaleUp($min)
467         {
468                 if (!$this->isValid()) {
469                         return false;
470                 }
471
472                 $width = $this->getWidth();
473                 $height = $this->getHeight();
474
475                 if ((!$width)|| (!$height)) {
476                         return false;
477                 }
478
479                 if ($width < $min && $height < $min) {
480                         if ($width > $height) {
481                                 $dest_width = $min;
482                                 $dest_height = intval(($height * $min) / $width);
483                         } else {
484                                 $dest_width = intval(($width * $min) / $height);
485                                 $dest_height = $min;
486                         }
487                 } else {
488                         if ($width < $min) {
489                                 $dest_width = $min;
490                                 $dest_height = intval(($height * $min) / $width);
491                         } else {
492                                 if ($height < $min) {
493                                         $dest_width = intval(($width * $min) / $height);
494                                         $dest_height = $min;
495                                 } else {
496                                         $dest_width = $width;
497                                         $dest_height = $height;
498                                 }
499                         }
500                 }
501
502                 return $this->scale($dest_width, $dest_height);
503         }
504
505         /**
506          * @param integer $dim dimension
507          * @return mixed
508          */
509         public function scaleToSquare($dim)
510         {
511                 if (!$this->isValid()) {
512                         return false;
513                 }
514
515                 return $this->scale($dim, $dim);
516         }
517
518         /**
519          * Scale image to target dimensions
520          *
521          * @param int $dest_width
522          * @param int $dest_height
523          * @return boolean
524          */
525         private function scale($dest_width, $dest_height)
526         {
527                 if (!$this->isValid()) {
528                         return false;
529                 }
530
531                 if ($this->isImagick()) {
532                         /*
533                          * If it is not animated, there will be only one iteration here,
534                          * so don't bother checking
535                          */
536                         // Don't forget to go back to the first frame
537                         $this->image->setFirstIterator();
538                         do {
539                                 // FIXME - implement horizontal bias for scaling as in following GD functions
540                                 // to allow very tall images to be constrained only horizontally.
541                                 $this->image->scaleImage($dest_width, $dest_height);
542                         } while ($this->image->nextImage());
543
544                         // These may not be necessary anymore
545                         $this->width  = $this->image->getImageWidth();
546                         $this->height = $this->image->getImageHeight();
547                 } else {
548                         $dest = imagecreatetruecolor($dest_width, $dest_height);
549                         imagealphablending($dest, false);
550                         imagesavealpha($dest, true);
551
552                         if ($this->type=='image/png') {
553                                 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
554                         }
555
556                         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
557
558                         if ($this->image) {
559                                 imagedestroy($this->image);
560                         }
561
562                         $this->image = $dest;
563                         $this->width  = imagesx($this->image);
564                         $this->height = imagesy($this->image);
565                 }
566
567                 return true;
568         }
569
570         /**
571          * @param integer $max maximum
572          * @param integer $x   x coordinate
573          * @param integer $y   y coordinate
574          * @param integer $w   width
575          * @param integer $h   height
576          * @return mixed
577          */
578         public function crop($max, $x, $y, $w, $h)
579         {
580                 if (!$this->isValid()) {
581                         return false;
582                 }
583
584                 if ($this->isImagick()) {
585                         $this->image->setFirstIterator();
586                         do {
587                                 $this->image->cropImage($w, $h, $x, $y);
588                                 /*
589                                  * We need to remove the canva,
590                                  * or the image is not resized to the crop:
591                                  * http://php.net/manual/en/imagick.cropimage.php#97232
592                                  */
593                                 $this->image->setImagePage(0, 0, 0, 0);
594                         } while ($this->image->nextImage());
595                         return $this->scaleDown($max);
596                 }
597
598                 $dest = imagecreatetruecolor($max, $max);
599                 imagealphablending($dest, false);
600                 imagesavealpha($dest, true);
601                 if ($this->type=='image/png') {
602                         imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
603                 }
604                 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
605                 if ($this->image) {
606                         imagedestroy($this->image);
607                 }
608                 $this->image = $dest;
609                 $this->width  = imagesx($this->image);
610                 $this->height = imagesy($this->image);
611         }
612
613         /**
614          * @param string $path file path
615          * @return mixed
616          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
617          */
618         public function saveToFilePath($path)
619         {
620                 if (!$this->isValid()) {
621                         return false;
622                 }
623
624                 $string = $this->asString();
625
626                 $stamp1 = microtime(true);
627                 file_put_contents($path, $string);
628                 DI::profiler()->saveTimestamp($stamp1, "file", System::callstack());
629         }
630
631         /**
632          * Magic method allowing string casting of an Image object
633          *
634          * Ex: $data = $Image->asString();
635          * can be replaced by
636          * $data = (string) $Image;
637          *
638          * @return string
639          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
640          */
641         public function __toString() {
642                 return $this->asString();
643         }
644
645         /**
646          * @return mixed
647          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
648          */
649         public function asString()
650         {
651                 if (!$this->isValid()) {
652                         return false;
653                 }
654
655                 if ($this->isImagick()) {
656                         /* Clean it */
657                         $this->image = $this->image->deconstructImages();
658                         $string = $this->image->getImagesBlob();
659                         return $string;
660                 }
661
662                 ob_start();
663
664                 // Enable interlacing
665                 imageinterlace($this->image, true);
666
667                 switch ($this->getType()) {
668                         case "image/png":
669                                 $quality = DI::config()->get('system', 'png_quality');
670                                 if ((!$quality) || ($quality > 9)) {
671                                         $quality = PNG_QUALITY;
672                                 }
673                                 imagepng($this->image, null, $quality);
674                                 break;
675                         case "image/jpeg":
676                                 $quality = DI::config()->get('system', 'jpeg_quality');
677                                 if ((!$quality) || ($quality > 100)) {
678                                         $quality = JPEG_QUALITY;
679                                 }
680                                 imagejpeg($this->image, null, $quality);
681                 }
682                 $string = ob_get_contents();
683                 ob_end_clean();
684
685                 return $string;
686         }
687
688         /**
689          * supported mimetypes and corresponding file extensions
690          *
691          * @return array
692          * @deprecated in version 2019.12 please use Util\Images::supportedTypes() instead.
693          */
694         public static function supportedTypes()
695         {
696                 return Images::supportedTypes();
697         }
698
699         /**
700          * Maps Mime types to Imagick formats
701          *
702          * @return array With with image formats (mime type as key)
703          * @deprecated in version 2019.12 please use Util\Images::getFormatsMap() instead.
704          */
705         public static function getFormatsMap()
706         {
707                 return Images::getFormatsMap();
708         }
709
710         /**
711          * @param string $url url
712          * @return array
713          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
714          * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
715          */
716         public static function getInfoFromURL($url)
717         {
718                 return Images::getInfoFromURLCached($url);
719         }
720
721         /**
722          * @param integer $width  width
723          * @param integer $height height
724          * @param integer $max    max
725          * @return array
726          * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
727          */
728         public static function getScalingDimensions($width, $height, $max)
729         {
730                 return Images::getScalingDimensions($width, $height, $max);
731         }
732 }