From daad6c6ec865bbf998e76a0a2cc6e7a30b321acc Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 27 Sep 2021 20:49:28 +0200 Subject: [PATCH] set python compatibility 3.6 -> 3.2 --- cli.py | 9 +++-- icnsutil/ArgbImage.py | 3 +- icnsutil/IcnsFile.py | 34 +++++++++-------- icnsutil/IcnsType.py | 82 ++++++++++++++++++++++------------------- icnsutil/__init__.py | 3 ++ tests/format-support.py | 27 +++++++------- tests/test_icnsutil.py | 8 ++-- 7 files changed, 93 insertions(+), 73 deletions(-) mode change 100755 => 100644 icnsutil/ArgbImage.py mode change 100755 => 100644 icnsutil/IcnsFile.py mode change 100755 => 100644 icnsutil/IcnsType.py mode change 100755 => 100644 icnsutil/__init__.py mode change 100755 => 100644 tests/format-support.py mode change 100755 => 100644 tests/test_icnsutil.py diff --git a/cli.py b/cli.py index 22b8570..eacdbfd 100755 --- a/cli.py +++ b/cli.py @@ -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 diff --git a/icnsutil/ArgbImage.py b/icnsutil/ArgbImage.py old mode 100755 new mode 100644 index a1e8097..1ad74c5 --- a/icnsutil/ArgbImage.py +++ b/icnsutil/ArgbImage.py @@ -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) diff --git a/icnsutil/IcnsFile.py b/icnsutil/IcnsFile.py old mode 100755 new mode 100644 index 61b3526..52be60e --- a/icnsutil/IcnsFile.py +++ b/icnsutil/IcnsFile.py @@ -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) diff --git a/icnsutil/IcnsType.py b/icnsutil/IcnsType.py old mode 100755 new mode 100644 index 3deb1c5..c9d6bb4 --- a/icnsutil/IcnsType.py +++ b/icnsutil/IcnsType.py @@ -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})) diff --git a/icnsutil/__init__.py b/icnsutil/__init__.py old mode 100755 new mode 100644 index 1ad7e1e..2098ed3 --- a/icnsutil/__init__.py +++ b/icnsutil/__init__.py @@ -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 diff --git a/tests/format-support.py b/tests/format-support.py old mode 100755 new mode 100644 index 14c29d0..5f6462f --- a/tests/format-support.py +++ b/tests/format-support.py @@ -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__': diff --git a/tests/test_icnsutil.py b/tests/test_icnsutil.py old mode 100755 new mode 100644 index 6e72525..ecd9bb4 --- a/tests/test_icnsutil.py +++ b/tests/test_icnsutil.py @@ -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: