2019-10-23 03:00:58 +03:00
|
|
|
|
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
|
2020-08-19 03:57:25 +03:00
|
|
|
|
from .stat import rebuildStatTable
|
2019-10-23 03:00:58 +03:00
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2020-08-19 23:54:30 +03:00
|
|
|
|
# decompose some glyphs
|
|
|
|
|
glyphNamesToDecompose = set()
|
2021-03-24 23:26:53 +03:00
|
|
|
|
componentReferences = set(ufo.componentReferences)
|
2020-08-19 23:54:30 +03:00
|
|
|
|
for g in ufo:
|
|
|
|
|
directives = findGlyphDirectives(g.note)
|
2021-03-24 23:26:53 +03:00
|
|
|
|
if self._shouldDecomposeGlyph(g, directives, componentReferences):
|
2020-08-19 23:54:30 +03:00
|
|
|
|
glyphNamesToDecompose.add(g.name)
|
|
|
|
|
self._decompose([ufo], glyphNamesToDecompose)
|
|
|
|
|
|
2019-10-23 03:00:58 +03:00
|
|
|
|
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))
|
|
|
|
|
|
2020-08-19 03:57:25 +03:00
|
|
|
|
# rebuild STAT table to correct VF instance information
|
|
|
|
|
rebuildStatTable(font, designspace)
|
|
|
|
|
|
2019-10-23 03:00:58 +03:00
|
|
|
|
log.debug("writing %s", outputFilename)
|
|
|
|
|
font.save(outputFilename)
|
|
|
|
|
|
|
|
|
|
|
2020-08-19 23:54:30 +03:00
|
|
|
|
def _decompose(self, ufos, glyphNamesToDecompose):
|
2021-03-24 22:25:45 +03:00
|
|
|
|
# Note: Used for building both static and variable fonts
|
2020-08-19 23:54:30 +03:00
|
|
|
|
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(ufos, glyphNamesToDecompose)
|
|
|
|
|
|
2021-03-24 23:26:53 +03:00
|
|
|
|
def _shouldDecomposeGlyph(self, g, directives, componentReferences):
|
2021-03-24 22:25:45 +03:00
|
|
|
|
# Note: Used for building both static and variable fonts
|
|
|
|
|
if 'decompose' in directives:
|
|
|
|
|
return True
|
|
|
|
|
if g.components:
|
2021-03-24 23:26:53 +03:00
|
|
|
|
if g.name in componentReferences:
|
2021-03-24 22:25:45 +03:00
|
|
|
|
# This means that the glyph...
|
|
|
|
|
# a) has component instances and
|
|
|
|
|
# b) is itself a component used by other glyphs as instances.
|
|
|
|
|
# Decomposing these glyphs satisfies the fontbakery check
|
|
|
|
|
# com.google.fonts/check/glyf_nested_components
|
|
|
|
|
# "Check glyphs do not have components which are themselves components."
|
|
|
|
|
# https://github.com/googlefonts/fontbakery/issues/2961
|
|
|
|
|
# https://github.com/arrowtype/recursive/issues/412
|
|
|
|
|
#
|
|
|
|
|
# ufo.componentReferences:
|
|
|
|
|
# A dict of describing the component relationships in the font’s main layer.
|
|
|
|
|
# The dictionary is of form {"base_glyph_name": ["ref_glyph_name"]}.
|
|
|
|
|
log.debug("decompose %r (glyf_nested_components)" % g.name)
|
|
|
|
|
return True
|
|
|
|
|
if not composedGlyphIsTrivial(g):
|
|
|
|
|
return True
|
|
|
|
|
return False
|
2019-10-23 03:00:58 +03:00
|
|
|
|
|
2020-08-19 23:54:30 +03:00
|
|
|
|
def _loadDesignspace(self, designspace):
|
2021-03-24 22:25:45 +03:00
|
|
|
|
# Note: Only used for building variable fonts
|
2019-10-23 03:00:58 +03:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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:
|
2021-03-24 22:25:45 +03:00
|
|
|
|
# Note: ufo is of type defcon.objects.font.Font
|
|
|
|
|
# update font version
|
|
|
|
|
updateFontVersion(ufo, dummy=False, isVF=True)
|
2021-03-24 23:26:53 +03:00
|
|
|
|
componentReferences = set(ufo.componentReferences)
|
2019-10-23 03:00:58 +03:00
|
|
|
|
for g in ufo:
|
2020-08-18 03:12:31 +03:00
|
|
|
|
directives = findGlyphDirectives(g.note)
|
2021-03-24 23:26:53 +03:00
|
|
|
|
if self._shouldDecomposeGlyph(g, directives, componentReferences):
|
2019-10-23 03:00:58 +03:00
|
|
|
|
glyphNamesToDecompose.add(g.name)
|
2020-08-18 03:12:31 +03:00
|
|
|
|
if 'removeoverlap' in directives:
|
2019-10-23 03:00:58 +03:00
|
|
|
|
if g.components and len(g.components) > 0:
|
|
|
|
|
glyphNamesToDecompose.add(g.name)
|
|
|
|
|
glyphsToRemoveOverlaps.add(g)
|
|
|
|
|
|
2020-08-19 23:54:30 +03:00
|
|
|
|
self._decompose(masters, glyphNamesToDecompose)
|
2019-10-23 03:00:58 +03:00
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|