Files
icnsutil/icnsutil/cli.py
2021-09-30 01:31:27 +02:00

181 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
'''
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) -> 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)):
# PathExist ensures that all files and directories exist
out = args.export_dir
if out and multiple:
out = os.path.join(out, str(i))
os.makedirs(out, exist_ok=True)
IcnsFile(fname).export(
out, allowed_ext='png' if args.png_only else '*',
recursive=args.recursive, convert_png=args.convert,
key_suffix=args.keys)
def cli_compose(args) -> None:
''' Create new icns file from provided image files. '''
dest = args.target
if not os.path.splitext(dest)[1]:
dest += '.icns' # for the lazy people
if not args.force and os.path.exists(dest):
print(
'File "{}" already exists. Force overwrite with -f.'.format(dest),
file=sys.stderr)
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) -> 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) -> None:
''' Test if icns file is valid. '''
for fname in enum_with_stdin(args.file):
is_valid = True # type: Optional[bool]
if not args.quiet:
print('File:', fname)
is_valid = None
for issue in IcnsFile.verify(fname):
if is_valid:
print('File:', fname)
is_valid = False
print(' ', issue)
if not args.quiet and is_valid is not False:
print('OK')
def enum_with_stdin(file_arg: list) -> Iterator[str]:
for x in file_arg:
if x == '-':
for line in sys.stdin.readlines():
yield line.strip()
else:
yield x
def main() -> None:
class PathExist:
def __init__(self, kind: Optional[str] = None, stdin: bool = False):
self.kind = kind
self.stdin = stdin
def __call__(self, path: str) -> str:
if self.stdin and path == '-':
return '-'
if not os.path.exists(path) or \
self.kind == 'f' and not os.path.isfile(path) or \
self.kind == 'd' and not os.path.isdir(path):
raise ArgumentTypeError('Does not exist "{}"'.format(path))
return path
# Args Parser
parser = ArgumentParser(description=__doc__,
formatter_class=RawTextHelpFormatter)
parser.set_defaults(func=lambda _: parser.print_help(sys.stderr))
parser.add_argument(
'-v', '--version', action='version', version='icnsutil ' + __version__)
sub_parser = parser.add_subparsers(metavar='command')
# Extract
cmd = sub_parser.add_parser(
'extract', aliases=['e'], formatter_class=RawTextHelpFormatter,
help=cli_extract.__doc__, description=cli_extract.__doc__.strip())
cmd.add_argument(
'-r', '--recursive', action='store_true',
help='extract nested icns files as well')
cmd.add_argument(
'-o', '--export-dir', type=PathExist('d'), metavar='DIR',
help='set custom export directory')
cmd.add_argument(
'-k', '--keys', action='store_true',
help='use icns key as filenam')
cmd.add_argument(
'-c', '--convert', action='store_true',
help='convert ARGB and RGB images to PNG')
cmd.add_argument(
'--png-only', action='store_true',
help='do not extract ARGB, binary, and meta files')
cmd.add_argument(
'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE',
help='One or more .icns files.')
cmd.set_defaults(func=cli_extract)
# Compose
cmd = sub_parser.add_parser(
'compose', aliases=['c'], formatter_class=RawTextHelpFormatter,
help=cli_compose.__doc__, description=cli_compose.__doc__.strip())
cmd.add_argument(
'-f', '--force', action='store_true',
help='force overwrite output file')
cmd.add_argument(
'--no-toc', action='store_true',
help='do not write table of contents to file')
cmd.add_argument(
'target', type=str, metavar='destination',
help='Output file for newly created icns file.')
cmd.add_argument(
'source', nargs='+', type=PathExist('f', stdin=True), metavar='src',
help='One or more media files: png, argb, plist, icns.')
cmd.set_defaults(func=cli_compose)
cmd.epilog = '''
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
'''
# Print
cmd = sub_parser.add_parser(
'print', aliases=['p'], formatter_class=RawTextHelpFormatter,
help=cli_print.__doc__, description=cli_print.__doc__.strip())
cmd.add_argument(
'-v', '--verbose', action='store_true',
help='print all keys with offsets and sizes')
cmd.add_argument(
'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE',
help='One or more .icns files.')
cmd.set_defaults(func=cli_print)
# Verify
cmd = sub_parser.add_parser(
'test', aliases=['t'], formatter_class=RawTextHelpFormatter,
help=cli_verify.__doc__, description=cli_verify.__doc__.strip())
cmd.add_argument(
'-q', '--quiet', action='store_true',
help='do not print OK results')
cmd.add_argument(
'file', nargs='+', type=PathExist('f', stdin=True), metavar='FILE',
help='One or more .icns files.')
cmd.set_defaults(func=cli_verify)
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()