summaryrefslogtreecommitdiff
path: root/misc/fontbuildlib/builder.py
blob: ae00ed5aaa4837067bf520962b57d4623ffb22ea (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
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 'decompose' in directives or (g.components and not composedGlyphIsTrivial(g)):
        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):
    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 _loadDesignspace(self, 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:
        directives = findGlyphDirectives(g.note)
        if 'decompose' in directives or (g.components and not composedGlyphIsTrivial(g)):
          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