xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/_k_e_r_n.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.ttLib import getSearchRange
2from fontTools.misc.textTools import safeEval, readHex
3from fontTools.misc.fixedTools import fixedToFloat as fi2fl, floatToFixed as fl2fi
4from . import DefaultTable
5import struct
6import sys
7import array
8import logging
9
10
11log = logging.getLogger(__name__)
12
13
14class table__k_e_r_n(DefaultTable.DefaultTable):
15    def getkern(self, format):
16        for subtable in self.kernTables:
17            if subtable.format == format:
18                return subtable
19        return None  # not found
20
21    def decompile(self, data, ttFont):
22        version, nTables = struct.unpack(">HH", data[:4])
23        apple = False
24        if (len(data) >= 8) and (version == 1):
25            # AAT Apple's "new" format. Hm.
26            version, nTables = struct.unpack(">LL", data[:8])
27            self.version = fi2fl(version, 16)
28            data = data[8:]
29            apple = True
30        else:
31            self.version = version
32            data = data[4:]
33        self.kernTables = []
34        for i in range(nTables):
35            if self.version == 1.0:
36                # Apple
37                length, coverage, subtableFormat = struct.unpack(">LBB", data[:6])
38            else:
39                # in OpenType spec the "version" field refers to the common
40                # subtable header; the actual subtable format is stored in
41                # the 8-15 mask bits of "coverage" field.
42                # This "version" is always 0 so we ignore it here
43                _, length, subtableFormat, coverage = struct.unpack(">HHBB", data[:6])
44                if nTables == 1 and subtableFormat == 0:
45                    # The "length" value is ignored since some fonts
46                    # (like OpenSans and Calibri) have a subtable larger than
47                    # its value.
48                    (nPairs,) = struct.unpack(">H", data[6:8])
49                    calculated_length = (nPairs * 6) + 14
50                    if length != calculated_length:
51                        log.warning(
52                            "'kern' subtable longer than defined: "
53                            "%d bytes instead of %d bytes" % (calculated_length, length)
54                        )
55                    length = calculated_length
56            if subtableFormat not in kern_classes:
57                subtable = KernTable_format_unkown(subtableFormat)
58            else:
59                subtable = kern_classes[subtableFormat](apple)
60            subtable.decompile(data[:length], ttFont)
61            self.kernTables.append(subtable)
62            data = data[length:]
63
64    def compile(self, ttFont):
65        if hasattr(self, "kernTables"):
66            nTables = len(self.kernTables)
67        else:
68            nTables = 0
69        if self.version == 1.0:
70            # AAT Apple's "new" format.
71            data = struct.pack(">LL", fl2fi(self.version, 16), nTables)
72        else:
73            data = struct.pack(">HH", self.version, nTables)
74        if hasattr(self, "kernTables"):
75            for subtable in self.kernTables:
76                data = data + subtable.compile(ttFont)
77        return data
78
79    def toXML(self, writer, ttFont):
80        writer.simpletag("version", value=self.version)
81        writer.newline()
82        for subtable in self.kernTables:
83            subtable.toXML(writer, ttFont)
84
85    def fromXML(self, name, attrs, content, ttFont):
86        if name == "version":
87            self.version = safeEval(attrs["value"])
88            return
89        if name != "kernsubtable":
90            return
91        if not hasattr(self, "kernTables"):
92            self.kernTables = []
93        format = safeEval(attrs["format"])
94        if format not in kern_classes:
95            subtable = KernTable_format_unkown(format)
96        else:
97            apple = self.version == 1.0
98            subtable = kern_classes[format](apple)
99        self.kernTables.append(subtable)
100        subtable.fromXML(name, attrs, content, ttFont)
101
102
103class KernTable_format_0(object):
104    # 'version' is kept for backward compatibility
105    version = format = 0
106
107    def __init__(self, apple=False):
108        self.apple = apple
109
110    def decompile(self, data, ttFont):
111        if not self.apple:
112            version, length, subtableFormat, coverage = struct.unpack(">HHBB", data[:6])
113            if version != 0:
114                from fontTools.ttLib import TTLibError
115
116                raise TTLibError("unsupported kern subtable version: %d" % version)
117            tupleIndex = None
118            # Should we also assert length == len(data)?
119            data = data[6:]
120        else:
121            length, coverage, subtableFormat, tupleIndex = struct.unpack(
122                ">LBBH", data[:8]
123            )
124            data = data[8:]
125        assert self.format == subtableFormat, "unsupported format"
126        self.coverage = coverage
127        self.tupleIndex = tupleIndex
128
129        self.kernTable = kernTable = {}
130
131        nPairs, searchRange, entrySelector, rangeShift = struct.unpack(
132            ">HHHH", data[:8]
133        )
134        data = data[8:]
135
136        datas = array.array("H", data[: 6 * nPairs])
137        if sys.byteorder != "big":
138            datas.byteswap()
139        it = iter(datas)
140        glyphOrder = ttFont.getGlyphOrder()
141        for k in range(nPairs):
142            left, right, value = next(it), next(it), next(it)
143            if value >= 32768:
144                value -= 65536
145            try:
146                kernTable[(glyphOrder[left], glyphOrder[right])] = value
147            except IndexError:
148                # Slower, but will not throw an IndexError on an invalid
149                # glyph id.
150                kernTable[(ttFont.getGlyphName(left), ttFont.getGlyphName(right))] = (
151                    value
152                )
153        if len(data) > 6 * nPairs + 4:  # Ignore up to 4 bytes excess
154            log.warning(
155                "excess data in 'kern' subtable: %d bytes", len(data) - 6 * nPairs
156            )
157
158    def compile(self, ttFont):
159        nPairs = min(len(self.kernTable), 0xFFFF)
160        searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6)
161        searchRange &= 0xFFFF
162        entrySelector = min(entrySelector, 0xFFFF)
163        rangeShift = min(rangeShift, 0xFFFF)
164        data = struct.pack(">HHHH", nPairs, searchRange, entrySelector, rangeShift)
165
166        # yeehee! (I mean, turn names into indices)
167        try:
168            reverseOrder = ttFont.getReverseGlyphMap()
169            kernTable = sorted(
170                (reverseOrder[left], reverseOrder[right], value)
171                for ((left, right), value) in self.kernTable.items()
172            )
173        except KeyError:
174            # Slower, but will not throw KeyError on invalid glyph id.
175            getGlyphID = ttFont.getGlyphID
176            kernTable = sorted(
177                (getGlyphID(left), getGlyphID(right), value)
178                for ((left, right), value) in self.kernTable.items()
179            )
180
181        for left, right, value in kernTable:
182            data = data + struct.pack(">HHh", left, right, value)
183
184        if not self.apple:
185            version = 0
186            length = len(data) + 6
187            if length >= 0x10000:
188                log.warning(
189                    '"kern" subtable overflow, '
190                    "truncating length value while preserving pairs."
191                )
192                length &= 0xFFFF
193            header = struct.pack(">HHBB", version, length, self.format, self.coverage)
194        else:
195            if self.tupleIndex is None:
196                # sensible default when compiling a TTX from an old fonttools
197                # or when inserting a Windows-style format 0 subtable into an
198                # Apple version=1.0 kern table
199                log.warning("'tupleIndex' is None; default to 0")
200                self.tupleIndex = 0
201            length = len(data) + 8
202            header = struct.pack(
203                ">LBBH", length, self.coverage, self.format, self.tupleIndex
204            )
205        return header + data
206
207    def toXML(self, writer, ttFont):
208        attrs = dict(coverage=self.coverage, format=self.format)
209        if self.apple:
210            if self.tupleIndex is None:
211                log.warning("'tupleIndex' is None; default to 0")
212                attrs["tupleIndex"] = 0
213            else:
214                attrs["tupleIndex"] = self.tupleIndex
215        writer.begintag("kernsubtable", **attrs)
216        writer.newline()
217        items = sorted(self.kernTable.items())
218        for (left, right), value in items:
219            writer.simpletag("pair", [("l", left), ("r", right), ("v", value)])
220            writer.newline()
221        writer.endtag("kernsubtable")
222        writer.newline()
223
224    def fromXML(self, name, attrs, content, ttFont):
225        self.coverage = safeEval(attrs["coverage"])
226        subtableFormat = safeEval(attrs["format"])
227        if self.apple:
228            if "tupleIndex" in attrs:
229                self.tupleIndex = safeEval(attrs["tupleIndex"])
230            else:
231                # previous fontTools versions didn't export tupleIndex
232                log.warning("Apple kern subtable is missing 'tupleIndex' attribute")
233                self.tupleIndex = None
234        else:
235            self.tupleIndex = None
236        assert subtableFormat == self.format, "unsupported format"
237        if not hasattr(self, "kernTable"):
238            self.kernTable = {}
239        for element in content:
240            if not isinstance(element, tuple):
241                continue
242            name, attrs, content = element
243            self.kernTable[(attrs["l"], attrs["r"])] = safeEval(attrs["v"])
244
245    def __getitem__(self, pair):
246        return self.kernTable[pair]
247
248    def __setitem__(self, pair, value):
249        self.kernTable[pair] = value
250
251    def __delitem__(self, pair):
252        del self.kernTable[pair]
253
254
255class KernTable_format_unkown(object):
256    def __init__(self, format):
257        self.format = format
258
259    def decompile(self, data, ttFont):
260        self.data = data
261
262    def compile(self, ttFont):
263        return self.data
264
265    def toXML(self, writer, ttFont):
266        writer.begintag("kernsubtable", format=self.format)
267        writer.newline()
268        writer.comment("unknown 'kern' subtable format")
269        writer.newline()
270        writer.dumphex(self.data)
271        writer.endtag("kernsubtable")
272        writer.newline()
273
274    def fromXML(self, name, attrs, content, ttFont):
275        self.decompile(readHex(content), ttFont)
276
277
278kern_classes = {0: KernTable_format_0}
279