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