2018-09-03 22:55:49 +03:00
|
|
|
#!/usr/bin/env python
|
2018-09-04 00:19:38 +03:00
|
|
|
from __future__ import print_function, absolute_import
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
import sys, os
|
2018-09-04 00:19:38 +03:00
|
|
|
from os.path import dirname, basename, abspath, relpath, join as pjoin
|
|
|
|
sys.path.append(abspath(pjoin(dirname(__file__), 'tools')))
|
|
|
|
from common import BASEDIR, VENVDIR, getGitHash, getVersion
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
import argparse
|
|
|
|
import datetime
|
|
|
|
import glyphsLib
|
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
import signal
|
|
|
|
import subprocess
|
|
|
|
from fontmake.font_project import FontProject
|
|
|
|
from fontTools import designspaceLib
|
|
|
|
from glyphsLib.interpolation import apply_instance_data
|
|
|
|
from mutatorMath.ufo.document import DesignSpaceDocumentReader
|
|
|
|
|
|
|
|
|
2018-09-04 05:08:46 +03:00
|
|
|
stripItalic_re = re.compile(r'(?:^|\b)italic(?:\b|$)', re.I | re.U)
|
|
|
|
|
|
|
|
|
|
|
|
def stripItalic(name):
|
|
|
|
return stripItalic_re.sub('', name.strip())
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
def sighandler(signum, frame):
|
|
|
|
sys.stdout.write('\n')
|
|
|
|
sys.stdout.flush()
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
def mkdirs(path):
|
|
|
|
if not os.access(path, os.F_OK):
|
|
|
|
os.makedirs(path)
|
|
|
|
|
|
|
|
|
|
|
|
# setFontInfo patches font.info
|
|
|
|
#
|
2018-09-04 00:19:38 +03:00
|
|
|
def setFontInfo(font, weight, updateCreated=True):
|
2018-09-03 22:55:49 +03:00
|
|
|
#
|
|
|
|
# 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
|
|
|
|
#
|
|
|
|
version = getVersion()
|
|
|
|
buildtag = getGitHash()
|
|
|
|
versionMajor, versionMinor = [int(num) for num in version.split(".")]
|
|
|
|
now = datetime.datetime.utcnow()
|
|
|
|
family = font.info.familyName # i.e. "Inter UI"
|
|
|
|
style = font.info.styleName # e.g. "Medium Italic"
|
|
|
|
isitalic = font.info.italicAngle != 0
|
|
|
|
|
|
|
|
# weight
|
|
|
|
font.info.openTypeOS2WeightClass = weight
|
|
|
|
|
|
|
|
# creation date & time (YYYY/MM/DD HH:MM:SS)
|
2018-09-04 00:19:38 +03:00
|
|
|
if updateCreated:
|
|
|
|
font.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
# version
|
|
|
|
font.info.version = version
|
|
|
|
font.info.versionMajor = versionMajor
|
|
|
|
font.info.versionMinor = versionMinor
|
2018-09-04 05:08:46 +03:00
|
|
|
font.info.woffMajorVersion = versionMajor
|
|
|
|
font.info.woffMinorVersion = versionMinor
|
2018-09-03 22:55:49 +03:00
|
|
|
font.info.year = now.year
|
|
|
|
font.info.openTypeNameVersion = "%s;%s" % (version, buildtag)
|
|
|
|
font.info.openTypeNameUniqueID = "%s %s:%d:%s" % (family, style, now.year, buildtag)
|
|
|
|
|
|
|
|
# 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
|
2018-09-04 05:08:46 +03:00
|
|
|
|
2018-09-03 22:55:49 +03:00
|
|
|
# name ID 17 "Typographic Subfamily name"
|
2018-09-04 05:08:46 +03:00
|
|
|
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) # "A Italic" => "A", "A" => "A"
|
|
|
|
if len(subfamily) == 0:
|
2018-09-03 22:55:49 +03:00
|
|
|
subfamily = "Regular"
|
|
|
|
subfamily_lc = subfamily.lower()
|
2018-09-04 05:08:46 +03:00
|
|
|
if subfamily_lc == "regular" or subfamily_lc == "bold":
|
2018-09-03 22:55:49 +03:00
|
|
|
font.info.styleMapFamilyName = family
|
2018-09-04 05:08:46 +03:00
|
|
|
# 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
|
|
|
|
# name ID 2 "Subfamily name" (legacy, but required)
|
2018-09-03 22:55:49 +03:00
|
|
|
if isitalic:
|
2018-09-04 05:08:46 +03:00
|
|
|
font.info.styleMapStyleName = "italic"
|
2018-09-03 22:55:49 +03:00
|
|
|
else:
|
2018-09-04 05:08:46 +03:00
|
|
|
font.info.styleMapStyleName = "regular"
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
class Main(object):
|
|
|
|
|
|
|
|
def __init__(self):
|
2018-09-04 00:19:38 +03:00
|
|
|
self.tmpdir = pjoin(BASEDIR,'build','tmp')
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
|
|
|
|
def main(self, argv):
|
|
|
|
# make ^C instantly exit program
|
|
|
|
signal.signal(signal.SIGINT, sighandler)
|
|
|
|
|
|
|
|
argparser = argparse.ArgumentParser(
|
|
|
|
description='',
|
|
|
|
usage='''
|
|
|
|
%(prog)s [options] <command> [<args>]
|
|
|
|
|
|
|
|
Commands:
|
|
|
|
compile Build font files
|
|
|
|
glyphsync Generate designspace and UFOs from Glyphs file
|
|
|
|
instancegen Generate instance UFOs for designspace
|
|
|
|
'''.strip().replace('\n ', '\n'))
|
|
|
|
|
|
|
|
argparser.add_argument('-v', '--verbose', action='store_true',
|
|
|
|
help='Print more details')
|
|
|
|
|
|
|
|
argparser.add_argument('command', metavar='<command>')
|
|
|
|
|
|
|
|
args = argparser.parse_args(argv[1:2])
|
|
|
|
if args.verbose:
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
else:
|
|
|
|
logging.basicConfig(level=logging.ERROR)
|
|
|
|
cmd = 'cmd_' + args.command.replace('-', '_')
|
|
|
|
if not hasattr(self, cmd):
|
|
|
|
print('Unrecognized command %s. Try --help' % args.command,
|
|
|
|
file=sys.stderr)
|
|
|
|
exit(1)
|
|
|
|
getattr(self, cmd)(argv[2:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_compile(self, argv):
|
|
|
|
argparser = argparse.ArgumentParser(
|
|
|
|
usage='%(prog)s compile [-h] [-o <file>] <ufo>',
|
|
|
|
description='Compile font files')
|
|
|
|
|
|
|
|
argparser.add_argument('ufo', metavar='<ufo>',
|
|
|
|
help='UFO source file')
|
|
|
|
|
|
|
|
argparser.add_argument('-o', '--output', metavar='<fontfile>',
|
|
|
|
help='Output font file (.otf, .ttf, .ufo or .gx)')
|
|
|
|
|
|
|
|
argparser.add_argument('--validate', action='store_true',
|
|
|
|
help='Enable ufoLib validation on reading/writing UFO files')
|
|
|
|
|
|
|
|
# argparser.add_argument('-f', '--formats', nargs='+', metavar='<format>',
|
|
|
|
# help='Output formats. Any of %(choices)s. Defaults to "otf".',
|
|
|
|
# choices=tuple(all_formats),
|
|
|
|
# default=('otf'))
|
|
|
|
|
|
|
|
args = argparser.parse_args(argv)
|
|
|
|
|
|
|
|
ext_to_format = {
|
|
|
|
'.ufo': 'ufo',
|
|
|
|
'.otf': 'otf',
|
|
|
|
'.ttf': 'ttf',
|
|
|
|
# 'ttf-interpolatable',
|
|
|
|
'.gx': 'variable',
|
|
|
|
}
|
|
|
|
|
|
|
|
formats = []
|
|
|
|
filename = args.output
|
|
|
|
if filename is None or filename == '':
|
2018-09-04 00:19:38 +03:00
|
|
|
ufoname = basename(args.ufo)
|
2018-09-03 22:55:49 +03:00
|
|
|
name, ext = os.path.splitext(ufoname)
|
|
|
|
filename = name + '.otf'
|
|
|
|
logging.info('setting --output %r' % filename)
|
|
|
|
# for filename in args.output:
|
|
|
|
ext = os.path.splitext(filename)[1]
|
|
|
|
ext_lc = ext.lower()
|
|
|
|
if ext_lc in ext_to_format:
|
|
|
|
formats.append(ext_to_format[ext_lc])
|
|
|
|
else:
|
|
|
|
print('Unsupported output format %s' % ext, file=sys.stderr)
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
mkdirs(self.tmpdir)
|
|
|
|
|
|
|
|
project = FontProject(
|
|
|
|
timing=None,
|
|
|
|
verbose='WARNING',
|
|
|
|
validate_ufo=args.validate
|
|
|
|
)
|
|
|
|
|
|
|
|
project.run_from_ufos(
|
|
|
|
[args.ufo],
|
|
|
|
output_dir=self.tmpdir,
|
2018-09-04 05:08:46 +03:00
|
|
|
output=formats,
|
2018-09-07 18:33:14 +03:00
|
|
|
subroutinize=True,
|
|
|
|
overlaps_backend='pathops', # use Skia's pathops
|
2018-09-03 22:55:49 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
# run through ots-sanitize
|
|
|
|
# for filename in args.output:
|
2018-09-04 00:19:38 +03:00
|
|
|
tmpfile = pjoin(self.tmpdir, basename(filename))
|
|
|
|
mkdirs(dirname(filename))
|
2018-09-03 22:55:49 +03:00
|
|
|
success = True
|
|
|
|
try:
|
|
|
|
otssan_res = subprocess.check_output(
|
|
|
|
['ots-sanitize', tmpfile, filename],
|
2018-09-04 05:08:46 +03:00
|
|
|
# ['cp', tmpfile, filename],
|
2018-09-03 22:55:49 +03:00
|
|
|
shell=False
|
|
|
|
).strip()
|
|
|
|
# Note: ots-sanitize does not exit with an error in many cases where
|
|
|
|
# it fails to sanitize the font.
|
|
|
|
success = otssan_res.find('Failed') == -1
|
|
|
|
except:
|
|
|
|
success = False
|
|
|
|
otssan_res = 'error'
|
|
|
|
|
|
|
|
if success:
|
|
|
|
os.unlink(tmpfile)
|
|
|
|
else:
|
|
|
|
print('ots-sanitize failed for %s: %s' % (
|
|
|
|
tmpfile, otssan_res), file=sys.stderr)
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_glyphsync(self, argv):
|
|
|
|
argparser = argparse.ArgumentParser(
|
|
|
|
usage='%(prog)s glyphsync <glyphsfile> [options]',
|
|
|
|
description='Generates designspace and UFOs from Glyphs file')
|
|
|
|
|
|
|
|
argparser.add_argument('glyphsfile', metavar='<glyphsfile>',
|
|
|
|
help='Glyphs source file')
|
|
|
|
|
|
|
|
argparser.add_argument('-o', '--outdir', metavar='<dir>',
|
|
|
|
help='''Write output to <dir>. If omitted, designspace and UFOs are
|
|
|
|
written to the directory of the glyphs file.
|
|
|
|
'''.strip().replace('\n ', ''))
|
|
|
|
|
|
|
|
args = argparser.parse_args(argv)
|
|
|
|
|
|
|
|
outdir = args.outdir
|
|
|
|
if outdir is None:
|
2018-09-04 00:19:38 +03:00
|
|
|
outdir = dirname(args.glyphsfile)
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
# files
|
|
|
|
master_dir = outdir
|
|
|
|
glyphsfile = args.glyphsfile
|
2018-09-04 00:19:38 +03:00
|
|
|
designspace_file = pjoin(outdir, 'Inter-UI.designspace')
|
|
|
|
instance_dir = pjoin(BASEDIR, 'build', 'ufo')
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
# load glyphs project file
|
|
|
|
print("generating %s from %s" % (
|
2018-09-04 00:19:38 +03:00
|
|
|
relpath(designspace_file, os.getcwd()),
|
|
|
|
relpath(glyphsfile, os.getcwd())
|
2018-09-03 22:55:49 +03:00
|
|
|
))
|
|
|
|
font = glyphsLib.GSFont(glyphsfile)
|
|
|
|
|
|
|
|
# generate designspace from glyphs project
|
|
|
|
designspace = glyphsLib.to_designspace(
|
|
|
|
font,
|
|
|
|
propagate_anchors=False,
|
2018-09-04 00:19:38 +03:00
|
|
|
instance_dir=relpath(instance_dir, master_dir)
|
2018-09-03 22:55:49 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
# strip lib data
|
|
|
|
designspace.lib.clear()
|
|
|
|
|
|
|
|
# fixup axes
|
|
|
|
for axis in designspace.axes:
|
|
|
|
if axis.tag == "wght":
|
|
|
|
axis.map = []
|
|
|
|
axis.minimum = 100
|
|
|
|
axis.maximum = 900
|
|
|
|
axis.default = 400
|
|
|
|
|
|
|
|
# patch and write UFO files
|
|
|
|
# TODO: Only write out-of-date UFOs
|
|
|
|
for source in designspace.sources:
|
|
|
|
# source : fontTools.designspaceLib.SourceDescriptor
|
|
|
|
# source.font : defcon.objects.font.Font
|
2018-09-04 00:19:38 +03:00
|
|
|
ufo_path = pjoin(master_dir, source.filename.replace('InterUI', 'Inter-UI'))
|
2018-09-03 22:55:49 +03:00
|
|
|
# no need to also set the relative 'filename' attribute as that
|
|
|
|
# will be auto-updated on writing the designspace document
|
|
|
|
|
|
|
|
# name "Inter UI Black" => "black"
|
|
|
|
source.name = source.styleName.lower().replace(' ', '')
|
|
|
|
|
|
|
|
# fixup font info
|
|
|
|
weight = int(source.location['Weight'])
|
2018-09-04 00:19:38 +03:00
|
|
|
setFontInfo(source.font, weight, updateCreated=False)
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
# cleanup lib
|
|
|
|
lib = dict()
|
|
|
|
for key, value in source.font.lib.iteritems():
|
|
|
|
if key.startswith('com.schriftgestaltung'):
|
|
|
|
continue
|
|
|
|
if key == 'public.postscriptNames':
|
|
|
|
continue
|
|
|
|
lib[key] = value
|
|
|
|
source.font.lib.clear()
|
|
|
|
source.font.lib.update(lib)
|
|
|
|
|
|
|
|
# write UFO file
|
|
|
|
source.path = ufo_path
|
2018-09-04 00:19:38 +03:00
|
|
|
print("write %s" % relpath(ufo_path, os.getcwd()))
|
2018-09-03 22:55:49 +03:00
|
|
|
source.font.save(ufo_path)
|
|
|
|
|
|
|
|
# patch instance names
|
|
|
|
for instance in designspace.instances:
|
|
|
|
# name "Inter UI Black Italic" => "blackitalic"
|
|
|
|
instance.name = instance.styleName.lower().replace(' ', '')
|
|
|
|
instance.filename = instance.filename.replace('InterUI', 'Inter-UI')
|
|
|
|
|
2018-09-04 00:19:38 +03:00
|
|
|
print("write %s" % relpath(designspace_file, os.getcwd()))
|
2018-09-03 22:55:49 +03:00
|
|
|
designspace.write(designspace_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_instancegen(self, argv):
|
|
|
|
argparser = argparse.ArgumentParser(
|
|
|
|
description='Generate UFO instances from designspace')
|
|
|
|
|
|
|
|
argparser.add_argument('designspacefile', metavar='<designspace>',
|
|
|
|
help='Designspace file')
|
|
|
|
|
|
|
|
argparser.add_argument('instances', metavar='<instance-name>', nargs='*',
|
|
|
|
help='Style instances to generate. Omit to generate all.')
|
|
|
|
|
|
|
|
args = argparser.parse_args(argv)
|
|
|
|
|
|
|
|
instances = set([s.lower() for s in args.instances])
|
|
|
|
all_instances = len(instances) == 0
|
|
|
|
|
|
|
|
# files
|
|
|
|
designspace_file = args.designspacefile
|
2018-09-04 00:19:38 +03:00
|
|
|
instance_dir = pjoin(BASEDIR, 'build', 'ufo')
|
2018-09-03 22:55:49 +03:00
|
|
|
|
|
|
|
# DesignSpaceDocumentReader generates UFOs
|
|
|
|
gen = DesignSpaceDocumentReader(
|
|
|
|
designspace_file,
|
|
|
|
ufoVersion=3,
|
|
|
|
roundGeometry=True,
|
|
|
|
verbose=True
|
|
|
|
)
|
|
|
|
|
|
|
|
designspace = designspaceLib.DesignSpaceDocument()
|
|
|
|
designspace.read(designspace_file)
|
|
|
|
|
|
|
|
# Generate UFOs for instances
|
|
|
|
instance_weight = dict()
|
|
|
|
instance_files = set()
|
|
|
|
for instance in designspace.instances:
|
|
|
|
if all_instances or instance.name in instances:
|
2018-09-04 00:19:38 +03:00
|
|
|
filebase = basename(instance.filename)
|
|
|
|
relname = relpath(
|
|
|
|
pjoin(dirname(designspace_file), instance.filename),
|
2018-09-03 22:55:49 +03:00
|
|
|
os.getcwd()
|
|
|
|
)
|
|
|
|
print('generating %s' % relname)
|
|
|
|
gen.readInstance(("name", instance.name))
|
|
|
|
instance_files.add(instance.filename)
|
|
|
|
instance_weight[filebase] = int(instance.location['Weight'])
|
|
|
|
if not all_instances:
|
|
|
|
instances.remove(instance.name)
|
|
|
|
|
|
|
|
if len(instances) > 0:
|
|
|
|
print('unknown style(s): %s' % ', '.join(list(instances)), file=sys.stderr)
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
ufos = apply_instance_data(designspace_file, instance_files)
|
|
|
|
|
|
|
|
# patch ufos (list of defcon.Font instances)
|
|
|
|
italicAngleKey = 'com.schriftgestaltung.customParameter.' +\
|
|
|
|
'InstanceDescriptorAsGSInstance.italicAngle'
|
|
|
|
for font in ufos:
|
|
|
|
# move italicAngle from lib to info
|
|
|
|
italicAngle = font.lib.get(italicAngleKey)
|
|
|
|
if italicAngle != None:
|
|
|
|
italicAngle = float(italicAngle)
|
|
|
|
del font.lib[italicAngleKey]
|
|
|
|
font.info.italicAngle = italicAngle
|
|
|
|
|
|
|
|
# update font info
|
2018-09-04 00:19:38 +03:00
|
|
|
weight = instance_weight[basename(font.path)]
|
2018-09-03 22:55:49 +03:00
|
|
|
setFontInfo(font, weight)
|
|
|
|
|
|
|
|
font.save()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def checkfont(self, fontfile):
|
|
|
|
try:
|
|
|
|
res = subprocess.check_output(
|
|
|
|
['ots-idempotent', fontfile],
|
|
|
|
shell=False
|
|
|
|
).strip()
|
|
|
|
# Note: ots-idempotent does not exit with an error in many cases where
|
|
|
|
# it fails to sanitize the font.
|
|
|
|
if res.find('Failed') != -1:
|
|
|
|
logging.error('[checkfont] ots-idempotent failed for %r: %s' % (
|
|
|
|
fontfile, res))
|
|
|
|
return False
|
|
|
|
except:
|
|
|
|
logging.error('[checkfont] ots-idempotent failed for %r' % fontfile)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def cmd_checkfont(self, argv):
|
|
|
|
argparser = argparse.ArgumentParser(
|
|
|
|
usage='%(prog)s checkfont <file> ...',
|
|
|
|
description='Verify integrity of font files')
|
|
|
|
|
|
|
|
argparser.add_argument('files', metavar='<file>', nargs='+',
|
|
|
|
help='Font files')
|
|
|
|
|
|
|
|
args = argparser.parse_args(argv)
|
|
|
|
|
|
|
|
for fontfile in args.files:
|
|
|
|
if not self.checkfont(fontfile):
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
# could use from multiprocessing import Pool
|
|
|
|
# p = Pool(8)
|
|
|
|
# p.map(self.checkfont, args.files)
|
|
|
|
# p.terminate()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
Main().main(sys.argv)
|