build_font_single.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. #!/usr/bin/env python
  2. # Copyright (C) 2014 The Android Open Source Project
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the 'License');
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an 'AS IS' BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """
  16. Rename the PS name of the input font.
  17. OpenType fonts (*.otf) and TrueType Collections (*.ttc) are not currently supported. They are copied to the destination
  18. without renaming. XML files are also copied in case they are passed there by mistake.
  19. Usage: build_font_single.py /path/to/input_font.ttf /path/to/output_font.ttf
  20. """
  21. import glob
  22. import os
  23. import re
  24. import shutil
  25. import sys
  26. import xml.etree.ElementTree as etree
  27. # Prevent .pyc files from being created.
  28. sys.dont_write_bytecode = True
  29. # fontTools is available at platform/external/fonttools
  30. from fontTools import ttx
  31. class FontInfo(object):
  32. family = None
  33. style = None
  34. version = None
  35. ends_in_regular = False
  36. fullname = None
  37. class InvalidFontException(Exception):
  38. pass
  39. # A constant to copy the font without modifying. This is useful when running
  40. # locally and speed up the time to build the SDK.
  41. COPY_ONLY = False
  42. # These constants represent the value of nameID parameter in the namerecord for
  43. # different information.
  44. # see http://scripts.sil.org/cms/scripts/page.php?item_id=IWS-Chapter08#3054f18b
  45. NAMEID_FAMILY = 1
  46. NAMEID_STYLE = 2
  47. NAMEID_FULLNAME = 4
  48. NAMEID_VERSION = 5
  49. # A list of extensions to process.
  50. EXTENSIONS = ['.ttf', '.ttc', '.otf', '.xml']
  51. def main(argv):
  52. if len(argv) < 2:
  53. print 'Incorrect usage: ' + str(argv)
  54. sys.exit('Usage: build_font_single.py /path/to/input/font.ttf /path/to/out/font.ttf')
  55. dest_path = argv[-1]
  56. input_path = argv[0]
  57. extension = os.path.splitext(input_path)[1].lower()
  58. if extension in EXTENSIONS:
  59. if not COPY_ONLY and extension == '.ttf':
  60. convert_font(input_path, dest_path)
  61. return
  62. shutil.copy(input_path, dest_path)
  63. def convert_font(input_path, dest_path):
  64. filename = os.path.basename(input_path)
  65. print 'Converting font: ' + filename
  66. # the path to the output file. The file name is the fontfilename.ttx
  67. ttx_path = dest_path[:-1] + 'x'
  68. try:
  69. # run ttx to generate an xml file in the output folder which represents all
  70. # its info
  71. ttx_args = ['-q', '-o', ttx_path, input_path]
  72. ttx.main(ttx_args)
  73. # now parse the xml file to change its PS name.
  74. tree = etree.parse(ttx_path)
  75. root = tree.getroot()
  76. for name in root.iter('name'):
  77. update_tag(name, get_font_info(name))
  78. tree.write(ttx_path, xml_declaration=True, encoding='utf-8')
  79. # generate the udpated font now.
  80. ttx_args = ['-q', '-o', dest_path, ttx_path]
  81. ttx.main(ttx_args)
  82. except InvalidFontException:
  83. # In case of invalid fonts, we exit.
  84. print filename + ' is not a valid font'
  85. raise
  86. except Exception as e:
  87. print 'Error converting font: ' + filename
  88. print e
  89. # Some fonts are too big to be handled by the ttx library.
  90. # Just copy paste them.
  91. shutil.copy(input_path, dest_path)
  92. try:
  93. # delete the temp ttx file is it exists.
  94. os.remove(ttx_path)
  95. except OSError:
  96. pass
  97. def get_font_info(tag):
  98. """ Returns a list of FontInfo representing the various sets of namerecords
  99. found in the name table of the font. """
  100. fonts = []
  101. font = None
  102. last_name_id = sys.maxint
  103. for namerecord in tag.iter('namerecord'):
  104. if 'nameID' in namerecord.attrib:
  105. name_id = int(namerecord.attrib['nameID'])
  106. # A new font should be created for each platform, encoding and language
  107. # id. But, since the nameIDs are sorted, we use the easy approach of
  108. # creating a new one when the nameIDs reset.
  109. if name_id <= last_name_id and font is not None:
  110. fonts.append(font)
  111. font = None
  112. last_name_id = name_id
  113. if font is None:
  114. font = FontInfo()
  115. if name_id == NAMEID_FAMILY:
  116. font.family = namerecord.text.strip()
  117. if name_id == NAMEID_STYLE:
  118. font.style = namerecord.text.strip()
  119. if name_id == NAMEID_FULLNAME:
  120. font.ends_in_regular = ends_in_regular(namerecord.text)
  121. font.fullname = namerecord.text.strip()
  122. if name_id == NAMEID_VERSION:
  123. font.version = get_version(namerecord.text)
  124. if font is not None:
  125. fonts.append(font)
  126. return fonts
  127. def update_tag(tag, fonts):
  128. last_name_id = sys.maxint
  129. fonts_iterator = fonts.__iter__()
  130. font = None
  131. for namerecord in tag.iter('namerecord'):
  132. if 'nameID' in namerecord.attrib:
  133. name_id = int(namerecord.attrib['nameID'])
  134. if name_id <= last_name_id:
  135. font = fonts_iterator.next()
  136. font = update_font_name(font)
  137. last_name_id = name_id
  138. if name_id == NAMEID_FAMILY:
  139. namerecord.text = font.family
  140. if name_id == NAMEID_FULLNAME:
  141. namerecord.text = font.fullname
  142. def update_font_name(font):
  143. """ Compute the new font family name and font fullname. If the font has a
  144. valid version, it's sanitized and appended to the font family name. The
  145. font fullname is then created by joining the new family name and the
  146. style. If the style is 'Regular', it is appended only if the original font
  147. had it. """
  148. if font.family is None or font.style is None:
  149. raise InvalidFontException('Font doesn\'t have proper family name or style')
  150. if font.version is not None:
  151. new_family = font.family + font.version
  152. else:
  153. new_family = font.family
  154. if font.style is 'Regular' and not font.ends_in_regular:
  155. font.fullname = new_family
  156. else:
  157. font.fullname = new_family + ' ' + font.style
  158. font.family = new_family
  159. return font
  160. def ends_in_regular(string):
  161. """ According to the specification, the font fullname should not end in
  162. 'Regular' for plain fonts. However, some fonts don't obey this rule. We
  163. keep the style info, to minimize the diff. """
  164. string = string.strip().split()[-1]
  165. return string is 'Regular'
  166. def get_version(string):
  167. string = string.strip()
  168. # The spec says that the version string should start with "Version ". But not
  169. # all fonts do. So, we return the complete string if it doesn't start with
  170. # the prefix, else we return the rest of the string after sanitizing it.
  171. prefix = 'Version '
  172. if string.startswith(prefix):
  173. string = string[len(prefix):]
  174. return sanitize(string)
  175. def sanitize(string):
  176. """ Remove non-standard chars. """
  177. return re.sub(r'[^\w-]+', '', string)
  178. if __name__ == '__main__':
  179. main(sys.argv[1:])