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 +++++++++++++++++++ 3 files changed, 880 insertions(+) create mode 100644 Zotlabs/Photo/PhotoDriver.php create mode 100644 Zotlabs/Photo/PhotoGd.php create mode 100644 Zotlabs/Photo/PhotoImagick.php (limited to 'Zotlabs/Photo') 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(); + } + +} -- cgit v1.2.3