Last active
April 1, 2017 09:55
-
-
Save askaidau/55193b288fa7e757b40a7b4a80e15d3a to your computer and use it in GitHub Desktop.
Image Compactr (PHP), requires pngquant
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/php | |
<?php | |
if( "cli"!==php_sapi_name() ) exit; | |
if( !isset($argv,$argv[1]) ){ | |
cli_error("usage: ./".basename(__FILE__)." <path>"); | |
cli_error("example: ./".basename(__FILE__)." /data/sites/example.com/fat-images/"); | |
exit; | |
} | |
$path_base = rtrim($argv[1],"/"); | |
# settings | |
define('VERSION', '1.0.2'); | |
define('GIST_URL', 'https://gist.github.com/askaidau/55193b288fa7e757b40a7b4a80e15d3a'); | |
define('VERBOSE', false); | |
define('RECURSIVE', true);//do sub-folders | |
define('DONE_FILE', 'img-compressed.done'); | |
define('DEBUG_ONLY', false);//true = dryrun | |
define('BACKUP', false);//keep copy of original as {filename}.bak | |
define('SLIM_JPG', true); | |
define('JPG_QUALITY', 80); | |
define('JPG_MIN_SIZE',80*1024);// only compress files > 80 KB | |
define('JPG_MAX_W', 1920);//resize if image exceeds this width | |
define('SLIM_PNG', true); | |
define('PNG_QUALITY', 80); | |
define('PNG_MIN_SIZE',50*1024);// only compress files > 50 KB | |
# cli colors | |
define( 'CLI_COLOR_RED', "\033[31m" ); | |
define( 'CLI_COLOR_YELLOW', "\033[1;33m" ); | |
define( 'CLI_COLOR_GREEN', "\033[32m" ); | |
define( 'CLI_COLOR_CYAN', "\033[36m" ); | |
define( 'CLI_COLOR_RESET', "\033[0m" ); | |
# this is really happening | |
cli_error( cli_color_str('blue','[info]')." Version ".VERSION ); | |
cli_error( cli_color_str('blue','[info]')." About to compress images inside '$path_base/' ..."); | |
sleep(3); | |
compress_dir_images( $path_base ); | |
function compress_dir_images( $path_base ) { | |
# checks | |
if( ! file_exists($path_base) || ! is_writable($path_base) ) { | |
cli_error("[error] Path does not exist or is not writable!"); exit; | |
} | |
# init done file | |
$path_done_file = "$path_base/".DONE_FILE; | |
if( ! file_exists($path_done_file) ) { | |
touch($path_done_file); | |
$done_files = []; | |
} | |
else { | |
$done_files = array_map('trim',file($path_done_file)); | |
} | |
$done_fp = fopen( $path_done_file, 'a' ); | |
if( $dir_handle = opendir($path_base) ) { | |
$total_old_bytes = 0; | |
$total_new_bytes = 0; | |
$file_bytes_saved = []; | |
$sub_dirs = []; | |
while( false !== ($filename = readdir($dir_handle) ) ) { | |
// the big loop | |
$path_file = "$path_base/$filename"; | |
if( preg_match('!\.(jpg|jpeg|png)!i',$filename,$matches)==1 ) { | |
if( strpos($filename,'-bak.') !== false ) { | |
// ignore -bak.{ext} files | |
continue; | |
} | |
# already processed? | |
if( in_array($filename, $done_files) ) { | |
continue; | |
} | |
$filesize = filesize($path_file); | |
$ext = $matches[1]; | |
switch( $ext ) { | |
case 'jpg': | |
case 'jpeg': | |
case 'JPG': | |
case 'JPEG': | |
if( true !== SLIM_JPG ) continue 2; | |
if( $filesize < JPG_MIN_SIZE ) { | |
if( true === VERBOSE ) cli_error("[info] $filename skipped (".get_kb($filesize)."k)"); | |
continue 2; | |
} | |
$slimmed = @app_compress_jpg($path_file, JPG_QUALITY); | |
break;//jpg | |
case 'png': | |
case 'PNG': | |
if( true !== SLIM_PNG ) continue 2; | |
if( $filesize < PNG_MIN_SIZE ) { | |
if( true === VERBOSE ) cli_error("[info] $filename skipped (".get_kb($filesize)."k)"); | |
continue 2; | |
} | |
$slimmed = @app_compress_png($path_file, PNG_QUALITY); | |
break; | |
default: | |
continue 2; | |
} | |
$slimsize = strlen($slimmed); | |
if( $slimsize==0 ) { | |
cli_error( cli_color_str('red','[error]')." failed slimming file: $filename" ); | |
continue; | |
} | |
$reduct_ratio = round( ( $filesize-$slimsize ) / $filesize * 100 , 0 ); | |
if( $reduct_ratio < 15 ) { | |
cli_error( cli_color_str('red','[error]')." ineffective reduction in size for $filename ($reduct_ratio%), skipping..." ); | |
continue; | |
} | |
cli_error("[info] $filename ".get_kb($filesize)."k => ".get_kb($slimsize)."k ($reduct_ratio%)"); | |
# record savings | |
$total_old_bytes += $filesize; | |
$total_new_bytes += $slimsize; | |
$file_bytes_saved[] = ($filesize - $slimsize); | |
# backup | |
if( true === BACKUP ) { | |
$path_backfile = str_replace( ".$ext", "-bak.$ext", $path_file); | |
copy($path_file, $path_backfile); | |
} | |
if( true === DEBUG_ONLY ) { | |
# TEST ONLY -- test slim output | |
$path_testfile = str_replace( ".$ext", "-slim.$ext", $path_file); | |
file_put_contents($path_testfile, $slimmed); | |
cli_error( cli_color_str('blue','[slimmed]')." $path_testfile"); | |
} | |
else { | |
# this is the real deal | |
file_put_contents($path_file, $slimmed); | |
fwrite($done_fp, $filename.PHP_EOL); | |
} | |
unset($slimmed);//reclaim memory hopefully? | |
if( count($file_bytes_saved)%50==0 ) { | |
cli_error( cli_color_str('blue','[info]').' '.count($file_bytes_saved).' files compressed' ); | |
sleep(3); | |
} | |
} | |
else {//not image file | |
if( ! in_array($filename,['.','..']) && true === RECURSIVE && is_dir($path_file) ) { | |
$sub_dirs[] = $path_file; | |
} | |
} | |
}//while | |
closedir($dir_handle); | |
fclose($done_fp); | |
# produce summary | |
cli_error(cli_color_str('green','[summary]')." Before: ".get_mb($total_old_bytes)."m"); | |
cli_error(cli_color_str('green','[summary]')." After: ".get_mb($total_new_bytes)."m"); | |
cli_error(cli_color_str('green','[summary]')." Reduced: ".get_mb(array_sum($file_bytes_saved))."m; over ".count($file_bytes_saved)." files"); | |
sleep(5); | |
// process sub directories if any | |
if( count($sub_dirs)>0 ) { | |
foreach( $sub_dirs as $sub_dir ) { | |
cli_error("[info] processing sub-dir '$sub_dir'"); | |
sleep(3); | |
compress_dir_images($sub_dir); | |
} | |
}//if count($sub_dirs)>0 | |
}//if $dir_handle | |
}//function compress_dir_images | |
# utilities | |
function cli_error($msg) | |
{ | |
fwrite(STDERR,"[".date("Y-m-d H:i:s")."] $msg".PHP_EOL); | |
} | |
function cli_color_str($color="",$str="") { | |
switch($color){ | |
case 'red': return CLI_COLOR_RED.$str.CLI_COLOR_RESET; | |
case 'yellow': return CLI_COLOR_YELLOW.$str.CLI_COLOR_RESET; | |
case 'green': return CLI_COLOR_GREEN.$str.CLI_COLOR_RESET; | |
case 'blue': case 'cyan': return CLI_COLOR_CYAN.$str.CLI_COLOR_RESET; | |
} | |
return $str; | |
} | |
function app_compress_jpg( $path_to_jpg_file, $quality=80 ) | |
{ | |
$img_size = getimagesize($path_to_jpg_file); | |
$w = $img_size[0]; | |
$h = $img_size[1]; | |
$img_src = imagecreatefromjpeg($path_to_jpg_file); | |
if( $w > JPG_MAX_W ) { | |
$new_w = JPG_MAX_W; | |
$new_h = round( ($new_w/$w)*$h ); | |
$img_new = imagecreatetruecolor( $new_w, $new_h ); | |
imagecopyresampled( $img_new, $img_src, 0, 0, 0, 0, $new_w, $new_h, $w, $h ); | |
unset($img_src); | |
$img_src = $img_new; | |
} | |
ob_start(); | |
imagejpeg( $img_src, null, $quality ); | |
return ob_get_clean(); | |
}//app_compress_jpg | |
function app_compress_png( $path_to_png_file, $max_quality=90 ) | |
{ | |
if (!file_exists($path_to_png_file)) { | |
throw new Exception("File does not exist: $path_to_png_file"); | |
} | |
// guarantee that quality won't be worse than that. | |
$min_quality = 70; | |
$compressed_png_content = shell_exec("pngquant --quality=$min_quality-$max_quality - < ".escapeshellarg($path_to_png_file)); | |
if (!$compressed_png_content) { | |
throw new Exception("Conversion to compressed PNG failed. Is pngquant 1.8+ installed on the server?"); | |
} | |
return $compressed_png_content; | |
}//app_compress_png | |
function get_kb( $bytes=0, $decimals=1 ) { | |
return round($bytes/1024, $decimals); | |
} | |
function get_mb( $bytes=0, $decimals=1 ) { | |
return round($bytes/(1024*1024), $decimals); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment