set python compatibility 3.6 -> 3.2

This commit is contained in:
relikd
2021-09-27 20:49:28 +02:00
parent c84869607b
commit daad6c6ec8
7 changed files with 93 additions and 73 deletions

9
cli.py
View File

@@ -32,8 +32,9 @@ def cli_compose(args):
if not os.path.splitext(dest)[1]: if not os.path.splitext(dest)[1]:
dest += '.icns' # for the lazy people dest += '.icns' # for the lazy people
if not args.force and os.path.exists(dest): if not args.force and os.path.exists(dest):
print(f'File "{dest}" already exists. Force overwrite with -f.', print(
file=stderr) 'File "{}" already exists. Force overwrite with -f.'.format(dest),
file=stderr)
return 1 return 1
img = icnsutil.IcnsFile() img = icnsutil.IcnsFile()
for x in args.source: for x in args.source:
@@ -126,13 +127,13 @@ def main():
'source', nargs='+', type=PathExist('f'), metavar='src', 'source', nargs='+', type=PathExist('f'), metavar='src',
help='One or more media files: png, argb, plist, icns.') help='One or more media files: png, argb, plist, icns.')
cmd.set_defaults(func=cli_compose) cmd.set_defaults(func=cli_compose)
cmd.epilog = f''' cmd.epilog = '''
Notes: Notes:
- TOC is optional but only a few bytes long (8b per media entry). - TOC is optional but only a few bytes long (8b per media entry).
- Icon dimensions are read directly from file. - Icon dimensions are read directly from file.
- Filename suffix "@2x.png" or "@2x.jp2" sets the retina flag. - Filename suffix "@2x.png" or "@2x.jp2" sets the retina flag.
- Use one of these suffixes to automatically assign icns files: - Use one of these suffixes to automatically assign icns files:
{', '.join(f'{x.name.lower()}.icns' for x in icnsutil.IcnsType.Role)} template, selected, dark
''' '''
# Print # Print

3
icnsutil/ArgbImage.py Executable file → Normal file
View File

@@ -140,4 +140,5 @@ class ArgbImage:
def __repr__(self): def __repr__(self):
typ = ['', 'Mono', 'Mono with Mask', 'RGB', 'RGBA'][self.channels] typ = ['', 'Mono', 'Mono with Mask', 'RGB', 'RGBA'][self.channels]
return f'<{type(self).__name__}: {self.size[0]}x{self.size[1]} {typ}>' return '<{}: {}x{} {}>'.format(
type(self).__name__, self.size[0], self.size[1], typ)

34
icnsutil/IcnsFile.py Executable file → Normal file
View File

