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