proper module with setup.py

+ 2 bugfixes ("T" and channels)
This commit is contained in:
relikd
2021-09-28 21:14:54 +02:00
parent ae009d4925
commit 823ed3aaa9
12 changed files with 345 additions and 168 deletions

23
.gitignore vendored
View File

@@ -2,3 +2,26 @@
/*.txt /*.txt
/tests/format-support-*/ /tests/format-support-*/
/tests/fixtures/tmp_* /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

View File

@@ -1,26 +1,44 @@
.PHONY: help test sys-icons-print sys-icons-test .PHONY: help
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: test:
python3 tests/test_icnsutil.py 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...' @echo 'Generate list of system icns files...'
find /Applications -type f -name '*.icns' > _listofsystemicns.txt || echo -find /Applications -type f -name '*.icns' > _icns_list.txt
find /Users -type f -name '*.icns' >> _listofsystemicns.txt || echo -find /Users -type f -name '*.icns' >> _icns_list.txt
find /Library -type f -name '*.icns' >> _listofsystemicns.txt || echo -find /Library -type f -name '*.icns' >> _icns_list.txt
find /System -not \( -path '/System/Volumes' -prune \) \ -find /System -not \( -path '/System/Volumes' -prune \) \
-not \( -path '/System/Library/Templates' -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 .PHONY: sys-icons-print
@while read fname; do \ sys-icons-print: _icns_list.txt
./cli.py print "$${fname}"; \ @cat _icns_list.txt | python3 -m icnsutil print -
done < _listofsystemicns.txt
sys-icons-test: _listofsystemicns.txt .PHONY: sys-icons-test
@while read fname; do \ sys-icons-test: _icns_list.txt
./cli.py test -q "$${fname}"; \ @cat _icns_list.txt | python3 -m icnsutil test -q -
done < _listofsystemicns.txt

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import PackBytes # pack, unpack, msb_stream from . import IcnsType, PackBytes
import IcnsType # match_maxsize
try: try:
from PIL import Image from PIL import Image
PIL_ENABLED = True PIL_ENABLED = True
@@ -108,6 +107,7 @@ class ArgbImage:
raise ImportError('Install Pillow to support PNG conversion.') raise ImportError('Install Pillow to support PNG conversion.')
img = Image.open(fname, mode='r') img = Image.open(fname, mode='r')
self.size = img.size self.size = img.size
self.channels = 4
self.a = [] self.a = []
self.r = [] self.r = []
self.g = [] self.g = []

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os # path import os # path, makedirs, remove
import sys # stderr
import RawData
import IcnsType
import struct # unpack float in _description() 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: class IcnsFile:
@@ -49,7 +48,7 @@ class IcnsFile:
yield 'Invalid data length for {}: {} != {}'.format( yield 'Invalid data length for {}: {} != {}'.format(
key, len(data), iType.maxsize) key, len(data), iType.maxsize)
# if file is not an icns file # if file is not an icns file
except TypeError as e: except RawData.ParserError as e:
yield e yield e
return return
@@ -86,30 +85,32 @@ class IcnsFile:
''' Expects an enumerator with (key, size, data) ''' ''' Expects an enumerator with (key, size, data) '''
txt = '' txt = ''
offset = 8 # already with icns header offset = 8 # already with icns header
for key, data in enumerator: try:
# actually, icns length should be -8 (artificially appended header) for key, data in enumerator:
size = len(data) size = len(data)
txt += ' ' * indent txt += os.linesep + ' ' * indent
txt += '{}: {} bytes'.format(key, size) txt += '{}: {} bytes'.format(key, size)
if verbose: if verbose:
txt += ', offset: {}'.format(offset) txt += ', offset: {}'.format(offset)
offset += size + 8 offset += size + 8
if key == 'name': if key == 'name':
txt += ', value: "{}"\n'.format(data.decode('utf-8')) txt += ', value: "{}"'.format(data.decode('utf-8'))
continue continue
if key == 'icnV': if key == 'icnV':
txt += ', value: {}\n'.format(struct.unpack('>f', data)[0]) txt += ', value: {}'.format(struct.unpack('>f', data)[0])
continue continue
ext = RawData.determine_file_ext(data) ext = RawData.determine_file_ext(data)
try: try:
iType = IcnsType.get(key) iType = IcnsType.get(key)
if not ext: if not ext:
ext = iType.types[-1] ext = iType.fallback_ext()
desc = iType.filename(size_only=True) txt += ', ' + ext + ': ' + iType.filename(size_only=True)
txt += ', {}: {}\n'.format(ext or 'binary', desc) except NotImplementedError:
except NotImplementedError: txt += ': UNKNOWN TYPE: ' + str(ext or data[:6])
txt += ': UNKNOWN TYPE: {}\n'.format(ext or data[:6]) return txt[len(os.linesep):] + os.linesep
return txt # if file is not an icns file
except RawData.ParserError as e:
return ' ' * indent + str(e) + os.linesep
def __init__(self, file=None): def __init__(self, file=None):
''' Read .icns file and load bundled media files into memory. ''' ''' Read .icns file and load bundled media files into memory. '''
@@ -123,7 +124,7 @@ class IcnsFile:
IcnsType.get(key) IcnsType.get(key)
except NotImplementedError: except NotImplementedError:
print('Warning: unknown media type: {}, {} bytes, "{}"'.format( 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): 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 = iType.filename(key_only=key_suffix, size_only=True)
fname = os.path.join(outdir, fname + '.png') fname = os.path.join(outdir, fname + '.png')
if iType.bits == 1: if iType.bits == 1:
# return None
ArgbImage.from_mono(data, iType).write_png(fname) ArgbImage.from_mono(data, iType).write_png(fname)
else: else:
mask_data = self.media[mask_key] if mask_key else None mask_data = self.media[mask_key] if mask_key else None
@@ -285,5 +285,5 @@ class IcnsFile:
type(self).__name__, self.infile, lst) type(self).__name__, self.infile, lst)
def __str__(self): 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) + IcnsFile._description(self.media.items(), indent=2)

