types!
This commit is contained in:
11
README.md
11
README.md
@@ -10,7 +10,8 @@ Here are two tools to open icns files directly in your browser. Both tools can b
|
||||
- The [inspector] shows the structure of an icns file (useful to understand byte-unpacking in ARGB and 24-bit RGB files).
|
||||
- The [viewer] displays icons in ARGB or 24-bit RGB file format.
|
||||
|
||||
[inspector]: https://relikd.github.io/icnsutil/html/inspector.html
|
||||
[inspector]: https://relikd.github.io/icnsutil/html/inspector.html
|
||||
[viewer]: https://relikd.github.io/icnsutil/html/viewer.html
|
||||
|
||||
|
||||
## Usage
|
||||
@@ -29,16 +30,16 @@ positional arguments:
|
||||
|
||||
```sh
|
||||
# extract
|
||||
# extract
|
||||
icnsutil e ExistingIcon.icns -o ./outdir/
|
||||
|
||||
# compose
|
||||
# compose
|
||||
icnsutil c NewIcon.icns 16x16.png 16x16@2x.png *.jp2
|
||||
|
||||
# print
|
||||
# print
|
||||
icnsutil p ExistingIcon.icns
|
||||
|
||||
# verify valid format
|
||||
# verify valid format
|
||||
icnsutil t ExistingIcon.icns
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Union, Iterator, Optional
|
||||
from . import IcnsType, PackBytes
|
||||
try:
|
||||
from PIL import Image
|
||||
@@ -11,7 +12,8 @@ class ArgbImage:
|
||||
__slots__ = ['a', 'r', 'g', 'b', 'size', 'channels']
|
||||
|
||||
@classmethod
|
||||
def from_mono(cls, data, iType):
|
||||
def from_mono(cls, data: bytes, iType: IcnsType.Media) -> 'ArgbImage':
|
||||
''' Load monochrome 1-bit image with or without mask. '''
|
||||
assert(iType.bits == 1)
|
||||
img = []
|
||||
for byte in data:
|
||||
@@ -28,11 +30,15 @@ class ArgbImage:
|
||||
self.r, self.g, self.b = img, img, img
|
||||
return self
|
||||
|
||||
def __init__(self, *, data=None, file=None, mask=None):
|
||||
def __init__(self, *, data: Optional[bytes] = None,
|
||||
file: Optional[str] = None,
|
||||
mask: Union[bytes, str, None] = None) -> None:
|
||||
'''
|
||||
Provide either a filename or raw binary data.
|
||||
- mask : Optional, may be either binary data or filename
|
||||
'''
|
||||
self.size = (0, 0)
|
||||
self.channels = 0
|
||||
if file:
|
||||
self.load_file(file)
|
||||
elif data:
|
||||
@@ -40,12 +46,12 @@ class ArgbImage:
|
||||
else:
|
||||
raise AttributeError('Neither data nor file provided.')
|
||||
if mask:
|
||||
if type(mask) == bytes:
|
||||
if isinstance(mask, bytes):
|
||||
self.load_mask(data=mask)
|
||||
else:
|
||||
self.load_mask(file=mask)
|
||||
|
||||
def load_file(self, fname):
|
||||
def load_file(self, fname: str) -> None:
|
||||
with open(fname, 'rb') as fp:
|
||||
if fp.read(4) == b'\x89PNG':
|
||||
self._load_png(fname)
|
||||
@@ -60,49 +66,50 @@ class ArgbImage:
|
||||
tmp = e # ignore previous exception to create a new one
|
||||
raise type(tmp)('{} File: "{}"'.format(str(tmp), fname))
|
||||
|
||||
def load_data(self, data):
|
||||
def load_data(self, data: bytes) -> None:
|
||||
''' Has support for ARGB and RGB-channels files. '''
|
||||
is_argb = data[:4] == b'ARGB'
|
||||
if is_argb or data[:4] == b'\x00\x00\x00\x00':
|
||||
data = data[4:] # remove ARGB and it32 header
|
||||
|
||||
data = PackBytes.unpack(data)
|
||||
iType = IcnsType.match_maxsize(len(data), 'argb' if is_argb else 'rgb')
|
||||
if not iType:
|
||||
raise ValueError('No (A)RGB image data. Could not determine size.')
|
||||
idat = PackBytes.unpack(data)
|
||||
iType = IcnsType.match_maxsize(len(idat), 'argb' if is_argb else 'rgb')
|
||||
|
||||
self.size = iType.size
|
||||
self.channels = iType.channels
|
||||
self.a, self.r, self.g, self.b = iType.split_channels(data)
|
||||
self.channels = iType.channels or 0
|
||||
self.a, self.r, self.g, self.b = iType.split_channels(idat)
|
||||
|
||||
def load_mask(self, *, file=None, data=None):
|
||||
def load_mask(self, *, file: Optional[str] = None,
|
||||
data: Optional[bytes] = None) -> None:
|
||||
''' Data must be uncompressed and same length as a single channel! '''
|
||||
if file:
|
||||
with open(file, 'rb') as fp:
|
||||
data = fp.read()
|
||||
else:
|
||||
assert(isinstance(data, bytes))
|
||||
if not data:
|
||||
raise AttributeError('Neither data nor file provided.')
|
||||
|
||||
assert(len(data) == len(self.r))
|
||||
self.a = data
|
||||
self.a = list(data)
|
||||
|
||||
def mask_data(self, bits=8, *, compress=False):
|
||||
def mask_data(self, bits: int = 8, *, compress: bool = False) -> bytes:
|
||||
if bits == 8: # default for rgb and argb
|
||||
return PackBytes.pack(self.a) if compress else bytes(self.a)
|
||||
return bytes(PackBytes.msb_stream(self.a, bits=bits))
|
||||
|
||||
def rgb_data(self, *, compress=True):
|
||||
def rgb_data(self, *, compress: bool = True) -> bytes:
|
||||
return b''.join(self._raw_rgb_channels(compress=compress))
|
||||
|
||||
def argb_data(self, *, compress=True):
|
||||
def argb_data(self, *, compress: bool = True) -> bytes:
|
||||
return b'ARGB' + self.mask_data(compress=compress) + \
|
||||
b''.join(self._raw_rgb_channels(compress=compress))
|
||||
|
||||
def _raw_rgb_channels(self, *, compress=True):
|
||||
def _raw_rgb_channels(self, *, compress: bool = True) -> Iterator[bytes]:
|
||||
for x in (self.r, self.g, self.b):
|
||||
yield PackBytes.pack(x) if compress else bytes(x)
|
||||
yield (PackBytes.pack(x) if compress else bytes(x))
|
||||
|
||||
def _load_png(self, fname):
|
||||
def _load_png(self, fname: str) -> None:
|
||||
if not PIL_ENABLED:
|
||||
raise ImportError('Install Pillow to support PNG conversion.')
|
||||
img = Image.open(fname, mode='r')
|
||||
@@ -126,7 +133,7 @@ class ArgbImage:
|
||||
self.g.append(g)
|
||||
self.b.append(b)
|
||||
|
||||
def write_png(self, fname):
|
||||
def write_png(self, fname: str) -> None:
|
||||
if not PIL_ENABLED:
|
||||
raise ImportError('Install Pillow to support PNG conversion.')
|
||||
img = Image.new(mode='RGBA', size=self.size)
|
||||
@@ -138,7 +145,7 @@ class ArgbImage:
|
||||
(x, y), (self.r[i], self.g[i], self.b[i], self.a[i]))
|
||||
img.save(fname)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
typ = ['', 'Mono', 'Mono with Mask', 'RGB', 'RGBA'][self.channels]
|
||||
return '<{}: {}x{} {}>'.format(
|
||||
type(self).__name__, self.size[0], self.size[1], typ)
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
import os # path, makedirs, remove
|
||||
import struct # unpack float in _description()
|
||||
from sys import stderr
|
||||
from typing import Iterator, Iterable, Tuple, Optional, List, Dict, Union
|
||||
from . import RawData, IcnsType
|
||||
from .ArgbImage import ArgbImage
|
||||
|
||||
|
||||
class IcnsFile:
|
||||
@staticmethod
|
||||
def verify(fname):
|
||||
def verify(fname: str) -> Iterator[str]:
|
||||
'''
|
||||
Yields an error message for each issue.
|
||||
You can check for validity with `is_invalid = any(obj.verify())`
|
||||
@@ -22,7 +23,7 @@ class IcnsFile:
|
||||
try:
|
||||
iType = IcnsType.get(key)
|
||||
except NotImplementedError:
|
||||
yield 'Unsupported icns type: {}'.format(key)
|
||||
yield 'Unsupported icns type: ' + str(key)
|
||||
continue
|
||||
|
||||
ext = RawData.determine_file_ext(data)
|
||||
@@ -32,7 +33,7 @@ class IcnsFile:
|
||||
# Check whether stored type is an expected file format
|
||||
if not (iType.is_type(ext) if ext else iType.is_binary()):
|
||||
yield 'Unexpected type for key {}: {} != {}'.format(
|
||||
key, ext or 'binary', iType.types)
|
||||
str(key), ext or 'binary', iType.types)
|
||||
|
||||
if ext in ['png', 'jp2', 'icns', 'plist']:
|
||||
continue
|
||||
@@ -40,16 +41,16 @@ class IcnsFile:
|
||||
# Check whether uncompressed size is equal to expected maxsize
|
||||
if key == 'it32' and data[:4] != b'\x00\x00\x00\x00':
|
||||
# TODO: check whether other it32 headers exist
|
||||
yield 'Unexpected it32 data header: {}'.format(data[:4])
|
||||
data = iType.decompress(data, ext) # ignores non-compressable
|
||||
yield 'Unexpected it32 data header: ' + str(data[:4])
|
||||
udata = iType.decompress(data, ext) or data
|
||||
|
||||
# Check expected uncompressed maxsize
|
||||
if iType.maxsize and len(data) != iType.maxsize:
|
||||
if iType.maxsize and len(udata) != iType.maxsize:
|
||||
yield 'Invalid data length for {}: {} != {}'.format(
|
||||
key, len(data), iType.maxsize)
|
||||
str(key), len(udata), iType.maxsize)
|
||||
# if file is not an icns file
|
||||
except RawData.ParserError as e:
|
||||
yield e
|
||||
yield str(e)
|
||||
return
|
||||
|
||||
# Check total size after enum. Enum may raise exception and break early
|
||||
@@ -76,12 +77,14 @@ class IcnsFile:
|
||||
x, y)
|
||||
|
||||
@staticmethod
|
||||
def description(fname, *, verbose=False, indent=0):
|
||||
def description(fname: str, *, verbose: bool = False, indent: int = 0) -> \
|
||||
str:
|
||||
return IcnsFile._description(
|
||||
RawData.parse_icns_file(fname), verbose=verbose, indent=indent)
|
||||
|
||||
@staticmethod
|
||||
def _description(enumerator, *, verbose=False, indent=0):
|
||||
def _description(enumerator: Iterable[Tuple[IcnsType.Media.KeyT, bytes]],
|
||||
*, verbose: bool = False, indent: int = 0) -> str:
|
||||
''' Expects an enumerator with (key, size, data) '''
|
||||
txt = ''
|
||||
offset = 8 # already with icns header
|
||||
@@ -89,7 +92,7 @@ class IcnsFile:
|
||||
for key, data in enumerator:
|
||||
size = len(data)
|
||||
txt += os.linesep + ' ' * indent
|
||||
txt += '{}: {} bytes'.format(key, size)
|
||||
txt += '{}: {} bytes'.format(str(key), size)
|
||||
if verbose:
|
||||
txt += ', offset: {}'.format(offset)
|
||||
offset += size + 8
|
||||
@@ -112,9 +115,9 @@ class IcnsFile:
|
||||
except RawData.ParserError as e:
|
||||
return ' ' * indent + str(e) + os.linesep
|
||||
|
||||
def __init__(self, file=None):
|
||||
def __init__(self, file: str = None) -> None:
|
||||
''' Read .icns file and load bundled media files into memory. '''
|
||||
self.media = {}
|
||||
self.media = {} # type: Dict[IcnsType.Media.KeyT, bytes]
|
||||
self.infile = file
|
||||
if not file: # create empty image
|
||||
return
|
||||
@@ -124,29 +127,31 @@ class IcnsFile:
|
||||
IcnsType.get(key)
|
||||
except NotImplementedError:
|
||||
print('Warning: unknown media type: {}, {} bytes, "{}"'.format(
|
||||
key, len(data), file), file=stderr)
|
||||
str(key), len(data), file), file=stderr)
|
||||
|
||||
def add_media(self, key=None, *, file=None, data=None, force=False):
|
||||
def add_media(self, key: Optional[IcnsType.Media.KeyT] = None, *,
|
||||
file: Optional[str] = None, data: Optional[bytes] = None,
|
||||
force: bool = False) -> None:
|
||||
'''
|
||||
If you provide both, data and file, data takes precedence.
|
||||
However, the filename is still used for type-guessing.
|
||||
- Declare retina images with suffix "@2x.png".
|
||||
- Declare icns file with suffix "-dark", "-template", or "-selected"
|
||||
'''
|
||||
assert(not key or len(key) == 4) # did you miss file= or data=?
|
||||
if file and not data:
|
||||
with open(file, 'rb') as fp:
|
||||
data = fp.read()
|
||||
|
||||
if not data:
|
||||
raise AttributeError('Did you miss file= or data= attribute?')
|
||||
if not key: # Determine ICNS type
|
||||
key = IcnsType.guess(data, file).key
|
||||
# Check if type is unique
|
||||
if not force and key in self.media.keys():
|
||||
raise KeyError(
|
||||
'Image with identical key "{}". File: {}'.format(key, file))
|
||||
raise KeyError('Image with identical key "{}". File: {}'.format(
|
||||
str(key), file))
|
||||
self.media[key] = data
|
||||
|
||||
def write(self, fname, *, toc=True):
|
||||
def write(self, fname: str, *, toc: bool = True) -> None:
|
||||
''' Create a new ICNS file from stored media. '''
|
||||
# Rebuild TOC to ensure soundness
|
||||
order = self._make_toc(enabled=toc)
|
||||
@@ -157,8 +162,11 @@ class IcnsFile:
|
||||
for key in order:
|
||||
RawData.icns_header_write_data(fp, key, self.media[key])
|
||||
|
||||
def export(self, outdir=None, *, allowed_ext=None, key_suffix=False,
|
||||
convert_png=False, decompress=False, recursive=False):
|
||||
def export(self, outdir: Optional[str] = None, *,
|
||||
allowed_ext: str = '*', key_suffix: bool = False,
|
||||
convert_png: bool = False, decompress: bool = False,
|
||||
recursive: bool = False) -> Dict[IcnsType.Media.KeyT,
|
||||
Union[str, Dict]]:
|
||||
'''
|
||||
Write all bundled media files to output directory.
|
||||
|
||||
@@ -177,52 +185,52 @@ class IcnsFile:
|
||||
elif not os.path.isdir(outdir):
|
||||
raise OSError('"{}" is not a directory. Abort.'.format(outdir))
|
||||
|
||||
exported_files = {'_': self.infile}
|
||||
export_files = {} # type: Dict[IcnsType.Media.KeyT, Union[str, Dict]]
|
||||
if self.infile:
|
||||
export_files['_'] = self.infile
|
||||
keys = list(self.media.keys())
|
||||
# Convert to PNG
|
||||
if convert_png:
|
||||
# keys = [x for x in keys if x not in []]
|
||||
for imgk, maskk in IcnsType.enum_png_convertable(keys):
|
||||
fname = self._export_to_png(outdir, imgk, maskk, key_suffix)
|
||||
if not fname:
|
||||
continue
|
||||
exported_files[imgk] = fname
|
||||
export_files[imgk] = fname
|
||||
if maskk:
|
||||
exported_files[maskk] = fname
|
||||
export_files[maskk] = fname
|
||||
if maskk in keys:
|
||||
keys.remove(maskk)
|
||||
keys.remove(imgk)
|
||||
|
||||
# prepare filter
|
||||
if type(allowed_ext) == str:
|
||||
allowed_ext = [allowed_ext]
|
||||
allowed = [] if allowed_ext == '*' else allowed_ext.split(',')
|
||||
if recursive:
|
||||
cleanup = allowed_ext and 'icns' not in allowed_ext
|
||||
cleanup = allowed and 'icns' not in allowed
|
||||
if cleanup:
|
||||
allowed_ext.append('icns')
|
||||
allowed.append('icns')
|
||||
|
||||
# Export remaining
|
||||
for key in keys:
|
||||
fname = self._export_single(outdir, key, key_suffix, decompress,
|
||||
allowed_ext)
|
||||
fname = self._export_single(outdir, key, key_suffix,
|
||||
decompress, allowed)
|
||||
if fname:
|
||||
exported_files[key] = fname
|
||||
export_files[key] = fname
|
||||
|
||||
# repeat for all icns
|
||||
if recursive:
|
||||
for key, fname in exported_files.items():
|
||||
if key == '_' or not fname.endswith('.icns'):
|
||||
for old_key, old_name in export_files.items():
|
||||
assert(isinstance(old_name, str))
|
||||
if not old_name.endswith('.icns') or old_key == '_':
|
||||
continue
|
||||
prev_fname = exported_files[key]
|
||||
exported_files[key] = IcnsFile(fname).export(
|
||||
export_files[old_key] = IcnsFile(old_name).export(
|
||||
allowed_ext=allowed_ext, key_suffix=key_suffix,
|
||||
convert_png=convert_png, decompress=decompress,
|
||||
recursive=True)
|
||||
if cleanup:
|
||||
os.remove(prev_fname)
|
||||
return exported_files
|
||||
os.remove(old_name)
|
||||
return export_files
|
||||
|
||||
def _make_toc(self, *, enabled):
|
||||
def _make_toc(self, *, enabled: bool) -> List[IcnsType.Media.KeyT]:
|
||||
# Rebuild TOC to ensure soundness
|
||||
if 'TOC ' in self.media.keys():
|
||||
del(self.media['TOC '])
|
||||
@@ -238,7 +246,9 @@ class IcnsFile:
|
||||
|
||||
return order
|
||||
|
||||
def _export_single(self, outdir, key, key_suffix, decompress, allowed_ext):
|
||||
def _export_single(self, outdir: str, key: IcnsType.Media.KeyT,
|
||||
key_suffix: bool, decompress: bool,
|
||||
allowed: List[str]) -> Optional[str]:
|
||||
''' You must ensure that keys exist in self.media '''
|
||||
data = self.media[key]
|
||||
ext = RawData.determine_file_ext(data)
|
||||
@@ -249,7 +259,7 @@ class IcnsFile:
|
||||
iType = IcnsType.get(key)
|
||||
fname = iType.filename(key_only=key_suffix)
|
||||
if decompress:
|
||||
data = iType.decompress(data, ext) # ignores non-compressable
|
||||
data = iType.decompress(data, ext) or data # type: ignore
|
||||
if not ext: # overwrite ext after (decompress requires None)
|
||||
ext = 'rgb' if iType.compressable else 'bin'
|
||||
except NotImplementedError: # If key unkown, export anyway
|
||||
@@ -257,14 +267,16 @@ class IcnsFile:
|
||||
if not ext:
|
||||
ext = 'unknown'
|
||||
|
||||
if allowed_ext and ext not in allowed_ext:
|
||||
if allowed and ext not in allowed:
|
||||
return None
|
||||
fname = os.path.join(outdir, fname + '.' + ext)
|
||||
with open(fname, 'wb') as fp:
|
||||
fp.write(data)
|
||||
return fname
|
||||
|
||||
def _export_to_png(self, outdir, img_key, mask_key, key_suffix):
|
||||
def _export_to_png(self, outdir: str, img_key: IcnsType.Media.KeyT,
|
||||
mask_key: Optional[IcnsType.Media.KeyT],
|
||||
key_suffix: bool) -> Optional[str]:
|
||||
''' You must ensure key and mask_key exists! '''
|
||||
data = self.media[img_key]
|
||||
if RawData.determine_file_ext(data) not in ['argb', None]:
|
||||
@@ -279,11 +291,11 @@ class IcnsFile:
|
||||
ArgbImage(data=data, mask=mask_data).write_png(fname)
|
||||
return fname
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
lst = ', '.join(str(k) for k in self.media.keys())
|
||||
return '<{}: file={}, [{}]>'.format(
|
||||
type(self).__name__, self.infile, lst)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return 'File: ' + (self.infile or '-mem-') + os.linesep \
|
||||
+ IcnsFile._description(self.media.items(), indent=2)
|
||||
|
||||
@@ -4,7 +4,7 @@ Namespace for the ICNS format.
|
||||
@see https://en.wikipedia.org/wiki/Apple_Icon_Image_format
|
||||
'''
|
||||
import os # path
|
||||
# import icnsutil # PackBytes, RawData
|
||||
from typing import Union, Optional, Tuple, Iterator, List, Iterable, Dict
|
||||
from . import PackBytes, RawData
|
||||
|
||||
|
||||
@@ -13,14 +13,17 @@ class CanNotDetermine(Exception):
|
||||
|
||||
|
||||
class Media:
|
||||
KeyT = Union[str, bytes]
|
||||
__slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability',
|
||||
'desc', 'compressable', 'retina', 'maxsize', 'ext_certain']
|
||||
|
||||
def __init__(self, key, types, size=None, *,
|
||||
ch=None, bits=None, os=None, desc=''):
|
||||
def __init__(self, key: KeyT, types: list,
|
||||
size: Optional[Union[int, Tuple[int, int]]] = None,
|
||||
*, ch: Optional[int] = None, bits: Optional[int] = None,
|
||||
os: Optional[float] = None, desc: str = '') -> None:
|
||||
self.key = key
|
||||
self.types = types if type(types) == list else [types]
|
||||
self.size = (size, size) if type(size) == int else size
|
||||
self.types = types
|
||||
self.size = (size, size) if isinstance(size, int) else size
|
||||
self.availability = os
|
||||
self.desc = desc
|
||||
# computed properties
|
||||
@@ -34,24 +37,25 @@ class Media:
|
||||
bits = 8
|
||||
self.channels = ch
|
||||
self.bits = bits
|
||||
self.maxsize = None
|
||||
if size and ch and bits:
|
||||
self.maxsize = None # type: Optional[int]
|
||||
if self.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):
|
||||
def is_type(self, typ: str) -> bool:
|
||||
return typ in self.types
|
||||
|
||||
def is_binary(self) -> bool:
|
||||
return any(x in self.types for x in ['rgb', 'bin'])
|
||||
|
||||
def fallback_ext(self):
|
||||
def fallback_ext(self) -> str:
|
||||
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: List[int]) -> Iterator[
|
||||
List[int]]:
|
||||
if self.channels not in [3, 4]:
|
||||
raise NotImplementedError('Only RGB and ARGB data supported.')
|
||||
if len(uncompressed_data) != self.maxsize:
|
||||
@@ -64,26 +68,28 @@ class Media:
|
||||
for i in range(self.channels):
|
||||
yield uncompressed_data[per_channel * i:per_channel * (i + 1)]
|
||||
|
||||
def decompress(self, data, ext='-?-'):
|
||||
if not self.compressable:
|
||||
return data
|
||||
if ext == '-?-':
|
||||
ext = RawData.determine_file_ext(data)
|
||||
if ext == 'argb':
|
||||
return PackBytes.unpack(data[4:]) # remove ARGB header
|
||||
if ext is None or ext == 'rgb': # RGB files dont have a magic number
|
||||
if self.key == 'it32':
|
||||
data = data[4:]
|
||||
return PackBytes.unpack(data)
|
||||
return data
|
||||
def decompress(self, data: bytes, ext: Optional[str] = '-?-') -> Optional[
|
||||
List[int]]:
|
||||
''' Returns None if media is not decompressable. '''
|
||||
if self.compressable:
|
||||
if ext == '-?-':
|
||||
ext = RawData.determine_file_ext(data)
|
||||
if ext == 'argb':
|
||||
return PackBytes.unpack(data[4:]) # remove ARGB header
|
||||
if ext is None or ext == 'rgb': # RGB files dont have magic number
|
||||
if self.key == 'it32':
|
||||
data = data[4:]
|
||||
return PackBytes.unpack(data)
|
||||
return None
|
||||
|
||||
def filename(self, *, key_only=False, size_only=False):
|
||||
def filename(self, *, key_only: bool = False, size_only: bool = False) \
|
||||
-> str:
|
||||
if key_only:
|
||||
if os.path.exists(__file__.upper()): # check case senstive
|
||||
if self.key in ['sb24', 'icsb']:
|
||||
return self.key + '-a'
|
||||
return self.key + '-a' # type: ignore
|
||||
elif self.key in ['SB24', 'icsB']:
|
||||
return self.key + '-b'
|
||||
return self.key + '-b' # type: ignore
|
||||
return str(self.key) # dont return directy, may be b''-str
|
||||
else:
|
||||
if self.is_type('icns'):
|
||||
@@ -106,11 +112,11 @@ class Media:
|
||||
suffix += '-mask{}b'.format(self.bits)
|
||||
return '{}x{}{}'.format(w, h, suffix)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return '<{}: {}, {}.{}>'.format(
|
||||
type(self).__name__, self.key, self.filename(), self.types[0])
|
||||
type(self).__name__, str(self.key), self.filename(), self.types[0])
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
T = ''
|
||||
if self.size:
|
||||
T += '{}x{}, '.format(*self.size)
|
||||
@@ -120,36 +126,36 @@ class Media:
|
||||
if self.desc:
|
||||
T += self.desc + ', '
|
||||
return '{}: {}macOS {}+'.format(
|
||||
self.key, T, self.availability or '?')
|
||||
str(self.key), T, self.availability or '?')
|
||||
|
||||
|
||||
_TYPES = {x.key: x for x in (
|
||||
# Read support for these:
|
||||
Media('ICON', 'bin', 32, ch=1, bits=1, os=1.0, desc='icon'),
|
||||
Media('ICN#', 'bin', 32, ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('icm#', 'bin', (16, 12), ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('icm4', 'bin', (16, 12), ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('icm8', 'bin', (16, 12), ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('ics#', 'bin', 16, ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('ics4', 'bin', 16, ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('ics8', 'bin', 16, ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('is32', 'rgb', 16, os=8.5),
|
||||
Media('s8mk', 'bin', 16, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('icl4', 'bin', 32, ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('icl8', 'bin', 32, ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('il32', 'rgb', 32, os=8.5),
|
||||
Media('l8mk', 'bin', 32, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('ich#', 'bin', 48, ch=2, bits=1, os=8.5, desc='iconmask'),
|
||||
Media('ich4', 'bin', 48, ch=1, bits=4, os=8.5, desc='icon'),
|
||||
Media('ich8', 'bin', 48, ch=1, bits=8, os=8.5, desc='icon'),
|
||||
Media('ih32', 'rgb', 48, os=8.5),
|
||||
Media('h8mk', 'bin', 48, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('it32', 'rgb', 128, os=10.0),
|
||||
Media('t8mk', 'bin', 128, ch=1, bits=8, os=10.0, desc='mask'),
|
||||
Media('ICON', ['bin'], 32, ch=1, bits=1, os=1.0, desc='icon'),
|
||||
Media('ICN#', ['bin'], 32, ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('icm#', ['bin'], (16, 12), ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('icm4', ['bin'], (16, 12), ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('icm8', ['bin'], (16, 12), ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('ics#', ['bin'], 16, ch=2, bits=1, os=6.0, desc='iconmask'),
|
||||
Media('ics4', ['bin'], 16, ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('ics8', ['bin'], 16, ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('is32', ['rgb'], 16, os=8.5),
|
||||
Media('s8mk', ['bin'], 16, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('icl4', ['bin'], 32, ch=1, bits=4, os=7.0, desc='icon'),
|
||||
Media('icl8', ['bin'], 32, ch=1, bits=8, os=7.0, desc='icon'),
|
||||
Media('il32', ['rgb'], 32, os=8.5),
|
||||
Media('l8mk', ['bin'], 32, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('ich#', ['bin'], 48, ch=2, bits=1, os=8.5, desc='iconmask'),
|
||||
Media('ich4', ['bin'], 48, ch=1, bits=4, os=8.5, desc='icon'),
|
||||
Media('ich8', ['bin'], 48, ch=1, bits=8, os=8.5, desc='icon'),
|
||||
Media('ih32', ['rgb'], 48, os=8.5),
|
||||
Media('h8mk', ['bin'], 48, ch=1, bits=8, os=8.5, desc='mask'),
|
||||
Media('it32', ['rgb'], 128, os=10.0),
|
||||
Media('t8mk', ['bin'], 128, ch=1, bits=8, os=10.0, desc='mask'),
|
||||
# Write support for these:
|
||||
Media('icp4', ['png', 'jp2', 'rgb'], 16, os=10.7),
|
||||
Media('icp5', ['png', 'jp2', 'rgb'], 32, os=10.7),
|
||||
Media('icp6', 'png', 64, os=10.7),
|
||||
Media('icp6', ['png'], 64, os=10.7),
|
||||
Media('ic07', ['png', 'jp2'], 128, os=10.7),
|
||||
Media('ic08', ['png', 'jp2'], 256, os=10.5),
|
||||
Media('ic09', ['png', 'jp2'], 512, os=10.5),
|
||||
@@ -165,36 +171,37 @@ _TYPES = {x.key: x for x in (
|
||||
Media('sb24', ['png', 'jp2'], 24),
|
||||
Media('SB24', ['png', 'jp2'], 48, desc='24x24@2x'),
|
||||
# ICNS media files
|
||||
Media('sbtp', 'icns', desc='template'),
|
||||
Media('slct', 'icns', desc='selected'),
|
||||
Media(b'\xFD\xD9\x2F\xA8', 'icns', os=10.14, desc='dark'),
|
||||
Media('sbtp', ['icns'], desc='template'),
|
||||
Media('slct', ['icns'], desc='selected'),
|
||||
Media(b'\xFD\xD9\x2F\xA8', ['icns'], os=10.14, desc='dark'),
|
||||
# Meta types:
|
||||
Media('TOC ', 'bin', os=10.7, desc='Table of Contents'),
|
||||
Media('icnV', 'bin', desc='4-byte Icon Composer.app bundle version'),
|
||||
Media('name', 'bin', desc='Unknown'),
|
||||
Media('info', 'plist', desc='Info binary plist'),
|
||||
)}
|
||||
Media('TOC ', ['bin'], os=10.7, desc='Table of Contents'),
|
||||
Media('icnV', ['bin'], desc='4-byte Icon Composer.app bundle version'),
|
||||
Media('name', ['bin'], desc='Unknown'),
|
||||
Media('info', ['plist'], desc='Info binary plist'),
|
||||
)} # type: Dict[Media.KeyT, Media]
|
||||
|
||||
|
||||
def enum_img_mask_pairs(available_keys):
|
||||
def enum_img_mask_pairs(available_keys: Iterable[Media.KeyT]) -> Iterator[
|
||||
Tuple[Optional[str], Optional[str]]]:
|
||||
for mask_k, *imgs in [ # list probably never changes, ARGB FTW
|
||||
('s8mk', 'is32', 'ics8', 'ics4', 'icp4'),
|
||||
('l8mk', 'il32', 'icl8', 'icl4', 'icp5'),
|
||||
('h8mk', 'ih32', 'ich8', 'ich4'),
|
||||
('t8mk', 'it32'),
|
||||
]:
|
||||
if mask_k not in available_keys:
|
||||
mask_k = None
|
||||
mk = mask_k if mask_k in available_keys else None
|
||||
any_img = False
|
||||
for img_k in imgs:
|
||||
if img_k in available_keys:
|
||||
any_img = True
|
||||
yield img_k, mask_k
|
||||
if mask_k and not any_img:
|
||||
yield None, mask_k
|
||||
yield img_k, mk
|
||||
if mk and not any_img:
|
||||
yield None, mk
|
||||
|
||||
|
||||
def enum_png_convertable(available_keys):
|
||||
def enum_png_convertable(available_keys: Iterable[Media.KeyT]) -> Iterator[
|
||||
Tuple[Media.KeyT, Optional[Media.KeyT]]]:
|
||||
''' Yield (image-key, mask-key or None) '''
|
||||
for img in _TYPES.values():
|
||||
if img.key not in available_keys:
|
||||
@@ -212,22 +219,21 @@ def enum_png_convertable(available_keys):
|
||||
yield img.key, mask_key
|
||||
|
||||
|
||||
def get(key):
|
||||
def get(key: Media.KeyT) -> Media:
|
||||
try:
|
||||
return _TYPES[key]
|
||||
except KeyError:
|
||||
pass
|
||||
raise NotImplementedError('Unsupported icns type "{}"'.format(key))
|
||||
raise NotImplementedError('Unsupported icns type "' + str(key) + '"')
|
||||
|
||||
|
||||
def match_maxsize(maxsize, typ):
|
||||
for x in _TYPES.values():
|
||||
if x.is_type(typ) and x.maxsize == maxsize:
|
||||
return x # TODO: handle cases with multiple options? eg: is32 icp4
|
||||
return None
|
||||
def match_maxsize(total: int, typ: str) -> Media:
|
||||
assert(typ == 'argb' or typ == 'rgb')
|
||||
ret = [x for x in _TYPES.values() if x.is_type(typ) and x.maxsize == total]
|
||||
return ret[0] # TODO: handle cases with multiple options? eg: is32 icp4
|
||||
|
||||
|
||||
def guess(data, filename=None):
|
||||
def guess(data: bytes, filename: Optional[str] = None) -> Media:
|
||||
'''
|
||||
Guess icns media type by analyzing the raw data + file naming convention.
|
||||
Use:
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
def pack(data):
|
||||
ret = []
|
||||
buf = []
|
||||
from typing import List, Iterator, Union
|
||||
|
||||
|
||||
def pack(data: List[int]) -> bytes:
|
||||
ret = [] # type: List[int]
|
||||
buf = [] # type: List[int]
|
||||
i = 0
|
||||
|
||||
def flush_buf():
|
||||
@@ -37,8 +40,8 @@ def pack(data):
|
||||
return bytes(ret)
|
||||
|
||||
|
||||
def unpack(data):
|
||||
ret = []
|
||||
def unpack(data: bytes) -> List[int]:
|
||||
ret = [] # type: List[int]
|
||||
i = 0
|
||||
end = len(data)
|
||||
while i < end:
|
||||
@@ -52,7 +55,7 @@ def unpack(data):
|
||||
return ret
|
||||
|
||||
|
||||
def get_size(data):
|
||||
def get_size(data: bytes) -> int:
|
||||
count = 0
|
||||
i = 0
|
||||
end = len(data)
|
||||
@@ -67,7 +70,7 @@ def get_size(data):
|
||||
return count
|
||||
|
||||
|
||||
def msb_stream(data, *, bits):
|
||||
def msb_stream(data: Union[bytes, List[int]], *, bits: int) -> Iterator[int]:
|
||||
if bits not in [1, 2, 4]:
|
||||
raise NotImplementedError('Unsupported bit-size.')
|
||||
c = 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import struct # pack, unpack
|
||||
from typing import Union, Optional, Tuple, Iterator, BinaryIO
|
||||
from . import IcnsType, PackBytes
|
||||
|
||||
|
||||
@@ -7,7 +8,7 @@ class ParserError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def determine_file_ext(data):
|
||||
def determine_file_ext(data: bytes) -> Optional[str]:
|
||||
'''
|
||||
Data should be at least 8 bytes long.
|
||||
Returns one of: png, argb, plist, jp2, icns, None
|
||||
@@ -28,12 +29,14 @@ def determine_file_ext(data):
|
||||
return None
|
||||
|
||||
|
||||
def determine_image_size(data, ext=None):
|
||||
def determine_image_size(data: bytes, ext: Optional[str] = None) \
|
||||
-> Optional[Tuple[int, int]]:
|
||||
''' Supports PNG, ARGB, and Jpeg 2000 image data. '''
|
||||
if not ext:
|
||||
ext = determine_file_ext(data)
|
||||
if ext == 'png':
|
||||
return struct.unpack('>II', data[16:24])
|
||||
w, h = struct.unpack('>II', data[16:24])
|
||||
return w, h
|
||||
elif ext == 'argb':
|
||||
total = PackBytes.get_size(data[4:]) # without ARGB header
|
||||
return IcnsType.match_maxsize(total, 'argb').size
|
||||
@@ -43,7 +46,8 @@ def determine_image_size(data, ext=None):
|
||||
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])
|
||||
w, h = struct.unpack('>II', data[8:16])
|
||||
return w, h
|
||||
len_ftype = struct.unpack('>I', data[12:16])[0]
|
||||
# file header + type box + header box (super box) + image header box
|
||||
offset = 12 + len_ftype + 8 + 8
|
||||
@@ -52,7 +56,7 @@ def determine_image_size(data, ext=None):
|
||||
return None # icns does not support other image types except binary
|
||||
|
||||
|
||||
def is_icns_without_header(data):
|
||||
def is_icns_without_header(data: bytes) -> bool:
|
||||
''' Returns True even if icns header is missing. '''
|
||||
offset = 0
|
||||
for i in range(2): # test n keys if they exist
|
||||
@@ -69,32 +73,33 @@ def is_icns_without_header(data):
|
||||
return True
|
||||
|
||||
|
||||
def icns_header_read(data):
|
||||
def icns_header_read(data: bytes) -> Tuple[IcnsType.Media.KeyT, int]:
|
||||
''' Returns icns type name and data length (incl. +8 for header) '''
|
||||
assert(type(data) == bytes)
|
||||
assert(isinstance(data, bytes))
|
||||
if len(data) != 8:
|
||||
return None, 0
|
||||
return '', 0
|
||||
length = struct.unpack('>I', data[4:])[0]
|
||||
try:
|
||||
name = data[:4].decode('utf8')
|
||||
return data[:4].decode('utf8'), length
|
||||
except UnicodeDecodeError:
|
||||
name = data[:4] # Fallback to bytes-string key
|
||||
return name, struct.unpack('>I', data[4:])[0]
|
||||
return data[:4], length # Fallback to bytes-string key
|
||||
|
||||
|
||||
def icns_header_write_data(fp, key, data):
|
||||
def icns_header_write_data(fp: BinaryIO, key: IcnsType.Media.KeyT,
|
||||
data: bytes) -> None:
|
||||
''' Calculates length from data. '''
|
||||
fp.write(key.encode('utf8') if type(key) == str else key)
|
||||
fp.write(key.encode('utf8') if isinstance(key, str) else key)
|
||||
fp.write(struct.pack('>I', len(data) + 8))
|
||||
fp.write(data)
|
||||
|
||||
|
||||
def icns_header_w_len(key, length):
|
||||
def icns_header_w_len(key: IcnsType.Media.KeyT, length: int) -> bytes:
|
||||
''' Adds +8 to length. '''
|
||||
name = key.encode('utf8') if type(key) == str else key
|
||||
name = key.encode('utf8') if isinstance(key, str) else key
|
||||
return name + struct.pack('>I', length + 8)
|
||||
|
||||
|
||||
def parse_icns_file(fname):
|
||||
def parse_icns_file(fname: str) -> Iterator[Tuple[IcnsType.Media.KeyT, bytes]]:
|
||||
'''
|
||||
Parse file and yield media entries: (key, data)
|
||||
:raises:
|
||||
|
||||
@@ -4,13 +4,14 @@ Export existing icns files or compose new ones.
|
||||
'''
|
||||
import os # path, makedirs
|
||||
import sys # path, stderr
|
||||
from typing import Iterator, Optional
|
||||
from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter
|
||||
if __name__ == '__main__':
|
||||
sys.path[0] = os.path.dirname(sys.path[0])
|
||||
from icnsutil import __version__, IcnsFile
|
||||
|
||||
|
||||
def cli_extract(args):
|
||||
def cli_extract(args) -> None:
|
||||
''' Read and extract contents of icns file(s). '''
|
||||
multiple = len(args.file) > 1 or '-' in args.file
|
||||
for i, fname in enumerate(enum_with_stdin(args.file)):
|
||||
@@ -20,13 +21,13 @@ def cli_extract(args):
|
||||
out = os.path.join(out, str(i))
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
pred = 'png' if args.png_only else None
|
||||
IcnsFile(fname).export(
|
||||
out, allowed_ext=pred, recursive=args.recursive,
|
||||
convert_png=args.convert, key_suffix=args.keys)
|
||||
out, allowed_ext='png' if args.png_only else '*',
|
||||
recursive=args.recursive, convert_png=args.convert,
|
||||
key_suffix=args.keys)
|
||||
|
||||
|
||||
def cli_compose(args):
|
||||
def cli_compose(args) -> None:
|
||||
''' Create new icns file from provided image files. '''
|
||||
dest = args.target
|
||||
if not os.path.splitext(dest)[1]:
|
||||
@@ -35,24 +36,24 @@ def cli_compose(args):
|
||||
print(
|
||||
'File "{}" already exists. Force overwrite with -f.'.format(dest),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
return
|
||||
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):
|
||||
def cli_print(args) -> None:
|
||||
''' Print contents of icns file(s). '''
|
||||
for fname in enum_with_stdin(args.file):
|
||||
print('File:', fname)
|
||||
print(IcnsFile.description(fname, verbose=args.verbose, indent=2))
|
||||
|
||||
|
||||
def cli_verify(args):
|
||||
def cli_verify(args) -> None:
|
||||
''' Test if icns file is valid. '''
|
||||
for fname in enum_with_stdin(args.file):
|
||||
is_valid = True
|
||||
is_valid = True # type: Optional[bool]
|
||||
if not args.quiet:
|
||||
print('File:', fname)
|
||||
is_valid = None
|
||||
@@ -65,7 +66,7 @@ def cli_verify(args):
|
||||
print('OK')
|
||||
|
||||
|
||||
def enum_with_stdin(file_arg):
|
||||
def enum_with_stdin(file_arg: list) -> Iterator[str]:
|
||||
for x in file_arg:
|
||||
if x == '-':
|
||||
for line in sys.stdin.readlines():
|
||||
@@ -74,13 +75,13 @@ def enum_with_stdin(file_arg):
|
||||
yield x
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
class PathExist:
|
||||
def __init__(self, kind=None, stdin=False):
|
||||
def __init__(self, kind: Optional[str] = None, stdin: bool = False):
|
||||
self.kind = kind
|
||||
self.stdin = stdin
|
||||
|
||||
def __call__(self, path):
|
||||
def __call__(self, path: str) -> str:
|
||||
if self.stdin and path == '-':
|
||||
return '-'
|
||||
if not os.path.exists(path) or \
|
||||
|
||||
2
setup.py
2
setup.py
@@ -23,7 +23,7 @@ setup(
|
||||
},
|
||||
long_description_content_type="text/markdown",
|
||||
long_description=longdesc,
|
||||
python_requires='>=3.2',
|
||||
python_requires='>=3.5',
|
||||
keywords=[
|
||||
'icns',
|
||||
'icon',
|
||||
|
||||
@@ -8,7 +8,7 @@ if __name__ == '__main__':
|
||||
from icnsutil import IcnsFile, PackBytes
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
# generate_raw_rgb()
|
||||
generate_icns()
|
||||
generate_random_it32_header()
|
||||
@@ -30,8 +30,8 @@ INFO = {
|
||||
}
|
||||
|
||||
|
||||
def generate_raw_rgb():
|
||||
def testpattern(w, h, *, ch, compress=True):
|
||||
def generate_raw_rgb() -> None:
|
||||
def testpattern(w: int, h: int, ch: int, compress: bool = True) -> bytes:
|
||||
ARGB = ch == 4
|
||||
sz = w * h
|
||||
if compress:
|
||||
@@ -60,7 +60,7 @@ def generate_raw_rgb():
|
||||
fp.write(rgb_data)
|
||||
|
||||
|
||||
def generate_icns():
|
||||
def generate_icns() -> None:
|
||||
os.makedirs('format-support-icns', exist_ok=True)
|
||||
with ZipFile('format-support-raw.zip') as Zip:
|
||||
for s, keys in INFO.items():
|
||||
@@ -88,14 +88,14 @@ def generate_icns():
|
||||
toc=False)
|
||||
|
||||
|
||||
def generate_random_it32_header():
|
||||
def generate_random_it32_header() -> None:
|
||||
print('testing random it32 header')
|
||||
os.makedirs('format-support-it32', exist_ok=True)
|
||||
with ZipFile('format-support-raw.zip') as Zip:
|
||||
with Zip.open('128x128.rgb') as f:
|
||||
data = f.read()
|
||||
|
||||
def random_header():
|
||||
def random_header() -> bytes:
|
||||
return bytes([randint(0, 255), randint(0, 255),
|
||||
randint(0, 255), randint(0, 255)])
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import unittest
|
||||
import shutil # rmtree
|
||||
import os # chdir, listdir, makedirs, path, remove
|
||||
from typing import Optional, Dict, Any
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
sys.path[0] = os.path.dirname(sys.path[0])
|
||||
@@ -41,7 +42,7 @@ class TestArgbImage(unittest.TestCase):
|
||||
self.assertEqual(img.g, [0] * w * w)
|
||||
self.assertEqual(img.b, [255] * w * w)
|
||||
# Test setting mask manually
|
||||
img.load_mask(data=[117] * w * w)
|
||||
img.load_mask(data=b'\x75' * w * w)
|
||||
self.assertEqual(img.size, (w, w))
|
||||
self.assertEqual(img.a, [117] * w * w)
|
||||
self.assertEqual(img.r, [128] * w * w)
|
||||
@@ -70,8 +71,10 @@ class TestArgbImage(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(PIL_ENABLED, 'PIL_ENABLED == False')
|
||||
def test_attributes(self):
|
||||
img = ArgbImage(file='rgb.icns.png')
|
||||
self.assertTrue(img.channels != 0)
|
||||
# will raise AttributeError if _load_png didnt init all attrributes
|
||||
str(ArgbImage(file='rgb.icns.png'))
|
||||
str(img)
|
||||
|
||||
def test_data_getter(self):
|
||||
img = ArgbImage(file='rgb.icns.argb')
|
||||
@@ -319,9 +322,6 @@ class TestIcnsType(unittest.TestCase):
|
||||
|
||||
def test_match_maxsize(self):
|
||||
for typ, size, key in [
|
||||
('bin', 512, 'icl4'),
|
||||
('bin', 192, 'icm8'),
|
||||
('png', 768, 'icp4'),
|
||||
('rgb', 768, 'is32'),
|
||||
('rgb', 3072, 'il32'),
|
||||
('rgb', 6912, 'ih32'),
|
||||
@@ -332,6 +332,13 @@ class TestIcnsType(unittest.TestCase):
|
||||
]:
|
||||
iType = IcnsType.match_maxsize(size, typ)
|
||||
self.assertEqual(iType.key, key, msg=f'{typ} ({size}) != {key}')
|
||||
for typ, size, key in [
|
||||
('bin', 512, 'icl4'),
|
||||
('bin', 192, 'icm8'),
|
||||
('png', 768, 'icp4'),
|
||||
]:
|
||||
with self.assertRaises(AssertionError):
|
||||
IcnsType.match_maxsize(size, typ)
|
||||
|
||||
def test_decompress(self):
|
||||
# Test ARGB deflate
|
||||
@@ -347,7 +354,7 @@ class TestIcnsType(unittest.TestCase):
|
||||
d = IcnsType.get('it32').decompress(data)
|
||||
self.assertEqual(len(d), 1966) # decompress removes 4-byte it32-header
|
||||
d = IcnsType.get('ic04').decompress(data, ext='png')
|
||||
self.assertEqual(len(d), 705) # if png, dont decompress
|
||||
self.assertEqual(d, None) # if png, dont decompress
|
||||
|
||||
def test_exceptions(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
@@ -417,10 +424,10 @@ class TestRawData(unittest.TestCase):
|
||||
#######################
|
||||
|
||||
class TestExport(unittest.TestCase):
|
||||
INFILE = None
|
||||
OUTDIR = None # set in setUpClass
|
||||
INFILE = None # type: Optional[str]
|
||||
OUTDIR = None # type: Optional[str] # set in setUpClass
|
||||
CLEANUP = True # for debugging purposes
|
||||
ARGS = {}
|
||||
ARGS = {} # type: Dict[str, Any]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
||||
Reference in New Issue
Block a user