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