Add OWNERS to content/browser/quota
[chromium-blink-merge.git] / tools / resources / optimize-ico-files.py
blobfcd9f5368fdffce45d01d6dac73a61ab16d6db6f
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.
6 """Windows ICO file crusher.
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.
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 """
18 import argparse
19 import logging
20 import os
21 import StringIO
22 import struct
23 import subprocess
24 import sys
25 import tempfile
27 OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh'
29 logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
31 class InvalidFile(Exception):
32 """Represents an invalid ICO file."""
34 def IsPng(png_data):
35 """Determines whether a sequence of bytes is a PNG."""
36 return png_data.startswith('\x89PNG\r\n\x1a\n')
38 def OptimizePngFile(temp_dir, png_filename, optimization_level=None):
39 """Optimize a PNG file.
41 Args:
42 temp_dir: The directory containing the PNG file. Must be the only file in
43 the directory.
44 png_filename: The full path to the PNG file to optimize.
46 Returns:
47 The raw bytes of a PNG file, an optimized version of the input.
48 """
49 logging.debug('Crushing PNG image...')
50 args = [OPTIMIZE_PNG_FILES]
51 if optimization_level is not None:
52 args.append('-o%d' % optimization_level)
53 args.append(temp_dir)
54 result = subprocess.call(args, stdout=sys.stderr)
55 if result != 0:
56 logging.warning('Warning: optimize-png-files failed (%d)', result)
57 else:
58 logging.debug('optimize-png-files succeeded')
60 with open(png_filename, 'rb') as png_file:
61 return png_file.read()
63 def OptimizePng(png_data, optimization_level=None):
64 """Optimize a PNG.
66 Args:
67 png_data: The raw bytes of a PNG file.
69 Returns:
70 The raw bytes of a PNG file, an optimized version of the input.
71 """
72 temp_dir = tempfile.mkdtemp()
73 try:
74 logging.debug('temp_dir = %s', temp_dir)
75 png_filename = os.path.join(temp_dir, 'image.png')
76 with open(png_filename, 'wb') as png_file:
77 png_file.write(png_data)
78 return OptimizePngFile(temp_dir, png_filename,
79 optimization_level=optimization_level)
81 finally:
82 if os.path.exists(png_filename):
83 os.unlink(png_filename)
84 os.rmdir(temp_dir)
86 def OptimizeIcoFile(infile, outfile, optimization_level=None):
87 """Read an ICO file, optimize its PNGs, and write the output to outfile.
89 Args:
90 infile: The file to read from. Must be a seekable file-like object
91 containing a Microsoft ICO file.
92 outfile: The file to write to.
93 """
94 filename = os.path.basename(infile.name)
95 icondir = infile.read(6)
96 zero, image_type, num_images = struct.unpack('<HHH', icondir)
97 if zero != 0:
98 raise InvalidFile('First word must be 0.')
99 if image_type not in (1, 2):
100 raise InvalidFile('Image type must be 1 or 2.')
102 # Read and unpack each ICONDIRENTRY.
103 icon_dir_entries = []
104 for i in range(num_images):
105 icondirentry = infile.read(16)
106 icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry))
108 # Read each icon's bitmap data, crush PNGs, and update icon dir entries.
109 current_offset = infile.tell()
110 icon_bitmap_data = []
111 for i in range(num_images):
112 width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i]
113 width = width or 256
114 height = height or 256
115 offset = current_offset
116 icon_data = infile.read(size)
117 if len(icon_data) != size:
118 raise EOFError()
120 entry_is_png = IsPng(icon_data)
121 logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width,
122 height, size, 'PNG' if entry_is_png else 'BMP')
124 if entry_is_png:
125 icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
126 elif width >= 256 or height >= 256:
127 # TODO(mgiuca): Automatically convert large BMP images to PNGs.
128 logging.warning('Entry #%d is a large image in uncompressed BMP format. '
129 'Please manually convert to PNG format before running '
130 'this utility.', i + 1)
132 new_size = len(icon_data)
133 current_offset += new_size
134 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3,
135 new_size, offset)
136 icon_bitmap_data.append(icon_data)
138 # Write the data back to outfile.
139 outfile.write(icondir)
140 for icon_dir_entry in icon_dir_entries:
141 outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry))
142 for icon_bitmap in icon_bitmap_data:
143 outfile.write(icon_bitmap)
145 def main(args=None):
146 if args is None:
147 args = sys.argv[1:]
149 parser = argparse.ArgumentParser(description='Crush Windows ICO files.')
150 parser.add_argument('files', metavar='ICO', type=argparse.FileType('r+b'),
151 nargs='+', help='.ico files to be crushed')
152 parser.add_argument('-o', dest='optimization_level', metavar='OPT', type=int,
153 help='optimization level')
154 parser.add_argument('-d', '--debug', dest='debug', action='store_true',
155 help='enable debug logging')
157 args = parser.parse_args()
159 if args.debug:
160 logging.getLogger().setLevel(logging.DEBUG)
162 for file in args.files:
163 buf = StringIO.StringIO()
164 file.seek(0, os.SEEK_END)
165 old_length = file.tell()
166 file.seek(0, os.SEEK_SET)
167 OptimizeIcoFile(file, buf, args.optimization_level)
169 new_length = len(buf.getvalue())
170 if new_length >= old_length:
171 logging.info('%s : Could not reduce file size.', file.name)
172 else:
173 file.truncate(new_length)
174 file.seek(0)
175 file.write(buf.getvalue())
177 saving = old_length - new_length
178 saving_percent = float(saving) / old_length
179 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, old_length,
180 new_length, saving, int(saving_percent * 100))
182 if __name__ == '__main__':
183 sys.exit(main())