chore: type formatting + refactor help text
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Union, Iterator, Optional
|
||||
from typing import Union, Optional
|
||||
from math import sqrt
|
||||
from . import IcnsType, PackBytes, RawData
|
||||
try:
|
||||
@@ -16,6 +16,8 @@ class ArgbImage:
|
||||
def from_mono(cls, data: bytes, iType: IcnsType.Media) -> 'ArgbImage':
|
||||
''' Load monochrome 1-bit image with or without mask. '''
|
||||
assert(iType.bits == 1)
|
||||
assert(iType.size)
|
||||
assert(iType.channels)
|
||||
img = []
|
||||
for byte in data:
|
||||
for i in range(7, -1, -1):
|
||||
@@ -31,9 +33,13 @@ class ArgbImage:
|
||||
self.r, self.g, self.b = img, img, img
|
||||
return self
|
||||
|
||||
def __init__(self, *, data: Optional[bytes] = None,
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
data: Optional[bytes] = None,
|
||||
file: Optional[str] = None,
|
||||
mask: Union[bytes, str, None] = None) -> 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
|
||||
@@ -91,8 +97,9 @@ class ArgbImage:
|
||||
self.g = uncompressed_data[(i + 1) * per_channel:(i + 2) * per_channel]
|
||||
self.b = uncompressed_data[(i + 2) * per_channel:(i + 3) * per_channel]
|
||||
|
||||
def load_mask(self, *, file: Optional[str] = None,
|
||||
data: Optional[bytes] = None) -> 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:
|
||||
|
||||
@@ -79,14 +79,18 @@ class IcnsFile:
|
||||
x, y)
|
||||
|
||||
@staticmethod
|
||||
def description(fname: str, *, verbose: bool = False, indent: int = 0) -> \
|
||||
str:
|
||||
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: Iterable[Tuple[IcnsType.Media.KeyT, bytes]],
|
||||
*, verbose: bool = False, indent: int = 0) -> str:
|
||||
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
|
||||
@@ -117,7 +121,7 @@ class IcnsFile:
|
||||
except RawData.ParserError as e:
|
||||
return ' ' * indent + str(e) + os.linesep
|
||||
|
||||
def __init__(self, file: str = None) -> None:
|
||||
def __init__(self, file: Optional[str] = None) -> None:
|
||||
''' Read .icns file and load bundled media files into memory. '''
|
||||
self.media = {} # type: Dict[IcnsType.Media.KeyT, bytes]
|
||||
self.infile = file
|
||||
@@ -134,9 +138,14 @@ class IcnsFile:
|
||||
def has_toc(self) -> bool:
|
||||
return 'TOC ' in self.media.keys()
|
||||
|
||||
def add_media(self, key: Optional[IcnsType.Media.KeyT] = None, *,
|
||||
file: Optional[str] = None, data: Optional[bytes] = None,
|
||||
force: bool = False) -> None:
|
||||
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.
|
||||
@@ -182,11 +191,16 @@ class IcnsFile:
|
||||
for key in order:
|
||||
RawData.icns_header_write_data(fp, key, self.media[key])
|
||||
|
||||
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]]:
|
||||
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.
|
||||
|
||||
@@ -266,9 +280,14 @@ class IcnsFile:
|
||||
|
||||
return order
|
||||
|
||||
def _export_single(self, outdir: str, key: IcnsType.Media.KeyT,
|
||||
key_suffix: bool, decompress: bool,
|
||||
allowed: List[str]) -> Optional[str]:
|
||||
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)
|
||||
@@ -294,9 +313,13 @@ class IcnsFile:
|
||||
fp.write(data)
|
||||
return fname
|
||||
|
||||
def _export_to_png(self, outdir: str, img_key: IcnsType.Media.KeyT,
|
||||
def _export_to_png(
|
||||
self,
|
||||
outdir: str,
|
||||
img_key: IcnsType.Media.KeyT,
|
||||
mask_key: Optional[IcnsType.Media.KeyT],
|
||||
key_suffix: bool) -> Optional[str]:
|
||||
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]:
|
||||
|
||||
@@ -4,7 +4,7 @@ Namespace for the ICNS format.
|
||||
@see https://en.wikipedia.org/wiki/Apple_Icon_Image_format
|
||||
'''
|
||||
import os # path
|
||||
from typing import Union, Optional, Tuple, Iterator, List, Iterable, Dict
|
||||
from typing import Union, Optional, Tuple, Iterator, List, Iterable
|
||||
from . import PackBytes, RawData
|
||||
|
||||
|
||||
@@ -17,10 +17,17 @@ class Media:
|
||||
__slots__ = ['key', 'types', 'size', 'channels', 'bits', 'availability',
|
||||
'desc', 'compressable', 'retina', 'maxsize', 'ext_certain']
|
||||
|
||||
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:
|
||||
def __init__(
|
||||
self,
|
||||
key: KeyT,
|
||||
types: List[str],
|
||||
size: Union[int, Tuple[int, int], None] = None,
|
||||
*,
|
||||
ch: Optional[int] = None,
|
||||
bits: Optional[int] = None,
|
||||
os: Optional[float] = None,
|
||||
desc: str = '',
|
||||
) -> None:
|
||||
self.key = key
|
||||
self.types = types
|
||||
self.size = (size, size) if isinstance(size, int) else size
|
||||
@@ -54,8 +61,8 @@ class Media:
|
||||
return self.desc # guaranteed to be icon, mask, or iconmask
|
||||
return self.types[-1]
|
||||
|
||||
def decompress(self, data: bytes, ext: Optional[str] = '-?-') -> Optional[
|
||||
List[int]]:
|
||||
def decompress(self, data: bytes, ext: Optional[str] = '-?-') \
|
||||
-> Optional[List[int]]:
|
||||
''' Returns None if media is not decompressable. '''
|
||||
if self.compressable:
|
||||
if ext == '-?-':
|
||||
@@ -165,11 +172,11 @@ _TYPES = {x.key: x for x in (
|
||||
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: Iterable[Media.KeyT]) -> Iterator[
|
||||
Tuple[Optional[str], Optional[str]]]:
|
||||
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'),
|
||||
@@ -186,8 +193,8 @@ def enum_img_mask_pairs(available_keys: Iterable[Media.KeyT]) -> Iterator[
|
||||
yield None, mk
|
||||
|
||||
|
||||
def enum_png_convertable(available_keys: Iterable[Media.KeyT]) -> Iterator[
|
||||
Tuple[Media.KeyT, Optional[Media.KeyT]]]:
|
||||
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:
|
||||
@@ -219,8 +226,8 @@ def key_from_readable(key: str) -> Media.KeyT:
|
||||
'selected': 'slct',
|
||||
'template': 'sbtp',
|
||||
'toc': 'TOC ',
|
||||
} # type: Dict[str, Media.KeyT]
|
||||
return key_mapping.get(key.lower(), key)
|
||||
}
|
||||
return key_mapping.get(key.lower(), key) # type: ignore[return-value]
|
||||
|
||||
|
||||
def match_maxsize(total: int, typ: str) -> Media:
|
||||
|
||||
@@ -7,7 +7,7 @@ def pack(data: List[int]) -> bytes:
|
||||
buf = [] # type: List[int]
|
||||
i = 0
|
||||
|
||||
def flush_buf():
|
||||
def flush_buf() -> None:
|
||||
# write out non-repeating bytes
|
||||
if len(buf) > 0:
|
||||
ret.append(len(buf) - 1)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import struct # pack, unpack
|
||||
from typing import Union, Optional, Tuple, Iterator, BinaryIO
|
||||
from typing import Optional, Tuple, Iterator, BinaryIO
|
||||
from . import IcnsType, PackBytes
|
||||
|
||||
|
||||
@@ -85,8 +85,9 @@ def icns_header_read(data: bytes) -> Tuple[IcnsType.Media.KeyT, int]:
|
||||
return data[:4], length # Fallback to bytes-string key
|
||||
|
||||
|
||||
def icns_header_write_data(fp: BinaryIO, key: IcnsType.Media.KeyT,
|
||||
data: bytes) -> None:
|
||||
def icns_header_write_data(
|
||||
fp: BinaryIO, key: IcnsType.Media.KeyT, data: bytes,
|
||||
) -> None:
|
||||
''' Calculates length from data. '''
|
||||
fp.write(key.encode('utf8') if isinstance(key, str) else key)
|
||||
fp.write(struct.pack('>I', len(data) + 8))
|
||||
|
||||
@@ -4,9 +4,8 @@ Export existing icns files or compose new ones.
|
||||
'''
|
||||
import os # path, makedirs
|
||||
import sys # path, stderr
|
||||
from typing import Iterator, Optional, Callable
|
||||
from argparse import ArgumentParser, ArgumentTypeError, RawTextHelpFormatter
|
||||
from argparse import Namespace as ArgParams
|
||||
from typing import Iterator, Optional, Callable, List
|
||||
from argparse import ArgumentParser, ArgumentTypeError, Namespace as ArgParams
|
||||
if __name__ == '__main__':
|
||||
sys.path[0] = os.path.dirname(sys.path[0])
|
||||
from icnsutil import __version__, IcnsFile, IcnsType, ArgbImage
|
||||
@@ -53,16 +52,15 @@ def cli_update(args: ArgParams) -> None:
|
||||
has_changes |= icns.remove_media(IcnsType.key_from_readable(x))
|
||||
# add media
|
||||
for key_val in args.set or []:
|
||||
def fail():
|
||||
raise ArgumentTypeError(
|
||||
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
|
||||
if key_val.lower() == 'toc':
|
||||
key_val = 'toc=1'
|
||||
if '=' not in key_val:
|
||||
fail()
|
||||
key, val = key_val.split('=')
|
||||
raise ArgumentTypeError(
|
||||
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
|
||||
key, val = key_val.split('=', 1)
|
||||
if not val:
|
||||
fail()
|
||||
raise ArgumentTypeError(
|
||||
'Expected arg format KEY=FILE - got "{}"'.format(key_val))
|
||||
|
||||
has_changes = True
|
||||
if key.lower() == 'toc':
|
||||
@@ -130,7 +128,7 @@ def cli_convert(args: ArgParams) -> None:
|
||||
exit(1)
|
||||
|
||||
|
||||
def enum_with_stdin(file_arg: list) -> Iterator[str]:
|
||||
def enum_with_stdin(file_arg: List[str]) -> Iterator[str]:
|
||||
for x in file_arg:
|
||||
if x == '-':
|
||||
for line in sys.stdin.readlines():
|
||||
@@ -155,18 +153,18 @@ def main() -> None:
|
||||
return path
|
||||
|
||||
# Args Parser
|
||||
parser = ArgumentParser(description=__doc__,
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser = ArgumentParser(description=__doc__)
|
||||
parser.set_defaults(func=lambda _: parser.print_help(sys.stdout))
|
||||
parser.add_argument(
|
||||
'-v', '--version', action='version', version='icnsutil ' + __version__)
|
||||
sub_parser = parser.add_subparsers(metavar='command')
|
||||
|
||||
# helper method
|
||||
def add_command(name: str, alias: str, fn: Callable[[ArgParams], None]):
|
||||
def add_command(
|
||||
name: str, alias: str, fn: Callable[[ArgParams], None]
|
||||
) -> ArgumentParser:
|
||||
desc = fn.__doc__ or ''
|
||||
cmd = sub_parser.add_parser(name, aliases=[alias],
|
||||
formatter_class=RawTextHelpFormatter,
|
||||
help=desc, description=desc.strip())
|
||||
cmd.set_defaults(func=fn)
|
||||
return cmd
|
||||
@@ -189,22 +187,20 @@ def main() -> None:
|
||||
# Compose
|
||||
cmd = add_command('compose', 'c', cli_compose)
|
||||
cmd.add_argument('-f', '--force', action='store_true',
|
||||
help='force overwrite output file')
|
||||
cmd.add_argument('--toc', action='store_true',
|
||||
help='write table of contents to file')
|
||||
help='Force overwrite output file')
|
||||
cmd.add_argument('--toc', action='store_true', help='''
|
||||
Write table of contents to file.
|
||||
TOC is optional and uses just a few bytes (8b per media entry).''')
|
||||
cmd.add_argument('target', type=str, metavar='destination',
|
||||
help='Output file for newly created icns file.')
|
||||
cmd.add_argument('source', type=PathExist('f', stdin=True), nargs='+',
|
||||
metavar='src',
|
||||
help='One or more media files: png, argb, rgb, jp2, icns')
|
||||
cmd.epilog = '''
|
||||
Notes:
|
||||
- TOC is optional but only a few bytes long (8b per media entry).
|
||||
- Icon dimensions are read directly from file.
|
||||
- Filename suffix "@2x.png" or "@2x.jp2" sets the retina flag.
|
||||
- Use one of these suffixes to automatically assign icns files:
|
||||
template, selected, dark
|
||||
'''
|
||||
metavar='src', help='''
|
||||
One or more media files: png, argb, rgb, jp2, icns.
|
||||
--
|
||||
Icon dimensions are read directly from file.
|
||||
Filename suffixes "@2x.png" and "@2x.jp2" will set the retina flag.
|
||||
If the suffix ends on one of these (template, selected, dark),
|
||||
the file is automatically assigned to an icns file field.''')
|
||||
|
||||
# Update
|
||||
cmd = add_command('update', 'u', cli_update)
|
||||
|
||||
Reference in New Issue
Block a user