1#!/usr/bin/env python
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) are not currently supported. They are copied to the destination without renaming.
21XML 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
65# A list of extensions to process.
66EXTENSIONS = ['.ttf', '.otf', '.xml']
67
68def main(argv):
69  if len(argv) < 2:
70    print 'Incorrect usage: ' + str(argv)
71    sys.exit('Usage: build_font_single.py /path/to/input/font.ttf /path/to/out/font.ttf')
72  dest_path = argv[-1]
73  input_path = argv[0]
74  extension = os.path.splitext(input_path)[1].lower()
75  if extension in EXTENSIONS:
76    if not COPY_ONLY and extension == '.ttf':
77      convert_font(input_path, dest_path)
78      return
79    shutil.copy(input_path, dest_path)
80
81
82def convert_font(input_path, dest_path):
83  filename = os.path.basename(input_path)
84  print 'Converting font: ' + filename
85  # the path to the output file. The file name is the fontfilename.ttx
86  ttx_path = dest_path[:-1] + 'x'
87  try:
88    # run ttx to generate an xml file in the output folder which represents all
89    # its info
90    ttx_args = ['-q', '-o', ttx_path, input_path]
91    ttx.main(ttx_args)
92    # now parse the xml file to change its PS name.
93    tree = etree.parse(ttx_path)
94    root = tree.getroot()
95    for name in root.iter('name'):
96      update_tag(name, get_font_info(name))
97    tree.write(ttx_path, xml_declaration=True, encoding='utf-8')
98    # generate the udpated font now.
99    ttx_args = ['-q', '-o', dest_path, ttx_path]
100    ttx.main(ttx_args)
101  except InvalidFontException:
102    # In case of invalid fonts, we exit.
103    print filename + ' is not a valid font'
104    raise
105  except Exception as e:
106    print 'Error converting font: ' + filename
107    print e
108    # Some fonts are too big to be handled by the ttx library.
109    # Just copy paste them.
110    shutil.copy(input_path, dest_path)
111  try:
112    # delete the temp ttx file is it exists.
113    os.remove(ttx_path)
114  except OSError:
115    pass
116
117
118def get_font_info(tag):
119  """ Returns a list of FontInfo representing the various sets of namerecords
120      found in the name table of the font. """
121  fonts = []
122  font = None
123  last_name_id = sys.maxint
124  for namerecord in tag.iter('namerecord'):
125    if 'nameID' in namerecord.attrib:
126      name_id = int(namerecord.attrib['nameID'])
127      # A new font should be created for each platform, encoding and language
128      # id. But, since the nameIDs are sorted, we use the easy approach of
129      # creating a new one when the nameIDs reset.
130      if name_id <= last_name_id and font is not None:
131        fonts.append(font)
132        font = None
133      last_name_id = name_id
134      if font is None:
135        font = FontInfo()
136      if name_id == NAMEID_FAMILY:
137        font.family = namerecord.text.strip()
138      if name_id == NAMEID_STYLE:
139        font.style = namerecord.text.strip()
140      if name_id == NAMEID_FULLNAME:
141        font.ends_in_regular = ends_in_regular(namerecord.text)
142        font.fullname = namerecord.text.strip()
143      if name_id == NAMEID_VERSION:
144        font.version = get_version(namerecord.text)
145  if font is not None:
146    fonts.append(font)
147  return fonts
148
149
150def update_tag(tag, fonts):
151  last_name_id = sys.maxint
152  fonts_iterator = fonts.__iter__()
153  font = None
154  for namerecord in tag.iter('namerecord'):
155    if 'nameID' in namerecord.attrib:
156      name_id = int(namerecord.attrib['nameID'])
157      if name_id <= last_name_id:
158        font = fonts_iterator.next()
159        font = update_font_name(font)
160      last_name_id = name_id
161      if name_id == NAMEID_FAMILY:
162        namerecord.text = font.family
163      if name_id == NAMEID_FULLNAME:
164        namerecord.text = font.fullname
165
166
167def update_font_name(font):
168  """ Compute the new font family name and font fullname. If the font has a
169      valid version, it's sanitized and appended to the font family name. The
170      font fullname is then created by joining the new family name and the
171      style. If the style is 'Regular', it is appended only if the original font
172      had it. """
173  if font.family is None or font.style is None:
174    raise InvalidFontException('Font doesn\'t have proper family name or style')
175  if font.version is not None:
176    new_family = font.family + font.version
177  else:
178    new_family = font.family
179  if font.style is 'Regular' and not font.ends_in_regular:
180    font.fullname = new_family
181  else:
182    font.fullname = new_family + ' ' + font.style
183  font.family = new_family
184  return font
185
186
187def ends_in_regular(string):
188  """ According to the specification, the font fullname should not end in
189      'Regular' for plain fonts. However, some fonts don't obey this rule. We
190      keep the style info, to minimize the diff. """
191  string = string.strip().split()[-1]
192  return string is 'Regular'
193
194
195def get_version(string):
196  string = string.strip()
197  # The spec says that the version string should start with "Version ". But not
198  # all fonts do. So, we return the complete string if it doesn't start with
199  # the prefix, else we return the rest of the string after sanitizing it.
200  prefix = 'Version '
201  if string.startswith(prefix):
202    string = string[len(prefix):]
203  return sanitize(string)
204
205
206def sanitize(string):
207  """ Remove non-standard chars. """
208  return re.sub(r'[^\w-]+', '', string)
209
210if __name__ == '__main__':
211  main(sys.argv[1:])
212