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
|
/*.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
|
||||||
|
|||||||
52
Makefile
52
Makefile
@@ -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
|
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
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.
|
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
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
|
#!/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()
|
||||||
|
|
||||||
|
|||||||
@@ -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__':
|
||||||
|
|||||||
Reference in New Issue
Block a user