Add utility scripts for font processing

fontmap.py:
Maps glyph images to characters via the file name notation and then outputs a font json

fontutils.py:
Contains functions and variables that are used by both fontmap.py and glyphpos.py

glyphpos.py:
Since the game now draws glyphs directly from their image, a glyph’s placement within its image is very important. glyphpos.py is a helper script to mass (re)position the glyphs within their images.
This commit is contained in:
Nathan Fulton 2017-04-29 23:45:11 -04:00
parent 4896fbe384
commit e02296b6ac
3 changed files with 503 additions and 0 deletions

60
font/fontmap.py Normal file
View File

@ -0,0 +1,60 @@
import sys
import json
import fontutils
if __name__ == "__main__":
art_dir = sys.argv[1]
out_dir = sys.argv[2]
fonts = fontutils.get_fonts(art_dir)
errors = list()
for font in fonts:
glyph_map = {}
images = fontutils.images_in_dir(font.path)
for image in images:
# image_name that the character will be mapped to
# this is the name the game will use to load the glyph
image_name = "{}/{}".format(font.dir_name, image)
# Magically get the chracter name
char = fontutils.get_character(image, font)
if char == None:
errors.append(image)
continue
# Different characters can be mapped to the same glyph, but you
# can't have the same character mapped to different glyphs in the
# same font
if char in glyph_map:
print ("{}: trying to add \"{}\" with key \"{}\" but"
"\"{}\" already has been mapped to \"{}\""
.format(font.name,image_name, char, char, glyph_map[char]))
# Map it
glyph_map[char] = image_name
# It's important that the default character exists becuase it is drawn
# whenever a character can't be found in the map
if font.default_char not in glyph_map:
# If this is happening, make sure an image with the appropriate
# notation exists in the font's dir
print ("Unable to write {}.json: deafult char \"{}\""
"doesn't exist in map.".format(font.name, font.default_char))
continue
# Construct the object
j = {}
j['default'] = glyph_map[font.default_char]
j['height'] = fontutils.get_font_height(font, False)
j['glyph_overlap'] = font.glyph_overlap
j['line_overlap'] = font.line_overlap
j['glyph_map'] = glyph_map
with open("{}/{}.json".format(out_dir, font.name), 'w') as outfile:
json.dump(j, outfile, sort_keys = True)
if len(errors) > 0:
print "Unprocessed images:"
for fn in errors:
print fn

226
font/fontutils.py Normal file
View File

