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
4 # found in the LICENSE file.
6 """Windows ICO file crusher.
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
10 optimize-png-files.sh, then packs them back into an ICO file.
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
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.)
27 OPTIMIZE_PNG_FILES
= 'tools/resources/optimize-png-files.sh'
29 logging
.basicConfig(level
=logging
.INFO
, format
='%(levelname)s: %(message)s')
31 class InvalidFile(Exception):
32 """Represents an invalid ICO file."""
35 """Determines whether a sequence of bytes is a PNG."""
36 return png_data
.startswith('\x89PNG\r\n\x1a\n')
38 def OptimizePngFile(temp_dir
, png_filename
, optimization_level
=None):
39 """Optimize a PNG file.
42 temp_dir: The directory containing the PNG file. Must be the only file in
44 png_filename: The full path to the PNG file to optimize.
47 The raw bytes of a PNG file, an optimized version of the input.
49 logging
.debug('Crushing PNG image...')
50 args
= [OPTIMIZE_PNG_FILES
]
51 if optimization_level
is not None:
52 args
.append('-o%d' % optimization_level
)
54 result
= subprocess
.call(args
, stdout
=sys
.stderr
)
56 logging
.warning('Warning: optimize-png-files failed (%d)', result
)
58 logging
.debug('optimize-png-files succeeded')
60 with
open(png_filename
, 'rb') as png_file
:
61 return png_file
.read()
63 def OptimizePng(png_data
, optimization_level
=None):
67 png_data: The raw bytes of a PNG file.
70 The raw bytes of a PNG file, an optimized version of the input.
72 temp_dir
= tempfile
.mkdtemp()
74 logging
.debug('temp_dir = %s', temp_dir
)
75 png_filename
= os
.path
.join(temp_dir
, 'image.png')
76 with
open(png_filename
, 'wb') as png_file
:
77 png_file
.write(png_data
)
78 return OptimizePngFile(temp_dir
, png_filename
,
79 optimization_level
=optimization_level
)
82 if os
.path
.exists(png_filename
):
83 os
.unlink(png_filename
)
86 def OptimizeIcoFile(infile
, outfile
, optimization_level
=None):
87 """Read an ICO file, optimize its PNGs, and write the output to outfile.
90 infile: The file to read from. Must be a seekable file-like object
91 containing a Microsoft ICO file.
92 outfile: The file to write to.
94 filename
= os
.path
.basename(infile
.name
)
95 icondir
= infile
.read(6)
96 zero
, image_type
, num_images
= struct
.unpack('<HHH', icondir
)
98 raise InvalidFile('First word must be 0.')
99 if image_type
not in (1, 2):
100 raise InvalidFile('Image type must be 1 or 2.')
102 # Read and unpack each ICONDIRENTRY.
103 icon_dir_entries
= []
104 for i
in range(num_images
):
105 icondirentry
= infile
.read(16)
106 icon_dir_entries
.append(struct
.unpack('<BBBBHHLL', icondirentry
))
108 # Read each icon's bitmap data, crush PNGs, and update icon dir entries.
109 current_offset
= infile
.tell()
110 icon_bitmap_data
= []
111 for i
in range(num_images
):
112 width
, height
, num_colors
, r1
, r2
, r3
, size
, _
= icon_dir_entries
[i
]
114 height
= height
or 256
115 offset
= current_offset
116 icon_data
= infile
.read(size
)
117 if len(icon_data
) != size
:
120 entry_is_png
= IsPng(icon_data
)
121 logging
.info('%s entry #%d: %dx%d, %d bytes (%s)', filename
, i
+ 1, width
,
122 height
, size
, 'PNG' if entry_is_png
else 'BMP')
125 icon_data
= OptimizePng(icon_data
, optimization_level
=optimization_level
)
126 elif width
>= 256 or height
>= 256:
127 # TODO(mgiuca): Automatically convert large BMP images to PNGs.
128 logging
.warning('Entry #%d is a large image in uncompressed BMP format. '
129 'Please manually convert to PNG format before running '
130 'this utility.', i
+ 1)
132 new_size
= len(icon_data
)
133 current_offset
+= new_size
134 icon_dir_entries
[i
] = (width
% 256, height
% 256, num_colors
, r1
, r2
, r3
,
136 icon_bitmap_data
.append(icon_data
)
138 # Write the data back to outfile.
139 outfile
.write(icondir
)
140 for icon_dir_entry
in icon_dir_entries
:
141 outfile
.write(struct
.pack('<BBBBHHLL', *icon_dir_entry
))
142 for icon_bitmap
in icon_bitmap_data
:
143 outfile
.write(icon_bitmap
)
149 parser
= argparse
.ArgumentParser(description
='Crush Windows ICO files.')
150 parser
.add_argument('files', metavar
='ICO', type=argparse
.FileType('r+b'),
151 nargs
='+', help='.ico files to be crushed')
152 parser
.add_argument('-o', dest
='optimization_level', metavar
='OPT', type=int,
153 help='optimization level')
154 parser
.add_argument('-d', '--debug', dest
='debug', action
='store_true',
155 help='enable debug logging')
157 args
= parser
.parse_args()
160 logging
.getLogger().setLevel(logging
.DEBUG
)
162 for file in args
.files
:
163 buf
= StringIO
.StringIO()
164 file.seek(0, os
.SEEK_END
)
165 old_length
= file.tell()
166 file.seek(0, os
.SEEK_SET
)
167 OptimizeIcoFile(file, buf
, args
.optimization_level
)
169 new_length
= len(buf
.getvalue())
170 if new_length
>= old_length
:
171 logging
.info('%s : Could not reduce file size.', file.name
)
173 file.truncate(new_length
)
175 file.write(buf
.getvalue())
177 saving
= old_length
- new_length
178 saving_percent
= float(saving
) / old_length
179 logging
.info('%s : %d => %d (%d bytes : %d %%)', file.name
, old_length
,
180 new_length
, saving
, int(saving_percent
* 100))
182 if __name__
== '__main__':