otTables.py revision a138467da399c071dbd78682938642b8e8d8a4ce
1"""fontTools.ttLib.tables.otTables -- A collection of classes representing the various
2OpenType subtables.
3
4Most are constructed upon import from data in otData.py, all are populated with
5converter objects from otConverters.py.
6"""
7
8from otBase import BaseTable, FormatSwitchingBaseTable
9from types import TupleType
10
11
12class LookupOrder(BaseTable):
13	"""Dummy class; this table isn't defined, but is used, and is always NULL."""
14
15
16class FeatureParams(BaseTable):
17	"""Dummy class; this table isn't defined, but is used, and is always NULL."""
18	# XXX The above is no longer true; the 'size' feature uses FeatureParams now.
19
20
21class Coverage(FormatSwitchingBaseTable):
22
23	# manual implementation to get rid of glyphID dependencies
24
25	def postRead(self, rawTable, font):
26		if self.Format == 1:
27			self.glyphs = rawTable["GlyphArray"]
28		elif self.Format == 2:
29			glyphs = self.glyphs = []
30			ranges = rawTable["RangeRecord"]
31			for r in ranges:
32				assert r.StartCoverageIndex == len(glyphs), \
33					(r.StartCoverageIndex, len(glyphs))
34				start = r.Start
35				end = r.End
36				startID = font.getGlyphID(start)
37				endID = font.getGlyphID(end)
38				glyphs.append(start)
39				for glyphID in range(startID + 1, endID):
40					glyphs.append(font.getGlyphName(glyphID))
41				if start != end:
42					glyphs.append(end)
43		else:
44			assert 0, "unknown format: %s" % self.Format
45
46	def preWrite(self, font):
47		glyphs = getattr(self, "glyphs", [])
48		format = 1
49		rawTable = {"GlyphArray": glyphs}
50		if glyphs:
51			# find out whether Format 2 is more compact or not
52			glyphIDs = []
53			for glyphName in glyphs:
54				glyphIDs.append(font.getGlyphID(glyphName))
55
56			last = glyphIDs[0]
57			ranges = [[last]]
58			for glyphID in glyphIDs[1:]:
59				if glyphID != last + 1:
60					ranges[-1].append(last)
61					ranges.append([glyphID])
62				last = glyphID
63			ranges[-1].append(last)
64
65			if len(ranges) * 3 < len(glyphs):  # 3 words vs. 1 word
66				# Format 2 is more compact
67				index = 0
68				for i in range(len(ranges)):
69					start, end = ranges[i]
70					r = RangeRecord()
71					r.Start = font.getGlyphName(start)
72					r.End = font.getGlyphName(end)
73					r.StartCoverageIndex = index
74					ranges[i] = r
75					index = index + end - start + 1
76				format = 2
77				rawTable = {"RangeRecord": ranges}
78			#else:
79			#	fallthrough; Format 1 is more compact
80		self.Format = format
81		return rawTable
82
83	def toXML2(self, xmlWriter, font):
84		for glyphName in getattr(self, "glyphs", []):
85			xmlWriter.simpletag("Glyph", value=glyphName)
86			xmlWriter.newline()
87
88	def fromXML(self, (name, attrs, content), font):
89		glyphs = getattr(self, "glyphs", None)
90		if glyphs is None:
91			glyphs = []
92			self.glyphs = glyphs
93		glyphs.append(attrs["value"])
94
95
96class SingleSubst(FormatSwitchingBaseTable):
97
98	def postRead(self, rawTable, font):
99		mapping = {}
100		input = rawTable["Coverage"].glyphs
101		if self.Format == 1:
102			delta = rawTable["DeltaGlyphID"]
103			for inGlyph in input:
104				glyphID = font.getGlyphID(inGlyph)
105				mapping[inGlyph] = font.getGlyphName(glyphID + delta)
106		elif self.Format == 2:
107			assert len(input) == rawTable["GlyphCount"], \
108					"invalid SingleSubstFormat2 table"
109			subst = rawTable["Substitute"]
110			for i in range(len(input)):
111				mapping[input[i]] = subst[i]
112		else:
113			assert 0, "unknown format: %s" % self.Format
114		self.mapping = mapping
115
116	def preWrite(self, font):
117		items = self.mapping.items()
118		for i in range(len(items)):
119			inGlyph, outGlyph = items[i]
120			items[i] = font.getGlyphID(inGlyph), font.getGlyphID(outGlyph), \
121					inGlyph, outGlyph
122		items.sort()
123
124		format = 2
125		delta = None
126		for inID, outID, inGlyph, outGlyph in items:
127			if delta is None:
128				delta = outID - inID
129			else:
130				if delta != outID - inID:
131					break
132		else:
133			format = 1
134
135		rawTable = {}
136		self.Format = format
137		cov = Coverage()
138		cov.glyphs = input = []
139		subst = []
140		for inID, outID, inGlyph, outGlyph in items:
141			input.append(inGlyph)
142			subst.append(outGlyph)
143		rawTable["Coverage"] = cov
144		if format == 1:
145			assert delta is not None
146			rawTable["DeltaGlyphID"] = delta
147		else:
148			rawTable["Substitute"] = subst
149		return rawTable
150
151	def toXML2(self, xmlWriter, font):
152		items = self.mapping.items()
153		items.sort()
154		for inGlyph, outGlyph in items:
155			xmlWriter.simpletag("Substitution",
156					[("in", inGlyph), ("out", outGlyph)])
157			xmlWriter.newline()
158
159	def fromXML(self, (name, attrs, content), font):
160		mapping = getattr(self, "mapping", None)
161		if mapping is None:
162			mapping = {}
163			self.mapping = mapping
164		mapping[attrs["in"]] = attrs["out"]
165
166
167class ClassDef(FormatSwitchingBaseTable):
168
169	def postRead(self, rawTable, font):
170		classDefs = {}
171		if self.Format == 1:
172			start = rawTable["StartGlyph"]
173			glyphID = font.getGlyphID(start)
174			for cls in rawTable["ClassValueArray"]:
175				classDefs[font.getGlyphName(glyphID)] = cls
176				glyphID = glyphID + 1
177		elif self.Format == 2:
178			records = rawTable["ClassRangeRecord"]
179			for rec in records:
180				start = rec.Start
181				end = rec.End
182				cls = rec.Class
183				classDefs[start] = cls
184				for glyphID in range(font.getGlyphID(start) + 1,
185						font.getGlyphID(end)):
186					classDefs[font.getGlyphName(glyphID)] = cls
187				classDefs[end] = cls
188		else:
189			assert 0, "unknown format: %s" % self.Format
190		self.classDefs = classDefs
191
192	def preWrite(self, font):
193		items = self.classDefs.items()
194		for i in range(len(items)):
195			glyphName, cls = items[i]
196			items[i] = font.getGlyphID(glyphName), glyphName, cls
197		items.sort()
198		last, lastName, lastCls = items[0]
199		rec = ClassRangeRecord()
200		rec.Start = lastName
201		rec.Class = lastCls
202		ranges = [rec]
203		for glyphID, glyphName, cls in items[1:]:
204			if glyphID != last + 1 or cls != lastCls:
205				rec.End = lastName
206				rec = ClassRangeRecord()
207				rec.Start = glyphName
208				rec.Class = cls
209				ranges.append(rec)
210			last = glyphID
211			lastName = glyphName
212			lastCls = cls
213		rec.End = lastName
214		self.Format = 2  # currently no support for Format 1
215		return {"ClassRangeRecord": ranges}
216
217	def toXML2(self, xmlWriter, font):
218		items = self.classDefs.items()
219		items.sort()
220		for glyphName, cls in items:
221			xmlWriter.simpletag("ClassDef", [("glyph", glyphName), ("class", cls)])
222			xmlWriter.newline()
223
224	def fromXML(self, (name, attrs, content), font):
225		classDefs = getattr(self, "classDefs", None)
226		if classDefs is None:
227			classDefs = {}
228			self.classDefs = classDefs
229		classDefs[attrs["glyph"]] = int(attrs["class"])
230
231
232class AlternateSubst(FormatSwitchingBaseTable):
233
234	def postRead(self, rawTable, font):
235		alternates = {}
236		if self.Format == 1:
237			input = rawTable["Coverage"].glyphs
238			alts = rawTable["AlternateSet"]
239			assert len(input) == len(alts)
240			for i in range(len(input)):
241				alternates[input[i]] = alts[i].Alternate
242		else:
243			assert 0, "unknown format: %s" % self.Format
244		self.alternates = alternates
245
246	def preWrite(self, font):
247		self.Format = 1
248		items = self.alternates.items()
249		for i in range(len(items)):
250			glyphName, set = items[i]
251			items[i] = font.getGlyphID(glyphName), glyphName, set
252		items.sort()
253		cov = Coverage()
254		glyphs = []
255		alternates = []
256		cov.glyphs = glyphs
257		for glyphID, glyphName, set in items:
258			glyphs.append(glyphName)
259			alts = AlternateSet()
260			alts.Alternate = set
261			alternates.append(alts)
262		return {"Coverage": cov, "AlternateSet": alternates}
263
264	def toXML2(self, xmlWriter, font):
265		items = self.alternates.items()
266		items.sort()
267		for glyphName, alternates in items:
268			xmlWriter.begintag("AlternateSet", glyph=glyphName)
269			xmlWriter.newline()
270			for alt in alternates:
271				xmlWriter.simpletag("Alternate", glyph=alt)
272				xmlWriter.newline()
273			xmlWriter.endtag("AlternateSet")
274			xmlWriter.newline()
275
276	def fromXML(self, (name, attrs, content), font):
277		alternates = getattr(self, "alternates", None)
278		if alternates is None:
279			alternates = {}
280			self.alternates = alternates
281		glyphName = attrs["glyph"]
282		set = []
283		alternates[glyphName] = set
284		for element in content:
285			if type(element) != TupleType:
286				continue
287			name, attrs, content = element
288			set.append(attrs["glyph"])
289
290
291class LigatureSubst(FormatSwitchingBaseTable):
292
293	def postRead(self, rawTable, font):
294		ligatures = {}
295		if self.Format == 1:
296			input = rawTable["Coverage"].glyphs
297			ligSets = rawTable["LigatureSet"]
298			assert len(input) == len(ligSets)
299			for i in range(len(input)):
300				ligatures[input[i]] = ligSets[i].Ligature
301		else:
302			assert 0, "unknown format: %s" % self.Format
303		self.ligatures = ligatures
304
305	def preWrite(self, font):
306		self.Format = 1
307		items = self.ligatures.items()
308		for i in range(len(items)):
309			glyphName, set = items[i]
310			items[i] = font.getGlyphID(glyphName), glyphName, set
311		items.sort()
312		glyphs = []
313		cov = Coverage()
314		cov.glyphs = glyphs
315		ligSets = []
316		for glyphID, glyphName, set in items:
317			glyphs.append(glyphName)
318			ligSet = LigatureSet()
319			ligs = ligSet.Ligature = []
320			for lig in set:
321				ligs.append(lig)
322			ligSets.append(ligSet)
323		return {"Coverage": cov, "LigatureSet": ligSets}
324
325	def toXML2(self, xmlWriter, font):
326		items = self.ligatures.items()
327		items.sort()
328		for glyphName, ligSets in items:
329			xmlWriter.begintag("LigatureSet", glyph=glyphName)
330			xmlWriter.newline()
331			for lig in ligSets:
332				xmlWriter.simpletag("Ligature", glyph=lig.LigGlyph,
333					components=",".join(lig.Component))
334				xmlWriter.newline()
335			xmlWriter.endtag("LigatureSet")
336			xmlWriter.newline()
337
338	def fromXML(self, (name, attrs, content), font):
339		ligatures = getattr(self, "ligatures", None)
340		if ligatures is None:
341			ligatures = {}
342			self.ligatures = ligatures
343		glyphName = attrs["glyph"]
344		ligs = []
345		ligatures[glyphName] = ligs
346		for element in content:
347			if type(element) != TupleType:
348				continue
349			name, attrs, content = element
350			lig = Ligature()
351			lig.LigGlyph = attrs["glyph"]
352			lig.Component = attrs["components"].split(",")
353			ligs.append(lig)
354
355
356#
357# For each subtable format there is a class. However, we don't really distinguish
358# between "field name" and "format name": often these are the same. Yet there's
359# a whole bunch of fields with different names. The following dict is a mapping
360# from "format name" to "field name". _buildClasses() uses this to create a
361# subclass for each alternate field name.
362#
363_equivalents = {
364	'MarkArray': ("Mark1Array",),
365	'LangSys': ('DefaultLangSys',),
366	'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage',
367			'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage',
368			'LookaheadCoverage'),
369	'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef',
370			'LookaheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'),
371	'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor',
372			'Mark2Anchor', 'MarkAnchor'),
373	'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice',
374			'XDeviceTable', 'YDeviceTable', 'DeviceTable'),
375	'Axis': ('HorizAxis', 'VertAxis',),
376	'MinMax': ('DefaultMinMax',),
377	'BaseCoord': ('MinCoord', 'MaxCoord',),
378	'JstfLangSys': ('DefJstfLangSys',),
379	'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB',
380			'ExtensionDisableGSUB',),
381	'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS',
382			'ExtensionDisableGPOS',),
383	'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',),
384}
385
386
387def _buildClasses():
388	import new, re
389	from otData import otData
390
391	formatPat = re.compile("([A-Za-z0-9]+)Format(\d+)$")
392	namespace = globals()
393
394	# populate module with classes
395	for name, table in otData:
396		baseClass = BaseTable
397		m = formatPat.match(name)
398		if m:
399			# XxxFormatN subtable, we only add the "base" table
400			name = m.group(1)
401			baseClass = FormatSwitchingBaseTable
402		if not namespace.has_key(name):
403			# the class doesn't exist yet, so the base implementation is used.
404			cls = new.classobj(name, (baseClass,), {})
405			namespace[name] = cls
406
407	for base, alts in _equivalents.items():
408		base = namespace[base]
409		for alt in alts:
410			namespace[alt] = new.classobj(alt, (base,), {})
411
412	global lookupTypes
413	lookupTypes = {
414		'GSUB': {
415			1: SingleSubst,
416			2: MultipleSubst,
417			3: AlternateSubst,
418			4: LigatureSubst,
419			5: ContextSubst,
420			6: ChainContextSubst,
421			7: ExtensionSubst,
422		},
423		'GPOS': {
424			1: SinglePos,
425			2: PairPos,
426			3: CursivePos,
427			4: MarkBasePos,
428			5: MarkLigPos,
429			6: MarkMarkPos,
430			7: ContextPos,
431			8: ChainContextPos,
432			9: ExtensionPos,
433		},
434	}
435	lookupTypes['JSTF'] = lookupTypes['GPOS']  # JSTF contains GPOS
436	for lookupEnum in lookupTypes.values():
437		for enum, cls in lookupEnum.items():
438			cls.LookupType = enum
439
440	# add converters to classes
441	from otConverters import buildConverters
442	for name, table in otData:
443		m = formatPat.match(name)
444		if m:
445			# XxxFormatN subtable, add converter to "base" table
446			name, format = m.groups()
447			format = int(format)
448			cls = namespace[name]
449			if not hasattr(cls, "converters"):
450				cls.converters = {}
451				cls.convertersByName = {}
452			converters, convertersByName = buildConverters(table[1:], namespace)
453			cls.converters[format] = converters
454			cls.convertersByName[format] = convertersByName
455		else:
456			cls = namespace[name]
457			cls.converters, cls.convertersByName = buildConverters(table, namespace)
458
459
460_buildClasses()
461