#!/usr/bin/env python2.6 # -*- coding: utf-8 -*- # Thanks to Jonas Kjellström and Cody Boisclair for their help in finding bugs in this script! import re import os import sys import tempfile from fontTools.ttLib import TTFont, newTable doc = """USAGE: python /path/to/inputCustomize.py [INPUT] [--dest=OUTPUT] [OPTIONS] Use this script to customize one or more Input font files. Requires TTX/FontTools: If INPUT is missing, it will customize all fonts in the Current Working Directory. If OUTPUT is missing, it will overwrite the INPUT files. Options: -h, --help Print this help text, and exit. --lineHeight= A multiplier for the font's built-in line-height. --fourStyleFamily Only works when four INPUT files are provided. Assigns Regular, Italic, Bold, and Bold Italic names to the INPUT fonts in the order provided. --suffix= Append a suffix to the font names. Takes a string with no spaces. --a=ss Swaps alternate single-story 'a' for the default double-story 'a' --g=ss Swaps alternate single-story 'g' for the default double-story 'a' --i=serif Swaps one of the alternate 'i' for the default in Sans/Mono serifs serifs_round topserif --l=serif Swaps one of the alternate 'l' for the default in Sans/Mono serifs serifs_round topserif --zero=slash Swaps the slashed zero for the default dotted zero nodot Swaps a dotless zero for the default dotted zero --asterisk=height Swaps the mid-height asterisk for the default superscripted asterisk --braces=straight Swaps tamer straight-sided braces for the default super-curly braces Example 1: $ cd /path/to/the/top/level/of/the/fonts/you/want/to/edit $ python /path/to/InputCustomize.py --dest=/path/to/output --lineHeight=1.5 --suffix=Hack --fourStyleFamily --a=ss --g=ss --i=topserif --l=serifs_round --zero=slash --asterisk=height Example 2: $ cd /path/to/the/top/level/of/the/fonts/you/want/to/edit $ python /path/to/InputCustomize.py InputSans-Regular.ttf InputSans-Italic.ttf InputSans-Bold.ttf InputSerif-Regular.ttf --suffix=Hack --fourStyleFamily """ class InputModifier(object): """ An object for manipulating Input, takes a TTFont. Sorry this is a little hacky. """ def __init__(self, f): self.f = f def changeLineHeight(self, lineHeight): """ Takes a line height multiplier and changes the line height. """ f = self.f baseAsc = f['OS/2'].sTypoAscender baseDesc = f['OS/2'].sTypoDescender multiplier = float(lineHeight) f['hhea'].ascent = round(baseAsc * multiplier) f['hhea'].descent = round(baseDesc * multiplier) f['OS/2'].usWinAscent = round(baseAsc * multiplier) f['OS/2'].usWinDescent = round(baseDesc * multiplier)*-1 def swap(self, swap): """ Takes a dictionary of glyphs to swap and swaps 'em. """ f = self.f glyphNames = f.getGlyphNames() maps = { 'a': {'a': 97, 'aring': 229, 'adieresis': 228, 'acyrillic': 1072, 'aacute': 225, 'amacron': 257, 'agrave': 224, 'atilde': 227, 'acircumflex': 226, 'aogonek': 261, 'abreve': 259}, 'g': {'gdotaccent': 289, 'gbreve': 287, 'gcircumflex': 285, 'gcommaaccent': 291, 'g': 103}, 'i': {'i': 105, 'iacute': 237, 'iogonek': 303, 'igrave': 236, 'itilde': 297, 'icircumflex': 238, 'imacron': 299, 'ij': 307, 'ibreve': 301, 'yicyrillic': 1111, 'idieresis': 239, 'icyrillic': 1110, 'dotlessi': 305,}, 'l': {'l': 108, 'lcaron': 318, 'lcommaaccent': 316, 'lacute': 314, 'lslash': 322, 'ldot': 320}, 'zero': {'zero': 48}, 'asterisk': {'asterisk': 42}, 'braces': {'braceleft': 123, 'braceright': 125} } swapMap = {} for k, v in swap.items(): for gname, u in maps[k].items(): newGname = gname + '.salt_' + v if newGname in glyphNames: swapMap[gname] = newGname for table in f['cmap'].tables: cmap = table.cmap for u, gname in cmap.items(): if swapMap.has_key(gname): cmap[u] = swapMap[gname] def fourStyleFamily(self, position, suffix=None): """ Replaces the name table and certain OS/2 values with those that will make a four-style family. """ f = self.f source = TTFont(fourStyleFamilySources[position]) tf = tempfile.mkstemp() pathToXML = tf[1] source.saveXML(pathToXML, tables=['name']) os.close(tf[0]) with open(pathToXML, "r") as temp: xml = temp.read() # make the changes if suffix: xml = xml.replace("Input", "Input" + suffix) # save the table with open(pathToXML, 'w') as temp: temp.write(xml) temp.write('\r') f['OS/2'].usWeightClass = source['OS/2'].usWeightClass f['OS/2'].fsType = source['OS/2'].fsType # write the table f['name'] = newTable('name') f.importXML(pathToXML) def changeNames(self, suffix=None): # this is a similar process to fourStyleFamily() tf = tempfile.mkstemp() pathToXML = tf[1] f.saveXML(pathToXML, tables=['name']) os.close(tf[0]) with open(pathToXML, "r") as temp: xml = temp.read() # make the changes if suffix: xml = xml.replace("Input", "Input" + suffix) # save the table with open(pathToXML, 'w') as temp: temp.write(xml) temp.write('\r') # write the table f['name'] = newTable('name') f.importXML(pathToXML) baseTemplatePath = os.path.split(__file__)[0] fourStyleFamilySources = [ os.path.join(baseTemplatePath, '_template_Regular.txt'), os.path.join(baseTemplatePath, '_template_Italic.txt'), os.path.join(baseTemplatePath, '_template_Bold.txt'), os.path.join(baseTemplatePath, '_template_BoldItalic.txt'), ] fourStyleFileNameAppend = [ 'Regular', 'Italic', 'Bold', 'BoldItalic' ] if __name__ == "__main__": # Get command-line arguments go = True arguments = sys.argv[1:] paths = [] swap = {} lineHeight = None fourStyleFamily = None suffix = None destBase = None # parse arguments for argument in arguments: key = None value = None if len(argument.split('=')) == 2: key, value = argument.split('=') key = key[2:] elif argument[0:2] == '--': key = argument[2:] value = True elif argument == '-h': print doc go = False else: key = argument value = None # assign preference variables if value is None: paths.append(key) elif key == 'lineHeight': lineHeight = value elif key == 'fourStyleFamily': fourStyleFamily = True elif key == 'suffix': suffix = value elif key == 'dest': destBase = value elif key == 'help': print doc go = False else: swap[key] = value # account for arguments where no value is given (for example, '--a' instead of '--a=ss') if swap.get('a') is True: swap['a'] = 'ss' if swap.get('g') is True: swap['g'] = 'ss' if swap.get('i') is True: swap['i'] = 'serifs' if swap.get('l') is True: swap['l'] = 'serifs' if swap.get('zero') is True: swap['zero'] = 'slash' if swap.get('asterisk') is True: swap['asterisk'] = 'height' if swap.get('braces') is True: swap['braces'] = 'straight' # if specific paths were not supplied, collect them from the current directory if not paths: for root, dirs, files in os.walk(os.getcwd()): for filename in files: basePath, ext = os.path.splitext(filename) if ext in ['.otf', '.ttf']: paths.append(os.path.join(root, filename)) # if four paths were not supplied, do not process as a four-style family if len(paths) != 4: fourStyleFamily = None if go: for i, path in enumerate(paths): print os.path.split(path)[1] f = TTFont(path) c = InputModifier(f) if lineHeight: c.changeLineHeight(lineHeight) if swap: c.swap(swap) if fourStyleFamily: c.fourStyleFamily(i, suffix) base, ext = os.path.splitext(path) path = base + '_as_' + fourStyleFileNameAppend[i] + ext elif suffix: c.changeNames(suffix) if destBase: baseScrap, fileAndExt = os.path.split(path) destPath = os.path.join(destBase, fileAndExt) else: destPath = path try: os.remove(destPath) except: pass # Take care of that weird "post" table issue, just in case. Delta#1 try: del f['post'].mapping['Delta#1'] except: pass f.save(destPath) print 'done'