| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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()) |
| OLD | NEW |