xref: /aosp_15_r20/external/fonttools/Lib/fontTools/merge/cmap.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
5from fontTools.merge.unicode import is_Default_Ignorable
6from fontTools.pens.recordingPen import DecomposingRecordingPen
7import logging
8
9
10log = logging.getLogger("fontTools.merge")
11
12
13def computeMegaGlyphOrder(merger, glyphOrders):
14    """Modifies passed-in glyphOrders to reflect new glyph names.
15    Stores merger.glyphOrder."""
16    megaOrder = {}
17    for glyphOrder in glyphOrders:
18        for i, glyphName in enumerate(glyphOrder):
19            if glyphName in megaOrder:
20                n = megaOrder[glyphName]
21                while (glyphName + "." + repr(n)) in megaOrder:
22                    n += 1
23                megaOrder[glyphName] = n
24                glyphName += "." + repr(n)
25                glyphOrder[i] = glyphName
26            megaOrder[glyphName] = 1
27    merger.glyphOrder = megaOrder = list(megaOrder.keys())
28
29
30def _glyphsAreSame(
31    glyphSet1,
32    glyphSet2,
33    glyph1,
34    glyph2,
35    advanceTolerance=0.05,
36    advanceToleranceEmpty=0.20,
37):
38    pen1 = DecomposingRecordingPen(glyphSet1)
39    pen2 = DecomposingRecordingPen(glyphSet2)
40    g1 = glyphSet1[glyph1]
41    g2 = glyphSet2[glyph2]
42    g1.draw(pen1)
43    g2.draw(pen2)
44    if pen1.value != pen2.value:
45        return False
46    # Allow more width tolerance for glyphs with no ink
47    tolerance = advanceTolerance if pen1.value else advanceToleranceEmpty
48    # TODO Warn if advances not the same but within tolerance.
49    if abs(g1.width - g2.width) > g1.width * tolerance:
50        return False
51    if hasattr(g1, "height") and g1.height is not None:
52        if abs(g1.height - g2.height) > g1.height * tolerance:
53            return False
54    return True
55
56
57# Valid (format, platformID, platEncID) triplets for cmap subtables containing
58# Unicode BMP-only and Unicode Full Repertoire semantics.
59# Cf. OpenType spec for "Platform specific encodings":
60# https://docs.microsoft.com/en-us/typography/opentype/spec/name
61class _CmapUnicodePlatEncodings:
62    BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)}
63    FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)}
64
65
66def computeMegaCmap(merger, cmapTables):
67    """Sets merger.cmap and merger.glyphOrder."""
68
69    # TODO Handle format=14.
70    # Only merge format 4 and 12 Unicode subtables, ignores all other subtables
71    # If there is a format 12 table for a font, ignore the format 4 table of it
72    chosenCmapTables = []
73    for fontIdx, table in enumerate(cmapTables):
74        format4 = None
75        format12 = None
76        for subtable in table.tables:
77            properties = (subtable.format, subtable.platformID, subtable.platEncID)
78            if properties in _CmapUnicodePlatEncodings.BMP:
79                format4 = subtable
80            elif properties in _CmapUnicodePlatEncodings.FullRepertoire:
81                format12 = subtable
82            else:
83                log.warning(
84                    "Dropped cmap subtable from font '%s':\t"
85                    "format %2s, platformID %2s, platEncID %2s",
86                    fontIdx,
87                    subtable.format,
88                    subtable.platformID,
89                    subtable.platEncID,
90                )
91        if format12 is not None:
92            chosenCmapTables.append((format12, fontIdx))
93        elif format4 is not None:
94            chosenCmapTables.append((format4, fontIdx))
95
96    # Build the unicode mapping
97    merger.cmap = cmap = {}
98    fontIndexForGlyph = {}
99    glyphSets = [None for f in merger.fonts] if hasattr(merger, "fonts") else None
100
101    for table, fontIdx in chosenCmapTables:
102        # handle duplicates
103        for uni, gid in table.cmap.items():
104            oldgid = cmap.get(uni, None)
105            if oldgid is None:
106                cmap[uni] = gid
107                fontIndexForGlyph[gid] = fontIdx
108            elif is_Default_Ignorable(uni) or uni in (0x25CC,):  # U+25CC DOTTED CIRCLE
109                continue
110            elif oldgid != gid:
111                # Char previously mapped to oldgid, now to gid.
112                # Record, to fix up in GSUB 'locl' later.
113                if merger.duplicateGlyphsPerFont[fontIdx].get(oldgid) is None:
114                    if glyphSets is not None:
115                        oldFontIdx = fontIndexForGlyph[oldgid]
116                        for idx in (fontIdx, oldFontIdx):
117                            if glyphSets[idx] is None:
118                                glyphSets[idx] = merger.fonts[idx].getGlyphSet()
119                        # if _glyphsAreSame(glyphSets[oldFontIdx], glyphSets[fontIdx], oldgid, gid):
120                        # 	continue
121                    merger.duplicateGlyphsPerFont[fontIdx][oldgid] = gid
122                elif merger.duplicateGlyphsPerFont[fontIdx][oldgid] != gid:
123                    # Char previously mapped to oldgid but oldgid is already remapped to a different
124                    # gid, because of another Unicode character.
125                    # TODO: Try harder to do something about these.
126                    log.warning(
127                        "Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid
128                    )
129
130
131def renameCFFCharStrings(merger, glyphOrder, cffTable):
132    """Rename topDictIndex charStrings based on glyphOrder."""
133    td = cffTable.cff.topDictIndex[0]
134
135    charStrings = {}
136    for i, v in enumerate(td.CharStrings.charStrings.values()):
137        glyphName = glyphOrder[i]
138        charStrings[glyphName] = v
139    td.CharStrings.charStrings = charStrings
140
141    td.charset = list(glyphOrder)
142