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 10from __future__ import print_function, division, absolute_import 11from fontTools.misc.py23 import * 12from fontTools import ttLib 13from fontTools.ttLib.tables import otTables 14from fontTools.misc import psCharStrings 15from fontTools.pens import basePen 16import sys 17import struct 18import time 19import array 20 21 22def _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 35def _uniq_sort(l): 36 return sorted(set(l)) 37 38def _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) 46def 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) 51def 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) 56def 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) 63def remap(self, coverage_map): 64 "Remaps coverage." 65 self.glyphs = [self.glyphs[i] for i in coverage_map] 66 67@_add_method(otTables.ClassDef) 68def 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) 75def 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) 83def 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) 96def 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) 102def 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) 107def 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) 113def 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) 119def 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) 131def 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) 137def 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) 145def 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) 153def 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) 164def 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) 177def 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) 194def 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) 206def 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) 213def 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) 242def 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) 250def 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) 260def prune_hints(self): 261 # Drop device tables / contour anchor point 262 self.ensureDecompiled() 263 self.Format = 1 264 265@_add_method(otTables.CursivePos) 266def 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) 274def 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) 298def 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) 310def 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) 335def 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) 348def 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) 372def 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) 395def 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) 409def 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) 427def 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) 439def prune_post_subset(self, options): 440 return True 441 442@_add_method(otTables.SingleSubst, 443 otTables.AlternateSubst, 444 otTables.ReverseChainSingleSubst) 445def may_have_non_1to1(self): 446 return False 447 448@_add_method(otTables.MultipleSubst, 449 otTables.LigatureSubst, 450 otTables.ContextSubst, 451 otTables.ChainContextSubst) 452def may_have_non_1to1(self): 453 return True 454 455@_add_method(otTables.ContextSubst, 456 otTables.ChainContextSubst, 457 otTables.ContextPos, 458 otTables.ChainContextPos) 459def __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) 555def 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) 640def 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) 701def 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) 729def 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) 744def 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) 751def 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) 759def 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) 767def 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) 775def 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) 783def 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) 791def 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) 798def 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) 804def 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) 812def 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) 818def 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) 826def subset_lookups(self, lookup_indices): 827 for s in self.SubTable: 828 s.subset_lookups(lookup_indices) 829 830@_add_method(otTables.Lookup) 831def 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) 836def 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) 840def 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) 848def 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) 853def 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) 861def 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) 870def 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) 885def 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) 895def collect_lookups(self): 896 return self.LookupListIndex[:] 897 898@_add_method(otTables.FeatureList) 899def 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) 912def 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) 918def 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) 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(otTables.DefaultLangSys, 939 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(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(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(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(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(ttLib.getTableClass('GSUB')) 976def 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')) 999def 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')) 1011def 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')) 1025def 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')) 1043def 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')) 1056def 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')) 1069def 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')) 1082def 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')) 1114def 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')) 1148def 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')) 1182def 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')) 1204def 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')) 1210def 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')) 1219def 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')) 1224def 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')) 1229def 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')) 1235def 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')) 1242def 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')) 1248def 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) 1253def 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')) 1279def 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')) 1297def 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')) 1306def 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')) 1319def 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 ')) 1326def 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 ')) 1342def 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) 1376def 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) 1388def 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 1414class _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 1432class _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 ')) 1573def 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')) 1701def 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')) 1720def 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')) 1733def 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')) 1765def 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 1790class 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 1913class 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 2067class 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 2117def 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 2151def 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 2157def 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 2250if __name__ == '__main__': 2251 main(sys.argv[1:]) 2252