diff --git a/src/tests/test_image_cli.py b/src/tests/test_image_cli.py index 92afa1a..4495047 100644 --- a/src/tests/test_image_cli.py +++ b/src/tests/test_image_cli.py @@ -132,6 +132,7 @@ def test_image_png_cli_strict_nopalette(parsers, test_input_file): with pytest.raises(TypeError): image.run(args) + def test_image_png_cli_strict_palette_pal_transparent(parsers, test_input_file): from ttblit.asset import image @@ -166,6 +167,7 @@ def test_image_png_cli_strict_palette_pal_transparent(parsers, test_input_file): image.run(args) + def test_image_png_cli_packed_blank(parsers): from ttblit.asset import image @@ -182,6 +184,7 @@ def test_image_png_cli_packed_blank(parsers): image.run(args) + def test_image_png_cli_packed_multi_transparent(parsers): from ttblit.asset import image diff --git a/src/tests/test_metadata.py b/src/tests/test_metadata.py new file mode 100644 index 0000000..de69362 --- /dev/null +++ b/src/tests/test_metadata.py @@ -0,0 +1,89 @@ +import argparse +import base64 +import tempfile + +import pytest + + +@pytest.fixture +def parsers(): + parser = argparse.ArgumentParser() + parser.add_argument('--debug', action='store_true', help='Enable exception traces') + return parser, parser.add_subparsers(dest='command', help='Commands') + + +@pytest.fixture +def test_binary_file(): + temp_bin = tempfile.NamedTemporaryFile('wb', suffix='.bin') + temp_bin.write(b'BLIT000000000000\x14\x00\x00\x00') + temp_bin.flush() + return temp_bin + + +@pytest.fixture +def test_invalid_binary_file(): + temp_bin = tempfile.NamedTemporaryFile('wb', suffix='.bin') + temp_bin.write(b'BLIT000000000000\x10\x00\x00\x00') + temp_bin.flush() + return temp_bin + + +@pytest.fixture +def test_metadata_file(): + temp_png = tempfile.NamedTemporaryFile('wb', suffix='.png') + temp_png.write(base64.b64decode(b'iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ4IDc5LjE2NDAzNiwgMjAxOS8wOC8xMy0wMTowNjo1NyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE5NkU4OENBNTk3NDExRUFCMTgyODFBRDFGMTZDODJGIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjE5NkU4OENCNTk3NDExRUFCMTgyODFBRDFGMTZDODJGIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTk2RTg4Qzg1OTc0MTFFQUIxODI4MUFEMUYxNkM4MkYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MTk2RTg4Qzk1OTc0MTFFQUIxODI4MUFEMUYxNkM4MkYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4ohDCNAAAACVBMVEUAAAD///8A/wDg4n4DAAAAmklEQVR42uzZMQqAMAxA0er9D+1Wl1ITijTi+6PQ8qYYsTVJUu/Ilj569gAAAABqAtIDM30UAAAAoDrAuwAAAODLK9nCdAQAAACoBHh/FD84AQAAALYDJEnpQTk6MPgCD98LAAAAUAiQXkXT9wIAAADUBEx/qEQBg2fhUQwAAAAAAAAAAHADFnbCKSC8EwIAAABsAkjST7sEGACd4xph9WtahAAAAABJRU5ErkJggg==')) + temp_png.flush() + + temp_yml = tempfile.NamedTemporaryFile('w', suffix='.yml') + temp_yml.write(f'''title: Rocks & Diamonds +description: A pulse pounding, rock rollin', diamond hunting adventure +splash: + file: {temp_png.name} +version: v1.0.0 +''') + temp_yml.flush() + return temp_yml, temp_png + + +def test_metadata_no_args(parsers): + from ttblit.tool import metadata + + parser, subparser = parsers + + metadata = metadata.Metadata(subparser) + + with pytest.raises(SystemExit): + parser.parse_args(['metadata']) + + +def test_metadata(parsers, test_metadata_file, test_binary_file): + from ttblit.tool import metadata + + test_metadata_file, test_metadata_splash_png = test_metadata_file + parser, subparser = parsers + + metadata = metadata.Metadata(subparser) + + args = parser.parse_args([ + 'metadata', + '--config', test_metadata_file.name, + '--file', test_binary_file.name]) + + metadata.run(args) + + +def test_metadata_invalid_bin(parsers, test_metadata_file, test_invalid_binary_file): + from ttblit.tool import metadata + + test_metadata_file, test_metadata_splash_png = test_metadata_file + parser, subparser = parsers + + metadata = metadata.Metadata(subparser) + + args = parser.parse_args([ + 'metadata', + '--config', test_metadata_file.name, + '--file', test_invalid_binary_file.name]) + + with pytest.raises(ValueError): + metadata.run(args) diff --git a/src/tests/test_setup.py b/src/tests/test_setup.py index 62b3ede..dc0b0ec 100644 --- a/src/tests/test_setup.py +++ b/src/tests/test_setup.py @@ -21,11 +21,13 @@ def test_cmake_instance(subparser): cmake.CMake(subparser) + def test_flasher_instance(subparser): from ttblit.tool import flasher flasher.Flasher(subparser) + def test_image_instance(subparser): from ttblit.asset import image diff --git a/src/ttblit/__init__.py b/src/ttblit/__init__.py index b66f7cc..12460d1 100644 --- a/src/ttblit/__init__.py +++ b/src/ttblit/__init__.py @@ -6,7 +6,7 @@ import sys from .asset import font, image, map, raw -from .tool import cmake, flasher, packer +from .tool import cmake, flasher, metadata, packer def exception_handler(exception_type, exception, traceback): @@ -41,6 +41,7 @@ def main(): tools[packer.Packer.command] = _packer tools[cmake.CMake.command] = cmake.CMake(subparsers) tools[flasher.Flasher.command] = flasher.Flasher(subparsers) + tools[metadata.Metadata.command] = metadata.Metadata(subparsers) args = parser.parse_args() @@ -50,4 +51,4 @@ def main(): if args.command is None: parser.print_help() else: - tools[args.command].run(args) + sys.exit(tools[args.command].run(args)) diff --git a/src/ttblit/asset/font.py b/src/ttblit/asset/font.py index 142881b..cbe36c9 100644 --- a/src/ttblit/asset/font.py +++ b/src/ttblit/asset/font.py @@ -1,8 +1,8 @@ import io import struct -from PIL import Image import freetype +from PIL import Image from ..core.assetbuilder import AssetBuilder @@ -13,7 +13,7 @@ class FontAsset(AssetBuilder): types = ['image', 'font'] typemap = { 'image': ('.png', '.gif'), - 'font': ('.ttf') # possibly other freetype supported formats... + 'font': ('.ttf') # possibly other freetype supported formats... } def __init__(self, parser): @@ -53,7 +53,7 @@ def process_image_font(self, input_data): char_height = h font_data = [] - font_w = [] # per character width for variable-width mode + font_w = [] # per character width for variable-width mode for c in range(0, self.num_chars): char_w = 0 @@ -76,7 +76,7 @@ def process_image_font(self, input_data): font_data.append(byte) - if c == 0: # space + if c == 0: # space font_w.append(self.space_width) else: font_w.append(char_w + self.horizontal_spacing) @@ -112,7 +112,7 @@ def process_ft_font(self, input_data): if self.height - face.glyph.bitmap_top < min_y: min_y = self.height - face.glyph.bitmap_top - char_height -= min_y # trim empty space at the top + char_height -= min_y # trim empty space at the top font_data = [] diff --git a/src/ttblit/asset/map.py b/src/ttblit/asset/map.py index 64faf97..26a26b6 100644 --- a/src/ttblit/asset/map.py +++ b/src/ttblit/asset/map.py @@ -20,7 +20,7 @@ def tiled_to_binary(self, input_data): layer.text, base=10, offset=-1)) # Tiled indexes from 1 rather than 0 - except ValueError as e: + except ValueError: raise RuntimeError("Failed to convert .tmx, does it contain blank (0) tiles? Tiled is 1-indexed, so these get converted to -1 and blow everyting up") return b''.join(data) diff --git a/src/ttblit/tool/metadata.py b/src/ttblit/tool/metadata.py new file mode 100644 index 0000000..45a7653 --- /dev/null +++ b/src/ttblit/tool/metadata.py @@ -0,0 +1,122 @@ +import argparse +import pathlib +import struct + +import yaml + +from ..asset.image import ImageAsset +from ..core.tool import Tool + + +class Metadata(Tool): + command = 'metadata' + help = 'Tag a 32Blit .bin file with metadata' + + def __init__(self, parser): + Tool.__init__(self, parser) + self.parser.add_argument('--config', required=True, type=pathlib.Path, help='Metadata config file') + self.parser.add_argument('--file', required=True, type=pathlib.Path, help='Input file') + self.parser.add_argument('--force', action='store_true', help='Force file overwrite') + + self.config = {} + + def parse_config(self, config_file): + config = open(config_file).read() + config = yaml.safe_load(config) + + required = ['title', 'description', 'version'] + + for option in required: + if option not in config: + raise ValueError(f'Missing required option "{option}" from {config_file}') + + self.config = config + + def prepare_image_asset(self, name, config): + image_file = pathlib.Path(config.get('file', '')) + config['input_file'] = image_file + config['output_file'] = image_file.with_suffix('.bin') + if not image_file.is_file(): + raise ValueError(f'{name} "{image_file}" does not exist!') + asset = ImageAsset(argparse.ArgumentParser().add_subparsers()) + asset.prepare(config) + + return asset.to_binary(open(image_file, 'rb').read()) + + def binary_size(self, bin): + return struct.unpack(' binary_size: + if bin[binary_size:binary_size + 8] == meta_header: + has_meta = True + bin = bin[:binary_size] + else: + raise ValueError(f'Invalid 32blit binary file {args.file}, expected {binary_size} bytes') + print(f'Using bin file at {args.file}') + else: + raise ValueError(f'Invalid 32blit binary file {args.file}') + else: + print(f'Unable to find bin file at {args.file}') + + if args.config.is_file(): + self.parse_config(args.config) + print(f'Using config at {args.config}') + else: + print(f'Unable to find metadata config at {args.config}') + + if 'icon' in self.config: + icon = self.prepare_image_asset('icon', self.config['icon']) + + if 'splash' in self.config: + splash = self.prepare_image_asset('splash', self.config['splash']) + + title = bytes(self.config.get('title').encode('utf-8')) + description = bytes(self.config.get('description').encode('utf-8')) + version = bytes(self.config.get('version').encode('utf-8')) + + if len(title) > 64: + raise ValueError('Title should be a maximum of 64 characters!"') + + if len(description) > 1024: + raise ValueError('Description should be a maximum of 1024 characters!') + + if len(version) > 16: + raise ValueError('Version should be a maximum of 16 characters! eg: "v1.0.2"') + + metadata = title + eof + metadata += description + eof + metadata += version + eof + metadata += icon + metadata += splash + + length = struct.pack('H', len(metadata)) + + metadata = meta_header + length + metadata + + if has_meta: + if not args.force: + print(f'Refusing to overwrite metadata in {args.file}') + return 1 + + print(f'Adding metadata to {args.file}') + bin = bin + metadata + open(args.file, 'wb').write(bin) + + return 0