# 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( '= 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('