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