set python compatibility 3.6 -> 3.2
This commit is contained in:
9
cli.py
9
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
|
||||
|
||||
3
icnsutil/ArgbImage.py
Executable file → Normal file
3
icnsutil/ArgbImage.py
Executable file → Normal 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
34
icnsutil/IcnsFile.py
Executable file → Normal 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
82
icnsutil/IcnsType.py
Executable file → Normal 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
3
icnsutil/__init__.py
Executable file → Normal 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
27
tests/format-support.py
Executable file → Normal 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
8
tests/test_icnsutil.py
Executable file → Normal 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:
|
||||
|
||||
Reference in New Issue
Block a user