proper module with setup.py
+ 2 bugfixes ("T" and channels)
This commit is contained in:
23
.gitignore
vendored
23
.gitignore
vendored
@@ -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
|
||||
|
||||
52
Makefile
52
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 -
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
3
icnsutil/__main__.py
Normal file
3
icnsutil/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
from .cli import main
|
||||
main()
|
||||
@@ -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)
|
||||
|
||||
57
setup.py
Normal file
57
setup.py
Normal 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',
|
||||
],
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user