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

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

Issue 1377713002: optimize-ico-files: Move the logic for this script into a module. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@optimize-ico-files-fixmasks
Patch Set: Created 5 years, 2 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
« no previous file with comments | « tools/resources/ico_tools.py ('k') | 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
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Windows ICO file crusher. 6 """Windows ICO file crusher.
7 7
8 Optimizes the PNG images within a Windows ICO icon file. This extracts all of 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 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. 10 optimize-png-files.sh, then packs them back into an ICO file.
11 11
12 NOTE: ICO files can contain both raw uncompressed BMP files and PNG files. This 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 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. 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.) 15 (Smaller icons should be BMPs for compatibility with Windows XP.)
16 """ 16 """
17 17
18 import argparse 18 import argparse
19 import logging 19 import logging
20 import math
21 import os 20 import os
22 import StringIO 21 import StringIO
23 import struct
24 import subprocess
25 import sys 22 import sys
26 import tempfile
27 23
28 OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh' 24 import ico_tools
29
30 logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
31
32 class InvalidFile(Exception):
33 """Represents an invalid ICO file."""
34
35 def IsPng(png_data):
36 """Determines whether a sequence of bytes is a PNG."""
37 return png_data.startswith('\x89PNG\r\n\x1a\n')
38
39 def OptimizePngFile(temp_dir, png_filename, optimization_level=None):
40 """Optimize a PNG file.
41
42 Args:
43 temp_dir: The directory containing the PNG file. Must be the only file in
44 the directory.
45 png_filename: The full path to the PNG file to optimize.
46
47 Returns:
48 The raw bytes of a PNG file, an optimized version of the input.
49 """
50 logging.debug('Crushing PNG image...')
51 args = [OPTIMIZE_PNG_FILES]
52 if optimization_level is not None:
53 args.append('-o%d' % optimization_level)
54 args.append(temp_dir)
55 result = subprocess.call(args, stdout=sys.stderr)
56 if result != 0:
57 logging.warning('Warning: optimize-png-files failed (%d)', result)
58 else:
59 logging.debug('optimize-png-files succeeded')
60
61 with open(png_filename, 'rb') as png_file:
62 return png_file.read()
63
64 def OptimizePng(png_data, optimization_level=None):
65 """Optimize a PNG.
66
67 Args:
68 png_data: The raw bytes of a PNG file.
69
70 Returns:
71 The raw bytes of a PNG file, an optimized version of the input.
72 """
73 temp_dir = tempfile.mkdtemp()
74 try:
75 logging.debug('temp_dir = %s', temp_dir)
76 png_filename = os.path.join(temp_dir, 'image.png')
77 with open(png_filename, 'wb') as png_file:
78 png_file.write(png_data)
79 return OptimizePngFile(temp_dir, png_filename,
80 optimization_level=optimization_level)
81
82 finally:
83 if os.path.exists(png_filename):
84 os.unlink(png_filename)
85 os.rmdir(temp_dir)
86
87 def ComputeANDMaskFromAlpha(image_data, width, height):
88 """Compute an AND mask from 32-bit BGRA image data."""
89 and_bytes = []
90 for y in range(height):
91 bit_count = 0
92 current_byte = 0
93 for x in range(width):
94 alpha = image_data[(y * width + x) * 4 + 3]
95 current_byte <<= 1
96 if ord(alpha) == 0:
97 current_byte |= 1
98 bit_count += 1
99 if bit_count == 8:
100 and_bytes.append(current_byte)
101 bit_count = 0
102 current_byte = 0
103
104 # At the end of a row, pad the current byte.
105 if bit_count > 0:
106 current_byte <<= (8 - bit_count)
107 and_bytes.append(current_byte)
108 # And keep padding until a multiple of 4 bytes.
109 while len(and_bytes) % 4 != 0:
110 and_bytes.append(0)
111
112 and_bytes = ''.join(map(chr, and_bytes))
113 return and_bytes
114
115 def RebuildANDMask(iconimage):
116 """Rebuild the AND mask in an icon image.
117
118 GIMP (<=2.8.14) creates a bad AND mask on 32-bit icon images (pixels with <50%
119 opacity are marked as transparent, which end up looking black on Windows). So,
120 if this is a 32-bit image, throw the mask away and recompute it from the alpha
121 data. (See: https://bugzilla.gnome.org/show_bug.cgi?id=755200)
122
123 Args:
124 iconimage: Bytes of an icon image (the BMP data for an entry in an ICO
125 file). Must be in BMP format, not PNG. Does not need to be 32-bit (if it
126 is not 32-bit, this is a no-op).
127
128 Returns:
129 An updated |iconimage|, with the AND mask re-computed using
130 ComputeANDMaskFromAlpha.
131 """
132 # Parse BITMAPINFOHEADER.
133 (_, width, height, _, bpp, _, _, _, _, num_colors, _) = struct.unpack(
134 '<LLLHHLLLLLL', iconimage[:40])
135
136 if bpp != 32:
137 # No alpha channel, so the mask cannot be "wrong" (it is the only source of
138 # transparency information).
139 return iconimage
140
141 height /= 2
142 xor_size = int(math.ceil(width * bpp / 32.0)) * 4 * height
143
144 # num_colors can be 0, implying 2^bpp colors.
145 xor_palette_size = (num_colors or (1 << bpp if bpp < 24 else 0)) * 4
146 xor_data = iconimage[40 + xor_palette_size :
147 40 + xor_palette_size + xor_size]
148
149 and_data = ComputeANDMaskFromAlpha(xor_data, width, height)
150
151 # Replace the AND mask in the original icon data.
152 return iconimage[:40 + xor_palette_size + xor_size] + and_data
153
154 def OptimizeIcoFile(infile, outfile, optimization_level=None):
155 """Read an ICO file, optimize its PNGs, and write the output to outfile.
156
157 Args:
158 infile: The file to read from. Must be a seekable file-like object
159 containing a Microsoft ICO file.
160 outfile: The file to write to.
161 """
162 filename = os.path.basename(infile.name)
163 icondir = infile.read(6)
164 zero, image_type, num_images = struct.unpack('<HHH', icondir)
165 if zero != 0:
166 raise InvalidFile('First word must be 0.')
167 if image_type not in (1, 2):
168 raise InvalidFile('Image type must be 1 or 2.')
169
170 # Read and unpack each ICONDIRENTRY.
171 icon_dir_entries = []
172 for i in range(num_images):
173 icondirentry = infile.read(16)
174 icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry))
175
176 # Read each icon's bitmap data, crush PNGs, and update icon dir entries.
177 current_offset = infile.tell()
178 icon_bitmap_data = []
179 for i in range(num_images):
180 width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i]
181 width = width or 256
182 height = height or 256
183 offset = current_offset
184 icon_data = infile.read(size)
185 if len(icon_data) != size:
186 raise EOFError()
187
188 entry_is_png = IsPng(icon_data)
189 logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width,
190 height, size, 'PNG' if entry_is_png else 'BMP')
191
192 if entry_is_png:
193 icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
194 else:
195 new_icon_data = RebuildANDMask(icon_data)
196 if new_icon_data != icon_data:
197 logging.info(' * Rebuilt AND mask for this image from alpha channel.')
198 icon_data = new_icon_data
199
200 if width >= 256 or height >= 256:
201 # TODO(mgiuca): Automatically convert large BMP images to PNGs.
202 logging.warning('Entry #%d is a large image in uncompressed BMP '
203 'format. Please manually convert to PNG format before '
204 'running this utility.', i + 1)
205
206 new_size = len(icon_data)
207 current_offset += new_size
208 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3,
209 new_size, offset)
210 icon_bitmap_data.append(icon_data)
211
212 # Write the data back to outfile.
213 outfile.write(icondir)
214 for icon_dir_entry in icon_dir_entries:
215 outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry))
216 for icon_bitmap in icon_bitmap_data:
217 outfile.write(icon_bitmap)
218 25
219 def main(args=None): 26 def main(args=None):
220 if args is None: 27 if args is None:
221 args = sys.argv[1:] 28 args = sys.argv[1:]
222 29
223 parser = argparse.ArgumentParser(description='Crush Windows ICO files.') 30 parser = argparse.ArgumentParser(description='Crush Windows ICO files.')
224 parser.add_argument('files', metavar='ICO', type=argparse.FileType('r+b'), 31 parser.add_argument('files', metavar='ICO', type=argparse.FileType('r+b'),
225 nargs='+', help='.ico files to be crushed') 32 nargs='+', help='.ico files to be crushed')
226 parser.add_argument('-o', dest='optimization_level', metavar='OPT', type=int, 33 parser.add_argument('-o', dest='optimization_level', metavar='OPT', type=int,
227 help='optimization level') 34 help='optimization level')
228 parser.add_argument('-d', '--debug', dest='debug', action='store_true', 35 parser.add_argument('-d', '--debug', dest='debug', action='store_true',
229 help='enable debug logging') 36 help='enable debug logging')
230 37
231 args = parser.parse_args() 38 args = parser.parse_args()
232 39
233 if args.debug: 40 if args.debug:
234 logging.getLogger().setLevel(logging.DEBUG) 41 logging.getLogger().setLevel(logging.DEBUG)
235 42
236 for file in args.files: 43 for file in args.files:
237 buf = StringIO.StringIO() 44 buf = StringIO.StringIO()
238 file.seek(0, os.SEEK_END) 45 file.seek(0, os.SEEK_END)
239 old_length = file.tell() 46 old_length = file.tell()
240 file.seek(0, os.SEEK_SET) 47 file.seek(0, os.SEEK_SET)
241 OptimizeIcoFile(file, buf, args.optimization_level) 48 ico_tools.OptimizeIcoFile(file, buf, args.optimization_level)
242 49
243 new_length = len(buf.getvalue()) 50 new_length = len(buf.getvalue())
244 51
245 # Always write (even if file size not reduced), because we make other fixes 52 # Always write (even if file size not reduced), because we make other fixes
246 # such as regenerating the AND mask. 53 # such as regenerating the AND mask.
247 file.truncate(new_length) 54 file.truncate(new_length)
248 file.seek(0) 55 file.seek(0)
249 file.write(buf.getvalue()) 56 file.write(buf.getvalue())
250 57
251 if new_length >= old_length: 58 if new_length >= old_length:
252 logging.info('%s : Could not reduce file size.', file.name) 59 logging.info('%s : Could not reduce file size.', file.name)
253 else: 60 else:
254 saving = old_length - new_length 61 saving = old_length - new_length
255 saving_percent = float(saving) / old_length 62 saving_percent = float(saving) / old_length
256 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, old_length, 63 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, old_length,
257 new_length, saving, int(saving_percent * 100)) 64 new_length, saving, int(saving_percent * 100))
258 65
259 if __name__ == '__main__': 66 if __name__ == '__main__':
260 sys.exit(main()) 67 sys.exit(main())
OLDNEW
« no previous file with comments | « tools/resources/ico_tools.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698