xref: /aosp_15_r20/external/fonttools/Lib/fontTools/merge/__init__.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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