View File

@@ -4,13 +4,17 @@ 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
import RawData # import icnsutil # PackBytes, RawData
import PackBytes from . import PackBytes, RawData
class CanNotDetermine(Exception):
pass
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', 'ext_certain']
def __init__(self, key, types, size=None, *, def __init__(self, key, types, size=None, *,
ch=None, bits=None, os=None, desc=''): ch=None, bits=None, os=None, desc=''):
@@ -33,6 +37,8 @@ class Media:
self.maxsize = None self.maxsize = None
if size and ch and bits: if size and ch and bits:
self.maxsize = self.size[0] * self.size[1] * ch * bits // 8 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): def is_type(self, typ):
return typ in self.types return typ in self.types
@@ -40,6 +46,11 @@ class Media:
def is_binary(self) -> bool: def is_binary(self) -> bool:
return any(x in self.types for x in ['rgb', 'bin']) 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): def split_channels(self, uncompressed_data):
if self.channels not in [3, 4]: if self.channels not in [3, 4]:
raise NotImplementedError('Only RGB and ARGB data supported.') raise NotImplementedError('Only RGB and ARGB data supported.')
@@ -108,7 +119,7 @@ class Media:
self.channels, self.bits, self.maxsize) self.channels, self.bits, self.maxsize)
if self.desc: if self.desc:
T += self.desc + ', ' T += self.desc + ', '
return '{}: {T}macOS {}+'.format( return '{}: {}macOS {}+'.format(
self.key, T, self.availability or '?') self.key, T, self.availability or '?')
@@ -231,9 +242,11 @@ def guess(data, filename=None):
return _TYPES[bname] return _TYPES[bname]
ext = RawData.determine_file_ext(data) ext = RawData.determine_file_ext(data)
if not ext and filename and filename.endswith('.rgb'):
ext = 'rgb'
# 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) if ext else None
retina = bname.lower().endswith('@2x') if filename else False retina = bname.lower().endswith('@2x') if filename else False
# Icns specific names # Icns specific names
desc = None desc = None
@@ -244,31 +257,34 @@ def guess(data, filename=None):
choices = [] choices = []
for x in _TYPES.values(): 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 if retina != x.retina: # png + jp2
continue continue
if desc and desc != x.desc: # icns only if ext:
continue 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) 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) # Try get most favorable type (sort order of types)
best_i = 99 if ext:
best_choice = [] best_i = 99
for x in choices: best_choice = []
i = x.types.index(ext) for x in choices:
if i < best_i: i = x.types.index(ext)
best_i = i if i < best_i:
best_choice = [x] best_i = i
elif i == best_i: best_choice = [x]
best_choice.append(x) elif i == best_i:
if len(best_choice) == 1: best_choice.append(x)
return best_choice[0] if len(best_choice) == 1:
return best_choice[0]
# Else # Else
raise ValueError('Could not determine type one of {} -- {}'.format( raise CanNotDetermine(
[x.key for x in choices], 'Could not determine type for file: "{}" one of {}.'.format(
{'type': ext, 'size': size, 'retina': retina})) filename, [x.key for x in choices]))

