#!/usr/bin/env python
# -*- 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: <http://sourceforge.net/projects/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=<float> 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=<string> 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 list(swap.items()):
for gname, u in list(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 list(cmap.items()):
if gname in swapMap:
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] + "/templates"
fourStyleFamilySources = [
os.path.join(baseTemplatePath, "Regular.txt"),
os.path.join(baseTemplatePath, "Italic.txt"),
os.path.join(baseTemplatePath, "Bold.txt"),
os.path.join(baseTemplatePath, "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")