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