php-file-layer/FileUtils.php

510 lines
16 KiB
PHP
Raw Permalink Normal View History

<?php
2018-09-10 02:33:47 +03:00
/**
* Simple file upload layer. Handles file metadata and storage
* FileUtils: part that handles image metadata and thumbnails
*
2019-05-05 01:02:19 +03:00
* Version 2019-05-05
* (c) Vitaliy Filippov 2018+
2018-09-10 02:33:47 +03:00
*/
class FileUtils
{
const ONLY_BINARY = 1;
const ONLY_IMAGES = 2;
const ONLY_SWF = 4;
const ONLY_VIDEO = 8;
const IMAGES_VIDEO = 10;
const ANY_MEDIA = 14;
const ANYTHING = 15;
const CROP_XY = 1;
const CROP_Y = 2;
const CROP_X = 3;
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();
}
/**
* Get image or file properties (content SHA1, width, height, selected EXIF properties)
*/
2018-09-10 02:33:47 +03:00
public static function getProps($allowed_types, $mime_blacklist, $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');
}
2018-09-10 02:33:47 +03:00
elseif (preg_match($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 = FileUtils::ONLY_BINARY;
$props = NULL;
if ($mime == 'application/x-shockwave-flash')
{
$props = self::getSWFProps($fs_path);
if ($props)
$flag = FileUtils::ONLY_SWF;
}
elseif (substr($mime, 0, 6) == 'image/')
{
$props = self::getImageProps($fs_path);
if ($props)
$flag = FileUtils::ONLY_IMAGES;
}
elseif (substr($mime, 0, 6) == 'video/')
{
$props = VideoUtils::getVideoProps($fs_path);
if ($props)
$flag = FileUtils::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);
}
}
}
public static function getGPS($file)
{
$props = $file['props'];
if (empty($props['GPSLatitude']) && empty($props['GPSLongitude']))
{
return NULL;
}
$latitude = FileUtils::exifGPS($props['GPSLatitude'], $props['GPSLatitudeRef']);
$longitude = FileUtils::exifGPS($props['GPSLongitude'], $props['GPSLongitudeRef']);
return [ $latitude, $longitude ];
}
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);
}
/**
* 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 ($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 == FileUtils::CROP_Y && $ih*$width/$iw < $height)
{
$height = $ih*$width/$iw;
}
elseif ($crop == FileUtils::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);
}
}
/**
* Get thumbnail type
*/
public static function getThumbType($file, $width, $height, $crop = false, $alignY = 0.5)
{
$size = FileUtils::getThumbSize($file, $width, $height, $crop);
if (!$size)
{
return NULL;
}
list($width, $height) = $size;
if (!$crop)
{
$type = intval($width);
}
else
{
$p = $crop == FileUtils::CROP_Y ? 'cy' : ($crop == FileUtils::CROP_X ? 'cx' : 'c');
2019-05-05 01:02:19 +03:00
$type = intval($width).'x'.intval($height).'_'.$p.$alignY;
}
return [ 'width' => $width, 'height' => $height, 'type' => $type ];
}
public static function generateThumbnail($sourcefn, $thumbfn, $file, $width, $height, $crop = false, $alignY = 0.5)
{
try
{
$im = FileUtils::magick();
$im->readImage($sourcefn);
$props = $file['props'];
if (!empty($props['Orientation']) && $props['Orientation'] > 5)
{
/* swap width & height */
$t = $width;
$width = $height;
$height = $t;
if ($crop == FileUtils::CROP_X || $crop == FileUtils::CROP_Y)
$crop = 5-$crop;
}
FileUtils::makeThumb($im, $width, $height, $crop, isset($props['Orientation']) ? $props['Orientation'] : NULL, $alignY);
$im->setCompressionQuality(FileUtils::$quality);
FileUtils::mkpath(dirname($thumbfn));
$im->writeImage($thumbfn);
}
catch (Exception $e)
{
trigger_error("$e");
return false;
}
return true;
}
}