@ -0,0 +1,226 @@
import os
from PIL import Image
SUFFIX = ".png"
FONT_DIR_NAMES = [
"Button font",
"Hudfont",
"Shadowfont",
"Standard font",
"Title font"
]
SPECIAL_CHARS = [
('and symbol', '&'),
('apostrophe', '\''),
('astrex symbol', '*'),
('colon', ':'),
('comma', ','),
('dash', '-'),
('dollar sign', '$'),
('equal symbol', '='),
('exclamation mark', '!'),
('exlamation point', '!'),
('forward slash', '/'),
('galaxite_icon', '@'),
('number symbol', '#'),
('percentage', '%'),
('period', '.'),
('plus', '+'),
('question mark', '?'),
('reactor_icon', '\\'),
('semicolon', ';'),
('at symbol', '@'),
('dollar symbol', '$'),
('equal sign', '='),
# ('multiply symbol', 'X'),
('quotation mark', '"'),
('quotation marks', '"'),
('zero_null', '~'),
# ('empty sum symbol', None), # u'\u2205'
('left arrow bracket', '<'),
('right arrow bracket', '>'),
('left bracket', '('),
('right bracket', ')'),
('left bracket ', '('),
('right bracket ', ')'),
('left parenthesis', '('),
('right parenthesis', ')'),
('space', ' '),
# These are only for hudfont
('currency_bar_left', 'A'),
('currency_bar_mid', 'B'),
('currency_bar_right', 'C'),
('power_bar_half', 'D'),
('power_bar_mid', 'E'),
('power_bar_right', 'F'),
('power_bar_arrows', 'H'),
('power_bar_half', 'L'),
('power_bar_tick_green', 'I'),
('power_bar_tick_red', 'K'),
('power_bar_tick_yellow', 'J'),
('power_symbol', 'G')
]
class Font():
def __init__(self, name, dir_name, path, default_char, glyph_overlap, line_overlap, glyph_space):
self.name = name # font name for glyph filename notation
self.dir_name = dir_name # name of font directoy
self.path = path # path to font directory (for accessing the files)
self.default_char = default_char # char to draw if the requested char isn't found
self.glyph_overlap = glyph_overlap
self.line_overlap = line_overlap
self.glyph_space = glyph_space # amount of whitespace to be added to the right of the glyph within the image
def get_fonts(art_dir):
fonts = list()
# Modify these as necessary
fonts.append(Font("buttonfont", "Button font",
"{}/Button font" .format(art_dir), "?", 0, 0, 1))
fonts.append(Font("hudfont", "Hudfont",
"{}/Hudfont" .format(art_dir), "0", 0, 0, 0))
fonts.append(Font("shadowfont", "Shadowfont",
"{}/Shadowfont" .format(art_dir), "?", 1, 2, 0))
fonts.append(Font("standardfont", "Standard font",
"{}/Standard font" .format(art_dir), "?", 0, 0, 1))
fonts.append(Font("titlefont", "Title font",
"{}/Title font" .format(art_dir), "?", 0, 0, 2))
return fonts
def images_in_dir(dir):
files = os.listdir(dir)
for file in files[:]:
if not file.endswith(SUFFIX):
files.remove(file)
continue
'''
if file.startswith("black_"):
files.remove(file)
continue
'''
return files
def parse_special_char_name(name):
for entry in SPECIAL_CHARS:
if entry[0] == name:
return entry[1]
return None
def get_character(im_name, font):
# Naming notations... In all cases, [char] should have a length of 1
#
# Normal notation: [fontname]_[char].png
# char will be mapped to this image
#
# Lowercase notation: [fontname]_lowercase_[char].png
# the lower case of char will be mapped to the image (this implies that
# char should be a letter)
#
# Underscore1 notation: [fontname]_[char]_1.png
# another notation for lowercase
#
# Special notation: [fontname]_[special].png
# special can be any string that's mapped in SPECIAL_CHARS
char = im_name
if not char.startswith(font.name):
return None
if not char.endswith(SUFFIX):
return None
# Strip [fontname]_ and [suffix]
char = char.replace("{}_".format(font.name), "")
char = char.replace(SUFFIX, "")
# Parsing error or image name not valid font notation
if len(char) == 0:
return None
# Normal notation
if len(char) == 1:
return char
# Could be lowercase notation, underscore1 notation, special notation,
# or an error
if len(char) > 1:
# Lowercase notation
if char.startswith("lowercase_"):
char = char.replace("lowercase_", "")
if len(char) != 1:
return None
char = char.lower()
return char
# Underscore1 notation
if char.endswith("_1"):
char = char.replace("_1", "")
if len(char) != 1:
return None
char = char.lower()
return char
# Special notation or error
char = parse_special_char_name(char)
if char == None:
return None
return char
# Execution shouldn't ever get here, but just in case
return None
def get_font_height(font, crop):
# Returns height of tallest image in font folder
# Pass crop=True to crop whitespace before height comparison
images = images_in_dir(font.path)
height = 0
for image in images:
im = Image.open("{}/{}".format(font.path, image), 'r')
if crop:
im = crop_whitespace(im)
if im.size[1] > height:
# Ensure that this image is valid for the font
if get_character(image, font) != None:
# We don't care about space's height
if get_character(image, font) != ' ':
height = im.size[1]
#print "{} {}".format(height, image)
return height
def crop_whitespace(i0):
# getbbox() only trims 0,0,0,0 pixels
# Convert all pixels with 0 alpha to 0,0,0,0
pixels = i0.load()
newData = []
for y in range(i0.size[1]):
for x in range(i0.size[0]):
pixel = pixels[x, y]
if pixel[3] == 0:
newData.append((0, 0, 0, 0))
else:
newData.append(pixel)
i0.putdata(newData)
return i0.crop(i0.getbbox())
def crop_horizontal_whitespace(i0):
# getbbox() only trims 0,0,0,0 pixels
# Convert all pixels with 0 alpha to 0,0,0,0
pixels = i0.load()
newData = []
for y in range(i0.size[1]):
for x in range(i0.size[0]):
pixel = pixels[x, y]
if pixel[3] == 0:
newData.append((0, 0, 0, 0))
else:
newData.append(pixel)
i0.putdata(newData)
return i0.crop((0, 0, i0.getbbox()[2], i0.size[1]))
def get_font(art_dir, font_name):
fonts = get_fonts(art_dir)
for font in fonts:
if font.name == font_name:
return font
return None

