Index: tools/resources/ico_tools.py |
diff --git a/tools/resources/ico_tools.py b/tools/resources/ico_tools.py |
index 9c3128b56a7e0d422a0ab6565b38f0cc8dfad6d0..db0cda11853c200926737890d2921f6b9ac627ee 100644 |
--- a/tools/resources/ico_tools.py |
+++ b/tools/resources/ico_tools.py |
@@ -70,6 +70,11 @@ def OptimizePng(png_data, optimization_level=None): |
os.unlink(png_filename) |
os.rmdir(temp_dir) |
+def BytesPerRowBMP(width, bpp): |
+ """Computes the number of bytes per row in a Windows BMP image.""" |
+ # width * bpp / 8, rounded up to the nearest multiple of 4. |
+ return int(math.ceil(width * bpp / 32.0)) * 4 |
+ |
def ExportSingleEntry(icon_dir_entry, icon_data, outfile): |
"""Export a single icon dir entry to its own ICO file. |
@@ -171,11 +176,46 @@ def ComputeANDMaskFromAlpha(image_data, width, height): |
and_bytes = ''.join(map(chr, and_bytes)) |
return and_bytes |
-def RebuildANDMask(iconimage): |
- """Rebuild the AND mask in an icon image. |
+def CheckANDMaskAgainstAlpha(xor_data, and_data, width, height): |
+ """Checks whether an AND mask is "good" for 32-bit BGRA image data. |
+ |
+ This checks that the mask is opaque wherever the alpha channel is not fully |
+ transparent. Pixels that violate this condition will show up as black in some |
+ contexts in Windows (http://crbug.com/526622). Also checks the inverse |
+ condition, that the mask is transparent wherever the alpha channel is fully |
+ transparent. While this does not appear to be strictly necessary, it is good |
+ practice for backwards compatibility. |
+ |
+ Returns True if the AND mask is "good", False otherwise. |
+ """ |
+ xor_bytes_per_row = width * 4 |
+ and_bytes_per_row = BytesPerRowBMP(width, 1) |
+ |
+ for y in range(height): |
+ for x in range(width): |
+ alpha = ord(xor_data[y * xor_bytes_per_row + x * 4 + 3]) |
+ mask = bool(ord(and_data[y * and_bytes_per_row + x // 8]) & |
+ (1 << (7 - (x % 8)))) |
+ |
+ if mask: |
+ if alpha > 0: |
+ # mask is transparent, alpha is partially or fully opaque. This pixel |
+ # can show up as black on Windows due to a rendering bug. |
+ return False |
+ else: |
+ if alpha == 0: |
+ # mask is opaque, alpha is transparent. This pixel should be marked as |
+ # transparent in the mask, for legacy reasons. |
+ return False |
+ |
+ return True |
+ |
+def CheckOrRebuildANDMask(iconimage, rebuild=False): |
+ """Checks the AND mask in an icon image for correctness, or rebuilds it. |
GIMP (<=2.8.14) creates a bad AND mask on 32-bit icon images (pixels with <50% |
- opacity are marked as transparent, which end up looking black on Windows). So, |
+ opacity are marked as transparent, which end up looking black on Windows). |
+ With rebuild == False, checks whether the mask is bad. With rebuild == True, |
if this is a 32-bit image, throw the mask away and recompute it from the alpha |
data. (See: https://bugzilla.gnome.org/show_bug.cgi?id=755200) |
@@ -185,7 +225,8 @@ def RebuildANDMask(iconimage): |
is not 32-bit, this is a no-op). |
Returns: |
- An updated |iconimage|, with the AND mask re-computed using |
+ If rebuild == False, a bool indicating whether the mask is "good". If |
+ rebuild == True, an updated |iconimage|, with the AND mask re-computed using |
ComputeANDMaskFromAlpha. |
""" |
# Parse BITMAPINFOHEADER. |
@@ -195,20 +236,85 @@ def RebuildANDMask(iconimage): |
if bpp != 32: |
# No alpha channel, so the mask cannot be "wrong" (it is the only source of |
# transparency information). |
- return iconimage |
+ return iconimage if rebuild else True |
height /= 2 |
- xor_size = int(math.ceil(width * bpp / 32.0)) * 4 * height |
+ xor_size = BytesPerRowBMP(width, bpp) * height |
# num_colors can be 0, implying 2^bpp colors. |
xor_palette_size = (num_colors or (1 << bpp if bpp < 24 else 0)) * 4 |
xor_data = iconimage[40 + xor_palette_size : |
40 + xor_palette_size + xor_size] |
- and_data = ComputeANDMaskFromAlpha(xor_data, width, height) |
+ if rebuild: |
+ and_data = ComputeANDMaskFromAlpha(xor_data, width, height) |
+ |
+ # Replace the AND mask in the original icon data. |
+ return iconimage[:40 + xor_palette_size + xor_size] + and_data |
+ else: |
+ and_data = iconimage[40 + xor_palette_size + xor_size:] |
+ return CheckANDMaskAgainstAlpha(xor_data, and_data, width, height) |
+ |
+def LintIcoFile(infile): |
+ """Read an ICO file and check whether it is acceptable. |
+ |
+ This checks for: |
+ - Basic structural integrity of the ICO. |
+ - Large BMPs that could be converted to PNGs. |
+ - 32-bit BMPs with buggy AND masks. |
+ |
+ It will *not* check whether PNG images have been compressed sufficiently. |
+ |
+ Args: |
+ infile: The file to read from. Must be a seekable file-like object |
+ containing a Microsoft ICO file. |
+ |
+ Returns: |
+ A sequence of strings, containing error messages. An empty sequence |
+ indicates a good icon. |
+ """ |
+ filename = os.path.basename(infile.name) |
+ icondir = infile.read(6) |
+ zero, image_type, num_images = struct.unpack('<HHH', icondir) |
+ if zero != 0: |
+ yield 'Invalid ICO: First word must be 0.' |
+ return |
+ |
+ if image_type not in (1, 2): |
+ yield 'Invalid ICO: Image type must be 1 or 2.' |
+ return |
+ |
+ # Read and unpack each ICONDIRENTRY. |
+ icon_dir_entries = [] |
+ for i in range(num_images): |
+ icondirentry = infile.read(16) |
+ icon_dir_entries.append(struct.unpack('<BBBBHHLL', icondirentry)) |
+ |
+ # Read each icon's bitmap data. |
+ current_offset = infile.tell() |
+ icon_bitmap_data = [] |
+ for i in range(num_images): |
+ width, height, num_colors, r1, r2, r3, size, _ = icon_dir_entries[i] |
+ width = width or 256 |
+ height = height or 256 |
+ offset = current_offset |
+ icon_data = infile.read(size) |
+ if len(icon_data) != size: |
+ yield 'Invalid ICO: Unexpected end of file' |
+ return |
+ |
+ entry_is_png = IsPng(icon_data) |
+ logging.debug('%s entry #%d: %dx%d, %d bytes (%s)', filename, i + 1, width, |
+ height, size, 'PNG' if entry_is_png else 'BMP') |
+ |
+ if not entry_is_png: |
+ if width >= 256 or height >= 256: |
+ yield ('Entry #%d is a large image in uncompressed BMP format. It ' |
+ 'should be in PNG format.' % (i + 1)) |
- # Replace the AND mask in the original icon data. |
- return iconimage[:40 + xor_palette_size + xor_size] + and_data |
+ if not CheckOrRebuildANDMask(icon_data, rebuild=False): |
+ yield ('Entry #%d has a bad mask that will display incorrectly in some ' |
+ 'places in Windows.' % (i + 1)) |
def OptimizeIcoFile(infile, outfile, optimization_level=None): |
"""Read an ICO file, optimize its PNGs, and write the output to outfile. |
@@ -259,7 +365,7 @@ def OptimizeIcoFile(infile, outfile, optimization_level=None): |
# all of the images. https://crbug.com/663136 |
icon_data = OptimizeBmp(icon_dir_entries[i], icon_data) |
else: |
- new_icon_data = RebuildANDMask(icon_data) |
+ new_icon_data = CheckOrRebuildANDMask(icon_data, rebuild=True) |
if new_icon_data != icon_data: |
logging.info(' * Rebuilt AND mask for this image from alpha channel.') |
icon_data = new_icon_data |