#!/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'