1#!/bin/bash -i 2# Copyright 2013 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6# The optimization code is based on pngslim (http://goo.gl/a0XHg) 7# and executes a similar pipleline to optimize the png file size. 8# The steps that require pngoptimizercl/pngrewrite/deflopt are omitted, 9# but this runs all other processes, including: 10# 1) various color-dependent optimizations using optipng. 11# 2) optimize the number of huffman blocks. 12# 3) randomize the huffman table. 13# 4) Further optimize using optipng and advdef (zlib stream). 14# Due to the step 3), each run may produce slightly different results. 15# 16# Note(oshima): In my experiment, advdef didn't reduce much. I'm keeping it 17# for now as it does not take much time to run. 18 19readonly ALL_DIRS=" 20ash/resources 21chrome/android/java/res 22chrome/app/theme 23chrome/browser/resources 24chrome/renderer/resources 25content/public/android/java/res 26content/renderer/resources 27content/shell/resources 28remoting/resources 29ui/resources 30ui/webui/resources/images 31webkit/glue/resources 32win8/metro_driver/resources 33" 34 35# Files larger than this file size (in bytes) will 36# use the optimization parameters tailored for large files. 37LARGE_FILE_THRESHOLD=3000 38 39# Constants used for optimization 40readonly DEFAULT_MIN_BLOCK_SIZE=128 41readonly DEFAULT_LIMIT_BLOCKS=256 42readonly DEFAULT_RANDOM_TRIALS=100 43# Taken from the recommendation in the pngslim's readme.txt. 44readonly LARGE_MIN_BLOCK_SIZE=1 45readonly LARGE_LIMIT_BLOCKS=2 46readonly LARGE_RANDOM_TRIALS=1 47 48# Global variables for stats 49TOTAL_OLD_BYTES=0 50TOTAL_NEW_BYTES=0 51TOTAL_FILE=0 52CORRUPTED_FILE=0 53PROCESSED_FILE=0 54 55declare -a THROBBER_STR=('-' '\\' '|' '/') 56THROBBER_COUNT=0 57 58VERBOSE=false 59 60# Echo only if verbose option is set. 61function info { 62 if $VERBOSE ; then 63 echo $@ 64 fi 65} 66 67# Show throbber character at current cursor position. 68function throbber { 69 info -ne "${THROBBER_STR[$THROBBER_COUNT]}\b" 70 let THROBBER_COUNT=$THROBBER_COUNT+1 71 let THROBBER_COUNT=$THROBBER_COUNT%4 72} 73 74# Usage: pngout_loop <file> <png_out_options> ... 75# Optimize the png file using pngout with the given options 76# using various block split thresholds and filter types. 77function pngout_loop { 78 local file=$1 79 shift 80 local opts=$* 81 if [ $OPTIMIZE_LEVEL == 1 ]; then 82 for j in $(eval echo {0..5}); do 83 throbber 84 pngout -q -k1 -s1 -f$j $opts $file 85 done 86 else 87 for i in 0 128 256 512; do 88 for j in $(eval echo {0..5}); do 89 throbber 90 pngout -q -k1 -s1 -b$i -f$j $opts $file 91 done 92 done 93 fi 94} 95 96# Usage: get_color_depth_list 97# Returns the list of color depth options for current optimization level. 98function get_color_depth_list { 99 if [ $OPTIMIZE_LEVEL == 1 ]; then 100 echo "-d0" 101 else 102 echo "-d1 -d2 -d4 -d8" 103 fi 104} 105 106# Usage: process_grayscale <file> 107# Optimize grayscale images for all color bit depths. 108# 109# TODO(oshima): Experiment with -d0 w/o -c0. 110function process_grayscale { 111 info -ne "\b\b\b\b\b\b\b\bgray...." 112 for opt in $(get_color_depth_list); do 113 pngout_loop $file -c0 $opt 114 done 115} 116 117# Usage: process_grayscale_alpha <file> 118# Optimize grayscale images with alpha for all color bit depths. 119function process_grayscale_alpha { 120 info -ne "\b\b\b\b\b\b\b\bgray-a.." 121 pngout_loop $file -c4 122 for opt in $(get_color_depth_list); do 123 pngout_loop $file -c3 $opt 124 done 125} 126 127# Usage: process_rgb <file> 128# Optimize rgb images with or without alpha for all color bit depths. 129function process_rgb { 130 info -ne "\b\b\b\b\b\b\b\brgb....." 131 for opt in $(get_color_depth_list); do 132 pngout_loop $file -c3 $opt 133 done 134 pngout_loop $file -c2 135 pngout_loop $file -c6 136} 137 138# Usage: huffman_blocks <file> 139# Optimize the huffman blocks. 140function huffman_blocks { 141 info -ne "\b\b\b\b\b\b\b\bhuffman." 142 local file=$1 143 local size=$(stat -c%s $file) 144 local min_block_size=$DEFAULT_MIN_BLOCK_SIZE 145 local limit_blocks=$DEFAULT_LIMIT_BLOCKS 146 147 if [ $size -gt $LARGE_FILE_THRESHOLD ]; then 148 min_block_size=$LARGE_MIN_BLOCK_SIZE 149 limit_blocks=$LARGE_LIMIT_BLOCKS 150 fi 151 let max_blocks=$size/$min_block_size 152 if [ $max_blocks -gt $limit_blocks ]; then 153 max_blocks=$limit_blocks 154 fi 155 156 for i in $(eval echo {2..$max_blocks}); do 157 throbber 158 pngout -q -k1 -ks -s1 -n$i $file 159 done 160} 161 162# Usage: random_huffman_table_trial <file> 163# Try compressing by randomizing the initial huffman table. 164# 165# TODO(oshima): Try adjusting different parameters for large files to 166# reduce runtime. 167function random_huffman_table_trial { 168 info -ne "\b\b\b\b\b\b\b\brandom.." 169 local file=$1 170 local old_size=$(stat -c%s $file) 171 local trials_count=$DEFAULT_RANDOM_TRIALS 172 173 if [ $old_size -gt $LARGE_FILE_THRESHOLD ]; then 174 trials_count=$LARGE_RANDOM_TRIALS 175 fi 176 for i in $(eval echo {1..$trials_count}); do 177 throbber 178 pngout -q -k1 -ks -s0 -r $file 179 done 180 local new_size=$(stat -c%s $file) 181 if [ $new_size -lt $old_size ]; then 182 random_huffman_table_trial $file 183 fi 184} 185 186# Usage: final_comprssion <file> 187# Further compress using optipng and advdef. 188# TODO(oshima): Experiment with 256. 189function final_compression { 190 info -ne "\b\b\b\b\b\b\b\bfinal..." 191 local file=$1 192 if [ $OPTIMIZE_LEVEL == 2 ]; then 193 for i in 32k 16k 8k 4k 2k 1k 512; do 194 throbber 195 optipng -q -nb -nc -zw$i -zc1-9 -zm1-9 -zs0-3 -f0-5 $file 196 done 197 fi 198 for i in $(eval echo {1..4}); do 199 throbber 200 advdef -q -z -$i $file 201 done 202 203 # Clear the current line. 204 if $VERBOSE ; then 205 printf "\033[0G\033[K" 206 fi 207} 208 209# Usage: get_color_type <file> 210# Returns the color type name of the png file. Here is the list of names 211# for each color type codes. 212# 0: grayscale 213# 2: RGB 214# 3: colormap 215# 4: gray+alpha 216# 6: RGBA 217# See http://en.wikipedia.org/wiki/Portable_Network_Graphics#Color_depth 218# for details about the color type code. 219function get_color_type { 220 local file=$1 221 echo $(file $file | awk -F, '{print $3}' | awk '{print $2}') 222} 223 224# Usage: optimize_size <file> 225# Performs png file optimization. 226function optimize_size { 227 # Print filename, trimmed to ensure it + status don't take more than 1 line 228 local filename_length=${#file} 229 local -i allowed_length=$COLUMNS-11 230 local -i trimmed_length=$filename_length-$COLUMNS+14 231 if [ "$filename_length" -lt "$allowed_length" ]; then 232 info -n "$file|........" 233 else 234 info -n "...${file:$trimmed_length}|........" 235 fi 236 237 local file=$1 238 239 advdef -q -z -4 $file 240 241 pngout -q -s4 -c0 -force $file $file.tmp.png 242 if [ -f $file.tmp.png ]; then 243 rm $file.tmp.png 244 process_grayscale $file 245 process_grayscale_alpha $file 246 else 247 pngout -q -s4 -c4 -force $file $file.tmp.png 248 if [ -f $file.tmp.png ]; then 249 rm $file.tmp.png 250 process_grayscale_alpha $file 251 else 252 process_rgb $file 253 fi 254 fi 255 256 info -ne "\b\b\b\b\b\b\b\bfilter.." 257 local old_color_type=$(get_color_type $file) 258 optipng -q -zc9 -zm8 -zs0-3 -f0-5 $file -out $file.tmp.png 259 local new_color_type=$(get_color_type $file.tmp.png) 260 # optipng may corrupt a png file when reducing the color type 261 # to grayscale/grayscale+alpha. Just skip such cases until 262 # the bug is fixed. See crbug.com/174505, crbug.com/174084. 263 # The issue is reported in 264 # https://sourceforge.net/tracker/?func=detail&aid=3603630&group_id=151404&atid=780913 265 if [[ $old_color_type == "RGBA" && $new_color_type == gray* ]] ; then 266 rm $file.tmp.png 267 else 268 mv $file.tmp.png $file 269 fi 270 pngout -q -k1 -s1 $file 271 272 huffman_blocks $file 273 274 # TODO(oshima): Experiment with strategy 1. 275 info -ne "\b\b\b\b\b\b\b\bstrategy" 276 if [ $OPTIMIZE_LEVEL == 2 ]; then 277 for i in 3 2 0; do 278 pngout -q -k1 -ks -s$i $file 279 done 280 else 281 pngout -q -k1 -ks -s1 $file 282 fi 283 284 if [ $OPTIMIZE_LEVEL == 2 ]; then 285 random_huffman_table_trial $file 286 fi 287 288 final_compression $file 289} 290 291# Usage: process_file <file> 292function process_file { 293 local file=$1 294 local name=$(basename $file) 295 # -rem alla removes all ancillary chunks except for tRNS 296 pngcrush -d $TMP_DIR -brute -reduce -rem alla $file > /dev/null 2>&1 297 298 if [ -f $TMP_DIR/$name -a $OPTIMIZE_LEVEL != 0 ]; then 299 optimize_size $TMP_DIR/$name 300 fi 301} 302 303# Usage: optimize_file <file> 304function optimize_file { 305 local file=$1 306 if $using_cygwin ; then 307 file=$(cygpath -w $file) 308 fi 309 310 local name=$(basename $file) 311 local old=$(stat -c%s $file) 312 local tmp_file=$TMP_DIR/$name 313 let TOTAL_FILE+=1 314 315 process_file $file 316 317 if [ ! -e $tmp_file ] ; then 318 let CORRUPTED_FILE+=1 319 echo "$file may be corrupted; skipping\n" 320 return 321 fi 322 323 local new=$(stat -c%s $tmp_file) 324 let diff=$old-$new 325 let percent=$diff*100 326 let percent=$percent/$old 327 328 if [ $new -lt $old ]; then 329 info "$file: $old => $new ($diff bytes: $percent%)" 330 cp "$tmp_file" "$file" 331 let TOTAL_OLD_BYTES+=$old 332 let TOTAL_NEW_BYTES+=$new 333 let PROCESSED_FILE+=1 334 else 335 if [ $OPTIMIZE_LEVEL == 0 ]; then 336 info "$file: Skipped" 337 else 338 info "$file: Unable to reduce size" 339 fi 340 rm $tmp_file 341 fi 342} 343 344function optimize_dir { 345 local dir=$1 346 if $using_cygwin ; then 347 dir=$(cygpath -w $dir) 348 fi 349 350 for f in $(find $dir -name "*.png"); do 351 optimize_file $f 352 done 353} 354 355function install_if_not_installed { 356 local program=$1 357 local package=$2 358 which $program > /dev/null 2>&1 359 if [ "$?" != "0" ]; then 360 if $using_cygwin ; then 361 echo "Couldn't find $program. " \ 362 "Please run cygwin's setup.exe and install the $package package." 363 exit 1 364 else 365 read -p "Couldn't find $program. Do you want to install? (y/n)" 366 [ "$REPLY" == "y" ] && sudo apt-get install $package 367 [ "$REPLY" == "y" ] || exit 368 fi 369 fi 370} 371 372function fail_if_not_installed { 373 local program=$1 374 local url=$2 375 which $program > /dev/null 2>&1 376 if [ $? != 0 ]; then 377 echo "Couldn't find $program. Please download and install it from $url ." 378 exit 1 379 fi 380} 381 382function show_help { 383 local program=$(basename $0) 384 echo \ 385"Usage: $program [options] <dir> ... 386 387$program is a utility to reduce the size of png files by removing 388unnecessary chunks and compressing the image. 389 390Options: 391 -o<optimize_level> Specify optimization level: (default is 1) 392 0 Just run pngcrush. It removes unnecessary chunks and perform basic 393 optimization on the encoded data. 394 1 Optimize png files using pngout/optipng and advdef. This can further 395 reduce addtional 5~30%. This is the default level. 396 2 Aggressively optimize the size of png files. This may produce 397 addtional 1%~5% reduction. Warning: this is *VERY* 398 slow and can take hours to process all files. 399 -r<revision> If this is specified, the script processes only png files 400 changed since this revision. The <dir> options will be used 401 to narrow down the files under specific directories. 402 -v Shows optimization process for each file. 403 -h Print this help text." 404 exit 1 405} 406 407if [ ! -e ../.gclient ]; then 408 echo "$0 must be run in src directory" 409 exit 1 410fi 411 412if [ "$(expr substr $(uname -s) 1 6)" == "CYGWIN" ]; then 413 using_cygwin=true 414else 415 using_cygwin=false 416fi 417 418# The -i in the shebang line should result in $COLUMNS being set on newer 419# versions of bash. If it's not set yet, attempt to set it. 420if [ -z $COLUMNS ]; then 421 which tput > /dev/null 2>&1 422 if [ "$?" == "0" ]; then 423 COLUMNS=$(tput cols) 424 else 425 # No tput either... give up and just guess 80 columns. 426 COLUMNS=80 427 fi 428 export COLUMNS 429fi 430 431OPTIMIZE_LEVEL=1 432# Parse options 433while getopts o:r:h:v opts 434do 435 case $opts in 436 r) 437 COMMIT=$(git svn find-rev r$OPTARG | tail -1) || exit 438 if [ -z "$COMMIT" ] ; then 439 echo "Revision $OPTARG not found" 440 show_help 441 fi 442 ;; 443 o) 444 if [[ "$OPTARG" != 0 && "$OPTARG" != 1 && "$OPTARG" != 2 ]] ; then 445 show_help 446 fi 447 OPTIMIZE_LEVEL=$OPTARG 448 ;; 449 v) 450 VERBOSE=true 451 ;; 452 [h?]) 453 show_help;; 454 esac 455done 456 457# Remove options from argument list. 458shift $(($OPTIND -1)) 459 460# Make sure we have all necessary commands installed. 461install_if_not_installed pngcrush pngcrush 462if [ $OPTIMIZE_LEVEL -ge 1 ]; then 463 install_if_not_installed optipng optipng 464 465 if $using_cygwin ; then 466 fail_if_not_installed advdef "http://advancemame.sourceforge.net/comp-readme.html" 467 else 468 install_if_not_installed advdef advancecomp 469 fi 470 471 if $using_cygwin ; then 472 pngout_url="http://www.advsys.net/ken/utils.htm" 473 else 474 pngout_url="http://www.jonof.id.au/kenutils" 475 fi 476 fail_if_not_installed pngout $pngout_url 477fi 478 479# Create tmp directory for crushed png file. 480TMP_DIR=$(mktemp -d) 481if $using_cygwin ; then 482 TMP_DIR=$(cygpath -w $TMP_DIR) 483fi 484 485# Make sure we cleanup temp dir 486#trap "rm -rf $TMP_DIR" EXIT 487 488# If no directories are specified, optimize all directories. 489DIRS=$@ 490set ${DIRS:=$ALL_DIRS} 491 492info "Optimize level=$OPTIMIZE_LEVEL" 493 494if [ -n "$COMMIT" ] ; then 495 ALL_FILES=$(git diff --name-only $COMMIT HEAD $DIRS | grep "png$") 496 ALL_FILES_LIST=( $ALL_FILES ) 497 echo "Processing ${#ALL_FILES_LIST[*]} files" 498 for f in $ALL_FILES; do 499 if [ -f $f ] ; then 500 optimize_file $f 501 else 502 echo "Skipping deleted file: $f"; 503 fi 504 done 505else 506 for d in $DIRS; do 507 if [ -d $d ] ; then 508 info "Optimizing png files in $d" 509 optimize_dir $d 510 info "" 511 elif [ -f $d ] ; then 512 optimize_file $d 513 else 514 echo "Not a file or directory: $d"; 515 fi 516 done 517fi 518 519# Print the results. 520echo "Optimized $PROCESSED_FILE/$TOTAL_FILE files in" \ 521 "$(date -d "0 + $SECONDS sec" +%Ts)" 522if [ $PROCESSED_FILE != 0 ]; then 523 let diff=$TOTAL_OLD_BYTES-$TOTAL_NEW_BYTES 524 let percent=$diff*100/$TOTAL_OLD_BYTES 525 echo "Result: $TOTAL_OLD_BYTES => $TOTAL_NEW_BYTES bytes" \ 526 "($diff bytes: $percent%)" 527fi 528if [ $CORRUPTED_FILE != 0 ]; then 529 echo "Warning: corrupted files found: $CORRUPTED_FILE" 530 echo "Please contact the author of the CL that landed corrupted png files" 531fi 532