Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(386)

Side by Side Diff: tools/resources/ico_tools.py

Issue 1372113003: Add a PRESUBMIT check for broken .ico files. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@optimize-ico-files-modularize
Patch Set: Update copyright year. Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « chrome/app/theme/PRESUBMIT.py ('k') | tools/resources/optimize-ico-files.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2015 The Chromium Authors. All rights reserved. 1 # Copyright 2015 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import logging 5 import logging
6 import math 6 import math
7 import os 7 import os
8 import struct 8 import struct
9 import subprocess 9 import subprocess
10 import sys 10 import sys
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after
63 with open(png_filename, 'wb') as png_file: 63 with open(png_filename, 'wb') as png_file:
64 png_file.write(png_data) 64 png_file.write(png_data)
65 return OptimizePngFile(temp_dir, png_filename, 65 return OptimizePngFile(temp_dir, png_filename,
66 optimization_level=optimization_level) 66 optimization_level=optimization_level)
67 67
68 finally: 68 finally:
69 if os.path.exists(png_filename): 69 if os.path.exists(png_filename):
70 os.unlink(png_filename) 70 os.unlink(png_filename)
71 os.rmdir(temp_dir) 71 os.rmdir(temp_dir)
72 72
73 def BytesPerRowBMP(width, bpp):
74 """Computes the number of bytes per row in a Windows BMP image."""
75 # width * bpp / 8, rounded up to the nearest multiple of 4.
76 return int(math.ceil(width * bpp / 32.0)) * 4
77
73 def ExportSingleEntry(icon_dir_entry, icon_data, outfile): 78 def ExportSingleEntry(icon_dir_entry, icon_data, outfile):
74 """Export a single icon dir entry to its own ICO file. 79 """Export a single icon dir entry to its own ICO file.
75 80
76 Args: 81 Args:
77 icon_dir_entry: Struct containing the fields of an ICONDIRENTRY. 82 icon_dir_entry: Struct containing the fields of an ICONDIRENTRY.
78 icon_data: Raw pixel data of the icon. 83 icon_data: Raw pixel data of the icon.
79 outfile: File object to write to. 84 outfile: File object to write to.
80 """ 85 """
81 # Write the ICONDIR header. 86 # Write the ICONDIR header.
82 logging.debug('len(icon_data) = %d', len(icon_data)) 87 logging.debug('len(icon_data) = %d', len(icon_data))
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
164 if bit_count > 0: 169 if bit_count > 0:
165 current_byte <<= (8 - bit_count) 170 current_byte <<= (8 - bit_count)
166 and_bytes.append(current_byte) 171 and_bytes.append(current_byte)
167 # And keep padding until a multiple of 4 bytes. 172 # And keep padding until a multiple of 4 bytes.
168 while len(and_bytes) % 4 != 0: 173 while len(and_bytes) % 4 != 0:
169 and_bytes.append(0) 174 and_bytes.append(0)
170 175
171 and_bytes = ''.join(map(chr, and_bytes)) 176 and_bytes = ''.join(map(chr, and_bytes))
172 return and_bytes 177 return and_bytes
173 178
174 def RebuildANDMask(iconimage): 179 def CheckANDMaskAgainstAlpha(xor_data, and_data, width, height):
175 """Rebuild the AND mask in an icon image. 180 """Checks whether an AND mask is "good" for 32-bit BGRA image data.
181
182 This checks that the mask is opaque wherever the alpha channel is not fully
183 transparent. Pixels that violate this condition will show up as black in some
184 contexts in Windows (http://crbug.com/526622). Also checks the inverse
185 condition, that the mask is transparent wherever the alpha channel is fully
186 transparent. While this does not appear to be strictly necessary, it is good
187 practice for backwards compatibility.
188
189 Returns True if the AND mask is "good", False otherwise.
190 """
191 xor_bytes_per_row = width * 4
192 and_bytes_per_row = BytesPerRowBMP(width, 1)
193
194 for y in range(height):
195 for x in range(width):
196 alpha = ord(xor_data[y * xor_bytes_per_row + x * 4 + 3])
197 mask = bool(ord(and_data[y * and_bytes_per_row + x // 8]) &
198 (1 << (7 - (x % 8))))
199
200 if mask:
201 if alpha > 0:
202 # mask is transparent, alpha is partially or fully opaque. This pixel
203 # can show up as black on Windows due to a rendering bug.
204 return False
205 else:
206 if alpha == 0:
207 # mask is opaque, alpha is transparent. This pixel should be marked as
208 # transparent in the mask, for legacy reasons.
209 return False
210
211 return True
212
213 def CheckOrRebuildANDMask(iconimage, rebuild=False):
214 """Checks the AND mask in an icon image for correctness, or rebuilds it.
176 215
177 GIMP (<=2.8.14) creates a bad AND mask on 32-bit icon images (pixels with <50% 216 GIMP (<=2.8.14) creates a bad AND mask on 32-bit icon images (pixels with <50%
178 opacity are marked as transparent, which end up looking black on Windows). So, 217 opacity are marked as transparent, which end up looking black on Windows).
218 With rebuild == False, checks whether the mask is bad. With rebuild == True,
179 if this is a 32-bit image, throw the mask away and recompute it from the alpha 219 if this is a 32-bit image, throw the mask away and recompute it from the alpha
180 data. (See: https://bugzilla.gnome.org/show_bug.cgi?id=755200) 220 data. (See: https://bugzilla.gnome.org/show_bug.cgi?id=755200)
181 221
182 Args: 222 Args:
183 iconimage: Bytes of an icon image (the BMP data for an entry in an ICO 223 iconimage: Bytes of an icon image (the BMP data for an entry in an ICO
184 file). Must be in BMP format, not PNG. Does not need to be 32-bit (if it 224 file). Must be in BMP format, not PNG. Does not need to be 32-bit (if it
185 is not 32-bit, this is a no-op). 225 is not 32-bit, this is a no-op).
186 226
187 Returns: 227 Returns:
188 An updated |iconimage|, with the AND mask re-computed using 228 If rebuild == False, a bool indicating whether the mask is "good". If
229 rebuild == True, an updated |iconimage|, with the AND mask re-computed using
189 ComputeANDMaskFromAlpha. 230 ComputeANDMaskFromAlpha.
190 """ 231 """
191 # Parse BITMAPINFOHEADER. 232 # Parse BITMAPINFOHEADER.
192 (_, width, height, _, bpp, _, _, _, _, num_colors, _) = struct.unpack( 233 (_, width, height, _, bpp, _, _, _, _, num_colors, _) = struct.unpack(
193 '<LLLHHLLLLLL', iconimage[:40]) 234 '<LLLHHLLLLLL', iconimage[:40])
194 235
195 if bpp != 32: 236 if bpp != 32:
196 # No alpha channel, so the mask cannot be "wrong" (it is the only source of 237 # No alpha channel, so the mask cannot be "wrong" (it is the only source of
197 # transparency information). 238 # transparency information).
198 return iconimage 239 return iconimage if rebuild else True
199 240
200 height /= 2 241 height /= 2
201 xor_size = int(math.ceil(width * bpp / 32.0)) * 4 * height 242 xor_size = BytesPerRowBMP(width, bpp) * height
202 243
203 # num_colors can be 0, implying 2^bpp colors. 244 # num_colors can be 0, implying 2^bpp colors.
204 xor_palette_size = (num_colors or (1 << bpp if bpp < 24 else 0)) * 4 245 xor_palette_size = (num_colors or (1 << bpp if bpp < 24 else 0)) * 4
205 xor_data = iconimage[40 + xor_palette_size : 246 xor_data = iconimage[40 + xor_palette_size :
206 40 + xor_palette_size + xor_size] 247 40 + xor_palette_size + xor_size]
207 248
208 and_data = ComputeANDMaskFromAlpha(xor_data, width, height) 249 if rebuild:
250 and_data = ComputeANDMaskFromAlpha(xor_data, width, height)
209 251
210 # Replace the AND mask in the original icon data. 252 # Replace the AND mask in the original icon data.
211 return iconimage[:40 + xor_palette_size + xor_size] + and_data 253 return iconimage[:40 + xor_palette_size + xor_size] + and_data
254 else:
255 and_data = iconimage[40 + xor_palette_size + xor_size:]
256 return CheckANDMaskAgainstAlpha(xor_data, and_data, width, height)
257
258 def LintIcoFile(infile):
259 """Read an ICO file and check whether it is acceptable.
260
261 This checks for:
262 - Basic structural integrity of the ICO.
263 - Large BMPs that could be converted to PNGs.
264 - 32-bit BMPs with buggy AND masks.
265
266 It will *not* check whether PNG images have been compressed sufficiently.
267
268 Args:
269 infile: The file to read from. Must be a seekable file-like object
270 containing a Microsoft ICO file.
271
272 Returns:
273 A sequence of strings, containing error messages. An empty sequence
274 indicates a good icon.
275 """
276 filename = os.path.basename(infile.name)
277 icondir = infile.read(6)
278 zero, image_type, num_images = struct.unpack('<HHH', icondir)
279 if zero != 0:
280 yield 'Invalid ICO: First word must be 0.'
281 return
282
283 if image_type not in (1, 2):
284 yield 'Invalid ICO: Image type must be 1 or 2.'
285 return
286
287 # Read and unpack each ICONDIRENTRY.
288 icon_dir_entries = []
289 for i in range(num_images):
290 icondirentry = infile.read(16)
291 icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry))
292
293 # Read each icon's bitmap data.
294 current_offset = infile.tell()
295 icon_bitmap_data = []
296 for i in range(num_images):
297 width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i]
298 width = width or 256
299 height = height or 256
300 offset = current_offset
301 icon_data = infile.read(size)
302 if len(icon_data) != size:
303 yield 'Invalid ICO: Unexpected end of file'
304 return
305
306 entry_is_png = IsPng(icon_data)
307 logging.debug('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width,
308 height, size, 'PNG' if entry_is_png else 'BMP')
309
310 if not entry_is_png:
311 if width >= 256 or height >= 256:
312 yield ('Entry #%d is a large image in uncompressed BMP format. It '
313 'should be in PNG format.' % (i + 1))
314
315 if not CheckOrRebuildANDMask(icon_data, rebuild=False):
316 yield ('Entry #%d has a bad mask that will display incorrectly in some '
317 'places in Windows.' % (i + 1))
212 318
213 def OptimizeIcoFile(infile, outfile, optimization_level=None): 319 def OptimizeIcoFile(infile, outfile, optimization_level=None):
214 """Read an ICO file, optimize its PNGs, and write the output to outfile. 320 """Read an ICO file, optimize its PNGs, and write the output to outfile.
215 321
216 Args: 322 Args:
217 infile: The file to read from. Must be a seekable file-like object 323 infile: The file to read from. Must be a seekable file-like object
218 containing a Microsoft ICO file. 324 containing a Microsoft ICO file.
219 outfile: The file to write to. 325 outfile: The file to write to.
220 """ 326 """
221 filename = os.path.basename(infile.name) 327 filename = os.path.basename(infile.name)
(...skipping 30 matching lines...) Expand all
252 # It is a PNG. Crush it. 358 # It is a PNG. Crush it.
253 icon_data = OptimizePng(icon_data, optimization_level=optimization_level) 359 icon_data = OptimizePng(icon_data, optimization_level=optimization_level)
254 elif width >= 256 or height >= 256: 360 elif width >= 256 or height >= 256:
255 # It is a large BMP. Reformat as a PNG, then crush it. 361 # It is a large BMP. Reformat as a PNG, then crush it.
256 # Note: Smaller images are kept uncompressed, for compatibility with 362 # Note: Smaller images are kept uncompressed, for compatibility with
257 # Windows XP. 363 # Windows XP.
258 # TODO(mgiuca): Now that we no longer support XP, we can probably compress 364 # TODO(mgiuca): Now that we no longer support XP, we can probably compress
259 # all of the images. https://crbug.com/663136 365 # all of the images. https://crbug.com/663136
260 icon_data = OptimizeBmp(icon_dir_entries[i], icon_data) 366 icon_data = OptimizeBmp(icon_dir_entries[i], icon_data)
261 else: 367 else:
262 new_icon_data = RebuildANDMask(icon_data) 368 new_icon_data = CheckOrRebuildANDMask(icon_data, rebuild=True)
263 if new_icon_data != icon_data: 369 if new_icon_data != icon_data:
264 logging.info(' * Rebuilt AND mask for this image from alpha channel.') 370 logging.info(' * Rebuilt AND mask for this image from alpha channel.')
265 icon_data = new_icon_data 371 icon_data = new_icon_data
266 372
267 new_size = len(icon_data) 373 new_size = len(icon_data)
268 current_offset += new_size 374 current_offset += new_size
269 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3, 375 icon_dir_entries[i] = (width % 256, height % 256, num_colors, r1, r2, r3,
270 new_size, offset) 376 new_size, offset)
271 icon_bitmap_data.append(icon_data) 377 icon_bitmap_data.append(icon_data)
272 378
273 # Write the data back to outfile. 379 # Write the data back to outfile.
274 outfile.write(icondir) 380 outfile.write(icondir)
275 for icon_dir_entry in icon_dir_entries: 381 for icon_dir_entry in icon_dir_entries:
276 outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry)) 382 outfile.write(struct.pack('<BBBBHHLL', *icon_dir_entry))
277 for icon_bitmap in icon_bitmap_data: 383 for icon_bitmap in icon_bitmap_data:
278 outfile.write(icon_bitmap) 384 outfile.write(icon_bitmap)
OLDNEW
« no previous file with comments | « chrome/app/theme/PRESUBMIT.py ('k') | tools/resources/optimize-ico-files.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698