Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 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 | """ |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 18 | Rename the PS name of all fonts in the input directories and copy them to the |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 19 | output directory. |
| 20 | |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 21 | Usage: build_font.py /path/to/input_fonts1/ /path/to/input_fonts2/ /path/to/output_fonts/ |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 22 | |
| 23 | """ |
| 24 | |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 25 | import glob |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 26 | from multiprocessing import Pool |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 38 | |
| 39 | # global variable |
| 40 | dest_dir = '/tmp' |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 41 | |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 | |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 64 | def main(argv): |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 65 | if len(argv) < 2: |
| 66 | sys.exit('Usage: build_font.py /path/to/input_fonts/ /path/to/out/dir/') |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 71 | dest_dir = argv[-1] |
| 72 | src_dirs = argv[:-1] |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 73 | cwd = os.getcwd() |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 74 | os.chdir(dest_dir) |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 75 | files = glob.glob('*') |
| 76 | for filename in files: |
| 77 | os.remove(filename) |
| 78 | os.chdir(cwd) |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 79 | input_fonts = list() |
Deepanshu Gupta | 49a60d4 | 2014-05-19 16:16:29 -0700 | [diff] [blame] | 80 | for src_dir in src_dirs: |
Deepanshu Gupta | 46eff27 | 2014-05-27 11:21:24 -0700 | [diff] [blame] | 81 | for dirname, dirnames, filenames in os.walk(src_dir): |
| 82 | for filename in filenames: |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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) |
Deepanshu Gupta | 46eff27 | 2014-05-27 11:21:24 -0700 | [diff] [blame] | 89 | if '.git' in dirnames: |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 90 | # don't go into any .git directories. |
| 91 | dirnames.remove('.git') |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 92 | # Create as many threads as the number of CPUs |
| 93 | pool = Pool(processes=None) |
| 94 | pool.map(convert_font, input_fonts) |
| 95 | |
| 96 | |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 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) |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 110 | root = tree.getroot() |
| 111 | for name in root.iter('name'): |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 112 | update_tag(name, get_font_info(name)) |
| 113 | tree.write(ttx_path, xml_declaration=True, encoding='utf-8') |
Deepanshu Gupta | d23417a | 2014-05-27 10:54:41 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 132 | |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 133 | |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 134 | def get_font_info(tag): |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 140 | for namerecord in tag.iter('namerecord'): |
| 141 | if 'nameID' in namerecord.attrib: |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 164 | |
| 165 | |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 166 | def update_tag(tag, fonts): |
| 167 | last_name_id = sys.maxint |
| 168 | fonts_iterator = fonts.__iter__() |
| 169 | font = None |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 170 | for namerecord in tag.iter('namerecord'): |
| 171 | if 'nameID' in namerecord.attrib: |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 181 | |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 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 | |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 210 | |
| 211 | def get_version(string): |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 212 | string = string.strip() |
Deepanshu Gupta | d654b6f | 2015-04-06 12:49:25 -0700 | [diff] [blame] | 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) |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 220 | |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 221 | |
| 222 | def sanitize(string): |
Deepanshu Gupta | d654b6f | 2015-04-06 12:49:25 -0700 | [diff] [blame] | 223 | """ Remove non-standard chars. """ |
Deepanshu Gupta | 7ea293b | 2014-06-27 18:39:32 -0700 | [diff] [blame] | 224 | return re.sub(r'[^\w-]+', '', string) |
| 225 | |
Deepanshu Gupta | ad3f288 | 2014-05-15 18:39:30 -0700 | [diff] [blame] | 226 | if __name__ == '__main__': |
| 227 | main(sys.argv[1:]) |