]> git.mxchange.org Git - friendica.git/blob - src/Object/Image.php
Fix test
[friendica.git] / src / Object / Image.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2022, 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                                         /*
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                                         $this->image->setCompressionQuality($quality);
151                         }
152
153                         // The 'width' and 'height' properties are only used by non-Imagick routines.
154                         $this->width  = $this->image->getImageWidth();
155                         $this->height = $this->image->getImageHeight();
156                         $this->valid  = true;
157
158                         return true;
159                 }
160
161                 $this->valid = false;
162                 $this->image = @imagecreatefromstring($data);
163                 if ($this->image !== false) {
164                         $this->width  = imagesx($this->image);
165                         $this->height = imagesy($this->image);
166                         $this->valid  = true;
167                         imagealphablending($this->image, false);
168                         imagesavealpha($this->image, true);
169
170                         return true;
171                 }
172
173                 return false;
174         }
175
176         /**
177          * @return boolean
178          */
179         public function isValid()
180         {
181                 if ($this->isImagick()) {
182                         return ($this->image !== false);
183                 }
184                 return $this->valid;
185         }
186
187         /**
188          * @return mixed
189          */
190         public function getWidth()
191         {
192                 if (!$this->isValid()) {
193                         return false;
194                 }
195
196                 if ($this->isImagick()) {
197                         return $this->image->getImageWidth();
198                 }
199                 return $this->width;
200         }
201
202         /**
203          * @return mixed
204          */
205         public function getHeight()
206         {
207                 if (!$this->isValid()) {
208                         return false;
209                 }
210
211                 if ($this->isImagick()) {
212                         return $this->image->getImageHeight();
213                 }
214                 return $this->height;
215         }
216
217         /**
218          * @return mixed
219          */
220         public function getImage()
221         {
222                 if (!$this->isValid()) {
223                         return false;
224                 }
225
226                 if ($this->isImagick()) {
227                         try {
228                                 /* Clean it */
229                                 $this->image = $this->image->deconstructImages();
230                                 return $this->image;
231                         } catch (Exception $e) {
232                                 return false;
233                         }
234                 }
235                 return $this->image;
236         }
237
238         /**
239          * @return mixed
240          */
241         public function getType()
242         {
243                 if (!$this->isValid()) {
244                         return false;
245                 }
246
247                 return $this->type;
248         }
249
250         /**
251          * @return mixed
252          */
253         public function getExt()
254         {
255                 if (!$this->isValid()) {
256                         return false;
257                 }
258
259                 return $this->types[$this->getType()];
260         }
261
262         /**
263          * @param integer $max max dimension
264          * @return mixed
265          */
266         public function scaleDown($max)
267         {
268                 if (!$this->isValid()) {
269                         return false;
270                 }
271
272                 $width = $this->getWidth();
273                 $height = $this->getHeight();
274
275                 if ((! $width)|| (! $height)) {
276                         return false;
277                 }
278
279                 if ($width > $max && $height > $max) {
280                         // very tall image (greater than 16:9)
281                         // constrain the width - let the height float.
282
283                         if ((($height * 9) / 16) > $width) {
284                                 $dest_width = $max;
285                                 $dest_height = intval(($height * $max) / $width);
286                         } elseif ($width > $height) {
287                                 // else constrain both dimensions
288                                 $dest_width = $max;
289                                 $dest_height = intval(($height * $max) / $width);
290                         } else {
291                                 $dest_width = intval(($width * $max) / $height);
292                                 $dest_height = $max;
293                         }
294                 } else {
295                         if ($width > $max) {
296                                 $dest_width = $max;
297                                 $dest_height = intval(($height * $max) / $width);
298                         } else {
299                                 if ($height > $max) {
300                                         // very tall image (greater than 16:9)
301                                         // but width is OK - don't do anything
302
303                                         if ((($height * 9) / 16) > $width) {
304                                                 $dest_width = $width;
305                                                 $dest_height = $height;
306                                         } else {
307                                                 $dest_width = intval(($width * $max) / $height);
308                                                 $dest_height = $max;
309                                         }
310                                 } else {
311                                         $dest_width = $width;
312                                         $dest_height = $height;
313                                 }
314                         }
315                 }
316
317                 return $this->scale($dest_width, $dest_height);
318         }
319
320         /**
321          * @param integer $degrees degrees to rotate image
322          * @return mixed
323          */
324         public function rotate($degrees)
325         {
326                 if (!$this->isValid()) {
327                         return false;
328                 }
329
330                 if ($this->isImagick()) {
331                         $this->image->setFirstIterator();
332                         do {
333                                 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
334                         } while ($this->image->nextImage());
335                         return;
336                 }
337
338                 // if script dies at this point check memory_limit setting in php.ini
339                 $this->image  = imagerotate($this->image, $degrees, 0);
340                 $this->width  = imagesx($this->image);
341                 $this->height = imagesy($this->image);
342         }
343
344         /**
345          * @param boolean $horiz optional, default true
346          * @param boolean $vert  optional, default false
347          * @return mixed
348          */
349         public function flip($horiz = true, $vert = false)
350         {
351                 if (!$this->isValid()) {
352                         return false;
353                 }
354
355                 if ($this->isImagick()) {
356                         $this->image->setFirstIterator();
357                         do {
358                                 if ($horiz) {
359                                         $this->image->flipImage();
360                                 }
361                                 if ($vert) {
362                                         $this->image->flopImage();
363                                 }
364                         } while ($this->image->nextImage());
365                         return;
366                 }
367
368                 $w = imagesx($this->image);
369                 $h = imagesy($this->image);
370                 $flipped = imagecreate($w, $h);
371                 if ($horiz) {
372                         for ($x = 0; $x < $w; $x++) {
373                                 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
374                         }
375                 }
376                 if ($vert) {
377                         for ($y = 0; $y < $h; $y++) {
378                                 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
379                         }
380                 }
381                 $this->image = $flipped;
382         }
383
384         /**
385          * @param string $filename filename
386          * @return mixed
387          */
388         public function orient($filename)
389         {
390                 if ($this->isImagick()) {
391                         // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
392                         $orientation = $this->image->getImageOrientation();
393                         switch ($orientation) {
394                                 case Imagick::ORIENTATION_BOTTOMRIGHT:
395                                         $this->image->rotateimage("#000", 180);
396                                         break;
397                                 case Imagick::ORIENTATION_RIGHTTOP:
398                                         $this->image->rotateimage("#000", 90);
399                                         break;
400                                 case Imagick::ORIENTATION_LEFTBOTTOM:
401                                         $this->image->rotateimage("#000", -90);
402                                         break;
403                         }
404
405                         $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
406                         return true;
407                 }
408                 // based off comment on http://php.net/manual/en/function.imagerotate.php
409
410                 if (!$this->isValid()) {
411                         return false;
412                 }
413
414                 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
415                         return;
416                 }
417
418                 $exif = @exif_read_data($filename, null, true);
419                 if (!$exif) {
420                         return;
421                 }
422
423                 $ort = isset($exif['IFD0']['Orientation']) ? $exif['IFD0']['Orientation'] : 1;
424
425                 switch ($ort) {
426                         case 1: // nothing
427                                 break;
428
429                         case 2: // horizontal flip
430                                 $this->flip();
431                                 break;
432
433                         case 3: // 180 rotate left
434                                 $this->rotate(180);
435                                 break;
436
437                         case 4: // vertical flip
438                                 $this->flip(false, true);
439                                 break;
440
441                         case 5: // vertical flip + 90 rotate right
442                                 $this->flip(false, true);
443                                 $this->rotate(-90);
444                                 break;
445
446                         case 6: // 90 rotate right
447                                 $this->rotate(-90);
448                                 break;
449
450                         case 7: // horizontal flip + 90 rotate right
451                                 $this->flip();
452                                 $this->rotate(-90);
453                                 break;
454
455                         case 8: // 90 rotate left
456                                 $this->rotate(90);
457                                 break;
458                 }
459
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                                 try {
543                                         $this->image->scaleImage($dest_width, $dest_height);
544                                 } catch (Exception $e) {
545                                         // Imagick couldn't use the data
546                                         return false;
547                                 }
548                         } while ($this->image->nextImage());
549
550                         // These may not be necessary anymore
551                         $this->width  = $this->image->getImageWidth();
552                         $this->height = $this->image->getImageHeight();
553                 } else {
554                         $dest = imagecreatetruecolor($dest_width, $dest_height);
555                         imagealphablending($dest, false);
556                         imagesavealpha($dest, true);
557
558                         if ($this->type=='image/png') {
559                                 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
560                         }
561
562                         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $this->width, $this->height);
563
564                         if ($this->image) {
565                                 imagedestroy($this->image);
566                         }
567
568                         $this->image = $dest;
569                         $this->width  = imagesx($this->image);
570                         $this->height = imagesy($this->image);
571                 }
572
573                 return true;
574         }
575
576         /**
577          * Convert a GIF to a PNG to make it static
578          */
579         public function toStatic()
580         {
581                 if ($this->type != 'image/gif') {
582                         return;
583                 }
584
585                 if ($this->isImagick()) {
586                         $this->type == 'image/png';
587                         $this->image->setFormat('png');
588                 }
589         }
590
591         /**
592          * @param integer $max maximum
593          * @param integer $x   x coordinate
594          * @param integer $y   y coordinate
595          * @param integer $w   width
596          * @param integer $h   height
597          * @return mixed
598          */
599         public function crop($max, $x, $y, $w, $h)
600         {
601                 if (!$this->isValid()) {
602                         return false;
603                 }
604
605                 if ($this->isImagick()) {
606                         $this->image->setFirstIterator();
607                         do {
608                                 $this->image->cropImage($w, $h, $x, $y);
609                                 /*
610                                  * We need to remove the canva,
611                                  * or the image is not resized to the crop:
612                                  * http://php.net/manual/en/imagick.cropimage.php#97232
613                                  */
614                                 $this->image->setImagePage(0, 0, 0, 0);
615                         } while ($this->image->nextImage());
616                         return $this->scaleDown($max);
617                 }
618
619                 $dest = imagecreatetruecolor($max, $max);
620                 imagealphablending($dest, false);
621                 imagesavealpha($dest, true);
622                 if ($this->type=='image/png') {
623                         imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
624                 }
625                 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
626                 if ($this->image) {
627                         imagedestroy($this->image);
628                 }
629                 $this->image = $dest;
630                 $this->width  = imagesx($this->image);
631                 $this->height = imagesy($this->image);
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                         try {
660                                 /* Clean it */
661                                 $this->image = $this->image->deconstructImages();
662                                 $string = $this->image->getImagesBlob();
663                                 return $string;
664                         } catch (Exception $e) {
665                                 return false;
666                         }
667                 }
668
669                 ob_start();
670
671                 // Enable interlacing
672                 imageinterlace($this->image, true);
673
674                 switch ($this->getType()) {
675                         case "image/png":
676                                 $quality = DI::config()->get('system', 'png_quality');
677                                 imagepng($this->image, null, $quality);
678                                 break;
679                         case "image/jpeg":
680                                 $quality = DI::config()->get('system', 'jpeg_quality');
681                                 imagejpeg($this->image, null, $quality);
682                 }
683                 $string = ob_get_contents();
684                 ob_end_clean();
685
686                 return $string;
687         }
688 }