mirror of
https://github.com/spiffcode/hostile-takeover.git
synced 2026-04-29 23:41:38 -06:00
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:
parent
4896fbe384
commit
e02296b6ac
60
font/fontmap.py
Normal file
60
font/fontmap.py
Normal 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
226
font/fontutils.py
Normal 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
217
font/glyphpos.py
Normal 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))
|
||||
Loading…
Reference in New Issue
Block a user