aboutsummaryrefslogtreecommitdiff
path: root/rename_font/build_font.py
blob: a53ebbc25cdb1846c65249f364b81f88fa594e62 (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
#!/usr/bin/env python

# Copyright (C) 2014 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Rename the PS name of all fonts in the input directories and copy them to the
output directory.

Usage: build_font.py /path/to/input_fonts1/ /path/to/input_fonts2/ /path/to/output_fonts/

"""

import glob
from multiprocessing import Pool
import os
import re
import shutil
import sys
import xml.etree.ElementTree as etree

# Prevent .pyc files from being created.
sys.dont_write_bytecode = True

# fontTools is available at platform/external/fonttools
from fontTools import ttx

# global variable
dest_dir = '/tmp'


class FontInfo(object):
  family = None
  style = None
  version = None
  ends_in_regular = False
  fullname = None


class InvalidFontException(Exception):
  pass


# These constants represent the value of nameID parameter in the namerecord for
# different information.
# see http://scripts.sil.org/cms/scripts/page.php?item_id=IWS-Chapter08#3054f18b
NAMEID_FAMILY = 1
NAMEID_STYLE = 2
NAMEID_FULLNAME = 4
NAMEID_VERSION = 5


def main(argv):
  if len(argv) < 2:
    sys.exit('Usage: build_font.py /path/to/input_fonts/ /path/to/out/dir/')
  for directory in argv:
    if not os.path.isdir(directory):
      sys.exit(directory + ' is not a valid directory')
  global dest_dir
  dest_dir = argv[-1]
  src_dirs = argv[:-1]
  cwd = os.getcwd()
  os.chdir(dest_dir)
  files = glob.glob('*')
  for filename in files:
    os.remove(filename)
  os.chdir(cwd)
  input_fonts = list()
  for src_dir in src_dirs:
    for dirname, dirnames, filenames in os.walk(src_dir):
      for filename in filenames:
        input_path = os.path.join(dirname, filename)
        extension = os.path.splitext(filename)[1].lower()
        if extension == '.ttf':
          input_fonts.append(input_path)
        elif extension == '.xml':
          shutil.copy(input_path, dest_dir)
      if '.git' in dirnames:
        # don't go into any .git directories.
        dirnames.remove('.git')
  # Create as many threads as the number of CPUs
  pool = Pool(processes=None)
  pool.map(convert_font, input_fonts)


def convert_font(input_path):
  filename = os.path.basename(input_path)
  print 'Converting font: ' + filename
  # the path to the output file. The file name is the fontfilename.ttx
  ttx_path = os.path.join(dest_dir, filename)
  ttx_path = ttx_path[:-1] + 'x'
  try:
    # run ttx to generate an xml file in the output folder which represents all
    # its info
    ttx_args = ['--no-recalc-timestamp', '-q', '-d', dest_dir, input_path]
    ttx.main(ttx_args)
    # now parse the xml file to change its PS name.
    tree = etree.parse(ttx_path)
    root = tree.getroot()
    for name in root.iter('name'):
      update_tag(name, get_font_info(name))
    tree.write(ttx_path, xml_declaration=True, encoding='utf-8')
    # generate the udpated font now.
    ttx_args = ['-q', '-d', dest_dir, ttx_path]
    ttx.main(ttx_args)
  except InvalidFontException:
    # In case of invalid fonts, we exit.
    print filename + ' is not a valid font'
    raise
  except Exception as e:
    print 'Error converting font: ' + filename
    print e
    # Some fonts are too big to be handled by the ttx library.
    # Just copy paste them.
    shutil.copy(input_path, dest_dir)
  try:
    # delete the temp ttx file is it exists.
    os.remove(ttx_path)
  except OSError:
    pass


def get_font_info(tag):
  """ Returns a list of FontInfo representing the various sets of namerecords
      found in the name table of the font. """
  fonts = []
  font = None
  last_name_id = sys.maxint
  for namerecord in tag.iter('namerecord'):
    if 'nameID' in namerecord.attrib:
      name_id = int(namerecord.attrib['nameID'])
      # A new font should be created for each platform, encoding and language
      # id. But, since the nameIDs are sorted, we use the easy approach of
      # creating a new one when the nameIDs reset.
      if name_id <= last_name_id and font is not None:
        fonts.append(font)
        font = None
      last_name_id = name_id
      if font is None:
        font = FontInfo()
      if name_id == NAMEID_FAMILY:
        font.family = namerecord.text.strip()
      if name_id == NAMEID_STYLE:
        font.style = namerecord.text.strip()
      if name_id == NAMEID_FULLNAME:
        font.ends_in_regular = ends_in_regular(namerecord.text)
        font.fullname = namerecord.text.strip()
      if name_id == NAMEID_VERSION:
        font.version = get_version(namerecord.text)
  if font is not None:
    fonts.append(font)
  return fonts


def update_tag(tag, fonts):
  last_name_id = sys.maxint
  fonts_iterator = fonts.__iter__()
  font = None
  for namerecord in tag.iter('namerecord'):
    if 'nameID' in namerecord.attrib:
      name_id = int(namerecord.attrib['nameID'])
      if name_id <= last_name_id:
        font = fonts_iterator.next()
        font = update_font_name(font)
      last_name_id = name_id
      if name_id == NAMEID_FAMILY:
        namerecord.text = font.family
      if name_id == NAMEID_FULLNAME:
        namerecord.text = font.fullname


def update_font_name(font):
  """ Compute the new font family name and font fullname. If the font has a
      valid version, it's sanitized and appended to the font family name. The
      font fullname is then created by joining the new family name and the
      style. If the style is 'Regular', it is appended only if the original font
      had it. """
  if font.family is None or font.style is None:
    raise InvalidFontException('Font doesn\'t have proper family name or style')
  if font.version is not None:
    new_family = font.family + font.version
  else:
    new_family = font.family
  if font.style is 'Regular' and not font.ends_in_regular:
    font.fullname = new_family
  else:
    font.fullname = new_family + ' ' + font.style
  font.family = new_family
  return font


def ends_in_regular(string):
  """ According to the specification, the font fullname should not end in
      'Regular' for plain fonts. However, some fonts don't obey this rule. We
      keep the style info, to minimize the diff. """
  string = string.strip().split()[-1]
  return string is 'Regular'


def get_version(string):
  string = string.strip()
  # The spec says that the version string should start with "Version ". But not
  # all fonts do. So, we return the complete string if it doesn't start with
  # the prefix, else we return the rest of the string after sanitizing it.
  prefix = 'Version '
  if string.startswith(prefix):
    string = string[len(prefix):]
  return sanitize(string)


def sanitize(string):
  """ Remove non-standard chars. """
  return re.sub(r'[^\w-]+', '', string)

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