2020-01-26 13:19:35 +00:00
< ? php declare ( strict_types = 1 );
2019-06-18 18:45:59 +00:00
2019-08-16 14:40:42 +00:00
require_once " config.php " ;
require_once " events.php " ;
require_once " media_engine.php " ;
2019-06-18 18:45:59 +00:00
/*
2019-06-24 15:05:16 +00:00
* This is used by the media code when there is an error
2019-06-18 18:45:59 +00:00
*/
2019-06-24 15:05:16 +00:00
class MediaException extends SCoreException
2019-06-18 18:45:59 +00:00
{
}
2019-06-24 15:05:16 +00:00
class Media extends Extension
2019-06-18 18:45:59 +00:00
{
2020-02-04 00:46:36 +00:00
/** @var MediaTheme */
protected $theme ;
2019-06-18 18:45:59 +00:00
const WEBP_LOSSY = " webp-lossy " ;
const WEBP_LOSSLESS = " webp-lossless " ;
2019-06-24 15:05:16 +00:00
const IMAGE_MEDIA_ENGINES = [
" GD " => MediaEngine :: GD ,
" ImageMagick " => MediaEngine :: IMAGICK ,
2019-06-18 18:45:59 +00:00
];
const LOSSLESS_FORMATS = [
self :: WEBP_LOSSLESS ,
2020-05-28 15:05:20 +00:00
EXTENSION_PNG ,
EXTENSION_PSD ,
EXTENSION_BMP ,
EXTENSION_ICO ,
EXTENSION_CUR ,
EXTENSION_ANI ,
EXTENSION_GIF
2019-06-25 20:17:13 +00:00
2019-06-18 18:45:59 +00:00
];
const ALPHA_FORMATS = [
self :: WEBP_LOSSLESS ,
self :: WEBP_LOSSY ,
2020-05-28 15:05:20 +00:00
EXTENSION_WEBP ,
EXTENSION_PNG ,
2019-06-18 18:45:59 +00:00
];
const FORMAT_ALIASES = [
2020-05-28 15:05:20 +00:00
EXTENSION_TIF => EXTENSION_TIFF ,
EXTENSION_JPEG => EXTENSION_JPG ,
2019-06-18 18:45:59 +00:00
];
2019-06-24 15:05:16 +00:00
//RIFF####WEBPVP8?..............ANIM
private const WEBP_ANIMATION_HEADER =
[ 0x52 , 0x49 , 0x46 , 0x46 , null , null , null , null , 0x57 , 0x45 , 0x42 , 0x50 , 0x56 , 0x50 , 0x38 , null ,
null , null , null , null , null , null , null , null , null , null , null , null , null , null , 0x41 , 0x4E , 0x49 , 0x4D ];
//RIFF####WEBPVP8L
private const WEBP_LOSSLESS_HEADER =
[ 0x52 , 0x49 , 0x46 , 0x46 , null , null , null , null , 0x57 , 0x45 , 0x42 , 0x50 , 0x56 , 0x50 , 0x38 , 0x4C ];
2019-09-29 13:30:55 +00:00
public static function imagick_available () : bool
2019-06-18 18:45:59 +00:00
{
return extension_loaded ( " imagick " );
}
/**
* High priority just so that it can be early in the settings
*/
public function get_priority () : int
{
return 30 ;
}
public function onInitExt ( InitExtEvent $event )
{
global $config ;
2019-06-24 15:05:16 +00:00
$config -> set_default_string ( MediaConfig :: FFPROBE_PATH , 'ffprobe' );
$config -> set_default_int ( MediaConfig :: MEM_LIMIT , parse_shorthand_int ( '8MB' ));
2019-06-25 20:17:13 +00:00
$config -> set_default_string ( MediaConfig :: FFMPEG_PATH , 'ffmpeg' );
2019-06-24 15:05:16 +00:00
$config -> set_default_string ( MediaConfig :: CONVERT_PATH , 'convert' );
2019-06-18 18:45:59 +00:00
}
2019-06-25 20:17:13 +00:00
public function onPageRequest ( PageRequestEvent $event )
{
2019-10-02 10:23:57 +00:00
global $page , $user ;
2019-06-25 20:17:13 +00:00
2019-09-29 18:00:51 +00:00
if ( $event -> page_matches ( " media_rescan/ " ) && $user -> can ( Permissions :: RESCAN_MEDIA ) && isset ( $_POST [ 'image_id' ])) {
2019-06-25 20:17:13 +00:00
$image = Image :: by_id ( int_escape ( $_POST [ 'image_id' ]));
2020-01-29 20:22:50 +00:00
send_event ( new MediaCheckPropertiesEvent ( $image ));
$image -> save_to_db ();
2019-06-25 20:17:13 +00:00
$page -> set_mode ( PageMode :: REDIRECT );
$page -> set_redirect ( make_link ( " post/view/ $image->id " ));
}
}
2019-06-18 18:45:59 +00:00
public function onSetupBuilding ( SetupBuildingEvent $event )
{
2019-06-24 15:05:16 +00:00
$sb = new SetupBlock ( " Media Engines " );
2019-06-18 18:45:59 +00:00
// if (self::imagick_available()) {
// try {
// $image = new Imagick(realpath('tests/favicon.png'));
// $image->clear();
// $sb->add_label("ImageMagick detected");
// } catch (ImagickException $e) {
// $sb->add_label("<b style='color:red'>ImageMagick not detected</b>");
// }
// } else {
2019-06-27 04:00:49 +00:00
$sb -> start_table ();
$sb -> add_table_header ( " Commands " );
$sb -> add_text_option ( MediaConfig :: CONVERT_PATH , " convert " , true );
2019-06-18 18:45:59 +00:00
// }
2019-11-29 02:20:48 +00:00
$sb -> add_text_option ( MediaConfig :: FFMPEG_PATH , " ffmpeg " , true );
$sb -> add_text_option ( MediaConfig :: FFPROBE_PATH , " ffprobe " , true );
2019-06-18 18:45:59 +00:00
2020-06-22 00:09:04 +00:00
$sb -> add_shorthand_int_option ( MediaConfig :: MEM_LIMIT , " Mem limit " , true );
2019-06-27 04:00:49 +00:00
$sb -> end_table ();
2019-06-18 18:45:59 +00:00
$event -> panel -> add_block ( $sb );
}
2019-06-25 20:17:13 +00:00
public function onAdminBuilding ( AdminBuildingEvent $event )
{
global $database ;
$types = $database -> get_all ( " SELECT ext, count(*) count FROM images group by ext " );
$this -> theme -> display_form ( $types );
}
public function onAdminAction ( AdminActionEvent $event )
{
$action = $event -> action ;
if ( method_exists ( $this , $action )) {
$event -> redirect = $this -> $action ();
}
}
public function onImageAdminBlockBuilding ( ImageAdminBlockBuildingEvent $event )
{
global $user ;
2019-07-09 14:10:21 +00:00
if ( $user -> can ( Permissions :: DELETE_IMAGE )) {
2019-06-25 20:17:13 +00:00
$event -> add_part ( $this -> theme -> get_buttons_html ( $event -> image -> id ));
}
}
public function onBulkActionBlockBuilding ( BulkActionBlockBuildingEvent $event )
{
global $user ;
2019-09-29 18:00:51 +00:00
if ( $user -> can ( Permissions :: RESCAN_MEDIA )) {
2019-06-25 20:17:13 +00:00
$event -> add_action ( " bulk_media_rescan " , " Scan Media Properties " );
}
}
public function onBulkAction ( BulkActionEvent $event )
{
2019-12-15 19:47:18 +00:00
global $page , $user ;
2019-06-25 20:17:13 +00:00
switch ( $event -> action ) {
case " bulk_media_rescan " :
2019-09-29 18:00:51 +00:00
if ( $user -> can ( Permissions :: RESCAN_MEDIA )) {
2019-06-25 20:17:13 +00:00
$total = 0 ;
2019-10-02 09:10:47 +00:00
$failed = 0 ;
2019-07-05 15:36:07 +00:00
foreach ( $event -> items as $image ) {
2019-06-25 20:17:13 +00:00
try {
2020-02-02 17:01:17 +00:00
log_debug ( " media " , " Rescanning media for { $image -> hash } ( { $image -> id } ) " );
2020-01-29 20:22:50 +00:00
send_event ( new MediaCheckPropertiesEvent ( $image ));
2020-02-02 17:01:17 +00:00
$image -> save_to_db ();
2019-06-25 20:17:13 +00:00
$total ++ ;
} catch ( MediaException $e ) {
2019-10-02 09:10:47 +00:00
$failed ++ ;
2019-06-25 20:17:13 +00:00
}
}
2019-12-15 19:47:18 +00:00
$page -> flash ( " Scanned media properties for $total items, failed for $failed " );
2019-06-25 20:17:13 +00:00
}
break ;
}
}
2019-10-04 19:50:36 +00:00
public function onCommand ( CommandEvent $event )
{
if ( $event -> cmd == " help " ) {
2019-10-04 20:10:00 +00:00
print " \t media-rescan <id / hash> \n " ;
2019-10-04 19:50:36 +00:00
print " \t \t refresh metadata for a given post \n \n " ;
}
if ( $event -> cmd == " media-rescan " ) {
$uid = $event -> args [ 0 ];
$image = Image :: by_id_or_hash ( $uid );
if ( $image ) {
2020-01-29 20:22:50 +00:00
send_event ( new MediaCheckPropertiesEvent ( $image ));
$image -> save_to_db ();
2019-10-04 19:50:36 +00:00
} else {
print ( " No post with ID ' $uid ' \n " );
}
}
}
2019-06-18 18:45:59 +00:00
/**
2019-06-24 15:05:16 +00:00
* @ param MediaResizeEvent $event
* @ throws MediaException
2019-06-18 18:45:59 +00:00
* @ throws InsufficientMemoryException
*/
2019-06-24 15:05:16 +00:00
public function onMediaResize ( MediaResizeEvent $event )
2019-06-18 18:45:59 +00:00
{
switch ( $event -> engine ) {
2019-06-24 15:05:16 +00:00
case MediaEngine :: GD :
2019-06-18 18:45:59 +00:00
$info = getimagesize ( $event -> input_path );
if ( $info === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " getimagesize failed for " . $event -> input_path );
2019-06-18 18:45:59 +00:00
}
self :: image_resize_gd (
$event -> input_path ,
$info ,
$event -> target_width ,
$event -> target_height ,
$event -> output_path ,
$event -> target_format ,
$event -> ignore_aspect_ratio ,
$event -> target_quality ,
2019-09-29 13:30:55 +00:00
$event -> allow_upscale
);
2019-06-18 18:45:59 +00:00
break ;
2019-06-24 15:05:16 +00:00
case MediaEngine :: IMAGICK :
2019-06-18 18:45:59 +00:00
// if (self::imagick_available()) {
// } else {
2019-06-18 18:55:18 +00:00
self :: image_resize_convert (
$event -> input_path ,
$event -> input_type ,
$event -> target_width ,
$event -> target_height ,
$event -> output_path ,
$event -> target_format ,
$event -> ignore_aspect_ratio ,
$event -> target_quality ,
$event -> minimize ,
2019-09-29 13:30:55 +00:00
$event -> allow_upscale
);
2019-06-18 18:45:59 +00:00
//}
break ;
2020-01-28 22:23:03 +00:00
case MediaEngine :: STATIC :
copy ( $event -> input_path , $event -> output_path );
break ;
2019-06-18 18:45:59 +00:00
default :
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Engine not supported for resize: " . $event -> engine );
2019-06-18 18:45:59 +00:00
}
// TODO: Get output optimization tools working better
// if ($config->get_bool("thumb_optim", false)) {
// exec("jpegoptim $outname", $output, $ret);
// }
}
2019-06-25 20:17:13 +00:00
2019-08-16 14:40:42 +00:00
const CONTENT_SEARCH_TERM_REGEX = " /^content[=|:]((video)|(audio)|(image)|(unknown)) $ /i " ;
2019-06-25 20:17:13 +00:00
public function onSearchTermParse ( SearchTermParseEvent $event )
2019-06-24 15:05:16 +00:00
{
2019-06-25 20:17:13 +00:00
global $database ;
2020-01-26 16:38:26 +00:00
if ( is_null ( $event -> term )) {
return ;
}
2020-01-26 13:19:35 +00:00
2019-06-25 20:17:13 +00:00
$matches = [];
if ( preg_match ( self :: CONTENT_SEARCH_TERM_REGEX , $event -> term , $matches )) {
2019-08-16 14:40:42 +00:00
$field = $matches [ 1 ];
2019-11-02 19:57:34 +00:00
if ( $field === " unknown " ) {
2020-02-01 22:44:50 +00:00
$event -> add_querylet ( new Querylet ( " video IS NULL OR audio IS NULL OR image IS NULL " ));
2019-08-16 14:40:42 +00:00
} else {
$event -> add_querylet ( new Querylet ( $database -> scoreql_to_sql ( " $field = SCORE_BOOL_Y " )));
}
2019-06-25 20:17:13 +00:00
}
}
2019-08-02 20:05:49 +00:00
public function onHelpPageBuilding ( HelpPageBuildingEvent $event )
{
2019-09-29 13:30:55 +00:00
if ( $event -> key === HelpPages :: SEARCH ) {
2019-08-02 20:05:49 +00:00
$block = new Block ();
$block -> header = " Media " ;
$block -> body = $this -> theme -> get_help_html ();
$event -> add_block ( $block );
}
}
2020-01-29 20:22:50 +00:00
public function onTagTermCheck ( TagTermCheckEvent $event )
2019-06-25 20:17:13 +00:00
{
2020-01-29 20:22:50 +00:00
if ( preg_match ( self :: CONTENT_SEARCH_TERM_REGEX , $event -> term )) {
2019-06-25 20:17:13 +00:00
$event -> metatag = true ;
}
}
2020-02-09 19:22:25 +00:00
public function onParseLinkTemplate ( ParseLinkTemplateEvent $event )
{
if ( $event -> image -> width && $event -> image -> height && $event -> image -> length ) {
$s = (( int )( $event -> image -> length / 100 )) / 10 ;
$event -> replace ( '$size' , " { $event -> image -> width } x { $event -> image -> height } , ${ s } s " );
} elseif ( $event -> image -> width && $event -> image -> height ) {
$event -> replace ( '$size' , " { $event -> image -> width } x { $event -> image -> height } " );
} elseif ( $event -> image -> length ) {
$s = (( int )( $event -> image -> length / 100 )) / 10 ;
$event -> replace ( '$size' , " ${ s } s " );
}
$event -> replace ( '$ext' , $event -> image -> ext );
}
2019-06-18 18:45:59 +00:00
/**
* Check Memory usage limits
*
* Old check : $memory_use = ( filesize ( $image_filename ) * 2 ) + ( $width * $height * 4 ) + ( 4 * 1024 * 1024 );
* New check : $memory_use = $width * $height * ( $bits_per_channel ) * channels * 2.5
*
* It didn ' t make sense to compute the memory usage based on the NEW size for the image . ( $width * $height * 4 )
* We need to consider the size that we are GOING TO instead .
*
* The factor of 2.5 is simply a rough guideline .
2020-03-25 11:47:00 +00:00
* https :// stackoverflow . com / questions / 527532 / reasonable - php - memory - limit - for - image - resize
2019-06-18 18:45:59 +00:00
*
* @ param array $info The output of getimagesize () for the source file in question .
* @ return int The number of bytes an image resize operation is estimated to use .
*/
2019-06-21 20:23:59 +00:00
public static function calc_memory_use ( array $info ) : int
2019-06-18 18:45:59 +00:00
{
if ( isset ( $info [ 'bits' ]) && isset ( $info [ 'channels' ])) {
$memory_use = ( $info [ 0 ] * $info [ 1 ] * ( $info [ 'bits' ] / 8 ) * $info [ 'channels' ] * 2.5 ) / 1024 ;
} else {
// If we don't have bits and channel info from the image then assume default values
// of 8 bits per color and 4 channels (R,G,B,A) -- ie: regular 24-bit color
$memory_use = ( $info [ 0 ] * $info [ 1 ] * 1 * 4 * 2.5 ) / 1024 ;
}
return ( int ) $memory_use ;
}
/**
* Creates a thumbnail using ffmpeg .
*
* @ param $hash
* @ return bool true if successful , false if not .
2019-06-24 15:05:16 +00:00
* @ throws MediaException
2019-06-18 18:45:59 +00:00
*/
2019-06-21 20:23:59 +00:00
public static function create_thumbnail_ffmpeg ( $hash ) : bool
2019-06-18 18:45:59 +00:00
{
global $config ;
2019-06-24 15:05:16 +00:00
$ffmpeg = $config -> get_string ( MediaConfig :: FFMPEG_PATH );
2019-06-18 18:45:59 +00:00
if ( $ffmpeg == null || $ffmpeg == " " ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " ffmpeg command configured " );
2019-06-18 18:45:59 +00:00
}
$inname = warehouse_path ( Image :: IMAGE_DIR , $hash );
$outname = warehouse_path ( Image :: THUMBNAIL_DIR , $hash );
$orig_size = self :: video_size ( $inname );
$scaled_size = get_thumbnail_size ( $orig_size [ 0 ], $orig_size [ 1 ], true );
$codec = " mjpeg " ;
$quality = $config -> get_int ( ImageConfig :: THUMB_QUALITY );
2020-05-28 15:05:20 +00:00
if ( $config -> get_string ( ImageConfig :: THUMB_TYPE ) == EXTENSION_WEBP ) {
2019-06-18 18:45:59 +00:00
$codec = " libwebp " ;
} else {
// mjpeg quality ranges from 2-31, with 2 being the best quality.
$quality = floor ( 31 - ( 31 * ( $quality / 100 )));
if ( $quality < 2 ) {
$quality = 2 ;
}
}
$args = [
escapeshellarg ( $ffmpeg ),
" -y " , " -i " , escapeshellarg ( $inname ),
" -vf " , " thumbnail,scale= { $scaled_size [ 0 ] } : { $scaled_size [ 1 ] } " ,
" -f " , " image2 " ,
" -vframes " , " 1 " ,
" -c:v " , $codec ,
" -q:v " , $quality ,
escapeshellarg ( $outname ),
];
$cmd = escapeshellcmd ( implode ( " " , $args ));
exec ( $cmd , $output , $ret );
if (( int ) $ret == ( int ) 0 ) {
2020-02-02 17:01:17 +00:00
log_debug ( 'media' , " Generating thumbnail with command ` $cmd `, returns $ret " );
2019-06-18 18:45:59 +00:00
return true ;
} else {
2020-02-02 17:01:17 +00:00
log_error ( 'media' , " Generating thumbnail with command ` $cmd `, returns $ret " );
2019-06-18 18:45:59 +00:00
return false ;
}
}
2019-06-25 20:17:13 +00:00
public static function get_ffprobe_data ( $filename ) : array
{
global $config ;
$ffprobe = $config -> get_string ( MediaConfig :: FFPROBE_PATH );
if ( $ffprobe == null || $ffprobe == " " ) {
throw new MediaException ( " ffprobe command configured " );
}
$args = [
escapeshellarg ( $ffprobe ),
" -print_format " , " json " ,
" -v " , " quiet " ,
" -show_format " ,
" -show_streams " ,
escapeshellarg ( $filename ),
];
$cmd = escapeshellcmd ( implode ( " " , $args ));
exec ( $cmd , $output , $ret );
if (( int ) $ret == ( int ) 0 ) {
2020-02-02 17:01:17 +00:00
log_debug ( 'media' , " Getting media data ` $cmd `, returns $ret " );
2019-06-25 20:17:13 +00:00
$output = implode ( $output );
2020-01-26 13:19:35 +00:00
return json_decode ( $output , true );
2019-06-25 20:17:13 +00:00
} else {
2020-02-02 17:01:17 +00:00
log_error ( 'media' , " Getting media data ` $cmd `, returns $ret " );
2019-06-25 20:17:13 +00:00
return [];
}
}
2020-01-26 23:12:48 +00:00
public static function determine_ext ( string $format ) : string
2019-06-18 18:45:59 +00:00
{
$format = self :: normalize_format ( $format );
switch ( $format ) {
case self :: WEBP_LOSSLESS :
case self :: WEBP_LOSSY :
2020-05-28 15:05:20 +00:00
return EXTENSION_WEBP ;
2019-06-18 18:45:59 +00:00
default :
return $format ;
}
}
// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
// {
// switch ($format) {
2020-05-28 15:05:20 +00:00
// case EXTENSION_PNG:
2019-06-18 18:45:59 +00:00
// $result = $image->setOption('png:compression-level', 9);
// if ($result !== true) {
// throw new GraphicsException("Could not set png compression option");
// }
// break;
// case Graphics::WEBP_LOSSLESS:
// $result = $image->setOption('webp:lossless', true);
// if ($result !== true) {
// throw new GraphicsException("Could not set lossless webp option");
// }
// break;
// default:
// $result = $image->setImageCompressionQuality($output_quality);
// if ($result !== true) {
// throw new GraphicsException("Could not set compression quality for $path to $output_quality");
// }
// break;
// }
//
// if (self::supports_alpha($format)) {
// $result = $image->setImageBackgroundColor(new \ImagickPixel('transparent'));
// } else {
// $result = $image->setImageBackgroundColor(new \ImagickPixel('black'));
// }
// if ($result !== true) {
// throw new GraphicsException("Could not set background color");
// }
//
//
// if ($minimize) {
// $profiles = $image->getImageProfiles("icc", true);
// $result = $image->stripImage();
// if ($result !== true) {
// throw new GraphicsException("Could not strip information from image");
// }
// if (!empty($profiles)) {
// $image->profileImage("icc", $profiles['icc']);
// }
// }
//
// $ext = self::determine_ext($format);
//
// $result = $image->writeImage($ext . ":" . $path);
// if ($result !== true) {
// throw new GraphicsException("Could not write image to $path");
// }
// }
// public static function image_resize_imagick(
// String $input_path,
// String $input_type,
// int $new_width,
// int $new_height,
// string $output_filename,
// string $output_type = null,
// bool $ignore_aspect_ratio = false,
// int $output_quality = 80,
// bool $minimize = false,
// bool $allow_upscale = true
// ): void
// {
// global $config;
//
// if (!empty($input_type)) {
// $input_type = self::determine_ext($input_type);
// }
//
// try {
// $image = new Imagick($input_type . ":" . $input_path);
// try {
// $result = $image->flattenImages();
// if ($result !== true) {
// throw new GraphicsException("Could not flatten image $input_path");
// }
//
// $height = $image->getImageHeight();
// $width = $image->getImageWidth();
// if (!$allow_upscale &&
// ($new_width > $width || $new_height > $height)) {
// $new_height = $height;
// $new_width = $width;
// }
//
// $result = $image->resizeImage($new_width, $new_width, Imagick::FILTER_LANCZOS, 0, !$ignore_aspect_ratio);
// if ($result !== true) {
// throw new GraphicsException("Could not perform image resize on $input_path");
// }
//
//
// if (empty($output_type)) {
// $output_type = $input_type;
// }
//
// self::image_save_imagick($image, $output_filename, $output_type, $output_quality);
//
// } finally {
// $image->destroy();
// }
// } catch (ImagickException $e) {
// throw new GraphicsException("Error while resizing with Imagick: " . $e->getMessage(), $e->getCode(), $e);
// }
// }
2019-09-29 13:30:55 +00:00
public static function is_lossless ( string $filename , string $format )
{
if ( in_array ( $format , self :: LOSSLESS_FORMATS )) {
2019-06-25 23:43:57 +00:00
return true ;
}
2019-09-29 13:30:55 +00:00
switch ( $format ) {
2020-05-28 15:05:20 +00:00
case EXTENSION_WEBP :
2019-06-25 23:43:57 +00:00
return self :: is_lossless_webp ( $filename );
break ;
}
return false ;
}
2019-06-18 18:45:59 +00:00
public static function image_resize_convert (
2020-01-26 23:12:48 +00:00
string $input_path ,
string $input_type ,
2019-06-18 18:45:59 +00:00
int $new_width ,
int $new_height ,
string $output_filename ,
string $output_type = null ,
bool $ignore_aspect_ratio = false ,
int $output_quality = 80 ,
bool $minimize = false ,
bool $allow_upscale = true
2019-09-29 13:30:55 +00:00
) : void {
2019-06-18 18:45:59 +00:00
global $config ;
2019-06-24 15:05:16 +00:00
$convert = $config -> get_string ( MediaConfig :: CONVERT_PATH );
2019-06-18 18:45:59 +00:00
2019-06-21 20:23:59 +00:00
if ( empty ( $convert )) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " convert command not configured " );
2019-06-18 18:45:59 +00:00
}
if ( empty ( $output_type )) {
$output_type = $input_type ;
}
2020-05-28 15:05:20 +00:00
if ( $output_type == EXTENSION_WEBP && self :: is_lossless ( $input_path , $input_type )) {
2019-06-25 23:43:57 +00:00
$output_type = self :: WEBP_LOSSLESS ;
}
2019-06-18 18:45:59 +00:00
$bg = " black " ;
if ( self :: supports_alpha ( $output_type )) {
$bg = " none " ;
}
if ( ! empty ( $input_type )) {
$input_type = $input_type . " : " ;
}
2019-06-25 23:43:57 +00:00
2019-06-18 18:45:59 +00:00
$resize_args = " " ;
if ( ! $allow_upscale ) {
$resize_args .= " \ > " ;
}
if ( $ignore_aspect_ratio ) {
$resize_args .= " \ ! " ;
}
2019-07-05 15:36:07 +00:00
$args = " " ;
2019-06-25 23:43:57 +00:00
switch ( $output_type ) {
case Media :: WEBP_LOSSLESS :
$args .= '-define webp:lossless=true' ;
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_PNG :
2019-06-25 23:43:57 +00:00
$args .= '-define png:compression-level=9' ;
break ;
}
2019-07-05 15:36:07 +00:00
if ( $minimize ) {
$args .= " -strip -thumbnail " ;
} else {
$args .= " -resize " ;
}
2019-06-25 23:43:57 +00:00
$output_ext = self :: determine_ext ( $output_type );
2019-06-28 00:20:22 +00:00
$format = '"%s" %s %ux%u%s -quality %u -background %s %s"%s[0]" %s:"%s" 2>&1' ;
2019-06-25 23:43:57 +00:00
$cmd = sprintf ( $format , $convert , $args , $new_width , $new_height , $resize_args , $output_quality , $bg , $input_type , $input_path , $output_ext , $output_filename );
2019-06-18 18:45:59 +00:00
$cmd = str_replace ( " \" convert \" " , " convert " , $cmd ); // quotes are only needed if the path to convert contains a space; some other times, quotes break things, see github bug #27
exec ( $cmd , $output , $ret );
if ( $ret != 0 ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Resizing image with command ` $cmd `, returns $ret , outputting " . implode ( " \r \n " , $output ));
2019-06-18 18:45:59 +00:00
} else {
2020-02-02 17:01:17 +00:00
log_debug ( 'media' , " Generating thumbnail with command ` $cmd `, returns $ret " );
2019-06-18 18:45:59 +00:00
}
}
/**
* Performs a resize operation on an image file using GD .
*
* @ param String $image_filename The source file to be resized .
* @ param array $info The output of getimagesize () for the source file .
* @ param int $new_width
* @ param int $new_height
* @ param string $output_filename
* @ param string | null $output_type If set to null , the output file type will be automatically determined via the $info parameter . Otherwise an exception will be thrown .
* @ param int $output_quality Defaults to 80.
2019-06-24 15:05:16 +00:00
* @ throws MediaException
2019-06-18 18:45:59 +00:00
* @ throws InsufficientMemoryException if the estimated memory usage exceeds the memory limit .
*/
public static function image_resize_gd (
2020-01-26 23:12:48 +00:00
string $image_filename ,
2019-06-18 18:45:59 +00:00
array $info ,
int $new_width ,
int $new_height ,
string $output_filename ,
string $output_type = null ,
bool $ignore_aspect_ratio = false ,
int $output_quality = 80 ,
bool $allow_upscale = true
2019-09-29 13:30:55 +00:00
) {
2019-06-18 18:45:59 +00:00
$width = $info [ 0 ];
$height = $info [ 1 ];
if ( $output_type == null ) {
/* If not specified, output to the same format as the original image */
switch ( $info [ 2 ]) {
case IMAGETYPE_GIF :
2020-05-28 15:05:20 +00:00
$output_type = EXTENSION_GIF ;
2019-06-18 18:45:59 +00:00
break ;
case IMAGETYPE_JPEG :
2020-05-28 15:05:20 +00:00
$output_type = EXTENSION_JPEG ;
2019-06-18 18:45:59 +00:00
break ;
case IMAGETYPE_PNG :
2020-05-28 15:05:20 +00:00
$output_type = EXTENSION_PNG ;
2019-06-18 18:45:59 +00:00
break ;
case IMAGETYPE_WEBP :
2020-05-28 15:05:20 +00:00
$output_type = EXTENSION_WEBP ;
2019-06-18 18:45:59 +00:00
break ;
case IMAGETYPE_BMP :
2020-05-28 15:05:20 +00:00
$output_type = EXTENSION_BMP ;
2019-06-18 18:45:59 +00:00
break ;
default :
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Failed to save the new image - Unsupported image type. " );
2019-06-18 18:45:59 +00:00
}
}
$memory_use = self :: calc_memory_use ( $info );
$memory_limit = get_memory_limit ();
if ( $memory_use > $memory_limit ) {
throw new InsufficientMemoryException ( " The image is too large to resize given the memory limits. ( $memory_use > $memory_limit ) " );
}
if ( ! $ignore_aspect_ratio ) {
list ( $new_width , $new_height ) = get_scaled_by_aspect_ratio ( $width , $height , $new_width , $new_height );
}
if ( ! $allow_upscale &&
( $new_width > $width || $new_height > $height )) {
$new_height = $height ;
$new_width = $width ;
}
$image = imagecreatefromstring ( file_get_contents ( $image_filename ));
$image_resized = imagecreatetruecolor ( $new_width , $new_height );
try {
if ( $image === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Could not load image: " . $image_filename );
2019-06-18 18:45:59 +00:00
}
if ( $image_resized === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Could not create output image with dimensions $new_width c $new_height " );
2019-06-18 18:45:59 +00:00
}
// Handle transparent images
switch ( $info [ 2 ]) {
case IMAGETYPE_GIF :
$transparency = imagecolortransparent ( $image );
$pallet_size = imagecolorstotal ( $image );
// If we have a specific transparent color
if ( $transparency >= 0 && $transparency < $pallet_size ) {
// Get the original image's transparent color's RGB values
$transparent_color = imagecolorsforindex ( $image , $transparency );
// Allocate the same color in the new image resource
$transparency = imagecolorallocate ( $image_resized , $transparent_color [ 'red' ], $transparent_color [ 'green' ], $transparent_color [ 'blue' ]);
if ( $transparency === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to allocate transparent color " );
2019-06-18 18:45:59 +00:00
}
// Completely fill the background of the new image with allocated color.
if ( imagefill ( $image_resized , 0 , 0 , $transparency ) === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to fill new image with transparent color " );
2019-06-18 18:45:59 +00:00
}
// Set the background color for new image to transparent
imagecolortransparent ( $image_resized , $transparency );
}
break ;
case IMAGETYPE_PNG :
case IMAGETYPE_WEBP :
//
2020-03-25 11:47:00 +00:00
// More info here: https://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
2019-06-18 18:45:59 +00:00
//
if ( imagealphablending ( $image_resized , false ) === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to disable image alpha blending " );
2019-06-18 18:45:59 +00:00
}
if ( imagesavealpha ( $image_resized , true ) === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to enable image save alpha " );
2019-06-18 18:45:59 +00:00
}
$transparent_color = imagecolorallocatealpha ( $image_resized , 255 , 255 , 255 , 127 );
if ( $transparent_color === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to allocate transparent color " );
2019-06-18 18:45:59 +00:00
}
if ( imagefilledrectangle ( $image_resized , 0 , 0 , $new_width , $new_height , $transparent_color ) === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to fill new image with transparent color " );
2019-06-18 18:45:59 +00:00
}
break ;
}
// Actually resize the image.
if ( imagecopyresampled (
2019-09-29 13:30:55 +00:00
$image_resized ,
$image ,
0 ,
0 ,
0 ,
0 ,
$new_width ,
$new_height ,
$width ,
$height
2019-11-02 19:57:34 +00:00
) === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Unable to copy resized image data to new image " );
2019-06-18 18:45:59 +00:00
}
switch ( $output_type ) {
2020-05-28 15:05:20 +00:00
case EXTENSION_BMP :
2019-06-18 18:45:59 +00:00
$result = imagebmp ( $image_resized , $output_filename , true );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_WEBP :
2019-06-24 15:05:16 +00:00
case Media :: WEBP_LOSSY :
2019-06-18 18:45:59 +00:00
$result = imagewebp ( $image_resized , $output_filename , $output_quality );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_JPG :
case EXTENSION_JPEG :
2019-06-18 18:45:59 +00:00
$result = imagejpeg ( $image_resized , $output_filename , $output_quality );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_PNG :
2019-06-18 18:45:59 +00:00
$result = imagepng ( $image_resized , $output_filename , 9 );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_GIF :
2019-06-18 18:45:59 +00:00
$result = imagegif ( $image_resized , $output_filename );
break ;
default :
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Failed to save the new image - Unsupported image type: $output_type " );
2019-06-18 18:45:59 +00:00
}
if ( $result === false ) {
2019-06-24 15:05:16 +00:00
throw new MediaException ( " Failed to save the new image, function returned false when saving type: $output_type " );
2019-06-18 18:45:59 +00:00
}
} finally {
@ imagedestroy ( $image );
@ imagedestroy ( $image_resized );
}
}
/**
* Determines if a file is an animated gif .
*
* @ param String $image_filename The path of the file to check .
* @ return bool true if the file is an animated gif , false if it is not .
*/
2020-01-26 23:12:48 +00:00
public static function is_animated_gif ( string $image_filename ) : bool
2019-06-18 18:45:59 +00:00
{
$is_anim_gif = 0 ;
if (( $fh = @ fopen ( $image_filename , 'rb' ))) {
2019-06-24 15:05:16 +00:00
try {
2020-03-25 11:47:00 +00:00
//check if gif is animated (via https://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
2019-06-24 15:05:16 +00:00
while ( ! feof ( $fh ) && $is_anim_gif < 2 ) {
$chunk = fread ( $fh , 1024 * 100 );
2020-01-26 13:19:35 +00:00
$is_anim_gif += preg_match_all ( '#\x00\x21\xF9\x04.{4}\x00[\x2C\x21]#s' , $chunk , $matches );
2019-06-24 15:05:16 +00:00
}
} finally {
@ fclose ( $fh );
2019-06-18 18:45:59 +00:00
}
}
return ( $is_anim_gif == 0 );
}
2019-06-24 15:05:16 +00:00
2020-01-26 23:12:48 +00:00
private static function compare_file_bytes ( string $file_name , array $comparison ) : bool
2019-06-24 15:05:16 +00:00
{
2019-06-25 20:17:13 +00:00
$size = filesize ( $file_name );
2019-06-24 15:05:16 +00:00
if ( $size < count ( $comparison )) {
// Can't match because it's too small
return false ;
}
if (( $fh = @ fopen ( $file_name , 'rb' ))) {
try {
2019-06-25 20:17:13 +00:00
$chunk = unpack ( " C* " , fread ( $fh , count ( $comparison )));
2019-06-24 15:05:16 +00:00
for ( $i = 0 ; $i < count ( $comparison ); $i ++ ) {
$byte = $comparison [ $i ];
2019-06-25 20:17:13 +00:00
if ( $byte == null ) {
2019-06-24 15:05:16 +00:00
continue ;
} else {
2019-06-25 20:17:13 +00:00
$fileByte = $chunk [ $i + 1 ];
if ( $fileByte != $byte ) {
2019-06-24 15:05:16 +00:00
return false ;
}
}
}
return true ;
} finally {
@ fclose ( $fh );
}
} else {
throw new MediaException ( " Unable to open file for byte check: $file_name " );
}
}
2020-01-26 23:12:48 +00:00
public static function is_animated_webp ( string $image_filename ) : bool
2019-06-24 15:05:16 +00:00
{
return self :: compare_file_bytes ( $image_filename , self :: WEBP_ANIMATION_HEADER );
}
2020-01-26 23:12:48 +00:00
public static function is_lossless_webp ( string $image_filename ) : bool
2019-06-24 15:05:16 +00:00
{
return self :: compare_file_bytes ( $image_filename , self :: WEBP_LOSSLESS_HEADER );
}
2020-01-26 23:12:48 +00:00
public static function supports_alpha ( string $format ) : bool
2019-06-18 18:45:59 +00:00
{
return in_array ( self :: normalize_format ( $format ), self :: ALPHA_FORMATS );
}
2019-06-25 23:43:57 +00:00
public static function is_input_supported ( string $engine , string $format , ? bool $lossless = null ) : bool
2019-06-18 18:45:59 +00:00
{
2019-06-25 20:17:13 +00:00
$format = self :: normalize_format ( $format , $lossless );
2019-06-24 15:05:16 +00:00
if ( ! in_array ( $format , MediaEngine :: INPUT_SUPPORT [ $engine ])) {
2019-06-18 18:45:59 +00:00
return false ;
}
return true ;
}
2019-06-25 23:43:57 +00:00
public static function is_output_supported ( string $engine , string $format , ? bool $lossless = false ) : bool
2019-06-18 18:45:59 +00:00
{
2019-06-25 23:43:57 +00:00
$format = self :: normalize_format ( $format , $lossless );
2019-06-24 15:05:16 +00:00
if ( ! in_array ( $format , MediaEngine :: OUTPUT_SUPPORT [ $engine ])) {
2019-06-18 18:45:59 +00:00
return false ;
}
return true ;
}
/**
* Checks if a format ( normally a file extension ) is a variant name of another format ( ie , jpg and jpeg ) .
2019-06-24 15:05:16 +00:00
* If one is found , then the maine name that the Media extension will recognize is returned ,
2019-06-18 18:45:59 +00:00
* otherwise the incoming format is returned .
*
* @ param $format
2019-06-24 15:05:16 +00:00
* @ return string | null The format name that the media extension will recognize .
2019-06-18 18:45:59 +00:00
*/
2019-09-29 13:30:55 +00:00
public static function normalize_format ( string $format , ? bool $lossless = null ) : ? string
2019-06-18 18:45:59 +00:00
{
2020-05-28 15:05:20 +00:00
if ( $format == EXTENSION_WEBP ) {
2019-06-25 20:17:13 +00:00
if ( $lossless === true ) {
$format = Media :: WEBP_LOSSLESS ;
} else {
$format = Media :: WEBP_LOSSY ;
}
}
2019-06-24 15:05:16 +00:00
if ( array_key_exists ( $format , Media :: FORMAT_ALIASES )) {
2019-06-18 18:45:59 +00:00
return self :: FORMAT_ALIASES [ $format ];
}
return $format ;
}
/**
* Determines the dimensions of a video file using ffmpeg .
*
* @ param string $filename
* @ return array [ width , height ]
*/
2019-09-29 13:30:55 +00:00
public static function video_size ( string $filename ) : array
2019-06-18 18:45:59 +00:00
{
global $config ;
2019-06-24 15:05:16 +00:00
$ffmpeg = $config -> get_string ( MediaConfig :: FFMPEG_PATH );
2019-06-18 18:45:59 +00:00
$cmd = escapeshellcmd ( implode ( " " , [
escapeshellarg ( $ffmpeg ),
" -y " , " -i " , escapeshellarg ( $filename ),
" -vstats "
]));
$output = shell_exec ( $cmd . " 2>&1 " );
// error_log("Getting size with `$cmd`");
$regex_sizes = " /Video: .* ([0-9] { 1,4})x([0-9] { 1,4})/ " ;
if ( preg_match ( $regex_sizes , $output , $regs )) {
if ( preg_match ( " /displaymatrix: rotation of (90|270).00 degrees/ " , $output )) {
2020-01-26 21:14:50 +00:00
$size = [( int ) $regs [ 2 ], ( int ) $regs [ 1 ]];
2019-06-18 18:45:59 +00:00
} else {
2020-01-26 21:14:50 +00:00
$size = [( int ) $regs [ 1 ], ( int ) $regs [ 2 ]];
2019-06-18 18:45:59 +00:00
}
} else {
$size = [ 1 , 1 ];
}
2020-02-02 17:01:17 +00:00
log_debug ( 'media' , " Getting video size with ` $cmd `, returns $output -- $size[0] , $size[1] " );
2019-06-18 18:45:59 +00:00
return $size ;
}
2019-08-16 14:40:42 +00:00
2019-11-03 17:19:37 +00:00
public function onDatabaseUpgrade ( DatabaseUpgradeEvent $event )
2019-08-16 14:40:42 +00:00
{
global $config , $database ;
2019-11-03 19:04:57 +00:00
if ( $this -> get_version ( MediaConfig :: VERSION ) < 1 ) {
2019-08-16 14:40:42 +00:00
$current_value = $config -> get_string ( " thumb_ffmpeg_path " );
if ( ! empty ( $current_value )) {
$config -> set_string ( MediaConfig :: FFMPEG_PATH , $current_value );
} elseif ( $ffmpeg = shell_exec (( PHP_OS == 'WINNT' ? 'where' : 'which' ) . ' ffmpeg' )) {
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
if ( is_executable ( strtok ( $ffmpeg , PHP_EOL ))) {
$config -> set_default_string ( MediaConfig :: FFMPEG_PATH , 'ffmpeg' );
}
}
if ( $ffprobe = shell_exec (( PHP_OS == 'WINNT' ? 'where' : 'which' ) . ' ffprobe' )) {
//ffprobe exists in PATH, check if it's executable, and if so, default to it instead of static
if ( is_executable ( strtok ( $ffprobe , PHP_EOL ))) {
$config -> set_default_string ( MediaConfig :: FFPROBE_PATH , 'ffprobe' );
}
}
$current_value = $config -> get_string ( " thumb_convert_path " );
if ( ! empty ( $current_value )) {
$config -> set_string ( MediaConfig :: CONVERT_PATH , $current_value );
} elseif ( $convert = shell_exec (( PHP_OS == 'WINNT' ? 'where' : 'which' ) . ' convert' )) {
//ffmpeg exists in PATH, check if it's executable, and if so, default to it instead of static
if ( is_executable ( strtok ( $convert , PHP_EOL ))) {
$config -> set_default_string ( MediaConfig :: CONVERT_PATH , 'convert' );
}
}
$current_value = $config -> get_int ( " thumb_mem_limit " );
if ( ! empty ( $current_value )) {
$config -> set_int ( MediaConfig :: MEM_LIMIT , $current_value );
}
2019-11-03 19:04:57 +00:00
$this -> set_version ( MediaConfig :: VERSION , 1 );
2019-08-16 14:40:42 +00:00
}
2019-11-03 19:04:57 +00:00
if ( $this -> get_version ( MediaConfig :: VERSION ) < 2 ) {
2019-08-16 14:40:42 +00:00
$database -> execute ( $database -> scoreql_to_sql (
" ALTER TABLE images ADD COLUMN image SCORE_BOOL NULL "
));
2019-11-02 19:57:34 +00:00
switch ( $database -> get_driver_name ()) {
2019-08-16 14:40:42 +00:00
case DatabaseDriver :: PGSQL :
case DatabaseDriver :: SQLITE :
$database -> execute ( 'CREATE INDEX images_image_idx ON images(image) WHERE image IS NOT NULL' );
break ;
default :
$database -> execute ( 'CREATE INDEX images_image_idx ON images(image)' );
break ;
}
2019-10-14 18:31:12 +00:00
$database -> set_timeout ( 300000 ); // These updates can take a little bit
2019-08-16 14:40:42 +00:00
2019-10-10 16:02:18 +00:00
if ( $database -> transaction === true ) {
2019-11-02 19:57:34 +00:00
$database -> commit (); // Each of these commands could hit a lot of data, combining them into one big transaction would not be a good idea.
2019-10-10 16:02:18 +00:00
}
2019-08-16 14:40:42 +00:00
log_info ( " upgrade " , " Setting predictable media values for known file types " );
$database -> execute ( $database -> scoreql_to_sql ( " UPDATE images SET image = SCORE_BOOL_N WHERE ext IN ('swf','mp3','ani','flv','mp4','m4v','ogv','webm') " ));
2019-11-03 17:21:50 +00:00
$database -> execute ( $database -> scoreql_to_sql ( " UPDATE images SET image = SCORE_BOOL_Y WHERE ext IN ('jpg','jpeg','ico','cur','png') " ));
2019-08-16 14:40:42 +00:00
2019-11-03 19:04:57 +00:00
$this -> set_version ( MediaConfig :: VERSION , 2 );
2019-10-10 16:02:18 +00:00
$database -> beginTransaction ();
2019-08-16 14:40:42 +00:00
}
}
2019-06-18 18:45:59 +00:00
}