@@ -23,7 +23,7 @@ class IcnsFile:
try: try:
iType = IcnsType.get(key) iType = IcnsType.get(key)
except NotImplementedError: except NotImplementedError:
yield f'Unsupported icns type: {key}' yield 'Unsupported icns type: {}'.format(key)
continue continue
ext = RawData.determine_file_ext(data) ext = RawData.determine_file_ext(data)
@@ -41,7 +41,7 @@ class IcnsFile:
# Check whether uncompressed size is equal to expected maxsize # Check whether uncompressed size is equal to expected maxsize
if key == 'it32' and data[:4] != b'\x00\x00\x00\x00': if key == 'it32' and data[:4] != b'\x00\x00\x00\x00':
# TODO: check whether other it32 headers exist # TODO: check whether other it32 headers exist
yield f'Unexpected it32 data header: {data[:4]}' yield 'Unexpected it32 data header: {}'.format(data[:4])
data = iType.decompress(data, ext) # ignores non-compressable data = iType.decompress(data, ext) # ignores non-compressable
# Check expected uncompressed maxsize # Check expected uncompressed maxsize
@@ -66,13 +66,15 @@ class IcnsFile:
if not img or not mask: if not img or not mask:
if not img: if not img:
img, mask = mask, img img, mask = mask, img
yield f'Missing key pair: {mask} found, {img} missing.' yield 'Missing key pair: {} found, {} missing.'.format(
mask, img)
# Check duplicate image dimensions # Check duplicate image dimensions
for x, y in [('is32', 'icp4'), ('il32', 'icp5'), ('it32', 'ic07'), for x, y in [('is32', 'icp4'), ('il32', 'icp5'), ('it32', 'ic07'),
('ic04', 'icp4'), ('ic05', 'icp5')]: ('ic04', 'icp4'), ('ic05', 'icp5')]:
if x in all_keys and y in all_keys: if x in all_keys and y in all_keys:
yield f'Redundant keys: {x} and {y} have identical size.' yield 'Redundant keys: {} and {} have identical size.'.format(
x, y)
@staticmethod @staticmethod
def description(fname, *, verbose=False, indent=0): def description(fname, *, verbose=False, indent=0):
@@ -88,15 +90,15 @@ class IcnsFile:
# actually, icns length should be -8 (artificially appended header) # actually, icns length should be -8 (artificially appended header)
size = len(data) size = len(data)
txt += ' ' * indent txt += ' ' * indent
txt += f'{key}: {size} bytes' txt += '{}: {} bytes'.format(key, size)
if verbose: if verbose:
txt += f', offset: {offset}' txt += ', offset: {}'.format(offset)
offset += size + 8 offset += size + 8
if key == 'name': if key == 'name':
txt += f', value: "{data.decode("utf-8")}"\n' txt += ', value: "{}"\n'.format(data.decode('utf-8'))
continue continue
if key == 'icnV': if key == 'icnV':
txt += f', value: {struct.unpack(">f", data)[0]}\n' txt += ', value: {}\n'.format(struct.unpack('>f', data)[0])
continue continue
ext = RawData.determine_file_ext(data) ext = RawData.determine_file_ext(data)
try: try:
@@ -104,9 +106,9 @@ class IcnsFile:
if not ext: if not ext:
ext = iType.types[-1] ext = iType.types[-1]
desc = iType.filename(size_only=True) desc = iType.filename(size_only=True)
txt += f', {ext or "binary"}: {desc}\n' txt += ', {}: {}\n'.format(ext or 'binary', desc)
except NotImplementedError: except NotImplementedError:
txt += f': UNKNOWN TYPE: {ext or data[:6]}\n' txt += ': UNKNOWN TYPE: {}\n'.format(ext or data[:6])
return txt return txt
def __init__(self, file=None): def __init__(self, file=None):
@@ -139,7 +141,8 @@ class IcnsFile:
key = IcnsType.guess(data, file).key key = IcnsType.guess(data, file).key
# Check if type is unique # Check if type is unique
if not force and key in self.media.keys(): if not force and key in self.media.keys():
raise KeyError(f'Image with identical key "{key}". File: {file}') raise KeyError(
'Image with identical key "{}". File: {}'.format(key, file))
self.media[key] = data self.media[key] = data
def write(self, fname, *, toc=True): def write(self, fname, *, toc=True):
@@ -171,7 +174,7 @@ class IcnsFile:
outdir = (self.infile or 'in-memory.icns') + '.export' outdir = (self.infile or 'in-memory.icns') + '.export'
os.makedirs(outdir, exist_ok=True) os.makedirs(outdir, exist_ok=True)
elif not os.path.isdir(outdir): elif not os.path.isdir(outdir):
raise NotADirectoryError(f'"{outdir}" is not a directory. Abort.') raise OSError('"{}" is not a directory. Abort.'.format(outdir))
exported_files = {'_': self.infile} exported_files = {'_': self.infile}
keys = list(self.media.keys()) keys = list(self.media.keys())
@@ -255,7 +258,7 @@ class IcnsFile:
if allowed_ext and ext not in allowed_ext: if allowed_ext and ext not in allowed_ext:
return None return None
fname = os.path.join(outdir, f'{fname}.{ext}') fname = os.path.join(outdir, fname + '.' + ext)
with open(fname, 'wb') as fp: with open(fname, 'wb') as fp:
fp.write(data) fp.write(data)
return fname return fname
@@ -278,8 +281,9 @@ class IcnsFile:
def __repr__(self): def __repr__(self):
lst = ', '.join(str(k) for k in self.media.keys()) lst = ', '.join(str(k) for k in self.media.keys())
return f'<{type(self).__name__}: file={self.infile}, [{lst}]>' return '<{}: file={}, [{}]>'.format(
type(self).__name__, self.infile, lst)
def __str__(self): def __str__(self):
return f'File: {self.infile or "-mem-"}\n' \ return 'File: ' + (self.infile or '-mem-') + '\n' \
+ IcnsFile._description(self.media.items(), indent=2) + IcnsFile._description(self.media.items(), indent=2)

