optimize-png-files.sh 14 KB

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