| OLD | NEW |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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) |
| OLD | NEW |