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