xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/tables/_p_o_s_t.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools import ttLib
2from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder
3from fontTools.misc import sstruct
4from fontTools.misc.textTools import bytechr, byteord, tobytes, tostr, safeEval, readHex
5from . import DefaultTable
6import sys
7import struct
8import array
9import logging
10
11log = logging.getLogger(__name__)
12
13postFormat = """
14	>
15	formatType:			16.16F
16	italicAngle:		16.16F		# italic angle in degrees
17	underlinePosition:	h
18	underlineThickness:	h
19	isFixedPitch:		L
20	minMemType42:		L			# minimum memory if TrueType font is downloaded
21	maxMemType42:		L			# maximum memory if TrueType font is downloaded
22	minMemType1:		L			# minimum memory if Type1 font is downloaded
23	maxMemType1:		L			# maximum memory if Type1 font is downloaded
24"""
25
26postFormatSize = sstruct.calcsize(postFormat)
27
28
29class table__p_o_s_t(DefaultTable.DefaultTable):
30    def decompile(self, data, ttFont):
31        sstruct.unpack(postFormat, data[:postFormatSize], self)
32        data = data[postFormatSize:]
33        if self.formatType == 1.0:
34            self.decode_format_1_0(data, ttFont)
35        elif self.formatType == 2.0:
36            self.decode_format_2_0(data, ttFont)
37        elif self.formatType == 3.0:
38            self.decode_format_3_0(data, ttFont)
39        elif self.formatType == 4.0:
40            self.decode_format_4_0(data, ttFont)
41        else:
42            # supported format
43            raise ttLib.TTLibError(
44                "'post' table format %f not supported" % self.formatType
45            )
46
47    def compile(self, ttFont):
48        data = sstruct.pack(postFormat, self)
49        if self.formatType == 1.0:
50            pass  # we're done
51        elif self.formatType == 2.0:
52            data = data + self.encode_format_2_0(ttFont)
53        elif self.formatType == 3.0:
54            pass  # we're done
55        elif self.formatType == 4.0:
56            data = data + self.encode_format_4_0(ttFont)
57        else:
58            # supported format
59            raise ttLib.TTLibError(
60                "'post' table format %f not supported" % self.formatType
61            )
62        return data
63
64    def getGlyphOrder(self):
65        """This function will get called by a ttLib.TTFont instance.
66        Do not call this function yourself, use TTFont().getGlyphOrder()
67        or its relatives instead!
68        """
69        if not hasattr(self, "glyphOrder"):
70            raise ttLib.TTLibError("illegal use of getGlyphOrder()")
71        glyphOrder = self.glyphOrder
72        del self.glyphOrder
73        return glyphOrder
74
75    def decode_format_1_0(self, data, ttFont):
76        self.glyphOrder = standardGlyphOrder[: ttFont["maxp"].numGlyphs]
77
78    def decode_format_2_0(self, data, ttFont):
79        (numGlyphs,) = struct.unpack(">H", data[:2])
80        numGlyphs = int(numGlyphs)
81        if numGlyphs > ttFont["maxp"].numGlyphs:
82            # Assume the numGlyphs field is bogus, so sync with maxp.
83            # I've seen this in one font, and if the assumption is
84            # wrong elsewhere, well, so be it: it's hard enough to
85            # work around _one_ non-conforming post format...
86            numGlyphs = ttFont["maxp"].numGlyphs
87        data = data[2:]
88        indices = array.array("H")
89        indices.frombytes(data[: 2 * numGlyphs])
90        if sys.byteorder != "big":
91            indices.byteswap()
92        data = data[2 * numGlyphs :]
93        maxIndex = max(indices)
94        self.extraNames = extraNames = unpackPStrings(data, maxIndex - 257)
95        self.glyphOrder = glyphOrder = [""] * int(ttFont["maxp"].numGlyphs)
96        for glyphID in range(numGlyphs):
97            index = indices[glyphID]
98            if index > 257:
99                try:
100                    name = extraNames[index - 258]
101                except IndexError:
102                    name = ""
103            else:
104                # fetch names from standard list
105                name = standardGlyphOrder[index]
106            glyphOrder[glyphID] = name
107        self.build_psNameMapping(ttFont)
108
109    def build_psNameMapping(self, ttFont):
110        mapping = {}
111        allNames = {}
112        for i in range(ttFont["maxp"].numGlyphs):
113            glyphName = psName = self.glyphOrder[i]
114            if glyphName == "":
115                glyphName = "glyph%.5d" % i
116            if glyphName in allNames:
117                # make up a new glyphName that's unique
118                n = allNames[glyphName]
119                while (glyphName + "#" + str(n)) in allNames:
120                    n += 1
121                allNames[glyphName] = n + 1
122                glyphName = glyphName + "#" + str(n)
123
124            self.glyphOrder[i] = glyphName
125            allNames[glyphName] = 1
126            if glyphName != psName:
127                mapping[glyphName] = psName
128
129        self.mapping = mapping
130
131    def decode_format_3_0(self, data, ttFont):
132        # Setting self.glyphOrder to None will cause the TTFont object
133        # try and construct glyph names from a Unicode cmap table.
134        self.glyphOrder = None
135
136    def decode_format_4_0(self, data, ttFont):
137        from fontTools import agl
138
139        numGlyphs = ttFont["maxp"].numGlyphs
140        indices = array.array("H")
141        indices.frombytes(data)
142        if sys.byteorder != "big":
143            indices.byteswap()
144        # In some older fonts, the size of the post table doesn't match
145        # the number of glyphs. Sometimes it's bigger, sometimes smaller.
146        self.glyphOrder = glyphOrder = [""] * int(numGlyphs)
147        for i in range(min(len(indices), numGlyphs)):
148            if indices[i] == 0xFFFF:
149                self.glyphOrder[i] = ""
150            elif indices[i] in agl.UV2AGL:
151                self.glyphOrder[i] = agl.UV2AGL[indices[i]]
152            else:
153                self.glyphOrder[i] = "uni%04X" % indices[i]
154        self.build_psNameMapping(ttFont)
155
156    def encode_format_2_0(self, ttFont):
157        numGlyphs = ttFont["maxp"].numGlyphs
158        glyphOrder = ttFont.getGlyphOrder()
159        assert len(glyphOrder) == numGlyphs
160        indices = array.array("H")
161        extraDict = {}
162        extraNames = self.extraNames = [
163            n for n in self.extraNames if n not in standardGlyphOrder
164        ]
165        for i in range(len(extraNames)):
166            extraDict[extraNames[i]] = i
167        for glyphID in range(numGlyphs):
168            glyphName = glyphOrder[glyphID]
169            if glyphName in self.mapping:
170                psName = self.mapping[glyphName]
171            else:
172                psName = glyphName
173            if psName in extraDict:
174                index = 258 + extraDict[psName]
175            elif psName in standardGlyphOrder:
176                index = standardGlyphOrder.index(psName)
177            else:
178                index = 258 + len(extraNames)
179                extraDict[psName] = len(extraNames)
180                extraNames.append(psName)
181            indices.append(index)
182        if sys.byteorder != "big":
183            indices.byteswap()
184        return (
185            struct.pack(">H", numGlyphs) + indices.tobytes() + packPStrings(extraNames)
186        )
187
188    def encode_format_4_0(self, ttFont):
189        from fontTools import agl
190
191        numGlyphs = ttFont["maxp"].numGlyphs
192        glyphOrder = ttFont.getGlyphOrder()
193        assert len(glyphOrder) == numGlyphs
194        indices = array.array("H")
195        for glyphID in glyphOrder:
196            glyphID = glyphID.split("#")[0]
197            if glyphID in agl.AGL2UV:
198                indices.append(agl.AGL2UV[glyphID])
199            elif len(glyphID) == 7 and glyphID[:3] == "uni":
200                indices.append(int(glyphID[3:], 16))
201            else:
202                indices.append(0xFFFF)
203        if sys.byteorder != "big":
204            indices.byteswap()
205        return indices.tobytes()
206
207    def toXML(self, writer, ttFont):
208        formatstring, names, fixes = sstruct.getformat(postFormat)
209        for name in names:
210            value = getattr(self, name)
211            writer.simpletag(name, value=value)
212            writer.newline()
213        if hasattr(self, "mapping"):
214            writer.begintag("psNames")
215            writer.newline()
216            writer.comment(
217                "This file uses unique glyph names based on the information\n"
218                "found in the 'post' table. Since these names might not be unique,\n"
219                "we have to invent artificial names in case of clashes. In order to\n"
220                "be able to retain the original information, we need a name to\n"
221                "ps name mapping for those cases where they differ. That's what\n"
222                "you see below.\n"
223            )
224            writer.newline()
225            items = sorted(self.mapping.items())
226            for name, psName in items:
227                writer.simpletag("psName", name=name, psName=psName)
228                writer.newline()
229            writer.endtag("psNames")
230            writer.newline()
231        if hasattr(self, "extraNames"):
232            writer.begintag("extraNames")
233            writer.newline()
234            writer.comment(
235                "following are the name that are not taken from the standard Mac glyph order"
236            )
237            writer.newline()
238            for name in self.extraNames:
239                writer.simpletag("psName", name=name)
240                writer.newline()
241            writer.endtag("extraNames")
242            writer.newline()
243        if hasattr(self, "data"):
244            writer.begintag("hexdata")
245            writer.newline()
246            writer.dumphex(self.data)
247            writer.endtag("hexdata")
248            writer.newline()
249
250    def fromXML(self, name, attrs, content, ttFont):
251        if name not in ("psNames", "extraNames", "hexdata"):
252            setattr(self, name, safeEval(attrs["value"]))
253        elif name == "psNames":
254            self.mapping = {}
255            for element in content:
256                if not isinstance(element, tuple):
257                    continue
258                name, attrs, content = element
259                if name == "psName":
260                    self.mapping[attrs["name"]] = attrs["psName"]
261        elif name == "extraNames":
262            self.extraNames = []
263            for element in content:
264                if not isinstance(element, tuple):
265                    continue
266                name, attrs, content = element
267                if name == "psName":
268                    self.extraNames.append(attrs["name"])
269        else:
270            self.data = readHex(content)
271
272
273def unpackPStrings(data, n):
274    # extract n Pascal strings from data.
275    # if there is not enough data, use ""
276
277    strings = []
278    index = 0
279    dataLen = len(data)
280
281    for _ in range(n):
282        if dataLen <= index:
283            length = 0
284        else:
285            length = byteord(data[index])
286        index += 1
287
288        if dataLen <= index + length - 1:
289            name = ""
290        else:
291            name = tostr(data[index : index + length], encoding="latin1")
292        strings.append(name)
293        index += length
294
295    if index < dataLen:
296        log.warning("%d extra bytes in post.stringData array", dataLen - index)
297
298    elif dataLen < index:
299        log.warning("not enough data in post.stringData array")
300
301    return strings
302
303
304def packPStrings(strings):
305    data = b""
306    for s in strings:
307        data = data + bytechr(len(s)) + tobytes(s, encoding="latin1")
308    return data
309