diff --git a/.gitignore b/.gitignore index 7933a00..2c8f1d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,26 @@ /*.txt /tests/format-support-*/ /tests/fixtures/tmp_* + +__pycache__/ +*.py[cod] + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST diff --git a/Makefile b/Makefile index 4be726b..52dc488 100644 --- a/Makefile +++ b/Makefile @@ -1,26 +1,44 @@ -.PHONY: help test sys-icons-print sys-icons-test - +.PHONY: help help: - @echo 'Available commands: test, sys-icons-print, sys-icons-test' + @echo 'commands:' + @echo ' install, uninstall, test, dist, sys-icons-print, sys-icons-test' +.PHONY: install +install: + [ -z "$${VIRTUAL_ENV}" ] \ + && python3 -m pip install -e . --user \ + || python3 -m pip install -e . + +.PHONY: uninstall +uninstall: + python3 -m pip uninstall icnsutil + rm -rf ./*.egg-info/ + +.PHONY: test test: python3 tests/test_icnsutil.py -_listofsystemicns.txt: +.PHONY: dist +dist: + @python3 setup.py sdist --formats=tar bdist_wheel \ + || echo '-> you can not do this inside a virtual environment.' + @echo + rm -rf ./*.egg-info/ ./build/ MANIFEST + +_icns_list.txt: @echo 'Generate list of system icns files...' - find /Applications -type f -name '*.icns' > _listofsystemicns.txt || echo - find /Users -type f -name '*.icns' >> _listofsystemicns.txt || echo - find /Library -type f -name '*.icns' >> _listofsystemicns.txt || echo - find /System -not \( -path '/System/Volumes' -prune \) \ + -find /Applications -type f -name '*.icns' > _icns_list.txt + -find /Users -type f -name '*.icns' >> _icns_list.txt + -find /Library -type f -name '*.icns' >> _icns_list.txt + -find /System -not \( -path '/System/Volumes' -prune \) \ -not \( -path '/System/Library/Templates' -prune \) \ - -type f -name '*.icns' >> _listofsystemicns.txt || echo 'Done.' + -type f -name '*.icns' >> _icns_list.txt + @echo 'Done.' -sys-icons-print: _listofsystemicns.txt - @while read fname; do \ - ./cli.py print "$${fname}"; \ - done < _listofsystemicns.txt +.PHONY: sys-icons-print +sys-icons-print: _icns_list.txt + @cat _icns_list.txt | python3 -m icnsutil print - -sys-icons-test: _listofsystemicns.txt - @while read fname; do \ - ./cli.py test -q "$${fname}"; \ - done < _listofsystemicns.txt +.PHONY: sys-icons-test +sys-icons-test: _icns_list.txt + @cat _icns_list.txt | python3 -m icnsutil test -q - diff --git a/icnsutil/ArgbImage.py b/icnsutil/ArgbImage.py index 1ad74c5..8f15088 100644 --- a/icnsutil/ArgbImage.py +++ b/icnsutil/ArgbImage.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import PackBytes # pack, unpack, msb_stream -import IcnsType # match_maxsize +from . import IcnsType, PackBytes try: from PIL import Image PIL_ENABLED = True @@ -108,6 +107,7 @@ class ArgbImage: raise ImportError('Install Pillow to support PNG conversion.') img = Image.open(fname, mode='r') self.size = img.size + self.channels = 4 self.a = [] self.r = [] self.g = [] diff --git a/icnsutil/IcnsFile.py b/icnsutil/IcnsFile.py index 52be60e..e640104 100644 --- a/icnsutil/IcnsFile.py +++ b/icnsutil/IcnsFile.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -import os # path -import sys # stderr -import RawData -import IcnsType +import os # path, makedirs, remove import struct # unpack float in _description() -from ArgbImage import ArgbImage # in _export_to_png() +from sys import stderr +from . import RawData, IcnsType +from .ArgbImage import ArgbImage class IcnsFile: @@ -49,7 +48,7 @@ class IcnsFile: yield 'Invalid data length for {}: {} != {}'.format( key, len(data), iType.maxsize) # if file is not an icns file - except TypeError as e: + except RawData.ParserError as e: yield e return @@ -86,30 +85,32 @@ class IcnsFile: ''' Expects an enumerator with (key, size, data) ''' txt = '' offset = 8 # already with icns header - for key, data in enumerator: - # actually, icns length should be -8 (artificially appended header) - size = len(data) - txt += ' ' * indent - txt += '{}: {} bytes'.format(key, size) - if verbose: - txt += ', offset: {}'.format(offset) - offset += size + 8 - if key == 'name': - txt += ', value: "{}"\n'.format(data.decode('utf-8')) - continue - if key == 'icnV': - txt += ', value: {}\n'.format(struct.unpack('>f', data)[0]) - continue - ext = RawData.determine_file_ext(data) - try: - iType = IcnsType.get(key) - if not ext: - ext = iType.types[-1] - desc = iType.filename(size_only=True) - txt += ', {}: {}\n'.format(ext or 'binary', desc) - except NotImplementedError: - txt += ': UNKNOWN TYPE: {}\n'.format(ext or data[:6]) - return txt + try: + for key, data in enumerator: + size = len(data) + txt += os.linesep + ' ' * indent + txt += '{}: {} bytes'.format(key, size) + if verbose: + txt += ', offset: {}'.format(offset) + offset += size + 8 + if key == 'name': + txt += ', value: "{}"'.format(data.decode('utf-8')) + continue + if key == 'icnV': + txt += ', value: {}'.format(struct.unpack('>f', data)[0]) + continue + ext = RawData.determine_file_ext(data) + try: + iType = IcnsType.get(key) + if not ext: + ext = iType.fallback_ext() + txt += ', ' + ext + ': ' + iType.filename(size_only=True) + except NotImplementedError: + txt += ': UNKNOWN TYPE: ' + str(ext or data[:6]) + return txt[len(os.linesep):] + os.linesep + # if file is not an icns file + except RawData.ParserError as e: + return ' ' * indent + str(e) + os.linesep def __init__(self, file=None): ''' Read .icns file and load bundled media files into memory. ''' @@ -123,7 +124,7 @@ class IcnsFile: IcnsType.get(key) except NotImplementedError: print('Warning: unknown media type: {}, {} bytes, "{}"'.format( - key, len(data), file), file=sys.stderr) + key, len(data), file), file=stderr) def add_media(self, key=None, *, file=None, data=None, force=False): ''' @@ -272,7 +273,6 @@ class IcnsFile: fname = iType.filename(key_only=key_suffix, size_only=True) fname = os.path.join(outdir, fname + '.png') if iType.bits == 1: - # return None ArgbImage.from_mono(data, iType).write_png(fname) else: mask_data = self.media[mask_key] if mask_key else None @@ -285,5 +285,5 @@ class IcnsFile: type(self).__name__, self.infile, lst) def __str__(self): - return 'File: ' + (self.infile or '-mem-') + '\n' \ + return 'File: ' + (self.infile or '-mem-') + os.linesep \ + IcnsFile._description(self.media.items(), indent=2) diff --git a/icnsutil/IcnsType.py b/icnsutil/IcnsType.py index c9d6bb4..8da36e0 100644 --- a/icnsutil/IcnsType.py +++ b/icnsutil/IcnsType.py @@ -4,13 +4,17 @@ Namespace for the ICNS format. @see https://en.wikipedia.org/wiki/Apple_Icon_Image_format ''' import os # path -import RawData -import PackBytes +# import icnsutil # PackBytes, RawData +from . import PackBytes, RawData + + +class CanNotDetermine(Exception): + pass class Media: __slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability', - 'desc', 'compressable', 'retina', 'maxsize'] + 'desc', 'compressable', 'retina', 'maxsize', 'ext_certain'] def __init__(self, key, types, size=None, *, ch=None, bits=None, os=None, desc=''): @@ -33,6 +37,8 @@ class Media: self.maxsize = None if size and ch and bits: self.maxsize = self.size[0] * self.size[1] * ch * bits // 8 + self.ext_certain = all(x in ['png', 'argb', 'plist', 'jp2', 'icns'] + for x in self.types) def is_type(self, typ): return typ in self.types @@ -40,6 +46,11 @@ class Media: def is_binary(self) -> bool: return any(x in self.types for x in ['rgb', 'bin']) + def fallback_ext(self): + if self.channels in [1, 2]: + return self.desc # guaranteed to be icon, mask, or iconmask + return self.types[-1] + def split_channels(self, uncompressed_data): if self.channels not in [3, 4]: raise NotImplementedError('Only RGB and ARGB data supported.') @@ -108,7 +119,7 @@ class Media: self.channels, self.bits, self.maxsize) if self.desc: T += self.desc + ', ' - return '{}: {T}macOS {}+'.format( + return '{}: {}macOS {}+'.format( self.key, T, self.availability or '?') @@ -231,9 +242,11 @@ def guess(data, filename=None): return _TYPES[bname] ext = RawData.determine_file_ext(data) + if not ext and filename and filename.endswith('.rgb'): + ext = 'rgb' # 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) if ext else None retina = bname.lower().endswith('@2x') if filename else False # Icns specific names desc = None @@ -244,31 +257,34 @@ def guess(data, filename=None): choices = [] for x in _TYPES.values(): - if size != x.size: # currently no support for RGB and binary data - continue - if ext and not x.is_type(ext): - continue if retina != x.retina: # png + jp2 continue - if desc and desc != x.desc: # icns only - continue + if ext: + if size != x.size or not x.is_type(ext): + continue + if desc and desc != x.desc: # icns only + continue + else: # not ext + if x.ext_certain: + 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] + if ext: + 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})) + raise CanNotDetermine( + 'Could not determine type for file: "{}" – one of {}.'.format( + filename, [x.key for x in choices])) diff --git a/icnsutil/RawData.py b/icnsutil/RawData.py index 778d808..a2f254e 100644 --- a/icnsutil/RawData.py +++ b/icnsutil/RawData.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 import struct # pack, unpack -import PackBytes # get_size -import IcnsType # get, match_maxsize +from . import IcnsType, PackBytes + + +class ParserError(Exception): + pass def determine_file_ext(data): @@ -34,6 +37,10 @@ def determine_image_size(data, ext=None): elif ext == 'argb': total = PackBytes.get_size(data[4:]) # without ARGB header return IcnsType.match_maxsize(total, 'argb').size + elif ext == 'rgb': + if data[:4] == '\x00\x00\x00\x00': + data = data[4:] # without it32 header + return IcnsType.match_maxsize(PackBytes.get_size(data), 'rgb').size elif ext == 'jp2': if data[:4] == b'\xFF\x4F\xFF\x51': return struct.unpack('>II', data[8:16]) @@ -91,13 +98,13 @@ def parse_icns_file(fname): ''' Parse file and yield media entries: (key, data) :raises: - TypeError: if file is not an icns file ("icns" header missing) + ParserError: if file is not an icns file ("icns" header missing) ''' with open(fname, 'rb') as fp: # Check whether it is an actual ICNS file magic_num, _ = icns_header_read(fp.read(8)) # ignore total size if magic_num != 'icns': - raise TypeError('Not an ICNS file, missing "icns" header.') + raise ParserError('Not an ICNS file, missing "icns" header.') # Read media entries as long as there is something to read while True: key, size = icns_header_read(fp.read(8)) diff --git a/icnsutil/__init__.py b/icnsutil/__init__.py index 2098ed3..10dff06 100644 --- a/icnsutil/__init__.py +++ b/icnsutil/__init__.py @@ -4,14 +4,6 @@ A fully-featured python library to handle reading and writing icns files. ''' __version__ = '1.0' -import sys -if __name__ != '__main__': - sys.path.insert(0, __path__[0]) - -# static modules -import IcnsType -import PackBytes -import RawData -# class modules -from ArgbImage import ArgbImage, PIL_ENABLED -from IcnsFile import IcnsFile +from .IcnsFile import IcnsFile +from .ArgbImage import ArgbImage, PIL_ENABLED +from . import IcnsType, PackBytes, RawData diff --git a/icnsutil/__main__.py b/icnsutil/__main__.py new file mode 100644 index 0000000..97d8b1f --- /dev/null +++ b/icnsutil/__main__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 +from .cli import main +main() diff --git a/cli.py b/icnsutil/cli.py similarity index 78% rename from cli.py rename to icnsutil/cli.py index eacdbfd..67589fe 100755 --- a/cli.py +++ b/icnsutil/cli.py @@ -2,18 +2,18 @@ ''' Export existing icns files or compose new ones. ''' -import os # path, mkdir -import icnsutil -from sys import stderr +import os # path, makedirs +import sys # path, stderr from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter - -__version__ = icnsutil.__version__ +if __name__ == '__main__': + sys.path[0] = os.path.dirname(sys.path[0]) +from icnsutil import __version__, IcnsFile def cli_extract(args): ''' Read and extract contents of icns file(s). ''' - multiple = len(args.file) > 1 - for i, fname in enumerate(args.file): + multiple = len(args.file) > 1 or '-' in args.file + for i, fname in enumerate(enum_with_stdin(args.file)): # PathExist ensures that all files and directories exist out = args.export_dir if out and multiple: @@ -21,7 +21,7 @@ def cli_extract(args): os.makedirs(out, exist_ok=True) pred = 'png' if args.png_only else None - icnsutil.IcnsFile(fname).export( + IcnsFile(fname).export( out, allowed_ext=pred, recursive=args.recursive, convert_png=args.convert, key_suffix=args.keys) @@ -34,30 +34,29 @@ def cli_compose(args): if not args.force and os.path.exists(dest): print( 'File "{}" already exists. Force overwrite with -f.'.format(dest), - file=stderr) + file=sys.stderr) return 1 - img = icnsutil.IcnsFile() - for x in args.source: + img = IcnsFile() + for x in enum_with_stdin(args.source): img.add_media(file=x) img.write(dest, toc=not args.no_toc) def cli_print(args): ''' Print contents of icns file(s). ''' - for fname in args.file: + for fname in enum_with_stdin(args.file): print('File:', fname) - print(icnsutil.IcnsFile.description( - fname, verbose=args.verbose, indent=2)) + print(IcnsFile.description(fname, verbose=args.verbose, indent=2)) def cli_verify(args): ''' Test if icns file is valid. ''' - for fname in args.file: + for fname in enum_with_stdin(args.file): is_valid = True if not args.quiet: print('File:', fname) is_valid = None - for issue in icnsutil.IcnsFile.verify(fname): + for issue in IcnsFile.verify(fname): if is_valid: print('File:', fname) is_valid = False @@ -66,12 +65,24 @@ def cli_verify(args): print('OK') +def enum_with_stdin(file_arg): + for x in file_arg: + if x == '-': + for line in sys.stdin.readlines(): + yield line.strip() + else: + yield x + + def main(): class PathExist: - def __init__(self, kind=None): + def __init__(self, kind=None, stdin=False): self.kind = kind + self.stdin = stdin def __call__(self, path): + if self.stdin and path == '-': + return '-' if not os.path.exists(path) or \ self.kind == 'f' and not os.path.isfile(path) or \ self.kind == 'd' and not os.path.isdir(path): @@ -81,7 +92,7 @@ def main(): # Args Parser parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) - parser.set_defaults(func=lambda _: parser.print_help(stderr)) + parser.set_defaults(func=lambda _: parser.print_help(sys.stderr)) parser.add_argument( '-v', '--version', action='version', version='icnsutil ' + __version__) sub_parser = parser.add_subparsers(metavar='command') @@ -106,7 +117,7 @@ def main(): '--png-only', action='store_true', help='do not extract ARGB, binary, and meta files') cmd.add_argument( - 'file', nargs='+', type=PathExist('f'), metavar='FILE', + 'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE', help='One or more .icns files.') cmd.set_defaults(func=cli_extract) @@ -124,7 +135,7 @@ def main(): 'target', type=str, metavar='destination', help='Output file for newly created icns file.') cmd.add_argument( - 'source', nargs='+', type=PathExist('f'), metavar='src', + 'source', nargs='+', type=PathExist('f', stdin=True), metavar='src', help='One or more media files: png, argb, plist, icns.') cmd.set_defaults(func=cli_compose) cmd.epilog = ''' @@ -144,7 +155,7 @@ Notes: '-v', '--verbose', action='store_true', help='print all keys with offsets and sizes') cmd.add_argument( - 'file', nargs='+', type=PathExist('f'), metavar='FILE', + 'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE', help='One or more .icns files.') cmd.set_defaults(func=cli_print) @@ -156,7 +167,7 @@ Notes: '-q', '--quiet', action='store_true', help='do not print OK results') cmd.add_argument( - 'file', nargs='+', type=PathExist('f'), metavar='FILE', + 'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE', help='One or more .icns files.') cmd.set_defaults(func=cli_verify) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..221bc47 --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +from setuptools import setup +from icnsutil import __doc__, __version__ + +with open('README.md') as fp: + longdesc = fp.read() + +setup( + name='icnsutil', + description=__doc__.strip(), + version=__version__, + author='relikd', + url='https://github.com/relikd/icnsutil', + license='MIT', + packages=['icnsutil'], + entry_points={ + 'console_scripts': [ + 'icnsutil=icnsutil.cli:main' + ] + }, + extras_require={ + 'convert': ['Pillow'], + }, + long_description_content_type="text/markdown", + long_description=longdesc, + python_requires='>=3.2', + keywords=[ + 'icns', + 'icon', + 'extract', + 'compose', + 'create', + ], + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: MacOS X', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Desktop Environment', + 'Topic :: Multimedia :: Graphics :: Graphics Conversion', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Utilities', + ], +) diff --git a/tests/format-support.py b/tests/format-support.py index 5f6462f..8bed8f1 100644 --- a/tests/format-support.py +++ b/tests/format-support.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -import os -import sys -import zipfile +import os # makedirs +from zipfile import ZipFile from random import randint if __name__ == '__main__': - sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + import sys + sys.path[0] = os.path.dirname(sys.path[0]) from icnsutil import IcnsFile, PackBytes @@ -62,7 +62,7 @@ def generate_raw_rgb(): def generate_icns(): os.makedirs('format-support-icns', exist_ok=True) - with zipfile.ZipFile('format-support-raw.zip') as Zip: + with ZipFile('format-support-raw.zip') as Zip: for s, keys in INFO.items(): print('generate icns for {}x{}'.format(s, s)) for key in keys: @@ -91,7 +91,7 @@ def generate_icns(): def generate_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 ZipFile('format-support-raw.zip') as Zip: with Zip.open('128x128.rgb') as f: data = f.read() diff --git a/tests/test_icnsutil.py b/tests/test_icnsutil.py index ecd9bb4..54f4482 100644 --- a/tests/test_icnsutil.py +++ b/tests/test_icnsutil.py @@ -2,9 +2,9 @@ import unittest import shutil # rmtree import os # chdir, listdir, makedirs, path, remove -import sys if __name__ == '__main__': - sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + import sys + sys.path[0] = os.path.dirname(sys.path[0]) from icnsutil import * @@ -68,6 +68,11 @@ class TestArgbImage(unittest.TestCase): self.assertEqual(img.size, (16, 16)) self.assertEqual(img.a, [255] * 16 * 16) + @unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') + def test_attributes(self): + # will raise AttributeError if _load_png didnt init all attrributes + str(ArgbImage(file='rgb.icns.png')) + def test_data_getter(self): img = ArgbImage(file='rgb.icns.argb') argb = img.argb_data(compress=True) @@ -115,9 +120,9 @@ class TestIcnsFile(unittest.TestCase): ['info', 'ic12', 'icsb', 'sb24', 'ic04', 'SB24', 'ic05', 'icsB', 'ic11', 'slct']) # Not an ICNS file - with self.assertRaises(TypeError): + with self.assertRaises(RawData.ParserError): IcnsFile(file='rgb.icns.argb') - with self.assertRaises(TypeError): + with self.assertRaises(RawData.ParserError): IcnsFile(file='rgb.icns.png') def test_load_file(self): @@ -195,6 +200,32 @@ class TestIcnsFile(unittest.TestCase): is_invalid = any(IcnsFile.verify('selected.icns')) self.assertEqual(is_invalid, False) + def test_description(self): + str = IcnsFile.description('rgb.icns', indent=0) + self.assertEqual(str, ''' +ICN#: 256 bytes, iconmask: 32x32-mono +il32: 2224 bytes, rgb: 32x32 +l8mk: 1024 bytes, mask: 32x32 +ics#: 64 bytes, iconmask: 16x16-mono +is32: 705 bytes, rgb: 16x16 +s8mk: 256 bytes, mask: 16x16 +it32: 14005 bytes, rgb: 128x128 +t8mk: 16384 bytes, mask: 128x128 +'''.lstrip().replace('\n', os.linesep)) + str = IcnsFile.description('selected.icns', verbose=True, indent=0) + self.assertEqual(str, ''' +info: 314 bytes, offset: 8, plist: info +ic12: 1863 bytes, offset: 330, png: 32x32@2x +icsb: 271 bytes, offset: 2201, argb: 18x18 +sb24: 748 bytes, offset: 2480, png: 24x24 +ic04: 215 bytes, offset: 3236, argb: 16x16 +SB24: 1681 bytes, offset: 3459, png: 24x24@2x +ic05: 690 bytes, offset: 5148, argb: 32x32 +icsB: 1001 bytes, offset: 5846, png: 18x18@2x +ic11: 1056 bytes, offset: 6855, png: 16x16@2x +slct: 7660 bytes, offset: 7919, icns: selected +'''.lstrip().replace('\n', os.linesep)) + class TestIcnsType(unittest.TestCase): def test_sizes(self): @@ -238,6 +269,17 @@ class TestIcnsType(unittest.TestCase): self.assertEqual(x.size, (256, 256)) self.assertEqual(x.compressable, False) self.assertEqual(x.availability, 10.5) + # Test rgb is detected by filename extension + with open('rgb.icns.rgb', 'rb') as fp: + x = IcnsType.guess(fp.read(), 'rgb.icns.rgb') + self.assertTrue(x.is_type('rgb')) + self.assertEqual(x.size, (16, 16)) + self.assertEqual(x.retina, False) + self.assertEqual(x.channels, 3) + self.assertEqual(x.compressable, True) + fp.seek(0) + with self.assertRaises(IcnsType.CanNotDetermine): + x = IcnsType.guess(fp.read(), 'rgb.icns.bin') def test_img_mask_pairs(self): for x, y in IcnsType.enum_img_mask_pairs(['t8mk']): @@ -310,9 +352,9 @@ class TestIcnsType(unittest.TestCase): def test_exceptions(self): with self.assertRaises(NotImplementedError): IcnsType.get('wrong key') - with self.assertRaises(ValueError): + with self.assertRaises(IcnsType.CanNotDetermine): IcnsType.guess(b'\x00') - with self.assertRaises(ValueError): # could be any icns + with self.assertRaises(IcnsType.CanNotDetermine): # could be any icns with open('rgb.icns', 'rb') as fp: IcnsType.guess(fp.read(6)) @@ -553,60 +595,68 @@ class TestIcp4RGB(TestExport): self.OUTDIR, fname)), msg='File does not exist: ' + fname) -if PIL_ENABLED: - class TestRGB_toPNG(TestExport): - INFILE = 'rgb.icns' - ARGS = {'convert_png': True} +@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') +class TestRGB_toPNG(TestExport): + INFILE = 'rgb.icns' + ARGS = {'convert_png': True} - def test_export_count(self): - self.assertExportCount(5) + def test_export_count(self): + self.assertExportCount(5) - def test_conversion(self): - img = ArgbImage(file=self.outfiles['il32']) - self.assertEqual(self.img.media['il32'], img.rgb_data()) - self.assertEqual(self.img.media['l8mk'], img.mask_data()) - self.assertTrue(self.outfiles['il32'].endswith('.png')) + def test_conversion(self): + img = ArgbImage(file=self.outfiles['il32']) + self.assertEqual(self.img.media['il32'], img.rgb_data()) + self.assertEqual(self.img.media['l8mk'], img.mask_data()) + self.assertTrue(self.outfiles['il32'].endswith('.png')) - class TestARGB_toPNG(TestExport): - INFILE = 'selected.icns' - ARGS = {'convert_png': True} - def test_export_count(self): - self.assertExportCount(10) +@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') +class TestARGB_toPNG(TestExport): + INFILE = 'selected.icns' + ARGS = {'convert_png': True} - def test_conversion(self): - img = ArgbImage(file=self.outfiles['ic05']) - self.assertEqual(self.img.media['ic05'], img.argb_data()) - self.assertTrue(self.outfiles['ic05'].endswith('.png')) - img = ArgbImage(file=self.outfiles['ic04']) # is a PNG - self.assertEqual(self.img.media['ic04'], img.argb_data()) - self.assertTrue(self.outfiles['ic04'].endswith('.png')) + def test_export_count(self): + self.assertExportCount(10) - class TestNested_toPNG(TestExport): - INFILE = 'selected.icns' - ARGS = {'convert_png': True, 'recursive': True} + def test_conversion(self): + img = ArgbImage(file=self.outfiles['ic05']) + self.assertEqual(self.img.media['ic05'], img.argb_data()) + self.assertTrue(self.outfiles['ic05'].endswith('.png')) + img = ArgbImage(file=self.outfiles['ic04']) # is a PNG + self.assertEqual(self.img.media['ic04'], img.argb_data()) + self.assertTrue(self.outfiles['ic04'].endswith('.png')) - def test_export_count(self): - self.assertExportCount(10 + 1) - def test_conversion(self): - fname = self.outfiles['slct']['ic05'] - self.assertTrue(fname.endswith('.png')) +@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') +class TestNested_toPNG(TestExport): + INFILE = 'selected.icns' + ARGS = {'convert_png': True, 'recursive': True} - class TestPngOnlyNested_toPNG(TestExport): - INFILE = 'selected.icns' - ARGS = {'allowed_ext': 'png', 'convert_png': True, 'recursive': True} + def test_export_count(self): + self.assertExportCount(10 + 1) - def test_export_count(self): - self.assertExportCount(8 + 1) - self.assertExportCount(8, self.outfiles['slct']['_'] + '.export') + def test_conversion(self): + fname = self.outfiles['slct']['ic05'] + self.assertTrue(fname.endswith('.png')) - class TestIcp4RGB_toPNG(TestExport): - INFILE = 'icp4rgb.icns' - ARGS = {'convert_png': True} - def test_export_count(self): - self.assertExportCount(2) +@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') +class TestPngOnlyNested_toPNG(TestExport): + INFILE = 'selected.icns' + ARGS = {'allowed_ext': 'png', 'convert_png': True, 'recursive': True} + + def test_export_count(self): + self.assertExportCount(8 + 1) + self.assertExportCount(8, self.outfiles['slct']['_'] + '.export') + + +@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False') +class TestIcp4RGB_toPNG(TestExport): + INFILE = 'icp4rgb.icns' + ARGS = {'convert_png': True} + + def test_export_count(self): + self.assertExportCount(2) if __name__ == '__main__':