View File

@@ -1,7 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import struct # pack, unpack import struct # pack, unpack
import PackBytes # get_size from . import IcnsType, PackBytes
import IcnsType # get, match_maxsize
class ParserError(Exception):
pass
def determine_file_ext(data): def determine_file_ext(data):
@@ -34,6 +37,10 @@ def determine_image_size(data, ext=None):
elif ext == 'argb': elif ext == 'argb':
total = PackBytes.get_size(data[4:]) # without ARGB header total = PackBytes.get_size(data[4:]) # without ARGB header
return IcnsType.match_maxsize(total, 'argb').size 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': elif ext == 'jp2':
if data[:4] == b'\xFF\x4F\xFF\x51': if data[:4] == b'\xFF\x4F\xFF\x51':
return struct.unpack('>II', data[8:16]) return struct.unpack('>II', data[8:16])
@@ -91,13 +98,13 @@ def parse_icns_file(fname):
''' '''
Parse file and yield media entries: (key, data) Parse file and yield media entries: (key, data)
:raises: :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: with open(fname, 'rb') as fp:
# Check whether it is an actual ICNS file # Check whether it is an actual ICNS file
magic_num, _ = icns_header_read(fp.read(8)) # ignore total size magic_num, _ = icns_header_read(fp.read(8)) # ignore total size
if magic_num != 'icns': 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 # Read media entries as long as there is something to read
while True: while True:
key, size = icns_header_read(fp.read(8)) key, size = icns_header_read(fp.read(8))

View File

@@ -4,14 +4,6 @@ A fully-featured python library to handle reading and writing icns files.
''' '''
__version__ = '1.0' __version__ = '1.0'
import sys from .IcnsFile import IcnsFile
if __name__ != '__main__': from .ArgbImage import ArgbImage, PIL_ENABLED
sys.path.insert(0, __path__[0]) from . import IcnsType, PackBytes, RawData
# static modules
import IcnsType
import PackBytes
import RawData
# class modules
from ArgbImage import ArgbImage, PIL_ENABLED
from IcnsFile import IcnsFile

3
icnsutil/__main__.py Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env python3
from .cli import main
main()

View File

