Smeargle 0.5.0

current
Kiyoshi Aman 2018-03-14 10:53:03 -05:00
parent 46eb139637
commit 49aad5bdbb
3 changed files with 135 additions and 53 deletions

11
example.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "Example",
"fonts": {
"Melissa 8": "melissa8.json"
}, "scripts": {
"test.txt": {
"max_tiles_per_line": 8,
"font": "Melissa 8"
}
}
}

View File

@ -1,16 +1,12 @@
Smeargle 0.4.0 readme Smeargle 0.4.0 readme
--------------------- ---------------------
Usage: smeargle.py font.json script.txt Usage: smeargle.py game.json
font.json is a JSON document which describes the font and provides a mapping game.json is a file which follows the Game JSON format outlined below.
of characters to font indexes and font widths.
script.txt is a plaintext document without formatting which is rendered as an
image in PNG with a limited palette.
Output Output
------ ------
Smeargle outputs three files, at present: Smeargle outputs three files per script, at present:
* <script>_raw.png is the undeduplicated rendering of the script with the * <script>_raw.png is the undeduplicated rendering of the script with the
specified font. specified font.
@ -19,12 +15,31 @@ Smeargle outputs three files, at present:
* <script>_index.txt provides a mapping of deduplicated tiles to the original * <script>_index.txt provides a mapping of deduplicated tiles to the original
text. text.
game.json format
----------------
The following format MUST be observed, or you will not get the output you want.
Remove '//' and everything following it in each line if you plan to cop/paste
this example for your own use. Do not leave a trailing comma on the final entry
in each object or array.
{
"name": "Example", // The name of the game, for reference.
"fonts": {
"Melissa 8": "melissa8.json" // Font name and its filename.
}, "scripts": {
"test.txt": { // Script filename.
"max_tiles_per_line": 8, // Omit or set to 0 for unlimited tiles.
"font": "Melissa 8" // Reference to the font table, above.
}
}
}
font.json format font.json format
---------------- ----------------
The following format MUST be observed, or you will not get the output you want. The following format MUST be observed, or you will not get the output you want.
Remove '//' and everything following it in each line if you plan to copy/paste Remove '//' and everything following it in each line if you plan to copy/paste
this example for your own use. Do not leave a trailing comma on the final entry this example for your own use. Do not leave a trailing comma on the final entry
in the map. in each object or array.
{ {
"font_name": "Example", // Human-readable, not currently used "font_name": "Example", // Human-readable, not currently used
@ -37,10 +52,12 @@ in the map.
[0, 0, 0] // A color in R,G,B format. [0, 0, 0] // A color in R,G,B format.
], ],
"map": { // character -> index & width "map": { // character -> index & width
" ": {"index": 115, "width": 4}, // Must be a blank tile somewher " ": {"index": 115, "width": 4}, // Must be a blank tile somewhere
} }
} }
The first color in the palette is assumed to be the background color.
porygon.py porygon.py
---------- ----------
Usage: porygon.py image format Usage: porygon.py image format
@ -50,6 +67,12 @@ arguments to see what formats are available.
Changelog Changelog
--------- ---------
0.5.0
* Introduce a master 'game.json' file in order to enable batch processing, for
games which use multiple scripts that have different fonts or rendering
requirements.
* Emit an error if no arguments are given.
0.4.0 0.4.0
* A complete rewrite of Smeargle to make it more modular. * A complete rewrite of Smeargle to make it more modular.
* Implemented a palette feature in order to ensure strict palette ordering in * Implemented a palette feature in order to ensure strict palette ordering in

View File

