Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2015 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 """Windows ICO file crusher. | |
| 7 | |
| 8 Optimizes the PNG images within a Windows ICO icon file. This extracts all of | |
| 9 the sub-images within the file, runs any PNG-formatted images through | |
| 10 optimize-png-files.sh, then packs them back into an ICO file. | |
| 11 | |
| 12 NOTE: ICO files can contain both raw uncompressed BMP files and PNG files. This | |
| 13 script does not touch the BMP files, which means if you have a huge uncompressed | |
| 14 image, it will not get smaller. 256x256 icons should be PNG-formatted first. | |
| 15 (Smaller icons should be BMPs for compatibility with Windows XP.) | |
| 16 """ | |
| 17 | |
| 18 import argparse | |
| 19 import logging | |
| 20 import os | |
| 21 import StringIO | |
| 22 import struct | |
| 23 import subprocess | |
| 24 import sys | |
| 25 import tempfile | |
| 26 | |
| 27 OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh' | |
| 28 | |
| 29 logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') | |
| 30 | |
| 31 class InvalidFile(Exception): | |
| 32 """Represents an invalid ICO file.""" | |
|
oshima
2015/08/18 01:41:26
2 indent
Matt Giuca
2015/08/18 04:43:45
https://google-styleguide.googlecode.com/svn/trunk
oshima
2015/08/18 06:36:34
Actually, https://www.chromium.org/chromium-os/pyt
Matt Giuca
2015/08/18 07:26:22
Hmm, that's a strange place to have the policy! (I
| |
| 33 | |
| 34 def is_png(png_data): | |
|
oshima
2015/08/18 01:41:26
looks like google style guide recommends underscor
Matt Giuca
2015/08/18 04:43:45
See previous comment.
| |
| 35 """Determines whether a sequence of bytes is a PNG.""" | |
| 36 return png_data.startswith('\x89PNG\r\n\x1a\n') | |
| 37 | |
| 38 def optimize_png_file(temp_dir, png_filename): | |
| 39 """Optimize a PNG file. | |
| 40 | |
| 41 Args: | |
| 42 temp_dir: The directory containing the PNG file. Must be the only file | |
| 43 in the directory. | |
| 44 png_filename: The full path to the PNG file to optimize. | |
| 45 | |
| 46 Returns: | |
| 47 The raw bytes of a PNG file, an optimized version of the input. | |
| 48 """ | |
| 49 logging.debug('Crushing PNG image...') | |
| 50 result = subprocess.call([OPTIMIZE_PNG_FILES, temp_dir], | |
| 51 stdout=sys.stderr) | |
|
oshima
2015/08/18 01:41:26
pass optimization level. you can just pass -o2, or
Matt Giuca
2015/08/18 04:43:45
I tested this and found that -o2 took a LOT longer
oshima
2015/08/18 06:36:34
Yep, -o2 is more for batch operation so option is
| |
| 52 if result != 0: | |
| 53 logging.warning('Warning: optimize-png-files failed (%d)', result) | |
| 54 else: | |
| 55 logging.debug('optimize-png-files succeeded') | |
| 56 | |
| 57 with open(png_filename, 'rb') as png_file: | |
| 58 return png_file.read() | |
| 59 | |
| 60 def optimize_png(png_data): | |
| 61 """Optimize a PNG. | |
| 62 | |
| 63 Args: | |
| 64 png_data: The raw bytes of a PNG file. | |
| 65 | |
| 66 Returns: | |
| 67 The raw bytes of a PNG file, an optimized version of the input. | |
| 68 """ | |
| 69 temp_dir = tempfile.mkdtemp() | |
| 70 try: | |
| 71 logging.debug('temp_dir = %s', temp_dir) | |
| 72 png_filename = os.path.join(temp_dir, 'image.png') | |
| 73 with open(png_filename, 'wb') as png_file: | |
| 74 png_file.write(png_data) | |
| 75 return optimize_png_file(temp_dir, png_filename) | |
| 76 | |
| 77 finally: | |
| 78 if os.path.exists(png_filename): | |
| 79 os.unlink(png_filename) | |
| 80 os.rmdir(temp_dir) | |
| 81 | |
| 82 def optimize_ico_file(infile, outfile): | |
| 83 """Read an ICO file, optimize its PNGs, and write the output to outfile. | |
| 84 | |
| 85 Args: | |
| 86 infile: The file to read from. Must be a seekable file-like object | |
| 87 containing a Microsoft ICO file. | |
| 88 outfile: The file to write to. | |
| 89 """ | |
| 90 filename = os.path.basename(infile.name) | |
| 91 icondir = infile.read(6) | |
| 92 zero, image_type, num_images = struct.unpack('<HHH', icondir) | |
| 93 if zero != 0: | |
| 94 raise InvalidFile('First word must be 0.') | |
| 95 if image_type not in (1, 2): | |
| 96 raise InvalidFile('Image type must be 1 or 2.') | |
| 97 | |
| 98 # Read and unpack each ICONDIRENTRY. | |
| 99 icon_dir_entries = [] | |
| 100 for i in range(num_images): | |
| 101 icondirentry = infile.read(16) | |
| 102 icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry)) | |
| 103 | |
| 104 # Read each icon's bitmap data, crush PNGs, and update icon dir entries. | |
| 105 current_offset = infile.tell() | |
| 106 icon_bitmap_data = [] | |
| 107 for i in range(num_images): | |
| 108 width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i] | |
| 109 width = width or 256 | |
| 110 height = height or 256 | |
| 111 offset = current_offset | |
| 112 icon_data = infile.read(size) | |
| 113 if len(icon_data) != size: | |
| 114 raise EOFError() | |
| 115 | |
| 116 entry_is_png = is_png(icon_data) | |
| 117 logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, | |
| 118 width, height, size, 'PNG' if entry_is_png else 'BMP') | |
| 119 | |
| 120 if entry_is_png: | |
| 121 icon_data = optimize_png(icon_data) | |
| 122 elif width >= 256 or height >= 256: | |
| 123 # TODO(mgiuca): Automatically convert large BMP images to PNGs. | |
| 124 logging.warning('Entry #%d is a large image in uncompressed BMP ' | |
| 125 'format. Please manually convert to PNG format ' | |
| 126 'before running this utility.', i + 1) | |
| 127 | |
| 128 size = len(icon_data) | |
|
oshima
2015/08/18 01:41:26
new_size ? (just to be clear)
Matt Giuca
2015/08/18 04:43:45
Done.
| |
| 129 current_offset += size | |
| 130 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, | |
| 131 r3, size, offset) | |
| 132 icon_bitmap_data.append(icon_data) | |
| 133 | |
| 134 # Write the data back to outfile. | |
| 135 outfile.write(icondir) | |
| 136 for icon_dir_entry in icon_dir_entries: | |
| 137 outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry)) | |
| 138 for icon_bitmap in icon_bitmap_data: | |
| 139 outfile.write(icon_bitmap) | |
| 140 | |
| 141 def main(args=None): | |
| 142 if args is None: | |
| 143 args = sys.argv[1:] | |
| 144 | |
| 145 parser = argparse.ArgumentParser(description='Crush Windows ICO files.') | |
| 146 parser.add_argument('files', metavar='ICO', type=argparse.FileType('r+b'), | |
| 147 nargs='+', help='.ico files to be crushed') | |
| 148 parser.add_argument('-d', '--debug', dest='debug', action='store_true', | |
| 149 help='enable debug logging') | |
| 150 | |
| 151 args = parser.parse_args() | |
| 152 | |
| 153 if args.debug: | |
| 154 logging.getLogger().setLevel(logging.DEBUG) | |
| 155 | |
| 156 for file in args.files: | |
| 157 buf = StringIO.StringIO() | |
| 158 file.seek(0, os.SEEK_END) | |
| 159 old_length = file.tell() | |
| 160 file.seek(0, os.SEEK_SET) | |
| 161 optimize_ico_file(file, buf) | |
| 162 | |
| 163 new_length = len(buf.getvalue()) | |
| 164 if new_length >= old_length: | |
| 165 logging.info('%s : Could not reduce file size.', file.name) | |
| 166 else: | |
| 167 file.truncate(new_length) | |
| 168 file.seek(0) | |
| 169 file.write(buf.getvalue()) | |
| 170 | |
| 171 saving = old_length - new_length | |
| 172 saving_percent = float(saving) / old_length | |
| 173 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, | |
| 174 old_length, new_length, saving, | |
| 175 int(saving_percent * 100)) | |
| 176 | |
| 177 if __name__ == '__main__': | |
| 178 sys.exit(main()) | |
| OLD | NEW |