]> git.mxchange.org Git - friendica.git/blob - include/Photo.php
Merge branch 'master' of https://github.com/friendica/friendica
[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             if($width > $height) {
231                 $dest_width = $max;
232                 $dest_height = intval(( $height * $max ) / $width);
233             }
234             else {
235                 $dest_width = intval(( $width * $max ) / $height);
236                 $dest_height = $max;
237             }
238         }
239         else {
240             if( $width > $max ) {
241                 $dest_width = $max;
242                 $dest_height = intval(( $height * $max ) / $width);
243             }
244             else {
245                 if( $height > $max ) {
246                     $dest_width = intval(( $width * $max ) / $height);
247                     $dest_height = $max;
248                 }
249                 else {
250                     $dest_width = $width;
251                     $dest_height = $height;
252                 }
253             }
254         }
255
256
257         $dest = imagecreatetruecolor( $dest_width, $dest_height );
258         imagealphablending($dest, false);
259         imagesavealpha($dest, true);
260         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
261         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
262         if($this->image)
263             imagedestroy($this->image);
264         $this->image = $dest;
265         $this->width  = imagesx($this->image);
266         $this->height = imagesy($this->image);
267     }
268
269     public function rotate($degrees) {
270         if(!$this->is_valid())
271             return FALSE;
272
273         if($this->is_imagick()) {
274             $this->image->setFirstIterator();
275             do {
276                 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
277             } while ($this->image->nextImage());
278             return;
279         }
280
281         $this->image  = imagerotate($this->image,$degrees,0);
282         $this->width  = imagesx($this->image);
283         $this->height = imagesy($this->image);
284     }
285
286     public function flip($horiz = true, $vert = false) {
287         if(!$this->is_valid())
288             return FALSE;
289
290         if($this->is_imagick()) {
291             $this->image->setFirstIterator();
292             do {
293                 if($horiz) $this->image->flipImage();
294                 if($vert) $this->image->flopImage();
295             } while ($this->image->nextImage());
296             return;
297         }
298
299         $w = imagesx($this->image);
300         $h = imagesy($this->image);
301         $flipped = imagecreate($w, $h);
302         if($horiz) {
303             for ($x = 0; $x < $w; $x++) {
304                 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
305             }
306         }
307         if($vert) {
308             for ($y = 0; $y < $h; $y++) {
309                 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
310             }
311         }
312         $this->image = $flipped;
313     }
314
315     public function orient($filename) {
316         // based off comment on http://php.net/manual/en/function.imagerotate.php
317
318         if(!$this->is_valid())
319             return FALSE;
320
321         if( (! function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg') )
322             return;
323
324         $exif = @exif_read_data($filename);
325
326                 if(! $exif)
327                         return;
328
329         $ort = $exif['Orientation'];
330
331         switch($ort)
332         {
333             case 1: // nothing
334                 break;
335
336             case 2: // horizontal flip
337                 $this->flip();
338                 break;
339
340             case 3: // 180 rotate left
341                 $this->rotate(180);
342                 break;
343
344             case 4: // vertical flip
345                 $this->flip(false, true);
346                 break;
347
348             case 5: // vertical flip + 90 rotate right
349                 $this->flip(false, true);
350                 $this->rotate(-90);
351                 break;
352
353             case 6: // 90 rotate right
354                 $this->rotate(-90);
355                 break;
356
357             case 7: // horizontal flip + 90 rotate right
358                 $this->flip();
359                 $this->rotate(-90);
360                 break;
361
362             case 8:    // 90 rotate left
363                 $this->rotate(90);
364                 break;
365         }
366     }
367
368
369
370     public function scaleImageUp($min) {
371         if(!$this->is_valid())
372             return FALSE;
373
374         if($this->is_imagick())
375             return $this->scaleImage($min);
376
377         $width = $this->width;
378         $height = $this->height;
379
380         $dest_width = $dest_height = 0;
381
382         if((! $width)|| (! $height))
383             return FALSE;
384
385         if($width < $min && $height < $min) {
386             if($width > $height) {
387                 $dest_width = $min;
388                 $dest_height = intval(( $height * $min ) / $width);
389             }
390             else {
391                 $dest_width = intval(( $width * $min ) / $height);
392                 $dest_height = $min;
393             }
394         }
395         else {
396             if( $width < $min ) {
397                 $dest_width = $min;
398                 $dest_height = intval(( $height * $min ) / $width);
399             }
400             else {
401                 if( $height < $min ) {
402                     $dest_width = intval(( $width * $min ) / $height);
403                     $dest_height = $min;
404                 }
405                 else {
406                     $dest_width = $width;
407                     $dest_height = $height;
408                 }
409             }
410         }
411
412
413         $dest = imagecreatetruecolor( $dest_width, $dest_height );
414         imagealphablending($dest, false);
415         imagesavealpha($dest, true);
416         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
417         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
418         if($this->image)
419             imagedestroy($this->image);
420         $this->image = $dest;
421         $this->width  = imagesx($this->image);
422         $this->height = imagesy($this->image);
423     }
424
425
426
427     public function scaleImageSquare($dim) {
428         if(!$this->is_valid())
429             return FALSE;
430
431         if($this->is_imagick()) {
432             $this->image->setFirstIterator();
433             do {
434                 $this->image->resizeImage($dim, $dim, imagick::FILTER_LANCZOS, 1, false);
435             } while ($this->image->nextImage());
436             return;
437         }
438
439         $dest = imagecreatetruecolor( $dim, $dim );
440         imagealphablending($dest, false);
441         imagesavealpha($dest, true);
442         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
443         imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height);
444         if($this->image)
445             imagedestroy($this->image);
446         $this->image = $dest;
447         $this->width  = imagesx($this->image);
448         $this->height = imagesy($this->image);
449     }
450
451
452     public function cropImage($max,$x,$y,$w,$h) {
453         if(!$this->is_valid())
454             return FALSE;
455
456         if($this->is_imagick()) {
457             $this->image->setFirstIterator();
458             do {
459                 $this->image->cropImage($w, $h, $x, $y);
460                 /**
461                  * We need to remove the canva,
462                  * or the image is not resized to the crop:
463                  * http://php.net/manual/en/imagick.cropimage.php#97232
464                  */
465                 $this->image->setImagePage(0, 0, 0, 0);
466             } while ($this->image->nextImage());
467             return $this->scaleImage($max);
468         }
469
470         $dest = imagecreatetruecolor( $max, $max );
471         imagealphablending($dest, false);
472         imagesavealpha($dest, true);
473         if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
474         imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
475         if($this->image)
476             imagedestroy($this->image);
477         $this->image = $dest;
478         $this->width  = imagesx($this->image);
479         $this->height = imagesy($this->image);
480     }
481
482     public function saveImage($path) {
483         if(!$this->is_valid())
484             return FALSE;
485
486         $string = $this->imageString();
487         file_put_contents($path, $string);
488     }
489
490     public function imageString() {
491         if(!$this->is_valid())
492             return FALSE;
493
494         if($this->is_imagick()) {
495             /* Clean it */
496             $this->image = $this->image->deconstructImages();
497             $string = $this->image->getImagesBlob();
498             return $string;
499         }
500
501         $quality = FALSE;
502
503         ob_start();
504
505         switch($this->getType()){
506             case "image/png":
507                 $quality = get_config('system','png_quality');
508                 if((! $quality) || ($quality > 9))
509                     $quality = PNG_QUALITY;
510                 imagepng($this->image,NULL, $quality);
511                 break;
512             case "image/jpeg":
513                 $quality = get_config('system','jpeg_quality');
514                 if((! $quality) || ($quality > 100))
515                     $quality = JPEG_QUALITY;
516                 imagejpeg($this->image,NULL,$quality);
517         }
518         $string = ob_get_contents();
519         ob_end_clean();
520
521         return $string;
522     }
523
524
525
526     public function store($uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '') {
527
528         $r = q("select `guid` from photo where `resource-id` = '%s' and `guid` != '' limit 1",
529             dbesc($rid)
530         );
531         if(count($r))
532             $guid = $r[0]['guid'];
533         else
534             $guid = get_guid();
535
536         $x = q("select id from photo where `resource-id` = '%s' and uid = %d and `contact-id` = %d and `scale` = %d limit 1",
537                 dbesc($rid),
538                 intval($uid),
539                 intval($cid),
540                 intval($scale)
541         );
542         if(count($x)) {
543             $r = q("UPDATE `photo`
544                 set `uid` = %d,
545                 `contact-id` = %d,
546                 `guid` = '%s',
547                 `resource-id` = '%s',
548                 `created` = '%s',
549                 `edited` = '%s',
550                 `filename` = '%s',
551                 `type` = '%s',
552                 `album` = '%s',
553                 `height` = %d,
554                 `width` = %d,
555                 `data` = '%s',
556                 `scale` = %d,
557                 `profile` = %d,
558                 `allow_cid` = '%s',
559                 `allow_gid` = '%s',
560                 `deny_cid` = '%s',
561                 `deny_gid` = '%s'
562                 where id = %d limit 1",
563
564                 intval($uid),
565                 intval($cid),
566                 dbesc($guid),
567                 dbesc($rid),
568                 dbesc(datetime_convert()),
569                 dbesc(datetime_convert()),
570                 dbesc(basename($filename)),
571                 dbesc($this->getType()),
572                 dbesc($album),
573                 intval($this->getHeight()),
574                 intval($this->getWidth()),
575                 dbesc($this->imageString()),
576                 intval($scale),
577                 intval($profile),
578                 dbesc($allow_cid),
579                 dbesc($allow_gid),
580                 dbesc($deny_cid),
581                 dbesc($deny_gid),
582                 intval($x[0]['id'])
583             );
584         }
585         else {
586             $r = q("INSERT INTO `photo`
587                 ( `uid`, `contact-id`, `guid`, `resource-id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `data`, `scale`, `profile`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` )
588                 VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, '%s', '%s', '%s', '%s' )",
589                 intval($uid),
590                 intval($cid),
591                 dbesc($guid),
592                 dbesc($rid),
593                 dbesc(datetime_convert()),
594                 dbesc(datetime_convert()),
595                 dbesc(basename($filename)),
596                 dbesc($this->getType()),
597                 dbesc($album),
598                 intval($this->getHeight()),
599                 intval($this->getWidth()),
600                 dbesc($this->imageString()),
601                 intval($scale),
602                 intval($profile),
603                 dbesc($allow_cid),
604                 dbesc($allow_gid),
605                 dbesc($deny_cid),
606                 dbesc($deny_gid)
607             );
608         }
609         return $r;
610     }
611 }}
612
613
614 /**
615  * Guess image mimetype from filename or from Content-Type header
616  *
617  * @arg $filename string Image filename
618  * @arg $fromcurl boolean Check Content-Type header from curl request
619  */
620 function guess_image_type($filename, $fromcurl=false) {
621     logger('Photo: guess_image_type: '.$filename . ($fromcurl?' from curl headers':''), LOGGER_DEBUG);
622     $type = null;
623     if ($fromcurl) {
624         $a = get_app();
625         $headers=array();
626         $h = explode("\n",$a->get_curl_headers());
627         foreach ($h as $l) {
628             list($k,$v) = array_map("trim", explode(":", trim($l), 2));
629             $headers[$k] = $v;
630         }
631         if (array_key_exists('Content-Type', $headers))
632             $type = $headers['Content-Type'];
633     }
634     if (is_null($type)){
635         // Guessing from extension? Isn't that... dangerous?
636         if(class_exists('Imagick')) {
637             /**
638              * Well, this not much better,
639              * but at least it comes from the data inside the image,
640              * we won't be tricked by a manipulated extension
641              */
642             $image = new Imagick($filename);
643             $type = $image->getImageMimeType();
644         } else {
645             $ext = pathinfo($filename, PATHINFO_EXTENSION);
646             $types = Photo::supportedTypes();
647             $type = "image/jpeg";
648             foreach ($types as $m=>$e){
649                 if ($ext==$e) $type = $m;
650             }
651         }
652     }
653     logger('Photo: guess_image_type: type='.$type, LOGGER_DEBUG);
654     return $type;
655
656 }
657
658 function import_profile_photo($photo,$uid,$cid) {
659
660     $a = get_app();
661
662     $r = q("select `resource-id` from photo where `uid` = %d and `contact-id` = %d and `scale` = 4 and `album` = 'Contact Photos' limit 1",
663         intval($uid),
664         intval($cid)
665     );
666     if(count($r) && strlen($r[0]['resource-id'])) {
667         $hash = $r[0]['resource-id'];
668     }
669     else {
670         $hash = photo_new_resource();
671     }
672
673     $photo_failure = false;
674
675     $filename = basename($photo);
676     $img_str = fetch_url($photo,true);
677
678     $type = guess_image_type($photo,true);
679     $img = new Photo($img_str, $type);
680     if($img->is_valid()) {
681
682         $img->scaleImageSquare(175);
683
684         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 4 );
685
686         if($r === false)
687             $photo_failure = true;
688
689         $img->scaleImage(80);
690
691         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 5 );
692
693         if($r === false)
694             $photo_failure = true;
695
696         $img->scaleImage(48);
697
698         $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 6 );
699
700         if($r === false)
701             $photo_failure = true;
702
703         $photo = $a->get_baseurl() . '/photo/' . $hash . '-4.' . $img->getExt();
704         $thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.' . $img->getExt();
705         $micro = $a->get_baseurl() . '/photo/' . $hash . '-6.' . $img->getExt();
706     }
707     else
708         $photo_failure = true;
709
710     if($photo_failure) {
711         $photo = $a->get_baseurl() . '/images/person-175.jpg';
712         $thumb = $a->get_baseurl() . '/images/person-80.jpg';
713         $micro = $a->get_baseurl() . '/images/person-48.jpg';
714     }
715
716     return(array($photo,$thumb,$micro));
717
718 }