82
icnsutil/IcnsType.py Executable file → Normal file
View File

@@ -4,17 +4,10 @@ Namespace for the ICNS format.
@see https://en.wikipedia.org/wiki/Apple_Icon_Image_format @see https://en.wikipedia.org/wiki/Apple_Icon_Image_format
''' '''
import os # path import os # path
from enum import Enum # IcnsType.Role
import RawData import RawData
import PackBytes import PackBytes
class Role(Enum):
DARK = b'\xFD\xD9\x2F\xA8'
TEMPLATE = 'sbtp'
SELECTED = 'slct'
class Media: class Media:
__slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability', __slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability',
'desc', 'compressable', 'retina', 'maxsize'] 'desc', 'compressable', 'retina', 'maxsize']
@@ -28,7 +21,7 @@ class Media:
self.desc = desc self.desc = desc
# computed properties # computed properties
self.compressable = self.is_type('argb') or self.is_type('rgb') self.compressable = self.is_type('argb') or self.is_type('rgb')
self.retina = ('@2x' in self.desc) if self.is_type('png') else None self.retina = '@2x' in self.desc
if self.is_type('rgb'): if self.is_type('rgb'):
ch = 3 ch = 3
bits = 8 bits = 8
@@ -80,12 +73,12 @@ class Media:
return self.key + '-a' return self.key + '-a'
elif self.key in ['SB24', 'icsB']: elif self.key in ['SB24', 'icsB']:
return self.key + '-b' return self.key + '-b'
return f'{self.key}' # dont return directy, may be b''-str return str(self.key) # dont return directy, may be b''-str
else: else:
if self.is_type('icns'): if self.is_type('icns'):
return Role(self.key).name.lower() return self.desc
if not self.size: if not self.size:
return f'{self.key}' # dont return directy, may be b''-str return str(self.key) # dont return directy, may be b''-str
w, h = self.size w, h = self.size
suffix = '' suffix = ''
if self.retina: if self.retina:
@@ -97,24 +90,26 @@ class Media:
suffix += '-mono' suffix += '-mono'
else: else:
if self.desc in ['icon', 'iconmask']: if self.desc in ['icon', 'iconmask']:
suffix += f'-icon{self.bits}b' suffix += '-icon{}b'.format(self.bits)
if self.desc in ['mask', 'iconmask']: if self.desc in ['mask', 'iconmask']:
suffix += f'-mask{self.bits}b' suffix += '-mask{}b'.format(self.bits)
return f'{w}x{h}{suffix}' return '{}x{}{}'.format(w, h, suffix)
def __repr__(self): def __repr__(self):
return '<{}: {}, {}.{}>'.format(type(self).__name__, self.key, return '<{}: {}, {}.{}>'.format(
self.filename(), self.types[0]) type(self).__name__, self.key, self.filename(), self.types[0])
def __str__(self): def __str__(self):
T = '' T = ''
if self.size: if self.size:
T += '{}x{}, '.format(*self.size) T += '{}x{}, '.format(*self.size)
if self.maxsize: if self.maxsize:
T += f'{self.channels}ch@{self.bits}-bit={self.maxsize}, ' T += '{}ch@{}-bit={}, '.format(
self.channels, self.bits, self.maxsize)
if self.desc: if self.desc:
T += f'{self.desc}, ' T += self.desc + ', '
return f'{self.key}: {T}macOS {self.availability or "?"}+' return '{}: {T}macOS {}+'.format(
self.key, T, self.availability or '?')
_TYPES = {x.key: x for x in ( _TYPES = {x.key: x for x in (
@@ -152,16 +147,16 @@ _TYPES = {x.key: x for x in (
Media('ic12', ['png', 'jp2'], 64, os=10.8, desc='32x32@2x'), Media('ic12', ['png', 'jp2'], 64, os=10.8, desc='32x32@2x'),
Media('ic13', ['png', 'jp2'], 256, os=10.8, desc='128x128@2x'), Media('ic13', ['png', 'jp2'], 256, os=10.8, desc='128x128@2x'),
Media('ic14', ['png', 'jp2'], 512, os=10.8, desc='256x256@2x'), Media('ic14', ['png', 'jp2'], 512, os=10.8, desc='256x256@2x'),
Media('ic04', 'argb', 16, os=11.0), Media('ic04', ['argb', 'png', 'jp2'], 16, os=11.0), # ARGB is macOS 11+
Media('ic05', 'argb', 32, os=11.0), Media('ic05', ['argb', 'png', 'jp2'], 32, os=11.0),
Media('icsb', 'argb', 18, os=11.0), Media('icsb', ['argb', 'png', 'jp2'], 18, os=11.0),
Media('icsB', ['png', 'jp2'], 36, desc='18x18@2x'), Media('icsB', ['png', 'jp2'], 36, desc='18x18@2x'),
Media('sb24', ['png', 'jp2'], 24), Media('sb24', ['png', 'jp2'], 24),
Media('SB24', ['png', 'jp2'], 48, desc='24x24@2x'), Media('SB24', ['png', 'jp2'], 48, desc='24x24@2x'),
# ICNS media files # ICNS media files
Media(Role.TEMPLATE.value, 'icns', desc='"template" icns'), Media('sbtp', 'icns', desc='template'),
Media(Role.SELECTED.value, 'icns', desc='"selected" icns'), Media('slct', 'icns', desc='selected'),
Media(Role.DARK.value, 'icns', os=10.14, desc='"dark" icns'), Media(b'\xFD\xD9\x2F\xA8', 'icns', os=10.14, desc='dark'),
# Meta types: # Meta types:
Media('TOC ', 'bin', os=10.7, desc='Table of Contents'), Media('TOC ', 'bin', os=10.7, desc='Table of Contents'),
Media('icnV', 'bin', desc='4-byte Icon Composer.app bundle version'), Media('icnV', 'bin', desc='4-byte Icon Composer.app bundle version'),
@@ -206,12 +201,12 @@ def enum_png_convertable(available_keys):
yield img.key, mask_key yield img.key, mask_key
def get(key): # support for IcnsType[key] def get(key):
try: try:
return _TYPES[key] return _TYPES[key]
except KeyError: except KeyError:
pass pass
raise NotImplementedError(f'Unsupported icns type "{key}"') raise NotImplementedError('Unsupported icns type "{}"'.format(key))
def match_maxsize(maxsize, typ): def match_maxsize(maxsize, typ):
@@ -236,18 +231,16 @@ def guess(data, filename=None):
return _TYPES[bname] return _TYPES[bname]
ext = RawData.determine_file_ext(data) ext = RawData.determine_file_ext(data)
# Icns specific names
if ext == 'icns' and filename:
for candidate in Role:
if filename.endswith(f'{candidate.name.lower()}.icns'):
return _TYPES[candidate.value]
# if not found, fallback and output all options
# Guess by image size and retina flag # Guess by image size and retina flag
size = RawData.determine_image_size(data, ext) # None for non-image types size = RawData.determine_image_size(data, ext) # None for non-image types
retina = None retina = bname.lower().endswith('@2x') if filename else False
if ext in ['png', 'jp2']: # Icns specific names
retina = bname.lower().endswith('@2x') if filename else False desc = None
if ext == 'icns' and filename:
for candidate in ['template', 'selected', 'dark']:
if filename.endswith(candidate + '.icns'):
desc = candidate
choices = [] choices = []
for x in _TYPES.values(): for x in _TYPES.values():
@@ -255,12 +248,27 @@ def guess(data, filename=None):
continue continue
if ext and not x.is_type(ext): if ext and not x.is_type(ext):
continue continue
if retina is not None and retina != x.retina: if retina != x.retina: # png + jp2
continue
if desc and desc != x.desc: # icns only
continue continue
choices.append(x) choices.append(x)
if len(choices) == 1: if len(choices) == 1:
return choices[0] return choices[0]
# Try get most favorable type (sort order of types)
best_i = 99
best_choice = []
for x in choices:
i = x.types.index(ext)
if i < best_i:
best_i = i
best_choice = [x]
elif i == best_i:
best_choice.append(x)
if len(best_choice) == 1:
return best_choice[0]
# Else
raise ValueError('Could not determine type one of {} -- {}'.format( raise ValueError('Could not determine type one of {} -- {}'.format(
[x.key for x in choices], [x.key for x in choices],
{'type': ext, 'size': size, 'retina': retina})) {'type': ext, 'size': size, 'retina': retina}))

3
icnsutil/__init__.py Executable file → Normal file
View File

@@ -1,4 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
'''
A fully-featured python library to handle reading and writing icns files.
'''
__version__ = '1.0' __version__ = '1.0'
import sys import sys

27
tests/format-support.py Executable file → Normal file
View File

@@ -50,13 +50,13 @@ def generate_raw_rgb():
os.makedirs('format-support-raw', exist_ok=True) os.makedirs('format-support-raw', exist_ok=True)
for s in INFO.keys(): for s in INFO.keys():
print(f'generate {s}x{s}.argb') print('generate {}x{}.argb'.format(s, s))
argb_data = testpattern(s, s, ch=4) argb_data = testpattern(s, s, ch=4)
with open(f'format-support-raw/{s}x{s}.argb', 'wb') as fp: with open('format-support-raw/{}x{}.argb'.format(s, s), 'wb') as fp:
fp.write(argb_data) fp.write(argb_data)
print(f'generate {s}x{s}.rgb') print('generate {}x{}.rgb'.format(s, s))
rgb_data = testpattern(s, s, ch=3) rgb_data = testpattern(s, s, ch=3)
with open(f'format-support-raw/{s}x{s}.rgb', 'wb') as fp: with open('format-support-raw/{}x{}.rgb'.format(s, s), 'wb') as fp:
fp.write(rgb_data) fp.write(rgb_data)
@@ -64,18 +64,18 @@ def generate_icns():
os.makedirs('format-support-icns', exist_ok=True) os.makedirs('format-support-icns', exist_ok=True)
with zipfile.ZipFile('format-support-raw.zip') as Zip: with zipfile.ZipFile('format-support-raw.zip') as Zip:
for s, keys in INFO.items(): for s, keys in INFO.items():
print(f'generate icns for {s}x{s}') print('generate icns for {}x{}'.format(s, s))
for key in keys: for key in keys:
# JPEG 2000, PNG, and ARGB # JPEG 2000, PNG, and ARGB
for ext in ['jp2', 'png', 'argb']: for ext in ['jp2', 'png', 'argb']:
img = IcnsFile() img = IcnsFile()
with Zip.open(f'{s}x{s}.{ext}') as f: with Zip.open('{}x{}.{}'.format(s, s, ext)) as f:
img.add_media(key, data=f.read()) img.add_media(key, data=f.read())
img.write(f'format-support-icns/{s}-{key}-{ext}.icns', img.write('format-support-icns/{}-{}-{}.icns'.format(
toc=False) s, key, ext), toc=False)
# RGB + mask # RGB + mask
img = IcnsFile() img = IcnsFile()
with Zip.open(f'{s}x{s}.rgb') as f: with Zip.open('{}x{}.rgb'.format(s, s)) as f:
data = f.read() data = f.read()
if key == 'it32': if key == 'it32':
data = b'\x00\x00\x00\x00' + data data = b'\x00\x00\x00\x00' + data
@@ -84,14 +84,15 @@ def generate_icns():
img.add_media('l8mk', data=b'\xFF' * 1024) img.add_media('l8mk', data=b'\xFF' * 1024)
img.add_media('h8mk', data=b'\xFF' * 2304) img.add_media('h8mk', data=b'\xFF' * 2304)
img.add_media('t8mk', data=b'\xFF' * 16384) img.add_media('t8mk', data=b'\xFF' * 16384)
img.write(f'format-support-icns/{s}-{key}-rgb.icns', toc=False) img.write('format-support-icns/{}-{}-rgb.icns'.format(s, key),
toc=False)
def generate_random_it32_header(): def generate_random_it32_header():
print(f'testing random it32 header') print('testing random it32 header')
os.makedirs('format-support-it32', exist_ok=True) os.makedirs('format-support-it32', exist_ok=True)
with zipfile.ZipFile('format-support-raw.zip') as Zip: with zipfile.ZipFile('format-support-raw.zip') as Zip:
with Zip.open(f'128x128.rgb') as f: with Zip.open('128x128.rgb') as f:
data = f.read() data = f.read()
def random_header(): def random_header():
@@ -102,7 +103,7 @@ def generate_random_it32_header():
img = IcnsFile() img = IcnsFile()
img.add_media('it32', data=random_header() + data) img.add_media('it32', data=random_header() + data)
img.add_media('t8mk', data=b'\xFF' * 16384) img.add_media('t8mk', data=b'\xFF' * 16384)
img.write(f'format-support-it32/{i}.icns', toc=False) img.write('format-support-it32/{}.icns'.format(i), toc=False)
if __name__ == '__main__': if __name__ == '__main__':

8
tests/test_icnsutil.py Executable file → Normal file
View File

@@ -207,8 +207,10 @@ class TestIcnsType(unittest.TestCase):
('icp6', 'png', '', (64, 64), None), ('icp6', 'png', '', (64, 64), None),
('ic14', 'png', '@2x', (512, 512), None), ('ic14', 'png', '@2x', (512, 512), None),
('info', 'plist', '', None, None), ('info', 'plist', '', None, None),
] + [(x.value, 'icns', '', None, None) ('sbtp', 'icns', 'template', None, None),
for x in IcnsType.Role]: ('slct', 'icns', 'selected', None, None),
(b'\xFD\xD9\x2F\xA8', 'icns', 'dark', None, None),
]:
m = IcnsType.get(key) m = IcnsType.get(key)
self.assertEqual(m.size, size) self.assertEqual(m.size, size)
self.assertTrue(m.is_type(ext)) self.assertTrue(m.is_type(ext))
@@ -227,7 +229,7 @@ class TestIcnsType(unittest.TestCase):
x = IcnsType.guess(fp.read(), 'rgb.icns.argb') x = IcnsType.guess(fp.read(), 'rgb.icns.argb')
self.assertTrue(x.is_type('argb')) self.assertTrue(x.is_type('argb'))
self.assertEqual(x.size, (16, 16)) self.assertEqual(x.size, (16, 16))
self.assertEqual(x.retina, None) self.assertEqual(x.retina, False)
self.assertEqual(x.channels, 4) self.assertEqual(x.channels, 4)
self.assertEqual(x.compressable, True) self.assertEqual(x.compressable, True)
with open('256x256.jp2', 'rb') as fp: with open('256x256.jp2', 'rb') as fp: