Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(16)

Side by Side Diff: tools/resources/optimize-ico-files.py

Issue 130583003: Added script optimize-ico-files. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: More fixes, and added warning for large uncompressed images. Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698