php-file-layer/FileUtils.php

510 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
/**
* Simple file upload layer. Handles file metadata and storage
* FileUtils: part that handles image metadata and thumbnails
*
* Version 2019-05-05
* (c) Vitaliy Filippov 2018+
*/
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)
*/
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');
}
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');
$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;
}
}