1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader 4 5from fontTools import ttLib 6import fontTools.merge.base 7from fontTools.merge.cmap import ( 8 computeMegaGlyphOrder, 9 computeMegaCmap, 10 renameCFFCharStrings, 11) 12from fontTools.merge.layout import layoutPreMerge, layoutPostMerge 13from fontTools.merge.options import Options 14import fontTools.merge.tables 15from fontTools.misc.loggingTools import Timer 16from functools import reduce 17import sys 18import logging 19 20 21log = logging.getLogger("fontTools.merge") 22timer = Timer(logger=logging.getLogger(__name__ + ".timer"), level=logging.INFO) 23 24 25class Merger(object): 26 """Font merger. 27 28 This class merges multiple files into a single OpenType font, taking into 29 account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and 30 cross-font metrics (e.g. ``hhea.ascent`` is set to the maximum value across 31 all the fonts). 32 33 If multiple glyphs map to the same Unicode value, and the glyphs are considered 34 sufficiently different (that is, they differ in any of paths, widths, or 35 height), then subsequent glyphs are renamed and a lookup in the ``locl`` 36 feature will be created to disambiguate them. For example, if the arguments 37 are an Arabic font and a Latin font and both contain a set of parentheses, 38 the Latin glyphs will be renamed to ``parenleft#1`` and ``parenright#1``, 39 and a lookup will be inserted into the to ``locl`` feature (creating it if 40 necessary) under the ``latn`` script to substitute ``parenleft`` with 41 ``parenleft#1`` etc. 42 43 Restrictions: 44 45 - All fonts must have the same units per em. 46 - If duplicate glyph disambiguation takes place as described above then the 47 fonts must have a ``GSUB`` table. 48 49 Attributes: 50 options: Currently unused. 51 """ 52 53 def __init__(self, options=None): 54 if not options: 55 options = Options() 56 57 self.options = options 58 59 def _openFonts(self, fontfiles): 60 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] 61 for font, fontfile in zip(fonts, fontfiles): 62 font._merger__fontfile = fontfile 63 font._merger__name = font["name"].getDebugName(4) 64 return fonts 65 66 def merge(self, fontfiles): 67 """Merges fonts together. 68 69 Args: 70 fontfiles: A list of file names to be merged 71 72 Returns: 73 A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on 74 this to write it out to an OTF file. 75 """ 76 # 77 # Settle on a mega glyph order. 78 # 79 fonts = self._openFonts(fontfiles) 80 glyphOrders = [list(font.getGlyphOrder()) for font in fonts] 81 computeMegaGlyphOrder(self, glyphOrders) 82 83 # Take first input file sfntVersion 84 sfntVersion = fonts[0].sfntVersion 85 86 # Reload fonts and set new glyph names on them. 87 fonts = self._openFonts(fontfiles) 88 for font, glyphOrder in zip(fonts, glyphOrders): 89 font.setGlyphOrder(glyphOrder) 90 if "CFF " in font: 91 renameCFFCharStrings(self, glyphOrder, font["CFF "]) 92 93 cmaps = [font["cmap"] for font in fonts] 94 self.duplicateGlyphsPerFont = [{} for _ in fonts] 95 computeMegaCmap(self, cmaps) 96 97 mega = ttLib.TTFont(sfntVersion=sfntVersion) 98 mega.setGlyphOrder(self.glyphOrder) 99 100 for font in fonts: 101 self._preMerge(font) 102 103 self.fonts = fonts 104 105 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) 106 allTags.remove("GlyphOrder") 107 108 for tag in sorted(allTags): 109 if tag in self.options.drop_tables: 110 continue 111 112 with timer("merge '%s'" % tag): 113 tables = [font.get(tag, NotImplemented) for font in fonts] 114 115 log.info("Merging '%s'.", tag) 116 clazz = ttLib.getTableClass(tag) 117 table = clazz(tag).merge(self, tables) 118 # XXX Clean this up and use: table = mergeObjects(tables) 119 120 if table is not NotImplemented and table is not False: 121 mega[tag] = table 122 log.info("Merged '%s'.", tag) 123 else: 124 log.info("Dropped '%s'.", tag) 125 126 del self.duplicateGlyphsPerFont 127 del self.fonts 128 129 self._postMerge(mega) 130 131 return mega 132 133 def mergeObjects(self, returnTable, logic, tables): 134 # Right now we don't use self at all. Will use in the future 135 # for options and logging. 136 137 allKeys = set.union( 138 set(), 139 *(vars(table).keys() for table in tables if table is not NotImplemented), 140 ) 141 for key in allKeys: 142 log.info(" %s", key) 143 try: 144 mergeLogic = logic[key] 145 except KeyError: 146 try: 147 mergeLogic = logic["*"] 148 except KeyError: 149 raise Exception( 150 "Don't know how to merge key %s of class %s" 151 % (key, returnTable.__class__.__name__) 152 ) 153 if mergeLogic is NotImplemented: 154 continue 155 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables) 156 if value is not NotImplemented: 157 setattr(returnTable, key, value) 158 159 return returnTable 160 161 def _preMerge(self, font): 162 layoutPreMerge(font) 163 164 def _postMerge(self, font): 165 layoutPostMerge(font) 166 167 if "OS/2" in font: 168 # https://github.com/fonttools/fonttools/issues/2538 169 # TODO: Add an option to disable this? 170 font["OS/2"].recalcAvgCharWidth(font) 171 172 173__all__ = ["Options", "Merger", "main"] 174 175 176@timer("make one with everything (TOTAL TIME)") 177def main(args=None): 178 """Merge multiple fonts into one""" 179 from fontTools import configLogger 180 181 if args is None: 182 args = sys.argv[1:] 183 184 options = Options() 185 args = options.parse_opts(args) 186 fontfiles = [] 187 if options.input_file: 188 with open(options.input_file) as inputfile: 189 fontfiles = [ 190 line.strip() 191 for line in inputfile.readlines() 192 if not line.lstrip().startswith("#") 193 ] 194 for g in args: 195 fontfiles.append(g) 196 197 if len(fontfiles) < 1: 198 print( 199 "usage: pyftmerge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]", 200 file=sys.stderr, 201 ) 202 print( 203 " [--drop-tables=tags] [--verbose] [--timing]", 204 file=sys.stderr, 205 ) 206 print("", file=sys.stderr) 207 print(" font1 ... fontN Files to merge.", file=sys.stderr) 208 print( 209 " --input-file=<filename> Read files to merge from a text file, each path new line. # Comment lines allowed.", 210 file=sys.stderr, 211 ) 212 print( 213 " --output-file=<filename> Specify output file name (default: merged.ttf).", 214 file=sys.stderr, 215 ) 216 print( 217 " --import-file=<filename> TTX file to import after merging. This can be used to set metadata.", 218 file=sys.stderr, 219 ) 220 print( 221 " --drop-tables=<table tags> Comma separated list of table tags to skip, case sensitive.", 222 file=sys.stderr, 223 ) 224 print( 225 " --verbose Output progress information.", 226 file=sys.stderr, 227 ) 228 print(" --timing Output progress timing.", file=sys.stderr) 229 return 1 230 231 configLogger(level=logging.INFO if options.verbose else logging.WARNING) 232 if options.timing: 233 timer.logger.setLevel(logging.DEBUG) 234 else: 235 timer.logger.disabled = True 236 237 merger = Merger(options=options) 238 font = merger.merge(fontfiles) 239 240 if options.import_file: 241 font.importXML(options.import_file) 242 243 with timer("compile and save font"): 244 font.save(options.output_file) 245 246 247if __name__ == "__main__": 248 sys.exit(main()) 249