1#!/usr/bin/env python3
2
3# Copyright (C) 2014 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the 'License');
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an 'AS IS' BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Rename the PS name of all fonts in the input directories and copy them to the
19output directory.
20
21Usage: build_font.py /path/to/input_fonts1/ /path/to/input_fonts2/ /path/to/output_fonts/
22
23"""
24
25import glob
26from multiprocessing import Pool
27import os
28import re
29import shutil
30import sys
31import xml.etree.ElementTree as etree
32
33# Prevent .pyc files from being created.
34sys.dont_write_bytecode = True
35
36# fontTools is available at platform/external/fonttools
37from fontTools import ttx
38
39# global variable
40dest_dir = '/tmp'
41
42
43class FontInfo(object):
44  family = None
45  style = None
46  version = None
47  ends_in_regular = False
48  fullname = None
49
50
51class InvalidFontException(Exception):
52  pass
53
54
55# These constants represent the value of nameID parameter in the namerecord for
56# different information.
57# see http://scripts.sil.org/cms/scripts/page.php?item_id=IWS-Chapter08#3054f18b
58NAMEID_FAMILY = 1
59NAMEID_STYLE = 2
60NAMEID_FULLNAME = 4
61NAMEID_VERSION = 5
62
63
64def main(argv):
65  if len(argv) < 2:
66    sys.exit('Usage: build_font.py /path/to/input_fonts/ /path/to/out/dir/')
67  for directory in argv:
68    if not os.path.isdir(directory):
69      sys.exit(directory + ' is not a valid directory')
70  global dest_dir
71  dest_dir = argv[-1]
72  src_dirs = argv[:-1]
73  cwd = os.getcwd()
74  os.chdir(dest_dir)
75  files = glob.glob('*')
76  for filename in files:
77    os.remove(filename)
78  os.chdir(cwd)
79  input_fonts = list()
80  for src_dir in src_dirs:
81    for dirname, dirnames, filenames in os.walk(src_dir):
82      for filename in filenames:
83        input_path = os.path.join(dirname, filename)
84        extension = os.path.splitext(filename)[1].lower()
85        if extension == '.ttf':
86          input_fonts.append(input_path)
87        elif extension == '.xml':
88          shutil.copy(input_path, dest_dir)
89      if '.git' in dirnames:
90        # don't go into any .git directories.
91        dirnames.remove('.git')
92  # Create as many threads as the number of CPUs
93  pool = Pool(processes=None)
94  pool.map(convert_font, input_fonts)
95
96
97def convert_font(input_path):
98  filename = os.path.basename(input_path)
99  print('Converting font: ' + filename)
100  # the path to the output file. The file name is the fontfilename.ttx
101  ttx_path = os.path.join(dest_dir, filename)
102  ttx_path = ttx_path[:-1] + 'x'
103  try:
104    # run ttx to generate an xml file in the output folder which represents all
105    # its info
106    ttx_args = ['--no-recalc-timestamp', '-q', '-d', dest_dir, input_path]
107    ttx.main(ttx_args)
108    # now parse the xml file to change its PS name.
109    tree = etree.parse(ttx_path)
110    root = tree.getroot()
111    for name in root.iter('name'):
112      update_tag(name, get_font_info(name))
113    tree.write(ttx_path, xml_declaration=True, encoding='utf-8')
114    # generate the udpated font now.
115    ttx_args = ['-q', '-d', dest_dir, ttx_path]
116    ttx.main(ttx_args)
117  except InvalidFontException:
118    # In case of invalid fonts, we exit.
119    print(filename + ' is not a valid font')
120    raise
121  except Exception as e:
122    print('Error converting font: ' + filename)
123    print(e)
124    # Some fonts are too big to be handled by the ttx library.
125    # Just copy paste them.
126    shutil.copy(input_path, dest_dir)
127  try:
128    # delete the temp ttx file is it exists.
129    os.remove(ttx_path)
130  except OSError:
131    pass
132
133
134def get_font_info(tag):
135  """ Returns a list of FontInfo representing the various sets of namerecords
136      found in the name table of the font. """
137  fonts = []
138  font = None
139  last_name_id = sys.maxsize
140  for namerecord in tag.iter('namerecord'):
141    if 'nameID' in namerecord.attrib:
142      name_id = int(namerecord.attrib['nameID'])
143      # A new font should be created for each platform, encoding and language
144      # id. But, since the nameIDs are sorted, we use the easy approach of
145      # creating a new one when the nameIDs reset.
146      if name_id <= last_name_id and font is not None:
147        fonts.append(font)
148        font = None
149      last_name_id = name_id
150      if font is None:
151        font = FontInfo()
152      if name_id == NAMEID_FAMILY:
153        font.family = namerecord.text.strip()
154      if name_id == NAMEID_STYLE:
155        font.style = namerecord.text.strip()
156      if name_id == NAMEID_FULLNAME:
157        font.ends_in_regular = ends_in_regular(namerecord.text)
158        font.fullname = namerecord.text.strip()
159      if name_id == NAMEID_VERSION:
160        font.version = get_version(namerecord.text)
161  if font is not None:
162    fonts.append(font)
163  return fonts
164
165
166def update_tag(tag, fonts):
167  last_name_id = sys.maxsize
168  fonts_iterator = fonts.__iter__()
169  font = None
170  for namerecord in tag.iter('namerecord'):
171    if 'nameID' in namerecord.attrib:
172      name_id = int(namerecord.attrib['nameID'])
173      if name_id <= last_name_id:
174        font = next(fonts_iterator)
175        font = update_font_name(font)
176      last_name_id = name_id
177      if name_id == NAMEID_FAMILY:
178        namerecord.text = font.family
179      if name_id == NAMEID_FULLNAME:
180        namerecord.text = font.fullname
181
182
183def update_font_name(font):
184  """ Compute the new font family name and font fullname. If the font has a
185      valid version, it's sanitized and appended to the font family name. The
186      font fullname is then created by joining the new family name and the
187      style. If the style is 'Regular', it is appended only if the original font
188      had it. """
189  if font.family is None or font.style is None:
190    raise InvalidFontException('Font doesn\'t have proper family name or style')
191  if font.version is not None:
192    new_family = font.family + font.version
193  else:
194    new_family = font.family
195  if font.style == 'Regular' and not font.ends_in_regular:
196    font.fullname = new_family
197  else:
198    font.fullname = new_family + ' ' + font.style
199  font.family = new_family
200  return font
201
202
203def ends_in_regular(string):
204  """ According to the specification, the font fullname should not end in
205      'Regular' for plain fonts. However, some fonts don't obey this rule. We
206      keep the style info, to minimize the diff. """
207  string = string.strip().split()[-1]
208  return string == 'Regular'
209
210
211def get_version(string):
212  string = string.strip()
213  # The spec says that the version string should start with "Version ". But not
214  # all fonts do. So, we return the complete string if it doesn't start with
215  # the prefix, else we return the rest of the string after sanitizing it.
216  prefix = 'Version '
217  if string.startswith(prefix):
218    string = string[len(prefix):]
219  return sanitize(string)
220
221
222def sanitize(string):
223  """ Remove non-standard chars. """
224  return re.sub(r'[^\w-]+', '', string)
225
226if __name__ == '__main__':
227  main(sys.argv[1:])
228