2019-06-18 18:45:59 +00:00
< ? php
/*
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
abstract class MediaConfig
2019-06-18 18:45:59 +00:00
{
2019-06-24 15:05:16 +00:00
const FFMPEG_PATH = " media_ffmpeg_path " ;
const FFPROBE_PATH = " media_ffprobe_path " ;
const CONVERT_PATH = " media_convert_path " ;
const VERSION = " ext_media_version " ;
const MEM_LIMIT = 'media_mem_limit' ;
2019-06-18 18:45:59 +00:00
}
2019-06-24 15:05:16 +00:00
abstract class MediaEngine
{
2019-06-21 20:23:59 +00:00
public const GD = " gd " ;
public const IMAGICK = " convert " ;
public const FFMPEG = " ffmpeg " ;
public const ALL = [
2019-06-24 15:05:16 +00:00
MediaEngine :: GD ,
MediaEngine :: FFMPEG ,
MediaEngine :: IMAGICK
2019-06-21 20:23:59 +00:00
];
public const OUTPUT_SUPPORT = [
2019-06-24 15:05:16 +00:00
MediaEngine :: GD => [
2019-06-21 20:23:59 +00:00
" gif " ,
" jpg " ,
" png " ,
" webp " ,
2019-06-24 15:05:16 +00:00
Media :: WEBP_LOSSY ,
2019-06-21 20:23:59 +00:00
],
2019-06-24 15:05:16 +00:00
MediaEngine :: IMAGICK => [
2019-06-21 20:23:59 +00:00
" gif " ,
" jpg " ,
" png " ,
" webp " ,
2019-06-24 15:05:16 +00:00
Media :: WEBP_LOSSY ,
Media :: WEBP_LOSSLESS ,
2019-06-21 20:23:59 +00:00
],
2019-06-24 15:05:16 +00:00
MediaEngine :: FFMPEG => [
2019-06-25 20:17:13 +00:00
" jpg " ,
" webp " ,
" png "
2019-06-21 20:23:59 +00:00
]
];
public const INPUT_SUPPORT = [
2019-06-24 15:05:16 +00:00
MediaEngine :: GD => [
2019-06-21 20:23:59 +00:00
" bmp " ,
" gif " ,
" jpg " ,
" png " ,
" webp " ,
2019-06-25 20:17:13 +00:00
Media :: WEBP_LOSSY ,
Media :: WEBP_LOSSLESS
2019-06-21 20:23:59 +00:00
],
2019-06-24 15:05:16 +00:00
MediaEngine :: IMAGICK => [
2019-06-21 20:23:59 +00:00
" bmp " ,
" gif " ,
" jpg " ,
" png " ,
" psd " ,
" tiff " ,
" webp " ,
2019-06-25 20:17:13 +00:00
Media :: WEBP_LOSSY ,
Media :: WEBP_LOSSLESS ,
2019-06-21 20:23:59 +00:00
" ico " ,
],
2019-06-24 15:05:16 +00:00
MediaEngine :: FFMPEG => [
2019-06-21 20:23:59 +00:00
" avi " ,
" mkv " ,
" webm " ,
" mp4 " ,
" mov " ,
" flv "
]
];
}
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 MediaResizeEvent extends Event
2019-06-18 18:45:59 +00:00
{
public $engine ;
public $input_path ;
public $input_type ;
public $output_path ;
public $target_format ;
public $target_width ;
public $target_height ;
public $target_quality ;
public $minimize ;
public $ignore_aspect_ratio ;
public $allow_upscale ;
2019-09-29 13:30:55 +00:00
public function __construct (
String $engine ,
string $input_path ,
string $input_type ,
string $output_path ,
int $target_width ,
int $target_height ,
bool $ignore_aspect_ratio = false ,
string $target_format = null ,
int $target_quality = 80 ,
bool $minimize = false ,
bool $allow_upscale = true
) {
2019-06-24 15:05:16 +00:00
assert ( in_array ( $engine , MediaEngine :: ALL ));
2019-06-18 18:45:59 +00:00
$this -> engine = $engine ;
$this -> input_path = $input_path ;
$this -> input_type = $input_type ;
$this -> output_path = $output_path ;
$this -> target_height = $target_height ;
$this -> target_width = $target_width ;
$this -> target_format = $target_format ;
$this -> target_quality = $target_quality ;
$this -> minimize = $minimize ;
$this -> ignore_aspect_ratio = $ignore_aspect_ratio ;
$this -> allow_upscale = $allow_upscale ;
}
}
2019-06-25 20:17:13 +00:00
class MediaCheckPropertiesEvent extends Event
2019-06-24 15:05:16 +00:00
{
public $file_name ;
public $ext ;
2019-06-25 20:17:13 +00:00
public $lossless = null ;
public $audio = null ;
public $video = null ;
public $length = null ;
public $height = null ;
public $width = null ;
2019-06-24 15:05:16 +00:00
public function __construct ( string $file_name , string $ext )
{
$this -> file_name = $file_name ;
$this -> ext = $ext ;
}
}
2019-06-25 20:17:13 +00:00
2019-06-24 15:05:16 +00:00
class Media extends Extension
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 ,
" png " ,
2019-06-25 20:17:13 +00:00
" psd " ,
" bmp " ,
" ico " ,
" cur " ,
" ani " ,
" gif "
2019-06-18 18:45:59 +00:00
];
const ALPHA_FORMATS = [
self :: WEBP_LOSSLESS ,
self :: WEBP_LOSSY ,
2019-06-25 20:17:13 +00:00
" webp " ,
2019-06-18 18:45:59 +00:00
" png " ,
];
const FORMAT_ALIASES = [
" tif " => " tiff " ,
" jpeg " => " jpg " ,
];
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-24 15:05:16 +00:00
if ( $config -> get_int ( MediaConfig :: VERSION ) < 1 ) {
2019-06-18 18:45:59 +00:00
$current_value = $config -> get_string ( " thumb_ffmpeg_path " );
2019-06-18 18:55:18 +00:00
if ( ! empty ( $current_value )) {
2019-06-24 15:05:16 +00:00
$config -> set_string ( MediaConfig :: FFMPEG_PATH , $current_value );
2019-06-18 18:45:59 +00:00
} 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 ))) {
2019-06-24 15:05:16 +00:00
$config -> set_default_string ( MediaConfig :: FFMPEG_PATH , 'ffmpeg' );
2019-06-18 18:45:59 +00:00
}
}
2019-06-25 20:17:13 +00:00
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' );
}
}
2019-06-18 18:45:59 +00:00
$current_value = $config -> get_string ( " thumb_convert_path " );
2019-06-18 18:55:18 +00:00
if ( ! empty ( $current_value )) {
2019-06-24 15:05:16 +00:00
$config -> set_string ( MediaConfig :: CONVERT_PATH , $current_value );
2019-06-18 18:45:59 +00:00
} 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 ))) {
2019-06-24 15:05:16 +00:00
$config -> set_default_string ( MediaConfig :: CONVERT_PATH , 'convert' );
2019-06-18 18:45:59 +00:00
}
}
$current_value = $config -> get_int ( " thumb_mem_limit " );
2019-06-18 18:55:18 +00:00
if ( ! empty ( $current_value )) {
2019-06-24 15:05:16 +00:00
$config -> set_int ( MediaConfig :: MEM_LIMIT , $current_value );
2019-06-18 18:45:59 +00:00
}
2019-06-24 15:05:16 +00:00
$config -> set_int ( MediaConfig :: VERSION , 1 );
log_info ( " media " , " extension installed " );
2019-06-18 18:45:59 +00:00
}
}
2019-06-25 20:17:13 +00:00
public function onPageRequest ( PageRequestEvent $event )
{
global $database , $page , $user ;
if ( $event -> page_matches ( " media_rescan/ " ) && $user -> is_admin () && isset ( $_POST [ 'image_id' ])) {
$image = Image :: by_id ( int_escape ( $_POST [ 'image_id' ]));
$this -> update_image_media_properties ( $image -> hash , $image -> ext );
$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-06-27 04:00:49 +00:00
$sb -> add_text_option ( MediaConfig :: FFMPEG_PATH , " <br/>ffmpeg " , true );
$sb -> add_text_option ( MediaConfig :: FFPROBE_PATH , " <br/>ffprobe " , true );
2019-06-18 18:45:59 +00:00
2019-06-27 04:00:49 +00:00
$sb -> add_shorthand_int_option ( MediaConfig :: MEM_LIMIT , " <br />Mem limit: " , true );
$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 ;
if ( $user -> is_admin ()) {
$event -> add_action ( " bulk_media_rescan " , " Scan Media Properties " );
}
}
public function onBulkAction ( BulkActionEvent $event )
{
global $user ;
switch ( $event -> action ) {
case " bulk_media_rescan " :
if ( $user -> is_admin ()) {
$total = 0 ;
2019-07-05 15:36:07 +00:00
foreach ( $event -> items as $image ) {
2019-06-25 20:17:13 +00:00
try {
$this -> update_image_media_properties ( $image -> hash , $image -> ext );
$total ++ ;
} catch ( MediaException $e ) {
}
}
flash_message ( " Scanned media properties for $total items " );
}
break ;
}
}
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 ;
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-07-05 15:36:07 +00:00
const CONTENT_SEARCH_TERM_REGEX = " /^content[=|:]((video)|(audio)) $ /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 ;
$matches = [];
if ( preg_match ( self :: CONTENT_SEARCH_TERM_REGEX , $event -> term , $matches )) {
$field = $matches [ 2 ];
2019-07-05 15:36:07 +00:00
$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 );
}
}
2019-06-25 20:17:13 +00:00
public function onTagTermParse ( TagTermParseEvent $event )
{
$matches = [];
2019-06-27 04:14:12 +00:00
if ( preg_match ( self :: CONTENT_SEARCH_TERM_REGEX , strtolower ( $event -> term ), $matches ) && $event -> parse ) {
2019-06-25 20:17:13 +00:00
// Nothing to save, just helping filter out reserved tags
}
if ( ! empty ( $matches )) {
$event -> metatag = true ;
}
}
private function media_rescan () : bool
{
$ext = " " ;
if ( array_key_exists ( " media_rescan_type " , $_POST )) {
$ext = $_POST [ " media_rescan_type " ];
}
$results = $this -> get_images ( $ext );
foreach ( $results as $result ) {
$this -> update_image_media_properties ( $result [ " hash " ], $result [ " ext " ]);
}
return true ;
}
public static function update_image_media_properties ( string $hash , string $ext )
{
global $database ;
$path = warehouse_path ( Image :: IMAGE_DIR , $hash );
$mcpe = new MediaCheckPropertiesEvent ( $path , $ext );
send_event ( $mcpe );
$database -> execute (
" UPDATE images SET
lossless = : lossless , video = : video , audio = : audio ,
height = : height , width = : width ,
length = : length WHERE hash = : hash " ,
[
" hash " => $hash ,
" width " => $mcpe -> width ? ? 0 ,
" height " => $mcpe -> height ? ? 0 ,
" lossless " => $database -> scoresql_value_prepare ( $mcpe -> lossless ),
" video " => $database -> scoresql_value_prepare ( $mcpe -> video ),
" audio " => $database -> scoresql_value_prepare ( $mcpe -> audio ),
" length " => $mcpe -> length
2019-09-29 13:30:55 +00:00
]
);
2019-06-25 20:17:13 +00:00
}
public function get_images ( String $ext = null )
{
global $database ;
$query = " SELECT id, hash, ext FROM images " ;
$args = [];
if ( ! empty ( $ext )) {
$query .= " WHERE ext = :ext " ;
$args [ " ext " ] = $ext ;
2019-06-24 15:05:16 +00:00
}
2019-06-25 20:17:13 +00:00
return $database -> get_all ( $query , $args );
2019-06-24 15:05:16 +00:00
}
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 .
* http :// stackoverflow . com / questions / 527532 / reasonable - php - memory - limit - for - image - resize
*
* @ 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 );
if ( $config -> get_string ( ImageConfig :: THUMB_TYPE ) == " webp " ) {
$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 ) {
2019-06-24 15:05:16 +00:00
log_debug ( 'Media' , " Generating thumbnail with command ` $cmd `, returns $ret " );
2019-06-18 18:45:59 +00:00
return true ;
} else {
2019-06-24 15:05:16 +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 ) {
log_debug ( 'Media' , " Getting media data ` $cmd `, returns $ret " );
$output = implode ( $output );
$data = json_decode ( $output , true );
return $data ;
} else {
log_error ( 'Media' , " Getting media data ` $cmd `, returns $ret " );
return [];
}
}
2019-06-18 18:45:59 +00:00
public static function determine_ext ( String $format ) : String
{
$format = self :: normalize_format ( $format );
switch ( $format ) {
case self :: WEBP_LOSSLESS :
case self :: WEBP_LOSSY :
return " webp " ;
default :
return $format ;
}
}
// private static function image_save_imagick(Imagick $image, string $path, string $format, int $output_quality = 80, bool $minimize)
// {
// switch ($format) {
// case "png":
// $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 ) {
2019-06-25 23:43:57 +00:00
case " webp " :
return self :: is_lossless_webp ( $filename );
break ;
}
return false ;
}
2019-06-18 18:45:59 +00:00
public static function image_resize_convert (
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
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 ;
}
2019-09-29 13:30:55 +00:00
if ( $output_type == " 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 ;
case " png " :
$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 {
2019-06-24 15:05:16 +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 (
String $image_filename ,
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 :
$output_type = " gif " ;
break ;
case IMAGETYPE_JPEG :
$output_type = " jpeg " ;
break ;
case IMAGETYPE_PNG :
$output_type = " png " ;
break ;
case IMAGETYPE_WEBP :
$output_type = " webp " ;
break ;
case IMAGETYPE_BMP :
$output_type = " bmp " ;
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 :
//
// More info here: http://stackoverflow.com/questions/279236/how-do-i-resize-pngs-with-transparency-in-php
//
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-06-18 18:45:59 +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 ) {
case " bmp " :
$result = imagebmp ( $image_resized , $output_filename , true );
break ;
case " 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 ;
case " jpg " :
case " jpeg " :
$result = imagejpeg ( $image_resized , $output_filename , $output_quality );
break ;
case " png " :
$result = imagepng ( $image_resized , $output_filename , 9 );
break ;
case " gif " :
$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 .
*/
2019-06-24 15:05:16 +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 {
//check if gif is animated (via http://www.php.net/manual/en/function.imagecreatefromgif.php#104473)
while ( ! feof ( $fh ) && $is_anim_gif < 2 ) {
$chunk = fread ( $fh , 1024 * 100 );
$is_anim_gif += preg_match_all ( '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s' , $chunk , $matches );
}
} finally {
@ fclose ( $fh );
2019-06-18 18:45:59 +00:00
}
}
return ( $is_anim_gif == 0 );
}
2019-06-24 15:05:16 +00:00
private static function compare_file_bytes ( String $file_name , array $comparison ) : bool
{
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 " );
}
}
public static function is_animated_webp ( String $image_filename ) : bool
{
return self :: compare_file_bytes ( $image_filename , self :: WEBP_ANIMATION_HEADER );
}
public static function is_lossless_webp ( String $image_filename ) : bool
{
return self :: compare_file_bytes ( $image_filename , self :: WEBP_LOSSLESS_HEADER );
}
2019-06-18 18:45:59 +00:00
public static function supports_alpha ( string $format )
{
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
{
2019-06-25 20:17:13 +00:00
if ( $format == " webp " ) {
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 )) {
$size = [ $regs [ 2 ], $regs [ 1 ]];
} else {
$size = [ $regs [ 1 ], $regs [ 2 ]];
}
} else {
$size = [ 1 , 1 ];
}
2019-06-24 15:05:16 +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 ;
}
}