generated from pimoroni/boilerplate-python
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from pimoroni/blit-metadata
Add metadata packing tool
- Loading branch information
Showing
7 changed files
with
225 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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('<I', bin[16:20])[0] & 0xffffff | ||
|
||
def run(self, args): | ||
self.working_path = pathlib.Path('.') | ||
game_header = bytes('BLIT'.encode('utf-8')) | ||
meta_header = bytes('BLITMETA'.encode('utf-8')) | ||
eof = bytes('\0'.encode('utf-8')) | ||
has_meta = False | ||
|
||
icon = bytes() | ||
splash = bytes() | ||
bin = bytes() | ||
|
||
if args.file.is_file(): | ||
bin = open(args.file, 'rb').read() | ||
if bin.startswith(game_header): | ||
binary_size = self.binary_size(bin) | ||
if len(bin) == binary_size: | ||
has_meta = False | ||
elif len(bin) > 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 |