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 all fonts in the input directories and copy them to the
     19 output directory.
     20 
     21 Usage: build_font.py /path/to/input_fonts1/ /path/to/input_fonts2/ /path/to/output_fonts/
     22 
     23 """
     24 
     25 import glob
     26 from multiprocessing import Pool
     27 import os
     28 import re
     29 import shutil
     30 import sys
     31 import xml.etree.ElementTree as etree
     32 
     33 # Prevent .pyc files from being created.
     34 sys.dont_write_bytecode = True
     35 
     36 # fontTools is available at platform/external/fonttools
     37 from fontTools import ttx
     38 
     39 # global variable
     40 dest_dir = '/tmp'
     41 
     42 
     43 class FontInfo(object):
     44   family = None
     45   style = None
     46   version = None
     47   ends_in_regular = False
     48   fullname = None
     49 
     50 
     51 class 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
     58 NAMEID_FAMILY = 1
     59 NAMEID_STYLE = 2
     60 NAMEID_FULLNAME = 4
     61 NAMEID_VERSION = 5
     62 
     63 
     64 def 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 
     97 def 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 = ['-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 
    134 def 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.maxint
    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 
    166 def update_tag(tag, fonts):
    167   last_name_id = sys.maxint
    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 = fonts_iterator.next()
    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 
    183 def 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 is '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 
    203 def 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 is 'Regular'
    209 
    210 
    211 def 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 
    222 def sanitize(string):
    223   """ Remove non-standard chars. """
    224   return re.sub(r'[^\w-]+', '', string)
    225 
    226 if __name__ == '__main__':
    227   main(sys.argv[1:])
    228