diff --git a/font/fontmap.py b/font/fontmap.py new file mode 100644 index 0000000..9590410 --- /dev/null +++ b/font/fontmap.py @@ -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 diff --git a/font/fontutils.py b/font/fontutils.py new file mode 100644 index 0000000..48aa871 --- /dev/null +++ b/font/fontutils.py @@ -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 diff --git a/font/glyphpos.py b/font/glyphpos.py new file mode 100644 index 0000000..222d20e --- /dev/null +++ b/font/glyphpos.py @@ -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))