Smeargle 0.5.0
parent
46eb139637
commit
49aad5bdbb
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "Example",
|
||||||
|
"fonts": {
|
||||||
|
"Melissa 8": "melissa8.json"
|
||||||
|
}, "scripts": {
|
||||||
|
"test.txt": {
|
||||||
|
"max_tiles_per_line": 8,
|
||||||
|
"font": "Melissa 8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
readme.txt
41
readme.txt
|
@ -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
|
||||||
|
|
128
smeargle.py
128
smeargle.py
|
@ -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,20 +185,36 @@ 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')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
class Game:
|
||||||
import sys
|
def __init__(self, filename):
|
||||||
import os.path
|
with open(filename, mode='rb') as f:
|
||||||
|
self._data = json.load(f)
|
||||||
|
|
||||||
if len(sys.argv) < 1:
|
self._fonts = {}
|
||||||
print('Usage: smeargle.py font.json script.txt')
|
self._scripts = {}
|
||||||
print('\nPlease see the included readme.txt for documentation on the font metadata.')
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
app = QGuiApplication(sys.argv)
|
for name, file in self._data['fonts']:
|
||||||
|
self._fonts[name] = Font(file)
|
||||||
|
|
||||||
font = sys.argv[1]
|
for script, data in self._data['scripts']:
|
||||||
script = sys.argv[2]
|
if 'max_tiles_per_line' not in data:
|
||||||
render_path = sys.argv[3] if len(sys.argv) > 3 else 'output'
|
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]
|
filebase = os.path.split(script)[-1]
|
||||||
name, ext = os.path.splitext(filebase)
|
name, ext = os.path.splitext(filebase)
|
||||||
|
@ -196,37 +223,58 @@ if __name__ == '__main__':
|
||||||
output_comp = os.path.join(render_path, name + '_compressed.png')
|
output_comp = os.path.join(render_path, name + '_compressed.png')
|
||||||
output_map = os.path.join(render_path, name + '_index.txt')
|
output_map = os.path.join(render_path, name + '_index.txt')
|
||||||
|
|
||||||
print("Loading font...", end='')
|
script, font = self._scripts[script]
|
||||||
font = Font(font)
|
|
||||||
print("done.")
|
|
||||||
|
|
||||||
print("Loading script...", end='')
|
if output: print('Rendering text...', end='')
|
||||||
script = Script(script)
|
lines = script.render_lines(font)
|
||||||
print("done.")
|
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__':
|
||||||
|
import sys
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
if len(sys.argv) < 1:
|
||||||
|
print('Usage: smeargle.py game.json [output_directory]')
|
||||||
|
print('\nPlease see the included readme.txt for documentation on file formats.')
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
app = QGuiApplication(sys.argv)
|
||||||
|
render_path = sys.argv[2] if len(sys.argv) > 2 else 'output'
|
||||||
|
|
||||||
|
print('Loading game data from {}...'.format(sys.argv[1]), end='')
|
||||||
|
game = Game(sys.argv[1])
|
||||||
|
print('done.')
|
||||||
|
|
||||||
|
for script in game.scripts():
|
||||||
|
print('Processing {}...'.format(script))
|
||||||
|
game.render_script(script, output=True)
|
||||||
|
print('{} processed.'.format(script))
|
||||||
|
|
||||||
print("Rendering text...", end='')
|
print("Rendering text...", end='')
|
||||||
lines = script.render_lines(font)
|
lines = script.render_lines(font)
|
||||||
print("done.")
|
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('Writing raw tiles...', end='')
|
|
||||||
script.render_tiles_to_file(font, raw, output_raw)
|
|
||||||
print('done.')
|
|
||||||
|
|
||||||
print('Writing map index...', end='')
|
|
||||||
with open(output_map, mode='wt') as f:
|
|
||||||
for text, index in indexes:
|
|
||||||
f.write('{} = {}\n'.format(text, index))
|
|
||||||
print('done.')
|
|
||||||
|
|
||||||
print()
|
|
||||||
print('Raw tiles: ', output_raw)
|
|
||||||
print('Compressed: ', output_comp)
|
|
||||||
print('Tile<->text: ', output_map)
|
|
||||||
|
|
Loading…
Reference in New Issue