summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Andersson <rasmus@notion.se>2019-10-22 22:27:06 +0300
committerRasmus Andersson <rasmus@notion.se>2019-10-22 22:31:20 +0300
commit0ba7c2b42f06dd2ded8cdeca8563dadf089f1d14 (patch)
treec06f63ab2eb51eef6da90fc86d84d6129cfe4bea
parentbc8b267b01d4652334e6f4a5452e77fbf79b329d (diff)
downloadinter-0ba7c2b42f06dd2ded8cdeca8563dadf089f1d14.tar.xz
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
-rwxr-xr-xmisc/fontbuild254
-rw-r--r--misc/tools/font_names.py148
2 files changed, 283 insertions, 119 deletions
diff --git a/misc/fontbuild b/misc/fontbuild
index 17280b42c..461ec7c20 100755
--- a/misc/fontbuild
+++ b/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()
-
-
- 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]
+ # 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
- 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)
+ # 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:
# 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)
+ # handle control back to fontmake
+ return designspace
- 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,
- )
-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)
diff --git a/misc/tools/font_names.py b/misc/tools/font_names.py
new file mode 100644
index 000000000..c321ca673
--- /dev/null
+++ b/misc/tools/font_names.py
@@ -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