summaryrefslogtreecommitdiff
path: root/misc/fontbuildlib/builder.py
blob: 2989734dabafa3302cb02c01d0165e00f26ab6a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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
from .stat import rebuildStatTable

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)

    # decompose some glyphs
    glyphNamesToDecompose = set()
    for g in ufo:
      directives = findGlyphDirectives(g.note)
      if self._shouldDecomposeGlyph(ufo, g, directives):
        glyphNamesToDecompose.add(g.name)
    self._decompose([ufo], glyphNamesToDecompose)

    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))

    # rebuild STAT table to correct VF instance information
    rebuildStatTable(font, designspace)

    log.debug("writing %s", outputFilename)
    font.save(outputFilename)


  def _decompose(self, ufos, glyphNamesToDecompose):
    # Note: Used for building both static and variable fonts
    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)

  def _shouldDecomposeGlyph(self, ufo, g, directives):
    # Note: Used for building both static and variable fonts
    if 'decompose' in directives:
      return True
    if g.components:
      if g.name in ufo.componentReferences:
        # 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

  def _loadDesignspace(self, designspace):
    # Note: Only used for building variable fonts
    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:
      # Note: ufo is of type defcon.objects.font.Font
      # update font version
      updateFontVersion(ufo, dummy=False, isVF=True)
      for g in ufo:
        directives = findGlyphDirectives(g.note)
        if self._shouldDecomposeGlyph(ufo, g, directives):
          glyphNamesToDecompose.add(g.name)
        if 'removeoverlap' in directives:
          if g.components and len(g.components) > 0:
            glyphNamesToDecompose.add(g.name)
          glyphsToRemoveOverlaps.add(g)

    self._decompose(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