Skip to content

Commit

Permalink
optimize-ico-files: Automatically convert large BMPs to PNG format.
Browse files Browse the repository at this point in the history
This ensures that we don't have large uncompressed images in .ico files
which could be stored as PNG. Only compresses large (>=256x256) images
(smaller images must be stored uncompressed for compatibility with
Windows XP).

Images are converted to PNG and then crushed.

BUG=332639

Review-Url: https://codereview.chromium.org/1272763003
Cr-Commit-Position: refs/heads/master@{#430442}
  • Loading branch information
mgiuca authored and Commit bot committed Nov 8, 2016
1 parent 71190f6 commit 0d45980
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 8 deletions.
4 changes: 2 additions & 2 deletions chrome/app/theme/README
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ process (using ImageMagick and GIMP) satisfies the above conditions:
4. Run src/tools/resources/optimize-ico-files.py on the resulting .ico file.

You can also run src/tools/resources/optimize-ico-files.py on existing .ico
files. This will run a basic PNG optimization pass and fix up any broken image
masks (http://crbug.com/534679).
files. This will convert BMPs to PNGs and run a basic PNG optimization pass, as
well as fix up any broken image masks (http://crbug.com/534679).
88 changes: 82 additions & 6 deletions tools/resources/ico_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import tempfile

OPTIMIZE_PNG_FILES = 'tools/resources/optimize-png-files.sh'
IMAGEMAGICK_CONVERT = 'convert'

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

Expand Down Expand Up @@ -69,6 +70,79 @@ def OptimizePng(png_data, optimization_level=None):
os.unlink(png_filename)
os.rmdir(temp_dir)

def ExportSingleEntry(icon_dir_entry, icon_data, outfile):
"""Export a single icon dir entry to its own ICO file.
Args:
icon_dir_entry: Struct containing the fields of an ICONDIRENTRY.
icon_data: Raw pixel data of the icon.
outfile: File object to write to.
"""
# Write the ICONDIR header.
logging.debug('len(icon_data) = %d', len(icon_data))
outfile.write(struct.pack('<HHH', 0, 1, 1))

# Write the ICONDIRENTRY header.
width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entry
offset = 22;
icon_dir_entry = width, height, num_colors, r1, r2, r3, size, offset
outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry))

# Write the image data.
outfile.write(icon_data)

def ConvertIcoToPng(ico_filename, png_filename):
"""Convert a single-entry ICO file to a PNG image.
Requires that the user has `convert` (ImageMagick) installed.
Raises:
OSError: If ImageMagick was not found.
subprocess.CalledProcessError: If convert failed.
"""
logging.debug('Converting BMP image to PNG...')
args = [IMAGEMAGICK_CONVERT, ico_filename, png_filename]
result = subprocess.check_call(args, stdout=sys.stderr)
logging.info('Converted BMP image to PNG format')

def OptimizeBmp(icon_dir_entry, icon_data):
"""Convert a BMP file to PNG and optimize it.
Args:
icon_dir_entry: Struct containing the fields of an ICONDIRENTRY.
icon_data: Raw pixel data of the icon.
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)
ico_filename = os.path.join(temp_dir, 'image.ico')
png_filename = os.path.join(temp_dir, 'image.png')
with open(ico_filename, 'wb') as ico_file:
logging.debug('writing %s', ico_filename)
ExportSingleEntry(icon_dir_entry, icon_data, ico_file)

try:
ConvertIcoToPng(ico_filename, png_filename)
except Exception as e:
logging.warning('Could not convert BMP to PNG format: %s', e)
if isinstance(e, OSError):
logging.info('This is because ImageMagick (`convert`) was not found. '
'Please install it, or manually convert large BMP images '
'into PNG before running this utility.')
return icon_data

return OptimizePngFile(temp_dir, png_filename)

finally:
if os.path.exists(ico_filename):
os.unlink(ico_filename)
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 = []
Expand Down Expand Up @@ -175,19 +249,21 @@ def OptimizeIcoFile(infile, outfile, optimization_level=None):
height, size, 'PNG' if entry_is_png else 'BMP')

if entry_is_png:
# It is a PNG. Crush it.
icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
elif width >= 256 or height >= 256:
# It is a large BMP. Reformat as a PNG, then crush it.
# Note: Smaller images are kept uncompressed, for compatibility with
# Windows XP.
# TODO(mgiuca): Now that we no longer support XP, we can probably compress
# all of the images. https://crbug.com/663136
icon_data = OptimizeBmp(icon_dir_entries[i], icon_data)
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,
Expand Down

0 comments on commit 0d45980

Please sign in to comment.