| Index: tools/resources/optimize-ico-files.py
|
| diff --git a/tools/resources/optimize-ico-files.py b/tools/resources/optimize-ico-files.py
|
| index 71185f1368611d1ca29a82f4124835a49ca5d3a2..2635e9c509ba5cf6c3cec048e97c584b9670de8e 100755
|
| --- a/tools/resources/optimize-ico-files.py
|
| +++ b/tools/resources/optimize-ico-files.py
|
| @@ -17,204 +17,11 @@ image, it will not get smaller. 256x256 icons should be PNG-formatted first.
|
|
|
| import argparse
|
| import logging
|
| -import math
|
| import os
|
| import StringIO
|
| -import struct
|
| -import subprocess
|
| import sys
|
| -import tempfile
|
|
|
| -OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh'
|
| -
|
| -logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
| -
|
| -class InvalidFile(Exception):
|
| - """Represents an invalid ICO file."""
|
| -
|
| -def IsPng(png_data):
|
| - """Determines whether a sequence of bytes is a PNG."""
|
| - return png_data.startswith('\x89PNG\r\n\x1a\n')
|
| -
|
| -def OptimizePngFile(temp_dir, png_filename, optimization_level=None):
|
| - """Optimize a PNG file.
|
| -
|
| - Args:
|
| - temp_dir: The directory containing the PNG file. Must be the only file in
|
| - the directory.
|
| - png_filename: The full path to the PNG file to optimize.
|
| -
|
| - Returns:
|
| - The raw bytes of a PNG file, an optimized version of the input.
|
| - """
|
| - logging.debug('Crushing PNG image...')
|
| - args = [OPTIMIZE_PNG_FILES]
|
| - if optimization_level is not None:
|
| - args.append('-o%d' % optimization_level)
|
| - args.append(temp_dir)
|
| - result = subprocess.call(args, stdout=sys.stderr)
|
| - if result != 0:
|
| - logging.warning('Warning: optimize-png-files failed (%d)', result)
|
| - else:
|
| - logging.debug('optimize-png-files succeeded')
|
| -
|
| - with open(png_filename, 'rb') as png_file:
|
| - return png_file.read()
|
| -
|
| -def OptimizePng(png_data, optimization_level=None):
|
| - """Optimize a PNG.
|
| -
|
| - Args:
|
| - png_data: The raw bytes of a PNG file.
|
| -
|
| - Returns:
|
| - The raw bytes of a PNG file, an optimized version of the input.
|
| - """
|
| - temp_dir = tempfile.mkdtemp()
|
| - try:
|
| - logging.debug('temp_dir = %s', temp_dir)
|
| - png_filename = os.path.join(temp_dir, 'image.png')
|
| - with open(png_filename, 'wb') as png_file:
|
| - png_file.write(png_data)
|
| - return OptimizePngFile(temp_dir, png_filename,
|
| - optimization_level=optimization_level)
|
| -
|
| - finally:
|
| - if os.path.exists(png_filename):
|
| - os.unlink(png_filename)
|
| - os.rmdir(temp_dir)
|
| -
|
| -def ComputeANDMaskFromAlpha(image_data, width, height):
|
| - """Compute an AND mask from 32-bit BGRA image data."""
|
| - and_bytes = []
|
| - for y in range(height):
|
| - bit_count = 0
|
| - current_byte = 0
|
| - for x in range(width):
|
| - alpha = image_data[(y * width + x) * 4 + 3]
|
| - current_byte <<= 1
|
| - if ord(alpha) == 0:
|
| - current_byte |= 1
|
| - bit_count += 1
|
| - if bit_count == 8:
|
| - and_bytes.append(current_byte)
|
| - bit_count = 0
|
| - current_byte = 0
|
| -
|
| - # At the end of a row, pad the current byte.
|
| - if bit_count > 0:
|
| - current_byte <<= (8 - bit_count)
|
| - and_bytes.append(current_byte)
|
| - # And keep padding until a multiple of 4 bytes.
|
| - while len(and_bytes) % 4 != 0:
|
| - and_bytes.append(0)
|
| -
|
| - and_bytes = ''.join(map(chr, and_bytes))
|
| - return and_bytes
|
| -
|
| -def RebuildANDMask(iconimage):
|
| - """Rebuild the AND mask in an icon image.
|
| -
|
| - GIMP (<=2.8.14) creates a bad AND mask on 32-bit icon images (pixels with <50%
|
| - opacity are marked as transparent, which end up looking black on Windows). So,
|
| - if this is a 32-bit image, throw the mask away and recompute it from the alpha
|
| - data. (See: https://bugzilla.gnome.org/show_bug.cgi?id=755200)
|
| -
|
| - Args:
|
| - iconimage: Bytes of an icon image (the BMP data for an entry in an ICO
|
| - file). Must be in BMP format, not PNG. Does not need to be 32-bit (if it
|
| - is not 32-bit, this is a no-op).
|
| -
|
| - Returns:
|
| - An updated |iconimage|, with the AND mask re-computed using
|
| - ComputeANDMaskFromAlpha.
|
| - """
|
| - # Parse BITMAPINFOHEADER.
|
| - (_, width, height, _, bpp, _, _, _, _, num_colors, _) = struct.unpack(
|
| - '<LLLHHLLLLLL', iconimage[:40])
|
| -
|
| - if bpp != 32:
|
| - # No alpha channel, so the mask cannot be "wrong" (it is the only source of
|
| - # transparency information).
|
| - return iconimage
|
| -
|
| - height /= 2
|
| - xor_size = int(math.ceil(width * bpp / 32.0)) * 4 * height
|
| -
|
| - # num_colors can be 0, implying 2^bpp colors.
|
| - xor_palette_size = (num_colors or (1 << bpp if bpp < 24 else 0)) * 4
|
| - xor_data = iconimage[40 + xor_palette_size :
|
| - 40 + xor_palette_size + xor_size]
|
| -
|
| - and_data = ComputeANDMaskFromAlpha(xor_data, width, height)
|
| -
|
| - # Replace the AND mask in the original icon data.
|
| - return iconimage[:40 + xor_palette_size + xor_size] + and_data
|
| -
|
| -def OptimizeIcoFile(infile, outfile, optimization_level=None):
|
| - """Read an ICO file, optimize its PNGs, and write the output to outfile.
|
| -
|
| - Args:
|
| - infile: The file to read from. Must be a seekable file-like object
|
| - containing a Microsoft ICO file.
|
| - outfile: The file to write to.
|
| - """
|
| - filename = os.path.basename(infile.name)
|
| - icondir = infile.read(6)
|
| - zero, image_type, num_images = struct.unpack('<HHH', icondir)
|
| - if zero != 0:
|
| - raise InvalidFile('First word must be 0.')
|
| - if image_type not in (1, 2):
|
| - raise InvalidFile('Image type must be 1 or 2.')
|
| -
|
| - # Read and unpack each ICONDIRENTRY.
|
| - icon_dir_entries = []
|
| - for i in range(num_images):
|
| - icondirentry = infile.read(16)
|
| - icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry))
|
| -
|
| - # Read each icon's bitmap data, crush PNGs, and update icon dir entries.
|
| - current_offset = infile.tell()
|
| - icon_bitmap_data = []
|
| - for i in range(num_images):
|
| - width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i]
|
| - width = width or 256
|
| - height = height or 256
|
| - offset = current_offset
|
| - icon_data = infile.read(size)
|
| - if len(icon_data) != size:
|
| - raise EOFError()
|
| -
|
| - entry_is_png = IsPng(icon_data)
|
| - logging.info('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width,
|
| - height, size, 'PNG' if entry_is_png else 'BMP')
|
| -
|
| - if entry_is_png:
|
| - icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
|
| - else:
|
| - new_icon_data = RebuildANDMask(icon_data)
|
| - if new_icon_data != icon_data:
|
| - logging.info(' * Rebuilt AND mask for this image from alpha channel.')
|
| - icon_data = new_icon_data
|
| -
|
| - if width >= 256 or height >= 256:
|
| - # TODO(mgiuca): Automatically convert large BMP images to PNGs.
|
| - logging.warning('Entry #%d is a large image in uncompressed BMP '
|
| - 'format. Please manually convert to PNG format before '
|
| - 'running this utility.', i + 1)
|
| -
|
| - new_size = len(icon_data)
|
| - current_offset += new_size
|
| - icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3,
|
| - new_size, offset)
|
| - icon_bitmap_data.append(icon_data)
|
| -
|
| - # Write the data back to outfile.
|
| - outfile.write(icondir)
|
| - for icon_dir_entry in icon_dir_entries:
|
| - outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry))
|
| - for icon_bitmap in icon_bitmap_data:
|
| - outfile.write(icon_bitmap)
|
| +import ico_tools
|
|
|
| def main(args=None):
|
| if args is None:
|
| @@ -238,7 +45,7 @@ def main(args=None):
|
| file.seek(0, os.SEEK_END)
|
| old_length = file.tell()
|
| file.seek(0, os.SEEK_SET)
|
| - OptimizeIcoFile(file, buf, args.optimization_level)
|
| + ico_tools.OptimizeIcoFile(file, buf, args.optimization_level)
|
|
|
| new_length = len(buf.getvalue())
|
|
|
|
|