xref: /aosp_15_r20/external/fonttools/Lib/fontTools/merge/tables.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, cffLib
6from fontTools.misc.psCharStrings import T2WidthExtractor
7from fontTools.ttLib.tables.DefaultTable import DefaultTable
8from fontTools.merge.base import add_method, mergeObjects
9from fontTools.merge.cmap import computeMegaCmap
10from fontTools.merge.util import *
11import logging
12
13
14log = logging.getLogger("fontTools.merge")
15
16
17ttLib.getTableClass("maxp").mergeMap = {
18    "*": max,
19    "tableTag": equal,
20    "tableVersion": equal,
21    "numGlyphs": sum,
22    "maxStorage": first,
23    "maxFunctionDefs": first,
24    "maxInstructionDefs": first,
25    # TODO When we correctly merge hinting data, update these values:
26    # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
27}
28
29headFlagsMergeBitMap = {
30    "size": 16,
31    "*": bitwise_or,
32    1: bitwise_and,  # Baseline at y = 0
33    2: bitwise_and,  # lsb at x = 0
34    3: bitwise_and,  # Force ppem to integer values. FIXME?
35    5: bitwise_and,  # Font is vertical
36    6: lambda bit: 0,  # Always set to zero
37    11: bitwise_and,  # Font data is 'lossless'
38    13: bitwise_and,  # Optimized for ClearType
39    14: bitwise_and,  # Last resort font. FIXME? equal or first may be better
40    15: lambda bit: 0,  # Always set to zero
41}
42
43ttLib.getTableClass("head").mergeMap = {
44    "tableTag": equal,
45    "tableVersion": max,
46    "fontRevision": max,
47    "checkSumAdjustment": lambda lst: 0,  # We need *something* here
48    "magicNumber": equal,
49    "flags": mergeBits(headFlagsMergeBitMap),
50    "unitsPerEm": equal,
51    "created": current_time,
52    "modified": current_time,
53    "xMin": min,
54    "yMin": min,
55    "xMax": max,
56    "yMax": max,
57    "macStyle": first,
58    "lowestRecPPEM": max,
59    "fontDirectionHint": lambda lst: 2,
60    "indexToLocFormat": first,
61    "glyphDataFormat": equal,
62}
63
64ttLib.getTableClass("hhea").mergeMap = {
65    "*": equal,
66    "tableTag": equal,
67    "tableVersion": max,
68    "ascent": max,
69    "descent": min,
70    "lineGap": max,
71    "advanceWidthMax": max,
72    "minLeftSideBearing": min,
73    "minRightSideBearing": min,
74    "xMaxExtent": max,
75    "caretSlopeRise": first,
76    "caretSlopeRun": first,
77    "caretOffset": first,
78    "numberOfHMetrics": recalculate,
79}
80
81ttLib.getTableClass("vhea").mergeMap = {
82    "*": equal,
83    "tableTag": equal,
84    "tableVersion": max,
85    "ascent": max,
86    "descent": min,
87    "lineGap": max,
88    "advanceHeightMax": max,
89    "minTopSideBearing": min,
90    "minBottomSideBearing": min,
91    "yMaxExtent": max,
92    "caretSlopeRise": first,
93    "caretSlopeRun": first,
94    "caretOffset": first,
95    "numberOfVMetrics": recalculate,
96}
97
98os2FsTypeMergeBitMap = {
99    "size": 16,
100    "*": lambda bit: 0,
101    1: bitwise_or,  # no embedding permitted
102    2: bitwise_and,  # allow previewing and printing documents
103    3: bitwise_and,  # allow editing documents
104    8: bitwise_or,  # no subsetting permitted
105    9: bitwise_or,  # no embedding of outlines permitted
106}
107
108
109def mergeOs2FsType(lst):
110    lst = list(lst)
111    if all(item == 0 for item in lst):
112        return 0
113
114    # Compute least restrictive logic for each fsType value
115    for i in range(len(lst)):
116        # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
117        if lst[i] & 0x000C:
118            lst[i] &= ~0x0002
119        # set bit 2 (allow previewing) if bit 3 is set (allow editing)
120        elif lst[i] & 0x0008:
121            lst[i] |= 0x0004
122        # set bits 2 and 3 if everything is allowed
123        elif lst[i] == 0:
124            lst[i] = 0x000C
125
126    fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
127    # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
128    if fsType & 0x0002:
129        fsType &= ~0x000C
130    return fsType
131
132
133ttLib.getTableClass("OS/2").mergeMap = {
134    "*": first,
135    "tableTag": equal,
136    "version": max,
137    "xAvgCharWidth": first,  # Will be recalculated at the end on the merged font
138    "fsType": mergeOs2FsType,  # Will be overwritten
139    "panose": first,  # FIXME: should really be the first Latin font
140    "ulUnicodeRange1": bitwise_or,
141    "ulUnicodeRange2": bitwise_or,
142    "ulUnicodeRange3": bitwise_or,
143    "ulUnicodeRange4": bitwise_or,
144    "fsFirstCharIndex": min,
145    "fsLastCharIndex": max,
146    "sTypoAscender": max,
147    "sTypoDescender": min,
148    "sTypoLineGap": max,
149    "usWinAscent": max,
150    "usWinDescent": max,
151    # Version 1
152    "ulCodePageRange1": onlyExisting(bitwise_or),
153    "ulCodePageRange2": onlyExisting(bitwise_or),
154    # Version 2, 3, 4
155    "sxHeight": onlyExisting(max),
156    "sCapHeight": onlyExisting(max),
157    "usDefaultChar": onlyExisting(first),
158    "usBreakChar": onlyExisting(first),
159    "usMaxContext": onlyExisting(max),
160    # version 5
161    "usLowerOpticalPointSize": onlyExisting(min),
162    "usUpperOpticalPointSize": onlyExisting(max),
163}
164
165
166@add_method(ttLib.getTableClass("OS/2"))
167def merge(self, m, tables):
168    DefaultTable.merge(self, m, tables)
169    if self.version < 2:
170        # bits 8 and 9 are reserved and should be set to zero
171        self.fsType &= ~0x0300
172    if self.version >= 3:
173        # Only one of bits 1, 2, and 3 may be set. We already take
174        # care of bit 1 implications in mergeOs2FsType. So unset
175        # bit 2 if bit 3 is already set.
176        if self.fsType & 0x0008:
177            self.fsType &= ~0x0004
178    return self
179
180
181ttLib.getTableClass("post").mergeMap = {
182    "*": first,
183    "tableTag": equal,
184    "formatType": max,
185    "isFixedPitch": min,
186    "minMemType42": max,
187    "maxMemType42": lambda lst: 0,
188    "minMemType1": max,
189    "maxMemType1": lambda lst: 0,
190    "mapping": onlyExisting(sumDicts),
191    "extraNames": lambda lst: [],
192}
193
194ttLib.getTableClass("vmtx").mergeMap = ttLib.getTableClass("hmtx").mergeMap = {
195    "tableTag": equal,
196    "metrics": sumDicts,
197}
198
199ttLib.getTableClass("name").mergeMap = {
200    "tableTag": equal,
201    "names": first,  # FIXME? Does mixing name records make sense?
202}
203
204ttLib.getTableClass("loca").mergeMap = {
205    "*": recalculate,
206    "tableTag": equal,
207}
208
209ttLib.getTableClass("glyf").mergeMap = {
210    "tableTag": equal,
211    "glyphs": sumDicts,
212    "glyphOrder": sumLists,
213    "_reverseGlyphOrder": recalculate,
214    "axisTags": equal,
215}
216
217
218@add_method(ttLib.getTableClass("glyf"))
219def merge(self, m, tables):
220    for i, table in enumerate(tables):
221        for g in table.glyphs.values():
222            if i:
223                # Drop hints for all but first font, since
224                # we don't map functions / CVT values.
225                g.removeHinting()
226            # Expand composite glyphs to load their
227            # composite glyph names.
228            if g.isComposite() or g.isVarComposite():
229                g.expand(table)
230    return DefaultTable.merge(self, m, tables)
231
232
233ttLib.getTableClass("prep").mergeMap = lambda self, lst: first(lst)
234ttLib.getTableClass("fpgm").mergeMap = lambda self, lst: first(lst)
235ttLib.getTableClass("cvt ").mergeMap = lambda self, lst: first(lst)
236ttLib.getTableClass("gasp").mergeMap = lambda self, lst: first(
237    lst
238)  # FIXME? Appears irreconcilable
239
240
241@add_method(ttLib.getTableClass("CFF "))
242def merge(self, m, tables):
243    if any(hasattr(table.cff[0], "FDSelect") for table in tables):
244        raise NotImplementedError("Merging CID-keyed CFF tables is not supported yet")
245
246    for table in tables:
247        table.cff.desubroutinize()
248
249    newcff = tables[0]
250    newfont = newcff.cff[0]
251    private = newfont.Private
252    newDefaultWidthX, newNominalWidthX = private.defaultWidthX, private.nominalWidthX
253    storedNamesStrings = []
254    glyphOrderStrings = []
255    glyphOrder = set(newfont.getGlyphOrder())
256
257    for name in newfont.strings.strings:
258        if name not in glyphOrder:
259            storedNamesStrings.append(name)
260        else:
261            glyphOrderStrings.append(name)
262
263    chrset = list(newfont.charset)
264    newcs = newfont.CharStrings
265    log.debug("FONT 0 CharStrings: %d.", len(newcs))
266
267    for i, table in enumerate(tables[1:], start=1):
268        font = table.cff[0]
269        defaultWidthX, nominalWidthX = (
270            font.Private.defaultWidthX,
271            font.Private.nominalWidthX,
272        )
273        widthsDiffer = (
274            defaultWidthX != newDefaultWidthX or nominalWidthX != newNominalWidthX
275        )
276        font.Private = private
277        fontGlyphOrder = set(font.getGlyphOrder())
278        for name in font.strings.strings:
279            if name in fontGlyphOrder:
280                glyphOrderStrings.append(name)
281        cs = font.CharStrings
282        gs = table.cff.GlobalSubrs
283        log.debug("Font %d CharStrings: %d.", i, len(cs))
284        chrset.extend(font.charset)
285        if newcs.charStringsAreIndexed:
286            for i, name in enumerate(cs.charStrings, start=len(newcs)):
287                newcs.charStrings[name] = i
288                newcs.charStringsIndex.items.append(None)
289        for name in cs.charStrings:
290            if widthsDiffer:
291                c = cs[name]
292                defaultWidthXToken = object()
293                extractor = T2WidthExtractor([], [], nominalWidthX, defaultWidthXToken)
294                extractor.execute(c)
295                width = extractor.width
296                if width is not defaultWidthXToken:
297                    c.program.pop(0)
298                else:
299                    width = defaultWidthX
300                if width != newDefaultWidthX:
301                    c.program.insert(0, width - newNominalWidthX)
302            newcs[name] = cs[name]
303
304    newfont.charset = chrset
305    newfont.numGlyphs = len(chrset)
306    newfont.strings.strings = glyphOrderStrings + storedNamesStrings
307
308    return newcff
309
310
311@add_method(ttLib.getTableClass("cmap"))
312def merge(self, m, tables):
313    # TODO Handle format=14.
314    if not hasattr(m, "cmap"):
315        computeMegaCmap(m, tables)
316    cmap = m.cmap
317
318    cmapBmpOnly = {uni: gid for uni, gid in cmap.items() if uni <= 0xFFFF}
319    self.tables = []
320    module = ttLib.getTableModule("cmap")
321    if len(cmapBmpOnly) != len(cmap):
322        # format-12 required.
323        cmapTable = module.cmap_classes[12](12)
324        cmapTable.platformID = 3
325        cmapTable.platEncID = 10
326        cmapTable.language = 0
327        cmapTable.cmap = cmap
328        self.tables.append(cmapTable)
329    # always create format-4
330    cmapTable = module.cmap_classes[4](4)
331    cmapTable.platformID = 3
332    cmapTable.platEncID = 1
333    cmapTable.language = 0
334    cmapTable.cmap = cmapBmpOnly
335    # ordered by platform then encoding
336    self.tables.insert(0, cmapTable)
337    self.tableVersion = 0
338    self.numSubTables = len(self.tables)
339    return self
340