]> git.mxchange.org Git - friendica.git/blob - src/Object/Image.php
Fix: The "extid" field wasn't updated
[friendica.git] / src / Object / Image.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2021, the Friendica project
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                         try {
234                                 /* Clean it */
235                                 $this->image = $this->image->deconstructImages();
236                                 return $this->image;
237                         } catch (Exception $e) {
238                                 return false;
239                         }
240                 }
241                 return $this->image;
242         }
243
244         /**
245          * @return mixed
246          */
247         public function getType()
248         {
249                 if (!$this->isValid()) {
250                         return false;
251                 }
252
253                 return $this->type;
254         }
255
256         /**
257          * @return mixed
258          */
259         public function getExt()
260         {
261                 if (!$this->isValid()) {
262                         return false;
263                 }
264
265                 return $this->types[$this->getType()];
266         }
267
268         /**
269          * @param integer $max max dimension
270          * @return mixed
271          */
272         public function scaleDown($max)
273         {
274                 if (!$this->isValid()) {
275                         return false;
276                 }
277
278                 $width = $this->getWidth();
279                 $height = $this->getHeight();
280
281                 if ((! $width)|| (! $height)) {
282                         return false;
283                 }
284
285                 if ($width > $max && $height > $max) {
286                         // very tall image (greater than 16:9)
287                         // constrain the width - let the height float.
288
289                         if ((($height * 9) / 16) > $width) {
290                                 $dest_width = $max;
291                                 $dest_height = intval(($height * $max) / $width);
292                         } elseif ($width > $height) {
293                                 // else constrain both dimensions
294                                 $dest_width = $max;
295                                 $dest_height = intval(($height * $max) / $width);
296                         } else {
297                                 $dest_width = intval(($width * $max) / $height);
298                                 $dest_height = $max;
299                         }
300                 } else {
301                         if ($width > $max) {
302                                 $dest_width = $max;
303                                 $dest_height = intval(($height * $max) / $width);
304                         } else {
305                                 if ($height > $max) {
306                                         // very tall image (greater than 16:9)
307                                         // but width is OK - don't do anything
308
309                                         if ((($height * 9) / 16) > $width) {
310                                                 $dest_width = $width;
311                                                 $dest_height = $height;
312                                         } else {
313                                                 $dest_width = intval(($width * $max) / $height);
314                                                 $dest_height = $max;
315                                         }
316                                 } else {
317                                         $dest_width = $width;
318                                         $dest_height = $height;
319                                 }
320                         }
321                 }
322
323                 return $this->scale($dest_width, $dest_height);
324         }
325
326         /**
327          * @param integer $degrees degrees to rotate image
328          * @return mixed
329          */
330         public function rotate($degrees)
331         {
332                 if (!$this->isValid()) {
333                         return false;
334                 }
335
336                 if ($this->isImagick()) {
337                         $this->image->setFirstIterator();
338                         do {
339                                 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
340                         } while ($this->image->nextImage());
341                         return;
342                 }
343
344                 // if script dies at this point check memory_limit setting in php.ini
345                 $this->image  = imagerotate($this->image, $degrees, 0);
346                 $this->width  = imagesx($this->image);
347                 $this->height = imagesy($this->image);
348         }
349
350         /**
351          * @param boolean $horiz optional, default true
352          * @param boolean $vert  optional, default false
353          * @return mixed
354          */
355         public function flip($horiz = true, $vert = false)
356         {
357                 if (!$this->isValid()) {
358                         return false;
359                 }
360
361                 if ($this->isImagick()) {
362                         $this->image->setFirstIterator();
363                         do {
364                                 if ($horiz) {
365                                         $this->image->flipImage();
366                                 }
367                                 if ($vert) {
368                                         $this->image->flopImage();
369                                 }
370                         } while ($this->image->nextImage());
371                         return;
372                 }
373
374                 $w = imagesx($this->image);
375                 $h = imagesy($this->image);
376                 $flipped = imagecreate($w, $h);
377                 if ($horiz) {
378                         for ($x = 0; $x < $w; $x++) {
379                                 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
380                         }
381                 }
382                 if ($vert) {
383                         for ($y = 0; $y < $h; $y++) {
384                                 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
385                         }
386                 }
387                 $this->image = $flipped;
388         }
389
390         /**
391          * @param string $filename filename
392          * @return mixed
393          */
394         public function orient($filename)
395         {
396                 if ($this->isImagick()) {
397                         // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
398                         $orientation = $this->image->getImageOrientation();
399                         switch ($orientation) {
400                                 case Imagick::ORIENTATION_BOTTOMRIGHT:
401                                         $this->image->rotateimage("#000", 180);
402                                         break;
403                                 case Imagick::ORIENTATION_RIGHTTOP:
404                                         $this->image->rotateimage("#000", 90);
405                                         break;
406                                 case Imagick::ORIENTATION_LEFTBOTTOM:
407                                         $this->image->rotateimage("#000", -90);
408                                         break;
409                         }
410
411                         $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
412                         return true;
413                 }
414                 // based off comment on http://php.net/manual/en/function.imagerotate.php
415
416                 if (!$this->isValid()) {
417                         return false;
418                 }
419
420                 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
421                         return;
422                 }
423
424                 $exif = @exif_read_data($filename, null, true);
425                 if (!$exif) {
426                         return;
427                 }
428
429                 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
430
431                 switch ($ort) {
432                         case 1: // nothing
433                                 break;
434
435                         case 2: // horizontal flip
436                                 $this->flip();
437                                 break;
438
439                         case 3: // 180 rotate left
440                                 $this->rotate(180);
441                                 break;
442
443                         case 4: // vertical flip
444                                 $this->flip(false, true);
445                                 break;
446
447                         case 5: // vertical flip + 90 rotate right
448                                 $this->flip(false, true);
449                                 $this->rotate(-90);
450                                 break;
451
452                         case 6: // 90 rotate right
453                                 $this->rotate(-90);
454                                 break;
455
456                         case 7: // horizontal flip + 90 rotate right
457                                 $this->flip();
458                                 $this->rotate(-90);
459                                 break;
460
461                         case 8: // 90 rotate left
462                                 $this->rotate(90);
463                                 break;
464                 }
465
466                 return $exif;
467         }
468
469         /**
470          * @param integer $min minimum dimension
471          * @return mixed
472          */
473         public function scaleUp($min)
474         {
475                 if (!$this->isValid()) {
476                         return false;
477                 }
478
479                 $width = $this->getWidth();
480                 $height = $this->getHeight();
481
482                 if ((!$width)|| (!$height)) {
483                         return false;
484                 }
485
486                 if ($width < $min && $height < $min) {
487                         if ($width > $height) {
488                                 $dest_width = $min;
489                                 $dest_height = intval(($height * $min) / $width);
490                         } else {
491                                 $dest_width = intval(($width * $min) / $height);
492                                 $dest_height = $min;
493                         }
494                 } else {
495                         if ($width < $min) {
496                                 $dest_width = $min;
497                                 $dest_height = intval(($height * $min) / $width);
498                         } else {
499                                 if ($height < $min) {
500                                         $dest_width = intval(($width * $min) / $height);
501                                         $dest_height = $min;
502                                 } else {
503                                         $dest_width = $width;
504                                         $dest_height = $height;
505                                 }
506                         }
507                 }
508
509                 return $this->scale($dest_width, $dest_height);
510         }
511
512         /**
513          * @param integer $dim dimension
514          * @return mixed
515          */
516         public function scaleToSquare($dim)
517         {
518                 if (!$this->isValid()) {
519                         return false;
520                 }
521
522                 return $this->scale($dim, $dim);
523         }
524
525         /**
526          * Scale image to target dimensions
527          *
528          * @param int $dest_width
529          * @param int $dest_height
530          * @return boolean
531          */
532         private function scale($dest_width, $dest_height)
533         {
534                 if (!$this->isValid()) {
535                         return false;
536                 }
537
538                 if ($this->isImagick()) {
539                         /*
540                          * If it is not animated, there will be only one iteration here,
541                          * so don't bother checking
542                          */
543                         // Don't forget to go back to the first frame
544                         $this->image->setFirstIterator();
545                         do {
546                                 // FIXME - implement horizontal bias for scaling as in following GD functions
547                                 // to allow very tall images to be constrained only horizontally.
548                                 try {
549                                         $this->image->scaleImage($dest_width, $dest_height);
550                                 } catch (Exception $e) {
551                                         // Imagick couldn't use the data
552                                         return false;
553                                 }
554                         } while ($this->image->nextImage());
555
556                         // These may not be necessary anymore
557                         $this->width  = $this->image->getImageWidth();
558                         $this->height = $this->image->getImageHeight();
559                 } else {
560                         $dest = imagecreatetruecolor($dest_width, $dest_height);
561                         imagealphablending($dest, false);
562                         imagesavealpha($dest, true);
563
564                         if ($this->type=='image/png') {
565                                 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
566                         }
567
568                         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
569
570                         if ($this->image) {
571                                 imagedestroy($this->image);
572                         }
573
574                         $this->image = $dest;
575                         $this->width  = imagesx($this->image);
576                         $this->height = imagesy($this->image);
577                 }
578
579                 return true;
580         }
581
582         /**
583          * Convert a GIF to a PNG to make it static
584          */
585         public function toStatic()
586         {
587                 if ($this->type != 'image/gif') {
588                         return;
589                 }
590
591                 if ($this->isImagick()) {
592                         $this->type == 'image/png';
593                         $this->image->setFormat('png');
594                 }
595         }
596
597         /**
598          * @param integer $max maximum
599          * @param integer $x   x coordinate
600          * @param integer $y   y coordinate
601          * @param integer $w   width
602          * @param integer $h   height
603          * @return mixed
604          */
605         public function crop($max, $x, $y, $w, $h)
606         {
607                 if (!$this->isValid()) {
608                         return false;
609                 }
610
611                 if ($this->isImagick()) {
612                         $this->image->setFirstIterator();
613                         do {
614                                 $this->image->cropImage($w, $h, $x, $y);
615                                 /*
616                                  * We need to remove the canva,
617                                  * or the image is not resized to the crop:
618                                  * http://php.net/manual/en/imagick.cropimage.php#97232
619                                  */
620                                 $this->image->setImagePage(0, 0, 0, 0);
621                         } while ($this->image->nextImage());
622                         return $this->scaleDown($max);
623                 }
624
625                 $dest = imagecreatetruecolor($max, $max);
626                 imagealphablending($dest, false);
627                 imagesavealpha($dest, true);
628                 if ($this->type=='image/png') {
629                         imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
630                 }
631                 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
632                 if ($this->image) {
633                         imagedestroy($this->image);
634                 }
635                 $this->image = $dest;
636                 $this->width  = imagesx($this->image);
637                 $this->height = imagesy($this->image);
638         }
639
640         /**
641          * @param string $path file path
642          * @return mixed
643          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
644          */
645         public function saveToFilePath($path)
646         {
647                 if (!$this->isValid()) {
648                         return false;
649                 }
650
651                 $string = $this->asString();
652
653                 $stamp1 = microtime(true);
654                 file_put_contents($path, $string);
655                 DI::profiler()->saveTimestamp($stamp1, "file");
656         }
657
658         /**
659          * Magic method allowing string casting of an Image object
660          *
661          * Ex: $data = $Image->asString();
662          * can be replaced by
663          * $data = (string) $Image;
664          *
665          * @return string
666          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
667          */
668         public function __toString() {
669                 return $this->asString();
670         }
671
672         /**
673          * @return mixed
674          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
675          */
676         public function asString()
677         {
678                 if (!$this->isValid()) {
679                         return false;
680                 }
681
682                 if ($this->isImagick()) {
683                         try {
684                                 /* Clean it */
685                                 $this->image = $this->image->deconstructImages();
686                                 $string = $this->image->getImagesBlob();
687                                 return $string;
688                         } catch (Exception $e) {
689                                 return false;
690                         }
691                 }
692
693                 ob_start();
694
695                 // Enable interlacing
696                 imageinterlace($this->image, true);
697
698                 switch ($this->getType()) {
699                         case "image/png":
700                                 $quality = DI::config()->get('system', 'png_quality');
701                                 if ((!$quality) || ($quality > 9)) {
702                                         $quality = PNG_QUALITY;
703                                 }
704                                 imagepng($this->image, null, $quality);
705                                 break;
706                         case "image/jpeg":
707                                 $quality = DI::config()->get('system', 'jpeg_quality');
708                                 if ((!$quality) || ($quality > 100)) {
709                                         $quality = JPEG_QUALITY;
710                                 }
711                                 imagejpeg($this->image, null, $quality);
712                 }
713                 $string = ob_get_contents();
714                 ob_end_clean();
715
716                 return $string;
717         }
718
719         /**
720          * supported mimetypes and corresponding file extensions
721          *
722          * @return array
723          * @deprecated in version 2019.12 please use Util\Images::supportedTypes() instead.
724          */
725         public static function supportedTypes()
726         {
727                 return Images::supportedTypes();
728         }
729
730         /**
731          * Maps Mime types to Imagick formats
732          *
733          * @return array With with image formats (mime type as key)
734          * @deprecated in version 2019.12 please use Util\Images::getFormatsMap() instead.
735          */
736         public static function getFormatsMap()
737         {
738                 return Images::getFormatsMap();
739         }
740
741         /**
742          * @param string $url url
743          * @return array
744          * @throws \Friendica\Network\HTTPException\InternalServerErrorException
745          * @deprecated in version 2019.12 please use Util\Images::getInfoFromURLCached() instead.
746          */
747         public static function getInfoFromURL($url)
748         {
749                 return Images::getInfoFromURLCached($url);
750         }
751
752         /**
753          * @param integer $width  width
754          * @param integer $height height
755          * @param integer $max    max
756          * @return array
757          * @deprecated in version 2019.12 please use Util\Images::getScalingDimensions() instead.
758          */
759         public static function getScalingDimensions($width, $height, $max)
760         {
761                 return Images::getScalingDimensions($width, $height, $max);
762         }
763 }