mirror of
https://github.com/rsms/inter.git
synced 2024-11-15 19:47:47 +01:00
New version of fontbuild which addresses several issues
Fixes for things that stopped working when we updated fontmake: - restore glyph decomposition for VF - restore glyph overlap removal for VF - restore version metadata writing for VF Improvements for VF - fix "full name" name table entry to say "Inter" instead of "Inter Regular" New and changed: - "rename" command for renaming metadata like family and style, optionally saving a separate file. Used to produce new "Inter V" family. - The "build" command no longer performs "style name compactation" for Google fonts. Instead, the new "rename" command is used. Closes #198 Closes #202
This commit is contained in:
parent
bc8b267b01
commit
0ba7c2b42f
2 changed files with 283 additions and 119 deletions
254
misc/fontbuild
254
misc/fontbuild
|
@ -16,6 +16,8 @@ import re
|
|||
import signal
|
||||
import subprocess
|
||||
import ufo2ft
|
||||
import font_names
|
||||
|
||||
from functools import partial
|
||||
from fontmake.font_project import FontProject
|
||||
from defcon import Font
|
||||
|
@ -110,124 +112,96 @@ def findGlyphDirectives(g): # -> set<string> | None
|
|||
return directives
|
||||
|
||||
|
||||
|
||||
def deep_copy_contours(ufo, parent, component, transformation):
|
||||
"""Copy contours from component to parent, including nested components."""
|
||||
for nested in component.components:
|
||||
deep_copy_contours(
|
||||
ufo, parent, ufo[nested.baseGlyph],
|
||||
transformation.transform(nested.transformation))
|
||||
if component != parent:
|
||||
pen = TransformPen(parent.getPen(), transformation)
|
||||
# if the transformation has a negative determinant, it will reverse
|
||||
# the contour direction of the component
|
||||
xx, xy, yx, yy = transformation[:4]
|
||||
if xx*yy - xy*yx < 0:
|
||||
pen = ReverseContourPen(pen)
|
||||
component.draw(pen)
|
||||
|
||||
|
||||
|
||||
def decompose_glyphs(ufos, glyphNamesToDecompose):
|
||||
for ufo in ufos:
|
||||
for glyphname in glyphNamesToDecompose:
|
||||
glyph = ufo[glyphname]
|
||||
deep_copy_contours(ufo, glyph, glyph, Transform())
|
||||
glyph.clearComponents()
|
||||
|
||||
|
||||
|
||||
# subclass of fontmake.FontProject that
|
||||
# - patches version metadata
|
||||
# - decomposes certain glyphs
|
||||
# - removes overlaps of certain glyphs
|
||||
#
|
||||
class VarFontProject(FontProject):
|
||||
def __init__(self, familyName=None, compact_style_names=False, *args, **kwargs):
|
||||
def __init__(self, compact_style_names=False, *args, **kwargs):
|
||||
super(VarFontProject, self).__init__(*args, **kwargs)
|
||||
self.familyName = familyName
|
||||
self.compact_style_names = compact_style_names
|
||||
|
||||
|
||||
def decompose_glyphs(self, designspace, glyph_filter=lambda g: True):
|
||||
"""Move components of UFOs' glyphs to their outlines."""
|
||||
for ufo in designspace:
|
||||
log.info('Decomposing glyphs for ' + self._font_name(ufo))
|
||||
for glyph in ufo:
|
||||
if not glyph.components or not glyph_filter(glyph):
|
||||
continue
|
||||
self._deep_copy_contours(ufo, glyph, glyph, Transform())
|
||||
glyph.clearComponents()
|
||||
# override FontProject._load_designspace_sources
|
||||
def _load_designspace_sources(self, designspace):
|
||||
designspace = FontProject._load_designspace_sources(designspace)
|
||||
masters = [s.font for s in designspace.sources] # list of UFO font objects
|
||||
|
||||
|
||||
def _deep_copy_contours(self, ufo, parent, component, transformation):
|
||||
"""Copy contours from component to parent, including nested components."""
|
||||
for nested in component.components:
|
||||
self._deep_copy_contours(
|
||||
ufo, parent, ufo[nested.baseGlyph],
|
||||
transformation.transform(nested.transformation))
|
||||
if component != parent:
|
||||
pen = TransformPen(parent.getPen(), transformation)
|
||||
# if the transformation has a negative determinant, it will reverse
|
||||
# the contour direction of the component
|
||||
xx, xy, yx, yy = transformation[:4]
|
||||
if xx*yy - xy*yx < 0:
|
||||
pen = ReverseContourPen(pen)
|
||||
component.draw(pen)
|
||||
|
||||
|
||||
def _build_interpolatable_masters(
|
||||
self,
|
||||
designspace,
|
||||
ttf,
|
||||
use_production_names=None,
|
||||
reverse_direction=True,
|
||||
conversion_error=None,
|
||||
feature_writers=None,
|
||||
cff_round_tolerance=None,
|
||||
**kwargs,
|
||||
):
|
||||
# We decompose any glyph with reflected components to make sure
|
||||
# that fontTools varLib is able to produce properly-slanting interpolation.
|
||||
|
||||
designspace = self._load_designspace_sources(designspace)
|
||||
|
||||
decomposeGlyphs = set()
|
||||
removeOverlapsGlyphs = set()
|
||||
masters = [s.font for s in designspace.sources]
|
||||
# Update the default source's full name to not include style name
|
||||
defaultFont = designspace.default.font
|
||||
defaultFont.info.openTypeNameCompatibleFullName = defaultFont.info.familyName
|
||||
|
||||
for ufo in masters:
|
||||
|
||||
if self.familyName is not None:
|
||||
ufo.info.familyName =\
|
||||
ufo.info.familyName.replace('Inter', self.familyName)
|
||||
ufo.info.styleMapFamilyName =\
|
||||
ufo.info.styleMapFamilyName.replace('Inter', self.familyName)
|
||||
ufo.info.postscriptFontName =\
|
||||
ufo.info.postscriptFontName.replace('Inter', self.familyName.replace(' ', ''))
|
||||
ufo.info.macintoshFONDName =\
|
||||
ufo.info.macintoshFONDName.replace('Inter', self.familyName)
|
||||
ufo.info.openTypeNamePreferredFamilyName =\
|
||||
ufo.info.openTypeNamePreferredFamilyName.replace('Inter', self.familyName)
|
||||
|
||||
# patch style name if --compact-style-names is set
|
||||
if args.compact_style_names:
|
||||
if self.compact_style_names:
|
||||
collapseFontStyleName(ufo)
|
||||
# update font version
|
||||
updateFontVersion(ufo, isVF=True)
|
||||
|
||||
updateFontVersion(ufo)
|
||||
ufoname = basename(ufo.path)
|
||||
|
||||
# find glyphs subject to decomposition and/or overlap removal
|
||||
glyphNamesToDecompose = set() # glyph names
|
||||
glyphsToRemoveOverlaps = set() # glyph names
|
||||
for ufo in masters:
|
||||
for g in ufo:
|
||||
directives = findGlyphDirectives(g)
|
||||
if g.components and composedGlyphIsNonTrivial(g):
|
||||
decomposeGlyphs.add(g.name)
|
||||
glyphNamesToDecompose.add(g.name)
|
||||
if 'removeoverlap' in directives:
|
||||
if g.components and len(g.components) > 0:
|
||||
decomposeGlyphs.add(g.name)
|
||||
removeOverlapsGlyphs.add(g)
|
||||
glyphNamesToDecompose.add(g.name)
|
||||
glyphsToRemoveOverlaps.add(g)
|
||||
|
||||
self.decompose_glyphs(masters, lambda g: g.name in decomposeGlyphs)
|
||||
# decompose
|
||||
if log.isEnabledFor(logging.INFO):
|
||||
log.info('Decomposing glyphs:\n %s', "\n ".join(glyphNamesToDecompose))
|
||||
decompose_glyphs(masters, glyphNamesToDecompose)
|
||||
|
||||
if len(removeOverlapsGlyphs) > 0:
|
||||
# remove overlaps
|
||||
if len(glyphsToRemoveOverlaps) > 0:
|
||||
rmoverlapFilter = RemoveOverlapsFilter(backend='pathops')
|
||||
rmoverlapFilter.start()
|
||||
for g in removeOverlapsGlyphs:
|
||||
if log.isEnabledFor(logging.INFO):
|
||||
log.info(
|
||||
'Removing overlaps in glyph "%s" of %s',
|
||||
g.name,
|
||||
basename(g.getParent().path)
|
||||
'Removing overlaps in glyphs:\n %s',
|
||||
"\n ".join(set([g.name for g in glyphsToRemoveOverlaps])),
|
||||
)
|
||||
for g in glyphsToRemoveOverlaps:
|
||||
rmoverlapFilter.filter(g)
|
||||
|
||||
|
||||
if ttf:
|
||||
return ufo2ft.compileInterpolatableTTFsFromDS(
|
||||
designspace,
|
||||
useProductionNames=use_production_names,
|
||||
reverseDirection=reverse_direction,
|
||||
cubicConversionError=conversion_error,
|
||||
featureWriters=feature_writers,
|
||||
inplace=True,
|
||||
)
|
||||
else:
|
||||
return ufo2ft.compileInterpolatableOTFsFromDS(
|
||||
designspace,
|
||||
useProductionNames=use_production_names,
|
||||
roundTolerance=cff_round_tolerance,
|
||||
featureWriters=feature_writers,
|
||||
inplace=True,
|
||||
)
|
||||
# handle control back to fontmake
|
||||
return designspace
|
||||
|
||||
|
||||
def updateFontVersion(font, dummy=False):
|
||||
|
||||
def updateFontVersion(font, dummy=False, isVF=False):
|
||||
version = getVersion()
|
||||
buildtag = getGitHash()
|
||||
now = datetime.datetime.utcnow()
|
||||
|
@ -242,11 +216,13 @@ def updateFontVersion(font, dummy=False):
|
|||
font.info.woffMajorVersion = versionMajor
|
||||
font.info.woffMinorVersion = versionMinor
|
||||
font.info.year = now.year
|
||||
font.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (
|
||||
versionMajor, versionMinor, buildtag)
|
||||
font.info.openTypeNameUniqueID = "%s %s:%d:%s" % (
|
||||
font.info.familyName, font.info.styleName, now.year, buildtag)
|
||||
# creation date & time (YYYY/MM/DD HH:MM:SS)
|
||||
font.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (versionMajor, versionMinor, buildtag)
|
||||
psFamily = re.sub(r'\s', '', font.info.familyName)
|
||||
if isVF:
|
||||
font.info.openTypeNameUniqueID = "%s:VF:%d:%s" % (psFamily, now.year, buildtag)
|
||||
else:
|
||||
psStyle = re.sub(r'\s', '', font.info.styleName)
|
||||
font.info.openTypeNameUniqueID = "%s-%s:%d:%s" % (psFamily, psStyle, now.year, buildtag)
|
||||
font.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")
|
||||
|
||||
|
||||
|
@ -362,6 +338,8 @@ class Main(object):
|
|||
compile-var Build variable font files
|
||||
glyphsync Generate designspace and UFOs from Glyphs file
|
||||
instancegen Generate instance UFOs for designspace
|
||||
checkfont Verify integrity of font files
|
||||
rename Rename fonts
|
||||
'''.strip().replace('\n ', '\n'))
|
||||
|
||||
argparser.add_argument('-v', '--verbose', action='store_true',
|
||||
|
@ -426,9 +404,6 @@ class Main(object):
|
|||
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
||||
help='Output font file')
|
||||
|
||||
argparser.add_argument('--name', metavar='<family-name>',
|
||||
help='Override family name, replacing "Inter"')
|
||||
|
||||
argparser.add_argument('--compact-style-names', action='store_true',
|
||||
help="Produce font files with style names that doesn't contain spaces. "\
|
||||
"E.g. \"SemiBoldItalic\" instead of \"Semi Bold Italic\"")
|
||||
|
@ -437,27 +412,14 @@ class Main(object):
|
|||
|
||||
# decide output filename (or check user-provided name)
|
||||
outfilename = args.output
|
||||
outformat = 'variable' # TTF
|
||||
if outfilename is None or outfilename == '':
|
||||
outfilename = os.path.splitext(basename(args.srcfile))[0] + '.var.ttf'
|
||||
outfilename = os.path.splitext(basename(args.srcfile))[0] + '.var.otf'
|
||||
log.info('setting --output %r' % outfilename)
|
||||
else:
|
||||
outfileext = os.path.splitext(outfilename)[1]
|
||||
if outfileext.lower() == '.otf':
|
||||
outformat = 'variable-cff2'
|
||||
elif outfileext.lower() != '.ttf':
|
||||
fatal('Invalid file extension %r (expected ".ttf")' % outfileext)
|
||||
|
||||
mkdirs(dirname(outfilename))
|
||||
|
||||
# override family name?
|
||||
familyName = None
|
||||
if args.name is not None and len(args.name) > 0:
|
||||
familyName = args.name
|
||||
|
||||
project = VarFontProject(
|
||||
verbose=self.logLevelName,
|
||||
familyName=familyName,
|
||||
compact_style_names=args.compact_style_names,
|
||||
)
|
||||
project.run_from_designspace(
|
||||
|
@ -467,11 +429,22 @@ class Main(object):
|
|||
use_production_names=True,
|
||||
round_instances=True,
|
||||
output_path=outfilename,
|
||||
output=[outformat],
|
||||
output=["variable"], # "variable-cff2" in the future
|
||||
optimize_cff=CFFOptimization.SUBROUTINIZE,
|
||||
overlaps_backend='pathops', # use Skia's pathops
|
||||
)
|
||||
|
||||
# Rename fullName record to familyName (VF only)
|
||||
# Note: Even though we set openTypeNameCompatibleFullName it seems that the fullName
|
||||
# record is still computed by fonttools, so we override it here.
|
||||
font = font_names.loadFont(outfilename)
|
||||
try:
|
||||
familyName = font_names.getFamilyName(font)
|
||||
font_names.setFullName(font, familyName)
|
||||
font.save(outfilename)
|
||||
finally:
|
||||
font.close()
|
||||
|
||||
self.log("write %s" % outfilename)
|
||||
|
||||
# Note: we can't run ots-sanitize on the generated file as OTS
|
||||
|
@ -652,7 +625,7 @@ class Main(object):
|
|||
italic = False
|
||||
if tag == 'italic':
|
||||
italic = True
|
||||
elif tag != 'upright':
|
||||
elif tag != 'roman':
|
||||
raise Exception('unexpected tag ' + tag)
|
||||
|
||||
for a in ds.axes:
|
||||
|
@ -785,11 +758,11 @@ class Main(object):
|
|||
self.log("write %s" % relpath(designspace_file, os.getcwd()))
|
||||
designspace.write(designspace_file)
|
||||
|
||||
# upright designspace
|
||||
upright_designspace_file = pjoin(outdir, 'Inter-upright.designspace')
|
||||
# roman designspace
|
||||
roman_designspace_file = pjoin(outdir, 'Inter-roman.designspace')
|
||||
p = Process(
|
||||
target=self._genSubsetDesignSpace,
|
||||
args=(designspace, 'upright', upright_designspace_file)
|
||||
args=(designspace, 'roman', roman_designspace_file)
|
||||
)
|
||||
p.start()
|
||||
procs.append(p)
|
||||
|
@ -929,5 +902,48 @@ class Main(object):
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_rename(self, argv):
|
||||
argparser = argparse.ArgumentParser(
|
||||
usage='%(prog)s rename <options> <file>',
|
||||
description='Rename family and/or styles of font'
|
||||
)
|
||||
a = lambda *args, **kwargs: argparser.add_argument(*args, **kwargs)
|
||||
|
||||
a('-o', '--output', metavar='<file>',
|
||||
help='Output font file. Defaults to input file (overwrite.)')
|
||||
a('--family', metavar='<name>',
|
||||
help='Rename family to <name>')
|
||||
a('--compact-style', action='store_true',
|
||||
help='Rename style names to CamelCase. e.g. "Extra Bold Italic" -> "ExtraBoldItalic"')
|
||||
a('input', metavar='<file>',
|
||||
help='Input font file')
|
||||
|
||||
args = argparser.parse_args(argv)
|
||||
|
||||
infile = args.input
|
||||
outfile = args.output or infile
|
||||
|
||||
font = font_names.loadFont(infile)
|
||||
editCount = 0
|
||||
try:
|
||||
if args.family:
|
||||
editCount += 1
|
||||
font_names.setFamilyName(font, args.family)
|
||||
|
||||
if args.compact_style:
|
||||
editCount += 1
|
||||
font_names.removeWhitespaceFromStyles(font)
|
||||
|
||||
if editCount > 0:
|
||||
font.save(outfile)
|
||||
else:
|
||||
print("no rename options provided", file=sys.stderr)
|
||||
argparser.print_usage(sys.stderr)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
font.close()
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Main().main(sys.argv)
|
||||
|
|
148
misc/tools/font_names.py
Normal file
148
misc/tools/font_names.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
#!/usr/bin/env python
|
||||
from fontTools.ttLib import TTFont
|
||||
import os, sys, re
|
||||
|
||||
# Adoptation of fonttools/blob/master/Snippets/rename-fonts.py
|
||||
|
||||
WINDOWS_ENGLISH_IDS = 3, 1, 0x409
|
||||
MAC_ROMAN_IDS = 1, 0, 0
|
||||
|
||||
LEGACY_FAMILY = 1
|
||||
TRUETYPE_UNIQUE_ID = 3
|
||||
FULL_NAME = 4
|
||||
POSTSCRIPT_NAME = 6
|
||||
PREFERRED_FAMILY = 16
|
||||
SUBFAMILY_NAME = 17
|
||||
WWS_FAMILY = 21
|
||||
|
||||
|
||||
FAMILY_RELATED_IDS = set([
|
||||
LEGACY_FAMILY,
|
||||
TRUETYPE_UNIQUE_ID,
|
||||
FULL_NAME,
|
||||
POSTSCRIPT_NAME,
|
||||
PREFERRED_FAMILY,
|
||||
WWS_FAMILY,
|
||||
])
|
||||
|
||||
whitespace_re = re.compile(r'\s+')
|
||||
|
||||
|
||||
def removeWhitespace(s):
|
||||
return whitespace_re.sub("", s)
|
||||
|
||||
|
||||
def setFullName(font, fullName):
|
||||
nameTable = font["name"]
|
||||
nameTable.setName(fullName, FULL_NAME, 1, 0, 0) # mac
|
||||
nameTable.setName(fullName, FULL_NAME, 3, 1, 0x409) # windows
|
||||
|
||||
|
||||
def getFamilyName(font):
|
||||
nameTable = font["name"]
|
||||
r = None
|
||||
for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS):
|
||||
for name_id in (PREFERRED_FAMILY, LEGACY_FAMILY):
|
||||
r = nameTable.getName(nameID=name_id, platformID=plat_id, platEncID=enc_id, langID=lang_id)
|
||||
if r is not None:
|
||||
break
|
||||
if r is not None:
|
||||
break
|
||||
if not r:
|
||||
raise ValueError("family name not found")
|
||||
return r.toUnicode()
|
||||
|
||||
|
||||
def removeWhitespaceFromStyles(font):
|
||||
familyName = getFamilyName(font)
|
||||
|
||||
# collect subfamily (style) name IDs for variable font's named instances
|
||||
vfInstanceSubfamilyNameIds = set()
|
||||
if "fvar" in font:
|
||||
for namedInstance in font["fvar"].instances:
|
||||
vfInstanceSubfamilyNameIds.add(namedInstance.subfamilyNameID)
|
||||
|
||||
nameTable = font["name"]
|
||||
for rec in nameTable.names:
|
||||
rid = rec.nameID
|
||||
if rid in (FULL_NAME, LEGACY_FAMILY):
|
||||
# style part of family name
|
||||
s = rec.toUnicode()
|
||||
start = s.find(familyName)
|
||||
if start != -1:
|
||||
s = familyName + " " + removeWhitespace(s[start + len(familyName):])
|
||||
else:
|
||||
s = removeWhitespace(s)
|
||||
rec.string = s
|
||||
if rid in (SUBFAMILY_NAME,) or rid in vfInstanceSubfamilyNameIds:
|
||||
rec.string = removeWhitespace(rec.toUnicode())
|
||||
# else: ignore standard names unrelated to style
|
||||
|
||||
|
||||
def setFamilyName(font, nextFamilyName):
|
||||
prevFamilyName = getFamilyName(font)
|
||||
if prevFamilyName == nextFamilyName:
|
||||
return
|
||||
# raise Exception("identical family name")
|
||||
|
||||
def renameRecord(nameRecord, prevFamilyName, nextFamilyName):
|
||||
# replaces prevFamilyName with nextFamilyName in nameRecord
|
||||
s = nameRecord.toUnicode()
|
||||
start = s.find(prevFamilyName)
|
||||
if start != -1:
|
||||
end = start + len(prevFamilyName)
|
||||
nextFamilyName = s[:start] + nextFamilyName + s[end:]
|
||||
nameRecord.string = nextFamilyName
|
||||
return s, nextFamilyName
|
||||
|
||||
# postcript name can't contain spaces
|
||||
psPrevFamilyName = prevFamilyName.replace(" ", "")
|
||||
psNextFamilyName = nextFamilyName.replace(" ", "")
|
||||
for rec in font["name"].names:
|
||||
name_id = rec.nameID
|
||||
if name_id not in FAMILY_RELATED_IDS:
|
||||
# leave uninteresting records unmodified
|
||||
continue
|
||||
if name_id == POSTSCRIPT_NAME:
|
||||
old, new = renameRecord(rec, psPrevFamilyName, psNextFamilyName)
|
||||
elif name_id == TRUETYPE_UNIQUE_ID:
|
||||
# The Truetype Unique ID rec may contain either the PostScript Name or the Full Name
|
||||
if psPrevFamilyName in rec.toUnicode():
|
||||
# Note: This is flawed -- a font called "Foo" renamed to "Bar Lol";
|
||||
# if this record is not a PS record, it will incorrectly be rename "BarLol".
|
||||
# However, in practice this is not abig deal since it's just an ID.
|
||||
old, new = renameRecord(rec, psPrevFamilyName, psNextFamilyName)
|
||||
else:
|
||||
old, new = renameRecord(rec, prevFamilyName, nextFamilyName)
|
||||
else:
|
||||
old, new = renameRecord(rec, prevFamilyName, nextFamilyName)
|
||||
# print(" %r: '%s' -> '%s'" % (rec, old, new))
|
||||
|
||||
|
||||
|
||||
def loadFont(file):
|
||||
return TTFont(file, recalcBBoxes=False, recalcTimestamp=False)
|
||||
|
||||
|
||||
def renameFontFamily(infile, outfile, newFamilyName):
|
||||
font = loadFont(infile)
|
||||
setFamilyName(font, newFamilyName)
|
||||
# print('write "%s"' % outfile)
|
||||
font.save(outfile)
|
||||
font.close()
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
infile = "./build/fonts/var/Inter.var.ttf"
|
||||
outfile = "./build/tmp/var2.otf"
|
||||
renameFontFamily(infile, outfile, "Inter V")
|
||||
print("%s familyName: %r" % (infile, getFamilyName(loadFont(infile)) ))
|
||||
print("%s familyName: %r" % (outfile, getFamilyName(loadFont(outfile)) ))
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
# Similar to:
|
||||
# ttx -i -e -o ./build/tmp/var.ttx ./build/fonts/var/Inter.var.ttf
|
||||
# ttx -b --no-recalc-timestamp -o ./build/tmp/var.otf ./build/tmp/var.ttx
|
Loading…
Reference in a new issue