subset.py revision b61d7ac5530bfe452dfdb834208990505a321689
1# Copyright 2013 Google, Inc. All Rights Reserved. 2# 3# Google Author(s): Behdad Esfahbod 4 5"""Python OpenType Layout Subsetter. 6 7Later grown into full OpenType subsetter, supporting all standard tables. 8""" 9 10import sys 11import struct 12import time 13import array 14 15import fontTools.ttLib 16import fontTools.ttLib.tables 17import fontTools.ttLib.tables.otTables 18import fontTools.cffLib 19import fontTools.misc.psCharStrings 20import fontTools.pens.basePen 21 22 23def _add_method(*clazzes): 24 """Returns a decorator function that adds a new method to one or 25 more classes.""" 26 def wrapper(method): 27 for clazz in clazzes: 28 assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.' 29 assert not hasattr(clazz, method.func_name), \ 30 "Oops, class '%s' has method '%s'." % (clazz.__name__, 31 method.func_name) 32 setattr(clazz, method.func_name, method) 33 return None 34 return wrapper 35 36def _uniq_sort(l): 37 return sorted(set(l)) 38 39def _set_update(s, *others): 40 # Jython's set.update only takes one other argument. 41 # Emulate real set.update... 42 for other in others: 43 s.update(other) 44 45 46@_add_method(fontTools.ttLib.tables.otTables.Coverage) 47def intersect(self, glyphs): 48 "Returns ascending list of matching coverage values." 49 return [i for i,g in enumerate(self.glyphs) if g in glyphs] 50 51@_add_method(fontTools.ttLib.tables.otTables.Coverage) 52def intersect_glyphs(self, glyphs): 53 "Returns set of intersecting glyphs." 54 return set(g for g in self.glyphs if g in glyphs) 55 56@_add_method(fontTools.ttLib.tables.otTables.Coverage) 57def subset(self, glyphs): 58 "Returns ascending list of remaining coverage values." 59 indices = self.intersect(glyphs) 60 self.glyphs = [g for g in self.glyphs if g in glyphs] 61 return indices 62 63@_add_method(fontTools.ttLib.tables.otTables.Coverage) 64def remap(self, coverage_map): 65 "Remaps coverage." 66 self.glyphs = [self.glyphs[i] for i in coverage_map] 67 68@_add_method(fontTools.ttLib.tables.otTables.ClassDef) 69def intersect(self, glyphs): 70 "Returns ascending list of matching class values." 71 return _uniq_sort( 72 ([0] if any(g not in self.classDefs for g in glyphs) else []) + 73 [v for g,v in self.classDefs.iteritems() if g in glyphs]) 74 75@_add_method(fontTools.ttLib.tables.otTables.ClassDef) 76def intersect_class(self, glyphs, klass): 77 "Returns set of glyphs matching class." 78 if klass == 0: 79 return set(g for g in glyphs if g not in self.classDefs) 80 return set(g for g,v in self.classDefs.iteritems() 81 if v == klass and g in glyphs) 82 83@_add_method(fontTools.ttLib.tables.otTables.ClassDef) 84def subset(self, glyphs, remap=False): 85 "Returns ascending list of remaining classes." 86 self.classDefs = dict((g,v) for g,v in self.classDefs.iteritems() if g in glyphs) 87 # Note: while class 0 has the special meaning of "not matched", 88 # if no glyph will ever /not match/, we can optimize class 0 out too. 89 indices = _uniq_sort( 90 ([0] if any(g not in self.classDefs for g in glyphs) else []) + 91 self.classDefs.values()) 92 if remap: 93 self.remap(indices) 94 return indices 95 96@_add_method(fontTools.ttLib.tables.otTables.ClassDef) 97def remap(self, class_map): 98 "Remaps classes." 99 self.classDefs = dict((g,class_map.index(v)) 100 for g,v in self.classDefs.iteritems()) 101 102@_add_method(fontTools.ttLib.tables.otTables.SingleSubst) 103def closure_glyphs(self, s, cur_glyphs=None): 104 if cur_glyphs == None: cur_glyphs = s.glyphs 105 if self.Format in [1, 2]: 106 s.glyphs.update(v for g,v in self.mapping.iteritems() if g in cur_glyphs) 107 else: 108 assert 0, "unknown format: %s" % self.Format 109 110@_add_method(fontTools.ttLib.tables.otTables.SingleSubst) 111def subset_glyphs(self, s): 112 if self.Format in [1, 2]: 113 self.mapping = dict((g,v) for g,v in self.mapping.iteritems() 114 if g in s.glyphs and v in s.glyphs) 115 return bool(self.mapping) 116 else: 117 assert 0, "unknown format: %s" % self.Format 118 119@_add_method(fontTools.ttLib.tables.otTables.MultipleSubst) 120def closure_glyphs(self, s, cur_glyphs=None): 121 if cur_glyphs == None: cur_glyphs = s.glyphs 122 if self.Format == 1: 123 indices = self.Coverage.intersect(cur_glyphs) 124 _set_update(s.glyphs, *(self.Sequence[i].Substitute for i in indices)) 125 else: 126 assert 0, "unknown format: %s" % self.Format 127 128@_add_method(fontTools.ttLib.tables.otTables.MultipleSubst) 129def subset_glyphs(self, s): 130 if self.Format == 1: 131 indices = self.Coverage.subset(s.glyphs) 132 self.Sequence = [self.Sequence[i] for i in indices] 133 # Now drop rules generating glyphs we don't want 134 indices = [i for i,seq in enumerate(self.Sequence) 135 if all(sub in s.glyphs for sub in seq.Substitute)] 136 self.Sequence = [self.Sequence[i] for i in indices] 137 self.Coverage.remap(indices) 138 self.SequenceCount = len(self.Sequence) 139 return bool(self.SequenceCount) 140 else: 141 assert 0, "unknown format: %s" % self.Format 142 143@_add_method(fontTools.ttLib.tables.otTables.AlternateSubst) 144def closure_glyphs(self, s, cur_glyphs=None): 145 if cur_glyphs == None: cur_glyphs = s.glyphs 146 if self.Format == 1: 147 _set_update(s.glyphs, *(vlist for g,vlist in self.alternates.iteritems() 148 if g in cur_glyphs)) 149 else: 150 assert 0, "unknown format: %s" % self.Format 151 152@_add_method(fontTools.ttLib.tables.otTables.AlternateSubst) 153def subset_glyphs(self, s): 154 if self.Format == 1: 155 self.alternates = dict((g,vlist) 156 for g,vlist in self.alternates.iteritems() 157 if g in s.glyphs and 158 all(v in s.glyphs for v in vlist)) 159 return bool(self.alternates) 160 else: 161 assert 0, "unknown format: %s" % self.Format 162 163@_add_method(fontTools.ttLib.tables.otTables.LigatureSubst) 164def closure_glyphs(self, s, cur_glyphs=None): 165 if cur_glyphs == None: cur_glyphs = s.glyphs 166 if self.Format == 1: 167 _set_update(s.glyphs, *([seq.LigGlyph for seq in seqs 168 if all(c in s.glyphs for c in seq.Component)] 169 for g,seqs in self.ligatures.iteritems() 170 if g in cur_glyphs)) 171 else: 172 assert 0, "unknown format: %s" % self.Format 173 174@_add_method(fontTools.ttLib.tables.otTables.LigatureSubst) 175def subset_glyphs(self, s): 176 if self.Format == 1: 177 self.ligatures = dict((g,v) for g,v in self.ligatures.iteritems() 178 if g in s.glyphs) 179 self.ligatures = dict((g,[seq for seq in seqs 180 if seq.LigGlyph in s.glyphs and 181 all(c in s.glyphs for c in seq.Component)]) 182 for g,seqs in self.ligatures.iteritems()) 183 self.ligatures = dict((g,v) for g,v in self.ligatures.iteritems() if v) 184 return bool(self.ligatures) 185 else: 186 assert 0, "unknown format: %s" % self.Format 187 188@_add_method(fontTools.ttLib.tables.otTables.ReverseChainSingleSubst) 189def closure_glyphs(self, s, cur_glyphs=None): 190 if cur_glyphs == None: cur_glyphs = s.glyphs 191 if self.Format == 1: 192 indices = self.Coverage.intersect(cur_glyphs) 193 if(not indices or 194 not all(c.intersect(s.glyphs) 195 for c in self.LookAheadCoverage + self.BacktrackCoverage)): 196 return 197 s.glyphs.update(self.Substitute[i] for i in indices) 198 else: 199 assert 0, "unknown format: %s" % self.Format 200 201@_add_method(fontTools.ttLib.tables.otTables.ReverseChainSingleSubst) 202def subset_glyphs(self, s): 203 if self.Format == 1: 204 indices = self.Coverage.subset(s.glyphs) 205 self.Substitute = [self.Substitute[i] for i in indices] 206 # Now drop rules generating glyphs we don't want 207 indices = [i for i,sub in enumerate(self.Substitute) 208 if sub in s.glyphs] 209 self.Substitute = [self.Substitute[i] for i in indices] 210 self.Coverage.remap(indices) 211 self.GlyphCount = len(self.Substitute) 212 return bool(self.GlyphCount and 213 all(c.subset(s.glyphs) 214 for c in self.LookAheadCoverage+self.BacktrackCoverage)) 215 else: 216 assert 0, "unknown format: %s" % self.Format 217 218@_add_method(fontTools.ttLib.tables.otTables.SinglePos) 219def subset_glyphs(self, s): 220 if self.Format == 1: 221 return len(self.Coverage.subset(s.glyphs)) 222 elif self.Format == 2: 223 indices = self.Coverage.subset(s.glyphs) 224 self.Value = [self.Value[i] for i in indices] 225 self.ValueCount = len(self.Value) 226 return bool(self.ValueCount) 227 else: 228 assert 0, "unknown format: %s" % self.Format 229 230@_add_method(fontTools.ttLib.tables.otTables.SinglePos) 231def prune_post_subset(self, options): 232 if not options.hinting: 233 # Drop device tables 234 self.ValueFormat &= ~0x00F0 235 return True 236 237@_add_method(fontTools.ttLib.tables.otTables.PairPos) 238def subset_glyphs(self, s): 239 if self.Format == 1: 240 indices = self.Coverage.subset(s.glyphs) 241 self.PairSet = [self.PairSet[i] for i in indices] 242 for p in self.PairSet: 243 p.PairValueRecord = [r for r in p.PairValueRecord 244 if r.SecondGlyph in s.glyphs] 245 p.PairValueCount = len(p.PairValueRecord) 246 self.PairSet = [p for p in self.PairSet if p.PairValueCount] 247 self.PairSetCount = len(self.PairSet) 248 return bool(self.PairSetCount) 249 elif self.Format == 2: 250 class1_map = self.ClassDef1.subset(s.glyphs, remap=True) 251 class2_map = self.ClassDef2.subset(s.glyphs, remap=True) 252 self.Class1Record = [self.Class1Record[i] for i in class1_map] 253 for c in self.Class1Record: 254 c.Class2Record = [c.Class2Record[i] for i in class2_map] 255 self.Class1Count = len(class1_map) 256 self.Class2Count = len(class2_map) 257 return bool(self.Class1Count and 258 self.Class2Count and 259 self.Coverage.subset(s.glyphs)) 260 else: 261 assert 0, "unknown format: %s" % self.Format 262 263@_add_method(fontTools.ttLib.tables.otTables.PairPos) 264def prune_post_subset(self, options): 265 if not options.hinting: 266 # Drop device tables 267 self.ValueFormat1 &= ~0x00F0 268 self.ValueFormat2 &= ~0x00F0 269 return True 270 271@_add_method(fontTools.ttLib.tables.otTables.CursivePos) 272def subset_glyphs(self, s): 273 if self.Format == 1: 274 indices = self.Coverage.subset(s.glyphs) 275 self.EntryExitRecord = [self.EntryExitRecord[i] for i in indices] 276 self.EntryExitCount = len(self.EntryExitRecord) 277 return bool(self.EntryExitCount) 278 else: 279 assert 0, "unknown format: %s" % self.Format 280 281@_add_method(fontTools.ttLib.tables.otTables.Anchor) 282def prune_hints(self): 283 # Drop device tables / contour anchor point 284 self.Format = 1 285 286@_add_method(fontTools.ttLib.tables.otTables.CursivePos) 287def prune_post_subset(self, options): 288 if not options.hinting: 289 for rec in self.EntryExitRecord: 290 if rec.EntryAnchor: rec.EntryAnchor.prune_hints() 291 if rec.ExitAnchor: rec.ExitAnchor.prune_hints() 292 return True 293 294@_add_method(fontTools.ttLib.tables.otTables.MarkBasePos) 295def subset_glyphs(self, s): 296 if self.Format == 1: 297 mark_indices = self.MarkCoverage.subset(s.glyphs) 298 self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] 299 for i in mark_indices] 300 self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) 301 base_indices = self.BaseCoverage.subset(s.glyphs) 302 self.BaseArray.BaseRecord = [self.BaseArray.BaseRecord[i] 303 for i in base_indices] 304 self.BaseArray.BaseCount = len(self.BaseArray.BaseRecord) 305 # Prune empty classes 306 class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) 307 self.ClassCount = len(class_indices) 308 for m in self.MarkArray.MarkRecord: 309 m.Class = class_indices.index(m.Class) 310 for b in self.BaseArray.BaseRecord: 311 b.BaseAnchor = [b.BaseAnchor[i] for i in class_indices] 312 return bool(self.ClassCount and 313 self.MarkArray.MarkCount and 314 self.BaseArray.BaseCount) 315 else: 316 assert 0, "unknown format: %s" % self.Format 317 318@_add_method(fontTools.ttLib.tables.otTables.MarkBasePos) 319def prune_post_subset(self, options): 320 if not options.hinting: 321 for m in self.MarkArray.MarkRecord: 322 m.MarkAnchor.prune_hints() 323 for b in self.BaseArray.BaseRecord: 324 for a in b.BaseAnchor: 325 a.prune_hints() 326 return True 327 328@_add_method(fontTools.ttLib.tables.otTables.MarkLigPos) 329def subset_glyphs(self, s): 330 if self.Format == 1: 331 mark_indices = self.MarkCoverage.subset(s.glyphs) 332 self.MarkArray.MarkRecord = [self.MarkArray.MarkRecord[i] 333 for i in mark_indices] 334 self.MarkArray.MarkCount = len(self.MarkArray.MarkRecord) 335 ligature_indices = self.LigatureCoverage.subset(s.glyphs) 336 self.LigatureArray.LigatureAttach = [self.LigatureArray.LigatureAttach[i] 337 for i in ligature_indices] 338 self.LigatureArray.LigatureCount = len(self.LigatureArray.LigatureAttach) 339 # Prune empty classes 340 class_indices = _uniq_sort(v.Class for v in self.MarkArray.MarkRecord) 341 self.ClassCount = len(class_indices) 342 for m in self.MarkArray.MarkRecord: 343 m.Class = class_indices.index(m.Class) 344 for l in self.LigatureArray.LigatureAttach: 345 for c in l.ComponentRecord: 346 c.LigatureAnchor = [c.LigatureAnchor[i] for i in class_indices] 347 return bool(self.ClassCount and 348 self.MarkArray.MarkCount and 349 self.LigatureArray.LigatureCount) 350 else: 351 assert 0, "unknown format: %s" % self.Format 352 353@_add_method(fontTools.ttLib.tables.otTables.MarkLigPos) 354def prune_post_subset(self, options): 355 if not options.hinting: 356 for m in self.MarkArray.MarkRecord: 357 m.MarkAnchor.prune_hints() 358 for l in self.LigatureArray.LigatureAttach: 359 for c in l.ComponentRecord: 360 for a in c.LigatureAnchor: 361 a.prune_hints() 362 return True 363 364@_add_method(fontTools.ttLib.tables.otTables.MarkMarkPos) 365def subset_glyphs(self, s): 366 if self.Format == 1: 367 mark1_indices = self.Mark1Coverage.subset(s.glyphs) 368 self.Mark1Array.MarkRecord = [self.Mark1Array.MarkRecord[i] 369 for i in mark1_indices] 370 self.Mark1Array.MarkCount = len(self.Mark1Array.MarkRecord) 371 mark2_indices = self.Mark2Coverage.subset(s.glyphs) 372 self.Mark2Array.Mark2Record = [self.Mark2Array.Mark2Record[i] 373 for i in mark2_indices] 374 self.Mark2Array.MarkCount = len(self.Mark2Array.Mark2Record) 375 # Prune empty classes 376 class_indices = _uniq_sort(v.Class for v in self.Mark1Array.MarkRecord) 377 self.ClassCount = len(class_indices) 378 for m in self.Mark1Array.MarkRecord: 379 m.Class = class_indices.index(m.Class) 380 for b in self.Mark2Array.Mark2Record: 381 b.Mark2Anchor = [b.Mark2Anchor[i] for i in class_indices] 382 return bool(self.ClassCount and 383 self.Mark1Array.MarkCount and 384 self.Mark2Array.MarkCount) 385 else: 386 assert 0, "unknown format: %s" % self.Format 387 388@_add_method(fontTools.ttLib.tables.otTables.MarkMarkPos) 389def prune_post_subset(self, options): 390 if not options.hinting: 391 # Drop device tables or contour anchor point 392 for m in self.Mark1Array.MarkRecord: 393 m.MarkAnchor.prune_hints() 394 for b in self.Mark2Array.Mark2Record: 395 for m in b.Mark2Anchor: 396 m.prune_hints() 397 return True 398 399@_add_method(fontTools.ttLib.tables.otTables.SingleSubst, 400 fontTools.ttLib.tables.otTables.MultipleSubst, 401 fontTools.ttLib.tables.otTables.AlternateSubst, 402 fontTools.ttLib.tables.otTables.LigatureSubst, 403 fontTools.ttLib.tables.otTables.ReverseChainSingleSubst, 404 fontTools.ttLib.tables.otTables.SinglePos, 405 fontTools.ttLib.tables.otTables.PairPos, 406 fontTools.ttLib.tables.otTables.CursivePos, 407 fontTools.ttLib.tables.otTables.MarkBasePos, 408 fontTools.ttLib.tables.otTables.MarkLigPos, 409 fontTools.ttLib.tables.otTables.MarkMarkPos) 410def subset_lookups(self, lookup_indices): 411 pass 412 413@_add_method(fontTools.ttLib.tables.otTables.SingleSubst, 414 fontTools.ttLib.tables.otTables.MultipleSubst, 415 fontTools.ttLib.tables.otTables.AlternateSubst, 416 fontTools.ttLib.tables.otTables.LigatureSubst, 417 fontTools.ttLib.tables.otTables.ReverseChainSingleSubst, 418 fontTools.ttLib.tables.otTables.SinglePos, 419 fontTools.ttLib.tables.otTables.PairPos, 420 fontTools.ttLib.tables.otTables.CursivePos, 421 fontTools.ttLib.tables.otTables.MarkBasePos, 422 fontTools.ttLib.tables.otTables.MarkLigPos, 423 fontTools.ttLib.tables.otTables.MarkMarkPos) 424def collect_lookups(self): 425 return [] 426 427@_add_method(fontTools.ttLib.tables.otTables.SingleSubst, 428 fontTools.ttLib.tables.otTables.MultipleSubst, 429 fontTools.ttLib.tables.otTables.AlternateSubst, 430 fontTools.ttLib.tables.otTables.LigatureSubst, 431 fontTools.ttLib.tables.otTables.ContextSubst, 432 fontTools.ttLib.tables.otTables.ChainContextSubst, 433 fontTools.ttLib.tables.otTables.ReverseChainSingleSubst, 434 fontTools.ttLib.tables.otTables.SinglePos, 435 fontTools.ttLib.tables.otTables.PairPos, 436 fontTools.ttLib.tables.otTables.CursivePos, 437 fontTools.ttLib.tables.otTables.MarkBasePos, 438 fontTools.ttLib.tables.otTables.MarkLigPos, 439 fontTools.ttLib.tables.otTables.MarkMarkPos, 440 fontTools.ttLib.tables.otTables.ContextPos, 441 fontTools.ttLib.tables.otTables.ChainContextPos) 442def prune_pre_subset(self, options): 443 return True 444 445@_add_method(fontTools.ttLib.tables.otTables.SingleSubst, 446 fontTools.ttLib.tables.otTables.MultipleSubst, 447 fontTools.ttLib.tables.otTables.AlternateSubst, 448 fontTools.ttLib.tables.otTables.LigatureSubst, 449 fontTools.ttLib.tables.otTables.ReverseChainSingleSubst, 450 fontTools.ttLib.tables.otTables.ContextSubst, 451 fontTools.ttLib.tables.otTables.ChainContextSubst, 452 fontTools.ttLib.tables.otTables.ContextPos, 453 fontTools.ttLib.tables.otTables.ChainContextPos) 454def prune_post_subset(self, options): 455 return True 456 457@_add_method(fontTools.ttLib.tables.otTables.SingleSubst, 458 fontTools.ttLib.tables.otTables.AlternateSubst, 459 fontTools.ttLib.tables.otTables.ReverseChainSingleSubst) 460def may_have_non_1to1(self): 461 return False 462 463@_add_method(fontTools.ttLib.tables.otTables.MultipleSubst, 464 fontTools.ttLib.tables.otTables.LigatureSubst, 465 fontTools.ttLib.tables.otTables.ContextSubst, 466 fontTools.ttLib.tables.otTables.ChainContextSubst) 467def may_have_non_1to1(self): 468 return True 469 470@_add_method(fontTools.ttLib.tables.otTables.ContextSubst, 471 fontTools.ttLib.tables.otTables.ChainContextSubst, 472 fontTools.ttLib.tables.otTables.ContextPos, 473 fontTools.ttLib.tables.otTables.ChainContextPos) 474def __classify_context(self): 475 476 class ContextHelper(object): 477 def __init__(self, klass, Format): 478 if klass.__name__.endswith('Subst'): 479 Typ = 'Sub' 480 Type = 'Subst' 481 else: 482 Typ = 'Pos' 483 Type = 'Pos' 484 if klass.__name__.startswith('Chain'): 485 Chain = 'Chain' 486 else: 487 Chain = '' 488 ChainTyp = Chain+Typ 489 490 self.Typ = Typ 491 self.Type = Type 492 self.Chain = Chain 493 self.ChainTyp = ChainTyp 494 495 self.LookupRecord = Type+'LookupRecord' 496 497 if Format == 1: 498 Coverage = lambda r: r.Coverage 499 ChainCoverage = lambda r: r.Coverage 500 ContextData = lambda r:(None,) 501 ChainContextData = lambda r:(None, None, None) 502 RuleData = lambda r:(r.Input,) 503 ChainRuleData = lambda r:(r.Backtrack, r.Input, r.LookAhead) 504 SetRuleData = None 505 ChainSetRuleData = None 506 elif Format == 2: 507 Coverage = lambda r: r.Coverage 508 ChainCoverage = lambda r: r.Coverage 509 ContextData = lambda r:(r.ClassDef,) 510 ChainContextData = lambda r:(r.LookAheadClassDef, 511 r.InputClassDef, 512 r.BacktrackClassDef) 513 RuleData = lambda r:(r.Class,) 514 ChainRuleData = lambda r:(r.LookAhead, r.Input, r.Backtrack) 515 def SetRuleData(r, d):(r.Class,) = d 516 def ChainSetRuleData(r, d):(r.LookAhead, r.Input, r.Backtrack) = d 517 elif Format == 3: 518 Coverage = lambda r: r.Coverage[0] 519 ChainCoverage = lambda r: r.InputCoverage[0] 520 ContextData = None 521 ChainContextData = None 522 RuleData = lambda r: r.Coverage 523 ChainRuleData = lambda r:(r.LookAheadCoverage + 524 r.InputCoverage + 525 r.BacktrackCoverage) 526 SetRuleData = None 527 ChainSetRuleData = None 528 else: 529 assert 0, "unknown format: %s" % Format 530 531 if Chain: 532 self.Coverage = ChainCoverage 533 self.ContextData = ChainContextData 534 self.RuleData = ChainRuleData 535 self.SetRuleData = ChainSetRuleData 536 else: 537 self.Coverage = Coverage 538 self.ContextData = ContextData 539 self.RuleData = RuleData 540 self.SetRuleData = SetRuleData 541 542 if Format == 1: 543 self.Rule = ChainTyp+'Rule' 544 self.RuleCount = ChainTyp+'RuleCount' 545 self.RuleSet = ChainTyp+'RuleSet' 546 self.RuleSetCount = ChainTyp+'RuleSetCount' 547 self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else [] 548 elif Format == 2: 549 self.Rule = ChainTyp+'ClassRule' 550 self.RuleCount = ChainTyp+'ClassRuleCount' 551 self.RuleSet = ChainTyp+'ClassSet' 552 self.RuleSetCount = ChainTyp+'ClassSetCount' 553 self.Intersect = lambda glyphs, c, r: c.intersect_class(glyphs, r) 554 555 self.ClassDef = 'InputClassDef' if Chain else 'ClassDef' 556 self.Input = 'Input' if Chain else 'Class' 557 558 if self.Format not in [1, 2, 3]: 559 return None # Don't shoot the messenger; let it go 560 if not hasattr(self.__class__, "__ContextHelpers"): 561 self.__class__.__ContextHelpers = {} 562 if self.Format not in self.__class__.__ContextHelpers: 563 helper = ContextHelper(self.__class__, self.Format) 564 self.__class__.__ContextHelpers[self.Format] = helper 565 return self.__class__.__ContextHelpers[self.Format] 566 567@_add_method(fontTools.ttLib.tables.otTables.ContextSubst, 568 fontTools.ttLib.tables.otTables.ChainContextSubst) 569def closure_glyphs(self, s, cur_glyphs=None): 570 if cur_glyphs == None: cur_glyphs = s.glyphs 571 c = self.__classify_context() 572 573 indices = c.Coverage(self).intersect(s.glyphs) 574 if not indices: 575 return [] 576 cur_glyphs = c.Coverage(self).intersect_glyphs(s.glyphs); 577 578 if self.Format == 1: 579 ContextData = c.ContextData(self) 580 rss = getattr(self, c.RuleSet) 581 for i in indices: 582 if not rss[i]: continue 583 for r in getattr(rss[i], c.Rule): 584 if not r: continue 585 if all(all(c.Intersect(s.glyphs, cd, k) for k in klist) 586 for cd,klist in zip(ContextData, c.RuleData(r))): 587 chaos = False 588 for ll in getattr(r, c.LookupRecord): 589 if not ll: continue 590 seqi = ll.SequenceIndex 591 if chaos: 592 pos_glyphs = s.glyphs 593 else: 594 if seqi == 0: 595 pos_glyphs = set([c.Coverage(self).glyphs[i]]) 596 else: 597 pos_glyphs = set([r.Input[seqi - 1]]) 598 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 599 chaos = chaos or lookup.may_have_non_1to1() 600 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 601 elif self.Format == 2: 602 ClassDef = getattr(self, c.ClassDef) 603 indices = ClassDef.intersect(cur_glyphs) 604 ContextData = c.ContextData(self) 605 rss = getattr(self, c.RuleSet) 606 for i in indices: 607 if not rss[i]: continue 608 for r in getattr(rss[i], c.Rule): 609 if not r: continue 610 if all(all(c.Intersect(s.glyphs, cd, k) for k in klist) 611 for cd,klist in zip(ContextData, c.RuleData(r))): 612 chaos = False 613 for ll in getattr(r, c.LookupRecord): 614 if not ll: continue 615 seqi = ll.SequenceIndex 616 if chaos: 617 pos_glyphs = s.glyphs 618 else: 619 if seqi == 0: 620 pos_glyphs = ClassDef.intersect_class(cur_glyphs, i) 621 else: 622 pos_glyphs = ClassDef.intersect_class(s.glyphs, 623 getattr(r, c.Input)[seqi - 1]) 624 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 625 chaos = chaos or lookup.may_have_non_1to1() 626 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 627 elif self.Format == 3: 628 if not all(x.intersect(s.glyphs) for x in c.RuleData(self)): 629 return [] 630 r = self 631 chaos = False 632 for ll in getattr(r, c.LookupRecord): 633 if not ll: continue 634 seqi = ll.SequenceIndex 635 if chaos: 636 pos_glyphs = s.glyphs 637 else: 638 if seqi == 0: 639 pos_glyphs = cur_glyphs 640 else: 641 pos_glyphs = r.InputCoverage[seqi].intersect_glyphs(s.glyphs) 642 lookup = s.table.LookupList.Lookup[ll.LookupListIndex] 643 chaos = chaos or lookup.may_have_non_1to1() 644 lookup.closure_glyphs(s, cur_glyphs=pos_glyphs) 645 else: 646 assert 0, "unknown format: %s" % self.Format 647 648@_add_method(fontTools.ttLib.tables.otTables.ContextSubst, 649 fontTools.ttLib.tables.otTables.ContextPos, 650 fontTools.ttLib.tables.otTables.ChainContextSubst, 651 fontTools.ttLib.tables.otTables.ChainContextPos) 652def subset_glyphs(self, s): 653 c = self.__classify_context() 654 655 if self.Format == 1: 656 indices = self.Coverage.subset(s.glyphs) 657 rss = getattr(self, c.RuleSet) 658 rss = [rss[i] for i in indices] 659 for rs in rss: 660 if not rs: continue 661 ss = getattr(rs, c.Rule) 662 ss = [r for r in ss 663 if r and all(all(g in s.glyphs for g in glist) 664 for glist in c.RuleData(r))] 665 setattr(rs, c.Rule, ss) 666 setattr(rs, c.RuleCount, len(ss)) 667 # Prune empty subrulesets 668 rss = [rs for rs in rss if rs and getattr(rs, c.Rule)] 669 setattr(self, c.RuleSet, rss) 670 setattr(self, c.RuleSetCount, len(rss)) 671 return bool(rss) 672 elif self.Format == 2: 673 if not self.Coverage.subset(s.glyphs): 674 return False 675 indices = getattr(self, c.ClassDef).subset(self.Coverage.glyphs, 676 remap=False) 677 rss = getattr(self, c.RuleSet) 678 rss = [rss[i] for i in indices] 679 ContextData = c.ContextData(self) 680 klass_maps = [x.subset(s.glyphs, remap=True) for x in ContextData] 681 for rs in rss: 682 if not rs: continue 683 ss = getattr(rs, c.Rule) 684 ss = [r for r in ss 685 if r and all(all(k in klass_map for k in klist) 686 for klass_map,klist in zip(klass_maps, c.RuleData(r)))] 687 setattr(rs, c.Rule, ss) 688 setattr(rs, c.RuleCount, len(ss)) 689 690 # Remap rule classes 691 for r in ss: 692 c.SetRuleData(r, [[klass_map.index(k) for k in klist] 693 for klass_map,klist in zip(klass_maps, c.RuleData(r))]) 694 # Prune empty subrulesets 695 rss = [rs for rs in rss if rs and getattr(rs, c.Rule)] 696 setattr(self, c.RuleSet, rss) 697 setattr(self, c.RuleSetCount, len(rss)) 698 return bool(rss) 699 elif self.Format == 3: 700 return all(x.subset(s.glyphs) for x in c.RuleData(self)) 701 else: 702 assert 0, "unknown format: %s" % self.Format 703 704@_add_method(fontTools.ttLib.tables.otTables.ContextSubst, 705 fontTools.ttLib.tables.otTables.ChainContextSubst, 706 fontTools.ttLib.tables.otTables.ContextPos, 707 fontTools.ttLib.tables.otTables.ChainContextPos) 708def subset_lookups(self, lookup_indices): 709 c = self.__classify_context() 710 711 if self.Format in [1, 2]: 712 for rs in getattr(self, c.RuleSet): 713 if not rs: continue 714 for r in getattr(rs, c.Rule): 715 if not r: continue 716 setattr(r, c.LookupRecord, 717 [ll for ll in getattr(r, c.LookupRecord) 718 if ll and ll.LookupListIndex in lookup_indices]) 719 for ll in getattr(r, c.LookupRecord): 720 if not ll: continue 721 ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) 722 elif self.Format == 3: 723 setattr(self, c.LookupRecord, 724 [ll for ll in getattr(self, c.LookupRecord) 725 if ll and ll.LookupListIndex in lookup_indices]) 726 for ll in getattr(self, c.LookupRecord): 727 if not ll: continue 728 ll.LookupListIndex = lookup_indices.index(ll.LookupListIndex) 729 else: 730 assert 0, "unknown format: %s" % self.Format 731 732@_add_method(fontTools.ttLib.tables.otTables.ContextSubst, 733 fontTools.ttLib.tables.otTables.ChainContextSubst, 734 fontTools.ttLib.tables.otTables.ContextPos, 735 fontTools.ttLib.tables.otTables.ChainContextPos) 736def collect_lookups(self): 737 c = self.__classify_context() 738 739 if self.Format in [1, 2]: 740 return [ll.LookupListIndex 741 for rs in getattr(self, c.RuleSet) if rs 742 for r in getattr(rs, c.Rule) if r 743 for ll in getattr(r, c.LookupRecord) if ll] 744 elif self.Format == 3: 745 return [ll.LookupListIndex 746 for ll in getattr(self, c.LookupRecord) if ll] 747 else: 748 assert 0, "unknown format: %s" % self.Format 749 750@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst) 751def closure_glyphs(self, s, cur_glyphs=None): 752 if self.Format == 1: 753 self.ExtSubTable.closure_glyphs(s, cur_glyphs) 754 else: 755 assert 0, "unknown format: %s" % self.Format 756 757@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst) 758def may_have_non_1to1(self): 759 if self.Format == 1: 760 return self.ExtSubTable.may_have_non_1to1() 761 else: 762 assert 0, "unknown format: %s" % self.Format 763 764@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, 765 fontTools.ttLib.tables.otTables.ExtensionPos) 766def prune_pre_subset(self, options): 767 if self.Format == 1: 768 return self.ExtSubTable.prune_pre_subset(options) 769 else: 770 assert 0, "unknown format: %s" % self.Format 771 772@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, 773 fontTools.ttLib.tables.otTables.ExtensionPos) 774def subset_glyphs(self, s): 775 if self.Format == 1: 776 return self.ExtSubTable.subset_glyphs(s) 777 else: 778 assert 0, "unknown format: %s" % self.Format 779 780@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, 781 fontTools.ttLib.tables.otTables.ExtensionPos) 782def prune_post_subset(self, options): 783 if self.Format == 1: 784 return self.ExtSubTable.prune_post_subset(options) 785 else: 786 assert 0, "unknown format: %s" % self.Format 787 788@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, 789 fontTools.ttLib.tables.otTables.ExtensionPos) 790def subset_lookups(self, lookup_indices): 791 if self.Format == 1: 792 return self.ExtSubTable.subset_lookups(lookup_indices) 793 else: 794 assert 0, "unknown format: %s" % self.Format 795 796@_add_method(fontTools.ttLib.tables.otTables.ExtensionSubst, 797 fontTools.ttLib.tables.otTables.ExtensionPos) 798def collect_lookups(self): 799 if self.Format == 1: 800 return self.ExtSubTable.collect_lookups() 801 else: 802 assert 0, "unknown format: %s" % self.Format 803 804@_add_method(fontTools.ttLib.tables.otTables.Lookup) 805def closure_glyphs(self, s, cur_glyphs=None): 806 for st in self.SubTable: 807 if not st: continue 808 st.closure_glyphs(s, cur_glyphs) 809 810@_add_method(fontTools.ttLib.tables.otTables.Lookup) 811def prune_pre_subset(self, options): 812 ret = False 813 for st in self.SubTable: 814 if not st: continue 815 if st.prune_pre_subset(options): ret = True 816 return ret 817 818@_add_method(fontTools.ttLib.tables.otTables.Lookup) 819def subset_glyphs(self, s): 820 self.SubTable = [st for st in self.SubTable if st and st.subset_glyphs(s)] 821 self.SubTableCount = len(self.SubTable) 822 return bool(self.SubTableCount) 823 824@_add_method(fontTools.ttLib.tables.otTables.Lookup) 825def prune_post_subset(self, options): 826 ret = False 827 for st in self.SubTable: 828 if not st: continue 829 if st.prune_post_subset(options): ret = True 830 return ret 831 832@_add_method(fontTools.ttLib.tables.otTables.Lookup) 833def subset_lookups(self, lookup_indices): 834 for s in self.SubTable: 835 s.subset_lookups(lookup_indices) 836 837@_add_method(fontTools.ttLib.tables.otTables.Lookup) 838def collect_lookups(self): 839 return _uniq_sort(sum((st.collect_lookups() for st in self.SubTable 840 if st), [])) 841 842@_add_method(fontTools.ttLib.tables.otTables.Lookup) 843def may_have_non_1to1(self): 844 return any(st.may_have_non_1to1() for st in self.SubTable if st) 845 846@_add_method(fontTools.ttLib.tables.otTables.LookupList) 847def prune_pre_subset(self, options): 848 ret = False 849 for l in self.Lookup: 850 if not l: continue 851 if l.prune_pre_subset(options): ret = True 852 return ret 853 854@_add_method(fontTools.ttLib.tables.otTables.LookupList) 855def subset_glyphs(self, s): 856 "Returns the indices of nonempty lookups." 857 return [i for i,l in enumerate(self.Lookup) if l and l.subset_glyphs(s)] 858 859@_add_method(fontTools.ttLib.tables.otTables.LookupList) 860def prune_post_subset(self, options): 861 ret = False 862 for l in self.Lookup: 863 if not l: continue 864 if l.prune_post_subset(options): ret = True 865 return ret 866 867@_add_method(fontTools.ttLib.tables.otTables.LookupList) 868def subset_lookups(self, lookup_indices): 869 self.Lookup = [self.Lookup[i] for i in lookup_indices 870 if i < self.LookupCount] 871 self.LookupCount = len(self.Lookup) 872 for l in self.Lookup: 873 l.subset_lookups(lookup_indices) 874 875@_add_method(fontTools.ttLib.tables.otTables.LookupList) 876def closure_lookups(self, lookup_indices): 877 lookup_indices = _uniq_sort(lookup_indices) 878 recurse = lookup_indices 879 while True: 880 recurse_lookups = sum((self.Lookup[i].collect_lookups() 881 for i in recurse if i < self.LookupCount), []) 882 recurse_lookups = [l for l in recurse_lookups 883 if l not in lookup_indices and l < self.LookupCount] 884 if not recurse_lookups: 885 return _uniq_sort(lookup_indices) 886 recurse_lookups = _uniq_sort(recurse_lookups) 887 lookup_indices.extend(recurse_lookups) 888 recurse = recurse_lookups 889 890@_add_method(fontTools.ttLib.tables.otTables.Feature) 891def subset_lookups(self, lookup_indices): 892 self.LookupListIndex = [l for l in self.LookupListIndex 893 if l in lookup_indices] 894 # Now map them. 895 self.LookupListIndex = [lookup_indices.index(l) 896 for l in self.LookupListIndex] 897 self.LookupCount = len(self.LookupListIndex) 898 return self.LookupCount 899 900@_add_method(fontTools.ttLib.tables.otTables.Feature) 901def collect_lookups(self): 902 return self.LookupListIndex[:] 903 904@_add_method(fontTools.ttLib.tables.otTables.FeatureList) 905def subset_lookups(self, lookup_indices): 906 "Returns the indices of nonempty features." 907 feature_indices = [i for i,f in enumerate(self.FeatureRecord) 908 if f.Feature.subset_lookups(lookup_indices)] 909 self.subset_features(feature_indices) 910 return feature_indices 911 912@_add_method(fontTools.ttLib.tables.otTables.FeatureList) 913def collect_lookups(self, feature_indices): 914 return _uniq_sort(sum((self.FeatureRecord[i].Feature.collect_lookups() 915 for i in feature_indices 916 if i < self.FeatureCount), [])) 917 918@_add_method(fontTools.ttLib.tables.otTables.FeatureList) 919def subset_features(self, feature_indices): 920 self.FeatureRecord = [self.FeatureRecord[i] for i in feature_indices] 921 self.FeatureCount = len(self.FeatureRecord) 922 return bool(self.FeatureCount) 923 924@_add_method(fontTools.ttLib.tables.otTables.DefaultLangSys, 925 fontTools.ttLib.tables.otTables.LangSys) 926def subset_features(self, feature_indices): 927 if self.ReqFeatureIndex in feature_indices: 928 self.ReqFeatureIndex = feature_indices.index(self.ReqFeatureIndex) 929 else: 930 self.ReqFeatureIndex = 65535 931 self.FeatureIndex = [f for f in self.FeatureIndex if f in feature_indices] 932 # Now map them. 933 self.FeatureIndex = [feature_indices.index(f) for f in self.FeatureIndex 934 if f in feature_indices] 935 self.FeatureCount = len(self.FeatureIndex) 936 return bool(self.FeatureCount or self.ReqFeatureIndex != 65535) 937 938@_add_method(fontTools.ttLib.tables.otTables.DefaultLangSys, 939 fontTools.ttLib.tables.otTables.LangSys) 940def collect_features(self): 941 feature_indices = self.FeatureIndex[:] 942 if self.ReqFeatureIndex != 65535: 943 feature_indices.append(self.ReqFeatureIndex) 944 return _uniq_sort(feature_indices) 945 946@_add_method(fontTools.ttLib.tables.otTables.Script) 947def subset_features(self, feature_indices): 948 if(self.DefaultLangSys and 949 not self.DefaultLangSys.subset_features(feature_indices)): 950 self.DefaultLangSys = None 951 self.LangSysRecord = [l for l in self.LangSysRecord 952 if l.LangSys.subset_features(feature_indices)] 953 self.LangSysCount = len(self.LangSysRecord) 954 return bool(self.LangSysCount or self.DefaultLangSys) 955 956@_add_method(fontTools.ttLib.tables.otTables.Script) 957def collect_features(self): 958 feature_indices = [l.LangSys.collect_features() for l in self.LangSysRecord] 959 if self.DefaultLangSys: 960 feature_indices.append(self.DefaultLangSys.collect_features()) 961 return _uniq_sort(sum(feature_indices, [])) 962 963@_add_method(fontTools.ttLib.tables.otTables.ScriptList) 964def subset_features(self, feature_indices): 965 self.ScriptRecord = [s for s in self.ScriptRecord 966 if s.Script.subset_features(feature_indices)] 967 self.ScriptCount = len(self.ScriptRecord) 968 return bool(self.ScriptCount) 969 970@_add_method(fontTools.ttLib.tables.otTables.ScriptList) 971def collect_features(self): 972 return _uniq_sort(sum((s.Script.collect_features() 973 for s in self.ScriptRecord), [])) 974 975@_add_method(fontTools.ttLib.getTableClass('GSUB')) 976def closure_glyphs(self, s): 977 s.table = self.table 978 feature_indices = self.table.ScriptList.collect_features() 979 lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) 980 while True: 981 orig_glyphs = s.glyphs.copy() 982 for i in lookup_indices: 983 if i >= self.table.LookupList.LookupCount: continue 984 if not self.table.LookupList.Lookup[i]: continue 985 self.table.LookupList.Lookup[i].closure_glyphs(s) 986 if orig_glyphs == s.glyphs: 987 break 988 del s.table 989 990@_add_method(fontTools.ttLib.getTableClass('GSUB'), 991 fontTools.ttLib.getTableClass('GPOS')) 992def subset_glyphs(self, s): 993 s.glyphs = s.glyphs_gsubed 994 lookup_indices = self.table.LookupList.subset_glyphs(s) 995 self.subset_lookups(lookup_indices) 996 self.prune_lookups() 997 return True 998 999@_add_method(fontTools.ttLib.getTableClass('GSUB'), 1000 fontTools.ttLib.getTableClass('GPOS')) 1001def subset_lookups(self, lookup_indices): 1002 """Retrains specified lookups, then removes empty features, language 1003 systems, and scripts.""" 1004 self.table.LookupList.subset_lookups(lookup_indices) 1005 feature_indices = self.table.FeatureList.subset_lookups(lookup_indices) 1006 self.table.ScriptList.subset_features(feature_indices) 1007 1008@_add_method(fontTools.ttLib.getTableClass('GSUB'), 1009 fontTools.ttLib.getTableClass('GPOS')) 1010def prune_lookups(self): 1011 "Remove unreferenced lookups" 1012 feature_indices = self.table.ScriptList.collect_features() 1013 lookup_indices = self.table.FeatureList.collect_lookups(feature_indices) 1014 lookup_indices = self.table.LookupList.closure_lookups(lookup_indices) 1015 self.subset_lookups(lookup_indices) 1016 1017@_add_method(fontTools.ttLib.getTableClass('GSUB'), 1018 fontTools.ttLib.getTableClass('GPOS')) 1019def subset_feature_tags(self, feature_tags): 1020 feature_indices = [i for i,f in 1021 enumerate(self.table.FeatureList.FeatureRecord) 1022 if f.FeatureTag in feature_tags] 1023 self.table.FeatureList.subset_features(feature_indices) 1024 self.table.ScriptList.subset_features(feature_indices) 1025 1026@_add_method(fontTools.ttLib.getTableClass('GSUB'), 1027 fontTools.ttLib.getTableClass('GPOS')) 1028def prune_pre_subset(self, options): 1029 if '*' not in options.layout_features: 1030 self.subset_feature_tags(options.layout_features) 1031 self.prune_lookups() 1032 self.table.LookupList.prune_pre_subset(options); 1033 return True 1034 1035@_add_method(fontTools.ttLib.getTableClass('GSUB'), 1036 fontTools.ttLib.getTableClass('GPOS')) 1037def prune_post_subset(self, options): 1038 self.table.LookupList.prune_post_subset(options); 1039 return True 1040 1041@_add_method(fontTools.ttLib.getTableClass('GDEF')) 1042def subset_glyphs(self, s): 1043 glyphs = s.glyphs_gsubed 1044 table = self.table 1045 if table.LigCaretList: 1046 indices = table.LigCaretList.Coverage.subset(glyphs) 1047 table.LigCaretList.LigGlyph = [table.LigCaretList.LigGlyph[i] 1048 for i in indices] 1049 table.LigCaretList.LigGlyphCount = len(table.LigCaretList.LigGlyph) 1050 if not table.LigCaretList.LigGlyphCount: 1051 table.LigCaretList = None 1052 if table.MarkAttachClassDef: 1053 table.MarkAttachClassDef.classDefs = dict((g,v) for g,v in 1054 table.MarkAttachClassDef. 1055 classDefs.iteritems() 1056 if g in glyphs) 1057 if not table.MarkAttachClassDef.classDefs: 1058 table.MarkAttachClassDef = None 1059 if table.GlyphClassDef: 1060 table.GlyphClassDef.classDefs = dict((g,v) for g,v in 1061 table.GlyphClassDef. 1062 classDefs.iteritems() 1063 if g in glyphs) 1064 if not table.GlyphClassDef.classDefs: 1065 table.GlyphClassDef = None 1066 if table.AttachList: 1067 indices = table.AttachList.Coverage.subset(glyphs) 1068 table.AttachList.AttachPoint = [table.AttachList.AttachPoint[i] 1069 for i in indices] 1070 table.AttachList.GlyphCount = len(table.AttachList.AttachPoint) 1071 if not table.AttachList.GlyphCount: 1072 table.AttachList = None 1073 return bool(table.LigCaretList or 1074 table.MarkAttachClassDef or 1075 table.GlyphClassDef or 1076 table.AttachList) 1077 1078@_add_method(fontTools.ttLib.getTableClass('kern')) 1079def prune_pre_subset(self, options): 1080 # Prune unknown kern table types 1081 self.kernTables = [t for t in self.kernTables if hasattr(t, 'kernTable')] 1082 return bool(self.kernTables) 1083 1084@_add_method(fontTools.ttLib.getTableClass('kern')) 1085def subset_glyphs(self, s): 1086 glyphs = s.glyphs_gsubed 1087 for t in self.kernTables: 1088 t.kernTable = dict(((a,b),v) for (a,b),v in t.kernTable.iteritems() 1089 if a in glyphs and b in glyphs) 1090 self.kernTables = [t for t in self.kernTables if t.kernTable] 1091 return bool(self.kernTables) 1092 1093@_add_method(fontTools.ttLib.getTableClass('vmtx'), 1094 fontTools.ttLib.getTableClass('hmtx')) 1095def subset_glyphs(self, s): 1096 self.metrics = dict((g,v) for g,v in self.metrics.iteritems() if g in s.glyphs) 1097 return bool(self.metrics) 1098 1099@_add_method(fontTools.ttLib.getTableClass('hdmx')) 1100def subset_glyphs(self, s): 1101 self.hdmx = dict((sz,_dict((g,v) for g,v in l.iteritems() if g in s.glyphs)) 1102 for sz,l in self.hdmx.iteritems()) 1103 return bool(self.hdmx) 1104 1105@_add_method(fontTools.ttLib.getTableClass('VORG')) 1106def subset_glyphs(self, s): 1107 self.VOriginRecords = dict((g,v) for g,v in self.VOriginRecords.iteritems() 1108 if g in s.glyphs) 1109 self.numVertOriginYMetrics = len(self.VOriginRecords) 1110 return True # Never drop; has default metrics 1111 1112@_add_method(fontTools.ttLib.getTableClass('post')) 1113def prune_pre_subset(self, options): 1114 if not options.glyph_names: 1115 self.formatType = 3.0 1116 return True 1117 1118@_add_method(fontTools.ttLib.getTableClass('post')) 1119def subset_glyphs(self, s): 1120 self.extraNames = [] # This seems to do it 1121 return True 1122 1123@_add_method(fontTools.ttLib.getTableModule('glyf').Glyph) 1124def getComponentNamesFast(self, glyfTable): 1125 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 1126 return [] # Not composite 1127 data = self.data 1128 i = 10 1129 components = [] 1130 more = 1 1131 while more: 1132 flags, glyphID = struct.unpack(">HH", data[i:i+4]) 1133 i += 4 1134 flags = int(flags) 1135 components.append(glyfTable.getGlyphName(int(glyphID))) 1136 1137 if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS 1138 else: i += 2 1139 if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE 1140 elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE 1141 elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO 1142 more = flags & 0x0020 # MORE_COMPONENTS 1143 1144 return components 1145 1146@_add_method(fontTools.ttLib.getTableModule('glyf').Glyph) 1147def remapComponentsFast(self, indices): 1148 if not self.data or struct.unpack(">h", self.data[:2])[0] >= 0: 1149 return # Not composite 1150 data = array.array("B", self.data) 1151 i = 10 1152 more = 1 1153 while more: 1154 flags =(data[i] << 8) | data[i+1] 1155 glyphID =(data[i+2] << 8) | data[i+3] 1156 # Remap 1157 glyphID = indices.index(glyphID) 1158 data[i+2] = glyphID >> 8 1159 data[i+3] = glyphID & 0xFF 1160 i += 4 1161 flags = int(flags) 1162 1163 if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS 1164 else: i += 2 1165 if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE 1166 elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE 1167 elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO 1168 more = flags & 0x0020 # MORE_COMPONENTS 1169 1170 self.data = data.tostring() 1171 1172@_add_method(fontTools.ttLib.getTableModule('glyf').Glyph) 1173def dropInstructionsFast(self): 1174 if not self.data: 1175 return 1176 numContours = struct.unpack(">h", self.data[:2])[0] 1177 data = array.array("B", self.data) 1178 i = 10 1179 if numContours >= 0: 1180 i += 2 * numContours # endPtsOfContours 1181 instructionLen =(data[i] << 8) | data[i+1] 1182 # Zero it 1183 data[i] = data [i+1] = 0 1184 i += 2 1185 if instructionLen: 1186 # Splice it out 1187 data = data[:i] + data[i+instructionLen:] 1188 else: 1189 more = 1 1190 while more: 1191 flags =(data[i] << 8) | data[i+1] 1192 # Turn instruction flag off 1193 flags &= ~0x0100 # WE_HAVE_INSTRUCTIONS 1194 data[i+0] = flags >> 8 1195 data[i+1] = flags & 0xFF 1196 i += 4 1197 flags = int(flags) 1198 1199 if flags & 0x0001: i += 4 # ARG_1_AND_2_ARE_WORDS 1200 else: i += 2 1201 if flags & 0x0008: i += 2 # WE_HAVE_A_SCALE 1202 elif flags & 0x0040: i += 4 # WE_HAVE_AN_X_AND_Y_SCALE 1203 elif flags & 0x0080: i += 8 # WE_HAVE_A_TWO_BY_TWO 1204 more = flags & 0x0020 # MORE_COMPONENTS 1205 1206 # Cut off 1207 data = data[:i] 1208 if len(data) % 4: 1209 # add pad bytes 1210 nPadBytes = 4 -(len(data) % 4) 1211 for i in range(nPadBytes): 1212 data.append(0) 1213 self.data = data.tostring() 1214 1215@_add_method(fontTools.ttLib.getTableClass('glyf')) 1216def closure_glyphs(self, s): 1217 decompose = s.glyphs 1218 # I don't know if component glyphs can be composite themselves. 1219 # We handle them anyway. 1220 while True: 1221 components = set() 1222 for g in decompose: 1223 if g not in self.glyphs: 1224 continue 1225 gl = self.glyphs[g] 1226 if hasattr(gl, "data"): 1227 for c in gl.getComponentNamesFast(self): 1228 if c not in s.glyphs: 1229 components.add(c) 1230 else: 1231 # TTX seems to expand gid0..3 always 1232 if gl.isComposite(): 1233 for c in gl.components: 1234 if c.glyphName not in s.glyphs: 1235 components.add(c.glyphName) 1236 components = set(c for c in components if c not in s.glyphs) 1237 if not components: 1238 break 1239 decompose = components 1240 s.glyphs.update(components) 1241 1242@_add_method(fontTools.ttLib.getTableClass('glyf')) 1243def prune_pre_subset(self, options): 1244 if options.notdef_glyph and not options.notdef_outline: 1245 g = self[self.glyphOrder[0]] 1246 # Yay, easy! 1247 g.__dict__.clear() 1248 g.data = "" 1249 return True 1250 1251@_add_method(fontTools.ttLib.getTableClass('glyf')) 1252def subset_glyphs(self, s): 1253 self.glyphs = dict((g,v) for g,v in self.glyphs.iteritems() if g in s.glyphs) 1254 indices = [i for i,g in enumerate(self.glyphOrder) if g in s.glyphs] 1255 for v in self.glyphs.itervalues(): 1256 if hasattr(v, "data"): 1257 v.remapComponentsFast(indices) 1258 else: 1259 pass # No need 1260 self.glyphOrder = [g for g in self.glyphOrder if g in s.glyphs] 1261 # Don't drop empty 'glyf' tables, otherwise 'loca' doesn't get subset. 1262 return True 1263 1264@_add_method(fontTools.ttLib.getTableClass('glyf')) 1265def prune_post_subset(self, options): 1266 if not options.hinting: 1267 for v in self.glyphs.itervalues(): 1268 if hasattr(v, "data"): 1269 v.dropInstructionsFast() 1270 else: 1271 v.program = fontTools.ttLib.tables.ttProgram.Program() 1272 v.program.fromBytecode([]) 1273 return True 1274 1275@_add_method(fontTools.ttLib.getTableClass('CFF ')) 1276def prune_pre_subset(self, options): 1277 cff = self.cff 1278 # CFF table must have one font only 1279 cff.fontNames = cff.fontNames[:1] 1280 1281 if options.notdef_glyph and not options.notdef_outline: 1282 for fontname in cff.keys(): 1283 font = cff[fontname] 1284 c,_ = font.CharStrings.getItemAndSelector('.notdef') 1285 # XXX we should preserve the glyph width 1286 c.bytecode = '\x0e' # endchar 1287 c.program = None 1288 1289 return True # bool(cff.fontNames) 1290 1291@_add_method(fontTools.ttLib.getTableClass('CFF ')) 1292def subset_glyphs(self, s): 1293 cff = self.cff 1294 for fontname in cff.keys(): 1295 font = cff[fontname] 1296 cs = font.CharStrings 1297 1298 # Load all glyphs 1299 for g in font.charset: 1300 if g not in s.glyphs: continue 1301 c,sel = cs.getItemAndSelector(g) 1302 1303 if cs.charStringsAreIndexed: 1304 indices = [i for i,g in enumerate(font.charset) if g in s.glyphs] 1305 csi = cs.charStringsIndex 1306 csi.items = [csi.items[i] for i in indices] 1307 csi.count = len(csi.items) 1308 del csi.file, csi.offsets 1309 if hasattr(font, "FDSelect"): 1310 sel = font.FDSelect 1311 sel.format = None 1312 sel.gidArray = [sel.gidArray[i] for i in indices] 1313 cs.charStrings = dict((g,indices.index(v)) 1314 for g,v in cs.charStrings.iteritems() 1315 if g in s.glyphs) 1316 else: 1317 cs.charStrings = dict((g,v) 1318 for g,v in cs.charStrings.iteritems() 1319 if g in s.glyphs) 1320 font.charset = [g for g in font.charset if g in s.glyphs] 1321 font.numGlyphs = len(font.charset) 1322 1323 return True # any(cff[fontname].numGlyphs for fontname in cff.keys()) 1324 1325@_add_method(fontTools.misc.psCharStrings.T2CharString) 1326def subset_subroutines(self, subrs, gsubrs): 1327 p = self.program 1328 assert len(p) 1329 for i in xrange(1, len(p)): 1330 if p[i] == 'callsubr': 1331 assert type(p[i-1]) is int 1332 p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias 1333 elif p[i] == 'callgsubr': 1334 assert type(p[i-1]) is int 1335 p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias 1336 1337@_add_method(fontTools.misc.psCharStrings.T2CharString) 1338def drop_hints(self): 1339 hints = self._hints 1340 1341 if hints.has_hint: 1342 self.program = self.program[hints.last_hint:] 1343 if hasattr(self, 'width'): 1344 # Insert width back if needed 1345 if self.width != self.private.defaultWidthX: 1346 self.program.insert(0, self.width - self.private.nominalWidthX) 1347 1348 if hints.has_hintmask: 1349 i = 0 1350 p = self.program 1351 while i < len(p): 1352 if p[i] in ['hintmask', 'cntrmask']: 1353 assert i + 1 <= len(p) 1354 del p[i:i+2] 1355 continue 1356 i += 1 1357 1358 assert len(self.program) 1359 1360 del self._hints 1361 1362class _MarkingT2Decompiler(fontTools.misc.psCharStrings.SimpleT2Decompiler): 1363 1364 def __init__(self, localSubrs, globalSubrs): 1365 fontTools.misc.psCharStrings.SimpleT2Decompiler.__init__(self, 1366 localSubrs, 1367 globalSubrs) 1368 for subrs in [localSubrs, globalSubrs]: 1369 if subrs and not hasattr(subrs, "_used"): 1370 subrs._used = set() 1371 1372 def op_callsubr(self, index): 1373 self.localSubrs._used.add(self.operandStack[-1]+self.localBias) 1374 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) 1375 1376 def op_callgsubr(self, index): 1377 self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias) 1378 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) 1379 1380class _DehintingT2Decompiler(fontTools.misc.psCharStrings.SimpleT2Decompiler): 1381 1382 class Hints: 1383 def __init__(self): 1384 # Whether calling this charstring produces any hint stems 1385 self.has_hint = False 1386 # Index to start at to drop all hints 1387 self.last_hint = 0 1388 # Index up to which we know more hints are possible. Only 1389 # relevant if status is 0 or 1. 1390 self.last_checked = 0 1391 # The status means: 1392 # 0: after dropping hints, this charstring is empty 1393 # 1: after dropping hints, there may be more hints continuing after this 1394 # 2: no more hints possible after this charstring 1395 self.status = 0 1396 # Has hintmask instructions; not recursive 1397 self.has_hintmask = False 1398 pass 1399 1400 def __init__(self, css, localSubrs, globalSubrs): 1401 self._css = css 1402 fontTools.misc.psCharStrings.SimpleT2Decompiler.__init__(self, 1403 localSubrs, 1404 globalSubrs) 1405 1406 def execute(self, charString): 1407 old_hints = charString._hints if hasattr(charString, '_hints') else None 1408 charString._hints = self.Hints() 1409 1410 fontTools.misc.psCharStrings.SimpleT2Decompiler.execute(self, charString) 1411 1412 hints = charString._hints 1413 1414 if hints.has_hint or hints.has_hintmask: 1415 self._css.add(charString) 1416 1417 if hints.status != 2: 1418 # Check from last_check, make sure we didn't have any operators. 1419 for i in xrange(hints.last_checked, len(charString.program) - 1): 1420 if type(charString.program[i]) == str: 1421 hints.status = 2 1422 break; 1423 else: 1424 hints.status = 1 # There's *something* here 1425 hints.last_checked = len(charString.program) 1426 1427 if old_hints: 1428 assert hints.__dict__ == old_hints.__dict__ 1429 1430 def op_callsubr(self, index): 1431 subr = self.localSubrs[self.operandStack[-1]+self.localBias] 1432 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callsubr(self, index) 1433 self.processSubr(index, subr) 1434 1435 def op_callgsubr(self, index): 1436 subr = self.globalSubrs[self.operandStack[-1]+self.globalBias] 1437 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index) 1438 self.processSubr(index, subr) 1439 1440 def op_hstem(self, index): 1441 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hstem(self, index) 1442 self.processHint(index) 1443 def op_vstem(self, index): 1444 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_vstem(self, index) 1445 self.processHint(index) 1446 def op_hstemhm(self, index): 1447 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hstemhm(self, index) 1448 self.processHint(index) 1449 def op_vstemhm(self, index): 1450 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_vstemhm(self, index) 1451 self.processHint(index) 1452 def op_hintmask(self, index): 1453 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_hintmask(self, index) 1454 self.processHintmask(index) 1455 def op_cntrmask(self, index): 1456 fontTools.misc.psCharStrings.SimpleT2Decompiler.op_cntrmask(self, index) 1457 self.processHintmask(index) 1458 1459 def processHintmask(self, index): 1460 cs = self.callingStack[-1] 1461 hints = cs._hints 1462 hints.has_hintmask = True 1463 if hints.status != 2 and hints.has_hint: 1464 # Check from last_check, see if we may be an implicit vstem 1465 for i in xrange(hints.last_checked, index - 1): 1466 if type(cs.program[i]) == str: 1467 hints.status = 2 1468 break; 1469 if hints.status != 2: 1470 # We are an implicit vstem 1471 hints.last_hint = index + 1 1472 hints.status = 0 1473 hints.last_checked = index + 1 1474 1475 def processHint(self, index): 1476 cs = self.callingStack[-1] 1477 hints = cs._hints 1478 hints.has_hint = True 1479 hints.last_hint = index 1480 hints.last_checked = index 1481 1482 def processSubr(self, index, subr): 1483 cs = self.callingStack[-1] 1484 hints = cs._hints 1485 subr_hints = subr._hints 1486 1487 if subr_hints.has_hint: 1488 if hints.status != 2: 1489 hints.has_hint = True 1490 hints.last_checked = index 1491 hints.status = subr_hints.status 1492 # Decide where to chop off from 1493 if subr_hints.status == 0: 1494 hints.last_hint = index 1495 else: 1496 hints.last_hint = index - 2 # Leave the subr call in 1497 else: 1498 # In my understanding, this is a font bug. Ie. it has hint stems 1499 # *after* path construction. I've seen this in widespread fonts. 1500 # Best to ignore the hints I suppose... 1501 pass 1502 #assert 0 1503 else: 1504 hints.status = max(hints.status, subr_hints.status) 1505 if hints.status != 2: 1506 # Check from last_check, make sure we didn't have 1507 # any operators. 1508 for i in xrange(hints.last_checked, index - 1): 1509 if type(cs.program[i]) == str: 1510 hints.status = 2 1511 break; 1512 hints.last_checked = index 1513 1514@_add_method(fontTools.ttLib.getTableClass('CFF ')) 1515def prune_post_subset(self, options): 1516 cff = self.cff 1517 for fontname in cff.keys(): 1518 font = cff[fontname] 1519 cs = font.CharStrings 1520 1521 1522 # 1523 # Drop unused FontDictionaries 1524 # 1525 if hasattr(font, "FDSelect"): 1526 sel = font.FDSelect 1527 indices = _uniq_sort(sel.gidArray) 1528 sel.gidArray = [indices.index (ss) for ss in sel.gidArray] 1529 arr = font.FDArray 1530 arr.items = [arr[i] for i in indices] 1531 arr.count = len(arr.items) 1532 del arr.file, arr.offsets 1533 1534 1535 # 1536 # Drop hints if not needed 1537 # 1538 if not options.hinting: 1539 1540 # 1541 # This can be tricky, but doesn't have to. What we do is: 1542 # 1543 # - Run all used glyph charstrings and recurse into subroutines, 1544 # - For each charstring (including subroutines), if it has any 1545 # of the hint stem operators, we mark it as such. Upon returning, 1546 # for each charstring we note all the subroutine calls it makes 1547 # that (recursively) contain a stem, 1548 # - Dropping hinting then consists of the following two ops: 1549 # * Drop the piece of the program in each charstring before the 1550 # last call to a stem op or a stem-calling subroutine, 1551 # * Drop all hintmask operations. 1552 # - It's trickier... A hintmask right after hints and a few numbers 1553 # will act as an implicit vstemhm. As such, we track whether 1554 # we have seen any non-hint operators so far and do the right 1555 # thing, recursively... Good luck understanding that :( 1556 # 1557 css = set() 1558 for g in font.charset: 1559 c,sel = cs.getItemAndSelector(g) 1560 # Make sure it's decompiled. We want our "decompiler" to walk 1561 # the program, not the bytecode. 1562 c.draw(fontTools.pens.basePen.NullPen()) 1563 subrs = getattr(c.private, "Subrs", []) 1564 decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs) 1565 decompiler.execute(c) 1566 for charstring in css: 1567 charstring.drop_hints() 1568 1569 1570 # 1571 # Renumber subroutines to remove unused ones 1572 # 1573 1574 # Mark all used subroutines 1575 for g in font.charset: 1576 c,sel = cs.getItemAndSelector(g) 1577 subrs = getattr(c.private, "Subrs", []) 1578 decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs) 1579 decompiler.execute(c) 1580 1581 all_subrs = [font.GlobalSubrs] 1582 if hasattr(font, 'FDSelect'): 1583 all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs) 1584 elif hasattr(font.Private, 'Subrs') and font.Private.Subrs: 1585 all_subrs.append(font.Private.Subrs) 1586 1587 subrs = set(subrs) # Remove duplicates 1588 1589 # Prepare 1590 for subrs in all_subrs: 1591 if not hasattr(subrs, '_used'): 1592 subrs._used = set() 1593 subrs._used = _uniq_sort(subrs._used) 1594 subrs._old_bias = fontTools.misc.psCharStrings.calcSubrBias(subrs) 1595 subrs._new_bias = fontTools.misc.psCharStrings.calcSubrBias(subrs._used) 1596 1597 # Renumber glyph charstrings 1598 for g in font.charset: 1599 c,sel = cs.getItemAndSelector(g) 1600 subrs = getattr(c.private, "Subrs", []) 1601 c.subset_subroutines (subrs, font.GlobalSubrs) 1602 1603 # Renumber subroutines themselves 1604 for subrs in all_subrs: 1605 1606 if subrs == font.GlobalSubrs: 1607 if not hasattr(font, 'FDSelect') and hasattr(font.Private, 'Subrs'): 1608 local_subrs = font.Private.Subrs 1609 else: 1610 local_subrs = [] 1611 else: 1612 local_subrs = subrs 1613 1614 subrs.items = [subrs.items[i] for i in subrs._used] 1615 subrs.count = len(subrs.items) 1616 del subrs.file 1617 if hasattr(subrs, 'offsets'): 1618 del subrs.offsets 1619 1620 for i in xrange (subrs.count): 1621 subrs[i].subset_subroutines (local_subrs, font.GlobalSubrs) 1622 1623 # Cleanup 1624 for subrs in all_subrs: 1625 del subrs._used, subrs._old_bias, subrs._new_bias 1626 1627 return True 1628 1629@_add_method(fontTools.ttLib.getTableClass('cmap')) 1630def closure_glyphs(self, s): 1631 tables = [t for t in self.tables 1632 if t.platformID == 3 and t.platEncID in [1, 10]] 1633 for u in s.unicodes_requested: 1634 found = False 1635 for table in tables: 1636 if u in table.cmap: 1637 s.glyphs.add(table.cmap[u]) 1638 found = True 1639 break 1640 if not found: 1641 s.log("No glyph for Unicode value %s; skipping." % u) 1642 1643@_add_method(fontTools.ttLib.getTableClass('cmap')) 1644def prune_pre_subset(self, options): 1645 if not options.legacy_cmap: 1646 # Drop non-Unicode / non-Symbol cmaps 1647 self.tables = [t for t in self.tables 1648 if t.platformID == 3 and t.platEncID in [0, 1, 10]] 1649 if not options.symbol_cmap: 1650 self.tables = [t for t in self.tables 1651 if t.platformID == 3 and t.platEncID in [1, 10]] 1652 # TODO(behdad) Only keep one subtable? 1653 # For now, drop format=0 which can't be subset_glyphs easily? 1654 self.tables = [t for t in self.tables if t.format != 0] 1655 return bool(self.tables) 1656 1657@_add_method(fontTools.ttLib.getTableClass('cmap')) 1658def subset_glyphs(self, s): 1659 s.glyphs = s.glyphs_cmaped 1660 for t in self.tables: 1661 # For reasons I don't understand I need this here 1662 # to force decompilation of the cmap format 14. 1663 try: 1664 getattr(t, "asdf") 1665 except AttributeError: 1666 pass 1667 if t.format == 14: 1668 # TODO(behdad) XXX We drop all the default-UVS mappings(g==None). 1669 t.uvsDict = dict((v,[(u,g) for u,g in l if g in s.glyphs]) 1670 for v,l in t.uvsDict.iteritems()) 1671 t.uvsDict = dict((v,l) for v,l in t.uvsDict.iteritems() if l) 1672 else: 1673 t.cmap = dict((u,g) for u,g in t.cmap.iteritems() 1674 if g in s.glyphs_requested or u in s.unicodes_requested) 1675 self.tables = [t for t in self.tables 1676 if (t.cmap if t.format != 14 else t.uvsDict)] 1677 # TODO(behdad) Convert formats when needed. 1678 # In particular, if we have a format=12 without non-BMP 1679 # characters, either drop format=12 one or convert it 1680 # to format=4 if there's not one. 1681 return bool(self.tables) 1682 1683@_add_method(fontTools.ttLib.getTableClass('name')) 1684def prune_pre_subset(self, options): 1685 if '*' not in options.name_IDs: 1686 self.names = [n for n in self.names if n.nameID in options.name_IDs] 1687 if not options.name_legacy: 1688 self.names = [n for n in self.names 1689 if n.platformID == 3 and n.platEncID == 1] 1690 if '*' not in options.name_languages: 1691 self.names = [n for n in self.names if n.langID in options.name_languages] 1692 return True # Retain even if empty 1693 1694 1695# TODO(behdad) OS/2 ulUnicodeRange / ulCodePageRange? 1696# TODO(behdad) Drop unneeded GSUB/GPOS Script/LangSys entries. 1697# TODO(behdad) Drop empty GSUB/GPOS, and GDEF if no GSUB/GPOS left 1698# TODO(behdad) Drop GDEF subitems if unused by lookups 1699# TODO(behdad) Avoid recursing too much (in GSUB/GPOS and in CFF) 1700# TODO(behdad) Text direction considerations. 1701# TODO(behdad) Text script / language considerations. 1702# TODO(behdad) Option to drop hmtx for CFF? 1703 1704 1705class Options(object): 1706 1707 class UnknownOptionError(Exception): 1708 pass 1709 1710 _drop_tables_default = ['BASE', 'JSTF', 'DSIG', 'EBDT', 'EBLC', 'EBSC', 'SVG ', 1711 'PCLT', 'LTSH'] 1712 _drop_tables_default += ['Feat', 'Glat', 'Gloc', 'Silf', 'Sill'] # Graphite 1713 _drop_tables_default += ['CBLC', 'CBDT', 'sbix', 'COLR', 'CPAL'] # Color 1714 _no_subset_tables_default = ['gasp', 'head', 'hhea', 'maxp', 'vhea', 'OS/2', 1715 'loca', 'name', 'cvt ', 'fpgm', 'prep'] 1716 _hinting_tables_default = ['cvt ', 'fpgm', 'prep', 'hdmx', 'VDMX'] 1717 1718 # Based on HarfBuzz shapers 1719 _layout_features_groups = { 1720 # Default shaper 1721 'common': ['ccmp', 'liga', 'locl', 'mark', 'mkmk', 'rlig'], 1722 'horizontal': ['calt', 'clig', 'curs', 'kern', 'rclt'], 1723 'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'], 1724 'ltr': ['ltra', 'ltrm'], 1725 'rtl': ['rtla', 'rtlm'], 1726 # Complex shapers 1727 'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3', 1728 'cswh', 'mset'], 1729 'hangul': ['ljmo', 'vjmo', 'tjmo'], 1730 'tibetal': ['abvs', 'blws', 'abvm', 'blwm'], 1731 'indic': ['nukt', 'akhn', 'rphf', 'rkrf', 'pref', 'blwf', 'half', 1732 'abvf', 'pstf', 'cfar', 'vatu', 'cjct', 'init', 'pres', 1733 'abvs', 'blws', 'psts', 'haln', 'dist', 'abvm', 'blwm'], 1734 } 1735 _layout_features_default = _uniq_sort(sum( 1736 _layout_features_groups.itervalues(), [])) 1737 1738 drop_tables = _drop_tables_default 1739 no_subset_tables = _no_subset_tables_default 1740 hinting_tables = _hinting_tables_default 1741 layout_features = _layout_features_default 1742 hinting = False 1743 glyph_names = False 1744 legacy_cmap = False 1745 symbol_cmap = False 1746 name_IDs = [1, 2] # Family and Style 1747 name_legacy = False 1748 name_languages = [0x0409] # English 1749 notdef_glyph = True # gid0 for TrueType / .notdef for CFF 1750 notdef_outline = False # No need for notdef to have an outline really 1751 recommended_glyphs = False # gid1, gid2, gid3 for TrueType 1752 recalc_bounds = False # Recalculate font bounding boxes 1753 canonical_order = False # Order tables as recommended 1754 flavor = None # May be 'woff' 1755 1756 def __init__(self, **kwargs): 1757 1758 self.set(**kwargs) 1759 1760 def set(self, **kwargs): 1761 for k,v in kwargs.iteritems(): 1762 if not hasattr(self, k): 1763 raise self.UnknownOptionError("Unknown option '%s'" % k) 1764 setattr(self, k, v) 1765 1766 def parse_opts(self, argv, ignore_unknown=False): 1767 ret = [] 1768 opts = {} 1769 for a in argv: 1770 orig_a = a 1771 if not a.startswith('--'): 1772 ret.append(a) 1773 continue 1774 a = a[2:] 1775 i = a.find('=') 1776 op = '=' 1777 if i == -1: 1778 if a.startswith("no-"): 1779 k = a[3:] 1780 v = False 1781 else: 1782 k = a 1783 v = True 1784 else: 1785 k = a[:i] 1786 if k[-1] in "-+": 1787 op = k[-1]+'=' # Ops is '-=' or '+=' now. 1788 k = k[:-1] 1789 v = a[i+1:] 1790 k = k.replace('-', '_') 1791 if not hasattr(self, k): 1792 if ignore_unknown == True or k in ignore_unknown: 1793 ret.append(orig_a) 1794 continue 1795 else: 1796 raise self.UnknownOptionError("Unknown option '%s'" % a) 1797 1798 ov = getattr(self, k) 1799 if isinstance(ov, bool): 1800 v = bool(v) 1801 elif isinstance(ov, int): 1802 v = int(v) 1803 elif isinstance(ov, list): 1804 vv = v.split(',') 1805 if vv == ['']: 1806 vv = [] 1807 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv] 1808 if op == '=': 1809 v = vv 1810 elif op == '+=': 1811 v = ov 1812 v.extend(vv) 1813 elif op == '-=': 1814 v = ov 1815 for x in vv: 1816 if x in v: 1817 v.remove(x) 1818 else: 1819 assert 0 1820 1821 opts[k] = v 1822 self.set(**opts) 1823 1824 return ret 1825 1826 1827class Subsetter(object): 1828 1829 def __init__(self, options=None, log=None): 1830 1831 if not log: 1832 log = Logger() 1833 if not options: 1834 options = Options() 1835 1836 self.options = options 1837 self.log = log 1838 self.unicodes_requested = set() 1839 self.glyphs_requested = set() 1840 self.glyphs = set() 1841 1842 def populate(self, glyphs=[], unicodes=[], text=""): 1843 self.unicodes_requested.update(unicodes) 1844 if isinstance(text, str): 1845 text = text.decode("utf8") 1846 for u in text: 1847 self.unicodes_requested.add(ord(u)) 1848 self.glyphs_requested.update(glyphs) 1849 self.glyphs.update(glyphs) 1850 1851 def _prune_pre_subset(self, font): 1852 1853 for tag in font.keys(): 1854 if tag == 'GlyphOrder': continue 1855 1856 if(tag in self.options.drop_tables or 1857 (tag in self.options.hinting_tables and not self.options.hinting)): 1858 self.log(tag, "dropped") 1859 del font[tag] 1860 continue 1861 1862 clazz = fontTools.ttLib.getTableClass(tag) 1863 1864 if hasattr(clazz, 'prune_pre_subset'): 1865 table = font[tag] 1866 self.log.lapse("load '%s'" % tag) 1867 retain = table.prune_pre_subset(self.options) 1868 self.log.lapse("prune '%s'" % tag) 1869 if not retain: 1870 self.log(tag, "pruned to empty; dropped") 1871 del font[tag] 1872 continue 1873 else: 1874 self.log(tag, "pruned") 1875 1876 def _closure_glyphs(self, font): 1877 1878 self.glyphs = self.glyphs_requested.copy() 1879 1880 if 'cmap' in font: 1881 font['cmap'].closure_glyphs(self) 1882 self.glyphs_cmaped = self.glyphs 1883 1884 if self.options.notdef_glyph: 1885 if 'glyf' in font: 1886 self.glyphs.add(font.getGlyphName(0)) 1887 self.log("Added gid0 to subset") 1888 else: 1889 self.glyphs.add('.notdef') 1890 self.log("Added .notdef to subset") 1891 if self.options.recommended_glyphs: 1892 if 'glyf' in font: 1893 for i in range(4): 1894 self.glyphs.add(font.getGlyphName(i)) 1895 self.log("Added first four glyphs to subset") 1896 1897 if 'GSUB' in font: 1898 self.log("Closing glyph list over 'GSUB': %d glyphs before" % 1899 len(self.glyphs)) 1900 self.log.glyphs(self.glyphs, font=font) 1901 font['GSUB'].closure_glyphs(self) 1902 self.log("Closed glyph list over 'GSUB': %d glyphs after" % 1903 len(self.glyphs)) 1904 self.log.glyphs(self.glyphs, font=font) 1905 self.log.lapse("close glyph list over 'GSUB'") 1906 self.glyphs_gsubed = self.glyphs.copy() 1907 1908 if 'glyf' in font: 1909 self.log("Closing glyph list over 'glyf': %d glyphs before" % 1910 len(self.glyphs)) 1911 self.log.glyphs(self.glyphs, font=font) 1912 font['glyf'].closure_glyphs(self) 1913 self.log("Closed glyph list over 'glyf': %d glyphs after" % 1914 len(self.glyphs)) 1915 self.log.glyphs(self.glyphs, font=font) 1916 self.log.lapse("close glyph list over 'glyf'") 1917 self.glyphs_glyfed = self.glyphs.copy() 1918 1919 self.glyphs_all = self.glyphs.copy() 1920 1921 self.log("Retaining %d glyphs: " % len(self.glyphs_all)) 1922 1923 def _subset_glyphs(self, font): 1924 for tag in font.keys(): 1925 if tag == 'GlyphOrder': continue 1926 clazz = fontTools.ttLib.getTableClass(tag) 1927 1928 if tag in self.options.no_subset_tables: 1929 self.log(tag, "subsetting not needed") 1930 elif hasattr(clazz, 'subset_glyphs'): 1931 table = font[tag] 1932 self.glyphs = self.glyphs_all 1933 retain = table.subset_glyphs(self) 1934 self.glyphs = self.glyphs_all 1935 self.log.lapse("subset '%s'" % tag) 1936 if not retain: 1937 self.log(tag, "subsetted to empty; dropped") 1938 del font[tag] 1939 else: 1940 self.log(tag, "subsetted") 1941 else: 1942 self.log(tag, "NOT subset; don't know how to subset; dropped") 1943 del font[tag] 1944 1945 glyphOrder = font.getGlyphOrder() 1946 glyphOrder = [g for g in glyphOrder if g in self.glyphs_all] 1947 font.setGlyphOrder(glyphOrder) 1948 font._buildReverseGlyphOrderDict() 1949 self.log.lapse("subset GlyphOrder") 1950 1951 def _prune_post_subset(self, font): 1952 for tag in font.keys(): 1953 if tag == 'GlyphOrder': continue 1954 clazz = fontTools.ttLib.getTableClass(tag) 1955 if hasattr(clazz, 'prune_post_subset'): 1956 table = font[tag] 1957 retain = table.prune_post_subset(self.options) 1958 self.log.lapse("prune '%s'" % tag) 1959 if not retain: 1960 self.log(tag, "pruned to empty; dropped") 1961 del font[tag] 1962 else: 1963 self.log(tag, "pruned") 1964 1965 def subset(self, font): 1966 1967 self._prune_pre_subset(font) 1968 self._closure_glyphs(font) 1969 self._subset_glyphs(font) 1970 self._prune_post_subset(font) 1971 1972 1973class Logger(object): 1974 1975 def __init__(self, verbose=False, xml=False, timing=False): 1976 self.verbose = verbose 1977 self.xml = xml 1978 self.timing = timing 1979 self.last_time = self.start_time = time.time() 1980 1981 def parse_opts(self, argv): 1982 argv = argv[:] 1983 for v in ['verbose', 'xml', 'timing']: 1984 if "--"+v in argv: 1985 setattr(self, v, True) 1986 argv.remove("--"+v) 1987 return argv 1988 1989 def __call__(self, *things): 1990 if not self.verbose: 1991 return 1992 print ' '.join(str(x) for x in things) 1993 1994 def lapse(self, *things): 1995 if not self.timing: 1996 return 1997 new_time = time.time() 1998 print "Took %0.3fs to %s" %(new_time - self.last_time, 1999 ' '.join(str(x) for x in things)) 2000 self.last_time = new_time 2001 2002 def glyphs(self, glyphs, font=None): 2003 self("Names: ", sorted(glyphs)) 2004 if font: 2005 reverseGlyphMap = font.getReverseGlyphMap() 2006 self("Gids : ", sorted(reverseGlyphMap[g] for g in glyphs)) 2007 2008 def font(self, font, file=sys.stdout): 2009 if not self.xml: 2010 return 2011 from fontTools.misc import xmlWriter 2012 writer = xmlWriter.XMLWriter(file) 2013 font.disassembleInstructions = False # Work around ttLib bug 2014 for tag in font.keys(): 2015 writer.begintag(tag) 2016 writer.newline() 2017 font[tag].toXML(writer, font) 2018 writer.endtag(tag) 2019 writer.newline() 2020 2021 2022def load_font(fontFile, 2023 options, 2024 checkChecksums=False, 2025 dontLoadGlyphNames=False): 2026 2027 font = fontTools.ttLib.TTFont(fontFile, 2028 checkChecksums=checkChecksums, 2029 recalcBBoxes=options.recalc_bounds) 2030 2031 # Hack: 2032 # 2033 # If we don't need glyph names, change 'post' class to not try to 2034 # load them. It avoid lots of headache with broken fonts as well 2035 # as loading time. 2036 # 2037 # Ideally ttLib should provide a way to ask it to skip loading 2038 # glyph names. But it currently doesn't provide such a thing. 2039 # 2040 if dontLoadGlyphNames: 2041 post = fontTools.ttLib.getTableClass('post') 2042 saved = post.decode_format_2_0 2043 post.decode_format_2_0 = post.decode_format_3_0 2044 f = font['post'] 2045 if f.formatType == 2.0: 2046 f.formatType = 3.0 2047 post.decode_format_2_0 = saved 2048 2049 return font 2050 2051def save_font(font, outfile, options): 2052 if options.flavor and not hasattr(font, 'flavor'): 2053 raise Exception("fonttools version does not support flavors.") 2054 font.flavor = options.flavor 2055 font.save(outfile, reorderTables=options.canonical_order) 2056 2057def main(args): 2058 2059 log = Logger() 2060 args = log.parse_opts(args) 2061 2062 options = Options() 2063 args = options.parse_opts(args, ignore_unknown=['text']) 2064 2065 if len(args) < 2: 2066 print >>sys.stderr, "usage: pyftsubset font-file glyph... [--text=ABC]... [--option=value]..." 2067 sys.exit(1) 2068 2069 fontfile = args[0] 2070 args = args[1:] 2071 2072 dontLoadGlyphNames =(not options.glyph_names and 2073 all(any(g.startswith(p) 2074 for p in ['gid', 'glyph', 'uni', 'U+']) 2075 for g in args)) 2076 2077 font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames) 2078 subsetter = Subsetter(options=options, log=log) 2079 log.lapse("load font") 2080 2081 names = font.getGlyphNames() 2082 log.lapse("loading glyph names") 2083 2084 glyphs = [] 2085 unicodes = [] 2086 text = "" 2087 for g in args: 2088 if g == '*': 2089 glyphs.extend(font.getGlyphOrder()) 2090 continue 2091 if g in names: 2092 glyphs.append(g) 2093 continue 2094 if g.startswith('--text='): 2095 text += g[7:] 2096 continue 2097 if g.startswith('uni') or g.startswith('U+'): 2098 if g.startswith('uni') and len(g) > 3: 2099 g = g[3:] 2100 elif g.startswith('U+') and len(g) > 2: 2101 g = g[2:] 2102 u = int(g, 16) 2103 unicodes.append(u) 2104 continue 2105 if g.startswith('gid') or g.startswith('glyph'): 2106 if g.startswith('gid') and len(g) > 3: 2107 g = g[3:] 2108 elif g.startswith('glyph') and len(g) > 5: 2109 g = g[5:] 2110 try: 2111 glyphs.append(font.getGlyphName(int(g), requireReal=1)) 2112 except ValueError: 2113 raise Exception("Invalid glyph identifier: %s" % g) 2114 continue 2115 raise Exception("Invalid glyph identifier: %s" % g) 2116 log.lapse("compile glyph list") 2117 log("Unicodes:", unicodes) 2118 log("Glyphs:", glyphs) 2119 2120 subsetter.populate(glyphs=glyphs, unicodes=unicodes, text=text) 2121 subsetter.subset(font) 2122 2123 outfile = fontfile + '.subset' 2124 2125 save_font (font, outfile, options) 2126 log.lapse("compile and save font") 2127 2128 log.last_time = log.start_time 2129 log.lapse("make one with everything(TOTAL TIME)") 2130 2131 if log.verbose: 2132 import os 2133 log("Input font: %d bytes" % os.path.getsize(fontfile)) 2134 log("Subset font: %d bytes" % os.path.getsize(outfile)) 2135 2136 log.font(font) 2137 2138 font.close() 2139 2140 2141__all__ = [ 2142 'Options', 2143 'Subsetter', 2144 'Logger', 2145 'load_font', 2146 'save_font', 2147 'main' 2148] 2149 2150if __name__ == '__main__': 2151 main(sys.argv[1:]) 2152