From e6dadb215e9e08491ae57ab851960a0973d3f704 Mon Sep 17 00:00:00 2001 From: Klaus Weidenbach Date: Tue, 5 Feb 2019 23:03:03 +0100 Subject: Refactor photo_driver to use namespaces. Add simple UnitTest, but it is not yet very meaningful. --- Zotlabs/Photo/PhotoDriver.php | 468 +++++++++++++++++++++++++++++++ Zotlabs/Photo/PhotoGd.php | 194 +++++++++++++ Zotlabs/Photo/PhotoImagick.php | 218 +++++++++++++++ include/photo/photo_driver.php | 470 +------------------------------- include/photo/photo_gd.php | 162 ----------- include/photo/photo_imagick.php | 213 --------------- tests/unit/Photo/PhotoGdTest.php | 147 ++++++++++ tests/unit/includes/PhotodriverTest.php | 39 +++ 8 files changed, 1078 insertions(+), 833 deletions(-) create mode 100644 Zotlabs/Photo/PhotoDriver.php create mode 100644 Zotlabs/Photo/PhotoGd.php create mode 100644 Zotlabs/Photo/PhotoImagick.php delete mode 100644 include/photo/photo_gd.php delete mode 100644 include/photo/photo_imagick.php create mode 100644 tests/unit/Photo/PhotoGdTest.php create mode 100644 tests/unit/includes/PhotodriverTest.php diff --git a/Zotlabs/Photo/PhotoDriver.php b/Zotlabs/Photo/PhotoDriver.php new file mode 100644 index 000000000..e2e143f8d --- /dev/null +++ b/Zotlabs/Photo/PhotoDriver.php @@ -0,0 +1,468 @@ +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 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..1143c565c --- /dev/null +++ b/Zotlabs/Photo/PhotoGd.php @@ -0,0 +1,194 @@ +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 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(); + } + + /** + * {@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/Zotlabs/Photo/PhotoImagick.php b/Zotlabs/Photo/PhotoImagick.php new file mode 100644 index 000000000..373bac40f --- /dev/null +++ b/Zotlabs/Photo/PhotoImagick.php @@ -0,0 +1,218 @@ + 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + ]; + } + + private function get_FormatsMap() { + return [ + 'image/jpeg' => 'JPG', + 'image/png' => 'PNG', + 'image/gif' => 'GIF', + ]; + } + + + protected function load($data, $type) { + $this->valid = false; + $this->image = new \Imagick(); + + if(! $data) + return; + + try { + $this->image->readImageBlob($data); + } catch(\Exception $e) { + logger('Imagick readImageBlob() exception:' . print_r($e, true)); + return; + } + + /* + * Setup the image to the format it will be saved to + */ + + $map = $this->get_FormatsMap(); + $format = $map[$type]; + + if($this->image) { + $this->image->setFormat($format); + + // 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'); + 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 + * the zlib compression level (quality / 10) and filter-type (quality % 10). + * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering, + * unless the image has a color map, in which case it means compression level 7 with no PNG filtering' + */ + $quality = $quality * 10; + $this->image->setCompressionQuality($quality); + break; + case 'image/jpeg': + $quality = get_config('system', 'jpeg_quality'); + if((! $quality) || ($quality > 100)) + $quality = JPEG_QUALITY; + $this->image->setCompressionQuality($quality); + default: + break; + } + } + } + + protected function destroy() { + if($this->is_valid()) { + $this->image->clear(); + $this->image->destroy(); + } + } + + 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); + + $this->image->stripImage(); + + 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; + + $this->image = $this->image->deconstructImages(); + return $this->image; + } + + public function doScaleImage($dest_width, $dest_height) { + /* + * If it is not animated, there will be only one iteration here, + * so don't bother checking + */ + // Don't forget to go back to the first frame + $this->image->setFirstIterator(); + do { + $this->image->scaleImage($dest_width, $dest_height); + } while($this->image->nextImage()); + + $this->setDimensions(); + } + + public function rotate($degrees) { + 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->setDimensions(); + } + + public function flip($horiz = true, $vert = 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()); + + $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; + + $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($maxx, $maxy); + } + + public function imageString() { + 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 718bf875b..c11580bdc 100644 --- a/include/photo/photo_driver.php +++ b/include/photo/photo_driver.php @@ -1,15 +1,19 @@ = 0) { - require_once('include/photo/photo_imagick.php'); - $ph = new photo_imagick($data, $type); + $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. @@ -45,462 +48,13 @@ function photo_factory($data, $type = null) { } if(! $ph) { - require_once('include/photo/photo_gd.php'); - $ph = new photo_gd($data, $type); + $ph = new PhotoGd($data, $type); } return $ph; } -/** - * @brief Abstract photo driver class. - * - */ -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 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 squre image - * @return boolean|void false on failure, otherwise void - */ - public function scaleImageSquare($dim) { - if(!$this->is_valid()) - return FALSE; - - $this->doScaleImage($dim, $dim); - } - - /** - * @brief reads exif data from 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 ' . $p['imgscale'] . ' returned ' . intval($r)); - - return $r; - } - -} - /** * @brief Guess image mimetype from filename or from Content-Type header. * @@ -519,7 +73,7 @@ function guess_image_type($filename, $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); @@ -566,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(); @@ -756,7 +310,7 @@ function import_xchan_photo($photo, $xchan, $thing = false, $force = false) { 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)); + return([$photo, $thumb, $micro, $type, $photo_failure, $modified]); } /** 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 @@ -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/include/photo/photo_imagick.php b/include/photo/photo_imagick.php deleted file mode 100644 index cb3ad27fb..000000000 --- a/include/photo/photo_imagick.php +++ /dev/null @@ -1,213 +0,0 @@ - 'jpg', - 'image/png' => 'png', - 'image/gif' => 'gif' - ); - } - - public function get_FormatsMap() { - return array( - 'image/jpeg' => 'JPG', - 'image/png' => 'PNG', - 'image/gif' => 'GIF' - ); - } - - - function load($data, $type) { - $this->valid = false; - $this->image = new Imagick(); - - if(! $data) - return; - - try { - $this->image->readImageBlob($data); - } - catch (Exception $e) { - logger('imagick readImageBlob() exception:' . print_r($e,true)); - return; - } - - /** - * Setup the image to the format it will be saved to - */ - - $map = $this->get_FormatsMap(); - $format = $map[$type]; - - if($this->image) { - $this->image->setFormat($format); - - // 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'); - 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 - * the zlib compression level (quality / 10) and filter-type (quality % 10). - * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering, - * unless the image has a color map, in which case it means compression level 7 with no PNG filtering' - */ - $quality = $quality * 10; - $this->image->setCompressionQuality($quality); - break; - 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() { - if($this->is_valid()) { - $this->image->clear(); - $this->image->destroy(); - } - } - - - public function setDimensions() { - $this->width = $this->image->getImageWidth(); - $this->height = $this->image->getImageHeight(); - } - - - public function clearexif() { - - $profiles = $this->image->getImageProfiles("icc", true); - - $this->image->stripImage(); - - if(!empty($profiles)) { - $this->image->profileImage("icc", $profiles['icc']); - } - } - - - - public function getImage() { - if(!$this->is_valid()) - return FALSE; - - $this->image = $this->image->deconstructImages(); - return $this->image; - } - - public function doScaleImage($dest_width,$dest_height) { - - /** - * If it is not animated, there will be only one iteration here, - * so don't bother checking - */ - // Don't forget to go back to the first frame - $this->image->setFirstIterator(); - do { - $this->image->scaleImage($dest_width, $dest_height); - } while ($this->image->nextImage()); - - $this->setDimensions(); - } - - public function rotate($degrees) { - 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->setDimensions(); - } - - public function flip($horiz = true, $vert = 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()); - - $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; - - $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($maxx,$maxy); - } - - public function imageString() { - if(!$this->is_valid()) - return FALSE; - - /* Clean it */ - $this->image = $this->image->deconstructImages(); - return $this->image->getImagesBlob(); - } - - - -} 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 @@ +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 @@ +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 -- cgit v1.2.3