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

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

Issue 1372843002: optimize-ico-files: Automatically rebuild the AND mask of 32-bit images. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Added link to bug. 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 | « chrome/app/theme/README ('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
20 import os 21 import os
21 import StringIO 22 import StringIO
22 import struct 23 import struct
23 import subprocess 24 import subprocess
24 import sys 25 import sys
25 import tempfile 26 import tempfile
26 27
27 OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh' 28 OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh'
28 29
29 logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') 30 logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
76 with open(png_filename, 'wb') as png_file: 77 with open(png_filename, 'wb') as png_file:
77 png_file.write(png_data) 78 png_file.write(png_data)
78 return OptimizePngFile(temp_dir, png_filename, 79 return OptimizePngFile(temp_dir, png_filename,
79 optimization_level=optimization_level) 80 optimization_level=optimization_level)
80 81
81 finally: 82 finally:
82 if os.path.exists(png_filename): 83 if os.path.exists(png_filename):
83 os.unlink(png_filename) 84 os.unlink(png_filename)
84 os.rmdir(temp_dir) 85 os.rmdir(temp_dir)
85 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
86 def OptimizeIcoFile(infile, outfile, optimization_level=None): 154 def OptimizeIcoFile(infile, outfile, optimization_level=None):
87 """Read an ICO file, optimize its PNGs, and write the output to outfile. 155 """Read an ICO file, optimize its PNGs, and write the output to outfile.
88 156
89 Args: 157 Args:
90 infile: The file to read from. Must be a seekable file-like object 158 infile: The file to read from. Must be a seekable file-like object
91 containing a Microsoft ICO file. 159 containing a Microsoft ICO file.
92 outfile: The file to write to. 160 outfile: The file to write to.
93 """ 161 """
94 filename = os.path.basename(infile.name) 162 filename = os.path.basename(infile.name)
95 icondir = infile.read(6) 163 icondir = infile.read(6)
(...skipping 20 matching lines...) Expand all
116 icon_data = infile.read(size) 184 icon_data = infile.read(size)
117 if len(icon_data) != size: 185 if len(icon_data) != size:
118 raise EOFError() 186 raise EOFError()
119 187
120 entry_is_png = IsPng(icon_data) 188 entry_is_png = IsPng(icon_data)
121 logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width, 189 logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width,
122 height, size, 'PNG' if entry_is_png else 'BMP') 190 height, size, 'PNG' if entry_is_png else 'BMP')
123 191
124 if entry_is_png: 192 if entry_is_png:
125 icon_data = OptimizePng(icon_data, optimization_level=optimization_level) 193 icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
126 elif width >= 256 or height >= 256: 194 else:
127 # TODO(mgiuca): Automatically convert large BMP images to PNGs. 195 new_icon_data = RebuildANDMask(icon_data)
128 logging.warning('Entry #%d is a large image in uncompressed BMP format. ' 196 if new_icon_data != icon_data:
129 'Please manually convert to PNG format before running ' 197 logging.info(' * Rebuilt AND mask for this image from alpha channel.')
130 'this utility.', i + 1) 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)
131 205
132 new_size = len(icon_data) 206 new_size = len(icon_data)
133 current_offset += new_size 207 current_offset += new_size
134 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3, 208 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3,
135 new_size, offset) 209 new_size, offset)
136 icon_bitmap_data.append(icon_data) 210 icon_bitmap_data.append(icon_data)
137 211
138 # Write the data back to outfile. 212 # Write the data back to outfile.
139 outfile.write(icondir) 213 outfile.write(icondir)
140 for icon_dir_entry in icon_dir_entries: 214 for icon_dir_entry in icon_dir_entries:
(...skipping 19 matching lines...) Expand all
160 logging.getLogger().setLevel(logging.DEBUG) 234 logging.getLogger().setLevel(logging.DEBUG)
161 235
162 for file in args.files: 236 for file in args.files:
163 buf = StringIO.StringIO() 237 buf = StringIO.StringIO()
164 file.seek(0, os.SEEK_END) 238 file.seek(0, os.SEEK_END)
165 old_length = file.tell() 239 old_length = file.tell()
166 file.seek(0, os.SEEK_SET) 240 file.seek(0, os.SEEK_SET)
167 OptimizeIcoFile(file, buf, args.optimization_level) 241 OptimizeIcoFile(file, buf, args.optimization_level)
168 242
169 new_length = len(buf.getvalue()) 243 new_length = len(buf.getvalue())
244
245 # Always write (even if file size not reduced), because we make other fixes
246 # such as regenerating the AND mask.
247 file.truncate(new_length)
248 file.seek(0)
249 file.write(buf.getvalue())
250
170 if new_length >= old_length: 251 if new_length >= old_length:
171 logging.info('%s : Could not reduce file size.', file.name) 252 logging.info('%s : Could not reduce file size.', file.name)
172 else: 253 else:
173 file.truncate(new_length)
174 file.seek(0)
175 file.write(buf.getvalue())
176
177 saving = old_length - new_length 254 saving = old_length - new_length
178 saving_percent = float(saving) / old_length 255 saving_percent = float(saving) / old_length
179 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, old_length, 256 logging.info('%s : %d => %d (%d bytes : %d %%)', file.name, old_length,
180 new_length, saving, int(saving_percent * 100)) 257 new_length, saving, int(saving_percent * 100))
181 258
182 if __name__ == '__main__': 259 if __name__ == '__main__':
183 sys.exit(main()) 260 sys.exit(main())
OLDNEW
« no previous file with comments | « chrome/app/theme/README ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698