• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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