__init__.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. # Copyright 2013 Google, Inc. All Rights Reserved.
  2. #
  3. # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
  4. from fontTools import ttLib
  5. import fontTools.merge.base
  6. from fontTools.merge.cmap import (
  7. computeMegaGlyphOrder,
  8. computeMegaCmap,
  9. renameCFFCharStrings,
  10. )
  11. from fontTools.merge.layout import layoutPreMerge, layoutPostMerge
  12. from fontTools.merge.options import Options
  13. import fontTools.merge.tables
  14. from fontTools.misc.loggingTools import Timer
  15. from functools import reduce
  16. import sys
  17. import logging
  18. log = logging.getLogger("fontTools.merge")
  19. timer = Timer(logger=logging.getLogger(__name__ + ".timer"), level=logging.INFO)
  20. class Merger(object):
  21. """Font merger.
  22. This class merges multiple files into a single OpenType font, taking into
  23. account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and
  24. cross-font metrics (e.g. ``hhea.ascent`` is set to the maximum value across
  25. all the fonts).
  26. If multiple glyphs map to the same Unicode value, and the glyphs are considered
  27. sufficiently different (that is, they differ in any of paths, widths, or
  28. height), then subsequent glyphs are renamed and a lookup in the ``locl``
  29. feature will be created to disambiguate them. For example, if the arguments
  30. are an Arabic font and a Latin font and both contain a set of parentheses,
  31. the Latin glyphs will be renamed to ``parenleft#1`` and ``parenright#1``,
  32. and a lookup will be inserted into the to ``locl`` feature (creating it if
  33. necessary) under the ``latn`` script to substitute ``parenleft`` with
  34. ``parenleft#1`` etc.
  35. Restrictions:
  36. - All fonts must have the same units per em.
  37. - If duplicate glyph disambiguation takes place as described above then the
  38. fonts must have a ``GSUB`` table.
  39. Attributes:
  40. options: Currently unused.
  41. """
  42. def __init__(self, options=None):
  43. if not options:
  44. options = Options()
  45. self.options = options
  46. def _openFonts(self, fontfiles):
  47. fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
  48. for font, fontfile in zip(fonts, fontfiles):
  49. font._merger__fontfile = fontfile
  50. font._merger__name = font["name"].getDebugName(4)
  51. return fonts
  52. def merge(self, fontfiles):
  53. """Merges fonts together.
  54. Args:
  55. fontfiles: A list of file names to be merged
  56. Returns:
  57. A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on
  58. this to write it out to an OTF file.
  59. """
  60. #
  61. # Settle on a mega glyph order.
  62. #
  63. fonts = self._openFonts(fontfiles)
  64. glyphOrders = [list(font.getGlyphOrder()) for font in fonts]
  65. computeMegaGlyphOrder(self, glyphOrders)
  66. # Take first input file sfntVersion
  67. sfntVersion = fonts[0].sfntVersion
  68. # Reload fonts and set new glyph names on them.
  69. fonts = self._openFonts(fontfiles)
  70. for font, glyphOrder in zip(fonts, glyphOrders):
  71. font.setGlyphOrder(glyphOrder)
  72. if "CFF " in font:
  73. renameCFFCharStrings(self, glyphOrder, font["CFF "])
  74. cmaps = [font["cmap"] for font in fonts]
  75. self.duplicateGlyphsPerFont = [{} for _ in fonts]
  76. computeMegaCmap(self, cmaps)
  77. mega = ttLib.TTFont(sfntVersion=sfntVersion)
  78. mega.setGlyphOrder(self.glyphOrder)
  79. for font in fonts:
  80. self._preMerge(font)
  81. self.fonts = fonts
  82. allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
  83. allTags.remove("GlyphOrder")
  84. for tag in sorted(allTags):
  85. if tag in self.options.drop_tables:
  86. continue
  87. with timer("merge '%s'" % tag):
  88. tables = [font.get(tag, NotImplemented) for font in fonts]
  89. log.info("Merging '%s'.", tag)
  90. clazz = ttLib.getTableClass(tag)
  91. table = clazz(tag).merge(self, tables)
  92. # XXX Clean this up and use: table = mergeObjects(tables)
  93. if table is not NotImplemented and table is not False:
  94. mega[tag] = table
  95. log.info("Merged '%s'.", tag)
  96. else:
  97. log.info("Dropped '%s'.", tag)
  98. del self.duplicateGlyphsPerFont
  99. del self.fonts
  100. self._postMerge(mega)
  101. return mega
  102. def mergeObjects(self, returnTable, logic, tables):
  103. # Right now we don't use self at all. Will use in the future
  104. # for options and logging.
  105. allKeys = set.union(
  106. set(),
  107. *(vars(table).keys() for table in tables if table is not NotImplemented),
  108. )
  109. for key in allKeys:
  110. try:
  111. mergeLogic = logic[key]
  112. except KeyError:
  113. try:
  114. mergeLogic = logic["*"]
  115. except KeyError:
  116. raise Exception(
  117. "Don't know how to merge key %s of class %s"
  118. % (key, returnTable.__class__.__name__)
  119. )
  120. if mergeLogic is NotImplemented:
  121. continue
  122. value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
  123. if value is not NotImplemented:
  124. setattr(returnTable, key, value)
  125. return returnTable
  126. def _preMerge(self, font):
  127. layoutPreMerge(font)
  128. def _postMerge(self, font):
  129. layoutPostMerge(font)
  130. if "OS/2" in font:
  131. # https://github.com/fonttools/fonttools/issues/2538
  132. # TODO: Add an option to disable this?
  133. font["OS/2"].recalcAvgCharWidth(font)
  134. __all__ = ["Options", "Merger", "main"]
  135. @timer("make one with everything (TOTAL TIME)")
  136. def main(args=None):
  137. """Merge multiple fonts into one"""
  138. from fontTools import configLogger
  139. if args is None:
  140. args = sys.argv[1:]
  141. options = Options()
  142. args = options.parse_opts(args, ignore_unknown=["output-file"])
  143. outfile = "merged.ttf"
  144. fontfiles = []
  145. for g in args:
  146. if g.startswith("--output-file="):
  147. outfile = g[14:]
  148. continue
  149. fontfiles.append(g)
  150. if len(args) < 1:
  151. print("usage: pyftmerge font...", file=sys.stderr)
  152. return 1
  153. configLogger(level=logging.INFO if options.verbose else logging.WARNING)
  154. if options.timing:
  155. timer.logger.setLevel(logging.DEBUG)
  156. else:
  157. timer.logger.disabled = True
  158. merger = Merger(options=options)
  159. font = merger.merge(fontfiles)
  160. with timer("compile and save font"):
  161. font.save(outfile)
  162. if __name__ == "__main__":
  163. sys.exit(main())