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