2020-01-26 13:19:35 +00:00
< ? php declare ( strict_types = 1 );
2019-06-12 22:54:06 +00:00
2019-10-10 15:32:01 +00:00
require_once " config.php " ;
2019-06-12 22:54:06 +00:00
/*
* This is used by the image transcoding code when there is an error while transcoding
*/
2019-06-14 12:47:50 +00:00
class ImageTranscodeException extends SCoreException
{
}
2019-06-12 22:54:06 +00:00
class TranscodeImage extends Extension
{
2020-02-04 00:46:36 +00:00
/** @var TranscodeImageTheme */
protected $theme ;
2019-06-27 17:26:09 +00:00
const ACTION_BULK_TRANSCODE = " bulk_transcode " ;
2019-06-12 22:54:06 +00:00
const INPUT_FORMATS = [
2020-05-28 15:05:20 +00:00
" BMP " => EXTENSION_BMP ,
" GIF " => EXTENSION_GIF ,
" ICO " => EXTENSION_ICO ,
" JPG " => EXTENSION_JPG ,
" PNG " => EXTENSION_PNG ,
" PSD " => EXTENSION_PSD ,
" TIFF " => EXTENSION_TIFF ,
" WEBP " => EXTENSION_WEBP ,
2019-06-12 22:54:06 +00:00
];
const OUTPUT_FORMATS = [
" " => " " ,
2020-05-28 15:05:20 +00:00
" JPEG (lossy) " => EXTENSION_JPG ,
" PNG (lossless) " => EXTENSION_PNG ,
2019-06-24 15:05:16 +00:00
" WEBP (lossy) " => Media :: WEBP_LOSSY ,
" WEBP (lossless) " => Media :: WEBP_LOSSLESS ,
2019-06-12 22:54:06 +00:00
];
/**
2019-06-13 16:45:34 +00:00
* Needs to be after upload , but before the processing extensions
2019-06-12 22:54:06 +00:00
*/
public function get_priority () : int
{
return 45 ;
}
public function onInitExt ( InitExtEvent $event )
{
global $config ;
2019-06-18 18:45:59 +00:00
$config -> set_default_bool ( TranscodeConfig :: ENABLED , true );
$config -> set_default_bool ( TranscodeConfig :: UPLOAD , false );
2019-06-24 15:05:16 +00:00
$config -> set_default_string ( TranscodeConfig :: ENGINE , MediaEngine :: GD );
2019-06-18 18:45:59 +00:00
$config -> set_default_int ( TranscodeConfig :: QUALITY , 80 );
2019-06-12 22:54:06 +00:00
2019-06-14 12:47:50 +00:00
foreach ( array_values ( self :: INPUT_FORMATS ) as $format ) {
2019-06-18 18:45:59 +00:00
$config -> set_default_string ( TranscodeConfig :: UPLOAD_PREFIX . $format , " " );
2019-06-12 22:54:06 +00:00
}
}
public function onImageAdminBlockBuilding ( ImageAdminBlockBuildingEvent $event )
{
global $user , $config ;
2019-09-29 18:00:51 +00:00
if ( $user -> can ( Permissions :: EDIT_FILES )) {
2019-06-18 18:45:59 +00:00
$engine = $config -> get_string ( TranscodeConfig :: ENGINE );
2019-06-25 20:17:13 +00:00
if ( $this -> can_convert_format ( $engine , $event -> image -> ext , $event -> image -> lossless )) {
$options = $this -> get_supported_output_formats ( $engine , $event -> image -> ext , $event -> image -> lossless ? ? false );
2019-06-12 22:54:06 +00:00
$event -> add_part ( $this -> theme -> get_transcode_html ( $event -> image , $options ));
}
}
}
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
public function onSetupBuilding ( SetupBuildingEvent $event )
{
global $config ;
2019-06-18 18:45:59 +00:00
$engine = $config -> get_string ( TranscodeConfig :: ENGINE );
2019-06-12 22:54:06 +00:00
$sb = new SetupBlock ( " Image Transcode " );
2019-06-27 04:00:49 +00:00
$sb -> start_table ();
$sb -> add_bool_option ( TranscodeConfig :: ENABLED , " Allow transcoding images: " , true );
$sb -> add_bool_option ( TranscodeConfig :: UPLOAD , " Transcode on upload: " , true );
2019-09-29 13:30:55 +00:00
$sb -> add_choice_option ( TranscodeConfig :: ENGINE , Media :: IMAGE_MEDIA_ENGINES , " Engine " , true );
2019-06-14 12:47:50 +00:00
foreach ( self :: INPUT_FORMATS as $display => $format ) {
2019-06-24 15:05:16 +00:00
if ( in_array ( $format , MediaEngine :: INPUT_SUPPORT [ $engine ])) {
2019-06-12 22:54:06 +00:00
$outputs = $this -> get_supported_output_formats ( $engine , $format );
2019-06-27 04:00:49 +00:00
$sb -> add_choice_option ( TranscodeConfig :: UPLOAD_PREFIX . $format , $outputs , " $display " , true );
2019-06-14 12:47:50 +00:00
}
2019-06-12 22:54:06 +00:00
}
2020-06-11 21:58:19 +00:00
$sb -> add_int_option ( TranscodeConfig :: QUALITY , " Lossy Format Quality " , true );
2019-06-27 04:00:49 +00:00
$sb -> end_table ();
2019-06-12 22:54:06 +00:00
$event -> panel -> add_block ( $sb );
}
public function onDataUpload ( DataUploadEvent $event )
{
2019-10-02 10:23:57 +00:00
global $config ;
2019-06-12 22:54:06 +00:00
2019-06-18 18:45:59 +00:00
if ( $config -> get_bool ( TranscodeConfig :: UPLOAD ) == true ) {
2019-06-12 22:54:06 +00:00
$ext = strtolower ( $event -> type );
2019-06-24 15:05:16 +00:00
$ext = Media :: normalize_format ( $ext );
2019-06-12 22:54:06 +00:00
2020-05-28 15:05:20 +00:00
if ( $event -> type == EXTENSION_GIF && Media :: is_animated_gif ( $event -> tmpname )) {
2019-06-12 22:54:06 +00:00
return ;
}
2019-06-14 12:47:50 +00:00
if ( in_array ( $ext , array_values ( self :: INPUT_FORMATS ))) {
2019-06-18 18:45:59 +00:00
$target_format = $config -> get_string ( TranscodeConfig :: UPLOAD_PREFIX . $ext );
2019-06-14 12:47:50 +00:00
if ( empty ( $target_format )) {
2019-06-12 22:54:06 +00:00
return ;
}
try {
$new_image = $this -> transcode_image ( $event -> tmpname , $ext , $target_format );
2019-06-24 15:05:16 +00:00
$event -> set_type ( Media :: determine_ext ( $target_format ));
2019-06-12 22:54:06 +00:00
$event -> set_tmpname ( $new_image );
2019-06-14 12:47:50 +00:00
} catch ( Exception $e ) {
log_error ( " transcode " , " Error while performing upload transcode: " . $e -> getMessage ());
// We don't want to interfere with the upload process,
2019-06-12 22:54:06 +00:00
// so if something goes wrong the untranscoded image jsut continues
}
}
2019-06-14 12:47:50 +00:00
}
2019-06-12 22:54:06 +00:00
}
public function onPageRequest ( PageRequestEvent $event )
{
global $page , $user ;
2019-09-29 18:00:51 +00:00
if ( $event -> page_matches ( " transcode " ) && $user -> can ( Permissions :: EDIT_FILES )) {
2019-11-04 00:42:06 +00:00
if ( $event -> count_args () >= 1 ) {
2019-11-04 00:40:10 +00:00
$image_id = int_escape ( $event -> get_arg ( 0 ));
2019-11-04 00:42:06 +00:00
} elseif ( isset ( $_POST [ 'image_id' ])) {
2019-11-04 00:40:10 +00:00
$image_id = int_escape ( $_POST [ 'image_id' ]);
2019-11-04 00:42:06 +00:00
} else {
2019-06-14 12:47:50 +00:00
throw new ImageTranscodeException ( " Can not resize Image: No valid Image ID given. " );
}
$image_obj = Image :: by_id ( $image_id );
if ( is_null ( $image_obj )) {
$this -> theme -> display_error ( 404 , " Image not found " , " No image in the database has the ID # $image_id " );
} else {
if ( isset ( $_POST [ 'transcode_format' ])) {
try {
2019-06-12 22:54:06 +00:00
$this -> transcode_and_replace_image ( $image_obj , $_POST [ 'transcode_format' ]);
2019-06-19 01:58:28 +00:00
$page -> set_mode ( PageMode :: REDIRECT );
2019-06-12 22:54:06 +00:00
$page -> set_redirect ( make_link ( " post/view/ " . $image_id ));
2019-06-14 12:47:50 +00:00
} catch ( ImageTranscodeException $e ) {
$this -> theme -> display_transcode_error ( $page , " Error Transcoding " , $e -> getMessage ());
}
}
}
}
2019-06-12 22:54:06 +00:00
}
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
public function onBulkActionBlockBuilding ( BulkActionBlockBuildingEvent $event )
{
global $user , $config ;
2019-06-18 18:45:59 +00:00
$engine = $config -> get_string ( TranscodeConfig :: ENGINE );
2019-06-12 22:54:06 +00:00
2019-09-29 18:00:51 +00:00
if ( $user -> can ( Permissions :: EDIT_FILES )) {
2019-09-29 13:30:55 +00:00
$event -> add_action ( self :: ACTION_BULK_TRANSCODE , " Transcode " , null , " " , $this -> theme -> get_transcode_picker_html ( $this -> get_supported_output_formats ( $engine )));
2019-06-12 22:54:06 +00:00
}
}
public function onBulkAction ( BulkActionEvent $event )
{
2019-12-15 19:47:18 +00:00
global $user , $database , $page ;
2019-06-12 22:54:06 +00:00
2019-06-14 12:47:50 +00:00
switch ( $event -> action ) {
2019-06-18 18:45:59 +00:00
case self :: ACTION_BULK_TRANSCODE :
2019-06-12 22:54:06 +00:00
if ( ! isset ( $_POST [ 'transcode_format' ])) {
return ;
}
2019-09-29 18:00:51 +00:00
if ( $user -> can ( Permissions :: EDIT_FILES )) {
2019-06-12 22:54:06 +00:00
$format = $_POST [ 'transcode_format' ];
$total = 0 ;
2020-02-24 14:49:40 +00:00
$size_difference = 0 ;
2019-07-05 15:24:46 +00:00
foreach ( $event -> items as $image ) {
2019-06-12 22:54:06 +00:00
try {
$database -> beginTransaction ();
2019-07-05 15:24:46 +00:00
2020-02-24 14:49:40 +00:00
$before_size = $image -> filesize ;
$new_image = $this -> transcode_and_replace_image ( $image , $format );
2019-06-18 18:45:59 +00:00
// If a subsequent transcode fails, the database needs to have everything about the previous
// transcodes recorded already, otherwise the image entries will be stuck pointing to
// missing image files
2019-06-12 22:54:06 +00:00
$database -> commit ();
$total ++ ;
2020-02-24 14:49:40 +00:00
$size_difference += ( $before_size - $new_image -> filesize );
2019-06-14 12:47:50 +00:00
} catch ( Exception $e ) {
2019-10-02 10:23:57 +00:00
log_error ( " transcode " , " Error while bulk transcode on item { $image -> id } to $format : " . $e -> getMessage ());
2019-06-12 22:54:06 +00:00
try {
$database -> rollback ();
2019-06-14 12:47:50 +00:00
} catch ( Exception $e ) {
2019-10-02 09:10:47 +00:00
// is this safe? o.o
2019-06-14 12:47:50 +00:00
}
2019-06-12 22:54:06 +00:00
}
}
2020-02-24 14:49:40 +00:00
if ( $size_difference > 0 ) {
2020-02-24 15:30:23 +00:00
$page -> flash ( " Transcoded $total items, reduced size by " . human_filesize ( $size_difference ));
2020-02-24 14:49:40 +00:00
} elseif ( $size_difference < 0 ) {
2020-02-24 15:30:23 +00:00
$page -> flash ( " Transcoded $total items, increased size by " . human_filesize ( - 1 * $size_difference ));
2020-02-24 14:49:40 +00:00
} else {
$page -> flash ( " Transcoded $total items, no size difference " );
}
2019-06-12 22:54:06 +00:00
}
break ;
}
}
2019-06-25 20:17:13 +00:00
private function can_convert_format ( $engine , $format , ? bool $lossless = null ) : bool
2019-06-12 22:54:06 +00:00
{
2019-06-25 20:17:13 +00:00
return Media :: is_input_supported ( $engine , $format , $lossless );
2019-06-12 22:54:06 +00:00
}
2019-06-18 18:45:59 +00:00
2019-06-25 20:17:13 +00:00
private function get_supported_output_formats ( $engine , ? String $omit_format = null , ? bool $lossless = null ) : array
2019-06-12 22:54:06 +00:00
{
2019-09-29 13:30:55 +00:00
if ( $omit_format != null ) {
2019-06-25 20:17:13 +00:00
$omit_format = Media :: normalize_format ( $omit_format , $lossless );
}
2019-06-12 22:54:06 +00:00
$output = [];
2019-06-25 20:17:13 +00:00
2019-06-14 12:47:50 +00:00
foreach ( self :: OUTPUT_FORMATS as $key => $value ) {
if ( $value == " " ) {
2019-06-12 22:54:06 +00:00
$output [ $key ] = $value ;
continue ;
}
2019-09-29 13:30:55 +00:00
if ( Media :: is_output_supported ( $engine , $value )
2019-06-25 20:17:13 +00:00
&& ( empty ( $omit_format ) || $omit_format != $value )) {
2019-06-12 22:54:06 +00:00
$output [ $key ] = $value ;
2019-06-14 12:47:50 +00:00
}
2019-06-12 22:54:06 +00:00
}
return $output ;
}
2019-08-07 19:53:59 +00:00
2019-06-18 18:45:59 +00:00
2019-06-12 22:54:06 +00:00
2020-02-24 14:49:40 +00:00
private function transcode_and_replace_image ( Image $image_obj , String $target_format ) : Image
2019-06-12 22:54:06 +00:00
{
2019-06-15 16:18:52 +00:00
$original_file = warehouse_path ( Image :: IMAGE_DIR , $image_obj -> hash );
2019-06-12 22:54:06 +00:00
$tmp_filename = $this -> transcode_image ( $original_file , $image_obj -> ext , $target_format );
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
$new_image = new Image ();
$new_image -> hash = md5_file ( $tmp_filename );
$new_image -> filesize = filesize ( $tmp_filename );
$new_image -> filename = $image_obj -> filename ;
$new_image -> width = $image_obj -> width ;
$new_image -> height = $image_obj -> height ;
2019-06-24 15:05:16 +00:00
$new_image -> ext = Media :: determine_ext ( $target_format );
2019-06-12 22:54:06 +00:00
/* Move the new image into the main storage location */
2019-06-15 16:18:52 +00:00
$target = warehouse_path ( Image :: IMAGE_DIR , $new_image -> hash );
2019-06-12 22:54:06 +00:00
if ( !@ copy ( $tmp_filename , $target )) {
throw new ImageTranscodeException ( " Failed to copy new image file from temporary location ( { $tmp_filename } ) to archive ( $target ) " );
}
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
/* Remove temporary file */
@ unlink ( $tmp_filename );
send_event ( new ImageReplaceEvent ( $image_obj -> id , $new_image ));
2020-02-24 14:49:40 +00:00
return $new_image ;
2019-06-14 12:47:50 +00:00
}
2019-06-12 22:54:06 +00:00
private function transcode_image ( String $source_name , String $source_format , string $target_format ) : string
{
global $config ;
2019-06-25 20:17:13 +00:00
if ( $source_format == $target_format ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Source and target formats are the same: " . $source_format );
}
$engine = $config -> get_string ( " transcode_engine " );
2019-06-14 12:47:50 +00:00
if ( ! $this -> can_convert_format ( $engine , $source_format )) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Engine $engine does not support input format $source_format " );
}
2019-06-24 15:05:16 +00:00
if ( ! in_array ( $target_format , MediaEngine :: OUTPUT_SUPPORT [ $engine ])) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Engine $engine does not support output format $target_format " );
}
2019-06-14 12:47:50 +00:00
switch ( $engine ) {
2019-06-12 22:54:06 +00:00
case " gd " :
return $this -> transcode_image_gd ( $source_name , $source_format , $target_format );
case " convert " :
return $this -> transcode_image_convert ( $source_name , $source_format , $target_format );
2019-10-02 10:23:57 +00:00
default :
throw new ImageTranscodeException ( " No engine specified " );
2019-06-12 22:54:06 +00:00
}
}
private function transcode_image_gd ( String $source_name , String $source_format , string $target_format ) : string
{
global $config ;
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
$q = $config -> get_int ( " transcode_quality " );
$tmp_name = tempnam ( " /tmp " , " shimmie_transcode " );
$image = imagecreatefromstring ( file_get_contents ( $source_name ));
try {
$result = false ;
2019-06-14 12:47:50 +00:00
switch ( $target_format ) {
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-12 22:54:06 +00:00
$result = imagewebp ( $image , $tmp_name , $q );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_PNG :
2019-06-12 22:54:06 +00:00
$result = imagepng ( $image , $tmp_name , 9 );
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_JPG :
2019-06-12 22:54:06 +00:00
// In case of alpha channels
$width = imagesx ( $image );
$height = imagesy ( $image );
$new_image = imagecreatetruecolor ( $width , $height );
2019-06-14 12:47:50 +00:00
if ( $new_image === false ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Could not create image with dimensions $width x $height " );
}
2019-06-14 12:47:50 +00:00
try {
$black = imagecolorallocate ( $new_image , 0 , 0 , 0 );
if ( $black === false ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Could not allocate background color " );
}
2019-06-14 12:47:50 +00:00
if ( imagefilledrectangle ( $new_image , 0 , 0 , $width , $height , $black ) === false ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Could not fill background color " );
}
2019-06-14 12:47:50 +00:00
if ( imagecopy ( $new_image , $image , 0 , 0 , 0 , 0 , $width , $height ) === false ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " Could not copy source image to new image " );
}
$result = imagejpeg ( $new_image , $tmp_name , $q );
} finally {
imagedestroy ( $new_image );
}
break ;
}
} finally {
imagedestroy ( $image );
}
2019-11-04 01:04:08 +00:00
if ( $result === false ) {
throw new ImageTranscodeException ( " Error while transcoding " . $source_name . " to " . $target_format );
}
return $tmp_name ;
2019-06-12 22:54:06 +00:00
}
private function transcode_image_convert ( String $source_name , String $source_format , string $target_format ) : string
{
global $config ;
2019-08-07 19:53:59 +00:00
2019-06-12 22:54:06 +00:00
$q = $config -> get_int ( " transcode_quality " );
2019-06-24 15:05:16 +00:00
$convert = $config -> get_string ( MediaConfig :: CONVERT_PATH );
2019-06-12 22:54:06 +00:00
2019-06-14 12:47:50 +00:00
if ( $convert == null || $convert == " " ) {
2019-06-12 22:54:06 +00:00
throw new ImageTranscodeException ( " ImageMagick path not configured " );
}
2019-06-24 15:05:16 +00:00
$ext = Media :: determine_ext ( $target_format );
2019-06-12 22:54:06 +00:00
2019-06-13 16:45:34 +00:00
$args = " -flatten " ;
2019-06-12 22:54:06 +00:00
$bg = " none " ;
2019-06-14 12:47:50 +00:00
switch ( $target_format ) {
2019-06-24 15:05:16 +00:00
case Media :: WEBP_LOSSLESS :
2019-06-13 16:45:34 +00:00
$args .= '-define webp:lossless=true' ;
2019-06-12 22:54:06 +00:00
break ;
2019-06-24 15:05:16 +00:00
case Media :: WEBP_LOSSY :
2019-06-13 16:45:34 +00:00
$args .= '' ;
2019-06-12 22:54:06 +00:00
break ;
2020-05-28 15:05:20 +00:00
case EXTENSION_PNG :
2019-06-13 16:45:34 +00:00
$args .= '-define png:compression-level=9' ;
2019-06-12 22:54:06 +00:00
break ;
default :
2019-06-13 16:45:34 +00:00
$bg = " black " ;
2019-06-12 22:54:06 +00:00
break ;
}
$tmp_name = tempnam ( " /tmp " , " shimmie_transcode " );
2019-06-14 17:59:12 +00:00
$source_type = " " ;
switch ( $source_format ) {
2020-05-28 15:05:20 +00:00
case EXTENSION_ICO :
2019-06-14 17:59:12 +00:00
$source_type = " ico: " ;
}
$format = '"%s" %s -quality %u -background %s %s"%s" %s:"%s" 2>&1' ;
$cmd = sprintf ( $format , $convert , $args , $q , $bg , $source_type , $source_name , $ext , $tmp_name );
2019-06-12 22:54:06 +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 );
log_debug ( 'transcode' , " Transcoding with command ` $cmd `, returns $ret " );
2019-06-14 12:47:50 +00:00
if ( $ret !== 0 ) {
2019-06-14 17:59:12 +00:00
throw new ImageTranscodeException ( " Transcoding failed with command " . $cmd . " , returning " . implode ( " \r \n " , $output ));
2019-06-12 22:54:06 +00:00
}
return $tmp_name ;
}
}