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