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