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""" 18Rename the PS name of the input font. 19 20OpenType fonts (*.otf) are not currently supported. They are copied to the destination without renaming. 21XML 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 65# A list of extensions to process. 66EXTENSIONS = ['.ttf', '.otf', '.xml'] 67 68def 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 82def 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 118def 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 150def 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 167def 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 187def 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 195def 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 206def sanitize(string): 207 """ Remove non-standard chars. """ 208 return re.sub(r'[^\w-]+', '', string) 209 210if __name__ == '__main__': 211 main(sys.argv[1:]) 212