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