Skip to content

Commit

Permalink
Merge pull request #16 from pimoroni/blit-metadata
Browse files Browse the repository at this point in the history
Add metadata packing tool
  • Loading branch information
Gadgetoid committed Sep 27, 2020
2 parents 46750e9 + 90bfa73 commit 965cfc3
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 8 deletions.
3 changes: 3 additions & 0 deletions src/tests/test_image_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
89 changes: 89 additions & 0 deletions src/tests/test_metadata.py
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)
2 changes: 2 additions & 0 deletions src/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions src/ttblit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()

Expand All @@ -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))
10 changes: 5 additions & 5 deletions src/ttblit/asset/font.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import io
import struct

from PIL import Image
import freetype
from PIL import Image

from ..core.assetbuilder import AssetBuilder

Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 = []

Expand Down
2 changes: 1 addition & 1 deletion src/ttblit/asset/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
122 changes: 122 additions & 0 deletions src/ttblit/tool/metadata.py
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

0 comments on commit 965cfc3

Please sign in to comment.