forked from rsms/inter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fontbuild: remove use of fontmake, simplifying things.
- Loading branch information
Showing
9 changed files
with
613 additions
and
390 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .builder import FontBuilder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) |
Oops, something went wrong.