aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Zotlabs/Photo/PhotoDriver.php498
-rw-r--r--Zotlabs/Photo/PhotoGd.php176
-rw-r--r--Zotlabs/Photo/PhotoImagick.php (renamed from include/photo/photo_imagick.php)143
-rw-r--r--include/photo/photo_driver.php857
-rw-r--r--include/photo/photo_gd.php162
-rw-r--r--tests/unit/Photo/PhotoGdTest.php147
-rw-r--r--tests/unit/includes/PhotodriverTest.php39
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