@ -72,7 +72,8 @@ class Font:
return sum(self.table[x]['width'] for x in text) return sum(self.table[x]['width'] for x in text)
class Script: class Script:
def __init__(self, filename): def __init__(self, filename, max_tiles=0):
self.max_tiles = max_tiles
with open(filename, mode='r', encoding='UTF-8') as f: with open(filename, mode='r', encoding='UTF-8') as f:
self._text = f.read().split('\n') self._text = f.read().split('\n')
@ -81,12 +82,20 @@ class Script:
def render_lines(self, font): def render_lines(self, font):
table = font.table table = font.table
lines = [] lines = []
max_tiles = self.max_tiles * font.width
for line in self._text: for line in self._text:
if len(line) < 1: if len(line) < 1:
continue continue
length = font.length(line) length = font.length(line)
length = ceil(length / font.width) * font.width length = ceil(length / font.width) * font.width
if max_tiles > 0 and length > max_tiles:
print('WARNING: "{}" exceeds {} tiles by {}px; truncating.'.format(
line,
max_tiles,
length - max_tiles
))
length = max_tiles
image = QImage(length, font.height, QImage.Format_RGB32) image = QImage(length, font.height, QImage.Format_RGB32)
image.fill(font.palette[0]) image.fill(font.palette[0])
pos = 0 pos = 0
@ -94,6 +103,8 @@ class Script:
self._painter.begin(image) self._painter.begin(image)
for glyph in line: for glyph in line:
width = font.table[glyph]['width'] width = font.table[glyph]['width']
if pos + width >= max_tiles:
break
self._painter.drawImage(pos, 0, font.index(font.table[glyph]['index'] - 1)) self._painter.drawImage(pos, 0, font.index(font.table[glyph]['index'] - 1))
pos += width pos += width
@ -174,59 +185,96 @@ class Script:
def render_tiles_to_file(self, font, tiles, filename): def render_tiles_to_file(self, font, tiles, filename):
self.render_tiles(font, tiles).save(filename, 'PNG') self.render_tiles(font, tiles).save(filename, 'PNG')
class Game:
def __init__(self, filename):
with open(filename, mode='rb') as f:
self._data = json.load(f)
self._fonts = {}
self._scripts = {}
for name, file in self._data['fonts']:
self._fonts[name] = Font(file)
for script, data in self._data['scripts']:
if 'max_tiles_per_line' not in data:
data['max_tiles_per_line'] = 0
self._scripts[script] = (
Script(script, data['max_tiles_per_line']),
self._fonts[data['font']]
)
@property
def fonts(self):
return tuple(self._fonts.keys())
@property
def scripts(self):
return tuple(self._scripts.keys())
def render_script(self, script, output=False):
if script not in self._scripts.keys():
raise KeyError('unknown script')
filebase = os.path.split(script)[-1]
name, ext = os.path.splitext(filebase)
output_raw = os.path.join(render_path, name + '_raw.png')
output_comp = os.path.join(render_path, name + '_compressed.png')
output_map = os.path.join(render_path, name + '_index.txt')
script, font = self._scripts[script]
if output: print('Rendering text...', end='')
lines = script.render_lines(font)
if output: print('done.')
if output: print("Generating tilemap...", end='')
(compressed, raw, map_index, indexes, total, unique) = script.generate_tilemap(font, lines)
if output: print("{} tiles generated, {} unique.".format(total, unique))
if output: print('Writing compressed tiles...', end='')
script.render_tiles_to_file(font, compressed, output_comp)
if output: print('done.')
if output: print('Writing raw tiles...', end='')
script.render_tiles_to_file(font, raw, output_raw)
if output: print('done.')
if output: print('Writing map index...', end='')
with open(output_map, mode='wt') as f:
for text, index in indexes:
f.write('{} = {}\n'.format(text, index))
if output: print('done.')
if output:
print()
print('Raw tiles: ', output_raw)
print('Compressed: ', output_comp)
print('Tile<->text: ', output_map)
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
import os.path import os.path
if len(sys.argv) < 1: if len(sys.argv) < 1:
print('Usage: smeargle.py font.json script.txt') print('Usage: smeargle.py game.json [output_directory]')
print('\nPlease see the included readme.txt for documentation on the font metadata.') print('\nPlease see the included readme.txt for documentation on file formats.')
sys.exit(-1) sys.exit(-1)
app = QGuiApplication(sys.argv) app = QGuiApplication(sys.argv)
render_path = sys.argv[2] if len(sys.argv) > 2 else 'output'
font = sys.argv[1] print('Loading game data from {}...'.format(sys.argv[1]), end='')
script = sys.argv[2] game = Game(sys.argv[1])
render_path = sys.argv[3] if len(sys.argv) > 3 else 'output'
filebase = os.path.split(script)[-1]
name, ext = os.path.splitext(filebase)
output_raw = os.path.join(render_path, name + '_raw.png')
output_comp = os.path.join(render_path, name + '_compressed.png')
output_map = os.path.join(render_path, name + '_index.txt')
print("Loading font...", end='')
font = Font(font)
print("done.")
print("Loading script...", end='')
script = Script(script)
print("done.")
print("Rendering text...", end='')
lines = script.render_lines(font)
print("done.")
print("Generating tilemap...", end='')
(compressed, raw, map_index, indexes, total, unique) = script.generate_tilemap(font, lines)
print("{} tiles generated, {} unique.".format(total, unique))
print('Writing compressed tiles...', end='')
script.render_tiles_to_file(font, compressed, output_comp)
print('done.') print('done.')
print('Writing raw tiles...', end='') for script in game.scripts():
script.render_tiles_to_file(font, raw, output_raw) print('Processing {}...'.format(script))
print('done.') game.render_script(script, output=True)
print('{} processed.'.format(script))
print('Writing map index...', end='') print("Rendering text...", end='')
with open(output_map, mode='wt') as f: lines = script.render_lines(font)
for text, index in indexes: print("done.")
f.write('{} = {}\n'.format(text, index))
print('done.')
print()
print('Raw tiles: ', output_raw)
print('Compressed: ', output_comp)
print('Tile<->text: ', output_map)