428 lines
13 KiB
PHP
428 lines
13 KiB
PHP
<?php
|
||
|
||
class FileUtils
|
||
{
|
||
public static $quality = 90;
|
||
public static $exifProps = [
|
||
'Model',
|
||
'Orientation',
|
||
'DateTime',
|
||
'DateTimeOriginal',
|
||
'ExposureTime',
|
||
'FNumber',
|
||
'ISOSpeedRatings',
|
||
'FocalLength',
|
||
'FocalLengthIn35mmFilm',
|
||
'GPSLongitude',
|
||
'GPSLongitudeRef',
|
||
'GPSLatitude',
|
||
'GPSLatitudeRef',
|
||
];
|
||
|
||
public static function magick()
|
||
{
|
||
static $class;
|
||
if (!$class)
|
||
{
|
||
// Use GraphicsMagick if possible
|
||
$class = class_exists('Gmagick') ? 'Gmagick' : 'Imagick';
|
||
}
|
||
return new $class();
|
||
}
|
||
|
||
public static function exifGPS($coordinate, $hemisphere)
|
||
{
|
||
for ($i = 0; $i < 3; $i++)
|
||
{
|
||
$part = explode('/', $coordinate[$i]);
|
||
if (count($part) == 1)
|
||
{
|
||
$coordinate[$i] = $part[0];
|
||
}
|
||
elseif (count($part) == 2)
|
||
{
|
||
$coordinate[$i] = floatval($part[0])/floatval($part[1]);
|
||
}
|
||
else
|
||
{
|
||
$coordinate[$i] = 0;
|
||
}
|
||
}
|
||
list($degrees, $minutes, $seconds) = $coordinate;
|
||
$sign = ($hemisphere == 'W' || $hemisphere == 'S') ? -1 : 1;
|
||
return $sign * ($degrees + $minutes/60 + $seconds/3600);
|
||
}
|
||
|
||
/**
|
||
* Get image or file properties (content SHA1, width, height, selected EXIF properties)
|
||
*/
|
||
public static function getProps($allowed_types, $fs_path, $orig_name)
|
||
{
|
||
if (!file_exists($fs_path))
|
||
{
|
||
throw new UserException('no-file-uploaded');
|
||
}
|
||
$mime = self::checkMime($fs_path);
|
||
if (!$mime)
|
||
{
|
||
throw new UserException('file-mime-type-unknown');
|
||
}
|
||
elseif (preg_match(App::$config['mime_blacklist'], $mime))
|
||
{
|
||
// Prevent uploads with dangerous MIME types
|
||
throw new UserException('file-mime-type-blacklisted');
|
||
}
|
||
$stdProps = [
|
||
'mimetype' => $mime,
|
||
'sha1' => sha1_file($fs_path),
|
||
'size' => filesize($fs_path),
|
||
'format' => '',
|
||
'width' => 0,
|
||
'height' => 0,
|
||
];
|
||
$flag = File::ONLY_BINARY;
|
||
$props = NULL;
|
||
if ($mime == 'application/x-shockwave-flash')
|
||
{
|
||
$props = self::getSWFProps($fs_path);
|
||
if ($props)
|
||
$flag = File::ONLY_SWF;
|
||
}
|
||
elseif (substr($mime, 0, 6) == 'image/')
|
||
{
|
||
$props = self::getImageProps($fs_path);
|
||
if ($props)
|
||
$flag = File::ONLY_IMAGES;
|
||
}
|
||
elseif (substr($mime, 0, 6) == 'video/')
|
||
{
|
||
$props = VideoUtils::getVideoProps($fs_path);
|
||
if ($props)
|
||
$flag = File::ONLY_VIDEO;
|
||
}
|
||
if (!($allowed_types & $flag))
|
||
{
|
||
throw new UserException('file-type-denied', [ 'allowed' => $allowed_types, 'type' => $flag ]);
|
||
}
|
||
$props = ($props ?: []) + $stdProps;
|
||
if (!$props['format'])
|
||
{
|
||
if (!empty($props['filename']))
|
||
$orig_name = preg_replace('#^.*/#is', '', $props['filename']);
|
||
$p = strrpos($orig_name, '.');
|
||
if ($p)
|
||
$props['format'] = strtolower(substr($orig_name, $p+1));
|
||
}
|
||
unset($props['filename']);
|
||
return $props;
|
||
}
|
||
|
||
/**
|
||
* Get image properties + compress uploaded BMP images to JPEG
|
||
*/
|
||
protected static function getImageProps($fn)
|
||
{
|
||
$format = NULL;
|
||
$props = [];
|
||
try
|
||
{
|
||
$exif = @exif_read_data($fn);
|
||
if ($exif)
|
||
{
|
||
foreach (self::$exifProps as $p)
|
||
{
|
||
if (isset($exif[$p]))
|
||
{
|
||
$props[$p] = $exif[$p];
|
||
}
|
||
}
|
||
}
|
||
// FIXME Сначала проверять ширину/высоту, не читая саму картинку в память
|
||
$im = static::magick();
|
||
$im->readImage($fn);
|
||
$format = strtolower($im->getImageFormat());
|
||
if ($format == 'jpeg')
|
||
{
|
||
$format = 'jpg';
|
||
}
|
||
if ($format != 'png' &&
|
||
$format != 'gif' &&
|
||
$format != 'jpeg' &&
|
||
$format != 'jpg' &&
|
||
$format != 'pdf' &&
|
||
$format != 'djvu' &&
|
||
$format != 'djv' &&
|
||
($format != 'ico' || $im->getImageWidth() > 48 || $im->getImageHeight() > 48)) // bmp, tiff, psd...
|
||
{
|
||
$im->mergeImageLayers(Imagick::LAYERMETHOD_FLATTEN);
|
||
if (!empty($props['Orientation']))
|
||
{
|
||
self::fixOrientation($im, $props['Orientation']);
|
||
unset($props['Orientation']);
|
||
}
|
||
$im->setCompressionQuality(static::$quality);
|
||
$im->setImageFormat('jpeg');
|
||
$format = 'jpg';
|
||
$fn = tempnam(sys_get_temp_dir(), 'imguniq');
|
||
$im->writeImage($fn);
|
||
}
|
||
$width = $im->getImageWidth();
|
||
$height = $im->getImageHeight();
|
||
if (!empty($props['Orientation']) && $props['Orientation'] >= 5)
|
||
{
|
||
$t = $width;
|
||
$width = $height;
|
||
$height = $t;
|
||
}
|
||
}
|
||
catch (Exception $e)
|
||
{
|
||
// Not an image
|
||
return NULL;
|
||
}
|
||
return [
|
||
'filename' => $fn,
|
||
'width' => $width,
|
||
'height' => $height,
|
||
'format' => $format,
|
||
'props' => $props,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Get MIME type of a file
|
||
*/
|
||
protected static function checkMime($filename)
|
||
{
|
||
$mime = false;
|
||
if (class_exists('finfo'))
|
||
{
|
||
static $finfo;
|
||
if (!$finfo)
|
||
{
|
||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||
}
|
||
if ($finfo)
|
||
{
|
||
$mime = $finfo->file($filename);
|
||
}
|
||
}
|
||
elseif (function_exists('mime_content_type'))
|
||
{
|
||
$mime = mime_content_type($filename);
|
||
}
|
||
else
|
||
{
|
||
$mime = shell_exec("file ".escapeshellarg($filename));
|
||
}
|
||
return $mime;
|
||
}
|
||
|
||
protected static function getSWFProps($fn)
|
||
{
|
||
$size = shell_exec("swfdump -X -Y ".escapeshellarg($fn));
|
||
if (preg_match('/-X (\d+) -Y (\d+)/', $size, $m))
|
||
{
|
||
return [
|
||
'format' => 'swf',
|
||
'width' => $m[1],
|
||
'height' => $m[2],
|
||
];
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/**
|
||
* Fix ImageMagick image $im orientation based on EXIF orientation $orient
|
||
*/
|
||
public static function fixOrientation($im, $orient)
|
||
{
|
||
$orient = intval($orient);
|
||
if ($orient > 1 && $orient <= 8)
|
||
{
|
||
if ($orient == 2)
|
||
{
|
||
$im->flopImage();
|
||
}
|
||
elseif ($orient == 3)
|
||
{
|
||
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 180);
|
||
}
|
||
elseif ($orient == 4)
|
||
{
|
||
$im->flipImage();
|
||
}
|
||
elseif ($orient == 5)
|
||
{
|
||
if (method_exists($im, 'transposeImage'))
|
||
{
|
||
$im->transposeImage();
|
||
}
|
||
else
|
||
{
|
||
// GraphicsMagick doesn't have Transpose
|
||
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 90);
|
||
$im->flopImage();
|
||
}
|
||
}
|
||
elseif ($orient == 6)
|
||
{
|
||
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 90);
|
||
}
|
||
elseif ($orient == 7)
|
||
{
|
||
if (method_exists($im, 'transverseImage'))
|
||
{
|
||
$im->transverseImage();
|
||
}
|
||
else
|
||
{
|
||
// GraphicsMagick doesn't have Transverse
|
||
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 270);
|
||
$im->flopImage();
|
||
}
|
||
}
|
||
elseif ($orient == 8)
|
||
{
|
||
$im->rotateImage($im instanceof Imagick ? new ImagickPixel() : new GmagickPixel(), 270);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calculate thumbnail image width and height
|
||
*/
|
||
public static function thumbnailSize($cw, $ch, $maxw, $maxh)
|
||
{
|
||
if ($maxw <= 0 && $maxh <= 0 || $cw <= 0 || $ch <= 0)
|
||
{
|
||
return [ $cw, $ch ];
|
||
}
|
||
if ($maxw > 0 && ($maxh <= 0 || $maxh/$ch >= $maxw/$cw))
|
||
{
|
||
$nw = $maxw;
|
||
$nh = $ch * $maxw / $cw;
|
||
}
|
||
elseif ($maxh > 0 && ($maxw <= 0 || $maxh/$ch < $maxw/$cw))
|
||
{
|
||
$nw = $cw * $maxh / $ch;
|
||
$nh = $maxh;
|
||
}
|
||
return [ intval($nw), intval($nh) ];
|
||
}
|
||
|
||
/**
|
||
* Return file size description
|
||
*/
|
||
static $sizeSuffix = [
|
||
'Кб' => [
|
||
'bytes' => 'bytes',
|
||
'Kb' => 'Кб',
|
||
'Mb' => 'Мб',
|
||
'Gb' => 'Гб',
|
||
],
|
||
];
|
||
public static function sizeString($bytes, $lang = 'ru')
|
||
{
|
||
$r = $bytes;
|
||
if (is_numeric($r))
|
||
{
|
||
if ($r >= 0 && $r < 1024)
|
||
$r = $r . ' ' . (isset(self::$sizeString[$lang]['bytes']) ? self::$sizeString[$lang]['bytes'] : 'bytes');
|
||
elseif ($r >= 1024 && $r < 1024*1024)
|
||
$r = sprintf('%.2f ', $r/1024) . (isset(self::$sizeString[$lang]['Kb']) ? self::$sizeString[$lang]['Kb'] : 'Kb');
|
||
elseif ($r >= 1024*1024 && $r < 1024*1024*1024)
|
||
$r = sprintf('%.2f ', $r/1024/1024) . (isset(self::$sizeString[$lang]['Mb']) ? self::$sizeString[$lang]['Mb'] : 'Mb');
|
||
elseif ($r >= 1024*1024*1024)
|
||
$r = sprintf('%.2f ', $r/1024/1024/1024) . (isset(self::$sizeString[$lang]['Gb']) ? self::$sizeString[$lang]['Gb'] : 'Gb');
|
||
elseif ($r < 0)
|
||
$r = sprintf('%.2f ', 2-($r/1024/1024/1024)) . (isset(self::$sizeString[$lang]['Gb']) ? self::$sizeString[$lang]['Gb'] : 'Gb');
|
||
}
|
||
return $r;
|
||
}
|
||
|
||
/**
|
||
* Recursively create a directory
|
||
*/
|
||
public static function mkpath($path, $throw_error = true)
|
||
{
|
||
if (is_dir($path) || @mkdir($path, 0777, true))
|
||
{
|
||
return true;
|
||
}
|
||
if ($throw_error)
|
||
{
|
||
$error = error_get_last();
|
||
throw new Exception("Failed to create path $path: ".$error['message']);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Get thumbnail size for $file
|
||
*/
|
||
public static function getThumbSize($file, $width, $height, $crop = false)
|
||
{
|
||
if (empty($file['id']))
|
||
return NULL;
|
||
if ($width < 0)
|
||
$width = 0;
|
||
if ($height < 0)
|
||
$height = 0;
|
||
if (!$width || !$height)
|
||
$crop = false;
|
||
if (!$file['width'] || !$width && !$height ||
|
||
(!$width || $file['width'] <= $width) &&
|
||
(!$height || $file['height'] < $height) && !$crop ||
|
||
($file['format'] == 'swf' || $file['format'] == 'video'))
|
||
return NULL;
|
||
if (!$crop)
|
||
return self::thumbnailSize($file['width'], $file['height'], $width, $height);
|
||
return [ $width, $height ];
|
||
}
|
||
|
||
/**
|
||
* Convert ImageMagick image $im to a thumbnail
|
||
*/
|
||
public static function makeThumb($im, $width, $height, $crop, $orientation, $alignY)
|
||
{
|
||
$cl = get_class($im);
|
||
if ($crop)
|
||
{
|
||
$iw = $im->getImageWidth();
|
||
$ih = $im->getImageHeight();
|
||
if ($crop == File::CROP_Y && $ih*$width/$iw < $height)
|
||
{
|
||
$height = $ih*$width/$iw;
|
||
}
|
||
elseif ($crop == File::CROP_X && $iw*$height/$ih < $width)
|
||
{
|
||
$width = $iw*$height/$ih;
|
||
}
|
||
if ($width/$height < $iw/$ih)
|
||
{
|
||
$cw = intval($width*$ih/$height);
|
||
$im->cropImage($cw, $ih, intval(($iw-$cw)/2), 0);
|
||
}
|
||
elseif ($width/$height > $iw/$ih)
|
||
{
|
||
$ch = intval($height*$iw/$width);
|
||
$im->cropImage($iw, $ch, 0, intval(($ih-$ch)*$alignY));
|
||
}
|
||
$im->resizeImage($width, $height, $cl::FILTER_LANCZOS, 1);
|
||
$im->setImagePage($width, $height, 0, 0); /* for gif cropping */
|
||
$iw = $im->getImageWidth();
|
||
$ih = $im->getImageHeight();
|
||
}
|
||
else
|
||
{
|
||
$im->resizeImage($width, $height, $cl::FILTER_LANCZOS, 1);
|
||
}
|
||
$im->stripImage();
|
||
if ($orientation !== NULL)
|
||
{
|
||
self::fixOrientation($im, $orientation);
|
||
}
|
||
}
|
||
}
|