1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
|
#!/usr/bin/python
# coding=utf-8
#
# Font build utility
#
import sys
import time
import os
import fontforge
import psMat
from tempfile import mkstemp
from fontTools.ttLib import TTFont
from fontTools.ttx import makeOutputFileName
import argparse
def flattenNestedReferences(font, ref, new_transform=(1, 0, 0, 1, 0, 0)):
"""Flattens nested references by replacing them with the ultimate reference
and applying any transformation matrices involved, so that the final font
has only simple composite glyphs. This to work around what seems to be an
Apple bug that results in ignoring transformation matrix of nested
references."""
name = ref[0]
transform = ref[1]
glyph = font[name]
new_ref = []
if glyph.references and glyph.foreground.isEmpty():
for nested_ref in glyph.references:
for i in flattenNestedReferences(font, nested_ref, transform):
matrix = psMat.compose(i[1], new_transform)
new_ref.append((i[0], matrix))
else:
matrix = psMat.compose(transform, new_transform)
new_ref.append((name, matrix))
return new_ref
def validateGlyphs(font):
"""Fixes some common FontForge validation warnings, currently handles:
* wrong direction
* flipped references
In addition to flattening nested references."""
wrong_dir = 0x8
flipped_ref = 0x10
for glyph in font.glyphs():
state = glyph.validate(True)
refs = []
if state & flipped_ref:
glyph.unlinkRef()
glyph.correctDirection()
if state & wrong_dir:
glyph.correctDirection()
for ref in glyph.references:
for i in flattenNestedReferences(font, ref):
refs.append(i)
if refs:
glyph.references = refs
def fixGasp(font, value=15):
try:
table = font.get('gasp')
table.gaspRange[65535] = value
except:
print('ER: {}: no table gasp')
def fixXAvgCharWidth(font):
"""xAvgCharWidth should be the average of all glyph widths in the font"""
width_sum = 0
count = 0
for glyph_id in font['glyf'].glyphs:
width = font['hmtx'].metrics[glyph_id][0]
if width > 0:
count += 1
width_sum += width
if count == 0:
fb.error("CRITICAL: Found no glyph width data!")
else:
expected_value = int(round(width_sum) / count)
font['OS/2'].xAvgCharWidth = int(round(width_sum) / count)
def opentype(infont, outdir, type, feature, version):
font = fontforge.open(infont)
if args.type == 'otf':
outfont = infont.replace(".sfd", ".otf")
flags = ("opentype", "round", "omit-instructions", "dummy-dsig")
else:
outfont = infont.replace(".sfd", ".ttf")
flags = ("opentype", "round", "omit-instructions", "dummy-dsig")
outfont = os.path.join(outdir, outfont)
print("Generating %s => %s" % (infont, outfont))
tmpfont = mkstemp(suffix=os.path.basename(outfont))[1]
# Remove all GSUB lookups
for lookup in font.gsub_lookups:
font.removeLookup(lookup)
# Remove all GPOS lookups
for lookup in font.gpos_lookups:
font.removeLookup(lookup)
# Merge the new featurefile
font.mergeFeature(feature)
font.version = version
font.appendSFNTName('English (US)', 'Version',
'Version ' + version + '.0+' + time.strftime('%Y%m%d'))
font.selection.all()
font.correctReferences()
font.simplify()
font.selection.none()
# fix some common font issues
validateGlyphs(font)
font.generate(tmpfont, flags=flags)
font.close()
# now open in fontTools
font = TTFont(tmpfont, recalcBBoxes=0)
# our 'name' table is a bit bulky, and of almost no use in for web fonts,
# so we strip all unnecessary entries.
name = font['name']
names = []
for record in name.names:
platID = record.platformID
langID = record.langID
nameID = record.nameID
# we keep only en_US entries in Windows and Mac platform id, every
# thing else is dropped
if (platID == 1 and langID == 0) or (platID == 3 and langID == 1033):
if nameID == 13:
# the full OFL text is too much, replace it with a simple
# string
if platID == 3:
# MS strings are UTF-16 encoded
text = 'OFL v1.1'.encode('utf_16_be')
else:
text = 'OFL v1.1'
record.string = text
names.append(record)
# keep every thing else except Descriptor, Sample Text
elif nameID not in (10, 19):
names.append(record)
name.names = names
font['OS/2'].version = 4
fixGasp(font)
fixXAvgCharWidth(font)
# FFTM is FontForge specific, remove it
del(font['FFTM'])
# force compiling GPOS/GSUB tables by fontTools, saves few tens of KBs
for tag in ('GPOS', 'GSUB'):
font[tag].compile(font)
font.save(outfont)
font.close()
os.remove(tmpfont)
def webfonts(infont, type):
font = TTFont(infont, recalcBBoxes=0)
# Generate WOFF2
woffFileName = makeOutputFileName(infont, outputDir=None, extension='.' + type)
print("Processing %s => %s" % (infont, woffFileName))
font.flavor = type
font.save(woffFileName, reorderTables=False)
font.close()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Build fonts')
parser.add_argument('-i', '--input', help='Input font', required=True)
parser.add_argument('-v', '--version', help='Version')
parser.add_argument('-f', '--feature', help='Feature file')
parser.add_argument('-t', '--type', help='Output type', default='otf')
parser.add_argument('-o', '--outdir', help='Output directory', default='build')
args = parser.parse_args()
if not os.path.exists(args.outdir):
os.mkdir(args.outdir)
if args.type == 'otf' or args.type == 'ttf':
opentype(args.input, args.outdir, args.type, args.feature, args.version)
if args.type == 'woff' or args.type == 'woff2':
webfonts(args.input, args.type)
|