| 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 |