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