@@ -2,18 +2,18 @@
''' '''
Export existing icns files or compose new ones. Export existing icns files or compose new ones.
''' '''
import os # path, mkdir import os # path, makedirs
import icnsutil import sys # path, stderr
from sys import stderr
from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter
if __name__ == '__main__':
__version__ = icnsutil.__version__ sys.path[0] = os.path.dirname(sys.path[0])
from icnsutil import __version__, IcnsFile
def cli_extract(args): def cli_extract(args):
''' Read and extract contents of icns file(s). ''' ''' Read and extract contents of icns file(s). '''
multiple = len(args.file) > 1 multiple = len(args.file) > 1 or '-' in args.file
for i, fname in enumerate(args.file): for i, fname in enumerate(enum_with_stdin(args.file)):
# PathExist ensures that all files and directories exist # PathExist ensures that all files and directories exist
out = args.export_dir out = args.export_dir
if out and multiple: if out and multiple:
@@ -21,7 +21,7 @@ def cli_extract(args):
os.makedirs(out, exist_ok=True) os.makedirs(out, exist_ok=True)
pred = 'png' if args.png_only else None pred = 'png' if args.png_only else None
icnsutil.IcnsFile(fname).export( IcnsFile(fname).export(
out, allowed_ext=pred, recursive=args.recursive, out, allowed_ext=pred, recursive=args.recursive,
convert_png=args.convert, key_suffix=args.keys) 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): if not args.force and os.path.exists(dest):
print( print(
'File "{}" already exists. Force overwrite with -f.'.format(dest), 'File "{}" already exists. Force overwrite with -f.'.format(dest),
file=stderr) file=sys.stderr)
return 1 return 1
img = icnsutil.IcnsFile() img = IcnsFile()
for x in args.source: for x in enum_with_stdin(args.source):
img.add_media(file=x) img.add_media(file=x)
img.write(dest, toc=not args.no_toc) img.write(dest, toc=not args.no_toc)
def cli_print(args): def cli_print(args):
''' Print contents of icns file(s). ''' ''' Print contents of icns file(s). '''
for fname in args.file: for fname in enum_with_stdin(args.file):
print('File:', fname) print('File:', fname)
print(icnsutil.IcnsFile.description( print(IcnsFile.description(fname, verbose=args.verbose, indent=2))
fname, verbose=args.verbose, indent=2))
def cli_verify(args): def cli_verify(args):
''' Test if icns file is valid. ''' ''' Test if icns file is valid. '''
for fname in args.file: for fname in enum_with_stdin(args.file):
is_valid = True is_valid = True
if not args.quiet: if not args.quiet:
print('File:', fname) print('File:', fname)
is_valid = None is_valid = None
for issue in icnsutil.IcnsFile.verify(fname): for issue in IcnsFile.verify(fname):
if is_valid: if is_valid:
print('File:', fname) print('File:', fname)
is_valid = False is_valid = False
@@ -66,12 +65,24 @@ def cli_verify(args):
print('OK') 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(): def main():
class PathExist: class PathExist:
def __init__(self, kind=None): def __init__(self, kind=None, stdin=False):
self.kind = kind self.kind = kind
self.stdin = stdin
def __call__(self, path): def __call__(self, path):
if self.stdin and path == '-':
return '-'
if not os.path.exists(path) or \ if not os.path.exists(path) or \
self.kind == 'f' and not os.path.isfile(path) or \ self.kind == 'f' and not os.path.isfile(path) or \
self.kind == 'd' and not os.path.isdir(path): self.kind == 'd' and not os.path.isdir(path):
@@ -81,7 +92,7 @@ def main():
# Args Parser # Args Parser
parser = ArgumentParser(description=__doc__, parser = ArgumentParser(description=__doc__,
formatter_class=RawTextHelpFormatter) 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( parser.add_argument(
'-v', '--version', action='version', version='icnsutil ' + __version__) '-v', '--version', action='version', version='icnsutil ' + __version__)
sub_parser = parser.add_subparsers(metavar='command') sub_parser = parser.add_subparsers(metavar='command')
@@ -106,7 +117,7 @@ def main():
'--png-only', action='store_true', '--png-only', action='store_true',
help='do not extract ARGB, binary, and meta files') help='do not extract ARGB, binary, and meta files')
cmd.add_argument( 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.') help='One or more .icns files.')
cmd.set_defaults(func=cli_extract) cmd.set_defaults(func=cli_extract)
@@ -124,7 +135,7 @@ def main():
'target', type=str, metavar='destination', 'target', type=str, metavar='destination',
help='Output file for newly created icns file.') help='Output file for newly created icns file.')
cmd.add_argument( 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.') help='One or more media files: png, argb, plist, icns.')
cmd.set_defaults(func=cli_compose) cmd.set_defaults(func=cli_compose)
cmd.epilog = ''' cmd.epilog = '''
@@ -144,7 +155,7 @@ Notes:
'-v', '--verbose', action='store_true', '-v', '--verbose', action='store_true',
help='print all keys with offsets and sizes') help='print all keys with offsets and sizes')
cmd.add_argument( 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.') help='One or more .icns files.')
cmd.set_defaults(func=cli_print) cmd.set_defaults(func=cli_print)
@@ -156,7 +167,7 @@ Notes:
'-q', '--quiet', action='store_true', '-q', '--quiet', action='store_true',
help='do not print OK results') help='do not print OK results')
cmd.add_argument( 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.') help='One or more .icns files.')
cmd.set_defaults(func=cli_verify) cmd.set_defaults(func=cli_verify)

57
setup.py Normal file
View File

@@ -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',
],
)

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os # makedirs
import sys from zipfile import ZipFile
import zipfile
from random import randint from random import randint
if __name__ == '__main__': 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 from icnsutil import IcnsFile, PackBytes
@@ -62,7 +62,7 @@ def generate_raw_rgb():
def generate_icns(): 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('format-support-raw.zip') as Zip:
for s, keys in INFO.items(): for s, keys in INFO.items():
print('generate icns for {}x{}'.format(s, s)) print('generate icns for {}x{}'.format(s, s))
for key in keys: for key in keys:
@@ -91,7 +91,7 @@ def generate_icns():
def generate_random_it32_header(): def generate_random_it32_header():
print('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('format-support-raw.zip') as Zip:
with Zip.open('128x128.rgb') as f: with Zip.open('128x128.rgb') as f:
data = f.read() data = f.read()

View File

@@ -2,9 +2,9 @@
import unittest import unittest
import shutil # rmtree import shutil # rmtree
import os # chdir, listdir, makedirs, path, remove import os # chdir, listdir, makedirs, path, remove
import sys
if __name__ == '__main__': 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 * from icnsutil import *
@@ -68,6 +68,11 @@ class TestArgbImage(unittest.TestCase):
self.assertEqual(img.size, (16, 16)) self.assertEqual(img.size, (16, 16))
self.assertEqual(img.a, [255] * 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): def test_data_getter(self):
img = ArgbImage(file='rgb.icns.argb') img = ArgbImage(file='rgb.icns.argb')
argb = img.argb_data(compress=True) argb = img.argb_data(compress=True)
@@ -115,9 +120,9 @@ class TestIcnsFile(unittest.TestCase):
['info', 'ic12', 'icsb', 'sb24', 'ic04', ['info', 'ic12', 'icsb', 'sb24', 'ic04',
'SB24', 'ic05', 'icsB', 'ic11', 'slct']) 'SB24', 'ic05', 'icsB', 'ic11', 'slct'])
# Not an ICNS file # Not an ICNS file
with self.assertRaises(TypeError): with self.assertRaises(RawData.ParserError):
IcnsFile(file='rgb.icns.argb') IcnsFile(file='rgb.icns.argb')
with self.assertRaises(TypeError): with self.assertRaises(RawData.ParserError):
IcnsFile(file='rgb.icns.png') IcnsFile(file='rgb.icns.png')
def test_load_file(self): def test_load_file(self):
@@ -195,6 +200,32 @@ class TestIcnsFile(unittest.TestCase):
is_invalid = any(IcnsFile.verify('selected.icns')) is_invalid = any(IcnsFile.verify('selected.icns'))
self.assertEqual(is_invalid, False) 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): class TestIcnsType(unittest.TestCase):
def test_sizes(self): def test_sizes(self):
@@ -238,6 +269,17 @@ class TestIcnsType(unittest.TestCase):
self.assertEqual(x.size, (256, 256)) self.assertEqual(x.size, (256, 256))
self.assertEqual(x.compressable, False) self.assertEqual(x.compressable, False)
self.assertEqual(x.availability, 10.5) 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): def test_img_mask_pairs(self):
for x, y in IcnsType.enum_img_mask_pairs(['t8mk']): for x, y in IcnsType.enum_img_mask_pairs(['t8mk']):
@@ -310,9 +352,9 @@ class TestIcnsType(unittest.TestCase):
def test_exceptions(self): def test_exceptions(self):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
IcnsType.get('wrong key') IcnsType.get('wrong key')
with self.assertRaises(ValueError): with self.assertRaises(IcnsType.CanNotDetermine):
IcnsType.guess(b'\x00') 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: with open('rgb.icns', 'rb') as fp:
IcnsType.guess(fp.read(6)) IcnsType.guess(fp.read(6))
@@ -553,60 +595,68 @@ class TestIcp4RGB(TestExport):
self.OUTDIR, fname)), msg='File does not exist: ' + fname) self.OUTDIR, fname)), msg='File does not exist: ' + fname)
if PIL_ENABLED: @unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False')
class TestRGB_toPNG(TestExport): class TestRGB_toPNG(TestExport):
INFILE = 'rgb.icns' INFILE = 'rgb.icns'
ARGS = {'convert_png': True} ARGS = {'convert_png': True}
def test_export_count(self): def test_export_count(self):
self.assertExportCount(5) self.assertExportCount(5)
def test_conversion(self): def test_conversion(self):
img = ArgbImage(file=self.outfiles['il32']) img = ArgbImage(file=self.outfiles['il32'])
self.assertEqual(self.img.media['il32'], img.rgb_data()) self.assertEqual(self.img.media['il32'], img.rgb_data())
self.assertEqual(self.img.media['l8mk'], img.mask_data()) self.assertEqual(self.img.media['l8mk'], img.mask_data())
self.assertTrue(self.outfiles['il32'].endswith('.png')) self.assertTrue(self.outfiles['il32'].endswith('.png'))
class TestARGB_toPNG(TestExport):
INFILE = 'selected.icns'
ARGS = {'convert_png': True}
def test_export_count(self): @unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False')
self.assertExportCount(10) class TestARGB_toPNG(TestExport):
INFILE = 'selected.icns'
ARGS = {'convert_png': True}
def test_conversion(self): def test_export_count(self):
img = ArgbImage(file=self.outfiles['ic05']) self.assertExportCount(10)
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'))
class TestNested_toPNG(TestExport): def test_conversion(self):
INFILE = 'selected.icns' img = ArgbImage(file=self.outfiles['ic05'])
ARGS = {'convert_png': True, 'recursive': True} 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): @unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False')
fname = self.outfiles['slct']['ic05'] class TestNested_toPNG(TestExport):
self.assertTrue(fname.endswith('.png')) INFILE = 'selected.icns'
ARGS = {'convert_png': True, 'recursive': True}
class TestPngOnlyNested_toPNG(TestExport): def test_export_count(self):
INFILE = 'selected.icns' self.assertExportCount(10 + 1)
ARGS = {'allowed_ext': 'png', 'convert_png': True, 'recursive': True}
def test_export_count(self): def test_conversion(self):
self.assertExportCount(8 + 1) fname = self.outfiles['slct']['ic05']
self.assertExportCount(8, self.outfiles['slct']['_'] + '.export') self.assertTrue(fname.endswith('.png'))
class TestIcp4RGB_toPNG(TestExport):
INFILE = 'icp4rgb.icns'
ARGS = {'convert_png': True}
def test_export_count(self): @unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False')
self.assertExportCount(2) 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__': if __name__ == '__main__':