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