Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_p_o_s_t.py: 12%
222 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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
11log = logging.getLogger(__name__)
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"""
26postFormatSize = sstruct.calcsize(postFormat)
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 )
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
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
75 def decode_format_1_0(self, data, ttFont):
76 self.glyphOrder = standardGlyphOrder[: ttFont["maxp"].numGlyphs]
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)
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)
124 self.glyphOrder[i] = glyphName
125 allNames[glyphName] = 1
126 if glyphName != psName:
127 mapping[glyphName] = psName
129 self.mapping = mapping
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
136 def decode_format_4_0(self, data, ttFont):
137 from fontTools import agl
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)
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 )
188 def encode_format_4_0(self, ttFont):
189 from fontTools import agl
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()
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()
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)
273def unpackPStrings(data, n):
274 # extract n Pascal strings from data.
275 # if there is not enough data, use ""
277 strings = []
278 index = 0
279 dataLen = len(data)
281 for _ in range(n):
282 if dataLen <= index:
283 length = 0
284 else:
285 length = byteord(data[index])
286 index += 1
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
295 if index < dataLen:
296 log.warning("%d extra bytes in post.stringData array", dataLen - index)
298 elif dataLen < index:
299 log.warning("not enough data in post.stringData array")
301 return strings
304def packPStrings(strings):
305 data = b""
306 for s in strings:
307 data = data + bytechr(len(s)) + tobytes(s, encoding="latin1")
308 return data