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