217
font/glyphpos.py Normal file
View File

@ -0,0 +1,217 @@
'''
This is a helper script to aid in the mass positioning of font glyphs.
The positioning of the glyph within the image is very important now that the
draws from the idividual glyph images.
The script helps calulate a baseline (the line that capital letters rest upon
when positioned as desired) and then positions most glyphs relative to that.
There's an option to preview the glyph positionings by exporting a font strip,
as well as exporting all the new repositioned glyph images to the disk.
'''
import fontutils
from PIL import Image
# Characters that will rest upon the baseline (excluding hudfont)
CHAR_SET_BASELINE = [
'!', '?', '.', ':', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0',
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
'h', 'i', 'k', 'l', 'm', 'n', 'o', 'r', 's', 't', 'u', 'v', 'w', 'x', 'z',
]
# Characters that are centered (excluding hudfont)
CHAR_SET_CENTER = [
'#', '$', '%', '&', '(', ')', '*', '+', '-', '/', '<', '=', '>', '~',
'@', # galaxite_icon
'\\' # reactor_icon
]
def get_positioned_glyph(char, font, height, baseline):
i0 = get_glyph(char, font)
if i0 == None:
if font.name != "hudfont": # suppress hudfont errors
print "Unable to get positioned glyph for \"{}\" in \"{}\"". \
format(char, font.name)
return None
width = i0.size[0]
if char != ' ':
width += font.glyph_space
# Hack: space space out the numbers in hudfont while maintaining
# hudfont's glyph_space of 0
if font.name == "hudfont" and char in '0123456789':
width += fontutils.get_font(art_dir, "standardfont").glyph_space
i0 = fontutils.crop_whitespace(i0)
i1 = Image.new('RGBA', (width, height), (0, 0, 0, 0))
y = calculate_glyph_Y(char, font, height, baseline)
if y < 0 or y + i0.size[1] > height:
if char != ' ': # space doesn't matter
print ("Positioned glyph \"{}\" in \"{}\" is outside font height "
"bounds: (0,{})".format(char, font.name, y))
i1.paste(i0, (0, y))
return i1
def calculate_glyph_Y(char, font, height, baseline):
i0 = get_glyph(char, font)
if font.name == "hudfont":
# All hudfont glpyhs get centered
return height/2 - i0.size[1]/2
if char in CHAR_SET_BASELINE:
return baseline - i0.size[1]
if char in CHAR_SET_CENTER:
return height/2 - i0.size[1]/2
if char == 'Q':
# Align top with top of standard upercase character
return calculate_glyph_Y('O', font, height, baseline)
if char in ['g', 'p', 'q', 'y']:
# Align top with top of standard lower character
return calculate_glyph_Y('o', font, height, baseline)
if char in ['"', '\'']:
# Theses should be at the top. Anywhere between 0-2 or whatever looks good
return 0
if char == 'j':
# Align top with top of i
return calculate_glyph_Y('i', font, height, baseline)
if char == ',':
# Align top with top of period
return calculate_glyph_Y('.', font, height, baseline)
if char == ';':
# Align top with top of colin
return calculate_glyph_Y(':', font, height, baseline)
if char == ' ':
# Space is invisible so location within image doesn't really matter
return 0
print "Error vertically aligning \"{}\"".format(char)
return 0
def get_glyph(char, font):
# Returns the glyph image for the passed character
images = fontutils.images_in_dir(font.path)
for image in images:
if fontutils.get_character(image, font) == char:
return fontutils.crop_whitespace(Image.open("{}/{}".format(font.path, image), 'r'))
return None
def get_baseline(font, height, baseline_mod):
# Assume A is the "normal" capital letter height
im = get_glyph('A', font)
return height/2 + im.size[1]/2 + baseline_mod
def make_font_strip(strip_string, font, width, height, baseline):
i0 = Image.new('RGBA', (width, height), (255, 255, 255, 0))
x = 0
for char in strip_string:
i1 = get_positioned_glyph(char, font, height, baseline)
if i1 == None:
continue
i0.paste(i1, (x, 0), i1)
x += i1.size[0] - font.glyph_overlap
return i0
def get_perfect_font_height(font, baseline_mod):
# A perfect font height is when the height is as small as possible while
# allowing all glyphs positioned correcly without any glyphs being
# truncated by the vertical edges.
# Start off by trying the height of the largest trimmed glyph
height = fontutils.get_font_height(font, True)
baseline = get_baseline(font, height, baseline_mod)
# Iterate through each glyph
images = fontutils.images_in_dir(font.path)
for image in images:
char = fontutils.get_character(image, font)
if char == None or char == ' ':
continue
i0 = get_glyph(char, font)
if i0 == None:
continue
i0 = fontutils.crop_whitespace(i0)
# Keep growing the height until the glyph is no longer truncated
# when its positioned
y = calculate_glyph_Y(char, font, height, baseline)
while y < 0 or y + i0.size[1] > height:
height += y + i0.size[1] - height
baseline = get_baseline(font, height, baseline_mod)
y = calculate_glyph_Y(char, font, height, baseline)
return height
if __name__ == "__main__":
# Path to art2432
art_dir = ''
# Path to save preview strips
strip_out_dir = ''
# String to write in preview
strip_string = '!"#$%&\'()*+,-./ ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz:;<=>?\@'
# strip_string = "Now is the time for the quick brown fox to jump over the lazy dog's back!"
# Modify flag as desired
# Exports correctly postioned glyphs in a strip for previewing
export_strips = False
# Modify flag as desired
# Overwrites existing font glyphs with correctly positioned glyphs
export_results = False
# Iterate over the available fonts
fonts = fontutils.get_fonts(art_dir)
for font in fonts:
# The baseline is calcualted as the line which capital letters rest
# upon when vertically centered. baseline_mod is a value that's added
# to that calculation, allowing for letter and number positioning to be
# tweaked on a per-font basis. Note: baseline_mod other than 0 will
# cause infinite loop in get_perfect_font_height() if a perfect height
# cannot be achieved with that baseline
baseline_mod = 0
if font.name == "standardfont":
baseline_mod = -2
if font.name == "titlefont":
baseline_mod = -1
# Minimum height necessary for font whitespace is cropped
# from all glyphs and no glyps have clipped edges vertically
height = get_perfect_font_height(font, baseline_mod)
# The line that capital letters rest upon when vertically
# within a perfect height
baseline = get_baseline(font, height, baseline_mod)
if export_strips:
i0 = make_font_strip(strip_string, font, 1200, height, baseline)
# crop horizontally but not vertically
if (i0.getbbox() != None):
i0 = fontutils.crop_horizontal_whitespace(i0)
i0.save("{}/{}.png".format(strip_out_dir, font.name))
else:
print "Failed to save strip for {}".format(font.name)
if export_results:
# Iterate over each image
images = fontutils.images_in_dir(font.path)
for image in images:
# Obtain the correctly positioned glyph image
char = fontutils.get_character(image, font)
if char == None:
print "Error: {}".format(image)
continue
i0 = get_positioned_glyph(char, font, height, baseline)
# Overwrite original
i0.save("{}/{}".format(font.path, image))