Home | History | Annotate | Download | only in rename_font
      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 """
     18 Rename the PS name of the input font.
     19 
     20 OpenType fonts (*.otf) are not currently supported. They are copied to the destination without renaming.
     21 XML files are also copied in case they are passed there by mistake.
     22 
     23 Usage: build_font_single.py /path/to/input_font.ttf /path/to/output_font.ttf
     24 
     25 """
     26 
     27 import glob
     28 import os
     29 import re
     30 import shutil
     31 import sys
     32 import xml.etree.ElementTree as etree
     33 
     34 # Prevent .pyc files from being created.
     35 sys.dont_write_bytecode = True
     36 
     37 # fontTools is available at platform/external/fonttools
     38 from fontTools import ttx
     39 
     40 
     41 class FontInfo(object):
     42   family = None
     43   style = None
     44   version = None
     45   ends_in_regular = False
     46   fullname = None
     47 
     48 
     49 class 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.
     55 COPY_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
     60 NAMEID_FAMILY = 1
     61 NAMEID_STYLE = 2
     62 NAMEID_FULLNAME = 4
     63 NAMEID_VERSION = 5
     64 
     65 # A list of extensions to process.
     66 EXTENSIONS = ['.ttf', '.otf', '.xml']
     67 
     68 def 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 
     82 def 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 
    118 def 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 
    150 def 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 
    167 def 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 
    187 def 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 
    195 def 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 
    206 def sanitize(string):
    207   """ Remove non-standard chars. """
    208   return re.sub(r'[^\w-]+', '', string)
    209 
    210 if __name__ == '__main__':
    211   main(sys.argv[1:])
    212