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]:
dest += '.icns' # for the lazy people
if not args.force and os.path.exists(dest):
print(f'File "{dest}" already exists. Force overwrite with -f.',
file=stderr)
print(
'File "{}" already exists. Force overwrite with -f.'.format(dest),
file=stderr)
return 1
img = icnsutil.IcnsFile()
for x in args.source:
@@ -126,13 +127,13 @@ def main():
'source', nargs='+', type=PathExist('f'), metavar='src',
help='One or more media files: png, argb, plist, icns.')
cmd.set_defaults(func=cli_compose)
cmd.epilog = f'''
cmd.epilog = '''
Notes:
- TOC is optional but only a few bytes long (8b per media entry).
- Icon dimensions are read directly from file.
- Filename suffix "@2x.png" or "@2x.jp2" sets the retina flag.
- 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

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

@@ -140,4 +140,5 @@ class ArgbImage:
def __repr__(self):
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:
iType = IcnsType.get(key)
except NotImplementedError:
yield f'Unsupported icns type: {key}'
yield 'Unsupported icns type: {}'.format(key)
continue
ext = RawData.determine_file_ext(data)
@@ -41,7 +41,7 @@ class IcnsFile:
# Check whether uncompressed size is equal to expected maxsize
if key == 'it32' and data[:4] != b'\x00\x00\x00\x00':
# 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
# Check expected uncompressed maxsize
@@ -66,13 +66,15 @@ class IcnsFile:
if not img or not mask:
if not 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
for x, y in [('is32', 'icp4'), ('il32', 'icp5'), ('it32', 'ic07'),
('ic04', 'icp4'), ('ic05', 'icp5')]:
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
def description(fname, *, verbose=False, indent=0):
@@ -88,15 +90,15 @@ class IcnsFile:
# actually, icns length should be -8 (artificially appended header)
size = len(data)
txt += ' ' * indent
txt += f'{key}: {size} bytes'
txt += '{}: {} bytes'.format(key, size)
if verbose:
txt += f', offset: {offset}'
txt += ', offset: {}'.format(offset)
offset += size + 8
if key == 'name':
txt += f', value: "{data.decode("utf-8")}"\n'
txt += ', value: "{}"\n'.format(data.decode('utf-8'))
continue
if key == 'icnV':
txt += f', value: {struct.unpack(">f", data)[0]}\n'
txt += ', value: {}\n'.format(struct.unpack('>f', data)[0])
continue
ext = RawData.determine_file_ext(data)
try:
@@ -104,9 +106,9 @@ class IcnsFile:
if not ext:
ext = iType.types[-1]
desc = iType.filename(size_only=True)
txt += f', {ext or "binary"}: {desc}\n'
txt += ', {}: {}\n'.format(ext or 'binary', desc)
except NotImplementedError:
txt += f': UNKNOWN TYPE: {ext or data[:6]}\n'
txt += ': UNKNOWN TYPE: {}\n'.format(ext or data[:6])
return txt
def __init__(self, file=None):
@@ -139,7 +141,8 @@ class IcnsFile:
key = IcnsType.guess(data, file).key
# Check if type is unique
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
def write(self, fname, *, toc=True):
@@ -171,7 +174,7 @@ class IcnsFile:
outdir = (self.infile or 'in-memory.icns') + '.export'
os.makedirs(outdir, exist_ok=True)
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}
keys = list(self.media.keys())
@@ -255,7 +258,7 @@ class IcnsFile:
if allowed_ext and ext not in allowed_ext:
return None
fname = os.path.join(outdir, f'{fname}.{ext}')
fname = os.path.join(outdir, fname + '.' + ext)
with open(fname, 'wb') as fp:
fp.write(data)
return fname
@@ -278,8 +281,9 @@ class IcnsFile:
def __repr__(self):
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):
return f'File: {self.infile or "-mem-"}\n' \
return 'File: ' + (self.infile or '-mem-') + '\n' \
+ 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
'''
import os # path
from enum import Enum # IcnsType.Role
import RawData
import PackBytes
class Role(Enum):
DARK = b'\xFD\xD9\x2F\xA8'
TEMPLATE = 'sbtp'
SELECTED = 'slct'
class Media:
__slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability',
'desc', 'compressable', 'retina', 'maxsize']
@@ -28,7 +21,7 @@ class Media:
self.desc = desc
# computed properties
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'):
ch = 3
bits = 8
@@ -80,12 +73,12 @@ class Media:
return self.key + '-a'
elif self.key in ['SB24', 'icsB']:
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:
if self.is_type('icns'):
return Role(self.key).name.lower()
return self.desc
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
suffix = ''
if self.retina:
@@ -97,24 +90,26 @@ class Media:
suffix += '-mono'
else:
if self.desc in ['icon', 'iconmask']:
suffix += f'-icon{self.bits}b'
suffix += '-icon{}b'.format(self.bits)
if self.desc in ['mask', 'iconmask']:
suffix += f'-mask{self.bits}b'
return f'{w}x{h}{suffix}'
suffix += '-mask{}b'.format(self.bits)
return '{}x{}{}'.format(w, h, suffix)
def __repr__(self):
return '<{}: {}, {}.{}>'.format(type(self).__name__, self.key,
self.filename(), self.types[0])
return '<{}: {}, {}.{}>'.format(
type(self).__name__, self.key, self.filename(), self.types[0])
def __str__(self):
T = ''
if self.size:
T += '{}x{}, '.format(*self.size)
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:
T += f'{self.desc}, '
return f'{self.key}: {T}macOS {self.availability or "?"}+'
T += self.desc + ', '
return '{}: {T}macOS {}+'.format(
self.key, T, self.availability or '?')
_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('ic13', ['png', 'jp2'], 256, os=10.8, desc='128x128@2x'),
Media('ic14', ['png', 'jp2'], 512, os=10.8, desc='256x256@2x'),
Media('ic04', 'argb', 16, os=11.0),
Media('ic05', 'argb', 32, os=11.0),
Media('icsb', 'argb', 18, os=11.0),
Media('ic04', ['argb', 'png', 'jp2'], 16, os=11.0), # ARGB is macOS 11+
Media('ic05', ['argb', 'png', 'jp2'], 32, os=11.0),
Media('icsb', ['argb', 'png', 'jp2'], 18, os=11.0),
Media('icsB', ['png', 'jp2'], 36, desc='18x18@2x'),
Media('sb24', ['png', 'jp2'], 24),
Media('SB24', ['png', 'jp2'], 48, desc='24x24@2x'),
# ICNS media files
Media(Role.TEMPLATE.value, 'icns', desc='"template" icns'),
Media(Role.SELECTED.value, 'icns', desc='"selected" icns'),
Media(Role.DARK.value, 'icns', os=10.14, desc='"dark" icns'),
Media('sbtp', 'icns', desc='template'),
Media('slct', 'icns', desc='selected'),
Media(b'\xFD\xD9\x2F\xA8', 'icns', os=10.14, desc='dark'),
# Meta types:
Media('TOC ', 'bin', os=10.7, desc='Table of Contents'),
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
def get(key): # support for IcnsType[key]
def get(key):
try:
return _TYPES[key]
except KeyError:
pass
raise NotImplementedError(f'Unsupported icns type "{key}"')
raise NotImplementedError('Unsupported icns type "{}"'.format(key))
def match_maxsize(maxsize, typ):
@@ -236,18 +231,16 @@ def guess(data, filename=None):
return _TYPES[bname]
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
size = RawData.determine_image_size(data, ext) # None for non-image types
retina = None
if ext in ['png', 'jp2']:
retina = bname.lower().endswith('@2x') if filename else False
retina = bname.lower().endswith('@2x') if filename else False
# Icns specific names
desc = None
if ext == 'icns' and filename:
for candidate in ['template', 'selected', 'dark']:
if filename.endswith(candidate + '.icns'):
desc = candidate
choices = []
for x in _TYPES.values():
@@ -255,12 +248,27 @@ def guess(data, filename=None):
continue
if ext and not x.is_type(ext):
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
choices.append(x)
if len(choices) == 1:
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(
[x.key for x in choices],
{'type': ext, 'size': size, 'retina': retina}))

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

@@ -1,4 +1,7 @@
#!/usr/bin/env python3
'''
A fully-featured python library to handle reading and writing icns files.
'''
__version__ = '1.0'
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)
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)
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)
print(f'generate {s}x{s}.rgb')
print('generate {}x{}.rgb'.format(s, s))
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)
@@ -64,18 +64,18 @@ def generate_icns():
os.makedirs('format-support-icns', exist_ok=True)
with zipfile.ZipFile('format-support-raw.zip') as Zip:
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:
# JPEG 2000, PNG, and ARGB
for ext in ['jp2', 'png', 'argb']:
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.write(f'format-support-icns/{s}-{key}-{ext}.icns',
toc=False)
img.write('format-support-icns/{}-{}-{}.icns'.format(
s, key, ext), toc=False)
# RGB + mask
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()
if key == 'it32':
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('h8mk', data=b'\xFF' * 2304)
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():
print(f'testing random it32 header')
print('testing random it32 header')
os.makedirs('format-support-it32', exist_ok=True)
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()
def random_header():
@@ -102,7 +103,7 @@ def generate_random_it32_header():
img = IcnsFile()
img.add_media('it32', data=random_header() + data)
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__':

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

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