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 the input font.
19
20OpenType fonts (*.otf) and TrueType Collections (*.ttc) are not currently supported. They are copied to the destination
21without renaming. XML files are also copied in case they are passed there by mistake.
22
23Usage: build_font_single.py /path/to/input_font.ttf /path/to/output_font.ttf
24
25"""
26
27import glob
28import os
29import re
30import shutil
31import sys
32import xml.etree.ElementTree as etree
33
34# Prevent .pyc files from being created.
35sys.dont_write_bytecode = True
36
37# fontTools is available at platform/external/fonttools
38from fontTools import ttx
39
40
41class FontInfo(object):
42  family = None
43  style = None
44  version = None
45  ends_in_regular = False
46  fullname = None
47
48
49class InvalidFontException(Exception):
50  pass
51
52
53# A constant to copy the font without modifying. This is useful when running
54# locally and speed up the time to build the SDK.
55COPY_ONLY = False
56
57# These constants represent the value of nameID parameter in the namerecord for
58# different information.
59# see http://scripts.sil.org/cms/scripts/page.php?item_id=IWS-Chapter08#3054f18b
60NAMEID_FAMILY = 1
61NAMEID_STYLE = 2
62NAMEID_FULLNAME = 4
63NAMEID_VERSION = 5
64
65NAMEID_LIST = [NAMEID_FAMILY, NAMEID_STYLE, NAMEID_FULLNAME, NAMEID_VERSION]
66NAMEID_LIST_MIN = min(NAMEID_LIST)
67NAMEID_LIST_MAX = max(NAMEID_LIST)
68
69# A list of extensions to process.
70EXTENSIONS = ['.ttf', '.ttc', '.otf', '.xml']
71
72def main(argv):
73  if len(argv) < 2:
74    print('Incorrect usage: ' + str(argv))
75    sys.exit('Usage: build_font_single.py /path/to/input/font.ttf /path/to/out/font.ttf')
76  dest_path = argv[-1]
77  input_path = argv[0]
78  extension = os.path.splitext(input_path)[1].lower()
79  if extension in EXTENSIONS:
80    if not COPY_ONLY and extension == '.ttf':
81      convert_font(input_path, dest_path)
82      return
83    shutil.copy(input_path, dest_path)
84
85
86def convert_font(input_path, dest_path):
87  filename = os.path.basename(input_path)
88  print('Converting font: ' + filename)
89  # the path to the output file. The file name is the fontfilename.ttx
90  ttx_path = dest_path[:-1] + 'x'
91  try:
92    # run ttx to generate an xml file in the output folder which represents all
93    # its info
94    ttx_args = ['--no-recalc-timestamp', '-q', '-o', ttx_path, input_path]
95    ttx.main(ttx_args)
96    # now parse the xml file to change its PS name.
97    tree = etree.parse(ttx_path)
98    root = tree.getroot()
99    for name in root.iter('name'):
100      update_tag(name, get_font_info(name))
101    tree.write(ttx_path, xml_declaration=True, encoding='utf-8')
102    # generate the udpated font now.
103    ttx_args = ['-q', '-o', dest_path, ttx_path]
104    ttx.main(ttx_args)
105  except InvalidFontException:
106    # assume, like for .ttc and .otf that font might be valid, but warn.
107    print('Family and/or Style nameIDs not found in '+ filename)
108    shutil.copy(input_path, dest_path)
109  except Exception as e:
110    print('Error converting font: ' + filename)
111    print(e)
112    # Some fonts are too big to be handled by the ttx library.
113    # Just copy paste them.
114    shutil.copy(input_path, dest_path)
115  try:
116    # delete the temp ttx file is it exists.
117    os.remove(ttx_path)
118  except OSError:
119    pass
120
121
122def get_font_info(tag):
123  """ Returns a list of FontInfo representing the various sets of namerecords
124      found in the name table of the font. """
125  fonts = []
126  font = None
127  last_name_id = sys.maxsize
128  for namerecord in tag.iter('namerecord'):
129    if 'nameID' in namerecord.attrib:
130      name_id = int(namerecord.attrib['nameID'])
131      # skip irrelevant records
132      if name_id < NAMEID_LIST_MIN or name_id > NAMEID_LIST_MAX:
133        continue
134      # A new font should be created for each platform, encoding and language
135      # id. But, since the nameIDs are sorted, we use the easy approach of
136      # creating a new one when the nameIDs reset.
137      if name_id <= last_name_id and font is not None:
138        fonts.append(font)
139        font = None
140      last_name_id = name_id
141      if font is None:
142        font = FontInfo()
143      if name_id == NAMEID_FAMILY:
144        font.family = namerecord.text.strip()
145      if name_id == NAMEID_STYLE:
146        font.style = namerecord.text.strip()
147      if name_id == NAMEID_FULLNAME:
148        font.ends_in_regular = ends_in_regular(namerecord.text)
149        font.fullname = namerecord.text.strip()
150      if name_id == NAMEID_VERSION:
151        font.version = get_version(namerecord.text)
152  if font is not None:
153    fonts.append(font)
154  return fonts
155
156
157def update_tag(tag, fonts):
158  last_name_id = sys.maxsize
159  fonts_iterator = fonts.__iter__()
160  font = None
161  for namerecord in tag.iter('namerecord'):
162    if 'nameID' in namerecord.attrib:
163      name_id = int(namerecord.attrib['nameID'])
164      # skip irrelevant records
165      if name_id < NAMEID_LIST_MIN or name_id > NAMEID_LIST_MAX:
166        continue
167      if name_id <= last_name_id:
168        font = next(fonts_iterator)
169        font = update_font_name(font)
170      last_name_id = name_id
171      if name_id == NAMEID_FAMILY:
172        namerecord.text = font.family
173      if name_id == NAMEID_FULLNAME:
174        namerecord.text = font.fullname
175
176
177def update_font_name(font):
178  """ Compute the new font family name and font fullname. If the font has a
179      valid version, it's sanitized and appended to the font family name. The
180      font fullname is then created by joining the new family name and the
181      style. If the style is 'Regular', it is appended only if the original font
182      had it. """
183  if font.family is None or font.style is None:
184    raise InvalidFontException('Font doesn\'t have proper family name or style')
185  if font.version is not None:
186    new_family = font.family + font.version
187  else:
188    new_family = font.family
189  if font.style == 'Regular' and not font.ends_in_regular:
190    font.fullname = new_family
191  else:
192    font.fullname = new_family + ' ' + font.style
193  font.family = new_family
194  return font
195
196
197def ends_in_regular(string):
198  """ According to the specification, the font fullname should not end in
199      'Regular' for plain fonts. However, some fonts don't obey this rule. We
200      keep the style info, to minimize the diff. """
201  string = string.strip().split()[-1]
202  return string == 'Regular'
203
204
205def get_version(string):
206  string = string.strip()
207  # The spec says that the version string should start with "Version ". But not
208  # all fonts do. So, we return the complete string if it doesn't start with
209  # the prefix, else we return the rest of the string after sanitizing it.
210  prefix = 'Version '
211  if string.startswith(prefix):
212    string = string[len(prefix):]
213  return sanitize(string)
214
215
216def sanitize(string):
217  """ Remove non-standard chars. """
218  return re.sub(r'[^\w-]+', '', string)
219
220if __name__ == '__main__':
221  main(sys.argv[1:])
222