otTables.py revision b2486125e9438443a1419706c50958ab0676eb5a
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
290#
291# For each subtable format there is a class. However, we don't really distinguish
292# between "field name" and "format name": often these are the same. Yet there's
293# a whole bunch of fields with different names. The following dict is a mapping
294# from "format name" to "field name". _buildClasses() uses this to create a
295# subclass for each alternate field name.
296#
297_equivalents = {
298	'MarkArray': ("Mark1Array",),
299	'LangSys': ('DefaultLangSys',),
300	'Coverage': ('MarkCoverage', 'BaseCoverage', 'LigatureCoverage', 'Mark1Coverage',
301			'Mark2Coverage', 'BacktrackCoverage', 'InputCoverage',
302			'LookaheadCoverage'),
303	'ClassDef': ('ClassDef1', 'ClassDef2', 'BacktrackClassDef', 'InputClassDef',
304			'LookaheadClassDef', 'GlyphClassDef', 'MarkAttachClassDef'),
305	'Anchor': ('EntryAnchor', 'ExitAnchor', 'BaseAnchor', 'LigatureAnchor',
306			'Mark2Anchor', 'MarkAnchor'),
307	'Device': ('XPlaDevice', 'YPlaDevice', 'XAdvDevice', 'YAdvDevice',
308			'XDeviceTable', 'YDeviceTable', 'DeviceTable'),
309	'Axis': ('HorizAxis', 'VertAxis',),
310	'MinMax': ('DefaultMinMax',),
311	'BaseCoord': ('MinCoord', 'MaxCoord',),
312	'JstfLangSys': ('DefJstfLangSys',),
313	'JstfGSUBModList': ('ShrinkageEnableGSUB', 'ShrinkageDisableGSUB', 'ExtensionEnableGSUB',
314			'ExtensionDisableGSUB',),
315	'JstfGPOSModList': ('ShrinkageEnableGPOS', 'ShrinkageDisableGPOS', 'ExtensionEnableGPOS',
316			'ExtensionDisableGPOS',),
317	'JstfMax': ('ShrinkageJstfMax', 'ExtensionJstfMax',),
318}
319
320
321def _buildClasses():
322	import new, re
323	from otData import otData
324
325	formatPat = re.compile("([A-Za-z0-9]+)Format(\d+)$")
326	namespace = globals()
327
328	# populate module with classes
329	for name, table in otData:
330		baseClass = BaseTable
331		m = formatPat.match(name)
332		if m:
333			# XxxFormatN subtable, we only add the "base" table
334			name = m.group(1)
335			baseClass = FormatSwitchingBaseTable
336		if not namespace.has_key(name):
337			# the class doesn't exist yet, so the base implementation is used.
338			cls = new.classobj(name, (baseClass,), {})
339			namespace[name] = cls
340
341	for base, alts in _equivalents.items():
342		base = namespace[base]
343		for alt in alts:
344			namespace[alt] = new.classobj(alt, (base,), {})
345
346	global lookupTypes
347	lookupTypes = {
348		'GSUB': {
349			1: SingleSubst,
350			2: MultipleSubst,
351			3: AlternateSubst,
352			4: LigatureSubst,
353			5: ContextSubst,
354			6: ChainContextSubst,
355			7: ExtensionSubst,
356		},
357		'GPOS': {
358			1: SinglePos,
359			2: PairPos,
360			3: CursivePos,
361			4: MarkBasePos,
362			5: MarkLigPos,
363			6: MarkMarkPos,
364			7: ContextPos,
365			8: ChainContextPos,
366			9: ExtensionPos,
367		},
368	}
369	lookupTypes['JSTF'] = lookupTypes['GPOS']  # JSTF contains GPOS
370	for lookupEnum in lookupTypes.values():
371		for enum, cls in lookupEnum.items():
372			cls.LookupType = enum
373
374	# add converters to classes
375	from otConverters import buildConverters
376	for name, table in otData:
377		m = formatPat.match(name)
378		if m:
379			# XxxFormatN subtable, add converter to "base" table
380			name, format = m.groups()
381			format = int(format)
382			cls = namespace[name]
383			if not hasattr(cls, "converters"):
384				cls.converters = {}
385				cls.convertersByName = {}
386			converters, convertersByName = buildConverters(table[1:], namespace)
387			cls.converters[format] = converters
388			cls.convertersByName[format] = convertersByName
389		else:
390			cls = namespace[name]
391			cls.converters, cls.convertersByName = buildConverters(table, namespace)
392
393
394_buildClasses()
395