summaryrefslogtreecommitdiff
path: root/misc/tools/postprocess-designspace.py
blob: fbe76a3645f714c4c1b1405f206d4d121d226b79 (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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import sys, os, os.path, re, argparse
import defcon
from multiprocessing import Pool
from fontTools.designspaceLib import DesignSpaceDocument
from ufo2ft.filters import loadFilters
from datetime import datetime

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tools')))
from common import getGitHash, getVersion
from postprocess_instance_ufo import ufo_set_wws


OPT_EDITABLE = False  # --editable


def update_version(ufo):
  version = getVersion()
  buildtag, buildtagErrs = getGitHash()
  now = datetime.utcnow()
  if buildtag == "" or len(buildtagErrs) > 0:
    buildtag = "src"
    print("warning: getGitHash() failed: %r" % buildtagErrs, file=sys.stderr)
  versionMajor, versionMinor = [int(num) for num in version.split(".")]
  ufo.info.versionMajor = versionMajor
  ufo.info.versionMinor = versionMinor
  ufo.info.year = now.year
  ufo.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (versionMajor, versionMinor, buildtag)
  psFamily = re.sub(r'\s', '', ufo.info.familyName)
  psStyle = re.sub(r'\s', '', ufo.info.styleName)
  #
  # id format:
  #   version ";" "git-" git-tag ";" foundry-tag ";" ps_family "-" ps_style
  # E.g.
  #   "4.001;git-4de559246;RSMS;Inter-DisplayThinItalic"
  # Note: this should match what generated by fontmake.
  # fix-static-display-names.py depends on this format being consistent for all fonts.
  #
  if buildtag != "src":
    buildtag = "git-" + buildtag
  ufo.info.openTypeNameUniqueID = "%d.%03d;%s;%s;%s-%s" % (
    versionMajor, versionMinor,
    buildtag,
    ufo.info.openTypeOS2VendorID,
    psFamily, psStyle)
  ufo.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")


def fix_opsz_range(designspace):
  opsz_min = 1000000
  opsz_max = 0
  opsz_name = ''
  opsz_axis = None

  for opsz_axis in designspace.axes:
    if opsz_axis.tag == "opsz":
      opsz_name = opsz_axis.name
      break

  for instance in designspace.instances:
    opsz_value = instance.location[opsz_name]
    if opsz_value < opsz_min:
      opsz_min = opsz_value
    if opsz_value > opsz_max:
      opsz_max = opsz_value

  opsz_axis.minimum = opsz_min
  opsz_axis.maximum = opsz_max

  return designspace


def fix_wght_range(designspace):
  for a in designspace.axes:
    if a.tag == "wght":
      a.minimum = 100
      a.maximum = 900
      break
  return designspace


def should_decompose_glyph(g):
  if not g.components or len(g.components) == 0:
    return False

  # Does the component have non-trivial transformation? (i.e. scaled or skewed)
  # Example of no transformation: (identity matrix)
  #   (1, 0, 0, 1, 0, 0)    no scale or offset
  # Example of simple offset transformation matrix:
  #   (1, 0, 0, 1, 20, 30)  20 x offset, 30 y offset
  # Example of scaled transformation matrix:
  #   (-1.0, 0, 0.3311, 1, 1464.0, 0)  flipped x axis, sheered and offset
  # Matrix order:
  #   (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
  for cn in g.components:
    # if g.name == 'dotmacron.lc':
    #   print(f"{g.name} cn {cn.baseGlyph}", cn.transformation)
    # Check if transformation is not identity (ignoring x & y offset)
    m = cn.transformation
    if m[0] + m[1] + m[2] + m[3] != 2.0:
      return True

  return False


def copy_component_anchors(font, g):
  # do nothing if there are no components or if g has anchors already
  if not g.components or len(g.anchors) > 0:
    return

  anchor_names = set()
  for cn in g.components:
    if cn.transformation[1] != 0.0 or cn.transformation[2] != 0.0:
      print(f"TODO: support transformations with skew ({g.name})")
      return
    cn_g = font[cn.baseGlyph]
    # copy_component_anchors(font, cn_g)  # depth first
    for a in cn_g.anchors:
      # Check if there are multiple components with achors with the same name.
      # Don't copy any anchors if there are duplicate "_..." anchors
      if a.name in anchor_names and len(a.name) > 1 and a.name[0] == '_':
        return
      anchor_names.add(a.name)

  if len(anchor_names) == 0:
    return

  anchor_names.clear()
  for cn in g.components:
    for a in font[cn.baseGlyph].anchors:
      if a.name in anchor_names:
        continue
      anchor_names.add(a.name)
      a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
      m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
      a2.x += m[4] * m[0]
      a2.y += m[5] * m[3]
      g.appendAnchor(a2)


def copy_anchors_from_components(font, g):
  # We use two passes here, to deduplicate anchors which appear in several components.
  # Two assumptions are made:
  # 1. Insertion order of Python dict() is retained (true for Python >=3.7)
  # 2. Base components are listed first, mark/accent comonents later.
  #    e.g. in /Ecircumflex, /E comes before /circumflexcomb, so we use "top"
  #    anchor from /circumflexcomb rather than /E

  # skip certain glyphs
  if len(g.unicodes) == 0 or len(g.anchors) > 0 or not g.components:
    return

  add_anchors = dict()
  names_to_copy = set(('top', 'bottom'))

  for cn in g.components:
    checked_cn_skew = False
    #print(f"    [{g.name}] cn {cn.baseGlyph}")
    cn_g = font[cn.baseGlyph]
    copy_anchors_from_components(font, cn_g)  # depth first
    for a in cn_g.anchors:
      if a.name not in names_to_copy:
        continue
      #print(f"    [{g.name}] use anchor {a.name}")
      m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
      if not checked_cn_skew:
        checked_cn_skew = True
        if m[1] != 0.0 or m[2] != 0.0:
          #print(f"TODO: skewed components ({cn_g.name} used by {g.name})")
          return
      a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
      a2.x += m[4] * m[0]
      a2.y += m[5] * m[3]
      add_anchors[a.name] = a2

  for a in add_anchors.values():
    #print(f"    [{g.name}] append anchor {a.name}")
    g.appendAnchor(a)


def find_glyphs_to_decompose(designspace_source):
  glyph_names = set()
  # print("find_glyphs_to_decompose inspecting %r" % designspace_source.name)
  font = defcon.Font(designspace_source.path)
  for g in font:
    # copy_anchors_from_components(font, g)
    if should_decompose_glyph(g):
      glyph_names.add(g.name)
  font.save(designspace_source.path)
  return list(glyph_names)


def set_ufo_filter(ufo, **filter_dict):
  filters = ufo.lib.setdefault("com.github.googlei18n.ufo2ft.filters", [])
  for i in range(len(filters)):
    if filters[i].get("name") == filter_dict["name"]:
      filters[i] = filter_dict
      return
  filters.append(filter_dict)


def del_ufo_filter(ufo, name):
  filters = ufo.lib.get("com.github.googlei18n.ufo2ft.filters")
  if not filters:
    return
  for i in range(len(filters)):
    if filters[i].get("name") == name:
      filters.pop(i)
      return


def update_source_ufo(ufo_file, glyphs_to_decompose):
  print(f"update {os.path.basename(ufo_file)}")

  ufo = defcon.Font(ufo_file)
  update_version(ufo)

  set_ufo_filter(ufo, name="decomposeComponents", include=glyphs_to_decompose)

  # decompose now, up front, instead of later when compiling fonts
  if not OPT_EDITABLE:
    preFilters, postFilters = loadFilters(ufo)
    for filter in preFilters:
      filter(ufo)
    for filter in postFilters:
      filter(ufo)
    # del_ufo_filter(ufo, "decomposeComponents")
    del ufo.lib["com.github.googlei18n.ufo2ft.filters"]

  ufo_set_wws(ufo) # Fix missing WWS entries for Display fonts
  ufo.save(ufo_file)


def update_sources(designspace):
  with Pool() as p:
    sources = [source for source in designspace.sources]
    # sources = [s for s in sources if s.name == "Inter Thin"] # DEBUG
    glyphs_to_decompose = set()
    for glyph_names in p.map(find_glyphs_to_decompose, sources):
      glyphs_to_decompose.update(glyph_names)
    glyphs_to_decompose = list(glyphs_to_decompose)
    # print("glyphs marked to be decomposed: %s" % ', '.join(glyphs_to_decompose))
    source_files = list(set([s.path for s in sources]))
    p.starmap(update_source_ufo, [(path, glyphs_to_decompose) for path in source_files])
  return designspace


def main(argv):
  ap = argparse.ArgumentParser(description=
    'Fixup designspace and source UFOs after they are generated by fontmake from Glyphs source')
  ap.add_argument('--editable', action='store_true',
    help="Generate UFOs suitable for further editing (don't apply filters)")
  ap.add_argument("designspace", help="Path to designspace file")

  args = ap.parse_args()
  OPT_EDITABLE = args.editable

  designspace = DesignSpaceDocument.fromfile(args.designspace)
  designspace = fix_opsz_range(designspace)
  designspace = fix_wght_range(designspace)
  designspace = update_sources(designspace)
  designspace.write(args.designspace)


if __name__ == '__main__':
  main(sys.argv)