]> git.mxchange.org Git - friendica.git/blob - include/Photo.php
only constrain horizontal image dimension for scaling very tall photos
[friendica.git] / include / Photo.php
1 <?php
2
3 if(! class_exists("Photo")) {
4 class Photo {
5
6     private $image;
7
8     /**
9      * Put back gd stuff, not everybody have Imagick
10      */
11     private $imagick;
12     private $width;
13     private $height;
14     private $valid;
15     private $type;
16     private $types;
17
18     /**
19      * supported mimetypes and corresponding file extensions
20      */
21     static function supportedTypes() {
22         if(class_exists('Imagick')) {
23             /**
24              * Imagick::queryFormats won't help us a lot there...
25              * At least, not yet, other parts of friendica uses this array
26              */
27             $t = array(
28                 'image/jpeg' => 'jpg',
29                 'image/png' => 'png',
30                 'image/gif' => 'gif'
31             );
32         } else {
33             $t = array();
34             $t['image/jpeg'] ='jpg';
35             if (imagetypes() & IMG_PNG) $t['image/png'] = 'png';
36         }
37
38         return $t;
39     }
40
41     public function __construct($data, $type=null) {
42         $this->imagick = class_exists('Imagick');
43         $this->types = $this->supportedTypes();
44         if (!array_key_exists($type,$this->types)){
45             $type='image/jpeg';
46         }
47         $this->type = $type;
48
49         if($this->is_imagick() && $this->load_data($data)) {
50                         return true;
51                 } else {
52                         // Failed to load with Imagick, fallback
53                         $this->imagick = false;
54                 }
55                 return $this->load_data($data);
56     }
57
58     public function __destruct() {
59         if($this->image) {
60             if($this->is_imagick()) {
61                 $this->image->clear();
62                 $this->image->destroy();
63                 return;
64             }
65             imagedestroy($this->image);
66         }
67     }
68
69     public function is_imagick() {
70         return $this->imagick;
71     }
72
73     /**
74      * Maps Mime types to Imagick formats
75      */
76     public function get_FormatsMap() {
77         $m = array(
78             'image/jpeg' => 'JPG',
79             'image/png' => 'PNG',
80             'image/gif' => 'GIF'
81         );
82         return $m;
83     }
84
85     private function load_data($data) {
86                 if($this->is_imagick()) {
87                         $this->image = new Imagick();
88             try {
89                                 $this->image->readImageBlob($data);
90                         }
91                         catch (Exception $e) {
92                                 // Imagick couldn't use the data
93                                 return false;
94                         }
95
96             /**
97              * Setup the image to the format it will be saved to
98              */
99             $map = $this->get_FormatsMap();
100             $format = $map[$type];
101             $this->image->setFormat($format);
102
103             // Always coalesce, if it is not a multi-frame image it won't hurt anyway
104             $this->image = $this->image->coalesceImages();
105
106             /**
107              * setup the compression here, so we'll do it only once
108              */
109             switch($this->getType()){
110                 case "image/png":
111                     $quality = get_config('system','png_quality');
112                     if((! $quality) || ($quality > 9))
113                         $quality = PNG_QUALITY;
114                     /**
115                      * From http://www.imagemagick.org/script/command-line-options.php#quality:
116                      *
117                      * 'For the MNG and PNG image formats, the quality value sets
118                      * the zlib compression level (quality / 10) and filter-type (quality % 10).
119                      * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering,
120                      * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
121                      */
122                     $quality = $quality * 10;
123                     $this->image->setCompressionQuality($quality);
124                     break;
125                 case "image/jpeg":
126                     $quality = get_config('system','jpeg_quality');
127                     if((! $quality) || ($quality > 100))
128                         $quality = JPEG_QUALITY;
129                     $this->image->setCompressionQuality($quality);
130             }
131
132             $this->width  = $this->image->getImageWidth();
133                         $this->height = $this->image->getImageHeight();
134                         $this->valid  = true;
135
136             return true;
137                 }
138
139                 $this->valid = false;
140                 $this->image = @imagecreatefromstring($data);
141                 if($this->image !== FALSE) {
142                         $this->width  = imagesx($this->image);
143                         $this->height = imagesy($this->image);
144                         $this->valid  = true;
145                         imagealphablending($this->image, false);
146                         imagesavealpha($this->image, true);
147
148                         return true;
149                 }
150                 
151                 return false;
152         }
153
154     public function is_valid() {
155         if($this->is_imagick())
156             return ($this->image !== FALSE);
157         return $this->valid;
158     }
159
160     public function getWidth() {
161         if(!$this->is_valid())
162             return FALSE;
163
164         if($this->is_imagick())
165             return $this->image->getImageWidth();
166         return $this->width;
167     }
168
169     public function getHeight() {
170         if(!$this->is_valid())
171             return FALSE;
172
173         if($this->is_imagick())
174             return $this->image->getImageHeight();
175         return $this->height;
176     }
177
178     public function getImage() {
179         if(!$this->is_valid())
180             return FALSE;
181
182         if($this->is_imagick()) {
183             /* Clean it */
184             $this->image = $this->image->deconstructImages();
185             return $this->image;
186         }
187         return $this->image;
188     }
189
190     public function getType() {
191         if(!$this->is_valid())
192             return FALSE;
193
194         return $this->type;
195     }
196
197     public function getExt() {
198         if(!$this->is_valid())
199             return FALSE;
200
201         return $this->types[$this->getType()];
202     }
203
204     public function scaleImage($max) {
205         if(!$this->is_valid())
206             return FALSE;
207
208         if($this->is_imagick()) {
209             /**
210              * If it is not animated, there will be only one iteration here,
211              * so don't bother checking
212              */
213             // Don't forget to go back to the first frame
214             $this->image->setFirstIterator();
215             do {
216                 $this->image->resizeImage($max, $max, imagick::FILTER_LANCZOS, 1, true);
217             } while ($this->image->nextImage());
218             return;
219         }
220
221         $width = $this->width;
222         $height = $this->height;
223
224         $dest_width = $dest_height = 0;
225
226         if((! $width)|| (! $height))
227             return FALSE;
228
229         if($width > $max && $height > $max) {
230
231                         // very tall image (greater than 16:9)
232                         // constrain the width - let the height float.
233
234                         if((($height * 9) / 16) > $width) {
235                                 $dest_width = $max;
236                 $dest_height = intval(( $height * $max ) / $width);
237                         }
238
239                         // else constrain both dimensions
240
241                         elseif($width > $height) {
242                 $dest_width = $max;
243                 $dest_height = intval(( $height * $max ) / $width);
244             }
245             else {
246                 $dest_width = intval(( $width * $max ) / $height);
247                 $dest_height = $max;
248             }
249         }
250         else {
251             if( $width > $max ) {
252                 $dest_width = $max;
253                 $dest_height = intval(( $height * $max ) / $width);
254             }
255             else {
256                 if( $height > $max ) {
257
258                                         // very tall image (greater than 16:9)
259                                         // constrain the width - let the height float.
260
261                                         if((($height * 9) / 16) > $width) {
262                                                 $dest_width = $max;
263                                 $dest_height = intval(( $height * $max ) / $width);
264                                         }
265                                         else {
266                             $dest_width = intval(( $width * $max ) / $height);
267                         $dest_height = $max;
268                                         }
269                 }
270                 else {
271                     $dest_width = $width;
272                     $dest_height = $height;
273                 }
274             }
275         }
276
277
278         $dest = imagecreatetruecolor( $dest_width, $dest_height );
279         imagealphablending($dest, false);
280         imagesavealpha($dest, true);
281         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
282         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
283         if($this->image)
284             imagedestroy($this->image);
285         $this->image = $dest;
286         $this->width  = imagesx($this->image);
287         $this->height = imagesy($this->image);
288     }
289
290     public function rotate($degrees) {
291         if(!$this->is_valid())
292             return FALSE;
293
294         if($this->is_imagick()) {
295             $this->image->setFirstIterator();
296             do {
297                 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
298             } while ($this->image->nextImage());
299             return;
300         }
301
302         $this->image  = imagerotate($this->image,$degrees,0);
303         $this->width  = imagesx($this->image);
304         $this->height = imagesy($this->image);
305     }
306
307     public function flip($horiz = true, $vert = false) {
308         if(!$this->is_valid())
309             return FALSE;
310
311         if($this->is_imagick()) {
312             $this->image->setFirstIterator();
313             do {
314                 if($horiz) $this->image->flipImage();
315                 if($vert) $this->image->flopImage();
316             } while ($this->image->nextImage());
317             return;
318         }
319
320         $w = imagesx($this->image);
321         $h = imagesy($this->image);
322         $flipped = imagecreate($w, $h);
323         if($horiz) {
324             for ($x = 0; $x < $w; $x++) {
325                 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
326             }
327         }
328         if($vert) {
329             for ($y = 0; $y < $h; $y++) {
330                 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
331             }
332         }
333         $this->image = $flipped;
334     }
335
336     public function orient($filename) {
337         // based off comment on http://php.net/manual/en/function.imagerotate.php
338
339         if(!$this->is_valid())
340             return FALSE;
341
342         if( (! function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg') )
343             return;
344
345         $exif = @exif_read_data($filename);
346
347                 if(! $exif)
348                         return;
349
350         $ort = $exif['Orientation'];
351
352         switch($ort)
353         {
354             case 1: // nothing
355                 break;
356
357             case 2: // horizontal flip
358                 $this->flip();
359                 break;
360
361             case 3: // 180 rotate left
362                 $this->rotate(180);
363                 break;
364
365             case 4: // vertical flip
366                 $this->flip(false, true);
367                 break;
368
369             case 5: // vertical flip + 90 rotate right
370                 $this->flip(false, true);
371                 $this->rotate(-90);
372                 break;
373
374             case 6: // 90 rotate right
375                 $this->rotate(-90);
376                 break;
377
378             case 7: // horizontal flip + 90 rotate right
379                 $this->flip();
380                 $this->rotate(-90);
381                 break;
382
383             case 8:    // 90 rotate left
384                 $this->rotate(90);
385                 break;
386         }
387     }
388
389
390
391     public function scaleImageUp($min) {
392         if(!$this->is_valid())
393             return FALSE;
394
395         if($this->is_imagick())
396             return $this->scaleImage($min);
397
398         $width = $this->width;
399         $height = $this->height;
400
401         $dest_width = $dest_height = 0;
402
403         if((! $width)|| (! $height))
404             return FALSE;
405
406         if($width < $min && $height < $min) {
407             if($width > $height) {
408                 $dest_width = $min;
409                 $dest_height = intval(( $height * $min ) / $width);
410             }
411             else {
412                 $dest_width = intval(( $width * $min ) / $height);
413                 $dest_height = $min;
414             }
415         }
416         else {
417             if( $width < $min ) {
418                 $dest_width = $min;
419                 $dest_height = intval(( $height * $min ) / $width);
420             }
421             else {
422                 if( $height < $min ) {
423                     $dest_width = intval(( $width * $min ) / $height);
424                     $dest_height = $min;
425                 }
426                 else {
427                     $dest_width = $width;
428                     $dest_height = $height;
429                 }
430             }
431         }
432
433
434         $dest = imagecreatetruecolor( $dest_width, $dest_height );
435         imagealphablending($dest, false);
436         imagesavealpha($dest, true);
437         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
438         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
439         if($this->image)
440             imagedestroy($this->image);
441         $this->image = $dest;
442         $this->width  = imagesx($this->image);
443         $this->height = imagesy($this->image);
444     }
445
446
447
448     public function scaleImageSquare($dim) {
449         if(!$this->is_valid())
450             return FALSE;
451
452         if($this->is_imagick()) {
453             $this->image->setFirstIterator();
454             do {
455                 $this->image->resizeImage($dim, $dim, imagick::FILTER_LANCZOS, 1, false);
456             } while ($this->image->nextImage());
457             return;
458         }
459
460         $dest = imagecreatetruecolor( $dim, $dim );
461         imagealphablending($dest, false);
462         imagesavealpha($dest, true);
463         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
464         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height);
465         if($this->image)
466             imagedestroy($this->image);
467         $this->image = $dest;
468         $this->width  = imagesx($this->image);
469         $this->height = imagesy($this->image);
470     }
471
472
473     public function cropImage($max,$x,$y,$w,$h) {
474         if(!$this->is_valid())
475             return FALSE;
476
477         if($this->is_imagick()) {
478             $this->image->setFirstIterator();
479             do {
480                 $this->image->cropImage($w, $h, $x, $y);
481                 /**
482                  * We need to remove the canva,
483                  * or the image is not resized to the crop:
484                  * http://php.net/manual/en/imagick.cropimage.php#97232
485                  */
486                 $this->image->setImagePage(0, 0, 0, 0);
487             } while ($this->image->nextImage());
488             return $this->scaleImage($max);
489         }
490
491         $dest = imagecreatetruecolor( $max, $max );
492         imagealphablending($dest, false);
493         imagesavealpha($dest, true);
494         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
495         imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
496         if($this->image)
497             imagedestroy($this->image);
498         $this->image = $dest;
499         $this->width  = imagesx($this->image);
500         $this->height = imagesy($this->image);
501     }
502
503     public function saveImage($path) {
504         if(!$this->is_valid())
505             return FALSE;
506
507         $string = $this->imageString();
508         file_put_contents($path, $string);
509     }
510
511     public function imageString() {
512         if(!$this->is_valid())
513             return FALSE;
514
515         if($this->is_imagick()) {
516             /* Clean it */
517             $this->image = $this->image->deconstructImages();
518             $string = $this->image->getImagesBlob();
519             return $string;
520         }
521
522         $quality = FALSE;
523
524         ob_start();
525
526         switch($this->getType()){
527             case "image/png":
528                 $quality = get_config('system','png_quality');
529                 if((! $quality) || ($quality > 9))
530                     $quality = PNG_QUALITY;
531                 imagepng($this->image,NULL, $quality);
532                 break;
533             case "image/jpeg":
534                 $quality = get_config('system','jpeg_quality');
535                 if((! $quality) || ($quality > 100))
536                     $quality = JPEG_QUALITY;
537                 imagejpeg($this->image,NULL,$quality);
538         }
539         $string = ob_get_contents();
540         ob_end_clean();
541
542         return $string;
543     }
544
545
546
547     public function store($uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '') {
548
549         $r = q("select `guid` from photo where `resource-id` = '%s' and `guid` != '' limit 1",
550             dbesc($rid)
551         );
552         if(count($r))
553             $guid = $r[0]['guid'];
554         else
555             $guid = get_guid();
556
557         $x = q("select id from photo where `resource-id` = '%s' and uid = %d and `contact-id` = %d and `scale` = %d limit 1",
558                 dbesc($rid),
559                 intval($uid),
560                 intval($cid),
561                 intval($scale)
562         );
563         if(count($x)) {
564             $r = q("UPDATE `photo`
565                 set `uid` = %d,
566                 `contact-id` = %d,
567                 `guid` = '%s',
568                 `resource-id` = '%s',
569                 `created` = '%s',
570                 `edited` = '%s',
571                 `filename` = '%s',
572                 `type` = '%s',
573                 `album` = '%s',
574                 `height` = %d,
575                 `width` = %d,
576                 `data` = '%s',
577                 `scale` = %d,
578                 `profile` = %d,
579                 `allow_cid` = '%s',
580                 `allow_gid` = '%s',
581                 `deny_cid` = '%s',
582                 `deny_gid` = '%s'
583                 where id = %d limit 1",
584
585                 intval($uid),
586                 intval($cid),
587                 dbesc($guid),
588                 dbesc($rid),
589                 dbesc(datetime_convert()),
590                 dbesc(datetime_convert()),
591                 dbesc(basename($filename)),
592                 dbesc($this->getType()),
593                 dbesc($album),
594                 intval($this->getHeight()),
595                 intval($this->getWidth()),
596                 dbesc($this->imageString()),
597                 intval($scale),
598                 intval($profile),
599                 dbesc($allow_cid),
600                 dbesc($allow_gid),
601                 dbesc($deny_cid),
602                 dbesc($deny_gid),
603                 intval($x[0]['id'])
604             );
605         }
606         else {
607             $r = q("INSERT INTO `photo`
608                 ( `uid`, `contact-id`, `guid`, `resource-id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `data`, `scale`, `profile`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` )
609                 VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, '%s', '%s', '%s', '%s' )",
610                 intval($uid),
611                 intval($cid),
612                 dbesc($guid),
613                 dbesc($rid),
614                 dbesc(datetime_convert()),
615                 dbesc(datetime_convert()),
616                 dbesc(basename($filename)),
617                 dbesc($this->getType()),
618                 dbesc($album),
619                 intval($this->getHeight()),
620                 intval($this->getWidth()),
621                 dbesc($this->imageString()),
622                 intval($scale),
623                 intval($profile),
624                 dbesc($allow_cid),
625                 dbesc($allow_gid),
626                 dbesc($deny_cid),
627                 dbesc($deny_gid)
628             );
629         }
630         return $r;
631     }
632 }}
633
634
635 /**
636  * Guess image mimetype from filename or from Content-Type header
637  *
638  * @arg $filename string Image filename
639  * @arg $fromcurl boolean Check Content-Type header from curl request
640  */
641 function guess_image_type($filename, $fromcurl=false) {
642     logger('Photo: guess_image_type: '.$filename . ($fromcurl?' from curl headers':''), LOGGER_DEBUG);
643     $type = null;
644     if ($fromcurl) {
645         $a = get_app();
646         $headers=array();
647         $h = explode("\n",$a->get_curl_headers());
648         foreach ($h as $l) {
649             list($k,$v) = array_map("trim", explode(":", trim($l), 2));
650             $headers[$k] = $v;
651         }
652         if (array_key_exists('Content-Type', $headers))
653             $type = $headers['Content-Type'];
654     }
655     if (is_null($type)){
656         // Guessing from extension? Isn't that... dangerous?
657         if(class_exists('Imagick')) {
658             /**
659              * Well, this not much better,
660              * but at least it comes from the data inside the image,
661              * we won't be tricked by a manipulated extension
662              */
663             $image = new Imagick($filename);
664             $type = $image->getImageMimeType();
665         } else {
666             $ext = pathinfo($filename, PATHINFO_EXTENSION);
667             $types = Photo::supportedTypes();
668             $type = "image/jpeg";
669             foreach ($types as $m=>$e){
670                 if ($ext==$e) $type = $m;
671             }
672         }
673     }
674     logger('Photo: guess_image_type: type='.$type, LOGGER_DEBUG);
675     return $type;
676
677 }
678
679 function import_profile_photo($photo,$uid,$cid) {
680
681     $a = get_app();
682
683     $r = q("select `resource-id` from photo where `uid` = %d and `contact-id` = %d and `scale` = 4 and `album` = 'Contact Photos' limit 1",
684         intval($uid),
685         intval($cid)
686     );
687     if(count($r) && strlen($r[0]['resource-id'])) {
688         $hash = $r[0]['resource-id'];
689     }
690     else {
691         $hash = photo_new_resource();
692     }
693
694     $photo_failure = false;
695
696     $filename = basename($photo);
697     $img_str = fetch_url($photo,true);
698
699     $type = guess_image_type($photo,true);
700     $img = new Photo($img_str, $type);
701     if($img->is_valid()) {
702
703         $img->scaleImageSquare(175);
704
705         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 4 );
706
707         if($r === false)
708             $photo_failure = true;
709
710         $img->scaleImage(80);
711
712         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 5 );
713
714         if($r === false)
715             $photo_failure = true;
716
717         $img->scaleImage(48);
718
719         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 6 );
720
721         if($r === false)
722             $photo_failure = true;
723
724         $photo = $a->get_baseurl() . '/photo/' . $hash . '-4.' . $img->getExt();
725         $thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.' . $img->getExt();
726         $micro = $a->get_baseurl() . '/photo/' . $hash . '-6.' . $img->getExt();
727     }
728     else
729         $photo_failure = true;
730
731     if($photo_failure) {
732         $photo = $a->get_baseurl() . '/images/person-175.jpg';
733         $thumb = $a->get_baseurl() . '/images/person-80.jpg';
734         $micro = $a->get_baseurl() . '/images/person-48.jpg';
735     }
736
737     return(array($photo,$thumb,$micro));
738
739 }