'inner', 'g' => 'inner_gray', 'o' => 'outer', 'og' => 'outer_gray', 'cf' => 'cropFirst', // crop top or left part 'cfg' => 'cropFirst_gray', 'cc' => 'cropCenter', // crop middle or center part 'ccg' => 'cropCenter_gray', 'cl' => 'cropLast', // crop bottom or right part 'clg' => 'cropLast_gray', ]; /** @var array */ protected static $_config; /** @var Zend_Cache_Frontend_File */ protected static $_sizeCacheObj; protected static $_sizeCacheId = 'registeredSizes'; /** @var string allowed sizes from size cache */ protected static $_sizeCache; /** * @param string $name * @return bool * @throws Qs_Exception */ public static function setAdapter($name) { if (!in_array($name, Qs_ImageFs::$_allowedAdapters)) { throw new Qs_Exception('Invalid Adapter: ' . $name); } Qs_ImageFs::$_adapter = $name; return true; } /** * Returns non quoted relative target image name * Example: userfiles/files/imageName.jpg * * @static * @param string $name * @param string|null $emptyImage * @return string */ protected static function _getFullName($name, $emptyImage = null) { $name = parent::_getFullName($name); if (empty($name) || !Qs_ImageFs::_isSourcePathAllowed(dirname($name))) { $emptyImage = ($emptyImage) ?: Qs_ImageFs::NOT_AVAILABLE; $name = Qs_ImageFs::WEB_PATH_STATIC . '/' . $emptyImage; } return $name; } /** * @param string $sourcePath relative file path * @return bool */ protected static function _isSourcePathAllowed($sourcePath) { $allowedPaths = (array) Qs_ImageFs::getConfig('sourcePath'); $sourcePath = ('/' === substr($sourcePath, -1)) ? $sourcePath : $sourcePath . '/'; foreach ($allowedPaths as $allowedPath) { if (0 === strpos($sourcePath, $allowedPath)) { return true; } } return false; } /** * Registers image size in Zend_Case. Images with unregistered image size will not be resized * * @static * @param int|null $width * @param int|null $height * @return bool */ public static function registerSize($width, $height) { $cache = Qs_ImageFs::_getSizeCacheObj(); $size = (int) $width . 'x' . (int) $height; if (($registeredSizes = $cache->load(Qs_ImageFs::$_sizeCacheId))) { $result = true; if (false === strpos($registeredSizes, $size)) { $registeredSizes .= '|' . $size; $result = $cache->save($registeredSizes, Qs_ImageFs::$_sizeCacheId); } Qs_ImageFs::$_sizeCache = $registeredSizes; } else { $result = $cache->save($size, Qs_ImageFs::$_sizeCacheId); Qs_ImageFs::$_sizeCache = $size; } return $result; } /** * @param int $width * @param int $height * @return bool */ protected static function _isSizeRegistered($width, $height) { if (empty(Qs_ImageFs::$_sizeCache)) { $cache = Qs_ImageFs::_getSizeCacheObj(); Qs_ImageFs::$_sizeCache = ($savedSizes = $cache->load(Qs_ImageFs::$_sizeCacheId)) ? $savedSizes : ''; } $size = (int) $width . 'x' . (int) $height; return (false !== strpos(Qs_ImageFs::$_sizeCache, $size)); } /** * @return Zend_Cache_Frontend_File */ protected static function _getSizeCacheObj() { if (null === Qs_ImageFs::$_sizeCacheObj) { $frontendOptions = [ 'lifetime' => null, 'automatic_serialization' => true, ]; Qs_ImageFs::$_sizeCacheObj = Qs_Cache::factory('Core', 'File', $frontendOptions); } return Qs_ImageFs::$_sizeCacheObj; } /** * Returns quoted full icon name build according to passed params * Example: thumbnails/userfiles/files/imageName_{$width}x{$height}{$methodAlias}.jpg * * @static * @param string $name * @param int $width [OPTIONAL] * @param int $height [OPTIONAL] * @param string $method [OPTIONAL] * @param string|null $emptyImage [OPTIONAL] * @return string */ public static function get($name, $width = 0, $height = 0, $method = 'inner', $emptyImage = null) { $fullName = Qs_ImageFs::_getFullName($name, $emptyImage); $width = (int) $width; $height = (int) $height; Qs_ImageFs::registerSize($width, $height); if ($width || $height) { $fullName = Qs_ImageFs::getIconName(Qs_ImageFs::THUMBNAIL_PATH . '/' . $fullName, $width, $height, $method); } return Qs_ImageFs::quotePath($fullName); } /** * @param string $name * @param int $width * @param int $height * @param string $method * @return string */ public static function getUrl($name, $width = 0, $height = 0, $method = 'inner') { return BASE_URL_LANGUAGE . '/' . Qs_ImageFs::get($name, $width, $height, $method); } /** * Returns rendered attributes src, width, height * width and height attributes contains real images size of image size after resize * * @static * @param string $name * @param int $width [OPTIONAL] * @param int $height [OPTIONAL] * @param string $method [OPTIONAL] * @return string */ public static function getFull($name, $width = 0, $height = 0, $method = 'inner') { $attribs = Qs_ImageFs::getFullAttribs($name, $width, $height, $method); return ' ' . Qs_ImageFs::renderAttribs($attribs); } /** * @param array $attribs * @return string */ public static function renderAttribs(array $attribs) { $items = []; foreach ($attribs as $name => $value) { if (!is_array($value)) { $value = preg_replace('!&(#?\w+);!', '%%%QS_START%%%\\1%%%QS_END%%%', $value); $value = htmlspecialchars($value); $value = str_replace(['%%%QS_START%%%', '%%%QS_END%%%'], ['&', ';'], $value); } else { continue; } $items[] = $name . '="' . $value . '"'; } return implode(' ', $items); } /** * @param $name * @param int $width * @param int $height * @param string $method * @return array */ public static function getFullAttribs($name, $width = 0, $height = 0, $method = 'inner') { $width = (int) $width; $height = (int) $height; $attribs = []; $attribs['src'] = Qs_ImageFs::get($name, $width, $height, $method); $fullName = Qs_ImageFs::_getFullName($name, $width, $height, $method); if ($width || $height) { $size = Qs_ImageFs::getNewSize($fullName, $width, $height, $method); $sizeInfo = (isset($size['crop'])) ? $size['crop'] : $size; } else { $sizeInfo = Qs_ImageFs::getInfo($fullName); } foreach (['width', 'height'] as $_name) { if (array_key_exists($_name, $sizeInfo)) { $attribs[$_name] = $sizeInfo[$_name]; } } return $attribs; } /** * Returns information extracted from icon name * @static * @param string $iconName * @return array array * string $file - relative name of original file, * int $width - requested width, * int $height - requested height, * string $method - resize method name */ public static function getThumbnailInfo($iconName) { $info = [ 'file' => $iconName, 'width' => 0, 'height' => 0, ]; if (false !== strpos($iconName, 'x')) { $matches = []; preg_match(Qs_ImageFs::$_thumbnailPattern, $iconName, $matches); $methodKey = !empty($matches[3]) ? $matches[3] : ''; $method = Qs_Array::get(Qs_ImageFs::$_methods, $methodKey, 'inner'); $width = isset($matches[1]) ? (int) $matches[1] : 0; $height = isset($matches[2]) ? (int) $matches[2] : 0; if ($width || $height) { $pathParts = pathinfo($iconName); if (isset($pathParts['extension'])) { $nameWww = substr($iconName, 0, -(1 + strlen($pathParts['extension']))); $nameLen = strrpos($nameWww, '_'); $name = substr($nameWww, 0, $nameLen) . '.' . $pathParts['extension']; } else { $name = substr($iconName, 0, strrpos($iconName, '_')); } $info = [ 'file' => $name, 'width' => intval($width), 'height' => intval($height), 'method' => $method, ]; } } return $info; } /** * @param array|string $options * @return array * @throws Exception */ public static function parse($options) { if (is_array($options)) { foreach (['width', 'height'] as $option) { if (!array_key_exists($option, $options)) { throw new Exception($option . ' is not defined'); } } if (!empty($options['method'])) { if (!in_array($options['method'], self::$_methods)) { throw new Exception('Invalid resize method ' . $options['method']); } } else { $options['method'] = reset(self::$_methods); } return Qs_Array::map($options, ['width', 'height', 'method']); } $matches = []; preg_match(Qs_ImageFs::$_optionsPattern, $options, $matches); array_shift($matches); $width = array_shift($matches); $height = array_shift($matches); $methodKey = array_shift($matches); $method = Qs_ImageFs::$_methods[$methodKey]; return compact('width', 'height', 'method'); } /** * Returns file name with appended params "_{$width}x{$height}{$methodAlias}" * @static * @param $fileName * @param $width * @param $height * @param string $method * @return string */ public static function getIconName($fileName, $width, $height, $method = 'inner') { $width = (int) $width; $height = (int) $height; $parts = pathinfo((string) $fileName); $methodAlias = Qs_ImageFs::getResizeMethodAlias($method); if (strpos($parts['basename'], '.') === false) { $iconName = $parts['basename'] . '_' . $width . 'x' . $height . $methodAlias; } else { $extLen = strlen($parts['extension']); $name = substr($parts['basename'], 0, -($extLen + 1)); $iconName = $name . '_' . $width . 'x' . $height . $methodAlias . '.' . $parts['extension']; } return ('.' === $parts['dirname']) ? $iconName : $parts['dirname'] . '/' . $iconName; } /** * @param $method * @return string */ public static function getResizeMethodAlias($method) { return array_search($method, Qs_ImageFs::$_methods); } /** * @param string $alias * @return bool|string */ public static function getResizeMethod($alias) { if (array_key_exists($alias, Qs_ImageFs::$_methods)) { return Qs_ImageFs::$_methods[$alias]; } return false; } /** * @param string $name * @return array */ public static function getInfo($name) { $res = getimagesize($name); if ($res !== false) { $res['width'] = $res[0]; $res['height'] = $res[1]; $res['type'] = $res[2]; $res['attr'] = $res[3]; } else { $res = []; } return $res; } /** * @param string $folder * @return bool */ public static function isSymlink($folder) { $pathParts = explode('/', $folder); $folder = ''; do { $folder .= ($folder) ? '/' : ''; $folder .= array_shift($pathParts); if (is_link($folder)) { return true; } } while ($pathParts); return false; } /** * @static * @param string $source Original file name * @param array|string $options Resize options * @return bool|null|string */ public static function resize($source, $options) { if (!is_file($source)) { return false; } $sizeCheck = false; $force = false; $method = 'inner'; $width = 0; $height = 0; $destination = null; $symlink = null; if (is_string($options)) { $options = Qs_ImageFs::parse($options); } extract($options); $width = (int) $width; $height = (int) $height; if ($sizeCheck && !Qs_ImageFs::_isSizeRegistered($width, $height)) { $source = htmlspecialchars($source); trigger_error("Trying to resize image '{$source}' to unallowed size: {$width}x{$height}", E_USER_WARNING); return false; } if (!$destination) { $destination = Qs_ImageFs::THUMBNAIL_PATH . '/' . Qs_ImageFs::getIconName($source, $width, $height, $method); } $path = dirname($destination); if (!is_dir($path)) { umask(0); mkdir($path, 0777, true); } $sourceInfo = Qs_ImageFs::getInfo($source); $destinationInfo = pathinfo($destination); if (null === $symlink) { $symlink = true; // true - create symlink; false - copy file } /** * перевірка чи місцепризначення іконки в директорії pub-а (шлях містить symlink). Це необхідно робити, бо * може виникнути проблемна ситуація коли іконки в pub-а, а файл що ресайзиться в домашній директорії якогось * розробника (наприклад в папці www/image) - в такому випадку відносний шлях по якому створюється symlink * неправильний. */ if ($symlink && Qs_ImageFs::isSymlink($destinationInfo['dirname'])) { $symlink = false; } $isSizeDifferent = Qs_ImageFs::isNewSizeDifferent($source, $width, $height, $method); if (!strstr($options['method'], '_gray') && ( empty($sourceInfo) || (!$height && !$width) || ($width == $sourceInfo['width'] && $height == $sourceInfo['height']) || ($width >= $sourceInfo['width'] && $height >= $sourceInfo['height'] && !$isSizeDifferent) || ($width == $sourceInfo['width'] && $height == 0) || ($height == $sourceInfo['height'] && $width == 0) ) ) { if ($symlink && !$force) { $currentDir = getcwd(); if (chdir($destinationInfo['dirname']) && !file_exists($destinationInfo['basename'])) { $target = str_repeat('../', substr_count($destination, DIRECTORY_SEPARATOR)) . $source; file_exists($target) && symlink($target, $destinationInfo['basename']); } chdir($currentDir); } else { copy($source, $destination); } return $destination; } if (!is_file($destination) || $force) { $function = 'resize' . ucfirst(Qs_ImageFs::$_adapter); Qs_ImageFs::$function($source, compact('width', 'height', 'force', 'method', 'destination')); } return $destination; } /** * @param string $source * @param array $options */ public static function resizeIm($source, $options) { $width = (empty($options['width'])) ? 0 : $options['width']; $height = (empty($options['height'])) ? 0 : $options['height']; $size = Qs_ImageFs::getNewSize($source, $width, $height, $options['method']); $cmd = 'convert ' . '-strip -interlace Plane ' . escapeshellarg($source) . ' ' . '-resize ' . $size['width'] . 'x' . $size['height'] . '!'; if ($size['gray']) { $cmd .= ' -colorspace GRAY'; } if (!empty($size['crop'])) { $crop = &$size['crop']; $cmd .= ' -crop ' . $crop['width'] . 'x' . $crop['height'] . '+' . $crop['left'] . '+' . $crop['top'] . ' ' . '+repage'; } $cmd .= ' +profile "*" ' . escapeshellarg($options['destination']); shell_exec($cmd); } /** * @param string $source * @param array $options */ public static function resizeGd($source, $options) { $width = (empty($options['width'])) ? 0 : $options['width']; $height = (empty($options['height'])) ? 0 : $options['height']; $size = Qs_ImageFs::getNewSize($source, $width, $height, $options['method']); $info = Qs_ImageFs::getInfo($source); if (!empty($size['crop'])) { $destImg = imagecreatetruecolor($size['crop']['width'], $size['crop']['height']); } else { $destImg = imagecreatetruecolor($size['width'], $size['height']); } $sourceImg = ''; switch ($info['type']) { case IMAGETYPE_GIF: $sourceImg = imagecreatefromgif($source); break; case IMAGETYPE_JPEG: $sourceImg = imagecreatefromjpeg($source); break; case IMAGETYPE_PNG: $sourceImg = imagecreatefrompng($source); break; default: break; } $sourceLeft = 0; $sourceTop = 0; $sourceWidth = $info['width']; $sourceHeight = $info['height']; $destinationWidth = $size['width']; $destinationHeight = $size['height']; if (!empty($size['crop'])) { $destinationWidth = $size['crop']['width']; $destinationHeight = $size['crop']['height']; $sourceLeft = round($size['crop']['left'] / $size['scale']); $sourceTop = round($size['crop']['top'] / $size['scale']); $sourceWidth = round($size['crop']['width'] / $size['scale']); $sourceHeight = round($size['crop']['height'] / $size['scale']); } imagecopyresampled($destImg, $sourceImg, 0, 0, $sourceLeft, $sourceTop, $destinationWidth, $destinationHeight, $sourceWidth, $sourceHeight); switch ($info['type']) { case IMAGETYPE_GIF: imagegif($destImg, $options['destination']); break; case IMAGETYPE_JPEG: imagejpeg($destImg, $options['destination'], Qs_ImageFs::$_quality); break; case IMAGETYPE_PNG: imagepng($destImg, $options['destination'], round(Qs_ImageFs::$_quality / 10)); break; default: break; } } /** * @param string $fullName * @return bool */ public static function delete($fullName) { Qs_ImageFs::delIcons($fullName); return parent::delete($fullName); } /** * @param $fullName * @return bool */ public static function delIcons($fullName) { if (0 === strpos($fullName, WWW_PATH)) { $fullName = substr($fullName, strlen(WWW_PATH) + 1); } if (file_exists($fullName) && 0 === strpos($fullName, Qs_ImageFs::WEB_PATH)) { $info = pathinfo($fullName); $pattern = Qs_ImageFs::THUMBNAIL_PATH . '/' . $info['dirname'] . '/' . $info['filename'] . '_*.' . $info['extension']; if (($thumbnails = glob($pattern))) { foreach ($thumbnails as $file) { if (preg_match("/_\d+[xX]\d+\.{$info['extension']}$/", $file)) { @unlink($file); } } return true; } } return false; } /** * @param string $fullPath * @param int $width * @param int $height * @param string $method * @return bool */ public static function isNewSizeDifferent($fullPath, $width, $height, $method = 'inner') { list($originalWidth, $originalHeight, ,) = getimagesize($fullPath); $newSize = Qs_ImageFs::getNewSize($fullPath, $width, $height, $method); return ($originalWidth !== $newSize['width'] || $originalHeight !== $newSize['height']); } /** * @static * @param string $fullPath * @param int $width * @param int $height * @param string $method * @return array SizeInfo */ public static function getNewSize($fullPath, $width, $height, $method = 'inner') { if (!is_file($fullPath)) { return []; } list($oldWidth, $oldHeight, ,) = getimagesize($fullPath); $result = []; $cropMethod = null; if ($width != 0 || $height != 0) { if ($width == 0) { $width = $oldWidth * $height / $oldHeight; } else if ($height == 0) { $height = $oldHeight * $width / $oldWidth; } $zx = $width / $oldWidth; $zy = $height / $oldHeight; $width = round($width); $height = round($height); $newx = -1; $newy = -1; $result['gray'] = false; if (strstr($method, '_gray')) { $method = str_replace('_gray', '', $method); $result['gray'] = true; } if (0 === strncmp('crop', $method, 4)) { $resizeMethod = 'outer'; $cropMethod = strtolower(substr($method, 4)); } else { $resizeMethod = $method; } switch ($resizeMethod) { case 'outer': if ($zx > $zy) { $z = $zx; $newx = $width; } else { $z = $zy; $newy = $height; } break; case 'inner': default: if ($zx < $zy) { $z = $zx; $newx = $width; } else { $z = $zy; $newy = $height; } break; } $result['width'] = ($newx == -1) ? round($oldWidth * $z) : $newx; $result['height'] = ($newy == -1) ? round($oldHeight * $z) : $newy; } else { $z = 1; $result['width'] = $oldWidth; $result['height'] = $oldHeight; } $result['scale'] = $z; switch ($cropMethod) { case 'first': $result['crop'] = [ 'width' => $width, 'height' => $height, 'left' => 0, 'top' => 0, ]; break; case 'center': $result['crop'] = [ 'width' => $width, 'height' => $height, 'left' => round($result['width'] / 2 - $width / 2), 'top' => round($result['height'] / 2 - $height / 2), ]; break; case 'last': $result['crop'] = [ 'width' => $width, 'height' => $height, 'left' => $result['width'] > $result['height'] ? $result['width'] - $width : 0, 'top' => $result['width'] > $result['height'] ? 0 : $result['height'] - $height, ]; break; default: break; } return Qs_ImageFs::_getNormalizedSizeInfo($result); } /** * Returns proportional scaled SizeInfo accordingly to MAX_RESIZE_WIDTH and MAX_RESIZE_HEIGHT * @static * @param array $info SizeInfo * @return array Scaled SizeInfo */ protected static function _getNormalizedSizeInfo($info) { list($width, $height, $scale) = Qs_ImageFs::_getNormalizedSize($info['width'], $info['height']); $info['width'] = $width; $info['height'] = $height; $info['scale'] = $info['scale'] * $scale; if (!empty($info['crop'])) { $info['crop']['top'] = round($info['crop']['top'] * $scale); $info['crop']['left'] = round($info['crop']['left'] * $scale); $info['crop']['width'] = round($info['crop']['width'] * $scale); $info['crop']['height'] = round($info['crop']['height'] * $scale); } return $info; } /** * Returns proportional sizes that are not larger than MAX_RESIZE_WIDTH and MAX_RESIZE_HEIGHT * @static * @param $width * @param $height * @return array [width, height, scale] */ protected static function _getNormalizedSize($width, $height) { $scale = 1; if ($width > Qs_ImageFs::MAX_RESIZE_WIDTH && $height > Qs_ImageFs::MAX_RESIZE_HEIGHT) { $widthScale = Qs_ImageFs::MAX_RESIZE_WIDTH / $width; $heightScale = Qs_ImageFs::MAX_RESIZE_HEIGHT / $height; $scale = ($widthScale < $heightScale) ? $widthScale : $heightScale; } else if ($width > Qs_ImageFs::MAX_RESIZE_WIDTH) { $scale = Qs_ImageFs::MAX_RESIZE_WIDTH / $width; } else if ($height > Qs_ImageFs::MAX_RESIZE_HEIGHT) { $scale = Qs_ImageFs::MAX_RESIZE_HEIGHT / $height; } if ($scale != 1) { $width = round($width * $scale); $height = round($height * $scale); } return [$width, $height, $scale]; } public static function getOrientation($file) { $cmd = "identify -format '%[EXIF:Orientation]' " . escapeshellarg($file); $result = shell_exec($cmd); $orientation = trim($result); if (empty($orientation)) { $orientation = self::ORIENTATION_TOP_LEFT; } if (!ctype_digit($orientation)) { trigger_error('Failed to detect image orientation: ' . $file); return false; } return (int) $orientation; } /** * @param $file * @return void */ public static function fixJpegOrientation($file) { $extension = pathinfo($file, PATHINFO_EXTENSION); if (strtolower($extension) != 'jpg') { return; } $orientation = self::getOrientation($file); if ($orientation == self::ORIENTATION_TOP_LEFT) { return; } $cmd = 'convert -auto-orient {file} +profile "*" {file}'; $cmd = Qs_String::fill($cmd, ['file' => escapeshellarg($file)], '{}'); shell_exec($cmd); return; } }