3 * @file src/Object/Photo.php
4 * @brief This file contains the Photo class for image processing
6 namespace Friendica\Object;
9 use Friendica\Core\Cache;
10 use Friendica\Core\Config;
11 use Friendica\Core\System;
12 use Friendica\Database\DBM;
17 require_once "include/photos.php";
20 * Class to handle Photos
27 * Put back gd stuff, not everybody have Imagick
37 * @brief supported mimetypes and corresponding file extensions
40 public static function supportedTypes()
42 if (class_exists('Imagick')) {
43 // Imagick::queryFormats won't help us a lot there...
44 // At least, not yet, other parts of friendica uses this array
46 'image/jpeg' => 'jpg',
52 $t['image/jpeg'] ='jpg';
53 if (imagetypes() & IMG_PNG) {
54 $t['image/png'] = 'png';
63 * @param object $data data
64 * @param boolean $type optional, default null
67 public function __construct($data, $type = null)
69 $this->imagick = class_exists('Imagick');
70 $this->types = static::supportedTypes();
71 if (!array_key_exists($type, $this->types)) {
76 if ($this->isImagick() && $this->loadData($data)) {
79 // Failed to load with Imagick, fallback
80 $this->imagick = false;
82 return $this->loadData($data);
89 public function __destruct()
92 if ($this->isImagick()) {
93 $this->image->clear();
94 $this->image->destroy();
97 if (is_resource($this->image)) {
98 imagedestroy($this->image);
106 public function isImagick()
108 return $this->imagick;
112 * @brief Maps Mime types to Imagick formats
113 * @return arr With with image formats (mime type as key)
115 public function getFormatsMap()
118 'image/jpeg' => 'JPG',
119 'image/png' => 'PNG',
126 * @param object $data data
129 private function loadData($data)
131 if ($this->isImagick()) {
132 $this->image = new Imagick();
134 $this->image->readImageBlob($data);
135 } catch (Exception $e) {
136 // Imagick couldn't use the data
141 * Setup the image to the format it will be saved to
143 $map = $this->getFormatsMap();
144 $format = $map[$type];
145 $this->image->setFormat($format);
147 // Always coalesce, if it is not a multi-frame image it won't hurt anyway
148 $this->image = $this->image->coalesceImages();
151 * setup the compression here, so we'll do it only once
153 switch ($this->getType()) {
155 $quality = Config::get('system', 'png_quality');
156 if ((! $quality) || ($quality > 9)) {
157 $quality = PNG_QUALITY;
160 * From http://www.imagemagick.org/script/command-line-options.php#quality:
162 * 'For the MNG and PNG image formats, the quality value sets
163 * the zlib compression level (quality / 10) and filter-type (quality % 10).
164 * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering,
165 * unless the image has a color map, in which case it means compression level 7 with no PNG filtering'
167 $quality = $quality * 10;
168 $this->image->setCompressionQuality($quality);
171 $quality = Config::get('system', 'jpeg_quality');
172 if ((! $quality) || ($quality > 100)) {
173 $quality = JPEG_QUALITY;
175 $this->image->setCompressionQuality($quality);
178 // The 'width' and 'height' properties are only used by non-Imagick routines.
179 $this->width = $this->image->getImageWidth();
180 $this->height = $this->image->getImageHeight();
186 $this->valid = false;
187 $this->image = @imagecreatefromstring($data);
188 if ($this->image !== false) {
189 $this->width = imagesx($this->image);
190 $this->height = imagesy($this->image);
192 imagealphablending($this->image, false);
193 imagesavealpha($this->image, true);
204 public function isValid()
206 if ($this->isImagick()) {
207 return ($this->image !== false);
215 public function getWidth()
217 if (!$this->isValid()) {
221 if ($this->isImagick()) {
222 return $this->image->getImageWidth();
230 public function getHeight()
232 if (!$this->isValid()) {
236 if ($this->isImagick()) {
237 return $this->image->getImageHeight();
239 return $this->height;
245 public function getImage()
247 if (!$this->isValid()) {
251 if ($this->isImagick()) {
253 $this->image = $this->image->deconstructImages();
262 public function getType()
264 if (!$this->isValid()) {
274 public function getExt()
276 if (!$this->isValid()) {
280 return $this->types[$this->getType()];
284 * @param integer $max max dimension
287 public function scaleImage($max)
289 if (!$this->isValid()) {
293 $width = $this->getWidth();
294 $height = $this->getHeight();
296 $dest_width = $dest_height = 0;
298 if ((! $width)|| (! $height)) {
302 if ($width > $max && $height > $max) {
303 // very tall image (greater than 16:9)
304 // constrain the width - let the height float.
306 if ((($height * 9) / 16) > $width) {
308 $dest_height = intval(($height * $max) / $width);
309 } elseif ($width > $height) {
310 // else constrain both dimensions
312 $dest_height = intval(($height * $max) / $width);
314 $dest_width = intval(($width * $max) / $height);
320 $dest_height = intval(($height * $max) / $width);
322 if ($height > $max) {
323 // very tall image (greater than 16:9)
324 // but width is OK - don't do anything
326 if ((($height * 9) / 16) > $width) {
327 $dest_width = $width;
328 $dest_height = $height;
330 $dest_width = intval(($width * $max) / $height);
334 $dest_width = $width;
335 $dest_height = $height;
341 if ($this->isImagick()) {
343 * If it is not animated, there will be only one iteration here,
344 * so don't bother checking
346 // Don't forget to go back to the first frame
347 $this->image->setFirstIterator();
349 // FIXME - implement horizantal bias for scaling as in followin GD functions
350 // to allow very tall images to be constrained only horizontally.
352 $this->image->scaleImage($dest_width, $dest_height);
353 } while ($this->image->nextImage());
355 // These may not be necessary any more
356 $this->width = $this->image->getImageWidth();
357 $this->height = $this->image->getImageHeight();
363 $dest = imagecreatetruecolor($dest_width, $dest_height);
364 imagealphablending($dest, false);
365 imagesavealpha($dest, true);
366 if ($this->type=='image/png') {
367 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
369 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
371 imagedestroy($this->image);
373 $this->image = $dest;
374 $this->width = imagesx($this->image);
375 $this->height = imagesy($this->image);
379 * @param integer $degrees degrees to rotate image
382 public function rotate($degrees)
384 if (!$this->isValid()) {
388 if ($this->isImagick()) {
389 $this->image->setFirstIterator();
391 $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate()
392 } while ($this->image->nextImage());
396 // if script dies at this point check memory_limit setting in php.ini
397 $this->image = imagerotate($this->image, $degrees, 0);
398 $this->width = imagesx($this->image);
399 $this->height = imagesy($this->image);
403 * @param boolean $horiz optional, default true
404 * @param boolean $vert optional, default false
407 public function flip($horiz = true, $vert = false)
409 if (!$this->isValid()) {
413 if ($this->isImagick()) {
414 $this->image->setFirstIterator();
417 $this->image->flipImage();
420 $this->image->flopImage();
422 } while ($this->image->nextImage());
426 $w = imagesx($this->image);
427 $h = imagesy($this->image);
428 $flipped = imagecreate($w, $h);
430 for ($x = 0; $x < $w; $x++) {
431 imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h);
435 for ($y = 0; $y < $h; $y++) {
436 imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1);
439 $this->image = $flipped;
443 * @param string $filename filename
446 public function orient($filename)
448 if ($this->isImagick()) {
449 // based off comment on http://php.net/manual/en/imagick.getimageorientation.php
450 $orientation = $this->image->getImageOrientation();
451 switch ($orientation) {
452 case Imagick::ORIENTATION_BOTTOMRIGHT:
453 $this->image->rotateimage("#000", 180);
455 case Imagick::ORIENTATION_RIGHTTOP:
456 $this->image->rotateimage("#000", 90);
458 case Imagick::ORIENTATION_LEFTBOTTOM:
459 $this->image->rotateimage("#000", -90);
463 $this->image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
466 // based off comment on http://php.net/manual/en/function.imagerotate.php
468 if (!$this->isValid()) {
472 if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) {
476 $exif = @exif_read_data($filename, null, true);
481 $ort = $exif['IFD0']['Orientation'];
487 case 2: // horizontal flip
491 case 3: // 180 rotate left
495 case 4: // vertical flip
496 $this->flip(false, true);
499 case 5: // vertical flip + 90 rotate right
500 $this->flip(false, true);
504 case 6: // 90 rotate right
508 case 7: // horizontal flip + 90 rotate right
513 case 8: // 90 rotate left
518 // logger('exif: ' . print_r($exif,true));
523 * @param integer $min minimum dimension
526 public function scaleImageUp($min)
528 if (!$this->isValid()) {
532 $width = $this->getWidth();
533 $height = $this->getHeight();
535 $dest_width = $dest_height = 0;
537 if ((!$width)|| (!$height)) {
541 if ($width < $min && $height < $min) {
542 if ($width > $height) {
544 $dest_height = intval(($height * $min) / $width);
546 $dest_width = intval(($width * $min) / $height);
552 $dest_height = intval(($height * $min) / $width);
554 if ($height < $min) {
555 $dest_width = intval(($width * $min) / $height);
558 $dest_width = $width;
559 $dest_height = $height;
564 if ($this->isImagick()) {
565 return $this->scaleImage($dest_width, $dest_height);
568 $dest = imagecreatetruecolor($dest_width, $dest_height);
569 imagealphablending($dest, false);
570 imagesavealpha($dest, true);
571 if ($this->type=='image/png') {
572 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
574 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height);
576 imagedestroy($this->image);
578 $this->image = $dest;
579 $this->width = imagesx($this->image);
580 $this->height = imagesy($this->image);
584 * @param integer $dim dimension
587 public function scaleImageSquare($dim)
589 if (!$this->isValid()) {
593 if ($this->isImagick()) {
594 $this->image->setFirstIterator();
596 $this->image->scaleImage($dim, $dim);
597 } while ($this->image->nextImage());
601 $dest = imagecreatetruecolor($dim, $dim);
602 imagealphablending($dest, false);
603 imagesavealpha($dest, true);
604 if ($this->type=='image/png') {
605 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
607 imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height);
609 imagedestroy($this->image);
611 $this->image = $dest;
612 $this->width = imagesx($this->image);
613 $this->height = imagesy($this->image);
617 * @param integer $max maximum
618 * @param integer $x x coordinate
619 * @param integer $y y coordinate
620 * @param integer $w width
621 * @param integer $h height
624 public function cropImage($max, $x, $y, $w, $h)
626 if (!$this->isValid()) {
630 if ($this->isImagick()) {
631 $this->image->setFirstIterator();
633 $this->image->cropImage($w, $h, $x, $y);
635 * We need to remove the canva,
636 * or the image is not resized to the crop:
637 * http://php.net/manual/en/imagick.cropimage.php#97232
639 $this->image->setImagePage(0, 0, 0, 0);
640 } while ($this->image->nextImage());
641 return $this->scaleImage($max);
644 $dest = imagecreatetruecolor($max, $max);
645 imagealphablending($dest, false);
646 imagesavealpha($dest, true);
647 if ($this->type=='image/png') {
648 imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha
650 imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h);
652 imagedestroy($this->image);
654 $this->image = $dest;
655 $this->width = imagesx($this->image);
656 $this->height = imagesy($this->image);
660 * @param string $path file path
663 public function saveImage($path)
665 if (!$this->isValid()) {
669 $string = $this->imageString();
673 $stamp1 = microtime(true);
674 file_put_contents($path, $string);
675 $a->save_timestamp($stamp1, "file");
681 public function imageString()
683 if (!$this->isValid()) {
687 if ($this->isImagick()) {
689 $this->image = $this->image->deconstructImages();
690 $string = $this->image->getImagesBlob();
698 // Enable interlacing
699 imageinterlace($this->image, true);
701 switch ($this->getType()) {
703 $quality = Config::get('system', 'png_quality');
704 if ((!$quality) || ($quality > 9)) {
705 $quality = PNG_QUALITY;
707 imagepng($this->image, null, $quality);
710 $quality = Config::get('system', 'jpeg_quality');
711 if ((!$quality) || ($quality > 100)) {
712 $quality = JPEG_QUALITY;
714 imagejpeg($this->image, null, $quality);
716 $string = ob_get_contents();
723 * @param integer $uid uid
724 * @param integer $cid cid
725 * @param integer $rid rid
726 * @param string $filename filename
727 * @param string $album album name
728 * @param integer $scale scale
729 * @param integer $profile optional, default = 0
730 * @param string $allow_cid optional, default = ''
731 * @param string $allow_gid optional, default = ''
732 * @param string $deny_cid optional, default = ''
733 * @param string $deny_gid optional, default = ''
734 * @param string $desc optional, default = ''
737 public function store($uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '', $desc = '')
739 $r = dba::select('photo', array('guid'), array("`resource-id` = ? AND `guid` != ?", $rid, ''), array('limit' => 1));
740 if (DBM::is_result($r)) {
746 $x = dba::select('photo', array('id'), array('resource-id' => $rid, 'uid' => $uid, 'contact-id' => $cid, 'scale' => $scale), array('limit' => 1));
748 $fields = array('uid' => $uid, 'contact-id' => $cid, 'guid' => $guid, 'resource-id' => $rid, 'created' => datetime_convert(), 'edited' => datetime_convert(),
749 'filename' => basename($filename), 'type' => $this->getType(), 'album' => $album, 'height' => $this->getHeight(), 'width' => $this->getWidth(),
750 'datasize' => strlen($this->imageString()), 'data' => $this->imageString(), 'scale' => $scale, 'profile' => $profile,
751 'allow_cid' => $allow_cid, 'allow_gid' => $allow_gid, 'deny_cid' => $deny_cid, 'deny_gid' => $deny_gid, 'desc' => $desc);
753 if (DBM::is_result($x)) {
754 $r = dba::update('photo', $fields, array('id' => $x['id']));
756 $r = dba::insert('photo', $fields);
763 * Guess image mimetype from filename or from Content-Type header
765 * @param string $filename Image filename
766 * @param boolean $fromcurl Check Content-Type header from curl request
770 public function guessImageType($filename, $fromcurl = false)
772 logger('Photo: guessImageType: '.$filename . ($fromcurl?' from curl headers':''), LOGGER_DEBUG);
777 $h = explode("\n", $a->get_curl_headers());
779 list($k,$v) = array_map("trim", explode(":", trim($l), 2));
782 if (array_key_exists('Content-Type', $headers))
783 $type = $headers['Content-Type'];
785 if (is_null($type)) {
786 // Guessing from extension? Isn't that... dangerous?
787 if (class_exists('Imagick') && file_exists($filename) && is_readable($filename)) {
789 * Well, this not much better,
790 * but at least it comes from the data inside the image,
791 * we won't be tricked by a manipulated extension
793 $image = new Imagick($filename);
794 $type = $image->getImageMimeType();
795 $image->setInterlaceScheme(Imagick::INTERLACE_PLANE);
797 $ext = pathinfo($filename, PATHINFO_EXTENSION);
798 $types = $this->supportedTypes();
799 $type = "image/jpeg";
800 foreach ($types as $m => $e) {
807 logger('Photo: guessImageType: type='.$type, LOGGER_DEBUG);
812 * @param string $photo photo
813 * @param integer $uid user id
814 * @param integer $cid contact id
815 * @param boolean $quit_on_error optional, default false
818 public function importProfilePhoto($photo, $uid, $cid, $quit_on_error = false)
822 array('resource-id'),
823 array('uid' => $uid, 'contact-id' => $cid, 'scale' => 4, 'album' => 'Contact Photos'),
827 if (DBM::is_result($r) && strlen($r['resource-id'])) {
828 $hash = $r['resource-id'];
830 $hash = photo_new_resource();
833 $photo_failure = false;
835 $filename = basename($photo);
836 $img_str = fetch_url($photo, true);
838 if ($quit_on_error && ($img_str == "")) {
842 $type = $this->guessImageType($photo, true);
843 $img = new Photo($img_str, $type);
844 if ($img->isValid()) {
845 $img->scaleImageSquare(175);
847 $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 4);
850 $photo_failure = true;
853 $img->scaleImage(80);
855 $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 5);
858 $photo_failure = true;
861 $img->scaleImage(48);
863 $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 6);
866 $photo_failure = true;
869 $suffix = '?ts='.time();
871 $photo = System::baseUrl() . '/photo/' . $hash . '-4.' . $img->getExt() . $suffix;
872 $thumb = System::baseUrl() . '/photo/' . $hash . '-5.' . $img->getExt() . $suffix;
873 $micro = System::baseUrl() . '/photo/' . $hash . '-6.' . $img->getExt() . $suffix;
875 // Remove the cached photo
877 $basepath = $a->get_basepath();
879 if (is_dir($basepath."/photo")) {
880 $filename = $basepath.'/photo/'.$hash.'-4.'.$img->getExt();
881 if (file_exists($filename)) {
884 $filename = $basepath.'/photo/'.$hash.'-5.'.$img->getExt();
885 if (file_exists($filename)) {
888 $filename = $basepath.'/photo/'.$hash.'-6.'.$img->getExt();
889 if (file_exists($filename)) {
894 $photo_failure = true;
897 if ($photo_failure && $quit_on_error) {
901 if ($photo_failure) {
902 $photo = System::baseUrl() . '/images/person-175.jpg';
903 $thumb = System::baseUrl() . '/images/person-80.jpg';
904 $micro = System::baseUrl() . '/images/person-48.jpg';
907 return array($photo, $thumb, $micro);
911 * @param string $url url
914 public function getInfoFromURL($url)
918 $data = Cache::get($url);
920 if (is_null($data) || !$data || !is_array($data)) {
921 $img_str = fetch_url($url, true, $redirects, 4);
922 $filesize = strlen($img_str);
924 if (function_exists("getimagesizefromstring")) {
925 $data = getimagesizefromstring($img_str);
927 $tempfile = tempnam(get_temppath(), "cache");
930 $stamp1 = microtime(true);
931 file_put_contents($tempfile, $img_str);
932 $a->save_timestamp($stamp1, "file");
934 $data = getimagesize($tempfile);
939 $data["size"] = $filesize;
942 Cache::set($url, $data);
949 * @param integer $width width
950 * @param integer $height height
951 * @param integer $max max
954 public function scaleImageTo($width, $height, $max)
956 $dest_width = $dest_height = 0;
958 if ((!$width) || (!$height)) {
962 if ($width > $max && $height > $max) {
963 // very tall image (greater than 16:9)
964 // constrain the width - let the height float.
966 if ((($height * 9) / 16) > $width) {
968 $dest_height = intval(($height * $max) / $width);
969 } elseif ($width > $height) {
970 // else constrain both dimensions
972 $dest_height = intval(($height * $max) / $width);
974 $dest_width = intval(($width * $max) / $height);
980 $dest_height = intval(($height * $max) / $width);
982 if ($height > $max) {
983 // very tall image (greater than 16:9)
984 // but width is OK - don't do anything
986 if ((($height * 9) / 16) > $width) {
987 $dest_width = $width;
988 $dest_height = $height;
990 $dest_width = intval(($width * $max) / $height);
994 $dest_width = $width;
995 $dest_height = $height;
999 return array("width" => $dest_width, "height" => $dest_height);