blob: 259f7f197612b6dcc92e924e413729d78d280a19 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import math
import os
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)