blob: f67c6ae6bbada62ffe9ce6aac73b585823a8a615 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Rebuilds PNG files intended for use as icon images on the Mac. It is
# reasonably robust, but is not designed to be robust against adversarially-
# constructed files.
#
# This is an opinionated script and makes assumptions about the desired
# characteristics of a PNG file for use as a Mac icon. All users of this script
# must verify that those are the correct assumptions for their use case before
# using it.
import argparse
import binascii
import os
import struct
import sys
_PNG_MAGIC = b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a'
_CHUNK_HEADER_STRUCT = struct.Struct('>I4s')
_CHUNK_CRC_STRUCT = struct.Struct('>I')
class FormatError(Exception):
pass
def _process_path(path):
with open(path, 'r+b') as file:
return _process_file(file)
def _process_file(file):
magic = file.read(len(_PNG_MAGIC))
if magic != _PNG_MAGIC:
raise FormatError(file, file.tell(), 'bad magic', magic, _PNG_MAGIC)
chunks = {}
while True:
chunk_header = file.read(_CHUNK_HEADER_STRUCT.size)
(chunk_length, chunk_type) = _CHUNK_HEADER_STRUCT.unpack(chunk_header)
chunk = chunk_header + file.read(chunk_length + _CHUNK_CRC_STRUCT.size)
if chunk_type in chunks:
raise FormatError(file, file.tell(), 'duplicate chunk', chunk_type)
chunks[chunk_type] = chunk
if chunk_type == b'IEND':
break
eof = file.read(1)
if len(eof) != 0:
raise FormatError(file, '\'IEND\' chunk not at end')
ihdr = chunks[b'IHDR'][_CHUNK_HEADER_STRUCT.size:-_CHUNK_CRC_STRUCT.size]
(ihdr_width, ihdr_height, ihdr_bit_depth, ihdr_color_type,
ihdr_compression_method, ihdr_filter_method,
ihdr_interlace_method) = struct.unpack('>2I5b', ihdr)
# The only two color types that have transparency and can be used for icons
# are types 3 (indexed) and 6 (direct RGBA).
if ihdr_color_type not in (3, 6):
raise FormatError(file, 'disallowed color type', ihdr_color_type)
if ihdr_color_type == 3 and b'PLTE' not in chunks:
raise FormatError(file, 'indexed color requires \'PLTE\' chunk')
if ihdr_color_type == 3 and b'tRNS' not in chunks:
raise FormatError(file, 'indexed color requires \'tRNS\' chunk')
if b'IDAT' not in chunks:
raise FormatError(file, 'missing \'IDAT\' chunk')
if b'iCCP' in chunks:
raise FormatError(file, 'disallowed color profile; sRGB only')
if b'sRGB' not in chunks:
# Note that a value of 0 is a perceptual rendering intent (e.g.
# photographs) while a value of 1 is a relative colorimetric rendering
# intent (e.g. icons). Every macOS icon that has an 'sRGB' chunk uses 0
# so that is what is used here. Please forgive us, UX.
#
# Reference:
# http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.sRGB
srgb_chunk_type = struct.pack('>4s', b'sRGB')
srgb_chunk_data = struct.pack('>b', 0) # Perceptual
srgb_chunk_length = struct.pack('>I', len(srgb_chunk_data))
srgb_chunk_crc = struct.pack(
'>I', binascii.crc32(srgb_chunk_type + srgb_chunk_data))
chunks[b'sRGB'] = (
srgb_chunk_length + srgb_chunk_type + srgb_chunk_data +
srgb_chunk_crc)
file.seek(len(_PNG_MAGIC), os.SEEK_SET)
file.write(chunks[b'IHDR'])
file.write(chunks[b'sRGB'])
if ihdr_color_type == 3:
file.write(chunks[b'PLTE'])
file.write(chunks[b'tRNS'])
file.write(chunks[b'IDAT'])
file.write(chunks[b'IEND'])
file.truncate()
def main(args):
parser = argparse.ArgumentParser()
parser.add_argument('paths', nargs='+', metavar='path')
parsed = parser.parse_args(args)
for path in parsed.paths:
print(path)
_process_path(path)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))