fontbuild: remove use of fontmake, simplifying things.
This commit is contained in:
parent
9c444deded
commit
aa7ad2d7a0
9 changed files with 612 additions and 389 deletions
427
misc/fontbuild
427
misc/fontbuild
|
|
@ -1,43 +1,30 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from __future__ import print_function, absolute_import
|
from __future__ import print_function, absolute_import
|
||||||
|
import sys
|
||||||
import sys, os
|
import os
|
||||||
from os.path import dirname, basename, abspath, relpath, join as pjoin
|
from os.path import dirname, basename, abspath, relpath, join as pjoin
|
||||||
sys.path.append(abspath(pjoin(dirname(__file__), 'tools')))
|
sys.path.append(abspath(pjoin(dirname(__file__), 'tools')))
|
||||||
from common import BASEDIR, VENVDIR, getGitHash, getVersion, execproc
|
from common import BASEDIR, execproc
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import datetime
|
|
||||||
import errno
|
|
||||||
import glyphsLib
|
import glyphsLib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import ufo2ft
|
|
||||||
import font_names
|
|
||||||
|
|
||||||
from functools import partial
|
from fontTools.designspaceLib import DesignSpaceDocument
|
||||||
from fontmake.font_project import FontProject
|
|
||||||
from defcon import Font
|
|
||||||
from fontTools import designspaceLib
|
|
||||||
from fontTools import varLib
|
|
||||||
from fontTools.misc.transform import Transform
|
|
||||||
from fontTools.pens.transformPen import TransformPen
|
|
||||||
from fontTools.pens.reverseContourPen import ReverseContourPen
|
|
||||||
from glyphsLib.interpolation import apply_instance_data
|
|
||||||
from mutatorMath.ufo.document import DesignSpaceDocumentReader
|
from mutatorMath.ufo.document import DesignSpaceDocumentReader
|
||||||
from multiprocessing import Process, Queue
|
from multiprocessing import Process, Queue
|
||||||
from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter
|
from glyphsLib.interpolation import apply_instance_data
|
||||||
from ufo2ft import CFFOptimization
|
|
||||||
|
from fontbuildlib import FontBuilder
|
||||||
|
from fontbuildlib.util import mkdirs, loadTTFont
|
||||||
|
from fontbuildlib.info import setFontInfo, updateFontVersion
|
||||||
|
from fontbuildlib.name import setFamilyName, removeWhitespaceFromStyles
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
stripItalic_re = re.compile(r'(?:^|\b)italic\b|italic$', re.I | re.U)
|
|
||||||
|
|
||||||
|
|
||||||
def stripItalic(name):
|
|
||||||
return stripItalic_re.sub('', name.strip())
|
|
||||||
|
|
||||||
|
|
||||||
def sighandler(signum, frame):
|
def sighandler(signum, frame):
|
||||||
|
|
@ -46,263 +33,11 @@ def sighandler(signum, frame):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def mkdirs(path):
|
|
||||||
try:
|
|
||||||
os.makedirs(path)
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno != errno.EEXIST:
|
|
||||||
raise # raises the error again
|
|
||||||
|
|
||||||
|
|
||||||
def fatal(msg):
|
def fatal(msg):
|
||||||
print(sys.argv[0] + ': ' + msg, file=sys.stderr)
|
print(sys.argv[0] + ': ' + msg, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def composedGlyphIsNonTrivial(g):
|
|
||||||
# A non-trivial glyph is one that uses reflecting component transformations.
|
|
||||||
if g.components and len(g.components) > 0:
|
|
||||||
for c in g.components:
|
|
||||||
# has non-trivial transformation? (i.e. scaled)
|
|
||||||
# Example of optimally trivial transformation:
|
|
||||||
# (1, 0, 0, 1, 0, 0) no scale or offset
|
|
||||||
# Example of scaled transformation matrix:
|
|
||||||
# (-1.0, 0, 0.3311, 1, 1464.0, 0) flipped x axis, sheered and offset
|
|
||||||
#
|
|
||||||
xScale = c.transformation[0]
|
|
||||||
yScale = c.transformation[3]
|
|
||||||
# If glyph is reflected along x or y axes, it won't slant well.
|
|
||||||
if xScale < 0 or yScale < 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Directives are glyph-specific post-processing directives for the compiler.
|
|
||||||
# A directive is added to the "note" section of a glyph and takes the
|
|
||||||
# following form:
|
|
||||||
#
|
|
||||||
# !post:DIRECTIVE
|
|
||||||
#
|
|
||||||
# Where DIRECTIVE is the name of a known directive.
|
|
||||||
# This string can appear anywhere in the glyph note.
|
|
||||||
# Directives are _not_ case sensitive but normalized by str.lower(), meaning
|
|
||||||
# that e.g. "removeoverlap" == "RemoveOverlap" == "REMOVEOVERLAP".
|
|
||||||
#
|
|
||||||
knownDirectives = set([
|
|
||||||
'removeoverlap', # applies overlap removal (boolean union)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
findDirectiveRegEx = re.compile(r'\!post:([^ ]+)', re.I | re.U)
|
|
||||||
|
|
||||||
def findGlyphDirectives(g): # -> set<string> | None
|
|
||||||
directives = set()
|
|
||||||
if g.note and len(g.note) > 0:
|
|
||||||
for directive in findDirectiveRegEx.findall(g.note):
|
|
||||||
directive = directive.lower()
|
|
||||||
if directive in knownDirectives:
|
|
||||||
directives.add(directive)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
'unknown glyph directive !post:%s in glyph %s' % (directive, g.name),
|
|
||||||
file=sys.stderr
|
|
||||||
)
|
|
||||||
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, compact_style_names=False, *args, **kwargs):
|
|
||||||
super(VarFontProject, self).__init__(*args, **kwargs)
|
|
||||||
self.compact_style_names = compact_style_names
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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 self.compact_style_names:
|
|
||||||
collapseFontStyleName(ufo)
|
|
||||||
# update font version
|
|
||||||
updateFontVersion(ufo, isVF=True)
|
|
||||||
|
|
||||||
# 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):
|
|
||||||
glyphNamesToDecompose.add(g.name)
|
|
||||||
if 'removeoverlap' in directives:
|
|
||||||
if g.components and len(g.components) > 0:
|
|
||||||
glyphNamesToDecompose.add(g.name)
|
|
||||||
glyphsToRemoveOverlaps.add(g)
|
|
||||||
|
|
||||||
# decompose
|
|
||||||
if log.isEnabledFor(logging.INFO):
|
|
||||||
log.info('Decomposing glyphs:\n %s', "\n ".join(glyphNamesToDecompose))
|
|
||||||
decompose_glyphs(masters, glyphNamesToDecompose)
|
|
||||||
|
|
||||||
# remove overlaps
|
|
||||||
if len(glyphsToRemoveOverlaps) > 0:
|
|
||||||
rmoverlapFilter = RemoveOverlapsFilter(backend='pathops')
|
|
||||||
rmoverlapFilter.start()
|
|
||||||
if log.isEnabledFor(logging.INFO):
|
|
||||||
log.info(
|
|
||||||
'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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def updateFontVersion(font, dummy=False, isVF=False):
|
|
||||||
version = getVersion()
|
|
||||||
buildtag = getGitHash()
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
if dummy:
|
|
||||||
version = "1.0"
|
|
||||||
buildtag = "src"
|
|
||||||
now = datetime.datetime(2016, 1, 1, 0, 0, 0, 0)
|
|
||||||
versionMajor, versionMinor = [int(num) for num in version.split(".")]
|
|
||||||
font.info.version = version
|
|
||||||
font.info.versionMajor = versionMajor
|
|
||||||
font.info.versionMinor = versionMinor
|
|
||||||
font.info.woffMajorVersion = versionMajor
|
|
||||||
font.info.woffMinorVersion = versionMinor
|
|
||||||
font.info.year = now.year
|
|
||||||
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")
|
|
||||||
|
|
||||||
|
|
||||||
# setFontInfo patches font.info
|
|
||||||
#
|
|
||||||
def setFontInfo(font, weight):
|
|
||||||
#
|
|
||||||
# For UFO3 names, see
|
|
||||||
# https://github.com/unified-font-object/ufo-spec/blob/gh-pages/versions/
|
|
||||||
# ufo3/fontinfo.plist.md
|
|
||||||
# For OpenType NAME table IDs, see
|
|
||||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
|
|
||||||
|
|
||||||
# Add " BETA" to light weights
|
|
||||||
if weight < 400:
|
|
||||||
font.info.styleName = font.info.styleName + " BETA"
|
|
||||||
|
|
||||||
family = font.info.familyName # i.e. "Inter"
|
|
||||||
style = font.info.styleName # e.g. "Medium Italic"
|
|
||||||
|
|
||||||
# Update italicAngle
|
|
||||||
isitalic = style.find("Italic") != -1
|
|
||||||
if isitalic:
|
|
||||||
font.info.italicAngle = float('%.8g' % font.info.italicAngle)
|
|
||||||
else:
|
|
||||||
font.info.italicAngle = 0 # avoid "-0.0" value in UFO
|
|
||||||
|
|
||||||
# weight
|
|
||||||
font.info.openTypeOS2WeightClass = weight
|
|
||||||
|
|
||||||
# version (dummy)
|
|
||||||
updateFontVersion(font, dummy=True)
|
|
||||||
|
|
||||||
# Names
|
|
||||||
family_nosp = re.sub(r'\s', '', family)
|
|
||||||
style_nosp = re.sub(r'\s', '', style)
|
|
||||||
font.info.macintoshFONDName = "%s %s" % (family_nosp, style_nosp)
|
|
||||||
font.info.postscriptFontName = "%s-%s" % (family_nosp, style_nosp)
|
|
||||||
|
|
||||||
# name ID 16 "Typographic Family name"
|
|
||||||
font.info.openTypeNamePreferredFamilyName = family
|
|
||||||
|
|
||||||
# name ID 17 "Typographic Subfamily name"
|
|
||||||
font.info.openTypeNamePreferredSubfamilyName = style
|
|
||||||
|
|
||||||
# name ID 1 "Family name" (legacy, but required)
|
|
||||||
# Restriction:
|
|
||||||
# "shared among at most four fonts that differ only in weight or style"
|
|
||||||
# So we map as follows:
|
|
||||||
# - Regular => "Family", ("regular" | "italic" | "bold" | "bold italic")
|
|
||||||
# - Medium => "Family Medium", ("regular" | "italic")
|
|
||||||
# - Black => "Family Black", ("regular" | "italic")
|
|
||||||
# and so on.
|
|
||||||
subfamily = stripItalic(style).strip() # "A Italic" => "A", "A" => "A"
|
|
||||||
if len(subfamily) == 0:
|
|
||||||
subfamily = "Regular"
|
|
||||||
subfamily_lc = subfamily.lower()
|
|
||||||
if subfamily_lc == "regular" or subfamily_lc == "bold":
|
|
||||||
font.info.styleMapFamilyName = family
|
|
||||||
# name ID 2 "Subfamily name" (legacy, but required)
|
|
||||||
# Value must be one of: "regular", "italic", "bold", "bold italic"
|
|
||||||
if subfamily_lc == "regular":
|
|
||||||
if isitalic:
|
|
||||||
font.info.styleMapStyleName = "italic"
|
|
||||||
else:
|
|
||||||
font.info.styleMapStyleName = "regular"
|
|
||||||
else: # bold
|
|
||||||
if isitalic:
|
|
||||||
font.info.styleMapStyleName = "bold italic"
|
|
||||||
else:
|
|
||||||
font.info.styleMapStyleName = "bold"
|
|
||||||
else:
|
|
||||||
font.info.styleMapFamilyName = (family + ' ' + subfamily).strip()
|
|
||||||
# name ID 2 "Subfamily name" (legacy, but required)
|
|
||||||
if isitalic:
|
|
||||||
font.info.styleMapStyleName = "italic"
|
|
||||||
else:
|
|
||||||
font.info.styleMapStyleName = "regular"
|
|
||||||
|
|
||||||
|
|
||||||
def collapseFontStyleName(font):
|
def collapseFontStyleName(font):
|
||||||
# collapse whitespace in style name. i.e. "Semi Bold Italic" -> "SemiBoldItalic"
|
# collapse whitespace in style name. i.e. "Semi Bold Italic" -> "SemiBoldItalic"
|
||||||
font.info.styleName = re.sub(r'\s', '', font.info.styleName)
|
font.info.styleName = re.sub(r'\s', '', font.info.styleName)
|
||||||
|
|
@ -365,7 +100,8 @@ class Main(object):
|
||||||
|
|
||||||
# parse CLI arguments
|
# parse CLI arguments
|
||||||
args = argparser.parse_args(argv[1:i])
|
args = argparser.parse_args(argv[1:i])
|
||||||
logFormat = '%(funcName)s: %(message)s'
|
|
||||||
|
# log config
|
||||||
if args.quiet:
|
if args.quiet:
|
||||||
self.quiet = True
|
self.quiet = True
|
||||||
if args.debug:
|
if args.debug:
|
||||||
|
|
@ -373,14 +109,13 @@ class Main(object):
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
fatal("--quiet and --verbose are mutually exclusive arguments")
|
fatal("--quiet and --verbose are mutually exclusive arguments")
|
||||||
elif args.debug:
|
elif args.debug:
|
||||||
logging.basicConfig(level=logging.DEBUG, format=logFormat)
|
logging.basicConfig(level=logging.DEBUG, format='%(funcName)s: %(message)s')
|
||||||
self.logLevelName = 'DEBUG'
|
self.logLevelName = 'DEBUG'
|
||||||
elif args.verbose:
|
elif args.verbose:
|
||||||
logging.basicConfig(level=logging.INFO, format=logFormat)
|
logging.basicConfig(level=logging.INFO, format='%(funcName)s: %(message)s')
|
||||||
self.logLevelName = 'INFO'
|
self.logLevelName = 'INFO'
|
||||||
else:
|
else:
|
||||||
logFormat = '%(message)s'
|
logging.basicConfig(level=logging.WARNING, format='%(message)s')
|
||||||
logging.basicConfig(level=logging.WARNING, format=logFormat)
|
|
||||||
self.logLevelName = 'WARNING'
|
self.logLevelName = 'WARNING'
|
||||||
|
|
||||||
if args.chdir:
|
if args.chdir:
|
||||||
|
|
@ -404,49 +139,22 @@ class Main(object):
|
||||||
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
||||||
help='Output font file')
|
help='Output font file')
|
||||||
|
|
||||||
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\"")
|
|
||||||
|
|
||||||
args = argparser.parse_args(argv)
|
args = argparser.parse_args(argv)
|
||||||
|
|
||||||
# decide output filename (or check user-provided name)
|
# decide output filename (or check user-provided name)
|
||||||
outfilename = args.output
|
outfilename = args.output
|
||||||
if outfilename is None or outfilename == '':
|
if outfilename is None or outfilename == '':
|
||||||
outfilename = os.path.splitext(basename(args.srcfile))[0] + '.var.otf'
|
outfilename = pjoin(
|
||||||
log.info('setting --output %r' % outfilename)
|
dirname(args.srcfile),
|
||||||
|
os.path.splitext(basename(args.srcfile))[0] + '.otf'
|
||||||
|
)
|
||||||
|
log.debug('setting --output %r' % outfilename)
|
||||||
|
|
||||||
mkdirs(dirname(outfilename))
|
mkdirs(dirname(outfilename))
|
||||||
|
|
||||||
project = VarFontProject(
|
FontBuilder().buildVariable(args.srcfile, outfilename)
|
||||||
verbose=self.logLevelName,
|
|
||||||
compact_style_names=args.compact_style_names,
|
|
||||||
)
|
|
||||||
project.run_from_designspace(
|
|
||||||
args.srcfile,
|
|
||||||
interpolate=False,
|
|
||||||
masters_as_instances=False,
|
|
||||||
use_production_names=True,
|
|
||||||
round_instances=True,
|
|
||||||
output_path=outfilename,
|
|
||||||
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)
|
self.log("write %s" % outfilename)
|
||||||
|
|
||||||
# Note: we can't run ots-sanitize on the generated file as OTS
|
# Note: we can't run ots-sanitize on the generated file as OTS
|
||||||
# currently doesn't support variable fonts.
|
# currently doesn't support variable fonts.
|
||||||
|
|
||||||
|
|
@ -461,80 +169,40 @@ class Main(object):
|
||||||
help='Source file (.ufo file)')
|
help='Source file (.ufo file)')
|
||||||
|
|
||||||
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
||||||
help='Output font file (.otf, .ttf or .ufo)')
|
help='Output font file (.otf or .ttf)')
|
||||||
|
|
||||||
argparser.add_argument('--validate', action='store_true',
|
argparser.add_argument('--validate', action='store_true',
|
||||||
help='Enable ufoLib validation on reading/writing UFO files')
|
help='Enable ufoLib validation on reading/writing UFO files')
|
||||||
|
|
||||||
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\"")
|
|
||||||
|
|
||||||
args = argparser.parse_args(argv)
|
args = argparser.parse_args(argv)
|
||||||
|
|
||||||
ext_to_format = {
|
# write an OTF/CFF (or a TTF if false)
|
||||||
'.ufo': 'ufo',
|
cff = True
|
||||||
'.otf': 'otf',
|
|
||||||
'.ttf': 'ttf',
|
|
||||||
|
|
||||||
# non-filename mapping targets: (kept for completeness)
|
|
||||||
# 'ttf-interpolatable',
|
|
||||||
# 'variable',
|
|
||||||
}
|
|
||||||
|
|
||||||
# decide output filename
|
# decide output filename
|
||||||
outfilename = args.output
|
outfilename = args.output
|
||||||
if outfilename is None or outfilename == '':
|
if outfilename is None or outfilename == '':
|
||||||
outfilename = os.path.splitext(basename(args.srcfile))[0] + '.otf'
|
outfilename = pjoin(
|
||||||
log.info('setting --output %r' % outfilename)
|
dirname(args.srcfile),
|
||||||
|
os.path.splitext(basename(args.srcfile))[0] + '.otf'
|
||||||
# build formats list from filename extension
|
)
|
||||||
formats = []
|
log.debug('setting --output %r' % outfilename)
|
||||||
# for outfilename in args.outputs:
|
|
||||||
ext = os.path.splitext(outfilename)[1]
|
|
||||||
ext_lc = ext.lower()
|
|
||||||
if ext_lc in ext_to_format:
|
|
||||||
formats.append(ext_to_format[ext_lc])
|
|
||||||
else:
|
else:
|
||||||
fatal('Unsupported output format %s' % ext)
|
fext = os.path.splitext(outfilename)[1].lower()
|
||||||
|
if fext == ".ttf":
|
||||||
|
cff = False
|
||||||
|
elif fext != ".otf":
|
||||||
|
raise Exception('invalid file format %r (expected ".otf" or ".ttf")' % fext)
|
||||||
|
|
||||||
# temp file to write to
|
# temp file to write to
|
||||||
tmpfilename = pjoin(self.tmpdir, basename(outfilename))
|
tmpfilename = pjoin(self.tmpdir, basename(outfilename))
|
||||||
mkdirs(self.tmpdir)
|
mkdirs(self.tmpdir)
|
||||||
|
|
||||||
project = FontProject(verbose=self.logLevelName, validate_ufo=args.validate)
|
# build OTF or TTF file from UFO
|
||||||
|
FontBuilder().buildStatic(args.srcfile, tmpfilename, cff)
|
||||||
|
|
||||||
ufo = Font(args.srcfile)
|
# Run ots-sanitize on produced OTF/TTF file and write sanitized version to outfilename
|
||||||
|
self._ots_sanitize(tmpfilename, outfilename)
|
||||||
# patch style name if --compact-style-names is set
|
|
||||||
if args.compact_style_names:
|
|
||||||
collapseFontStyleName(ufo)
|
|
||||||
|
|
||||||
# update version to actual, real version.
|
|
||||||
# must come after collapseFontStyleName or any other call to setFontInfo.
|
|
||||||
updateFontVersion(ufo)
|
|
||||||
|
|
||||||
# if outfile is a ufo, simply move it to outfilename instead
|
|
||||||
# of running ots-sanitize.
|
|
||||||
output_path = tmpfilename
|
|
||||||
if formats[0] == 'ufo':
|
|
||||||
output_path = outfilename
|
|
||||||
|
|
||||||
# run fontmake to produce OTF/TTF file at tmpfilename
|
|
||||||
project.run_from_ufos(
|
|
||||||
[ ufo ],
|
|
||||||
output_path=output_path,
|
|
||||||
output=formats,
|
|
||||||
use_production_names=True,
|
|
||||||
optimize_cff=CFFOptimization.SUBROUTINIZE, # NONE
|
|
||||||
overlaps_backend='pathops', # use Skia's pathops
|
|
||||||
remove_overlaps=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if output_path == tmpfilename:
|
|
||||||
# Run ots-sanitize on produced OTF/TTF file and write sanitized version
|
|
||||||
# to outfilename
|
|
||||||
self._ots_sanitize(output_path, outfilename)
|
|
||||||
|
|
||||||
|
|
||||||
def _ots_sanitize(self, tmpfilename, outfilename):
|
def _ots_sanitize(self, tmpfilename, outfilename):
|
||||||
|
|
@ -680,6 +348,8 @@ class Main(object):
|
||||||
if outdir is None:
|
if outdir is None:
|
||||||
outdir = dirname(args.glyphsfile)
|
outdir = dirname(args.glyphsfile)
|
||||||
|
|
||||||
|
# TODO: Move into fontbuildlib
|
||||||
|
|
||||||
# files
|
# files
|
||||||
master_dir = outdir
|
master_dir = outdir
|
||||||
glyphsfile = args.glyphsfile
|
glyphsfile = args.glyphsfile
|
||||||
|
|
@ -808,7 +478,7 @@ class Main(object):
|
||||||
verbose=True
|
verbose=True
|
||||||
)
|
)
|
||||||
|
|
||||||
designspace = designspaceLib.DesignSpaceDocument()
|
designspace = DesignSpaceDocument()
|
||||||
designspace.read(designspace_file)
|
designspace.read(designspace_file)
|
||||||
|
|
||||||
# Generate UFOs for instances
|
# Generate UFOs for instances
|
||||||
|
|
@ -923,23 +593,24 @@ class Main(object):
|
||||||
infile = args.input
|
infile = args.input
|
||||||
outfile = args.output or infile
|
outfile = args.output or infile
|
||||||
|
|
||||||
font = font_names.loadFont(infile)
|
font = loadTTFont(infile)
|
||||||
editCount = 0
|
editCount = 0
|
||||||
try:
|
try:
|
||||||
if args.family:
|
if args.family:
|
||||||
editCount += 1
|
editCount += 1
|
||||||
font_names.setFamilyName(font, args.family)
|
setFamilyName(font, args.family)
|
||||||
|
|
||||||
if args.compact_style:
|
if args.compact_style:
|
||||||
editCount += 1
|
editCount += 1
|
||||||
font_names.removeWhitespaceFromStyles(font)
|
removeWhitespaceFromStyles(font)
|
||||||
|
|
||||||
if editCount > 0:
|
if editCount == 0:
|
||||||
font.save(outfile)
|
|
||||||
else:
|
|
||||||
print("no rename options provided", file=sys.stderr)
|
print("no rename options provided", file=sys.stderr)
|
||||||
argparser.print_usage(sys.stderr)
|
argparser.print_help(sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
font.save(outfile)
|
||||||
finally:
|
finally:
|
||||||
font.close()
|
font.close()
|
||||||
|
|
||||||
|
|
|
||||||
1
misc/fontbuildlib/__init__.py
Normal file
1
misc/fontbuildlib/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from .builder import FontBuilder
|
||||||
149
misc/fontbuildlib/builder.py
Normal file
149
misc/fontbuildlib/builder.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import logging
|
||||||
|
import ufo2ft
|
||||||
|
from defcon import Font
|
||||||
|
from ufo2ft.util import _LazyFontName
|
||||||
|
from ufo2ft.filters.removeOverlaps import RemoveOverlapsFilter
|
||||||
|
from fontTools.designspaceLib import DesignSpaceDocument
|
||||||
|
from .name import getFamilyName, setFullName
|
||||||
|
from .info import updateFontVersion
|
||||||
|
from .glyph import findGlyphDirectives, composedGlyphIsTrivial, decomposeGlyphs
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FontBuilder:
|
||||||
|
# def __init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def buildStatic(self,
|
||||||
|
ufo, # input UFO as filename string or defcon.Font object
|
||||||
|
outputFilename, # output filename string
|
||||||
|
cff=True, # true = makes CFF outlines. false = makes TTF outlines.
|
||||||
|
**kwargs, # passed along to ufo2ft.compile*()
|
||||||
|
):
|
||||||
|
if isinstance(ufo, str):
|
||||||
|
ufo = Font(ufo)
|
||||||
|
|
||||||
|
# update version to actual, real version. Must come after any call to setFontInfo.
|
||||||
|
updateFontVersion(ufo, dummy=False, isVF=False)
|
||||||
|
|
||||||
|
compilerOptions = dict(
|
||||||
|
useProductionNames=True,
|
||||||
|
inplace=True, # avoid extra copy
|
||||||
|
removeOverlaps=True,
|
||||||
|
overlapsBackend='pathops', # use Skia's pathops
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("compiling %s -> %s (%s)", _LazyFontName(ufo), outputFilename,
|
||||||
|
"OTF/CFF-2" if cff else "TTF")
|
||||||
|
|
||||||
|
if cff:
|
||||||
|
font = ufo2ft.compileOTF(ufo, **compilerOptions)
|
||||||
|
else: # ttf
|
||||||
|
font = ufo2ft.compileTTF(ufo, **compilerOptions)
|
||||||
|
|
||||||
|
log.debug("writing %s", outputFilename)
|
||||||
|
font.save(outputFilename)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def buildVariable(self,
|
||||||
|
designspace, # designspace filename string or DesignSpaceDocument object
|
||||||
|
outputFilename, # output filename string
|
||||||
|
cff=False, # if true, builds CFF-2 font, else TTF
|
||||||
|
**kwargs, # passed along to ufo2ft.compileVariable*()
|
||||||
|
):
|
||||||
|
designspace = self._loadDesignspace(designspace)
|
||||||
|
|
||||||
|
# check in the designspace's <lib> element if user supplied a custom featureWriters
|
||||||
|
# configuration; if so, use that for all the UFOs built from this designspace.
|
||||||
|
featureWriters = None
|
||||||
|
if ufo2ft.featureWriters.FEATURE_WRITERS_KEY in designspace.lib:
|
||||||
|
featureWriters = ufo2ft.featureWriters.loadFeatureWriters(designspace)
|
||||||
|
|
||||||
|
compilerOptions = dict(
|
||||||
|
useProductionNames=True,
|
||||||
|
featureWriters=featureWriters,
|
||||||
|
inplace=True, # avoid extra copy
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
if log.isEnabledFor(logging.INFO):
|
||||||
|
log.info("compiling %s -> %s (%s)", designspace.path, outputFilename,
|
||||||
|
"OTF/CFF-2" if cff else "TTF")
|
||||||
|
|
||||||
|
if cff:
|
||||||
|
font = ufo2ft.compileVariableCFF2(designspace, **compilerOptions)
|
||||||
|
else:
|
||||||
|
font = ufo2ft.compileVariableTTF(designspace, **compilerOptions)
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
setFullName(font, getFamilyName(font))
|
||||||
|
|
||||||
|
log.debug("writing %s", outputFilename)
|
||||||
|
font.save(outputFilename)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _loadDesignspace(designspace):
|
||||||
|
log.info("loading designspace sources")
|
||||||
|
if isinstance(designspace, str):
|
||||||
|
designspace = DesignSpaceDocument.fromfile(designspace)
|
||||||
|
else:
|
||||||
|
# copy that we can mess with
|
||||||
|
designspace = DesignSpaceDocument.fromfile(designspace.path)
|
||||||
|
|
||||||
|
masters = designspace.loadSourceFonts(opener=Font)
|
||||||
|
# masters = [s.font for s in designspace.sources] # list of UFO font objects
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
# update font version
|
||||||
|
updateFontVersion(ufo, dummy=False, isVF=True)
|
||||||
|
|
||||||
|
log.info("Preprocessing glyphs")
|
||||||
|
# find glyphs subject to decomposition and/or overlap removal
|
||||||
|
# TODO: Find out why this loop is SO DAMN SLOW. It might just be so that defcon is
|
||||||
|
# really slow when reading glyphs. Perhaps we can sidestep defcon and just
|
||||||
|
# read & parse the .glif files ourselves.
|
||||||
|
glyphNamesToDecompose = set() # glyph names
|
||||||
|
glyphsToRemoveOverlaps = set() # glyph objects
|
||||||
|
for ufo in masters:
|
||||||
|
for g in ufo:
|
||||||
|
if g.components and not composedGlyphIsTrivial(g):
|
||||||
|
glyphNamesToDecompose.add(g.name)
|
||||||
|
if 'removeoverlap' in findGlyphDirectives(g.note):
|
||||||
|
if g.components and len(g.components) > 0:
|
||||||
|
glyphNamesToDecompose.add(g.name)
|
||||||
|
glyphsToRemoveOverlaps.add(g)
|
||||||
|
|
||||||
|
# decompose
|
||||||
|
if glyphNamesToDecompose:
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
log.debug('Decomposing glyphs:\n %s', "\n ".join(glyphNamesToDecompose))
|
||||||
|
elif log.isEnabledFor(logging.INFO):
|
||||||
|
log.info('Decomposing %d glyphs', len(glyphNamesToDecompose))
|
||||||
|
decomposeGlyphs(masters, glyphNamesToDecompose)
|
||||||
|
|
||||||
|
# remove overlaps
|
||||||
|
if glyphsToRemoveOverlaps:
|
||||||
|
rmoverlapFilter = RemoveOverlapsFilter(backend='pathops')
|
||||||
|
rmoverlapFilter.start()
|
||||||
|
if log.isEnabledFor(logging.DEBUG):
|
||||||
|
log.debug(
|
||||||
|
'Removing overlaps in glyphs:\n %s',
|
||||||
|
"\n ".join(set([g.name for g in glyphsToRemoveOverlaps])),
|
||||||
|
)
|
||||||
|
elif log.isEnabledFor(logging.INFO):
|
||||||
|
log.info('Removing overlaps in %d glyphs', len(glyphsToRemoveOverlaps))
|
||||||
|
for g in glyphsToRemoveOverlaps:
|
||||||
|
rmoverlapFilter.filter(g)
|
||||||
|
|
||||||
|
# handle control back to fontmake
|
||||||
|
return designspace
|
||||||
|
|
||||||
86
misc/fontbuildlib/glyph.py
Normal file
86
misc/fontbuildlib/glyph.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import re
|
||||||
|
from fontTools.pens.transformPen import TransformPen
|
||||||
|
from fontTools.misc.transform import Transform
|
||||||
|
from fontTools.pens.reverseContourPen import ReverseContourPen
|
||||||
|
|
||||||
|
# Directives are glyph-specific post-processing directives for the compiler.
|
||||||
|
# A directive is added to the "note" section of a glyph and takes the
|
||||||
|
# following form:
|
||||||
|
#
|
||||||
|
# !post:DIRECTIVE
|
||||||
|
#
|
||||||
|
# Where DIRECTIVE is the name of a known directive.
|
||||||
|
# This string can appear anywhere in the glyph note.
|
||||||
|
# Directives are _not_ case sensitive but normalized by str.lower(), meaning
|
||||||
|
# that e.g. "removeoverlap" == "RemoveOverlap" == "REMOVEOVERLAP".
|
||||||
|
#
|
||||||
|
knownDirectives = set([
|
||||||
|
'removeoverlap', # applies overlap removal (boolean union)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
_findDirectiveRegEx = re.compile(r'\!post:([^ ]+)', re.I | re.U)
|
||||||
|
|
||||||
|
|
||||||
|
def findGlyphDirectives(string): # -> set<string> | None
|
||||||
|
directives = set()
|
||||||
|
if string and len(string) > 0:
|
||||||
|
for directive in _findDirectiveRegEx.findall(string):
|
||||||
|
directive = directive.lower()
|
||||||
|
if directive in knownDirectives:
|
||||||
|
directives.add(directive)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
'unknown glyph directive !post:%s in glyph %s' % (directive, g.name),
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
return directives
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def composedGlyphIsTrivial(g):
|
||||||
|
# A trivial glyph is one that does not use components or where component transformation
|
||||||
|
# does not include mirroring (i.e. "flipped").
|
||||||
|
if g.components and len(g.components) > 0:
|
||||||
|
for c in g.components:
|
||||||
|
# has non-trivial transformation? (i.e. scaled)
|
||||||
|
# Example of optimally trivial transformation:
|
||||||
|
# (1, 0, 0, 1, 0, 0) no scale or offset
|
||||||
|
# Example of scaled transformation matrix:
|
||||||
|
# (-1.0, 0, 0.3311, 1, 1464.0, 0) flipped x axis, sheered and offset
|
||||||
|
#
|
||||||
|
xScale = c.transformation[0]
|
||||||
|
yScale = c.transformation[3]
|
||||||
|
# If glyph is reflected along x or y axes, it won't slant well.
|
||||||
|
if xScale < 0 or yScale < 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def decomposeGlyphs(ufos, glyphNamesToDecompose):
|
||||||
|
for ufo in ufos:
|
||||||
|
for glyphname in glyphNamesToDecompose:
|
||||||
|
glyph = ufo[glyphname]
|
||||||
|
_deepCopyContours(ufo, glyph, glyph, Transform())
|
||||||
|
glyph.clearComponents()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _deepCopyContours(ufo, parent, component, transformation):
|
||||||
|
"""Copy contours from component to parent, including nested components."""
|
||||||
|
for nested in component.components:
|
||||||
|
_deepCopyContours(
|
||||||
|
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)
|
||||||
148
misc/fontbuildlib/info.py
Normal file
148
misc/fontbuildlib/info.py
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from common import getGitHash, getVersion
|
||||||
|
from .util import readTextFile, BASEDIR, pjoin
|
||||||
|
|
||||||
|
|
||||||
|
_gitHash = None
|
||||||
|
def getGitHash():
|
||||||
|
global _gitHash
|
||||||
|
if _gitHash is None:
|
||||||
|
_gitHash = ''
|
||||||
|
try:
|
||||||
|
_gitHash = subprocess.check_output(
|
||||||
|
['git', '-C', BASEDIR, 'rev-parse', '--short', 'HEAD'],
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
**_enc_kwargs
|
||||||
|
).strip()
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
# git rev-parse --short HEAD > githash.txt
|
||||||
|
_gitHash = readTextFile(pjoin(BASEDIR, 'githash.txt')).strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return _gitHash
|
||||||
|
|
||||||
|
|
||||||
|
_version = None
|
||||||
|
def getVersion():
|
||||||
|
global _version
|
||||||
|
if _version is None:
|
||||||
|
_version = readTextFile(pjoin(BASEDIR, 'version.txt')).strip()
|
||||||
|
return _version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def updateFontVersion(font, dummy, isVF):
|
||||||
|
if dummy:
|
||||||
|
version = "1.0"
|
||||||
|
buildtag = "src"
|
||||||
|
now = datetime(2016, 1, 1, 0, 0, 0, 0)
|
||||||
|
else:
|
||||||
|
version = getVersion()
|
||||||
|
buildtag = getGitHash()
|
||||||
|
now = datetime.utcnow()
|
||||||
|
versionMajor, versionMinor = [int(num) for num in version.split(".")]
|
||||||
|
font.info.version = version
|
||||||
|
font.info.versionMajor = versionMajor
|
||||||
|
font.info.versionMinor = versionMinor
|
||||||
|
font.info.woffMajorVersion = versionMajor
|
||||||
|
font.info.woffMinorVersion = versionMinor
|
||||||
|
font.info.year = now.year
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# setFontInfo patches font.info
|
||||||
|
def setFontInfo(font, weight=None):
|
||||||
|
#
|
||||||
|
# For UFO3 names, see
|
||||||
|
# https://github.com/unified-font-object/ufo-spec/blob/gh-pages/versions/
|
||||||
|
# ufo3/fontinfo.plist.md
|
||||||
|
# For OpenType NAME table IDs, see
|
||||||
|
# https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
|
||||||
|
|
||||||
|
if weight is None:
|
||||||
|
weight = font.info.openTypeOS2WeightClass
|
||||||
|
|
||||||
|
# Add " BETA" to light weights
|
||||||
|
if weight < 400:
|
||||||
|
font.info.styleName = font.info.styleName + " BETA"
|
||||||
|
|
||||||
|
family = font.info.familyName # i.e. "Inter"
|
||||||
|
style = font.info.styleName # e.g. "Medium Italic"
|
||||||
|
|
||||||
|
# Update italicAngle
|
||||||
|
isitalic = style.find("Italic") != -1
|
||||||
|
if isitalic:
|
||||||
|
font.info.italicAngle = float('%.8g' % font.info.italicAngle)
|
||||||
|
else:
|
||||||
|
font.info.italicAngle = 0 # avoid "-0.0" value in UFO
|
||||||
|
|
||||||
|
# weight
|
||||||
|
font.info.openTypeOS2WeightClass = weight
|
||||||
|
|
||||||
|
# version (dummy)
|
||||||
|
updateFontVersion(font, dummy=True, isVF=False)
|
||||||
|
|
||||||
|
# Names
|
||||||
|
family_nosp = re.sub(r'\s', '', family)
|
||||||
|
style_nosp = re.sub(r'\s', '', style)
|
||||||
|
font.info.macintoshFONDName = "%s %s" % (family_nosp, style_nosp)
|
||||||
|
font.info.postscriptFontName = "%s-%s" % (family_nosp, style_nosp)
|
||||||
|
|
||||||
|
# name ID 16 "Typographic Family name"
|
||||||
|
font.info.openTypeNamePreferredFamilyName = family
|
||||||
|
|
||||||
|
# name ID 17 "Typographic Subfamily name"
|
||||||
|
font.info.openTypeNamePreferredSubfamilyName = style
|
||||||
|
|
||||||
|
# name ID 1 "Family name" (legacy, but required)
|
||||||
|
# Restriction:
|
||||||
|
# "shared among at most four fonts that differ only in weight or style"
|
||||||
|
# So we map as follows:
|
||||||
|
# - Regular => "Family", ("regular" | "italic" | "bold" | "bold italic")
|
||||||
|
# - Medium => "Family Medium", ("regular" | "italic")
|
||||||
|
# - Black => "Family Black", ("regular" | "italic")
|
||||||
|
# and so on.
|
||||||
|
subfamily = stripItalic(style).strip() # "A Italic" => "A", "A" => "A"
|
||||||
|
if len(subfamily) == 0:
|
||||||
|
subfamily = "Regular"
|
||||||
|
subfamily_lc = subfamily.lower()
|
||||||
|
if subfamily_lc == "regular" or subfamily_lc == "bold":
|
||||||
|
font.info.styleMapFamilyName = family
|
||||||
|
# name ID 2 "Subfamily name" (legacy, but required)
|
||||||
|
# Value must be one of: "regular", "italic", "bold", "bold italic"
|
||||||
|
if subfamily_lc == "regular":
|
||||||
|
if isitalic:
|
||||||
|
font.info.styleMapStyleName = "italic"
|
||||||
|
else:
|
||||||
|
font.info.styleMapStyleName = "regular"
|
||||||
|
else: # bold
|
||||||
|
if isitalic:
|
||||||
|
font.info.styleMapStyleName = "bold italic"
|
||||||
|
else:
|
||||||
|
font.info.styleMapStyleName = "bold"
|
||||||
|
else:
|
||||||
|
font.info.styleMapFamilyName = (family + ' ' + subfamily).strip()
|
||||||
|
# name ID 2 "Subfamily name" (legacy, but required)
|
||||||
|
if isitalic:
|
||||||
|
font.info.styleMapStyleName = "italic"
|
||||||
|
else:
|
||||||
|
font.info.styleMapStyleName = "regular"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
stripItalic_re = re.compile(r'(?:^|\b)italic\b|italic$', re.I | re.U)
|
||||||
|
|
||||||
|
|
||||||
|
def stripItalic(name):
|
||||||
|
return stripItalic_re.sub('', name.strip())
|
||||||
143
misc/fontbuildlib/name.py
Normal file
143
misc/fontbuildlib/name.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from .util import loadTTFont
|
||||||
|
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 renameFontFamily(infile, outfile, newFamilyName):
|
||||||
|
# font = loadTTFont(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(loadTTFont(infile)) ))
|
||||||
|
# print("%s familyName: %r" % (outfile, getFamilyName(loadTTFont(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
|
||||||
29
misc/fontbuildlib/util.py
Normal file
29
misc/fontbuildlib/util.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
|
from fontTools.ttLib import TTFont
|
||||||
|
from os.path import dirname, abspath, join as pjoin
|
||||||
|
|
||||||
|
PYVER = sys.version_info[0]
|
||||||
|
BASEDIR = abspath(pjoin(dirname(__file__), os.pardir, os.pardir))
|
||||||
|
|
||||||
|
_enc_kwargs = {}
|
||||||
|
if PYVER >= 3:
|
||||||
|
_enc_kwargs = {'encoding': 'utf-8'}
|
||||||
|
|
||||||
|
|
||||||
|
def readTextFile(filename):
|
||||||
|
with open(filename, 'r', **_enc_kwargs) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def mkdirs(path):
|
||||||
|
try:
|
||||||
|
os.makedirs(path)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno != errno.EEXIST:
|
||||||
|
raise # raises the error again
|
||||||
|
|
||||||
|
|
||||||
|
def loadTTFont(file):
|
||||||
|
return TTFont(file, recalcBBoxes=False, recalcTimestamp=False)
|
||||||
0
misc/fontbuildlib/version.py
Normal file
0
misc/fontbuildlib/version.py
Normal file
|
|
@ -1,16 +1,12 @@
|
||||||
# note: These should match requirements of fontmake
|
fonttools[lxml,unicode,ufo]==4.0.1
|
||||||
|
cu2qu==1.6.6
|
||||||
fontmake==2.0.1
|
glyphsLib==5.0.1
|
||||||
fonttools[lxml,unicode,ufo]==3.44.0
|
ufo2ft[pathops]==2.9.1
|
||||||
glyphsLib==4.1.2
|
defcon[lxml]==0.6.0
|
||||||
skia-pathops==0.2.0.post2
|
skia-pathops==0.2.0.post2
|
||||||
ufo2ft==2.9.1
|
|
||||||
fs==2.4.10
|
|
||||||
|
|
||||||
# for fontTools/varLib/interpolatable.py
|
# only used for DesignSpaceDocumentReader in fontbuild
|
||||||
numpy==1.17.1
|
MutatorMath==2.1.2
|
||||||
scipy==1.3.1
|
|
||||||
munkres==1.1.2
|
|
||||||
|
|
||||||
# for woff2
|
# for woff2
|
||||||
brotli==1.0.7
|
brotli==1.0.7
|
||||||
|
|
|
||||||
Reference in a new issue