1from . import DefaultTable
2from fontTools.misc import sstruct
3from fontTools.misc.textTools import safeEval
4import struct
5
6VDMX_HeaderFmt = """
7	>                 # big endian
8	version:     H    # Version number (0 or 1)
9	numRecs:     H    # Number of VDMX groups present
10	numRatios:   H    # Number of aspect ratio groupings
11"""
12# the VMDX header is followed by an array of RatRange[numRatios] (i.e. aspect
13# ratio ranges);
14VDMX_RatRangeFmt = """
15	>                 # big endian
16	bCharSet:    B    # Character set
17	xRatio:      B    # Value to use for x-Ratio
18	yStartRatio: B    # Starting y-Ratio value
19	yEndRatio:   B    # Ending y-Ratio value
20"""
21# followed by an array of offset[numRatios] from start of VDMX table to the
22# VDMX Group for this ratio range (offsets will be re-calculated on compile);
23# followed by an array of Group[numRecs] records;
24VDMX_GroupFmt = """
25	>                 # big endian
26	recs:        H    # Number of height records in this group
27	startsz:     B    # Starting yPelHeight
28	endsz:       B    # Ending yPelHeight
29"""
30# followed by an array of vTable[recs] records.
31VDMX_vTableFmt = """
32	>                 # big endian
33	yPelHeight:  H    # yPelHeight to which values apply
34	yMax:        h    # Maximum value (in pels) for this yPelHeight
35	yMin:        h    # Minimum value (in pels) for this yPelHeight
36"""
37
38
39class table_V_D_M_X_(DefaultTable.DefaultTable):
40
41	def decompile(self, data, ttFont):
42		pos = 0  # track current position from to start of VDMX table
43		dummy, data = sstruct.unpack2(VDMX_HeaderFmt, data, self)
44		pos += sstruct.calcsize(VDMX_HeaderFmt)
45		self.ratRanges = []
46		for i in range(self.numRatios):
47			ratio, data = sstruct.unpack2(VDMX_RatRangeFmt, data)
48			pos += sstruct.calcsize(VDMX_RatRangeFmt)
49			# the mapping between a ratio and a group is defined further below
50			ratio['groupIndex'] = None
51			self.ratRanges.append(ratio)
52		lenOffset = struct.calcsize('>H')
53		_offsets = []  # temporarily store offsets to groups
54		for i in range(self.numRatios):
55			offset = struct.unpack('>H', data[0:lenOffset])[0]
56			data = data[lenOffset:]
57			pos += lenOffset
58			_offsets.append(offset)
59		self.groups = []
60		for groupIndex in range(self.numRecs):
61			# the offset to this group from beginning of the VDMX table
62			currOffset = pos
63			group, data = sstruct.unpack2(VDMX_GroupFmt, data)
64			# the group lenght and bounding sizes are re-calculated on compile
65			recs = group.pop('recs')
66			startsz = group.pop('startsz')
67			endsz = group.pop('endsz')
68			pos += sstruct.calcsize(VDMX_GroupFmt)
69			for j in range(recs):
70				vTable, data = sstruct.unpack2(VDMX_vTableFmt, data)
71				vTableLength = sstruct.calcsize(VDMX_vTableFmt)
72				pos += vTableLength
73				# group is a dict of (yMax, yMin) tuples keyed by yPelHeight
74				group[vTable['yPelHeight']] = (vTable['yMax'], vTable['yMin'])
75			# make sure startsz and endsz match the calculated values
76			minSize = min(group.keys())
77			maxSize = max(group.keys())
78			assert startsz == minSize, \
79				"startsz (%s) must equal min yPelHeight (%s): group %d" % \
80				(group.startsz, minSize, groupIndex)
81			assert endsz == maxSize, \
82				"endsz (%s) must equal max yPelHeight (%s): group %d" % \
83				(group.endsz, maxSize, groupIndex)
84			self.groups.append(group)
85			# match the defined offsets with the current group's offset
86			for offsetIndex, offsetValue in enumerate(_offsets):
87				# when numRecs < numRatios there can more than one ratio range
88				# sharing the same VDMX group
89				if currOffset == offsetValue:
90					# map the group with the ratio range thas has the same
91					# index as the offset to that group (it took me a while..)
92					self.ratRanges[offsetIndex]['groupIndex'] = groupIndex
93		# check that all ratio ranges have a group
94		for i in range(self.numRatios):
95			ratio = self.ratRanges[i]
96			if ratio['groupIndex'] is None:
97				from fontTools import ttLib
98				raise ttLib.TTLibError(
99					"no group defined for ratRange %d" % i)
100
101	def _getOffsets(self):
102		"""
103		Calculate offsets to VDMX_Group records.
104		For each ratRange return a list of offset values from the beginning of
105		the VDMX table to a VDMX_Group.
106		"""
107		lenHeader = sstruct.calcsize(VDMX_HeaderFmt)
108		lenRatRange = sstruct.calcsize(VDMX_RatRangeFmt)
109		lenOffset = struct.calcsize('>H')
110		lenGroupHeader = sstruct.calcsize(VDMX_GroupFmt)
111		lenVTable = sstruct.calcsize(VDMX_vTableFmt)
112		# offset to the first group
113		pos = lenHeader + self.numRatios*lenRatRange + self.numRatios*lenOffset
114		groupOffsets = []
115		for group in self.groups:
116			groupOffsets.append(pos)
117			lenGroup = lenGroupHeader + len(group) * lenVTable
118			pos += lenGroup  # offset to next group
119		offsets = []
120		for ratio in self.ratRanges:
121			groupIndex = ratio['groupIndex']
122			offsets.append(groupOffsets[groupIndex])
123		return offsets
124
125	def compile(self, ttFont):
126		if not(self.version == 0 or self.version == 1):
127			from fontTools import ttLib
128			raise ttLib.TTLibError(
129				"unknown format for VDMX table: version %s" % self.version)
130		data = sstruct.pack(VDMX_HeaderFmt, self)
131		for ratio in self.ratRanges:
132			data += sstruct.pack(VDMX_RatRangeFmt, ratio)
133		# recalculate offsets to VDMX groups
134		for offset in self._getOffsets():
135			data += struct.pack('>H', offset)
136		for group in self.groups:
137			recs = len(group)
138			startsz = min(group.keys())
139			endsz = max(group.keys())
140			gHeader = {'recs': recs, 'startsz': startsz, 'endsz': endsz}
141			data += sstruct.pack(VDMX_GroupFmt, gHeader)
142			for yPelHeight, (yMax, yMin) in sorted(group.items()):
143				vTable = {'yPelHeight': yPelHeight, 'yMax': yMax, 'yMin': yMin}
144				data += sstruct.pack(VDMX_vTableFmt, vTable)
145		return data
146
147	def toXML(self, writer, ttFont):
148		writer.simpletag("version", value=self.version)
149		writer.newline()
150		writer.begintag("ratRanges")
151		writer.newline()
152		for ratio in self.ratRanges:
153			groupIndex = ratio['groupIndex']
154			writer.simpletag(
155				"ratRange",
156				bCharSet=ratio['bCharSet'],
157				xRatio=ratio['xRatio'],
158				yStartRatio=ratio['yStartRatio'],
159				yEndRatio=ratio['yEndRatio'],
160				groupIndex=groupIndex
161				)
162			writer.newline()
163		writer.endtag("ratRanges")
164		writer.newline()
165		writer.begintag("groups")
166		writer.newline()
167		for groupIndex in range(self.numRecs):
168			group = self.groups[groupIndex]
169			recs = len(group)
170			startsz = min(group.keys())
171			endsz = max(group.keys())
172			writer.begintag("group", index=groupIndex)
173			writer.newline()
174			writer.comment("recs=%d, startsz=%d, endsz=%d" %
175							(recs, startsz, endsz))
176			writer.newline()
177			for yPelHeight, (yMax, yMin) in sorted(group.items()):
178				writer.simpletag(
179					"record",
180					[('yPelHeight', yPelHeight), ('yMax', yMax), ('yMin', yMin)])
181				writer.newline()
182			writer.endtag("group")
183			writer.newline()
184		writer.endtag("groups")
185		writer.newline()
186
187	def fromXML(self, name, attrs, content, ttFont):
188		if name == "version":
189			self.version = safeEval(attrs["value"])
190		elif name == "ratRanges":
191			if not hasattr(self, "ratRanges"):
192				self.ratRanges = []
193			for element in content:
194				if not isinstance(element, tuple):
195					continue
196				name, attrs, content = element
197				if name == "ratRange":
198					if not hasattr(self, "numRatios"):
199						self.numRatios = 1
200					else:
201						self.numRatios += 1
202					ratio = {
203						"bCharSet": safeEval(attrs["bCharSet"]),
204						"xRatio": safeEval(attrs["xRatio"]),
205						"yStartRatio": safeEval(attrs["yStartRatio"]),
206						"yEndRatio": safeEval(attrs["yEndRatio"]),
207						"groupIndex": safeEval(attrs["groupIndex"])
208						}
209					self.ratRanges.append(ratio)
210		elif name == "groups":
211			if not hasattr(self, "groups"):
212				self.groups = []
213			for element in content:
214				if not isinstance(element, tuple):
215					continue
216				name, attrs, content = element
217				if name == "group":
218					if not hasattr(self, "numRecs"):
219						self.numRecs = 1
220					else:
221						self.numRecs += 1
222					group = {}
223					for element in content:
224						if not isinstance(element, tuple):
225							continue
226						name, attrs, content = element
227						if name == "record":
228							yPelHeight = safeEval(attrs["yPelHeight"])
229							yMax = safeEval(attrs["yMax"])
230							yMin = safeEval(attrs["yMin"])
231							group[yPelHeight] = (yMax, yMin)
232					self.groups.append(group)
233