diff options
author | Mario <mario@mariovavti.com> | 2019-02-18 10:02:46 +0100 |
---|---|---|
committer | Mario <mario@mariovavti.com> | 2019-02-18 10:02:46 +0100 |
commit | b5109d2a1ae6056ec845c66ad89ccb02b4de0a05 (patch) | |
tree | cc5fc990c3178af5c537a47891148a36baa77020 | |
parent | f11c17063cfb2133bd3e90b4417e8ffddbe3c29b (diff) | |
parent | b9df4c99cf9fcd1b16757309b4206ad084376aa2 (diff) | |
download | volse-hubzilla-b5109d2a1ae6056ec845c66ad89ccb02b4de0a05.tar.gz volse-hubzilla-b5109d2a1ae6056ec845c66ad89ccb02b4de0a05.tar.bz2 volse-hubzilla-b5109d2a1ae6056ec845c66ad89ccb02b4de0a05.zip |
Merge branch 'photo_driver_refactor' into 'dev'
Photo driver refactor
See merge request hubzilla/core!1515
-rw-r--r-- | Zotlabs/Photo/PhotoDriver.php | 498 | ||||
-rw-r--r-- | Zotlabs/Photo/PhotoGd.php | 176 | ||||
-rw-r--r-- | Zotlabs/Photo/PhotoImagick.php (renamed from include/photo/photo_imagick.php) | 143 | ||||
-rw-r--r-- | include/photo/photo_driver.php | 857 | ||||
-rw-r--r-- | include/photo/photo_gd.php | 162 | ||||
-rw-r--r-- | tests/unit/Photo/PhotoGdTest.php | 147 | ||||
-rw-r--r-- | tests/unit/includes/PhotodriverTest.php | 39 |
7 files changed, 1145 insertions, 877 deletions
diff --git a/Zotlabs/Photo/PhotoDriver.php b/Zotlabs/Photo/PhotoDriver.php new file mode 100644 index 000000000..c47a7c3b2 --- /dev/null +++ b/Zotlabs/Photo/PhotoDriver.php @@ -0,0 +1,498 @@ +<?php + +namespace Zotlabs\Photo; + +/** + * @brief Abstract photo driver class. + * + * Inheritance seems not to be the best design pattern for such photo drivers. + */ +abstract class PhotoDriver { + + /** + * @brief This variable keeps the image. + * + * For GD it is a PHP image resource. + * For ImageMagick it is an \Imagick object. + * + * @var resource|\Imagick + */ + protected $image; + + /** + * @var integer + */ + protected $width; + + /** + * @var integer + */ + protected $height; + + /** + * @var boolean + */ + protected $valid; + + /** + * @brief The mimetype of the image. + * + * @var string + */ + protected $type; + + /** + * @brief Supported mimetypes by the used photo driver. + * + * @var array + */ + protected $types; + + /** + * @brief Return an array with supported mimetypes. + * + * @return array + * Associative array with mimetype as key and file extension as value. + */ + abstract public function supportedTypes(); + + abstract protected function load($data, $type); + + abstract protected function destroy(); + + abstract protected function setDimensions(); + + /** + * @brief Return the current image. + * + * @fixme Shouldn't his method be protected, because outside of the current + * driver it makes no sense at all because of the different return values. + * + * @return boolean|resource|\Imagick + * false on failure, a PHP image resource for GD driver, an \Imagick object + * for ImageMagick driver. + */ + abstract public function getImage(); + + abstract public function doScaleImage($new_width, $new_height); + + abstract public function rotate($degrees); + + abstract public function flip($horiz = true, $vert = false); + + /** + * @brief Crops the image. + * + * @param int $maxx width of the new image + * @param int $maxy height of the new image + * @param int $x x-offset for region + * @param int $y y-offset for region + * @param int $w width of region + * @param int $h height of region + * + * @return boolean|void false on failure + */ + abstract public function cropImageRect($maxx, $maxy, $x, $y, $w, $h); + + /** + * @brief Return a binary string from the image resource. + * + * @return string A Binary String. + */ + abstract public function imageString(); + + abstract public function clearexif(); + + + /** + * @brief PhotoDriver constructor. + * + * @param string $data Image + * @param string $type mimetype + */ + public function __construct($data, $type = '') { + $this->types = $this->supportedTypes(); + if(! array_key_exists($type, $this->types)) { + $type = 'image/jpeg'; + } + $this->type = $type; + $this->valid = false; + $this->load($data, $type); + } + + public function __destruct() { + if($this->is_valid()) + $this->destroy(); + } + + /** + * @brief Is it a valid image object. + * + * @return boolean + */ + public function is_valid() { + return $this->valid; + } + + /** + * @brief Get the width of the image. + * + * @return boolean|number Width of image in pixels, or false on failure + */ + public function getWidth() { + if(! $this->is_valid()) + return false; + + return $this->width; + } + + /** + * @brief Get the height of the image. + * + * @return boolean|number Height of image in pixels, or false on failure + */ + public function getHeight() { + if(! $this->is_valid()) + return false; + + return $this->height; + } + + /** + * @brief Saves the image resource to a file in filesystem. + * + * @param string $path Path and filename where to save the image + * @return boolean False on failure, otherwise true + */ + public function saveImage($path) { + if(! $this->is_valid()) + return false; + + return (file_put_contents($path, $this->imageString()) ? true : false); + } + + /** + * @brief Return mimetype of the image resource. + * + * @return boolean|string False on failure, otherwise mimetype. + */ + public function getType() { + if(! $this->is_valid()) + return false; + + return $this->type; + } + + /** + * @brief Return file extension of the image resource. + * + * @return boolean|string False on failure, otherwise file extension. + */ + public function getExt() { + if(! $this->is_valid()) + return false; + + return $this->types[$this->getType()]; + } + + /** + * @brief Scale image to max pixel size in either dimension. + * + * @param int $max maximum pixel size in either dimension + * @param boolean $float_height (optional) + * If true allow height to float to any length on tall images, constraining + * only the width + * @return boolean|void false on failure, otherwise void + */ + public function scaleImage($max, $float_height = true) { + if(! $this->is_valid()) + return false; + + $width = $this->width; + $height = $this->height; + + $dest_width = $dest_height = 0; + + if((! $width) || (! $height)) + return false; + + if($width > $max && $height > $max) { + + // very tall image (greater than 16:9) + // constrain the width - let the height float. + + if(((($height * 9) / 16) > $width) && ($float_height)) { + $dest_width = $max; + $dest_height = intval(($height * $max) / $width); + } // else constrain both dimensions + elseif($width > $height) { + $dest_width = $max; + $dest_height = intval(($height * $max) / $width); + } else { + $dest_width = intval(($width * $max) / $height); + $dest_height = $max; + } + } else { + if($width > $max) { + $dest_width = $max; + $dest_height = intval(($height * $max) / $width); + } else { + if($height > $max) { + + // very tall image (greater than 16:9) + // but width is OK - don't do anything + + if(((($height * 9) / 16) > $width) && ($float_height)) { + $dest_width = $width; + $dest_height = $height; + } else { + $dest_width = intval(($width * $max) / $height); + $dest_height = $max; + } + } else { + $dest_width = $width; + $dest_height = $height; + } + } + } + $this->doScaleImage($dest_width, $dest_height); + } + + public function scaleImageUp($min) { + if(! $this->is_valid()) { + return false; + } + + $width = $this->width; + $height = $this->height; + + $dest_width = $dest_height = 0; + + if((! $width) || (! $height)) + return false; + + if($width < $min && $height < $min) { + if($width > $height) { + $dest_width = $min; + $dest_height = intval(($height * $min) / $width); + } else { + $dest_width = intval(($width * $min) / $height); + $dest_height = $min; + } + } else { + if($width < $min) { + $dest_width = $min; + $dest_height = intval(($height * $min) / $width); + } else { + if($height < $min) { + $dest_width = intval(($width * $min) / $height); + $dest_height = $min; + } else { + $dest_width = $width; + $dest_height = $height; + } + } + } + $this->doScaleImage($dest_width, $dest_height); + } + + /** + * @brief Scales image to a square. + * + * @param int $dim Pixel of square image + * @return boolean|void false on failure, otherwise void + */ + public function scaleImageSquare($dim) { + if(! $this->is_valid()) + return false; + + $this->doScaleImage($dim, $dim); + } + + /** + * @brief Crops a square image. + * + * @see cropImageRect() + * + * @param int $max size of the new image + * @param int $x x-offset for region + * @param int $y y-offset for region + * @param int $w width of region + * @param int $h height of region + * + * @return boolean|void false on failure + */ + public function cropImage($max, $x, $y, $w, $h) { + if(! $this->is_valid()) + return false; + + $this->cropImageRect($max, $max, $x, $y, $w, $h); + } + + /** + * @brief Reads exif data from a given filename. + * + * @param string $filename + * @return boolean|array + */ + public function exif($filename) { + if((! function_exists('exif_read_data')) + || (! in_array($this->getType(), ['image/jpeg', 'image/tiff']))) + { + return false; + } + + /* + * PHP 7.2 allows you to use a stream resource, which should reduce/avoid + * memory exhaustion on large images. + */ + + if(version_compare(PHP_VERSION, '7.2.0') >= 0) { + $f = @fopen($filename, 'rb'); + } else { + $f = $filename; + } + + if($f) { + return @exif_read_data($f, null, true); + } + + return false; + } + + /** + * @brief Orients current image based on exif orientation information. + * + * @param array $exif + * @return boolean true if oriented, otherwise false + */ + public function orient($exif) { + if(! ($this->is_valid() && $exif)) { + return false; + } + + $ort = ((array_key_exists('IFD0', $exif)) ? $exif['IFD0']['Orientation'] : $exif['Orientation']); + + if(! $ort) { + return false; + } + + switch($ort) { + case 1 : // nothing + break; + case 2 : // horizontal flip + $this->flip(); + break; + case 3 : // 180 rotate left + $this->rotate(180); + break; + case 4 : // vertical flip + $this->flip(false, true); + break; + case 5 : // vertical flip + 90 rotate right + $this->flip(false, true); + $this->rotate(-90); + break; + case 6 : // 90 rotate right + $this->rotate(-90); + break; + case 7 : // horizontal flip + 90 rotate right + $this->flip(); + $this->rotate(-90); + break; + case 8 : // 90 rotate left + $this->rotate(90); + break; + default : + break; + } + + return true; + } + + /** + * @brief Save photo to database. + * + * @param array $arr + * @param boolean $skipcheck (optional) default false + * @return boolean|array + */ + public function save($arr, $skipcheck = false) { + if(! ($skipcheck || $this->is_valid())) { + logger('Attempt to store invalid photo.'); + return false; + } + + $p = []; + + $p['aid'] = ((intval($arr['aid'])) ? intval($arr['aid']) : 0); + $p['uid'] = ((intval($arr['uid'])) ? intval($arr['uid']) : 0); + $p['xchan'] = (($arr['xchan']) ? $arr['xchan'] : ''); + $p['resource_id'] = (($arr['resource_id']) ? $arr['resource_id'] : ''); + $p['filename'] = (($arr['filename']) ? $arr['filename'] : ''); + $p['mimetype'] = (($arr['mimetype']) ? $arr['mimetype'] : $this->getType()); + $p['album'] = (($arr['album']) ? $arr['album'] : ''); + $p['imgscale'] = ((intval($arr['imgscale'])) ? intval($arr['imgscale']) : 0); + $p['allow_cid'] = (($arr['allow_cid']) ? $arr['allow_cid'] : ''); + $p['allow_gid'] = (($arr['allow_gid']) ? $arr['allow_gid'] : ''); + $p['deny_cid'] = (($arr['deny_cid']) ? $arr['deny_cid'] : ''); + $p['deny_gid'] = (($arr['deny_gid']) ? $arr['deny_gid'] : ''); + $p['edited'] = (($arr['edited']) ? $arr['edited'] : datetime_convert()); + $p['title'] = (($arr['title']) ? $arr['title'] : ''); + $p['description'] = (($arr['description']) ? $arr['description'] : ''); + $p['photo_usage'] = intval($arr['photo_usage']); + $p['os_storage'] = intval($arr['os_storage']); + $p['os_path'] = $arr['os_path']; + $p['os_syspath'] = ((array_key_exists('os_syspath', $arr)) ? $arr['os_syspath'] : ''); + $p['display_path'] = (($arr['display_path']) ? $arr['display_path'] : ''); + $p['width'] = (($arr['width']) ? $arr['width'] : $this->getWidth()); + $p['height'] = (($arr['height']) ? $arr['height'] : $this->getHeight()); + $p['expires'] = (($arr['expires']) ? $arr['expires'] : gmdate('Y-m-d H:i:s', time() + get_config('system', 'photo_cache_time', 86400))); + + if(! intval($p['imgscale'])) + logger('save: ' . print_r($arr, true), LOGGER_DATA); + + $x = q("select id, created from photo where resource_id = '%s' and uid = %d and xchan = '%s' and imgscale = %d limit 1", dbesc($p['resource_id']), intval($p['uid']), dbesc($p['xchan']), intval($p['imgscale'])); + + if($x) { + $p['created'] = (($x['created']) ? $x['created'] : $p['edited']); + $r = q("UPDATE photo set + aid = %d, + uid = %d, + xchan = '%s', + resource_id = '%s', + created = '%s', + edited = '%s', + filename = '%s', + mimetype = '%s', + album = '%s', + height = %d, + width = %d, + content = '%s', + os_storage = %d, + filesize = %d, + imgscale = %d, + photo_usage = %d, + title = '%s', + description = '%s', + os_path = '%s', + display_path = '%s', + allow_cid = '%s', + allow_gid = '%s', + deny_cid = '%s', + deny_gid = '%s', + expires = '%s' + where id = %d", + intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires']), intval($x[0]['id'])); + } else { + $p['created'] = (($arr['created']) ? $arr['created'] : $p['edited']); + $r = q("INSERT INTO photo + ( aid, uid, xchan, resource_id, created, edited, filename, mimetype, album, height, width, content, os_storage, filesize, imgscale, photo_usage, title, description, os_path, display_path, allow_cid, allow_gid, deny_cid, deny_gid, expires ) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )", intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires'])); + } + logger('Photo save imgscale ' . $p['imgscale'] . ' returned ' . intval($r)); + + return $r; + } + +} diff --git a/Zotlabs/Photo/PhotoGd.php b/Zotlabs/Photo/PhotoGd.php new file mode 100644 index 000000000..4054e1866 --- /dev/null +++ b/Zotlabs/Photo/PhotoGd.php @@ -0,0 +1,176 @@ +<?php + +namespace Zotlabs\Photo; + +/** + * @brief GD photo driver. + * + */ +class PhotoGd extends PhotoDriver { + + /** + * {@inheritDoc} + * @see \Zotlabs\Photo\PhotoDriver::supportedTypes() + */ + public function supportedTypes() { + $t = []; + $t['image/jpeg'] = 'jpg'; + if(imagetypes() & IMG_PNG) + $t['image/png'] = 'png'; + if(imagetypes() & IMG_GIF) + $t['image/gif'] = 'gif'; + + return $t; + } + + protected function load($data, $type) { + $this->valid = false; + if(! $data) + return; + + $this->image = @imagecreatefromstring($data); + if($this->image !== false) { + $this->valid = true; + $this->setDimensions(); + imagealphablending($this->image, false); + imagesavealpha($this->image, true); + } + } + + protected function setDimensions() { + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + /** + * @brief GD driver does not preserve EXIF, so not need to clear it. + * + * @return void + */ + public function clearexif() { + return; + } + + protected function destroy() { + if($this->is_valid()) { + imagedestroy($this->image); + } + } + + /** + * @brief Return a PHP image resource of the current image. + * + * @see \Zotlabs\Photo\PhotoDriver::getImage() + * + * @return boolean|resource + */ + public function getImage() { + if(! $this->is_valid()) + return false; + + return $this->image; + } + + public function doScaleImage($dest_width, $dest_height) { + + $dest = imagecreatetruecolor($dest_width, $dest_height); + $width = imagesx($this->image); + $height = imagesy($this->image); + + imagealphablending($dest, false); + imagesavealpha($dest, true); + if($this->type == 'image/png') + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha + + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); + if($this->image) + imagedestroy($this->image); + + $this->image = $dest; + $this->setDimensions(); + } + + public function rotate($degrees) { + if(! $this->is_valid()) + return false; + + $this->image = imagerotate($this->image, $degrees, 0); + $this->setDimensions(); + } + + public function flip($horiz = true, $vert = false) { + if(! $this->is_valid()) + return false; + + $w = imagesx($this->image); + $h = imagesy($this->image); + $flipped = imagecreate($w, $h); + if($horiz) { + for($x = 0; $x < $w; $x++) { + imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h); + } + } + if($vert) { + for($y = 0; $y < $h; $y++) { + imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1); + } + } + $this->image = $flipped; + $this->setDimensions(); // Shouldn't really be necessary + } + + public function cropImageRect($maxx, $maxy, $x, $y, $w, $h) { + if(! $this->is_valid()) + return false; + + $dest = imagecreatetruecolor($maxx, $maxy); + imagealphablending($dest, false); + imagesavealpha($dest, true); + if($this->type == 'image/png') + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha + + imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $maxx, $maxy, $w, $h); + if($this->image) + imagedestroy($this->image); + + $this->image = $dest; + $this->setDimensions(); + } + + /** + * {@inheritDoc} + * @see \Zotlabs\Photo\PhotoDriver::imageString() + */ + public function imageString() { + if(! $this->is_valid()) + return false; + + $quality = false; + + ob_start(); + + switch($this->getType()){ + case 'image/png': + $quality = get_config('system', 'png_quality'); + if((! $quality) || ($quality > 9)) + $quality = PNG_QUALITY; + + \imagepng($this->image, NULL, $quality); + break; + case 'image/jpeg': + // gd can lack imagejpeg(), but we verify during installation it is available + default: + $quality = get_config('system', 'jpeg_quality'); + if((! $quality) || ($quality > 100)) + $quality = JPEG_QUALITY; + + \imagejpeg($this->image, NULL, $quality); + break; + } + $string = ob_get_contents(); + ob_end_clean(); + + return $string; + } + +} diff --git a/include/photo/photo_imagick.php b/Zotlabs/Photo/PhotoImagick.php index cb3ad27fb..a7026e8ca 100644 --- a/include/photo/photo_imagick.php +++ b/Zotlabs/Photo/PhotoImagick.php @@ -1,45 +1,44 @@ -<?php /** @file */ +<?php +namespace Zotlabs\Photo; -require_once('include/photo/photo_driver.php'); +/** + * @brief ImageMagick photo driver. + */ +class PhotoImagick extends PhotoDriver { - -class photo_imagick extends photo_driver { - - - function supportedTypes() { - return array( + public function supportedTypes() { + return [ 'image/jpeg' => 'jpg', 'image/png' => 'png', - 'image/gif' => 'gif' - ); + 'image/gif' => 'gif', + ]; } - public function get_FormatsMap() { - return array( + private function get_FormatsMap() { + return [ 'image/jpeg' => 'JPG', 'image/png' => 'PNG', - 'image/gif' => 'GIF' - ); + 'image/gif' => 'GIF', + ]; } - function load($data, $type) { + protected function load($data, $type) { $this->valid = false; - $this->image = new Imagick(); + $this->image = new \Imagick(); if(! $data) return; try { $this->image->readImageBlob($data); - } - catch (Exception $e) { - logger('imagick readImageBlob() exception:' . print_r($e,true)); + } catch(\Exception $e) { + logger('Imagick readImageBlob() exception:' . print_r($e, true)); return; } - /** + /* * Setup the image to the format it will be saved to */ @@ -52,19 +51,18 @@ class photo_imagick extends photo_driver { // Always coalesce, if it is not a multi-frame image it won't hurt anyway $this->image = $this->image->coalesceImages(); - $this->valid = true; $this->setDimensions(); - /** + /* * setup the compression here, so we'll do it only once */ switch($this->getType()) { - case "image/png": - $quality = get_config('system','png_quality'); + case 'image/png': + $quality = get_config('system', 'png_quality'); if((! $quality) || ($quality > 9)) $quality = PNG_QUALITY; - /** + /* * From http://www.imagemagick.org/script/command-line-options.php#quality: * * 'For the MNG and PNG image formats, the quality value sets @@ -75,56 +73,64 @@ class photo_imagick extends photo_driver { $quality = $quality * 10; $this->image->setCompressionQuality($quality); break; - case "image/jpeg": - $quality = get_config('system','jpeg_quality'); + case 'image/jpeg': + $quality = get_config('system', 'jpeg_quality'); if((! $quality) || ($quality > 100)) $quality = JPEG_QUALITY; $this->image->setCompressionQuality($quality); default: break; - } } } - public function destroy() { + protected function destroy() { if($this->is_valid()) { $this->image->clear(); $this->image->destroy(); } } - - public function setDimensions() { + protected function setDimensions() { $this->width = $this->image->getImageWidth(); $this->height = $this->image->getImageHeight(); } - + /** + * @brief Strips the image of all profiles and comments. + * + * Keep ICC profile for better colors. + * + * @see \Zotlabs\Photo\PhotoDriver::clearexif() + */ public function clearexif() { - - $profiles = $this->image->getImageProfiles("icc", true); + $profiles = $this->image->getImageProfiles('icc', true); $this->image->stripImage(); - if(!empty($profiles)) { - $this->image->profileImage("icc", $profiles['icc']); + if(! empty($profiles)) { + $this->image->profileImage('icc', $profiles['icc']); } } - + /** + * @brief Return a \Imagick object of the current image. + * + * @see \Zotlabs\Photo\PhotoDriver::getImage() + * + * @return boolean|\Imagick + */ public function getImage() { - if(!$this->is_valid()) - return FALSE; + if(! $this->is_valid()) + return false; $this->image = $this->image->deconstructImages(); return $this->image; } - public function doScaleImage($dest_width,$dest_height) { - - /** + public function doScaleImage($dest_width, $dest_height) { + /* * If it is not animated, there will be only one iteration here, * so don't bother checking */ @@ -132,82 +138,63 @@ class photo_imagick extends photo_driver { $this->image->setFirstIterator(); do { $this->image->scaleImage($dest_width, $dest_height); - } while ($this->image->nextImage()); + } while($this->image->nextImage()); $this->setDimensions(); } public function rotate($degrees) { - if(!$this->is_valid()) - return FALSE; + if(! $this->is_valid()) + return false; $this->image->setFirstIterator(); do { // ImageMagick rotates in the opposite direction of imagerotate() - $this->image->rotateImage(new ImagickPixel(), -$degrees); - } while ($this->image->nextImage()); + $this->image->rotateImage(new \ImagickPixel(), -$degrees); + } while($this->image->nextImage()); $this->setDimensions(); } public function flip($horiz = true, $vert = false) { - if(!$this->is_valid()) - return FALSE; + if(! $this->is_valid()) + return false; $this->image->setFirstIterator(); do { if($horiz) $this->image->flipImage(); if($vert) $this->image->flopImage(); - } while ($this->image->nextImage()); + } while($this->image->nextImage()); $this->setDimensions(); // Shouldn't really be necessary } - public function cropImage($max,$x,$y,$w,$h) { - if(!$this->is_valid()) - return FALSE; - - $this->image->setFirstIterator(); - do { - $this->image->cropImage($w, $h, $x, $y); - /** - * We need to remove the canvas, - * or the image is not resized to the crop: - * http://php.net/manual/en/imagick.cropimage.php#97232 - */ - $this->image->setImagePage(0, 0, 0, 0); - } while ($this->image->nextImage()); - - $this->doScaleImage($max,$max); - } - - public function cropImageRect($maxx,$maxy,$x,$y,$w,$h) { - if(!$this->is_valid()) - return FALSE; + public function cropImageRect($maxx, $maxy, $x, $y, $w, $h) { + if(! $this->is_valid()) + return false; $this->image->setFirstIterator(); do { $this->image->cropImage($w, $h, $x, $y); - /** + /* * We need to remove the canvas, * or the image is not resized to the crop: * http://php.net/manual/en/imagick.cropimage.php#97232 */ $this->image->setImagePage(0, 0, 0, 0); - } while ($this->image->nextImage()); + } while($this->image->nextImage()); - $this->doScaleImage($maxx,$maxy); + $this->doScaleImage($maxx, $maxy); } public function imageString() { - if(!$this->is_valid()) - return FALSE; + if(! $this->is_valid()) + return false; /* Clean it */ $this->image = $this->image->deconstructImages(); + return $this->image->getImagesBlob(); } - - } diff --git a/include/photo/photo_driver.php b/include/photo/photo_driver.php index 5c8ed9bdc..c11580bdc 100644 --- a/include/photo/photo_driver.php +++ b/include/photo/photo_driver.php @@ -1,18 +1,34 @@ -<?php /** @file */ +<?php +use Zotlabs\Photo\PhotoDriver; +use Zotlabs\Photo\PhotoGd; +use Zotlabs\Photo\PhotoImagick; + +/** + * @brief Return a PhotoDriver object. + * + * Use this factory when manipulating images. + * + * Return a photo driver object implementing ImageMagick or GD. + * + * @param string $data Image data + * @param string $type Mimetype + * @return null|PhotoDriver + * NULL if unsupported image type or failure, otherwise photo driver object + */ function photo_factory($data, $type = null) { $ph = null; + $m = null; - - $unsupported_types = array( + $unsupported_types = [ 'image/bmp', 'image/vnd.microsoft.icon', 'image/tiff', - 'image/svg+xml' - ); + 'image/svg+xml', + ]; - if($type && in_array(strtolower($type),$unsupported_types)) { - logger('photo_factory: unsupported image type'); + if($type && in_array(strtolower($type), $unsupported_types)) { + logger('Unsupported image type ' . $type); return null; } @@ -21,475 +37,47 @@ function photo_factory($data, $type = null) { if(class_exists('Imagick') && !$ignore_imagick) { $v = Imagick::getVersion(); preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $m); - if(version_compare($m[1],'6.6.7') >= 0) { - require_once('include/photo/photo_imagick.php'); - $ph = new photo_imagick($data,$type); - } - else { + if(version_compare($m[1], '6.6.7') >= 0) { + $ph = new PhotoImagick($data, $type); + } else { // earlier imagick versions have issues with scaling png's // don't log this because it will just fill the logfile. - // leave this note here so those who are looking for why + // leave this note here so those who are looking for why // we aren't using imagick can find it } } if(! $ph) { - require_once('include/photo/photo_gd.php'); - $ph = new photo_gd($data,$type); + $ph = new PhotoGd($data, $type); } return $ph; } - - -abstract class photo_driver { - - protected $image; - protected $width; - protected $height; - protected $valid; - protected $type; - protected $types; - - abstract function supportedTypes(); - - abstract function load($data,$type); - - abstract function destroy(); - - abstract function setDimensions(); - - abstract function getImage(); - - abstract function doScaleImage($new_width,$new_height); - - abstract function rotate($degrees); - - abstract function flip($horiz = true, $vert = false); - - abstract function cropImage($max,$x,$y,$w,$h); - - abstract function cropImageRect($maxx,$maxy,$x,$y,$w,$h); - - abstract function imageString(); - - abstract function clearexif(); - - public function __construct($data, $type='') { - $this->types = $this->supportedTypes(); - if (! array_key_exists($type,$this->types)){ - $type='image/jpeg'; - } - $this->type = $type; - $this->valid = false; - $this->load($data,$type); - } - - public function __destruct() { - if($this->is_valid()) - $this->destroy(); - } - - public function is_valid() { - return $this->valid; - } - - public function getWidth() { - if(!$this->is_valid()) - return FALSE; - return $this->width; - } - - public function getHeight() { - if(!$this->is_valid()) - return FALSE; - return $this->height; - } - - - public function saveImage($path) { - if(!$this->is_valid()) - return FALSE; - return (file_put_contents($path, $this->imageString()) ? true : false); - } - - - public function getType() { - if(!$this->is_valid()) - return FALSE; - - return $this->type; - } - - public function getExt() { - if(!$this->is_valid()) - return FALSE; - - return $this->types[$this->getType()]; - } - - /** - * @brief scale image - * int $max maximum pixel size in either dimension - * boolean $float_height - if true allow height to float to any length on tall images, - * constraining only the width - */ - - public function scaleImage($max, $float_height = true) { - if(!$this->is_valid()) - return FALSE; - - $width = $this->width; - $height = $this->height; - - $dest_width = $dest_height = 0; - - if((! $width)|| (! $height)) - return FALSE; - - if($width > $max && $height > $max) { - - // very tall image (greater than 16:9) - // constrain the width - let the height float. - - if(((($height * 9) / 16) > $width) && ($float_height)) { - $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } - - // else constrain both dimensions - - elseif($width > $height) { - $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } - else { - $dest_width = intval(( $width * $max ) / $height); - $dest_height = $max; - } - } - else { - if( $width > $max ) { - $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } - else { - if( $height > $max ) { - - // very tall image (greater than 16:9) - // but width is OK - don't do anything - - if(((($height * 9) / 16) > $width) && ($float_height)) { - $dest_width = $width; - $dest_height = $height; - } - else { - $dest_width = intval(( $width * $max ) / $height); - $dest_height = $max; - } - } - else { - $dest_width = $width; - $dest_height = $height; - } - } - } - $this->doScaleImage($dest_width,$dest_height); - } - - public function scaleImageUp($min) { - if(!$this->is_valid()) - return FALSE; - - - $width = $this->width; - $height = $this->height; - - $dest_width = $dest_height = 0; - - if((! $width)|| (! $height)) - return FALSE; - - if($width < $min && $height < $min) { - if($width > $height) { - $dest_width = $min; - $dest_height = intval(( $height * $min ) / $width); - } - else { - $dest_width = intval(( $width * $min ) / $height); - $dest_height = $min; - } - } - else { - if( $width < $min ) { - $dest_width = $min; - $dest_height = intval(( $height * $min ) / $width); - } - else { - if( $height < $min ) { - $dest_width = intval(( $width * $min ) / $height); - $dest_height = $min; - } - else { - $dest_width = $width; - $dest_height = $height; - } - } - } - $this->doScaleImage($dest_width,$dest_height); - } - - - public function scaleImageSquare($dim) { - if(!$this->is_valid()) - return FALSE; - $this->doScaleImage($dim,$dim); - } - - - /** - * @brief reads exif data from filename - */ - - public function exif($filename) { - - - if((! function_exists('exif_read_data')) - || (! in_array($this->getType(), [ 'image/jpeg' , 'image/tiff'] ))) { - return false; - } - - /* - * PHP 7.2 allows you to use a stream resource, which should reduce/avoid - * memory exhaustion on large images. - */ - - if(version_compare(PHP_VERSION,'7.2.0') >= 0) { - $f = @fopen($filename,'rb'); - } - else { - $f = $filename; - } - - if($f) { - return @exif_read_data($f,null,true); - } - - return false; - } - - /** - * @brief orients current image based on exif orientation information - */ - - public function orient($exif) { - - if(! ($this->is_valid() && $exif)) { - return false; - } - - $ort = ((array_key_exists('IFD0',$exif)) ? $exif['IFD0']['Orientation'] : $exif['Orientation']); - - if(! $ort) { - return false; - } - - switch($ort) { - case 1: // nothing - break; - case 2: // horizontal flip - $this->flip(); - break; - case 3: // 180 rotate left - $this->rotate(180); - break; - case 4: // vertical flip - $this->flip(false, true); - break; - case 5: // vertical flip + 90 rotate right - $this->flip(false, true); - $this->rotate(-90); - break; - case 6: // 90 rotate right - $this->rotate(-90); - break; - case 7: // horizontal flip + 90 rotate right - $this->flip(); - $this->rotate(-90); - break; - case 8: // 90 rotate left - $this->rotate(90); - break; - default: - break; - } - - return true; - } - - - public function save($arr, $skipcheck = false) { - - if(! ($skipcheck || $this->is_valid())) { - logger('attempt to store invalid photo.'); - return false; - } - - $p = array(); - - $p['aid'] = ((intval($arr['aid'])) ? intval($arr['aid']) : 0); - $p['uid'] = ((intval($arr['uid'])) ? intval($arr['uid']) : 0); - $p['xchan'] = (($arr['xchan']) ? $arr['xchan'] : ''); - $p['resource_id'] = (($arr['resource_id']) ? $arr['resource_id'] : ''); - $p['filename'] = (($arr['filename']) ? $arr['filename'] : ''); - $p['mimetype'] = (($arr['mimetype']) ? $arr['mimetype'] : $this->getType()); - $p['album'] = (($arr['album']) ? $arr['album'] : ''); - $p['imgscale'] = ((intval($arr['imgscale'])) ? intval($arr['imgscale']) : 0); - $p['allow_cid'] = (($arr['allow_cid']) ? $arr['allow_cid'] : ''); - $p['allow_gid'] = (($arr['allow_gid']) ? $arr['allow_gid'] : ''); - $p['deny_cid'] = (($arr['deny_cid']) ? $arr['deny_cid'] : ''); - $p['deny_gid'] = (($arr['deny_gid']) ? $arr['deny_gid'] : ''); - $p['edited'] = (($arr['edited']) ? $arr['edited'] : datetime_convert()); - $p['title'] = (($arr['title']) ? $arr['title'] : ''); - $p['description'] = (($arr['description']) ? $arr['description'] : ''); - $p['photo_usage'] = intval($arr['photo_usage']); - $p['os_storage'] = intval($arr['os_storage']); - $p['os_path'] = $arr['os_path']; - $p['os_syspath'] = ((array_key_exists('os_syspath',$arr)) ? $arr['os_syspath'] : ''); - $p['display_path'] = (($arr['display_path']) ? $arr['display_path'] : ''); - $p['width'] = (($arr['width']) ? $arr['width'] : $this->getWidth()); - $p['height'] = (($arr['height']) ? $arr['height'] : $this->getHeight()); - $p['expires'] = (($arr['expires']) ? $arr['expires'] : gmdate('Y-m-d H:i:s', time() + get_config('system','photo_cache_time', 86400))); - - if(! intval($p['imgscale'])) - logger('save: ' . print_r($arr,true), LOGGER_DATA); - - $x = q("select id, created from photo where resource_id = '%s' and uid = %d and xchan = '%s' and imgscale = %d limit 1", - dbesc($p['resource_id']), - intval($p['uid']), - dbesc($p['xchan']), - intval($p['imgscale']) - ); - - if($x) { - $p['created'] = (($x['created']) ? $x['created'] : $p['edited']); - $r = q("UPDATE photo set - aid = %d, - uid = %d, - xchan = '%s', - resource_id = '%s', - created = '%s', - edited = '%s', - filename = '%s', - mimetype = '%s', - album = '%s', - height = %d, - width = %d, - content = '%s', - os_storage = %d, - filesize = %d, - imgscale = %d, - photo_usage = %d, - title = '%s', - description = '%s', - os_path = '%s', - display_path = '%s', - allow_cid = '%s', - allow_gid = '%s', - deny_cid = '%s', - deny_gid = '%s', - expires = '%s' - where id = %d", - - intval($p['aid']), - intval($p['uid']), - dbesc($p['xchan']), - dbesc($p['resource_id']), - dbescdate($p['created']), - dbescdate($p['edited']), - dbesc(basename($p['filename'])), - dbesc($p['mimetype']), - dbesc($p['album']), - intval($p['height']), - intval($p['width']), - (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), - intval($p['os_storage']), - (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), - intval($p['imgscale']), - intval($p['photo_usage']), - dbesc($p['title']), - dbesc($p['description']), - dbesc($p['os_path']), - dbesc($p['display_path']), - dbesc($p['allow_cid']), - dbesc($p['allow_gid']), - dbesc($p['deny_cid']), - dbesc($p['deny_gid']), - dbescdate($p['expires']), - intval($x[0]['id']) - ); - } - else { - $p['created'] = (($arr['created']) ? $arr['created'] : $p['edited']); - $r = q("INSERT INTO photo - ( aid, uid, xchan, resource_id, created, edited, filename, mimetype, album, height, width, content, os_storage, filesize, imgscale, photo_usage, title, description, os_path, display_path, allow_cid, allow_gid, deny_cid, deny_gid, expires ) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )", - intval($p['aid']), - intval($p['uid']), - dbesc($p['xchan']), - dbesc($p['resource_id']), - dbescdate($p['created']), - dbescdate($p['edited']), - dbesc(basename($p['filename'])), - dbesc($p['mimetype']), - dbesc($p['album']), - intval($p['height']), - intval($p['width']), - (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), - intval($p['os_storage']), - (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), - intval($p['imgscale']), - intval($p['photo_usage']), - dbesc($p['title']), - dbesc($p['description']), - dbesc($p['os_path']), - dbesc($p['display_path']), - dbesc($p['allow_cid']), - dbesc($p['allow_gid']), - dbesc($p['deny_cid']), - dbesc($p['deny_gid']), - dbescdate($p['expires']) - ); - } - logger('photo save ' . $p['imgscale'] . ' returned ' . intval($r)); - return $r; - } - -} - - - /** - * Guess image mimetype from filename or from Content-Type header + * @brief Guess image mimetype from filename or from Content-Type header. * - * @arg $filename string Image filename - * @arg $headers string Headers to check for Content-Type (from curl request) + * @param string $filename + * Image filename + * @param string $headers (optional) + * Headers to check for Content-Type (from curl request) + * @return null|string Guessed mimetype */ - function guess_image_type($filename, $headers = '') { // logger('Photo: guess_image_type: '.$filename . ($headers?' from curl headers':''), LOGGER_DEBUG); $type = null; - if ($headers) { - $hdrs=array(); - $h = explode("\n",$headers); + $m = null; + + if($headers) { + $hdrs = []; + $h = explode("\n", $headers); foreach ($h as $l) { - list($k,$v) = array_map("trim", explode(":", trim($l), 2)); + list($k, $v) = array_map('trim', explode(':', trim($l), 2)); $hdrs[strtolower($k)] = $v; } - logger('Curl headers: '.var_export($hdrs, true), LOGGER_DEBUG); - if (array_key_exists('content-type', $hdrs)) { + logger('Curl headers: ' .var_export($hdrs, true), LOGGER_DEBUG); + if(array_key_exists('content-type', $hdrs)) { $ph = photo_factory(''); $types = $ph->supportedTypes(); @@ -498,13 +86,13 @@ function guess_image_type($filename, $headers = '') { } } - if (is_null($type)){ + if(is_null($type)){ $ignore_imagick = get_config('system', 'ignore_imagick'); // Guessing from extension? Isn't that... dangerous? if(class_exists('Imagick') && file_exists($filename) && is_readable($filename) && !$ignore_imagick) { $v = Imagick::getVersion(); preg_match('/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $m); - if(version_compare($m[1],'6.6.7') >= 0) { + if(version_compare($m[1], '6.6.7') >= 0) { /** * Well, this not much better, * but at least it comes from the data inside the image, @@ -516,7 +104,7 @@ function guess_image_type($filename, $headers = '') { else { // earlier imagick versions have issues with scaling png's // don't log this because it will just fill the logfile. - // leave this note here so those who are looking for why + // leave this note here so those who are looking for why // we aren't using imagick can find it } } @@ -532,7 +120,7 @@ function guess_image_type($filename, $headers = '') { } } - if(is_null($type) && (strpos($filename,'http') === false)) { + if(is_null($type) && (strpos($filename, 'http') === false)) { $size = getimagesize($filename); $ph = photo_factory(''); $types = $ph->supportedTypes(); @@ -551,33 +139,36 @@ function guess_image_type($filename, $headers = '') { } logger('Photo: guess_image_type: filename = ' . $filename . ' type = ' . $type, LOGGER_DEBUG); - return $type; + return $type; } - -function delete_thing_photo($url,$ob_hash) { +/** + * @brief Delete thing photo from database. + * + * @param string $url + * @param string $ob_hash + * @return void + */ +function delete_thing_photo($url, $ob_hash) { $hash = basename($url); - $hash = substr($hash,0,strpos($hash,'-')); + $hash = substr($hash, 0, strpos($hash, '-')); - // hashes should be 32 bytes. + // hashes should be 32 bytes. if((! $ob_hash) || (strlen($hash) < 16)) - return; + return; - $r = q("delete from photo where xchan = '%s' and photo_usage = %d and resource_id = '%s'", + q("delete from photo where xchan = '%s' and photo_usage = %d and resource_id = '%s'", dbesc($ob_hash), intval(PHOTO_THING), dbesc($hash) ); - } - - /** - * @brief fetches an photo from external site and prepares its miniatures. + * @brief Fetches a photo from external site and prepares its miniatures. * * @param string $photo * external URL to fetch base image @@ -596,223 +187,215 @@ function delete_thing_photo($url,$ob_hash) { * * \e boolean \b 4 => TRUE if fetch failure * * \e string \b 5 => modification date */ +function import_xchan_photo($photo, $xchan, $thing = false, $force = false) { + $modified = ''; + $o = null; + + $flags = (($thing) ? PHOTO_THING : PHOTO_XCHAN); + $album = (($thing) ? 'Things' : 'Contact Photos'); + + logger('Updating channel photo from ' . $photo . ' for ' . $xchan, LOGGER_DEBUG); + + if($thing) { + $hash = photo_new_resource(); + } else { + $r = q("select resource_id, edited, mimetype from photo where xchan = '%s' and photo_usage = %d and imgscale = 4 limit 1", dbesc($xchan), intval(PHOTO_XCHAN)); + if($r) { + $hash = $r[0]['resource_id']; + $modified = $r[0]['edited']; + $type = $r[0]['mimetype']; + } else { + $hash = photo_new_resource(); + } + } -function import_xchan_photo($photo,$xchan,$thing = false,$force = false) { - - $modified = ''; - - $flags = (($thing) ? PHOTO_THING : PHOTO_XCHAN); - $album = (($thing) ? 'Things' : 'Contact Photos'); - - logger('import_xchan_photo: updating channel photo from ' . $photo . ' for ' . $xchan, LOGGER_DEBUG); - - if($thing) { - $hash = photo_new_resource(); - } - else { - $r = q("select resource_id, edited, mimetype from photo where xchan = '%s' and photo_usage = %d and imgscale = 4 limit 1", - dbesc($xchan), - intval(PHOTO_XCHAN) - ); - if($r) { - $hash = $r[0]['resource_id']; - $modified = $r[0]['edited']; - $type = $r[0]['mimetype']; - } - else { - $hash = photo_new_resource(); - } - } - - $photo_failure = false; - $img_str = ''; - - if($photo) { - $filename = basename($photo); - - if($force || $modified == '') { - $result = z_fetch_url($photo,true); - } - else { - $h = array('headers' => array("If-Modified-Since: " . gmdate("D, d M Y H:i:s", strtotime($modified . "Z")) . " GMT")); - $result = z_fetch_url($photo,true,0,$h); - } - - if($result['success']) { - $img_str = $result['body']; - $type = guess_image_type($photo, $result['header']); - $modified = gmdate('Y-m-d H:i:s', (preg_match('/last-modified: (.+) \S+/i', $result['header'], $o) ? strtotime($o[1] . 'Z') : time())); - if(is_null($type)) - $photo_failure = true; - } - elseif($result['return_code'] == 304) { - $photo = z_root() . '/photo/' . $hash . '-4'; - $thumb = z_root() . '/photo/' . $hash . '-5'; - $micro = z_root() . '/photo/' . $hash . '-6'; - } - else { - $photo_failure = true; - } - - } - else - $photo_failure = true; - - if(! $photo_failure && $result['return_code'] != 304) { - $img = photo_factory($img_str, $type); - if($img->is_valid()) { - $width = $img->getWidth(); - $height = $img->getHeight(); - - if($width && $height) { - if(($width / $height) > 1.2) { - // crop out the sides - $margin = $width - $height; - $img->cropImage(300,($margin / 2),0,$height,$height); - } - elseif(($height / $width) > 1.2) { - // crop out the bottom - $margin = $height - $width; - $img->cropImage(300,0,0,$width,$width); - - } - else { - $img->scaleImageSquare(300); - } - - } - else - $photo_failure = true; - - $p = array( - 'xchan' => $xchan, - 'resource_id' => $hash, - 'filename' => basename($photo), - 'album' => $album, - 'photo_usage' => $flags, - 'imgscale' => 4, - 'edited' => $modified - ); - - $r = $img->save($p); - - if($r === false) - $photo_failure = true; - - $img->scaleImage(80); - $p['imgscale'] = 5; - - $r = $img->save($p); - - if($r === false) - $photo_failure = true; - - $img->scaleImage(48); - $p['imgscale'] = 6; - - $r = $img->save($p); - - if($r === false) - $photo_failure = true; - - $photo = z_root() . '/photo/' . $hash . '-4'; - $thumb = z_root() . '/photo/' . $hash . '-5'; - $micro = z_root() . '/photo/' . $hash . '-6'; - } - else { - logger('import_xchan_photo: invalid image from ' . $photo); - $photo_failure = true; - } - } - if($photo_failure) { - $default = get_default_profile_photo(); - $photo = z_root() . '/' . $default; - $thumb = z_root() . '/' . get_default_profile_photo(80); - $micro = z_root() . '/' . get_default_profile_photo(48); - $type = 'image/png'; - $modified = gmdate('Y-m-d H:i:s', filemtime($default)); - } - - logger('HTTP code: ' . $result['return_code'] . '; modified: ' . $modified . '; failure: ' . ($photo_failure ? 'yes' : 'no') . '; URL: ' . $photo, LOGGER_DEBUG); - return(array($photo,$thumb,$micro,$type,$photo_failure,$modified)); - -} - -function import_channel_photo_from_url($photo,$aid,$uid) { + $photo_failure = false; + $img_str = ''; if($photo) { - $filename = basename($photo); - $result = z_fetch_url($photo,true); + if($force || $modified == '') { + $result = z_fetch_url($photo, true); + } else { + $h = [ + 'headers' => [ + 'If-Modified-Since: ' . gmdate('D, d M Y H:i:s', strtotime($modified . 'Z')) . ' GMT' + ] + ]; + $result = z_fetch_url($photo, true, 0, $h); + } if($result['success']) { $img_str = $result['body']; $type = guess_image_type($photo, $result['header']); + $modified = gmdate('Y-m-d H:i:s', (preg_match('/last-modified: (.+) \S+/i', $result['header'], $o) ? strtotime($o[1] . 'Z') : time())); + if(is_null($type)) + $photo_failure = true; + } elseif($result['return_code'] == 304) { + $photo = z_root() . '/photo/' . $hash . '-4'; + $thumb = z_root() . '/photo/' . $hash . '-5'; + $micro = z_root() . '/photo/' . $hash . '-6'; + } else { + $photo_failure = true; + } + } else { + $photo_failure = true; + } + + if(!$photo_failure && $result['return_code'] != 304) { + $img = photo_factory($img_str, $type); + if($img->is_valid()) { + $width = $img->getWidth(); + $height = $img->getHeight(); + + if($width && $height) { + if(($width / $height) > 1.2) { + // crop out the sides + $margin = $width - $height; + $img->cropImage(300, ($margin / 2), 0, $height, $height); + } elseif(($height / $width) > 1.2) { + // crop out the bottom + $margin = $height - $width; + $img->cropImage(300, 0, 0, $width, $width); + } else { + $img->scaleImageSquare(300); + } + } else { + $photo_failure = true; + } - if(is_null($type)) - $photo_failure = true; + $p = [ + 'xchan' => $xchan, + 'resource_id' => $hash, + 'filename' => basename($photo), + 'album' => $album, + 'photo_usage' => $flags, + 'imgscale' => 4, + 'edited' => $modified, + ]; + + $r = $img->save($p); + if($r === false) + $photo_failure = true; + + $img->scaleImage(80); + $p['imgscale'] = 5; + $r = $img->save($p); + if($r === false) + $photo_failure = true; + + $img->scaleImage(48); + $p['imgscale'] = 6; + $r = $img->save($p); + if($r === false) + $photo_failure = true; + + $photo = z_root() . '/photo/' . $hash . '-4'; + $thumb = z_root() . '/photo/' . $hash . '-5'; + $micro = z_root() . '/photo/' . $hash . '-6'; + } else { + logger('Invalid image from ' . $photo); + $photo_failure = true; } } - else { - $photo_failure = true; + if($photo_failure) { + $default = get_default_profile_photo(); + $photo = z_root() . '/' . $default; + $thumb = z_root() . '/' . get_default_profile_photo(80); + $micro = z_root() . '/' . get_default_profile_photo(48); + $type = 'image/png'; + $modified = gmdate('Y-m-d H:i:s', filemtime($default)); } - import_channel_photo($img_str,$type,$aid,$uid); + logger('HTTP code: ' . $result['return_code'] . '; modified: ' . $modified + . '; failure: ' . ($photo_failure ? 'yes' : 'no') . '; URL: ' . $photo, LOGGER_DEBUG); - return $type; + return([$photo, $thumb, $micro, $type, $photo_failure, $modified]); } +/** + * @brief Import channel photo from a URL. + * + * @param string $photo URL to a photo + * @param int $aid + * @param int $uid channel_id + * @return null|string Guessed image mimetype or null. + */ +function import_channel_photo_from_url($photo, $aid, $uid) { + $type = null; -function import_channel_photo($photo,$type,$aid,$uid) { + if($photo) { + $result = z_fetch_url($photo, true); - logger('import_channel_photo: importing channel photo for ' . $uid, LOGGER_DEBUG); + if($result['success']) { + $img_str = $result['body']; + $type = guess_image_type($photo, $result['header']); - $hash = photo_new_resource(); + import_channel_photo($img_str, $type, $aid, $uid); + } + } - $photo_failure = false; + return $type; +} +/** + * @brief Import a channel photo and prepare its miniatures. + * + * @param string $photo Image data + * @param string $type + * @param int $aid + * @param int $uid channel_id + * @return boolean|string false on failure, otherwise resource_id of photo + */ +function import_channel_photo($photo, $type, $aid, $uid) { + logger('Importing channel photo for ' . $uid, LOGGER_DEBUG); + + $photo_failure = false; + $hash = photo_new_resource(); $filename = $hash; $img = photo_factory($photo, $type); if($img->is_valid()) { + // config array for image save method + $p = [ + 'aid' => $aid, + 'uid' => $uid, + 'resource_id' => $hash, + 'filename' => $filename, + 'album' => t('Profile Photos'), + 'photo_usage' => PHOTO_PROFILE, + 'imgscale' => 4, + ]; + + // photo size $img->scaleImageSquare(300); - - $p = array('aid' => $aid, 'uid' => $uid, 'resource_id' => $hash, 'filename' => $filename, 'album' => t('Profile Photos'), 'photo_usage' => PHOTO_PROFILE, 'imgscale' => 4); - $r = $img->save($p); - if($r === false) $photo_failure = true; + // thumb size $img->scaleImage(80); $p['imgscale'] = 5; - $r = $img->save($p); - if($r === false) $photo_failure = true; + // micro size $img->scaleImage(48); $p['imgscale'] = 6; - $r = $img->save($p); - if($r === false) $photo_failure = true; - } - else { - logger('import_channel_photo: invalid image.'); + } else { + logger('Invalid image.'); $photo_failure = true; } - //return(($photo_failure)? false : true); - if($photo_failure) return false; else return $hash; - } diff --git a/include/photo/photo_gd.php b/include/photo/photo_gd.php deleted file mode 100644 index e98ac2827..000000000 --- a/include/photo/photo_gd.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php /** @file */ - - -require_once('include/photo/photo_driver.php'); - - -class photo_gd extends photo_driver { - - function supportedTypes() { - $t = array(); - $t['image/jpeg'] ='jpg'; - if (imagetypes() & IMG_PNG) $t['image/png'] = 'png'; - if (imagetypes() & IMG_GIF) $t['image/gif'] = 'gif'; - return $t; - - } - - function load($data, $type) { - $this->valid = false; - if(! $data) - return; - - $this->image = @imagecreatefromstring($data); - if($this->image !== FALSE) { - $this->valid = true; - $this->setDimensions(); - imagealphablending($this->image, false); - imagesavealpha($this->image, true); - } - } - - function setDimensions() { - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } - - - public function clearexif() { - return; - } - - - public function destroy() { - if($this->is_valid()) { - imagedestroy($this->image); - } - } - - public function getImage() { - if(!$this->is_valid()) - return FALSE; - - return $this->image; - } - - public function doScaleImage($dest_width,$dest_height) { - - $dest = imagecreatetruecolor( $dest_width, $dest_height ); - $width = imagesx($this->image); - $height = imagesy($this->image); - - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->setDimensions(); - } - - public function rotate($degrees) { - if(!$this->is_valid()) - return FALSE; - - $this->image = imagerotate($this->image,$degrees,0); - $this->setDimensions(); - } - - public function flip($horiz = true, $vert = false) { - if(!$this->is_valid()) - return FALSE; - - $w = imagesx($this->image); - $h = imagesy($this->image); - $flipped = imagecreate($w, $h); - if($horiz) { - for ($x = 0; $x < $w; $x++) { - imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h); - } - } - if($vert) { - for ($y = 0; $y < $h; $y++) { - imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1); - } - } - $this->image = $flipped; - $this->setDimensions(); // Shouldn't really be necessary - } - - public function cropImage($max,$x,$y,$w,$h) { - if(!$this->is_valid()) - return FALSE; - - $dest = imagecreatetruecolor( $max, $max ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->setDimensions(); - } - - public function cropImageRect($maxx,$maxy,$x,$y,$w,$h) { - if(!$this->is_valid()) - return FALSE; - - $dest = imagecreatetruecolor( $maxx, $maxy ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $maxx, $maxy, $w, $h); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->setDimensions(); - } - - - - public function imageString() { - if(!$this->is_valid()) - return FALSE; - - $quality = FALSE; - - ob_start(); - - switch($this->getType()){ - case "image/png": - $quality = get_config('system','png_quality'); - if((! $quality) || ($quality > 9)) - $quality = PNG_QUALITY; - imagepng($this->image,NULL, $quality); - break; - case "image/jpeg": - default: - $quality = get_config('system','jpeg_quality'); - if((! $quality) || ($quality > 100)) - $quality = JPEG_QUALITY; - imagejpeg($this->image,NULL,$quality); - break; - } - $string = ob_get_contents(); - ob_end_clean(); - - return $string; - } - -} diff --git a/tests/unit/Photo/PhotoGdTest.php b/tests/unit/Photo/PhotoGdTest.php new file mode 100644 index 000000000..1d4f9467f --- /dev/null +++ b/tests/unit/Photo/PhotoGdTest.php @@ -0,0 +1,147 @@ +<?php + +namespace Zotlabs\Tests\Unit\Photo; + +use Zotlabs\Photo\PhotoGd; +use phpmock\phpunit\PHPMock; +use Zotlabs\Tests\Unit\UnitTestCase; + +/** + * @brief PhotoGd test case. + * + * These tests are not really useful yet, just some obvious behaviour. + * + * @todo Compare the actual results. + * @todo Test different image types. + */ +class PhotoGdTest extends UnitTestCase { + + use PHPMock; + + /** + * @var PhotoGd + */ + private $photoGd; + + /** + * Prepares the environment before running a test. + */ + protected function setUp() { + parent::setUp(); + + $data = file_get_contents('images/hz-16.png'); + + $this->photoGd = new PhotoGd($data, 'image/png'); + } + + /** + * Cleans up the environment after running a test. + */ + protected function tearDown() { + $this->photoGd = null; + + parent::tearDown(); + } + + /** + * Tests PhotoGd->supportedTypes() + * + * Without mocking gd this check is environment dependent. + * + public function testSupportedTypes() { + $sft = $this->photoGd->supportedTypes(); + + $this->assertArrayHasKey('image/jpeg', $sft); + $this->assertArrayHasKey('image/gif', $sft); + $this->assertArrayHasKey('image/png', $sft); + + $this->assertArrayNotHasKey('image/foo', $sft); + } + */ + + /** + * Tests PhotoGd->clearexif() + */ + public function testClearexifIsNotImplementedInGdAndDoesNotAlterImageOrReturnSomething() { + $data_before = $this->photoGd->getImage(); + $this->assertNull($this->photoGd->clearexif()); + $this->assertSame($data_before, $this->photoGd->getImage()); + } + + /** + * Tests PhotoGd->getImage() + */ + public function testGetimageReturnsAResource() { + $res = $this->photoGd->getImage(); + $this->assertIsResource($res); + $this->assertEquals('gd', get_resource_type($res)); + } + public function testGetimageReturnsFalseOnFailure() { + $this->photoGd = new PhotoGd(''); + $this->assertFalse($this->photoGd->getImage()); + } + + /** + * Tests PhotoGd->doScaleImage() + */ + public function testDoscaleImageSetsCorrectDimensions() { + $this->photoGd->doScaleImage(5, 8); + + $this->assertSame(5, $this->photoGd->getWidth()); + $this->assertSame(8, $this->photoGd->getHeight()); + } + + /** + * Tests PhotoGd->rotate() + */ + public function testRotate360DegreesCreatesANewImage() { + $data = $this->photoGd->getImage(); + $this->photoGd->rotate(360); + $this->assertNotEquals($data, $this->photoGd->getImage()); + } + + /** + * Tests PhotoGd->flip() + * + public function testFlip() { + // TODO Auto-generated PhotoGdTest->testFlip() + $this->markTestIncomplete("flip test not implemented"); + + $this->photoGd->flip(); + } + */ + + /** + * Tests PhotoGd->cropImageRect() + */ + public function testCropimagerectSetsCorrectDimensions() { + $this->photoGd->cropImageRect(10, 12, 1, 2, 11, 11); + + $this->assertSame(10, $this->photoGd->getWidth()); + $this->assertSame(12, $this->photoGd->getHeight()); + } + + /** + * Tests PhotoGd->imageString() + */ + public function testImagestringReturnsABinaryString() { + // Create a stub for global function get_config() + // get_config('system', 'png_quality') + // get_config('system', 'jpeg_quality'); + $gc = $this->getFunctionMock('Zotlabs\Photo', 'get_config'); + $gc->expects($this->once())->willReturnCallback( + function() { + switch($this->photoGd->getType()){ + case 'image/png': + return 7; + case 'image/jpeg': + default: + return 70; + } + } + ); + + $this->assertIsString($this->photoGd->imageString()); + } + +} diff --git a/tests/unit/includes/PhotodriverTest.php b/tests/unit/includes/PhotodriverTest.php new file mode 100644 index 000000000..6f6ad0ffe --- /dev/null +++ b/tests/unit/includes/PhotodriverTest.php @@ -0,0 +1,39 @@ +<?php + +namespace Zotlabs\Tests\Unit\includes; + +//use Zotlabs\Photo\PhotoGd; +use Zotlabs\Tests\Unit\UnitTestCase; +//use phpmock\phpunit\PHPMock; + +/** + * @brief Unit Test cases for include/photo/photo_driver.php file. + */ +class PhotodriverTest extends UnitTestCase { + //use PHPMock; + + public function testPhotofactoryReturnsNullForUnsupportedType() { + // php-mock can not mock global functions which is called by a global function. + // If the calling function is in a namespace it would work. + //$logger = $this->getFunctionMock(__NAMESPACE__, 'logger'); + //$logger->expects($this->once()); + + //$ph = \photo_factory('', 'image/bmp'); + //$this->assertNull($ph); + + $this->markTestIncomplete('Need to mock logger(), otherwise not unit testable.'); + } + + public function testPhotofactoryReturnsPhotogdIfConfigIgnore_imagickIsSet() { + // php-mock can not mock global functions which is called by a global function. + // If the calling function is in a namespace it would work. + //$gc = $this->getFunctionMock(__NAMESPACE__, 'get_config'); + // simulate get_config('system', 'ignore_imagick') configured + //$gc->expects($this->once())->willReturn(1) + + //$ph = \photo_factory(file_get_contents('images/hz-16.png'), 'image/png'); + //$this->assertInstanceOf(PhotoGd::class, $ph); + + $this->markTestIncomplete('Need to mock get_config(), otherwise not unit testable.'); + } +}
\ No newline at end of file |