xref: /aosp_15_r20/external/fonttools/Lib/fontTools/merge/layout.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
6from fontTools.ttLib.tables.DefaultTable import DefaultTable
7from fontTools.ttLib.tables import otTables
8from fontTools.merge.base import add_method, mergeObjects
9from fontTools.merge.util import *
10import logging
11
12
13log = logging.getLogger("fontTools.merge")
14
15
16def mergeLookupLists(lst):
17    # TODO Do smarter merge.
18    return sumLists(lst)
19
20
21def mergeFeatures(lst):
22    assert lst
23    self = otTables.Feature()
24    self.FeatureParams = None
25    self.LookupListIndex = mergeLookupLists(
26        [l.LookupListIndex for l in lst if l.LookupListIndex]
27    )
28    self.LookupCount = len(self.LookupListIndex)
29    return self
30
31
32def mergeFeatureLists(lst):
33    d = {}
34    for l in lst:
35        for f in l:
36            tag = f.FeatureTag
37            if tag not in d:
38                d[tag] = []
39            d[tag].append(f.Feature)
40    ret = []
41    for tag in sorted(d.keys()):
42        rec = otTables.FeatureRecord()
43        rec.FeatureTag = tag
44        rec.Feature = mergeFeatures(d[tag])
45        ret.append(rec)
46    return ret
47
48
49def mergeLangSyses(lst):
50    assert lst
51
52    # TODO Support merging ReqFeatureIndex
53    assert all(l.ReqFeatureIndex == 0xFFFF for l in lst)
54
55    self = otTables.LangSys()
56    self.LookupOrder = None
57    self.ReqFeatureIndex = 0xFFFF
58    self.FeatureIndex = mergeFeatureLists(
59        [l.FeatureIndex for l in lst if l.FeatureIndex]
60    )
61    self.FeatureCount = len(self.FeatureIndex)
62    return self
63
64
65def mergeScripts(lst):
66    assert lst
67
68    if len(lst) == 1:
69        return lst[0]
70    langSyses = {}
71    for sr in lst:
72        for lsr in sr.LangSysRecord:
73            if lsr.LangSysTag not in langSyses:
74                langSyses[lsr.LangSysTag] = []
75            langSyses[lsr.LangSysTag].append(lsr.LangSys)
76    lsrecords = []
77    for tag, langSys_list in sorted(langSyses.items()):
78        lsr = otTables.LangSysRecord()
79        lsr.LangSys = mergeLangSyses(langSys_list)
80        lsr.LangSysTag = tag
81        lsrecords.append(lsr)
82
83    self = otTables.Script()
84    self.LangSysRecord = lsrecords
85    self.LangSysCount = len(lsrecords)
86    dfltLangSyses = [s.DefaultLangSys for s in lst if s.DefaultLangSys]
87    if dfltLangSyses:
88        self.DefaultLangSys = mergeLangSyses(dfltLangSyses)
89    else:
90        self.DefaultLangSys = None
91    return self
92
93
94def mergeScriptRecords(lst):
95    d = {}
96    for l in lst:
97        for s in l:
98            tag = s.ScriptTag
99            if tag not in d:
100                d[tag] = []
101            d[tag].append(s.Script)
102    ret = []
103    for tag in sorted(d.keys()):
104        rec = otTables.ScriptRecord()
105        rec.ScriptTag = tag
106        rec.Script = mergeScripts(d[tag])
107        ret.append(rec)
108    return ret
109
110
111otTables.ScriptList.mergeMap = {
112    "ScriptCount": lambda lst: None,  # TODO
113    "ScriptRecord": mergeScriptRecords,
114}
115otTables.BaseScriptList.mergeMap = {
116    "BaseScriptCount": lambda lst: None,  # TODO
117    # TODO: Merge duplicate entries
118    "BaseScriptRecord": lambda lst: sorted(
119        sumLists(lst), key=lambda s: s.BaseScriptTag
120    ),
121}
122
123otTables.FeatureList.mergeMap = {
124    "FeatureCount": sum,
125    "FeatureRecord": lambda lst: sorted(sumLists(lst), key=lambda s: s.FeatureTag),
126}
127
128otTables.LookupList.mergeMap = {
129    "LookupCount": sum,
130    "Lookup": sumLists,
131}
132
133otTables.Coverage.mergeMap = {
134    "Format": min,
135    "glyphs": sumLists,
136}
137
138otTables.ClassDef.mergeMap = {
139    "Format": min,
140    "classDefs": sumDicts,
141}
142
143otTables.LigCaretList.mergeMap = {
144    "Coverage": mergeObjects,
145    "LigGlyphCount": sum,
146    "LigGlyph": sumLists,
147}
148
149otTables.AttachList.mergeMap = {
150    "Coverage": mergeObjects,
151    "GlyphCount": sum,
152    "AttachPoint": sumLists,
153}
154
155# XXX Renumber MarkFilterSets of lookups
156otTables.MarkGlyphSetsDef.mergeMap = {
157    "MarkSetTableFormat": equal,
158    "MarkSetCount": sum,
159    "Coverage": sumLists,
160}
161
162otTables.Axis.mergeMap = {
163    "*": mergeObjects,
164}
165
166# XXX Fix BASE table merging
167otTables.BaseTagList.mergeMap = {
168    "BaseTagCount": sum,
169    "BaselineTag": sumLists,
170}
171
172otTables.GDEF.mergeMap = otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = (
173    otTables.BASE.mergeMap
174) = otTables.JSTF.mergeMap = otTables.MATH.mergeMap = {
175    "*": mergeObjects,
176    "Version": max,
177}
178
179ttLib.getTableClass("GDEF").mergeMap = ttLib.getTableClass("GSUB").mergeMap = (
180    ttLib.getTableClass("GPOS").mergeMap
181) = ttLib.getTableClass("BASE").mergeMap = ttLib.getTableClass(
182    "JSTF"
183).mergeMap = ttLib.getTableClass(
184    "MATH"
185).mergeMap = {
186    "tableTag": onlyExisting(equal),  # XXX clean me up
187    "table": mergeObjects,
188}
189
190
191@add_method(ttLib.getTableClass("GSUB"))
192def merge(self, m, tables):
193    assert len(tables) == len(m.duplicateGlyphsPerFont)
194    for i, (table, dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
195        if not dups:
196            continue
197        if table is None or table is NotImplemented:
198            log.warning(
199                "Have non-identical duplicates to resolve for '%s' but no GSUB. Are duplicates intended?: %s",
200                m.fonts[i]._merger__name,
201                dups,
202            )
203            continue
204
205        synthFeature = None
206        synthLookup = None
207        for script in table.table.ScriptList.ScriptRecord:
208            if script.ScriptTag == "DFLT":
209                continue  # XXX
210            for langsys in [script.Script.DefaultLangSys] + [
211                l.LangSys for l in script.Script.LangSysRecord
212            ]:
213                if langsys is None:
214                    continue  # XXX Create!
215                feature = [v for v in langsys.FeatureIndex if v.FeatureTag == "locl"]
216                assert len(feature) <= 1
217                if feature:
218                    feature = feature[0]
219                else:
220                    if not synthFeature:
221                        synthFeature = otTables.FeatureRecord()
222                        synthFeature.FeatureTag = "locl"
223                        f = synthFeature.Feature = otTables.Feature()
224                        f.FeatureParams = None
225                        f.LookupCount = 0
226                        f.LookupListIndex = []
227                        table.table.FeatureList.FeatureRecord.append(synthFeature)
228                        table.table.FeatureList.FeatureCount += 1
229                    feature = synthFeature
230                    langsys.FeatureIndex.append(feature)
231                    langsys.FeatureIndex.sort(key=lambda v: v.FeatureTag)
232
233                if not synthLookup:
234                    subtable = otTables.SingleSubst()
235                    subtable.mapping = dups
236                    synthLookup = otTables.Lookup()
237                    synthLookup.LookupFlag = 0
238                    synthLookup.LookupType = 1
239                    synthLookup.SubTableCount = 1
240                    synthLookup.SubTable = [subtable]
241                    if table.table.LookupList is None:
242                        # mtiLib uses None as default value for LookupList,
243                        # while feaLib points to an empty array with count 0
244                        # TODO: make them do the same
245                        table.table.LookupList = otTables.LookupList()
246                        table.table.LookupList.Lookup = []
247                        table.table.LookupList.LookupCount = 0
248                    table.table.LookupList.Lookup.append(synthLookup)
249                    table.table.LookupList.LookupCount += 1
250
251                if feature.Feature.LookupListIndex[:1] != [synthLookup]:
252                    feature.Feature.LookupListIndex[:0] = [synthLookup]
253                    feature.Feature.LookupCount += 1
254
255    DefaultTable.merge(self, m, tables)
256    return self
257
258
259@add_method(
260    otTables.SingleSubst,
261    otTables.MultipleSubst,
262    otTables.AlternateSubst,
263    otTables.LigatureSubst,
264    otTables.ReverseChainSingleSubst,
265    otTables.SinglePos,
266    otTables.PairPos,
267    otTables.CursivePos,
268    otTables.MarkBasePos,
269    otTables.MarkLigPos,
270    otTables.MarkMarkPos,
271)
272def mapLookups(self, lookupMap):
273    pass
274
275
276# Copied and trimmed down from subset.py
277@add_method(
278    otTables.ContextSubst,
279    otTables.ChainContextSubst,
280    otTables.ContextPos,
281    otTables.ChainContextPos,
282)
283def __merge_classify_context(self):
284    class ContextHelper(object):
285        def __init__(self, klass, Format):
286            if klass.__name__.endswith("Subst"):
287                Typ = "Sub"
288                Type = "Subst"
289            else:
290                Typ = "Pos"
291                Type = "Pos"
292            if klass.__name__.startswith("Chain"):
293                Chain = "Chain"
294            else:
295                Chain = ""
296            ChainTyp = Chain + Typ
297
298            self.Typ = Typ
299            self.Type = Type
300            self.Chain = Chain
301            self.ChainTyp = ChainTyp
302
303            self.LookupRecord = Type + "LookupRecord"
304
305            if Format == 1:
306                self.Rule = ChainTyp + "Rule"
307                self.RuleSet = ChainTyp + "RuleSet"
308            elif Format == 2:
309                self.Rule = ChainTyp + "ClassRule"
310                self.RuleSet = ChainTyp + "ClassSet"
311
312    if self.Format not in [1, 2, 3]:
313        return None  # Don't shoot the messenger; let it go
314    if not hasattr(self.__class__, "_merge__ContextHelpers"):
315        self.__class__._merge__ContextHelpers = {}
316    if self.Format not in self.__class__._merge__ContextHelpers:
317        helper = ContextHelper(self.__class__, self.Format)
318        self.__class__._merge__ContextHelpers[self.Format] = helper
319    return self.__class__._merge__ContextHelpers[self.Format]
320
321
322@add_method(
323    otTables.ContextSubst,
324    otTables.ChainContextSubst,
325    otTables.ContextPos,
326    otTables.ChainContextPos,
327)
328def mapLookups(self, lookupMap):
329    c = self.__merge_classify_context()
330
331    if self.Format in [1, 2]:
332        for rs in getattr(self, c.RuleSet):
333            if not rs:
334                continue
335            for r in getattr(rs, c.Rule):
336                if not r:
337                    continue
338                for ll in getattr(r, c.LookupRecord):
339                    if not ll:
340                        continue
341                    ll.LookupListIndex = lookupMap[ll.LookupListIndex]
342    elif self.Format == 3:
343        for ll in getattr(self, c.LookupRecord):
344            if not ll:
345                continue
346            ll.LookupListIndex = lookupMap[ll.LookupListIndex]
347    else:
348        assert 0, "unknown format: %s" % self.Format
349
350
351@add_method(otTables.ExtensionSubst, otTables.ExtensionPos)
352def mapLookups(self, lookupMap):
353    if self.Format == 1:
354        self.ExtSubTable.mapLookups(lookupMap)
355    else:
356        assert 0, "unknown format: %s" % self.Format
357
358
359@add_method(otTables.Lookup)
360def mapLookups(self, lookupMap):
361    for st in self.SubTable:
362        if not st:
363            continue
364        st.mapLookups(lookupMap)
365
366
367@add_method(otTables.LookupList)
368def mapLookups(self, lookupMap):
369    for l in self.Lookup:
370        if not l:
371            continue
372        l.mapLookups(lookupMap)
373
374
375@add_method(otTables.Lookup)
376def mapMarkFilteringSets(self, markFilteringSetMap):
377    if self.LookupFlag & 0x0010:
378        self.MarkFilteringSet = markFilteringSetMap[self.MarkFilteringSet]
379
380
381@add_method(otTables.LookupList)
382def mapMarkFilteringSets(self, markFilteringSetMap):
383    for l in self.Lookup:
384        if not l:
385            continue
386        l.mapMarkFilteringSets(markFilteringSetMap)
387
388
389@add_method(otTables.Feature)
390def mapLookups(self, lookupMap):
391    self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
392
393
394@add_method(otTables.FeatureList)
395def mapLookups(self, lookupMap):
396    for f in self.FeatureRecord:
397        if not f or not f.Feature:
398            continue
399        f.Feature.mapLookups(lookupMap)
400
401
402@add_method(otTables.DefaultLangSys, otTables.LangSys)
403def mapFeatures(self, featureMap):
404    self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
405    if self.ReqFeatureIndex != 65535:
406        self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
407
408
409@add_method(otTables.Script)
410def mapFeatures(self, featureMap):
411    if self.DefaultLangSys:
412        self.DefaultLangSys.mapFeatures(featureMap)
413    for l in self.LangSysRecord:
414        if not l or not l.LangSys:
415            continue
416        l.LangSys.mapFeatures(featureMap)
417
418
419@add_method(otTables.ScriptList)
420def mapFeatures(self, featureMap):
421    for s in self.ScriptRecord:
422        if not s or not s.Script:
423            continue
424        s.Script.mapFeatures(featureMap)
425
426
427def layoutPreMerge(font):
428    # Map indices to references
429
430    GDEF = font.get("GDEF")
431    GSUB = font.get("GSUB")
432    GPOS = font.get("GPOS")
433
434    for t in [GSUB, GPOS]:
435        if not t:
436            continue
437
438        if t.table.LookupList:
439            lookupMap = {i: v for i, v in enumerate(t.table.LookupList.Lookup)}
440            t.table.LookupList.mapLookups(lookupMap)
441            t.table.FeatureList.mapLookups(lookupMap)
442
443            if (
444                GDEF
445                and GDEF.table.Version >= 0x00010002
446                and GDEF.table.MarkGlyphSetsDef
447            ):
448                markFilteringSetMap = {
449                    i: v for i, v in enumerate(GDEF.table.MarkGlyphSetsDef.Coverage)
450                }
451                t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
452
453        if t.table.FeatureList and t.table.ScriptList:
454            featureMap = {i: v for i, v in enumerate(t.table.FeatureList.FeatureRecord)}
455            t.table.ScriptList.mapFeatures(featureMap)
456
457    # TODO FeatureParams nameIDs
458
459
460def layoutPostMerge(font):
461    # Map references back to indices
462
463    GDEF = font.get("GDEF")
464    GSUB = font.get("GSUB")
465    GPOS = font.get("GPOS")
466
467    for t in [GSUB, GPOS]:
468        if not t:
469            continue
470
471        if t.table.FeatureList and t.table.ScriptList:
472            # Collect unregistered (new) features.
473            featureMap = GregariousIdentityDict(t.table.FeatureList.FeatureRecord)
474            t.table.ScriptList.mapFeatures(featureMap)
475
476            # Record used features.
477            featureMap = AttendanceRecordingIdentityDict(
478                t.table.FeatureList.FeatureRecord
479            )
480            t.table.ScriptList.mapFeatures(featureMap)
481            usedIndices = featureMap.s
482
483            # Remove unused features
484            t.table.FeatureList.FeatureRecord = [
485                f
486                for i, f in enumerate(t.table.FeatureList.FeatureRecord)
487                if i in usedIndices
488            ]
489
490            # Map back to indices.
491            featureMap = NonhashableDict(t.table.FeatureList.FeatureRecord)
492            t.table.ScriptList.mapFeatures(featureMap)
493
494            t.table.FeatureList.FeatureCount = len(t.table.FeatureList.FeatureRecord)
495
496        if t.table.LookupList:
497            # Collect unregistered (new) lookups.
498            lookupMap = GregariousIdentityDict(t.table.LookupList.Lookup)
499            t.table.FeatureList.mapLookups(lookupMap)
500            t.table.LookupList.mapLookups(lookupMap)
501
502            # Record used lookups.
503            lookupMap = AttendanceRecordingIdentityDict(t.table.LookupList.Lookup)
504            t.table.FeatureList.mapLookups(lookupMap)
505            t.table.LookupList.mapLookups(lookupMap)
506            usedIndices = lookupMap.s
507
508            # Remove unused lookups
509            t.table.LookupList.Lookup = [
510                l for i, l in enumerate(t.table.LookupList.Lookup) if i in usedIndices
511            ]
512
513            # Map back to indices.
514            lookupMap = NonhashableDict(t.table.LookupList.Lookup)
515            t.table.FeatureList.mapLookups(lookupMap)
516            t.table.LookupList.mapLookups(lookupMap)
517
518            t.table.LookupList.LookupCount = len(t.table.LookupList.Lookup)
519
520            if GDEF and GDEF.table.Version >= 0x00010002:
521                markFilteringSetMap = NonhashableDict(
522                    GDEF.table.MarkGlyphSetsDef.Coverage
523                )
524                t.table.LookupList.mapMarkFilteringSets(markFilteringSetMap)
525
526    # TODO FeatureParams nameIDs
527