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