123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- # Copyright 2013 Google, Inc. All Rights Reserved.
- #
- # Google Author(s): Behdad Esfahbod, Roozbeh Pournader
- from fontTools import ttLib
- import fontTools.merge.base
- from fontTools.merge.cmap import (
- computeMegaGlyphOrder,
- computeMegaCmap,
- renameCFFCharStrings,
- )
- from fontTools.merge.layout import layoutPreMerge, layoutPostMerge
- from fontTools.merge.options import Options
- import fontTools.merge.tables
- from fontTools.misc.loggingTools import Timer
- from functools import reduce
- import sys
- import logging
- log = logging.getLogger("fontTools.merge")
- timer = Timer(logger=logging.getLogger(__name__ + ".timer"), level=logging.INFO)
- class Merger(object):
- """Font merger.
- This class merges multiple files into a single OpenType font, taking into
- account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and
- cross-font metrics (for example ``hhea.ascent`` is set to the maximum value
- across all the fonts).
- If multiple glyphs map to the same Unicode value, and the glyphs are considered
- sufficiently different (that is, they differ in any of paths, widths, or
- height), then subsequent glyphs are renamed and a lookup in the ``locl``
- feature will be created to disambiguate them. For example, if the arguments
- are an Arabic font and a Latin font and both contain a set of parentheses,
- the Latin glyphs will be renamed to ``parenleft.1`` and ``parenright.1``,
- and a lookup will be inserted into the to ``locl`` feature (creating it if
- necessary) under the ``latn`` script to substitute ``parenleft`` with
- ``parenleft.1`` etc.
- Restrictions:
- - All fonts must have the same units per em.
- - If duplicate glyph disambiguation takes place as described above then the
- fonts must have a ``GSUB`` table.
- Attributes:
- options: Currently unused.
- """
- def __init__(self, options=None):
- if not options:
- options = Options()
- self.options = options
- def _openFonts(self, fontfiles):
- fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
- for font, fontfile in zip(fonts, fontfiles):
- font._merger__fontfile = fontfile
- font._merger__name = font["name"].getDebugName(4)
- return fonts
- def merge(self, fontfiles):
- """Merges fonts together.
- Args:
- fontfiles: A list of file names to be merged
- Returns:
- A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on
- this to write it out to an OTF file.
- """
- #
- # Settle on a mega glyph order.
- #
- fonts = self._openFonts(fontfiles)
- glyphOrders = [list(font.getGlyphOrder()) for font in fonts]
- computeMegaGlyphOrder(self, glyphOrders)
- # Take first input file sfntVersion
- sfntVersion = fonts[0].sfntVersion
- # Reload fonts and set new glyph names on them.
- fonts = self._openFonts(fontfiles)
- for font, glyphOrder in zip(fonts, glyphOrders):
- font.setGlyphOrder(glyphOrder)
- if "CFF " in font:
- renameCFFCharStrings(self, glyphOrder, font["CFF "])
- cmaps = [font["cmap"] for font in fonts]
- self.duplicateGlyphsPerFont = [{} for _ in fonts]
- computeMegaCmap(self, cmaps)
- mega = ttLib.TTFont(sfntVersion=sfntVersion)
- mega.setGlyphOrder(self.glyphOrder)
- for font in fonts:
- self._preMerge(font)
- self.fonts = fonts
- allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
- allTags.remove("GlyphOrder")
- for tag in sorted(allTags):
- if tag in self.options.drop_tables:
- continue
- with timer("merge '%s'" % tag):
- tables = [font.get(tag, NotImplemented) for font in fonts]
- log.info("Merging '%s'.", tag)
- clazz = ttLib.getTableClass(tag)
- table = clazz(tag).merge(self, tables)
- # XXX Clean this up and use: table = mergeObjects(tables)
- if table is not NotImplemented and table is not False:
- mega[tag] = table
- log.info("Merged '%s'.", tag)
- else:
- log.info("Dropped '%s'.", tag)
- del self.duplicateGlyphsPerFont
- del self.fonts
- self._postMerge(mega)
- return mega
- def mergeObjects(self, returnTable, logic, tables):
- # Right now we don't use self at all. Will use in the future
- # for options and logging.
- allKeys = set.union(
- set(),
- *(vars(table).keys() for table in tables if table is not NotImplemented),
- )
- for key in allKeys:
- log.info(" %s", key)
- try:
- mergeLogic = logic[key]
- except KeyError:
- try:
- mergeLogic = logic["*"]
- except KeyError:
- raise Exception(
- "Don't know how to merge key %s of class %s"
- % (key, returnTable.__class__.__name__)
- )
- if mergeLogic is NotImplemented:
- continue
- value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
- if value is not NotImplemented:
- setattr(returnTable, key, value)
- return returnTable
- def _preMerge(self, font):
- layoutPreMerge(font)
- def _postMerge(self, font):
- layoutPostMerge(font)
- if "OS/2" in font:
- # https://github.com/fonttools/fonttools/issues/2538
- # TODO: Add an option to disable this?
- font["OS/2"].recalcAvgCharWidth(font)
- __all__ = ["Options", "Merger", "main"]
- @timer("make one with everything (TOTAL TIME)")
- def main(args=None):
- """Merge multiple fonts into one"""
- from fontTools import configLogger
- if args is None:
- args = sys.argv[1:]
- options = Options()
- args = options.parse_opts(args)
- fontfiles = []
- if options.input_file:
- with open(options.input_file) as inputfile:
- fontfiles = [
- line.strip()
- for line in inputfile.readlines()
- if not line.lstrip().startswith("#")
- ]
- for g in args:
- fontfiles.append(g)
- if len(fontfiles) < 1:
- print(
- "usage: pyftmerge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]",
- file=sys.stderr,
- )
- print(
- " [--drop-tables=tags] [--verbose] [--timing]",
- file=sys.stderr,
- )
- print("", file=sys.stderr)
- print(" font1 ... fontN Files to merge.", file=sys.stderr)
- print(
- " --input-file=<filename> Read files to merge from a text file, each path new line. # Comment lines allowed.",
- file=sys.stderr,
- )
- print(
- " --output-file=<filename> Specify output file name (default: merged.ttf).",
- file=sys.stderr,
- )
- print(
- " --import-file=<filename> TTX file to import after merging. This can be used to set metadata.",
- file=sys.stderr,
- )
- print(
- " --drop-tables=<table tags> Comma separated list of table tags to skip, case sensitive.",
- file=sys.stderr,
- )
- print(
- " --verbose Output progress information.",
- file=sys.stderr,
- )
- print(" --timing Output progress timing.", file=sys.stderr)
- return 1
- configLogger(level=logging.INFO if options.verbose else logging.WARNING)
- if options.timing:
- timer.logger.setLevel(logging.DEBUG)
- else:
- timer.logger.disabled = True
- merger = Merger(options=options)
- font = merger.merge(fontfiles)
- if options.import_file:
- font.importXML(options.import_file)
- with timer("compile and save font"):
- font.save(options.output_file)
- if __name__ == "__main__":
- sys.exit(main())
|