1 #!/usr/bin/env python 2 """Script to add a suffix to all family names in the input font's `name` table, 3 and to optionally rename the output files with the given suffix. 4 5 The current family name substring is searched in the nameIDs 1, 3, 4, 6, 16, 6 and 21, and if found the suffix is inserted after it; or else the suffix is 7 appended at the end. 8 """ 9 from __future__ import print_function, absolute_import, unicode_literals 10 import os 11 import argparse 12 import logging 13 from fontTools.ttLib import TTFont 14 from fontTools.misc.cliTools import makeOutputFileName 15 16 17 logger = logging.getLogger() 18 19 WINDOWS_ENGLISH_IDS = 3, 1, 0x409 20 MAC_ROMAN_IDS = 1, 0, 0 21 22 FAMILY_RELATED_IDS = dict( 23 LEGACY_FAMILY=1, 24 TRUETYPE_UNIQUE_ID=3, 25 FULL_NAME=4, 26 POSTSCRIPT_NAME=6, 27 PREFERRED_FAMILY=16, 28 WWS_FAMILY=21, 29 ) 30 31 32 def get_current_family_name(table): 33 family_name_rec = None 34 for plat_id, enc_id, lang_id in (WINDOWS_ENGLISH_IDS, MAC_ROMAN_IDS): 35 for name_id in ( 36 FAMILY_RELATED_IDS["PREFERRED_FAMILY"], 37 FAMILY_RELATED_IDS["LEGACY_FAMILY"], 38 ): 39 family_name_rec = table.getName( 40 nameID=name_id, 41 platformID=plat_id, 42 platEncID=enc_id, 43 langID=lang_id, 44 ) 45 if family_name_rec is not None: 46 break 47 if family_name_rec is not None: 48 break 49 if not family_name_rec: 50 raise ValueError("family name not found; can't add suffix") 51 return family_name_rec.toUnicode() 52 53 54 def insert_suffix(string, family_name, suffix): 55 # check whether family_name is a substring 56 start = string.find(family_name) 57 if start != -1: 58 # insert suffix after the family_name substring 59 end = start + len(family_name) 60 new_string = string[:end] + suffix + string[end:] 61 else: 62 # it's not, we just append the suffix at the end 63 new_string = string + suffix 64 return new_string 65 66 67 def rename_record(name_record, family_name, suffix): 68 string = name_record.toUnicode() 69 new_string = insert_suffix(string, family_name, suffix) 70 name_record.string = new_string 71 return string, new_string 72 73 74 def rename_file(filename, family_name, suffix): 75 filename, ext = os.path.splitext(filename) 76 ps_name = family_name.replace(" ", "") 77 if ps_name in filename: 78 ps_suffix = suffix.replace(" ", "") 79 return insert_suffix(filename, ps_name, ps_suffix) + ext 80 else: 81 return insert_suffix(filename, family_name, suffix) + ext 82 83 84 def add_family_suffix(font, suffix): 85 table = font["name"] 86 87 family_name = get_current_family_name(table) 88 logger.info(" Current family name: '%s'", family_name) 89 90 # postcript name can't contain spaces 91 ps_family_name = family_name.replace(" ", "") 92 ps_suffix = suffix.replace(" ", "") 93 for rec in table.names: 94 name_id = rec.nameID 95 if name_id not in FAMILY_RELATED_IDS.values(): 96 continue 97 if name_id == FAMILY_RELATED_IDS["POSTSCRIPT_NAME"]: 98 old, new = rename_record(rec, ps_family_name, ps_suffix) 99 elif name_id == FAMILY_RELATED_IDS["TRUETYPE_UNIQUE_ID"]: 100 # The Truetype Unique ID rec may contain either the PostScript 101 # Name or the Full Name string, so we try both 102 if ps_family_name in rec.toUnicode(): 103 old, new = rename_record(rec, ps_family_name, ps_suffix) 104 else: 105 old, new = rename_record(rec, family_name, suffix) 106 else: 107 old, new = rename_record(rec, family_name, suffix) 108 logger.info(" %r: '%s' -> '%s'", rec, old, new) 109 110 return family_name 111 112 113 def main(args=None): 114 parser = argparse.ArgumentParser( 115 description=__doc__, 116 formatter_class=argparse.RawDescriptionHelpFormatter, 117 ) 118 parser.add_argument("-s", "--suffix", required=True) 119 parser.add_argument("input_fonts", metavar="FONTFILE", nargs="+") 120 output_group = parser.add_mutually_exclusive_group() 121 output_group.add_argument("-i", "--inplace", action="store_true") 122 output_group.add_argument("-d", "--output-dir") 123 output_group.add_argument("-o", "--output-file") 124 parser.add_argument("-R", "--rename-files", action="store_true") 125 parser.add_argument("-v", "--verbose", action="count", default=0) 126 options = parser.parse_args(args) 127 128 if not options.verbose: 129 level = "WARNING" 130 elif options.verbose == 1: 131 level = "INFO" 132 else: 133 level = "DEBUG" 134 logging.basicConfig(level=level, format="%(message)s") 135 136 if options.output_file and len(options.input_fonts) > 1: 137 parser.error( 138 "argument -o/--output-file can't be used with multiple inputs" 139 ) 140 if options.rename_files and (options.inplace or options.output_file): 141 parser.error("argument -R not allowed with arguments -i or -o") 142 143 for input_name in options.input_fonts: 144 logger.info("Renaming font: '%s'", input_name) 145 146 font = TTFont(input_name) 147 family_name = add_family_suffix(font, options.suffix) 148 149 if options.inplace: 150 output_name = input_name 151 elif options.output_file: 152 output_name = options.output_file 153 else: 154 if options.rename_files: 155 input_name = rename_file( 156 input_name, family_name, options.suffix 157 ) 158 output_name = makeOutputFileName(input_name, options.output_dir) 159 160 font.save(output_name) 161 logger.info("Saved font: '%s'", output_name) 162 163 font.close() 164 del font 165 166 logger.info("Done!") 167 168 169 if __name__ == "__main__": 170 main() 171