1from fontTools.misc.py23 import byteord, tobytes 2from fontTools.feaLib.error import FeatureLibError 3from fontTools.feaLib.location import FeatureLibLocation 4from fontTools.misc.encodingTools import getEncoding 5from collections import OrderedDict 6import itertools 7 8SHIFT = " " * 4 9 10__all__ = [ 11 "Element", 12 "FeatureFile", 13 "Comment", 14 "GlyphName", 15 "GlyphClass", 16 "GlyphClassName", 17 "MarkClassName", 18 "AnonymousBlock", 19 "Block", 20 "FeatureBlock", 21 "NestedBlock", 22 "LookupBlock", 23 "GlyphClassDefinition", 24 "GlyphClassDefStatement", 25 "MarkClass", 26 "MarkClassDefinition", 27 "AlternateSubstStatement", 28 "Anchor", 29 "AnchorDefinition", 30 "AttachStatement", 31 "AxisValueLocationStatement", 32 "BaseAxis", 33 "CVParametersNameStatement", 34 "ChainContextPosStatement", 35 "ChainContextSubstStatement", 36 "CharacterStatement", 37 "CursivePosStatement", 38 "ElidedFallbackName", 39 "ElidedFallbackNameID", 40 "Expression", 41 "FeatureNameStatement", 42 "FeatureReferenceStatement", 43 "FontRevisionStatement", 44 "HheaField", 45 "IgnorePosStatement", 46 "IgnoreSubstStatement", 47 "IncludeStatement", 48 "LanguageStatement", 49 "LanguageSystemStatement", 50 "LigatureCaretByIndexStatement", 51 "LigatureCaretByPosStatement", 52 "LigatureSubstStatement", 53 "LookupFlagStatement", 54 "LookupReferenceStatement", 55 "MarkBasePosStatement", 56 "MarkLigPosStatement", 57 "MarkMarkPosStatement", 58 "MultipleSubstStatement", 59 "NameRecord", 60 "OS2Field", 61 "PairPosStatement", 62 "ReverseChainSingleSubstStatement", 63 "ScriptStatement", 64 "SinglePosStatement", 65 "SingleSubstStatement", 66 "SizeParameters", 67 "Statement", 68 "STATAxisValueStatement", 69 "STATDesignAxisStatement", 70 "STATNameStatement", 71 "SubtableStatement", 72 "TableBlock", 73 "ValueRecord", 74 "ValueRecordDefinition", 75 "VheaField", 76] 77 78 79def deviceToString(device): 80 if device is None: 81 return "<device NULL>" 82 else: 83 return "<device %s>" % ", ".join("%d %d" % t for t in device) 84 85 86fea_keywords = set( 87 [ 88 "anchor", 89 "anchordef", 90 "anon", 91 "anonymous", 92 "by", 93 "contour", 94 "cursive", 95 "device", 96 "enum", 97 "enumerate", 98 "excludedflt", 99 "exclude_dflt", 100 "feature", 101 "from", 102 "ignore", 103 "ignorebaseglyphs", 104 "ignoreligatures", 105 "ignoremarks", 106 "include", 107 "includedflt", 108 "include_dflt", 109 "language", 110 "languagesystem", 111 "lookup", 112 "lookupflag", 113 "mark", 114 "markattachmenttype", 115 "markclass", 116 "nameid", 117 "null", 118 "parameters", 119 "pos", 120 "position", 121 "required", 122 "righttoleft", 123 "reversesub", 124 "rsub", 125 "script", 126 "sub", 127 "substitute", 128 "subtable", 129 "table", 130 "usemarkfilteringset", 131 "useextension", 132 "valuerecorddef", 133 "base", 134 "gdef", 135 "head", 136 "hhea", 137 "name", 138 "vhea", 139 "vmtx", 140 ] 141) 142 143 144def asFea(g): 145 if hasattr(g, "asFea"): 146 return g.asFea() 147 elif isinstance(g, tuple) and len(g) == 2: 148 return asFea(g[0]) + " - " + asFea(g[1]) # a range 149 elif g.lower() in fea_keywords: 150 return "\\" + g 151 else: 152 return g 153 154 155class Element(object): 156 """A base class representing "something" in a feature file.""" 157 158 def __init__(self, location=None): 159 #: location of this element as a `FeatureLibLocation` object. 160 if location and not isinstance(location, FeatureLibLocation): 161 location = FeatureLibLocation(*location) 162 self.location = location 163 164 def build(self, builder): 165 pass 166 167 def asFea(self, indent=""): 168 """Returns this element as a string of feature code. For block-type 169 elements (such as :class:`FeatureBlock`), the `indent` string is 170 added to the start of each line in the output.""" 171 raise NotImplementedError 172 173 def __str__(self): 174 return self.asFea() 175 176 177class Statement(Element): 178 pass 179 180 181class Expression(Element): 182 pass 183 184 185class Comment(Element): 186 """A comment in a feature file.""" 187 188 def __init__(self, text, location=None): 189 super(Comment, self).__init__(location) 190 #: Text of the comment 191 self.text = text 192 193 def asFea(self, indent=""): 194 return self.text 195 196 197class NullGlyph(Expression): 198 """The NULL glyph, used in glyph deletion substitutions.""" 199 200 def __init__(self, location=None): 201 Expression.__init__(self, location) 202 #: The name itself as a string 203 204 def glyphSet(self): 205 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 206 return () 207 208 def asFea(self, indent=""): 209 return "NULL" 210 211 212class GlyphName(Expression): 213 """A single glyph name, such as ``cedilla``.""" 214 215 def __init__(self, glyph, location=None): 216 Expression.__init__(self, location) 217 #: The name itself as a string 218 self.glyph = glyph 219 220 def glyphSet(self): 221 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 222 return (self.glyph,) 223 224 def asFea(self, indent=""): 225 return asFea(self.glyph) 226 227 228class GlyphClass(Expression): 229 """A glyph class, such as ``[acute cedilla grave]``.""" 230 231 def __init__(self, glyphs=None, location=None): 232 Expression.__init__(self, location) 233 #: The list of glyphs in this class, as :class:`GlyphName` objects. 234 self.glyphs = glyphs if glyphs is not None else [] 235 self.original = [] 236 self.curr = 0 237 238 def glyphSet(self): 239 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 240 return tuple(self.glyphs) 241 242 def asFea(self, indent=""): 243 if len(self.original): 244 if self.curr < len(self.glyphs): 245 self.original.extend(self.glyphs[self.curr :]) 246 self.curr = len(self.glyphs) 247 return "[" + " ".join(map(asFea, self.original)) + "]" 248 else: 249 return "[" + " ".join(map(asFea, self.glyphs)) + "]" 250 251 def extend(self, glyphs): 252 """Add a list of :class:`GlyphName` objects to the class.""" 253 self.glyphs.extend(glyphs) 254 255 def append(self, glyph): 256 """Add a single :class:`GlyphName` object to the class.""" 257 self.glyphs.append(glyph) 258 259 def add_range(self, start, end, glyphs): 260 """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end`` 261 are either :class:`GlyphName` objects or strings representing the 262 start and end glyphs in the class, and ``glyphs`` is the full list of 263 :class:`GlyphName` objects in the range.""" 264 if self.curr < len(self.glyphs): 265 self.original.extend(self.glyphs[self.curr :]) 266 self.original.append((start, end)) 267 self.glyphs.extend(glyphs) 268 self.curr = len(self.glyphs) 269 270 def add_cid_range(self, start, end, glyphs): 271 """Add a range to the class by glyph ID. ``start`` and ``end`` are the 272 initial and final IDs, and ``glyphs`` is the full list of 273 :class:`GlyphName` objects in the range.""" 274 if self.curr < len(self.glyphs): 275 self.original.extend(self.glyphs[self.curr :]) 276 self.original.append(("\\{}".format(start), "\\{}".format(end))) 277 self.glyphs.extend(glyphs) 278 self.curr = len(self.glyphs) 279 280 def add_class(self, gc): 281 """Add glyphs from the given :class:`GlyphClassName` object to the 282 class.""" 283 if self.curr < len(self.glyphs): 284 self.original.extend(self.glyphs[self.curr :]) 285 self.original.append(gc) 286 self.glyphs.extend(gc.glyphSet()) 287 self.curr = len(self.glyphs) 288 289 290class GlyphClassName(Expression): 291 """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated 292 with a :class:`GlyphClassDefinition` object.""" 293 294 def __init__(self, glyphclass, location=None): 295 Expression.__init__(self, location) 296 assert isinstance(glyphclass, GlyphClassDefinition) 297 self.glyphclass = glyphclass 298 299 def glyphSet(self): 300 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 301 return tuple(self.glyphclass.glyphSet()) 302 303 def asFea(self, indent=""): 304 return "@" + self.glyphclass.name 305 306 307class MarkClassName(Expression): 308 """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``. 309 This must be instantiated with a :class:`MarkClass` object.""" 310 311 def __init__(self, markClass, location=None): 312 Expression.__init__(self, location) 313 assert isinstance(markClass, MarkClass) 314 self.markClass = markClass 315 316 def glyphSet(self): 317 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 318 return self.markClass.glyphSet() 319 320 def asFea(self, indent=""): 321 return "@" + self.markClass.name 322 323 324class AnonymousBlock(Statement): 325 """An anonymous data block.""" 326 327 def __init__(self, tag, content, location=None): 328 Statement.__init__(self, location) 329 self.tag = tag #: string containing the block's "tag" 330 self.content = content #: block data as string 331 332 def asFea(self, indent=""): 333 res = "anon {} {{\n".format(self.tag) 334 res += self.content 335 res += "}} {};\n\n".format(self.tag) 336 return res 337 338 339class Block(Statement): 340 """A block of statements: feature, lookup, etc.""" 341 342 def __init__(self, location=None): 343 Statement.__init__(self, location) 344 self.statements = [] #: Statements contained in the block 345 346 def build(self, builder): 347 """When handed a 'builder' object of comparable interface to 348 :class:`fontTools.feaLib.builder`, walks the statements in this 349 block, calling the builder callbacks.""" 350 for s in self.statements: 351 s.build(builder) 352 353 def asFea(self, indent=""): 354 indent += SHIFT 355 return ( 356 indent 357 + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements]) 358 + "\n" 359 ) 360 361 362class FeatureFile(Block): 363 """The top-level element of the syntax tree, containing the whole feature 364 file in its ``statements`` attribute.""" 365 366 def __init__(self): 367 Block.__init__(self, location=None) 368 self.markClasses = {} # name --> ast.MarkClass 369 370 def asFea(self, indent=""): 371 return "\n".join(s.asFea(indent=indent) for s in self.statements) 372 373 374class FeatureBlock(Block): 375 """A named feature block.""" 376 377 def __init__(self, name, use_extension=False, location=None): 378 Block.__init__(self, location) 379 self.name, self.use_extension = name, use_extension 380 381 def build(self, builder): 382 """Call the ``start_feature`` callback on the builder object, visit 383 all the statements in this feature, and then call ``end_feature``.""" 384 # TODO(sascha): Handle use_extension. 385 builder.start_feature(self.location, self.name) 386 # language exclude_dflt statements modify builder.features_ 387 # limit them to this block with temporary builder.features_ 388 features = builder.features_ 389 builder.features_ = {} 390 Block.build(self, builder) 391 for key, value in builder.features_.items(): 392 features.setdefault(key, []).extend(value) 393 builder.features_ = features 394 builder.end_feature() 395 396 def asFea(self, indent=""): 397 res = indent + "feature %s " % self.name.strip() 398 if self.use_extension: 399 res += "useExtension " 400 res += "{\n" 401 res += Block.asFea(self, indent=indent) 402 res += indent + "} %s;\n" % self.name.strip() 403 return res 404 405 406class NestedBlock(Block): 407 """A block inside another block, for example when found inside a 408 ``cvParameters`` block.""" 409 410 def __init__(self, tag, block_name, location=None): 411 Block.__init__(self, location) 412 self.tag = tag 413 self.block_name = block_name 414 415 def build(self, builder): 416 Block.build(self, builder) 417 if self.block_name == "ParamUILabelNameID": 418 builder.add_to_cv_num_named_params(self.tag) 419 420 def asFea(self, indent=""): 421 res = "{}{} {{\n".format(indent, self.block_name) 422 res += Block.asFea(self, indent=indent) 423 res += "{}}};\n".format(indent) 424 return res 425 426 427class LookupBlock(Block): 428 """A named lookup, containing ``statements``.""" 429 430 def __init__(self, name, use_extension=False, location=None): 431 Block.__init__(self, location) 432 self.name, self.use_extension = name, use_extension 433 434 def build(self, builder): 435 # TODO(sascha): Handle use_extension. 436 builder.start_lookup_block(self.location, self.name) 437 Block.build(self, builder) 438 builder.end_lookup_block() 439 440 def asFea(self, indent=""): 441 res = "lookup {} ".format(self.name) 442 if self.use_extension: 443 res += "useExtension " 444 res += "{\n" 445 res += Block.asFea(self, indent=indent) 446 res += "{}}} {};\n".format(indent, self.name) 447 return res 448 449 450class TableBlock(Block): 451 """A ``table ... { }`` block.""" 452 453 def __init__(self, name, location=None): 454 Block.__init__(self, location) 455 self.name = name 456 457 def asFea(self, indent=""): 458 res = "table {} {{\n".format(self.name.strip()) 459 res += super(TableBlock, self).asFea(indent=indent) 460 res += "}} {};\n".format(self.name.strip()) 461 return res 462 463 464class GlyphClassDefinition(Statement): 465 """Example: ``@UPPERCASE = [A-Z];``.""" 466 467 def __init__(self, name, glyphs, location=None): 468 Statement.__init__(self, location) 469 self.name = name #: class name as a string, without initial ``@`` 470 self.glyphs = glyphs #: a :class:`GlyphClass` object 471 472 def glyphSet(self): 473 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 474 return tuple(self.glyphs.glyphSet()) 475 476 def asFea(self, indent=""): 477 return "@" + self.name + " = " + self.glyphs.asFea() + ";" 478 479 480class GlyphClassDefStatement(Statement): 481 """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters 482 must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or 483 ``None``.""" 484 485 def __init__( 486 self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None 487 ): 488 Statement.__init__(self, location) 489 self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs) 490 self.ligatureGlyphs = ligatureGlyphs 491 self.componentGlyphs = componentGlyphs 492 493 def build(self, builder): 494 """Calls the builder's ``add_glyphClassDef`` callback.""" 495 base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple() 496 liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple() 497 mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple() 498 comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple() 499 builder.add_glyphClassDef(self.location, base, liga, mark, comp) 500 501 def asFea(self, indent=""): 502 return "GlyphClassDef {}, {}, {}, {};".format( 503 self.baseGlyphs.asFea() if self.baseGlyphs else "", 504 self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "", 505 self.markGlyphs.asFea() if self.markGlyphs else "", 506 self.componentGlyphs.asFea() if self.componentGlyphs else "", 507 ) 508 509 510class MarkClass(object): 511 """One `or more` ``markClass`` statements for the same mark class. 512 513 While glyph classes can be defined only once, the feature file format 514 allows expanding mark classes with multiple definitions, each using 515 different glyphs and anchors. The following are two ``MarkClassDefinitions`` 516 for the same ``MarkClass``:: 517 518 markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS; 519 markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS; 520 521 The ``MarkClass`` object is therefore just a container for a list of 522 :class:`MarkClassDefinition` statements. 523 """ 524 525 def __init__(self, name): 526 self.name = name 527 self.definitions = [] 528 self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions 529 530 def addDefinition(self, definition): 531 """Add a :class:`MarkClassDefinition` statement to this mark class.""" 532 assert isinstance(definition, MarkClassDefinition) 533 self.definitions.append(definition) 534 for glyph in definition.glyphSet(): 535 if glyph in self.glyphs: 536 otherLoc = self.glyphs[glyph].location 537 if otherLoc is None: 538 end = "" 539 else: 540 end = f" at {otherLoc}" 541 raise FeatureLibError( 542 "Glyph %s already defined%s" % (glyph, end), definition.location 543 ) 544 self.glyphs[glyph] = definition 545 546 def glyphSet(self): 547 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 548 return tuple(self.glyphs.keys()) 549 550 def asFea(self, indent=""): 551 res = "\n".join(d.asFea() for d in self.definitions) 552 return res 553 554 555class MarkClassDefinition(Statement): 556 """A single ``markClass`` statement. The ``markClass`` should be a 557 :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object, 558 and the ``glyphs`` parameter should be a `glyph-containing object`_ . 559 560 Example: 561 562 .. code:: python 563 564 mc = MarkClass("FRENCH_ACCENTS") 565 mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800), 566 GlyphClass([ GlyphName("acute"), GlyphName("grave") ]) 567 ) ) 568 mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200), 569 GlyphClass([ GlyphName("cedilla") ]) 570 ) ) 571 572 mc.asFea() 573 # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS; 574 # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS; 575 576 """ 577 578 def __init__(self, markClass, anchor, glyphs, location=None): 579 Statement.__init__(self, location) 580 assert isinstance(markClass, MarkClass) 581 assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression) 582 self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs 583 584 def glyphSet(self): 585 """The glyphs in this class as a tuple of :class:`GlyphName` objects.""" 586 return self.glyphs.glyphSet() 587 588 def asFea(self, indent=""): 589 return "markClass {} {} @{};".format( 590 self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name 591 ) 592 593 594class AlternateSubstStatement(Statement): 595 """A ``sub ... from ...`` statement. 596 597 ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of 598 `glyph-containing objects`_. ``glyph`` should be a `one element list`.""" 599 600 def __init__(self, prefix, glyph, suffix, replacement, location=None): 601 Statement.__init__(self, location) 602 self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix) 603 self.replacement = replacement 604 605 def build(self, builder): 606 """Calls the builder's ``add_alternate_subst`` callback.""" 607 glyph = self.glyph.glyphSet() 608 assert len(glyph) == 1, glyph 609 glyph = list(glyph)[0] 610 prefix = [p.glyphSet() for p in self.prefix] 611 suffix = [s.glyphSet() for s in self.suffix] 612 replacement = self.replacement.glyphSet() 613 builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement) 614 615 def asFea(self, indent=""): 616 res = "sub " 617 if len(self.prefix) or len(self.suffix): 618 if len(self.prefix): 619 res += " ".join(map(asFea, self.prefix)) + " " 620 res += asFea(self.glyph) + "'" # even though we really only use 1 621 if len(self.suffix): 622 res += " " + " ".join(map(asFea, self.suffix)) 623 else: 624 res += asFea(self.glyph) 625 res += " from " 626 res += asFea(self.replacement) 627 res += ";" 628 return res 629 630 631class Anchor(Expression): 632 """An ``Anchor`` element, used inside a ``pos`` rule. 633 634 If a ``name`` is given, this will be used in preference to the coordinates. 635 Other values should be integer. 636 """ 637 638 def __init__( 639 self, 640 x, 641 y, 642 name=None, 643 contourpoint=None, 644 xDeviceTable=None, 645 yDeviceTable=None, 646 location=None, 647 ): 648 Expression.__init__(self, location) 649 self.name = name 650 self.x, self.y, self.contourpoint = x, y, contourpoint 651 self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable 652 653 def asFea(self, indent=""): 654 if self.name is not None: 655 return "<anchor {}>".format(self.name) 656 res = "<anchor {} {}".format(self.x, self.y) 657 if self.contourpoint: 658 res += " contourpoint {}".format(self.contourpoint) 659 if self.xDeviceTable or self.yDeviceTable: 660 res += " " 661 res += deviceToString(self.xDeviceTable) 662 res += " " 663 res += deviceToString(self.yDeviceTable) 664 res += ">" 665 return res 666 667 668class AnchorDefinition(Statement): 669 """A named anchor definition. (2.e.viii). ``name`` should be a string.""" 670 671 def __init__(self, name, x, y, contourpoint=None, location=None): 672 Statement.__init__(self, location) 673 self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint 674 675 def asFea(self, indent=""): 676 res = "anchorDef {} {}".format(self.x, self.y) 677 if self.contourpoint: 678 res += " contourpoint {}".format(self.contourpoint) 679 res += " {};".format(self.name) 680 return res 681 682 683class AttachStatement(Statement): 684 """A ``GDEF`` table ``Attach`` statement.""" 685 686 def __init__(self, glyphs, contourPoints, location=None): 687 Statement.__init__(self, location) 688 self.glyphs = glyphs #: A `glyph-containing object`_ 689 self.contourPoints = contourPoints #: A list of integer contour points 690 691 def build(self, builder): 692 """Calls the builder's ``add_attach_points`` callback.""" 693 glyphs = self.glyphs.glyphSet() 694 builder.add_attach_points(self.location, glyphs, self.contourPoints) 695 696 def asFea(self, indent=""): 697 return "Attach {} {};".format( 698 self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints) 699 ) 700 701 702class ChainContextPosStatement(Statement): 703 """A chained contextual positioning statement. 704 705 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 706 `glyph-containing objects`_ . 707 708 ``lookups`` should be a list of elements representing what lookups 709 to apply at each glyph position. Each element should be a 710 :class:`LookupBlock` to apply a single chaining lookup at the given 711 position, a list of :class:`LookupBlock`\ s to apply multiple 712 lookups, or ``None`` to apply no lookup. The length of the outer 713 list should equal the length of ``glyphs``; the inner lists can be 714 of variable length.""" 715 716 def __init__(self, prefix, glyphs, suffix, lookups, location=None): 717 Statement.__init__(self, location) 718 self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix 719 self.lookups = list(lookups) 720 for i, lookup in enumerate(lookups): 721 if lookup: 722 try: 723 (_ for _ in lookup) 724 except TypeError: 725 self.lookups[i] = [lookup] 726 727 def build(self, builder): 728 """Calls the builder's ``add_chain_context_pos`` callback.""" 729 prefix = [p.glyphSet() for p in self.prefix] 730 glyphs = [g.glyphSet() for g in self.glyphs] 731 suffix = [s.glyphSet() for s in self.suffix] 732 builder.add_chain_context_pos( 733 self.location, prefix, glyphs, suffix, self.lookups 734 ) 735 736 def asFea(self, indent=""): 737 res = "pos " 738 if ( 739 len(self.prefix) 740 or len(self.suffix) 741 or any([x is not None for x in self.lookups]) 742 ): 743 if len(self.prefix): 744 res += " ".join(g.asFea() for g in self.prefix) + " " 745 for i, g in enumerate(self.glyphs): 746 res += g.asFea() + "'" 747 if self.lookups[i]: 748 for lu in self.lookups[i]: 749 res += " lookup " + lu.name 750 if i < len(self.glyphs) - 1: 751 res += " " 752 if len(self.suffix): 753 res += " " + " ".join(map(asFea, self.suffix)) 754 else: 755 res += " ".join(map(asFea, self.glyph)) 756 res += ";" 757 return res 758 759 760class ChainContextSubstStatement(Statement): 761 """A chained contextual substitution statement. 762 763 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 764 `glyph-containing objects`_ . 765 766 ``lookups`` should be a list of elements representing what lookups 767 to apply at each glyph position. Each element should be a 768 :class:`LookupBlock` to apply a single chaining lookup at the given 769 position, a list of :class:`LookupBlock`\ s to apply multiple 770 lookups, or ``None`` to apply no lookup. The length of the outer 771 list should equal the length of ``glyphs``; the inner lists can be 772 of variable length.""" 773 774 def __init__(self, prefix, glyphs, suffix, lookups, location=None): 775 Statement.__init__(self, location) 776 self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix 777 self.lookups = list(lookups) 778 for i, lookup in enumerate(lookups): 779 if lookup: 780 try: 781 (_ for _ in lookup) 782 except TypeError: 783 self.lookups[i] = [lookup] 784 785 def build(self, builder): 786 """Calls the builder's ``add_chain_context_subst`` callback.""" 787 prefix = [p.glyphSet() for p in self.prefix] 788 glyphs = [g.glyphSet() for g in self.glyphs] 789 suffix = [s.glyphSet() for s in self.suffix] 790 builder.add_chain_context_subst( 791 self.location, prefix, glyphs, suffix, self.lookups 792 ) 793 794 def asFea(self, indent=""): 795 res = "sub " 796 if ( 797 len(self.prefix) 798 or len(self.suffix) 799 or any([x is not None for x in self.lookups]) 800 ): 801 if len(self.prefix): 802 res += " ".join(g.asFea() for g in self.prefix) + " " 803 for i, g in enumerate(self.glyphs): 804 res += g.asFea() + "'" 805 if self.lookups[i]: 806 for lu in self.lookups[i]: 807 res += " lookup " + lu.name 808 if i < len(self.glyphs) - 1: 809 res += " " 810 if len(self.suffix): 811 res += " " + " ".join(map(asFea, self.suffix)) 812 else: 813 res += " ".join(map(asFea, self.glyph)) 814 res += ";" 815 return res 816 817 818class CursivePosStatement(Statement): 819 """A cursive positioning statement. Entry and exit anchors can either 820 be :class:`Anchor` objects or ``None``.""" 821 822 def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None): 823 Statement.__init__(self, location) 824 self.glyphclass = glyphclass 825 self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor 826 827 def build(self, builder): 828 """Calls the builder object's ``add_cursive_pos`` callback.""" 829 builder.add_cursive_pos( 830 self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor 831 ) 832 833 def asFea(self, indent=""): 834 entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>" 835 exit = self.exitAnchor.asFea() if self.exitAnchor else "<anchor NULL>" 836 return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit) 837 838 839class FeatureReferenceStatement(Statement): 840 """Example: ``feature salt;``""" 841 842 def __init__(self, featureName, location=None): 843 Statement.__init__(self, location) 844 self.location, self.featureName = (location, featureName) 845 846 def build(self, builder): 847 """Calls the builder object's ``add_feature_reference`` callback.""" 848 builder.add_feature_reference(self.location, self.featureName) 849 850 def asFea(self, indent=""): 851 return "feature {};".format(self.featureName) 852 853 854class IgnorePosStatement(Statement): 855 """An ``ignore pos`` statement, containing `one or more` contexts to ignore. 856 857 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples, 858 with each of ``prefix``, ``glyphs`` and ``suffix`` being 859 `glyph-containing objects`_ .""" 860 861 def __init__(self, chainContexts, location=None): 862 Statement.__init__(self, location) 863 self.chainContexts = chainContexts 864 865 def build(self, builder): 866 """Calls the builder object's ``add_chain_context_pos`` callback on each 867 rule context.""" 868 for prefix, glyphs, suffix in self.chainContexts: 869 prefix = [p.glyphSet() for p in prefix] 870 glyphs = [g.glyphSet() for g in glyphs] 871 suffix = [s.glyphSet() for s in suffix] 872 builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, []) 873 874 def asFea(self, indent=""): 875 contexts = [] 876 for prefix, glyphs, suffix in self.chainContexts: 877 res = "" 878 if len(prefix) or len(suffix): 879 if len(prefix): 880 res += " ".join(map(asFea, prefix)) + " " 881 res += " ".join(g.asFea() + "'" for g in glyphs) 882 if len(suffix): 883 res += " " + " ".join(map(asFea, suffix)) 884 else: 885 res += " ".join(map(asFea, glyphs)) 886 contexts.append(res) 887 return "ignore pos " + ", ".join(contexts) + ";" 888 889 890class IgnoreSubstStatement(Statement): 891 """An ``ignore sub`` statement, containing `one or more` contexts to ignore. 892 893 ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples, 894 with each of ``prefix``, ``glyphs`` and ``suffix`` being 895 `glyph-containing objects`_ .""" 896 897 def __init__(self, chainContexts, location=None): 898 Statement.__init__(self, location) 899 self.chainContexts = chainContexts 900 901 def build(self, builder): 902 """Calls the builder object's ``add_chain_context_subst`` callback on 903 each rule context.""" 904 for prefix, glyphs, suffix in self.chainContexts: 905 prefix = [p.glyphSet() for p in prefix] 906 glyphs = [g.glyphSet() for g in glyphs] 907 suffix = [s.glyphSet() for s in suffix] 908 builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, []) 909 910 def asFea(self, indent=""): 911 contexts = [] 912 for prefix, glyphs, suffix in self.chainContexts: 913 res = "" 914 if len(prefix) or len(suffix): 915 if len(prefix): 916 res += " ".join(map(asFea, prefix)) + " " 917 res += " ".join(g.asFea() + "'" for g in glyphs) 918 if len(suffix): 919 res += " " + " ".join(map(asFea, suffix)) 920 else: 921 res += " ".join(map(asFea, glyphs)) 922 contexts.append(res) 923 return "ignore sub " + ", ".join(contexts) + ";" 924 925 926class IncludeStatement(Statement): 927 """An ``include()`` statement.""" 928 929 def __init__(self, filename, location=None): 930 super(IncludeStatement, self).__init__(location) 931 self.filename = filename #: String containing name of file to include 932 933 def build(self): 934 # TODO: consider lazy-loading the including parser/lexer? 935 raise FeatureLibError( 936 "Building an include statement is not implemented yet. " 937 "Instead, use Parser(..., followIncludes=True) for building.", 938 self.location, 939 ) 940 941 def asFea(self, indent=""): 942 return indent + "include(%s);" % self.filename 943 944 945class LanguageStatement(Statement): 946 """A ``language`` statement within a feature.""" 947 948 def __init__(self, language, include_default=True, required=False, location=None): 949 Statement.__init__(self, location) 950 assert len(language) == 4 951 self.language = language #: A four-character language tag 952 self.include_default = include_default #: If false, "exclude_dflt" 953 self.required = required 954 955 def build(self, builder): 956 """Call the builder object's ``set_language`` callback.""" 957 builder.set_language( 958 location=self.location, 959 language=self.language, 960 include_default=self.include_default, 961 required=self.required, 962 ) 963 964 def asFea(self, indent=""): 965 res = "language {}".format(self.language.strip()) 966 if not self.include_default: 967 res += " exclude_dflt" 968 if self.required: 969 res += " required" 970 res += ";" 971 return res 972 973 974class LanguageSystemStatement(Statement): 975 """A top-level ``languagesystem`` statement.""" 976 977 def __init__(self, script, language, location=None): 978 Statement.__init__(self, location) 979 self.script, self.language = (script, language) 980 981 def build(self, builder): 982 """Calls the builder object's ``add_language_system`` callback.""" 983 builder.add_language_system(self.location, self.script, self.language) 984 985 def asFea(self, indent=""): 986 return "languagesystem {} {};".format(self.script, self.language.strip()) 987 988 989class FontRevisionStatement(Statement): 990 """A ``head`` table ``FontRevision`` statement. ``revision`` should be a 991 number, and will be formatted to three significant decimal places.""" 992 993 def __init__(self, revision, location=None): 994 Statement.__init__(self, location) 995 self.revision = revision 996 997 def build(self, builder): 998 builder.set_font_revision(self.location, self.revision) 999 1000 def asFea(self, indent=""): 1001 return "FontRevision {:.3f};".format(self.revision) 1002 1003 1004class LigatureCaretByIndexStatement(Statement): 1005 """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be 1006 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1007 1008 def __init__(self, glyphs, carets, location=None): 1009 Statement.__init__(self, location) 1010 self.glyphs, self.carets = (glyphs, carets) 1011 1012 def build(self, builder): 1013 """Calls the builder object's ``add_ligatureCaretByIndex_`` callback.""" 1014 glyphs = self.glyphs.glyphSet() 1015 builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets)) 1016 1017 def asFea(self, indent=""): 1018 return "LigatureCaretByIndex {} {};".format( 1019 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1020 ) 1021 1022 1023class LigatureCaretByPosStatement(Statement): 1024 """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be 1025 a `glyph-containing object`_, and ``carets`` should be a list of integers.""" 1026 1027 def __init__(self, glyphs, carets, location=None): 1028 Statement.__init__(self, location) 1029 self.glyphs, self.carets = (glyphs, carets) 1030 1031 def build(self, builder): 1032 """Calls the builder object's ``add_ligatureCaretByPos_`` callback.""" 1033 glyphs = self.glyphs.glyphSet() 1034 builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets)) 1035 1036 def asFea(self, indent=""): 1037 return "LigatureCaretByPos {} {};".format( 1038 self.glyphs.asFea(), " ".join(str(x) for x in self.carets) 1039 ) 1040 1041 1042class LigatureSubstStatement(Statement): 1043 """A chained contextual substitution statement. 1044 1045 ``prefix``, ``glyphs``, and ``suffix`` should be lists of 1046 `glyph-containing objects`_; ``replacement`` should be a single 1047 `glyph-containing object`_. 1048 1049 If ``forceChain`` is True, this is expressed as a chaining rule 1050 (e.g. ``sub f' i' by f_i``) even when no context is given.""" 1051 1052 def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None): 1053 Statement.__init__(self, location) 1054 self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix) 1055 self.replacement, self.forceChain = replacement, forceChain 1056 1057 def build(self, builder): 1058 prefix = [p.glyphSet() for p in self.prefix] 1059 glyphs = [g.glyphSet() for g in self.glyphs] 1060 suffix = [s.glyphSet() for s in self.suffix] 1061 builder.add_ligature_subst( 1062 self.location, prefix, glyphs, suffix, self.replacement, self.forceChain 1063 ) 1064 1065 def asFea(self, indent=""): 1066 res = "sub " 1067 if len(self.prefix) or len(self.suffix) or self.forceChain: 1068 if len(self.prefix): 1069 res += " ".join(g.asFea() for g in self.prefix) + " " 1070 res += " ".join(g.asFea() + "'" for g in self.glyphs) 1071 if len(self.suffix): 1072 res += " " + " ".join(g.asFea() for g in self.suffix) 1073 else: 1074 res += " ".join(g.asFea() for g in self.glyphs) 1075 res += " by " 1076 res += asFea(self.replacement) 1077 res += ";" 1078 return res 1079 1080 1081class LookupFlagStatement(Statement): 1082 """A ``lookupflag`` statement. The ``value`` should be an integer value 1083 representing the flags in use, but not including the ``markAttachment`` 1084 class and ``markFilteringSet`` values, which must be specified as 1085 glyph-containing objects.""" 1086 1087 def __init__( 1088 self, value=0, markAttachment=None, markFilteringSet=None, location=None 1089 ): 1090 Statement.__init__(self, location) 1091 self.value = value 1092 self.markAttachment = markAttachment 1093 self.markFilteringSet = markFilteringSet 1094 1095 def build(self, builder): 1096 """Calls the builder object's ``set_lookup_flag`` callback.""" 1097 markAttach = None 1098 if self.markAttachment is not None: 1099 markAttach = self.markAttachment.glyphSet() 1100 markFilter = None 1101 if self.markFilteringSet is not None: 1102 markFilter = self.markFilteringSet.glyphSet() 1103 builder.set_lookup_flag(self.location, self.value, markAttach, markFilter) 1104 1105 def asFea(self, indent=""): 1106 res = [] 1107 flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"] 1108 curr = 1 1109 for i in range(len(flags)): 1110 if self.value & curr != 0: 1111 res.append(flags[i]) 1112 curr = curr << 1 1113 if self.markAttachment is not None: 1114 res.append("MarkAttachmentType {}".format(self.markAttachment.asFea())) 1115 if self.markFilteringSet is not None: 1116 res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea())) 1117 if not res: 1118 res = ["0"] 1119 return "lookupflag {};".format(" ".join(res)) 1120 1121 1122class LookupReferenceStatement(Statement): 1123 """Represents a ``lookup ...;`` statement to include a lookup in a feature. 1124 1125 The ``lookup`` should be a :class:`LookupBlock` object.""" 1126 1127 def __init__(self, lookup, location=None): 1128 Statement.__init__(self, location) 1129 self.location, self.lookup = (location, lookup) 1130 1131 def build(self, builder): 1132 """Calls the builder object's ``add_lookup_call`` callback.""" 1133 builder.add_lookup_call(self.lookup.name) 1134 1135 def asFea(self, indent=""): 1136 return "lookup {};".format(self.lookup.name) 1137 1138 1139class MarkBasePosStatement(Statement): 1140 """A mark-to-base positioning rule. The ``base`` should be a 1141 `glyph-containing object`_. The ``marks`` should be a list of 1142 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1143 1144 def __init__(self, base, marks, location=None): 1145 Statement.__init__(self, location) 1146 self.base, self.marks = base, marks 1147 1148 def build(self, builder): 1149 """Calls the builder object's ``add_mark_base_pos`` callback.""" 1150 builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks) 1151 1152 def asFea(self, indent=""): 1153 res = "pos base {}".format(self.base.asFea()) 1154 for a, m in self.marks: 1155 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1156 res += ";" 1157 return res 1158 1159 1160class MarkLigPosStatement(Statement): 1161 """A mark-to-ligature positioning rule. The ``ligatures`` must be a 1162 `glyph-containing object`_. The ``marks`` should be a list of lists: each 1163 element in the top-level list represents a component glyph, and is made 1164 up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing 1165 mark attachment points for that position. 1166 1167 Example:: 1168 1169 m1 = MarkClass("TOP_MARKS") 1170 m2 = MarkClass("BOTTOM_MARKS") 1171 # ... add definitions to mark classes... 1172 1173 glyph = GlyphName("lam_meem_jeem") 1174 marks = [ 1175 [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam) 1176 [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem) 1177 [ ] # No attachments on the jeem 1178 ] 1179 mlp = MarkLigPosStatement(glyph, marks) 1180 1181 mlp.asFea() 1182 # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS 1183 # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS; 1184 1185 """ 1186 1187 def __init__(self, ligatures, marks, location=None): 1188 Statement.__init__(self, location) 1189 self.ligatures, self.marks = ligatures, marks 1190 1191 def build(self, builder): 1192 """Calls the builder object's ``add_mark_lig_pos`` callback.""" 1193 builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks) 1194 1195 def asFea(self, indent=""): 1196 res = "pos ligature {}".format(self.ligatures.asFea()) 1197 ligs = [] 1198 for l in self.marks: 1199 temp = "" 1200 if l is None or not len(l): 1201 temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>" 1202 else: 1203 for a, m in l: 1204 temp += ( 1205 "\n" 1206 + indent 1207 + SHIFT * 2 1208 + "{} mark @{}".format(a.asFea(), m.name) 1209 ) 1210 ligs.append(temp) 1211 res += ("\n" + indent + SHIFT + "ligComponent").join(ligs) 1212 res += ";" 1213 return res 1214 1215 1216class MarkMarkPosStatement(Statement): 1217 """A mark-to-mark positioning rule. The ``baseMarks`` must be a 1218 `glyph-containing object`_. The ``marks`` should be a list of 1219 (:class:`Anchor`, :class:`MarkClass`) tuples.""" 1220 1221 def __init__(self, baseMarks, marks, location=None): 1222 Statement.__init__(self, location) 1223 self.baseMarks, self.marks = baseMarks, marks 1224 1225 def build(self, builder): 1226 """Calls the builder object's ``add_mark_mark_pos`` callback.""" 1227 builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks) 1228 1229 def asFea(self, indent=""): 1230 res = "pos mark {}".format(self.baseMarks.asFea()) 1231 for a, m in self.marks: 1232 res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name) 1233 res += ";" 1234 return res 1235 1236 1237class MultipleSubstStatement(Statement): 1238 """A multiple substitution statement. 1239 1240 Args: 1241 prefix: a list of `glyph-containing objects`_. 1242 glyph: a single glyph-containing object. 1243 suffix: a list of glyph-containing objects. 1244 replacement: a list of glyph-containing objects. 1245 forceChain: If true, the statement is expressed as a chaining rule 1246 (e.g. ``sub f' i' by f_i``) even when no context is given. 1247 """ 1248 1249 def __init__( 1250 self, prefix, glyph, suffix, replacement, forceChain=False, location=None 1251 ): 1252 Statement.__init__(self, location) 1253 self.prefix, self.glyph, self.suffix = prefix, glyph, suffix 1254 self.replacement = replacement 1255 self.forceChain = forceChain 1256 1257 def build(self, builder): 1258 """Calls the builder object's ``add_multiple_subst`` callback.""" 1259 prefix = [p.glyphSet() for p in self.prefix] 1260 suffix = [s.glyphSet() for s in self.suffix] 1261 builder.add_multiple_subst( 1262 self.location, prefix, self.glyph, suffix, self.replacement, self.forceChain 1263 ) 1264 1265 def asFea(self, indent=""): 1266 res = "sub " 1267 if len(self.prefix) or len(self.suffix) or self.forceChain: 1268 if len(self.prefix): 1269 res += " ".join(map(asFea, self.prefix)) + " " 1270 res += asFea(self.glyph) + "'" 1271 if len(self.suffix): 1272 res += " " + " ".join(map(asFea, self.suffix)) 1273 else: 1274 res += asFea(self.glyph) 1275 replacement = self.replacement or [NullGlyph()] 1276 res += " by " 1277 res += " ".join(map(asFea, replacement)) 1278 res += ";" 1279 return res 1280 1281 1282class PairPosStatement(Statement): 1283 """A pair positioning statement. 1284 1285 ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_. 1286 ``valuerecord1`` should be a :class:`ValueRecord` object; 1287 ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``. 1288 If ``enumerated`` is true, then this is expressed as an 1289 `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_. 1290 """ 1291 1292 def __init__( 1293 self, 1294 glyphs1, 1295 valuerecord1, 1296 glyphs2, 1297 valuerecord2, 1298 enumerated=False, 1299 location=None, 1300 ): 1301 Statement.__init__(self, location) 1302 self.enumerated = enumerated 1303 self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1 1304 self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2 1305 1306 def build(self, builder): 1307 """Calls a callback on the builder object: 1308 1309 * If the rule is enumerated, calls ``add_specific_pair_pos`` on each 1310 combination of first and second glyphs. 1311 * If the glyphs are both single :class:`GlyphName` objects, calls 1312 ``add_specific_pair_pos``. 1313 * Else, calls ``add_class_pair_pos``. 1314 """ 1315 if self.enumerated: 1316 g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()] 1317 for glyph1, glyph2 in itertools.product(*g): 1318 builder.add_specific_pair_pos( 1319 self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2 1320 ) 1321 return 1322 1323 is_specific = isinstance(self.glyphs1, GlyphName) and isinstance( 1324 self.glyphs2, GlyphName 1325 ) 1326 if is_specific: 1327 builder.add_specific_pair_pos( 1328 self.location, 1329 self.glyphs1.glyph, 1330 self.valuerecord1, 1331 self.glyphs2.glyph, 1332 self.valuerecord2, 1333 ) 1334 else: 1335 builder.add_class_pair_pos( 1336 self.location, 1337 self.glyphs1.glyphSet(), 1338 self.valuerecord1, 1339 self.glyphs2.glyphSet(), 1340 self.valuerecord2, 1341 ) 1342 1343 def asFea(self, indent=""): 1344 res = "enum " if self.enumerated else "" 1345 if self.valuerecord2: 1346 res += "pos {} {} {} {};".format( 1347 self.glyphs1.asFea(), 1348 self.valuerecord1.asFea(), 1349 self.glyphs2.asFea(), 1350 self.valuerecord2.asFea(), 1351 ) 1352 else: 1353 res += "pos {} {} {};".format( 1354 self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea() 1355 ) 1356 return res 1357 1358 1359class ReverseChainSingleSubstStatement(Statement): 1360 """A reverse chaining substitution statement. You don't see those every day. 1361 1362 Note the unusual argument order: ``suffix`` comes `before` ``glyphs``. 1363 ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be 1364 lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should 1365 be one-item lists. 1366 """ 1367 1368 def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None): 1369 Statement.__init__(self, location) 1370 self.old_prefix, self.old_suffix = old_prefix, old_suffix 1371 self.glyphs = glyphs 1372 self.replacements = replacements 1373 1374 def build(self, builder): 1375 prefix = [p.glyphSet() for p in self.old_prefix] 1376 suffix = [s.glyphSet() for s in self.old_suffix] 1377 originals = self.glyphs[0].glyphSet() 1378 replaces = self.replacements[0].glyphSet() 1379 if len(replaces) == 1: 1380 replaces = replaces * len(originals) 1381 builder.add_reverse_chain_single_subst( 1382 self.location, prefix, suffix, dict(zip(originals, replaces)) 1383 ) 1384 1385 def asFea(self, indent=""): 1386 res = "rsub " 1387 if len(self.old_prefix) or len(self.old_suffix): 1388 if len(self.old_prefix): 1389 res += " ".join(asFea(g) for g in self.old_prefix) + " " 1390 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1391 if len(self.old_suffix): 1392 res += " " + " ".join(asFea(g) for g in self.old_suffix) 1393 else: 1394 res += " ".join(map(asFea, self.glyphs)) 1395 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1396 return res 1397 1398 1399class SingleSubstStatement(Statement): 1400 """A single substitution statement. 1401 1402 Note the unusual argument order: ``prefix`` and suffix come `after` 1403 the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and 1404 ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and 1405 ``replace`` should be one-item lists. 1406 """ 1407 1408 def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None): 1409 Statement.__init__(self, location) 1410 self.prefix, self.suffix = prefix, suffix 1411 self.forceChain = forceChain 1412 self.glyphs = glyphs 1413 self.replacements = replace 1414 1415 def build(self, builder): 1416 """Calls the builder object's ``add_single_subst`` callback.""" 1417 prefix = [p.glyphSet() for p in self.prefix] 1418 suffix = [s.glyphSet() for s in self.suffix] 1419 originals = self.glyphs[0].glyphSet() 1420 replaces = self.replacements[0].glyphSet() 1421 if len(replaces) == 1: 1422 replaces = replaces * len(originals) 1423 builder.add_single_subst( 1424 self.location, 1425 prefix, 1426 suffix, 1427 OrderedDict(zip(originals, replaces)), 1428 self.forceChain, 1429 ) 1430 1431 def asFea(self, indent=""): 1432 res = "sub " 1433 if len(self.prefix) or len(self.suffix) or self.forceChain: 1434 if len(self.prefix): 1435 res += " ".join(asFea(g) for g in self.prefix) + " " 1436 res += " ".join(asFea(g) + "'" for g in self.glyphs) 1437 if len(self.suffix): 1438 res += " " + " ".join(asFea(g) for g in self.suffix) 1439 else: 1440 res += " ".join(asFea(g) for g in self.glyphs) 1441 res += " by {};".format(" ".join(asFea(g) for g in self.replacements)) 1442 return res 1443 1444 1445class ScriptStatement(Statement): 1446 """A ``script`` statement.""" 1447 1448 def __init__(self, script, location=None): 1449 Statement.__init__(self, location) 1450 self.script = script #: the script code 1451 1452 def build(self, builder): 1453 """Calls the builder's ``set_script`` callback.""" 1454 builder.set_script(self.location, self.script) 1455 1456 def asFea(self, indent=""): 1457 return "script {};".format(self.script.strip()) 1458 1459 1460class SinglePosStatement(Statement): 1461 """A single position statement. ``prefix`` and ``suffix`` should be 1462 lists of `glyph-containing objects`_. 1463 1464 ``pos`` should be a one-element list containing a (`glyph-containing object`_, 1465 :class:`ValueRecord`) tuple.""" 1466 1467 def __init__(self, pos, prefix, suffix, forceChain, location=None): 1468 Statement.__init__(self, location) 1469 self.pos, self.prefix, self.suffix = pos, prefix, suffix 1470 self.forceChain = forceChain 1471 1472 def build(self, builder): 1473 """Calls the builder object's ``add_single_pos`` callback.""" 1474 prefix = [p.glyphSet() for p in self.prefix] 1475 suffix = [s.glyphSet() for s in self.suffix] 1476 pos = [(g.glyphSet(), value) for g, value in self.pos] 1477 builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain) 1478 1479 def asFea(self, indent=""): 1480 res = "pos " 1481 if len(self.prefix) or len(self.suffix) or self.forceChain: 1482 if len(self.prefix): 1483 res += " ".join(map(asFea, self.prefix)) + " " 1484 res += " ".join( 1485 [ 1486 asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "") 1487 for x in self.pos 1488 ] 1489 ) 1490 if len(self.suffix): 1491 res += " " + " ".join(map(asFea, self.suffix)) 1492 else: 1493 res += " ".join( 1494 [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos] 1495 ) 1496 res += ";" 1497 return res 1498 1499 1500class SubtableStatement(Statement): 1501 """Represents a subtable break.""" 1502 1503 def __init__(self, location=None): 1504 Statement.__init__(self, location) 1505 1506 def build(self, builder): 1507 """Calls the builder objects's ``add_subtable_break`` callback.""" 1508 builder.add_subtable_break(self.location) 1509 1510 def asFea(self, indent=""): 1511 return "subtable;" 1512 1513 1514class ValueRecord(Expression): 1515 """Represents a value record.""" 1516 1517 def __init__( 1518 self, 1519 xPlacement=None, 1520 yPlacement=None, 1521 xAdvance=None, 1522 yAdvance=None, 1523 xPlaDevice=None, 1524 yPlaDevice=None, 1525 xAdvDevice=None, 1526 yAdvDevice=None, 1527 vertical=False, 1528 location=None, 1529 ): 1530 Expression.__init__(self, location) 1531 self.xPlacement, self.yPlacement = (xPlacement, yPlacement) 1532 self.xAdvance, self.yAdvance = (xAdvance, yAdvance) 1533 self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice) 1534 self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice) 1535 self.vertical = vertical 1536 1537 def __eq__(self, other): 1538 return ( 1539 self.xPlacement == other.xPlacement 1540 and self.yPlacement == other.yPlacement 1541 and self.xAdvance == other.xAdvance 1542 and self.yAdvance == other.yAdvance 1543 and self.xPlaDevice == other.xPlaDevice 1544 and self.xAdvDevice == other.xAdvDevice 1545 ) 1546 1547 def __ne__(self, other): 1548 return not self.__eq__(other) 1549 1550 def __hash__(self): 1551 return ( 1552 hash(self.xPlacement) 1553 ^ hash(self.yPlacement) 1554 ^ hash(self.xAdvance) 1555 ^ hash(self.yAdvance) 1556 ^ hash(self.xPlaDevice) 1557 ^ hash(self.yPlaDevice) 1558 ^ hash(self.xAdvDevice) 1559 ^ hash(self.yAdvDevice) 1560 ) 1561 1562 def asFea(self, indent=""): 1563 if not self: 1564 return "<NULL>" 1565 1566 x, y = self.xPlacement, self.yPlacement 1567 xAdvance, yAdvance = self.xAdvance, self.yAdvance 1568 xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice 1569 xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice 1570 vertical = self.vertical 1571 1572 # Try format A, if possible. 1573 if x is None and y is None: 1574 if xAdvance is None and vertical: 1575 return str(yAdvance) 1576 elif yAdvance is None and not vertical: 1577 return str(xAdvance) 1578 1579 # Make any remaining None value 0 to avoid generating invalid records. 1580 x = x or 0 1581 y = y or 0 1582 xAdvance = xAdvance or 0 1583 yAdvance = yAdvance or 0 1584 1585 # Try format B, if possible. 1586 if ( 1587 xPlaDevice is None 1588 and yPlaDevice is None 1589 and xAdvDevice is None 1590 and yAdvDevice is None 1591 ): 1592 return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance) 1593 1594 # Last resort is format C. 1595 return "<%s %s %s %s %s %s %s %s>" % ( 1596 x, 1597 y, 1598 xAdvance, 1599 yAdvance, 1600 deviceToString(xPlaDevice), 1601 deviceToString(yPlaDevice), 1602 deviceToString(xAdvDevice), 1603 deviceToString(yAdvDevice), 1604 ) 1605 1606 def __bool__(self): 1607 return any( 1608 getattr(self, v) is not None 1609 for v in [ 1610 "xPlacement", 1611 "yPlacement", 1612 "xAdvance", 1613 "yAdvance", 1614 "xPlaDevice", 1615 "yPlaDevice", 1616 "xAdvDevice", 1617 "yAdvDevice", 1618 ] 1619 ) 1620 1621 __nonzero__ = __bool__ 1622 1623 1624class ValueRecordDefinition(Statement): 1625 """Represents a named value record definition.""" 1626 1627 def __init__(self, name, value, location=None): 1628 Statement.__init__(self, location) 1629 self.name = name #: Value record name as string 1630 self.value = value #: :class:`ValueRecord` object 1631 1632 def asFea(self, indent=""): 1633 return "valueRecordDef {} {};".format(self.value.asFea(), self.name) 1634 1635 1636def simplify_name_attributes(pid, eid, lid): 1637 if pid == 3 and eid == 1 and lid == 1033: 1638 return "" 1639 elif pid == 1 and eid == 0 and lid == 0: 1640 return "1" 1641 else: 1642 return "{} {} {}".format(pid, eid, lid) 1643 1644 1645class NameRecord(Statement): 1646 """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)""" 1647 1648 def __init__(self, nameID, platformID, platEncID, langID, string, location=None): 1649 Statement.__init__(self, location) 1650 self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name) 1651 self.platformID = platformID #: Platform ID as integer 1652 self.platEncID = platEncID #: Platform encoding ID as integer 1653 self.langID = langID #: Language ID as integer 1654 self.string = string #: Name record value 1655 1656 def build(self, builder): 1657 """Calls the builder object's ``add_name_record`` callback.""" 1658 builder.add_name_record( 1659 self.location, 1660 self.nameID, 1661 self.platformID, 1662 self.platEncID, 1663 self.langID, 1664 self.string, 1665 ) 1666 1667 def asFea(self, indent=""): 1668 def escape(c, escape_pattern): 1669 # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS 1670 if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C): 1671 return chr(c) 1672 else: 1673 return escape_pattern % c 1674 1675 encoding = getEncoding(self.platformID, self.platEncID, self.langID) 1676 if encoding is None: 1677 raise FeatureLibError("Unsupported encoding", self.location) 1678 s = tobytes(self.string, encoding=encoding) 1679 if encoding == "utf_16_be": 1680 escaped_string = "".join( 1681 [ 1682 escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x") 1683 for i in range(0, len(s), 2) 1684 ] 1685 ) 1686 else: 1687 escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s]) 1688 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1689 if plat != "": 1690 plat += " " 1691 return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string) 1692 1693 1694class FeatureNameStatement(NameRecord): 1695 """Represents a ``sizemenuname`` or ``name`` statement.""" 1696 1697 def build(self, builder): 1698 """Calls the builder object's ``add_featureName`` callback.""" 1699 NameRecord.build(self, builder) 1700 builder.add_featureName(self.nameID) 1701 1702 def asFea(self, indent=""): 1703 if self.nameID == "size": 1704 tag = "sizemenuname" 1705 else: 1706 tag = "name" 1707 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1708 if plat != "": 1709 plat += " " 1710 return '{} {}"{}";'.format(tag, plat, self.string) 1711 1712 1713class STATNameStatement(NameRecord): 1714 """Represents a STAT table ``name`` statement.""" 1715 1716 def asFea(self, indent=""): 1717 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1718 if plat != "": 1719 plat += " " 1720 return 'name {}"{}";'.format(plat, self.string) 1721 1722 1723class SizeParameters(Statement): 1724 """A ``parameters`` statement.""" 1725 1726 def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None): 1727 Statement.__init__(self, location) 1728 self.DesignSize = DesignSize 1729 self.SubfamilyID = SubfamilyID 1730 self.RangeStart = RangeStart 1731 self.RangeEnd = RangeEnd 1732 1733 def build(self, builder): 1734 """Calls the builder object's ``set_size_parameters`` callback.""" 1735 builder.set_size_parameters( 1736 self.location, 1737 self.DesignSize, 1738 self.SubfamilyID, 1739 self.RangeStart, 1740 self.RangeEnd, 1741 ) 1742 1743 def asFea(self, indent=""): 1744 res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID) 1745 if self.RangeStart != 0 or self.RangeEnd != 0: 1746 res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10)) 1747 return res + ";" 1748 1749 1750class CVParametersNameStatement(NameRecord): 1751 """Represent a name statement inside a ``cvParameters`` block.""" 1752 1753 def __init__( 1754 self, nameID, platformID, platEncID, langID, string, block_name, location=None 1755 ): 1756 NameRecord.__init__( 1757 self, nameID, platformID, platEncID, langID, string, location=location 1758 ) 1759 self.block_name = block_name 1760 1761 def build(self, builder): 1762 """Calls the builder object's ``add_cv_parameter`` callback.""" 1763 item = "" 1764 if self.block_name == "ParamUILabelNameID": 1765 item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0)) 1766 builder.add_cv_parameter(self.nameID) 1767 self.nameID = (self.nameID, self.block_name + item) 1768 NameRecord.build(self, builder) 1769 1770 def asFea(self, indent=""): 1771 plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID) 1772 if plat != "": 1773 plat += " " 1774 return 'name {}"{}";'.format(plat, self.string) 1775 1776 1777class CharacterStatement(Statement): 1778 """ 1779 Statement used in cvParameters blocks of Character Variant features (cvXX). 1780 The Unicode value may be written with either decimal or hexadecimal 1781 notation. The value must be preceded by '0x' if it is a hexadecimal value. 1782 The largest Unicode value allowed is 0xFFFFFF. 1783 """ 1784 1785 def __init__(self, character, tag, location=None): 1786 Statement.__init__(self, location) 1787 self.character = character 1788 self.tag = tag 1789 1790 def build(self, builder): 1791 """Calls the builder object's ``add_cv_character`` callback.""" 1792 builder.add_cv_character(self.character, self.tag) 1793 1794 def asFea(self, indent=""): 1795 return "Character {:#x};".format(self.character) 1796 1797 1798class BaseAxis(Statement): 1799 """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList`` 1800 pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair.""" 1801 1802 def __init__(self, bases, scripts, vertical, location=None): 1803 Statement.__init__(self, location) 1804 self.bases = bases #: A list of baseline tag names as strings 1805 self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate) 1806 self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False 1807 1808 def build(self, builder): 1809 """Calls the builder object's ``set_base_axis`` callback.""" 1810 builder.set_base_axis(self.bases, self.scripts, self.vertical) 1811 1812 def asFea(self, indent=""): 1813 direction = "Vert" if self.vertical else "Horiz" 1814 scripts = [ 1815 "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) 1816 for a in self.scripts 1817 ] 1818 return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format( 1819 direction, " ".join(self.bases), indent, direction, ", ".join(scripts) 1820 ) 1821 1822 1823class OS2Field(Statement): 1824 """An entry in the ``OS/2`` table. Most ``values`` should be numbers or 1825 strings, apart from when the key is ``UnicodeRange``, ``CodePageRange`` 1826 or ``Panose``, in which case it should be an array of integers.""" 1827 1828 def __init__(self, key, value, location=None): 1829 Statement.__init__(self, location) 1830 self.key = key 1831 self.value = value 1832 1833 def build(self, builder): 1834 """Calls the builder object's ``add_os2_field`` callback.""" 1835 builder.add_os2_field(self.key, self.value) 1836 1837 def asFea(self, indent=""): 1838 def intarr2str(x): 1839 return " ".join(map(str, x)) 1840 1841 numbers = ( 1842 "FSType", 1843 "TypoAscender", 1844 "TypoDescender", 1845 "TypoLineGap", 1846 "winAscent", 1847 "winDescent", 1848 "XHeight", 1849 "CapHeight", 1850 "WeightClass", 1851 "WidthClass", 1852 "LowerOpSize", 1853 "UpperOpSize", 1854 ) 1855 ranges = ("UnicodeRange", "CodePageRange") 1856 keywords = dict([(x.lower(), [x, str]) for x in numbers]) 1857 keywords.update([(x.lower(), [x, intarr2str]) for x in ranges]) 1858 keywords["panose"] = ["Panose", intarr2str] 1859 keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)] 1860 if self.key in keywords: 1861 return "{} {};".format( 1862 keywords[self.key][0], keywords[self.key][1](self.value) 1863 ) 1864 return "" # should raise exception 1865 1866 1867class HheaField(Statement): 1868 """An entry in the ``hhea`` table.""" 1869 1870 def __init__(self, key, value, location=None): 1871 Statement.__init__(self, location) 1872 self.key = key 1873 self.value = value 1874 1875 def build(self, builder): 1876 """Calls the builder object's ``add_hhea_field`` callback.""" 1877 builder.add_hhea_field(self.key, self.value) 1878 1879 def asFea(self, indent=""): 1880 fields = ("CaretOffset", "Ascender", "Descender", "LineGap") 1881 keywords = dict([(x.lower(), x) for x in fields]) 1882 return "{} {};".format(keywords[self.key], self.value) 1883 1884 1885class VheaField(Statement): 1886 """An entry in the ``vhea`` table.""" 1887 1888 def __init__(self, key, value, location=None): 1889 Statement.__init__(self, location) 1890 self.key = key 1891 self.value = value 1892 1893 def build(self, builder): 1894 """Calls the builder object's ``add_vhea_field`` callback.""" 1895 builder.add_vhea_field(self.key, self.value) 1896 1897 def asFea(self, indent=""): 1898 fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap") 1899 keywords = dict([(x.lower(), x) for x in fields]) 1900 return "{} {};".format(keywords[self.key], self.value) 1901 1902 1903class STATDesignAxisStatement(Statement): 1904 """A STAT table Design Axis 1905 1906 Args: 1907 tag (str): a 4 letter axis tag 1908 axisOrder (int): an int 1909 names (list): a list of :class:`STATNameStatement` objects 1910 """ 1911 1912 def __init__(self, tag, axisOrder, names, location=None): 1913 Statement.__init__(self, location) 1914 self.tag = tag 1915 self.axisOrder = axisOrder 1916 self.names = names 1917 self.location = location 1918 1919 def build(self, builder): 1920 builder.addDesignAxis(self, self.location) 1921 1922 def asFea(self, indent=""): 1923 indent += SHIFT 1924 res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n" 1925 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1926 res += "};" 1927 return res 1928 1929 1930class ElidedFallbackName(Statement): 1931 """STAT table ElidedFallbackName 1932 1933 Args: 1934 names: a list of :class:`STATNameStatement` objects 1935 """ 1936 1937 def __init__(self, names, location=None): 1938 Statement.__init__(self, location) 1939 self.names = names 1940 self.location = location 1941 1942 def build(self, builder): 1943 builder.setElidedFallbackName(self.names, self.location) 1944 1945 def asFea(self, indent=""): 1946 indent += SHIFT 1947 res = "ElidedFallbackName { \n" 1948 res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n" 1949 res += "};" 1950 return res 1951 1952 1953class ElidedFallbackNameID(Statement): 1954 """STAT table ElidedFallbackNameID 1955 1956 Args: 1957 value: an int pointing to an existing name table name ID 1958 """ 1959 1960 def __init__(self, value, location=None): 1961 Statement.__init__(self, location) 1962 self.value = value 1963 self.location = location 1964 1965 def build(self, builder): 1966 builder.setElidedFallbackName(self.value, self.location) 1967 1968 def asFea(self, indent=""): 1969 return f"ElidedFallbackNameID {self.value};" 1970 1971 1972class STATAxisValueStatement(Statement): 1973 """A STAT table Axis Value Record 1974 1975 Args: 1976 names (list): a list of :class:`STATNameStatement` objects 1977 locations (list): a list of :class:`AxisValueLocationStatement` objects 1978 flags (int): an int 1979 """ 1980 1981 def __init__(self, names, locations, flags, location=None): 1982 Statement.__init__(self, location) 1983 self.names = names 1984 self.locations = locations 1985 self.flags = flags 1986 1987 def build(self, builder): 1988 builder.addAxisValueRecord(self, self.location) 1989 1990 def asFea(self, indent=""): 1991 res = "AxisValue {\n" 1992 for location in self.locations: 1993 res += location.asFea() 1994 1995 for nameRecord in self.names: 1996 res += nameRecord.asFea() 1997 res += "\n" 1998 1999 if self.flags: 2000 flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"] 2001 flagStrings = [] 2002 curr = 1 2003 for i in range(len(flags)): 2004 if self.flags & curr != 0: 2005 flagStrings.append(flags[i]) 2006 curr = curr << 1 2007 res += f"flag {' '.join(flagStrings)};\n" 2008 res += "};" 2009 return res 2010 2011 2012class AxisValueLocationStatement(Statement): 2013 """ 2014 A STAT table Axis Value Location 2015 2016 Args: 2017 tag (str): a 4 letter axis tag 2018 values (list): a list of ints and/or floats 2019 """ 2020 2021 def __init__(self, tag, values, location=None): 2022 Statement.__init__(self, location) 2023 self.tag = tag 2024 self.values = values 2025 2026 def asFea(self, res=""): 2027 res += f"location {self.tag} " 2028 res += f"{' '.join(str(i) for i in self.values)};\n" 2029 return res 2030