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 |