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