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