1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134 |
- from fontTools.feaLib.error import FeatureLibError
- from fontTools.feaLib.location import FeatureLibLocation
- from fontTools.misc.encodingTools import getEncoding
- from fontTools.misc.textTools import byteord, tobytes
- from collections import OrderedDict
- import itertools
- SHIFT = " " * 4
- __all__ = [
- "Element",
- "FeatureFile",
- "Comment",
- "GlyphName",
- "GlyphClass",
- "GlyphClassName",
- "MarkClassName",
- "AnonymousBlock",
- "Block",
- "FeatureBlock",
- "NestedBlock",
- "LookupBlock",
- "GlyphClassDefinition",
- "GlyphClassDefStatement",
- "MarkClass",
- "MarkClassDefinition",
- "AlternateSubstStatement",
- "Anchor",
- "AnchorDefinition",
- "AttachStatement",
- "AxisValueLocationStatement",
- "BaseAxis",
- "CVParametersNameStatement",
- "ChainContextPosStatement",
- "ChainContextSubstStatement",
- "CharacterStatement",
- "ConditionsetStatement",
- "CursivePosStatement",
- "ElidedFallbackName",
- "ElidedFallbackNameID",
- "Expression",
- "FeatureNameStatement",
- "FeatureReferenceStatement",
- "FontRevisionStatement",
- "HheaField",
- "IgnorePosStatement",
- "IgnoreSubstStatement",
- "IncludeStatement",
- "LanguageStatement",
- "LanguageSystemStatement",
- "LigatureCaretByIndexStatement",
- "LigatureCaretByPosStatement",
- "LigatureSubstStatement",
- "LookupFlagStatement",
- "LookupReferenceStatement",
- "MarkBasePosStatement",
- "MarkLigPosStatement",
- "MarkMarkPosStatement",
- "MultipleSubstStatement",
- "NameRecord",
- "OS2Field",
- "PairPosStatement",
- "ReverseChainSingleSubstStatement",
- "ScriptStatement",
- "SinglePosStatement",
- "SingleSubstStatement",
- "SizeParameters",
- "Statement",
- "STATAxisValueStatement",
- "STATDesignAxisStatement",
- "STATNameStatement",
- "SubtableStatement",
- "TableBlock",
- "ValueRecord",
- "ValueRecordDefinition",
- "VheaField",
- ]
- def deviceToString(device):
- if device is None:
- return "<device NULL>"
- else:
- return "<device %s>" % ", ".join("%d %d" % t for t in device)
- fea_keywords = set(
- [
- "anchor",
- "anchordef",
- "anon",
- "anonymous",
- "by",
- "contour",
- "cursive",
- "device",
- "enum",
- "enumerate",
- "excludedflt",
- "exclude_dflt",
- "feature",
- "from",
- "ignore",
- "ignorebaseglyphs",
- "ignoreligatures",
- "ignoremarks",
- "include",
- "includedflt",
- "include_dflt",
- "language",
- "languagesystem",
- "lookup",
- "lookupflag",
- "mark",
- "markattachmenttype",
- "markclass",
- "nameid",
- "null",
- "parameters",
- "pos",
- "position",
- "required",
- "righttoleft",
- "reversesub",
- "rsub",
- "script",
- "sub",
- "substitute",
- "subtable",
- "table",
- "usemarkfilteringset",
- "useextension",
- "valuerecorddef",
- "base",
- "gdef",
- "head",
- "hhea",
- "name",
- "vhea",
- "vmtx",
- ]
- )
- def asFea(g):
- if hasattr(g, "asFea"):
- return g.asFea()
- elif isinstance(g, tuple) and len(g) == 2:
- return asFea(g[0]) + " - " + asFea(g[1]) # a range
- elif g.lower() in fea_keywords:
- return "\\" + g
- else:
- return g
- class Element(object):
- """A base class representing "something" in a feature file."""
- def __init__(self, location=None):
- #: location of this element as a `FeatureLibLocation` object.
- if location and not isinstance(location, FeatureLibLocation):
- location = FeatureLibLocation(*location)
- self.location = location
- def build(self, builder):
- pass
- def asFea(self, indent=""):
- """Returns this element as a string of feature code. For block-type
- elements (such as :class:`FeatureBlock`), the `indent` string is
- added to the start of each line in the output."""
- raise NotImplementedError
- def __str__(self):
- return self.asFea()
- class Statement(Element):
- pass
- class Expression(Element):
- pass
- class Comment(Element):
- """A comment in a feature file."""
- def __init__(self, text, location=None):
- super(Comment, self).__init__(location)
- #: Text of the comment
- self.text = text
- def asFea(self, indent=""):
- return self.text
- class NullGlyph(Expression):
- """The NULL glyph, used in glyph deletion substitutions."""
- def __init__(self, location=None):
- Expression.__init__(self, location)
- #: The name itself as a string
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return ()
- def asFea(self, indent=""):
- return "NULL"
- class GlyphName(Expression):
- """A single glyph name, such as ``cedilla``."""
- def __init__(self, glyph, location=None):
- Expression.__init__(self, location)
- #: The name itself as a string
- self.glyph = glyph
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return (self.glyph,)
- def asFea(self, indent=""):
- return asFea(self.glyph)
- class GlyphClass(Expression):
- """A glyph class, such as ``[acute cedilla grave]``."""
- def __init__(self, glyphs=None, location=None):
- Expression.__init__(self, location)
- #: The list of glyphs in this class, as :class:`GlyphName` objects.
- self.glyphs = glyphs if glyphs is not None else []
- self.original = []
- self.curr = 0
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return tuple(self.glyphs)
- def asFea(self, indent=""):
- if len(self.original):
- if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr :])
- self.curr = len(self.glyphs)
- return "[" + " ".join(map(asFea, self.original)) + "]"
- else:
- return "[" + " ".join(map(asFea, self.glyphs)) + "]"
- def extend(self, glyphs):
- """Add a list of :class:`GlyphName` objects to the class."""
- self.glyphs.extend(glyphs)
- def append(self, glyph):
- """Add a single :class:`GlyphName` object to the class."""
- self.glyphs.append(glyph)
- def add_range(self, start, end, glyphs):
- """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
- are either :class:`GlyphName` objects or strings representing the
- start and end glyphs in the class, and ``glyphs`` is the full list of
- :class:`GlyphName` objects in the range."""
- if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr :])
- self.original.append((start, end))
- self.glyphs.extend(glyphs)
- self.curr = len(self.glyphs)
- def add_cid_range(self, start, end, glyphs):
- """Add a range to the class by glyph ID. ``start`` and ``end`` are the
- initial and final IDs, and ``glyphs`` is the full list of
- :class:`GlyphName` objects in the range."""
- if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr :])
- self.original.append(("\\{}".format(start), "\\{}".format(end)))
- self.glyphs.extend(glyphs)
- self.curr = len(self.glyphs)
- def add_class(self, gc):
- """Add glyphs from the given :class:`GlyphClassName` object to the
- class."""
- if self.curr < len(self.glyphs):
- self.original.extend(self.glyphs[self.curr :])
- self.original.append(gc)
- self.glyphs.extend(gc.glyphSet())
- self.curr = len(self.glyphs)
- class GlyphClassName(Expression):
- """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
- with a :class:`GlyphClassDefinition` object."""
- def __init__(self, glyphclass, location=None):
- Expression.__init__(self, location)
- assert isinstance(glyphclass, GlyphClassDefinition)
- self.glyphclass = glyphclass
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return tuple(self.glyphclass.glyphSet())
- def asFea(self, indent=""):
- return "@" + self.glyphclass.name
- class MarkClassName(Expression):
- """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``.
- This must be instantiated with a :class:`MarkClass` object."""
- def __init__(self, markClass, location=None):
- Expression.__init__(self, location)
- assert isinstance(markClass, MarkClass)
- self.markClass = markClass
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return self.markClass.glyphSet()
- def asFea(self, indent=""):
- return "@" + self.markClass.name
- class AnonymousBlock(Statement):
- """An anonymous data block."""
- def __init__(self, tag, content, location=None):
- Statement.__init__(self, location)
- self.tag = tag #: string containing the block's "tag"
- self.content = content #: block data as string
- def asFea(self, indent=""):
- res = "anon {} {{\n".format(self.tag)
- res += self.content
- res += "}} {};\n\n".format(self.tag)
- return res
- class Block(Statement):
- """A block of statements: feature, lookup, etc."""
- def __init__(self, location=None):
- Statement.__init__(self, location)
- self.statements = [] #: Statements contained in the block
- def build(self, builder):
- """When handed a 'builder' object of comparable interface to
- :class:`fontTools.feaLib.builder`, walks the statements in this
- block, calling the builder callbacks."""
- for s in self.statements:
- s.build(builder)
- def asFea(self, indent=""):
- indent += SHIFT
- return (
- indent
- + ("\n" + indent).join([s.asFea(indent=indent) for s in self.statements])
- + "\n"
- )
- class FeatureFile(Block):
- """The top-level element of the syntax tree, containing the whole feature
- file in its ``statements`` attribute."""
- def __init__(self):
- Block.__init__(self, location=None)
- self.markClasses = {} # name --> ast.MarkClass
- def asFea(self, indent=""):
- return "\n".join(s.asFea(indent=indent) for s in self.statements)
- class FeatureBlock(Block):
- """A named feature block."""
- def __init__(self, name, use_extension=False, location=None):
- Block.__init__(self, location)
- self.name, self.use_extension = name, use_extension
- def build(self, builder):
- """Call the ``start_feature`` callback on the builder object, visit
- all the statements in this feature, and then call ``end_feature``."""
- # TODO(sascha): Handle use_extension.
- builder.start_feature(self.location, self.name)
- # language exclude_dflt statements modify builder.features_
- # limit them to this block with temporary builder.features_
- features = builder.features_
- builder.features_ = {}
- Block.build(self, builder)
- for key, value in builder.features_.items():
- features.setdefault(key, []).extend(value)
- builder.features_ = features
- builder.end_feature()
- def asFea(self, indent=""):
- res = indent + "feature %s " % self.name.strip()
- if self.use_extension:
- res += "useExtension "
- res += "{\n"
- res += Block.asFea(self, indent=indent)
- res += indent + "} %s;\n" % self.name.strip()
- return res
- class NestedBlock(Block):
- """A block inside another block, for example when found inside a
- ``cvParameters`` block."""
- def __init__(self, tag, block_name, location=None):
- Block.__init__(self, location)
- self.tag = tag
- self.block_name = block_name
- def build(self, builder):
- Block.build(self, builder)
- if self.block_name == "ParamUILabelNameID":
- builder.add_to_cv_num_named_params(self.tag)
- def asFea(self, indent=""):
- res = "{}{} {{\n".format(indent, self.block_name)
- res += Block.asFea(self, indent=indent)
- res += "{}}};\n".format(indent)
- return res
- class LookupBlock(Block):
- """A named lookup, containing ``statements``."""
- def __init__(self, name, use_extension=False, location=None):
- Block.__init__(self, location)
- self.name, self.use_extension = name, use_extension
- def build(self, builder):
- # TODO(sascha): Handle use_extension.
- builder.start_lookup_block(self.location, self.name)
- Block.build(self, builder)
- builder.end_lookup_block()
- def asFea(self, indent=""):
- res = "lookup {} ".format(self.name)
- if self.use_extension:
- res += "useExtension "
- res += "{\n"
- res += Block.asFea(self, indent=indent)
- res += "{}}} {};\n".format(indent, self.name)
- return res
- class TableBlock(Block):
- """A ``table ... { }`` block."""
- def __init__(self, name, location=None):
- Block.__init__(self, location)
- self.name = name
- def asFea(self, indent=""):
- res = "table {} {{\n".format(self.name.strip())
- res += super(TableBlock, self).asFea(indent=indent)
- res += "}} {};\n".format(self.name.strip())
- return res
- class GlyphClassDefinition(Statement):
- """Example: ``@UPPERCASE = [A-Z];``."""
- def __init__(self, name, glyphs, location=None):
- Statement.__init__(self, location)
- self.name = name #: class name as a string, without initial ``@``
- self.glyphs = glyphs #: a :class:`GlyphClass` object
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return tuple(self.glyphs.glyphSet())
- def asFea(self, indent=""):
- return "@" + self.name + " = " + self.glyphs.asFea() + ";"
- class GlyphClassDefStatement(Statement):
- """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters
- must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or
- ``None``."""
- def __init__(
- self, baseGlyphs, markGlyphs, ligatureGlyphs, componentGlyphs, location=None
- ):
- Statement.__init__(self, location)
- self.baseGlyphs, self.markGlyphs = (baseGlyphs, markGlyphs)
- self.ligatureGlyphs = ligatureGlyphs
- self.componentGlyphs = componentGlyphs
- def build(self, builder):
- """Calls the builder's ``add_glyphClassDef`` callback."""
- base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple()
- liga = self.ligatureGlyphs.glyphSet() if self.ligatureGlyphs else tuple()
- mark = self.markGlyphs.glyphSet() if self.markGlyphs else tuple()
- comp = self.componentGlyphs.glyphSet() if self.componentGlyphs else tuple()
- builder.add_glyphClassDef(self.location, base, liga, mark, comp)
- def asFea(self, indent=""):
- return "GlyphClassDef {}, {}, {}, {};".format(
- self.baseGlyphs.asFea() if self.baseGlyphs else "",
- self.ligatureGlyphs.asFea() if self.ligatureGlyphs else "",
- self.markGlyphs.asFea() if self.markGlyphs else "",
- self.componentGlyphs.asFea() if self.componentGlyphs else "",
- )
- class MarkClass(object):
- """One `or more` ``markClass`` statements for the same mark class.
- While glyph classes can be defined only once, the feature file format
- allows expanding mark classes with multiple definitions, each using
- different glyphs and anchors. The following are two ``MarkClassDefinitions``
- for the same ``MarkClass``::
- markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
- markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
- The ``MarkClass`` object is therefore just a container for a list of
- :class:`MarkClassDefinition` statements.
- """
- def __init__(self, name):
- self.name = name
- self.definitions = []
- self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions
- def addDefinition(self, definition):
- """Add a :class:`MarkClassDefinition` statement to this mark class."""
- assert isinstance(definition, MarkClassDefinition)
- self.definitions.append(definition)
- for glyph in definition.glyphSet():
- if glyph in self.glyphs:
- otherLoc = self.glyphs[glyph].location
- if otherLoc is None:
- end = ""
- else:
- end = f" at {otherLoc}"
- raise FeatureLibError(
- "Glyph %s already defined%s" % (glyph, end), definition.location
- )
- self.glyphs[glyph] = definition
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return tuple(self.glyphs.keys())
- def asFea(self, indent=""):
- res = "\n".join(d.asFea() for d in self.definitions)
- return res
- class MarkClassDefinition(Statement):
- """A single ``markClass`` statement. The ``markClass`` should be a
- :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
- and the ``glyphs`` parameter should be a `glyph-containing object`_ .
- Example:
- .. code:: python
- mc = MarkClass("FRENCH_ACCENTS")
- mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800),
- GlyphClass([ GlyphName("acute"), GlyphName("grave") ])
- ) )
- mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200),
- GlyphClass([ GlyphName("cedilla") ])
- ) )
- mc.asFea()
- # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
- # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
- """
- def __init__(self, markClass, anchor, glyphs, location=None):
- Statement.__init__(self, location)
- assert isinstance(markClass, MarkClass)
- assert isinstance(anchor, Anchor) and isinstance(glyphs, Expression)
- self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs
- def glyphSet(self):
- """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
- return self.glyphs.glyphSet()
- def asFea(self, indent=""):
- return "markClass {} {} @{};".format(
- self.glyphs.asFea(), self.anchor.asFea(), self.markClass.name
- )
- class AlternateSubstStatement(Statement):
- """A ``sub ... from ...`` statement.
- ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
- `glyph-containing objects`_. ``glyph`` should be a `one element list`."""
- def __init__(self, prefix, glyph, suffix, replacement, location=None):
- Statement.__init__(self, location)
- self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix)
- self.replacement = replacement
- def build(self, builder):
- """Calls the builder's ``add_alternate_subst`` callback."""
- glyph = self.glyph.glyphSet()
- assert len(glyph) == 1, glyph
- glyph = list(glyph)[0]
- prefix = [p.glyphSet() for p in self.prefix]
- suffix = [s.glyphSet() for s in self.suffix]
- replacement = self.replacement.glyphSet()
- builder.add_alternate_subst(self.location, prefix, glyph, suffix, replacement)
- def asFea(self, indent=""):
- res = "sub "
- if len(self.prefix) or len(self.suffix):
- if len(self.prefix):
- res += " ".join(map(asFea, self.prefix)) + " "
- res += asFea(self.glyph) + "'" # even though we really only use 1
- if len(self.suffix):
- res += " " + " ".join(map(asFea, self.suffix))
- else:
- res += asFea(self.glyph)
- res += " from "
- res += asFea(self.replacement)
- res += ";"
- return res
- class Anchor(Expression):
- """An ``Anchor`` element, used inside a ``pos`` rule.
- If a ``name`` is given, this will be used in preference to the coordinates.
- Other values should be integer.
- """
- def __init__(
- self,
- x,
- y,
- name=None,
- contourpoint=None,
- xDeviceTable=None,
- yDeviceTable=None,
- location=None,
- ):
- Expression.__init__(self, location)
- self.name = name
- self.x, self.y, self.contourpoint = x, y, contourpoint
- self.xDeviceTable, self.yDeviceTable = xDeviceTable, yDeviceTable
- def asFea(self, indent=""):
- if self.name is not None:
- return "<anchor {}>".format(self.name)
- res = "<anchor {} {}".format(self.x, self.y)
- if self.contourpoint:
- res += " contourpoint {}".format(self.contourpoint)
- if self.xDeviceTable or self.yDeviceTable:
- res += " "
- res += deviceToString(self.xDeviceTable)
- res += " "
- res += deviceToString(self.yDeviceTable)
- res += ">"
- return res
- class AnchorDefinition(Statement):
- """A named anchor definition. (2.e.viii). ``name`` should be a string."""
- def __init__(self, name, x, y, contourpoint=None, location=None):
- Statement.__init__(self, location)
- self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
- def asFea(self, indent=""):
- res = "anchorDef {} {}".format(self.x, self.y)
- if self.contourpoint:
- res += " contourpoint {}".format(self.contourpoint)
- res += " {};".format(self.name)
- return res
- class AttachStatement(Statement):
- """A ``GDEF`` table ``Attach`` statement."""
- def __init__(self, glyphs, contourPoints, location=None):
- Statement.__init__(self, location)
- self.glyphs = glyphs #: A `glyph-containing object`_
- self.contourPoints = contourPoints #: A list of integer contour points
- def build(self, builder):
- """Calls the builder's ``add_attach_points`` callback."""
- glyphs = self.glyphs.glyphSet()
- builder.add_attach_points(self.location, glyphs, self.contourPoints)
- def asFea(self, indent=""):
- return "Attach {} {};".format(
- self.glyphs.asFea(), " ".join(str(c) for c in self.contourPoints)
- )
- class ChainContextPosStatement(Statement):
- r"""A chained contextual positioning statement.
- ``prefix``, ``glyphs``, and ``suffix`` should be lists of
- `glyph-containing objects`_ .
- ``lookups`` should be a list of elements representing what lookups
- to apply at each glyph position. Each element should be a
- :class:`LookupBlock` to apply a single chaining lookup at the given
- position, a list of :class:`LookupBlock`\ s to apply multiple
- lookups, or ``None`` to apply no lookup. The length of the outer
- list should equal the length of ``glyphs``; the inner lists can be
- of variable length."""
- def __init__(self, prefix, glyphs, suffix, lookups, location=None):
- Statement.__init__(self, location)
- self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
- self.lookups = list(lookups)
- for i, lookup in enumerate(lookups):
- if lookup:
- try:
- (_ for _ in lookup)
- except TypeError:
- self.lookups[i] = [lookup]
- def build(self, builder):
- """Calls the builder's ``add_chain_context_pos`` callback."""
- prefix = [p.glyphSet() for p in self.prefix]
- glyphs = [g.glyphSet() for g in self.glyphs]
- suffix = [s.glyphSet() for s in self.suffix]
- builder.add_chain_context_pos(
- self.location, prefix, glyphs, suffix, self.lookups
- )
- def asFea(self, indent=""):
- res = "pos "
- if (
- len(self.prefix)
- or len(self.suffix)
- or any([x is not None for x in self.lookups])
- ):
- if len(self.prefix):
- res += " ".join(g.asFea() for g in self.prefix) + " "
- for i, g in enumerate(self.glyphs):
- res += g.asFea() + "'"
- if self.lookups[i]:
- for lu in self.lookups[i]:
- res += " lookup " + lu.name
- if i < len(self.glyphs) - 1:
- res += " "
- if len(self.suffix):
- res += " " + " ".join(map(asFea, self.suffix))
- else:
- res += " ".join(map(asFea, self.glyph))
- res += ";"
- return res
- class ChainContextSubstStatement(Statement):
- r"""A chained contextual substitution statement.
- ``prefix``, ``glyphs``, and ``suffix`` should be lists of
- `glyph-containing objects`_ .
- ``lookups`` should be a list of elements representing what lookups
- to apply at each glyph position. Each element should be a
- :class:`LookupBlock` to apply a single chaining lookup at the given
- position, a list of :class:`LookupBlock`\ s to apply multiple
- lookups, or ``None`` to apply no lookup. The length of the outer
- list should equal the length of ``glyphs``; the inner lists can be
- of variable length."""
- def __init__(self, prefix, glyphs, suffix, lookups, location=None):
- Statement.__init__(self, location)
- self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
- self.lookups = list(lookups)
- for i, lookup in enumerate(lookups):
- if lookup:
- try:
- (_ for _ in lookup)
- except TypeError:
- self.lookups[i] = [lookup]
- def build(self, builder):
- """Calls the builder's ``add_chain_context_subst`` callback."""
- prefix = [p.glyphSet() for p in self.prefix]
- glyphs = [g.glyphSet() for g in self.glyphs]
- suffix = [s.glyphSet() for s in self.suffix]
- builder.add_chain_context_subst(
- self.location, prefix, glyphs, suffix, self.lookups
- )
- def asFea(self, indent=""):
- res = "sub "
- if (
- len(self.prefix)
- or len(self.suffix)
- or any([x is not None for x in self.lookups])
- ):
- if len(self.prefix):
- res += " ".join(g.asFea() for g in self.prefix) + " "
- for i, g in enumerate(self.glyphs):
- res += g.asFea() + "'"
- if self.lookups[i]:
- for lu in self.lookups[i]:
- res += " lookup " + lu.name
- if i < len(self.glyphs) - 1:
- res += " "
- if len(self.suffix):
- res += " " + " ".join(map(asFea, self.suffix))
- else:
- res += " ".join(map(asFea, self.glyph))
- res += ";"
- return res
- class CursivePosStatement(Statement):
- """A cursive positioning statement. Entry and exit anchors can either
- be :class:`Anchor` objects or ``None``."""
- def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None):
- Statement.__init__(self, location)
- self.glyphclass = glyphclass
- self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor
- def build(self, builder):
- """Calls the builder object's ``add_cursive_pos`` callback."""
- builder.add_cursive_pos(
- self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor
- )
- def asFea(self, indent=""):
- entry = self.entryAnchor.asFea() if self.entryAnchor else "<anchor NULL>"
- exit = self.exitAnchor.asFea() if self.exitAnchor else "<anchor NULL>"
- return "pos cursive {} {} {};".format(self.glyphclass.asFea(), entry, exit)
- class FeatureReferenceStatement(Statement):
- """Example: ``feature salt;``"""
- def __init__(self, featureName, location=None):
- Statement.__init__(self, location)
- self.location, self.featureName = (location, featureName)
- def build(self, builder):
- """Calls the builder object's ``add_feature_reference`` callback."""
- builder.add_feature_reference(self.location, self.featureName)
- def asFea(self, indent=""):
- return "feature {};".format(self.featureName)
- class IgnorePosStatement(Statement):
- """An ``ignore pos`` statement, containing `one or more` contexts to ignore.
- ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
- with each of ``prefix``, ``glyphs`` and ``suffix`` being
- `glyph-containing objects`_ ."""
- def __init__(self, chainContexts, location=None):
- Statement.__init__(self, location)
- self.chainContexts = chainContexts
- def build(self, builder):
- """Calls the builder object's ``add_chain_context_pos`` callback on each
- rule context."""
- for prefix, glyphs, suffix in self.chainContexts:
- prefix = [p.glyphSet() for p in prefix]
- glyphs = [g.glyphSet() for g in glyphs]
- suffix = [s.glyphSet() for s in suffix]
- builder.add_chain_context_pos(self.location, prefix, glyphs, suffix, [])
- def asFea(self, indent=""):
- contexts = []
- for prefix, glyphs, suffix in self.chainContexts:
- res = ""
- if len(prefix) or len(suffix):
- if len(prefix):
- res += " ".join(map(asFea, prefix)) + " "
- res += " ".join(g.asFea() + "'" for g in glyphs)
- if len(suffix):
- res += " " + " ".join(map(asFea, suffix))
- else:
- res += " ".join(map(asFea, glyphs))
- contexts.append(res)
- return "ignore pos " + ", ".join(contexts) + ";"
- class IgnoreSubstStatement(Statement):
- """An ``ignore sub`` statement, containing `one or more` contexts to ignore.
- ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
- with each of ``prefix``, ``glyphs`` and ``suffix`` being
- `glyph-containing objects`_ ."""
- def __init__(self, chainContexts, location=None):
- Statement.__init__(self, location)
- self.chainContexts = chainContexts
- def build(self, builder):
- """Calls the builder object's ``add_chain_context_subst`` callback on
- each rule context."""
- for prefix, glyphs, suffix in self.chainContexts:
- prefix = [p.glyphSet() for p in prefix]
- glyphs = [g.glyphSet() for g in glyphs]
- suffix = [s.glyphSet() for s in suffix]
- builder.add_chain_context_subst(self.location, prefix, glyphs, suffix, [])
- def asFea(self, indent=""):
- contexts = []
- for prefix, glyphs, suffix in self.chainContexts:
- res = ""
- if len(prefix):
- res += " ".join(map(asFea, prefix)) + " "
- res += " ".join(g.asFea() + "'" for g in glyphs)
- if len(suffix):
- res += " " + " ".join(map(asFea, suffix))
- contexts.append(res)
- return "ignore sub " + ", ".join(contexts) + ";"
- class IncludeStatement(Statement):
- """An ``include()`` statement."""
- def __init__(self, filename, location=None):
- super(IncludeStatement, self).__init__(location)
- self.filename = filename #: String containing name of file to include
- def build(self):
- # TODO: consider lazy-loading the including parser/lexer?
- raise FeatureLibError(
- "Building an include statement is not implemented yet. "
- "Instead, use Parser(..., followIncludes=True) for building.",
- self.location,
- )
- def asFea(self, indent=""):
- return indent + "include(%s);" % self.filename
- class LanguageStatement(Statement):
- """A ``language`` statement within a feature."""
- def __init__(self, language, include_default=True, required=False, location=None):
- Statement.__init__(self, location)
- assert len(language) == 4
- self.language = language #: A four-character language tag
- self.include_default = include_default #: If false, "exclude_dflt"
- self.required = required
- def build(self, builder):
- """Call the builder object's ``set_language`` callback."""
- builder.set_language(
- location=self.location,
- language=self.language,
- include_default=self.include_default,
- required=self.required,
- )
- def asFea(self, indent=""):
- res = "language {}".format(self.language.strip())
- if not self.include_default:
- res += " exclude_dflt"
- if self.required:
- res += " required"
- res += ";"
- return res
- class LanguageSystemStatement(Statement):
- """A top-level ``languagesystem`` statement."""
- def __init__(self, script, language, location=None):
- Statement.__init__(self, location)
- self.script, self.language = (script, language)
- def build(self, builder):
- """Calls the builder object's ``add_language_system`` callback."""
- builder.add_language_system(self.location, self.script, self.language)
- def asFea(self, indent=""):
- return "languagesystem {} {};".format(self.script, self.language.strip())
- class FontRevisionStatement(Statement):
- """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
- number, and will be formatted to three significant decimal places."""
- def __init__(self, revision, location=None):
- Statement.__init__(self, location)
- self.revision = revision
- def build(self, builder):
- builder.set_font_revision(self.location, self.revision)
- def asFea(self, indent=""):
- return "FontRevision {:.3f};".format(self.revision)
- class LigatureCaretByIndexStatement(Statement):
- """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
- a `glyph-containing object`_, and ``carets`` should be a list of integers."""
- def __init__(self, glyphs, carets, location=None):
- Statement.__init__(self, location)
- self.glyphs, self.carets = (glyphs, carets)
- def build(self, builder):
- """Calls the builder object's ``add_ligatureCaretByIndex_`` callback."""
- glyphs = self.glyphs.glyphSet()
- builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets))
- def asFea(self, indent=""):
- return "LigatureCaretByIndex {} {};".format(
- self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
- )
- class LigatureCaretByPosStatement(Statement):
- """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
- a `glyph-containing object`_, and ``carets`` should be a list of integers."""
- def __init__(self, glyphs, carets, location=None):
- Statement.__init__(self, location)
- self.glyphs, self.carets = (glyphs, carets)
- def build(self, builder):
- """Calls the builder object's ``add_ligatureCaretByPos_`` callback."""
- glyphs = self.glyphs.glyphSet()
- builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets))
- def asFea(self, indent=""):
- return "LigatureCaretByPos {} {};".format(
- self.glyphs.asFea(), " ".join(str(x) for x in self.carets)
- )
- class LigatureSubstStatement(Statement):
- """A chained contextual substitution statement.
- ``prefix``, ``glyphs``, and ``suffix`` should be lists of
- `glyph-containing objects`_; ``replacement`` should be a single
- `glyph-containing object`_.
- If ``forceChain`` is True, this is expressed as a chaining rule
- (e.g. ``sub f' i' by f_i``) even when no context is given."""
- def __init__(self, prefix, glyphs, suffix, replacement, forceChain, location=None):
- Statement.__init__(self, location)
- self.prefix, self.glyphs, self.suffix = (prefix, glyphs, suffix)
- self.replacement, self.forceChain = replacement, forceChain
- def build(self, builder):
- prefix = [p.glyphSet() for p in self.prefix]
- glyphs = [g.glyphSet() for g in self.glyphs]
- suffix = [s.glyphSet() for s in self.suffix]
- builder.add_ligature_subst(
- self.location, prefix, glyphs, suffix, self.replacement, self.forceChain
- )
- def asFea(self, indent=""):
- res = "sub "
- if len(self.prefix) or len(self.suffix) or self.forceChain:
- if len(self.prefix):
- res += " ".join(g.asFea() for g in self.prefix) + " "
- res += " ".join(g.asFea() + "'" for g in self.glyphs)
- if len(self.suffix):
- res += " " + " ".join(g.asFea() for g in self.suffix)
- else:
- res += " ".join(g.asFea() for g in self.glyphs)
- res += " by "
- res += asFea(self.replacement)
- res += ";"
- return res
- class LookupFlagStatement(Statement):
- """A ``lookupflag`` statement. The ``value`` should be an integer value
- representing the flags in use, but not including the ``markAttachment``
- class and ``markFilteringSet`` values, which must be specified as
- glyph-containing objects."""
- def __init__(
- self, value=0, markAttachment=None, markFilteringSet=None, location=None
- ):
- Statement.__init__(self, location)
- self.value = value
- self.markAttachment = markAttachment
- self.markFilteringSet = markFilteringSet
- def build(self, builder):
- """Calls the builder object's ``set_lookup_flag`` callback."""
- markAttach = None
- if self.markAttachment is not None:
- markAttach = self.markAttachment.glyphSet()
- markFilter = None
- if self.markFilteringSet is not None:
- markFilter = self.markFilteringSet.glyphSet()
- builder.set_lookup_flag(self.location, self.value, markAttach, markFilter)
- def asFea(self, indent=""):
- res = []
- flags = ["RightToLeft", "IgnoreBaseGlyphs", "IgnoreLigatures", "IgnoreMarks"]
- curr = 1
- for i in range(len(flags)):
- if self.value & curr != 0:
- res.append(flags[i])
- curr = curr << 1
- if self.markAttachment is not None:
- res.append("MarkAttachmentType {}".format(self.markAttachment.asFea()))
- if self.markFilteringSet is not None:
- res.append("UseMarkFilteringSet {}".format(self.markFilteringSet.asFea()))
- if not res:
- res = ["0"]
- return "lookupflag {};".format(" ".join(res))
- class LookupReferenceStatement(Statement):
- """Represents a ``lookup ...;`` statement to include a lookup in a feature.
- The ``lookup`` should be a :class:`LookupBlock` object."""
- def __init__(self, lookup, location=None):
- Statement.__init__(self, location)
- self.location, self.lookup = (location, lookup)
- def build(self, builder):
- """Calls the builder object's ``add_lookup_call`` callback."""
- builder.add_lookup_call(self.lookup.name)
- def asFea(self, indent=""):
- return "lookup {};".format(self.lookup.name)
- class MarkBasePosStatement(Statement):
- """A mark-to-base positioning rule. The ``base`` should be a
- `glyph-containing object`_. The ``marks`` should be a list of
- (:class:`Anchor`, :class:`MarkClass`) tuples."""
- def __init__(self, base, marks, location=None):
- Statement.__init__(self, location)
- self.base, self.marks = base, marks
- def build(self, builder):
- """Calls the builder object's ``add_mark_base_pos`` callback."""
- builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks)
- def asFea(self, indent=""):
- res = "pos base {}".format(self.base.asFea())
- for a, m in self.marks:
- res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
- res += ";"
- return res
- class MarkLigPosStatement(Statement):
- """A mark-to-ligature positioning rule. The ``ligatures`` must be a
- `glyph-containing object`_. The ``marks`` should be a list of lists: each
- element in the top-level list represents a component glyph, and is made
- up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing
- mark attachment points for that position.
- Example::
- m1 = MarkClass("TOP_MARKS")
- m2 = MarkClass("BOTTOM_MARKS")
- # ... add definitions to mark classes...
- glyph = GlyphName("lam_meem_jeem")
- marks = [
- [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
- [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
- [ ] # No attachments on the jeem
- ]
- mlp = MarkLigPosStatement(glyph, marks)
- mlp.asFea()
- # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
- # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
- """
- def __init__(self, ligatures, marks, location=None):
- Statement.__init__(self, location)
- self.ligatures, self.marks = ligatures, marks
- def build(self, builder):
- """Calls the builder object's ``add_mark_lig_pos`` callback."""
- builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks)
- def asFea(self, indent=""):
- res = "pos ligature {}".format(self.ligatures.asFea())
- ligs = []
- for l in self.marks:
- temp = ""
- if l is None or not len(l):
- temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>"
- else:
- for a, m in l:
- temp += (
- "\n"
- + indent
- + SHIFT * 2
- + "{} mark @{}".format(a.asFea(), m.name)
- )
- ligs.append(temp)
- res += ("\n" + indent + SHIFT + "ligComponent").join(ligs)
- res += ";"
- return res
- class MarkMarkPosStatement(Statement):
- """A mark-to-mark positioning rule. The ``baseMarks`` must be a
- `glyph-containing object`_. The ``marks`` should be a list of
- (:class:`Anchor`, :class:`MarkClass`) tuples."""
- def __init__(self, baseMarks, marks, location=None):
- Statement.__init__(self, location)
- self.baseMarks, self.marks = baseMarks, marks
- def build(self, builder):
- """Calls the builder object's ``add_mark_mark_pos`` callback."""
- builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks)
- def asFea(self, indent=""):
- res = "pos mark {}".format(self.baseMarks.asFea())
- for a, m in self.marks:
- res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
- res += ";"
- return res
- class MultipleSubstStatement(Statement):
- """A multiple substitution statement.
- Args:
- prefix: a list of `glyph-containing objects`_.
- glyph: a single glyph-containing object.
- suffix: a list of glyph-containing objects.
- replacement: a list of glyph-containing objects.
- forceChain: If true, the statement is expressed as a chaining rule
- (e.g. ``sub f' i' by f_i``) even when no context is given.
- """
- def __init__(
- self, prefix, glyph, suffix, replacement, forceChain=False, location=None
- ):
- Statement.__init__(self, location)
- self.prefix, self.glyph, self.suffix = prefix, glyph, suffix
- self.replacement = replacement
- self.forceChain = forceChain
- def build(self, builder):
- """Calls the builder object's ``add_multiple_subst`` callback."""
- prefix = [p.glyphSet() for p in self.prefix]
- suffix = [s.glyphSet() for s in self.suffix]
- if hasattr(self.glyph, "glyphSet"):
- originals = self.glyph.glyphSet()
- else:
- originals = [self.glyph]
- count = len(originals)
- replaces = []
- for r in self.replacement:
- if hasattr(r, "glyphSet"):
- replace = r.glyphSet()
- else:
- replace = [r]
- if len(replace) == 1 and len(replace) != count:
- replace = replace * count
- replaces.append(replace)
- replaces = list(zip(*replaces))
- seen_originals = set()
- for i, original in enumerate(originals):
- if original not in seen_originals:
- seen_originals.add(original)
- builder.add_multiple_subst(
- self.location,
- prefix,
- original,
- suffix,
- replaces and replaces[i] or (),
- self.forceChain,
- )
- def asFea(self, indent=""):
- res = "sub "
- if len(self.prefix) or len(self.suffix) or self.forceChain:
- if len(self.prefix):
- res += " ".join(map(asFea, self.prefix)) + " "
- res += asFea(self.glyph) + "'"
- if len(self.suffix):
- res += " " + " ".join(map(asFea, self.suffix))
- else:
- res += asFea(self.glyph)
- replacement = self.replacement or [NullGlyph()]
- res += " by "
- res += " ".join(map(asFea, replacement))
- res += ";"
- return res
- class PairPosStatement(Statement):
- """A pair positioning statement.
- ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_.
- ``valuerecord1`` should be a :class:`ValueRecord` object;
- ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``.
- If ``enumerated`` is true, then this is expressed as an
- `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
- """
- def __init__(
- self,
- glyphs1,
- valuerecord1,
- glyphs2,
- valuerecord2,
- enumerated=False,
- location=None,
- ):
- Statement.__init__(self, location)
- self.enumerated = enumerated
- self.glyphs1, self.valuerecord1 = glyphs1, valuerecord1
- self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2
- def build(self, builder):
- """Calls a callback on the builder object:
- * If the rule is enumerated, calls ``add_specific_pair_pos`` on each
- combination of first and second glyphs.
- * If the glyphs are both single :class:`GlyphName` objects, calls
- ``add_specific_pair_pos``.
- * Else, calls ``add_class_pair_pos``.
- """
- if self.enumerated:
- g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
- seen_pair = False
- for glyph1, glyph2 in itertools.product(*g):
- seen_pair = True
- builder.add_specific_pair_pos(
- self.location, glyph1, self.valuerecord1, glyph2, self.valuerecord2
- )
- if not seen_pair:
- raise FeatureLibError(
- "Empty glyph class in positioning rule", self.location
- )
- return
- is_specific = isinstance(self.glyphs1, GlyphName) and isinstance(
- self.glyphs2, GlyphName
- )
- if is_specific:
- builder.add_specific_pair_pos(
- self.location,
- self.glyphs1.glyph,
- self.valuerecord1,
- self.glyphs2.glyph,
- self.valuerecord2,
- )
- else:
- builder.add_class_pair_pos(
- self.location,
- self.glyphs1.glyphSet(),
- self.valuerecord1,
- self.glyphs2.glyphSet(),
- self.valuerecord2,
- )
- def asFea(self, indent=""):
- res = "enum " if self.enumerated else ""
- if self.valuerecord2:
- res += "pos {} {} {} {};".format(
- self.glyphs1.asFea(),
- self.valuerecord1.asFea(),
- self.glyphs2.asFea(),
- self.valuerecord2.asFea(),
- )
- else:
- res += "pos {} {} {};".format(
- self.glyphs1.asFea(), self.glyphs2.asFea(), self.valuerecord1.asFea()
- )
- return res
- class ReverseChainSingleSubstStatement(Statement):
- """A reverse chaining substitution statement. You don't see those every day.
- Note the unusual argument order: ``suffix`` comes `before` ``glyphs``.
- ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be
- lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
- be one-item lists.
- """
- def __init__(self, old_prefix, old_suffix, glyphs, replacements, location=None):
- Statement.__init__(self, location)
- self.old_prefix, self.old_suffix = old_prefix, old_suffix
- self.glyphs = glyphs
- self.replacements = replacements
- def build(self, builder):
- prefix = [p.glyphSet() for p in self.old_prefix]
- suffix = [s.glyphSet() for s in self.old_suffix]
- originals = self.glyphs[0].glyphSet()
- replaces = self.replacements[0].glyphSet()
- if len(replaces) == 1:
- replaces = replaces * len(originals)
- builder.add_reverse_chain_single_subst(
- self.location, prefix, suffix, dict(zip(originals, replaces))
- )
- def asFea(self, indent=""):
- res = "rsub "
- if len(self.old_prefix) or len(self.old_suffix):
- if len(self.old_prefix):
- res += " ".join(asFea(g) for g in self.old_prefix) + " "
- res += " ".join(asFea(g) + "'" for g in self.glyphs)
- if len(self.old_suffix):
- res += " " + " ".join(asFea(g) for g in self.old_suffix)
- else:
- res += " ".join(map(asFea, self.glyphs))
- res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
- return res
- class SingleSubstStatement(Statement):
- """A single substitution statement.
- Note the unusual argument order: ``prefix`` and suffix come `after`
- the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and
- ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and
- ``replace`` should be one-item lists.
- """
- def __init__(self, glyphs, replace, prefix, suffix, forceChain, location=None):
- Statement.__init__(self, location)
- self.prefix, self.suffix = prefix, suffix
- self.forceChain = forceChain
- self.glyphs = glyphs
- self.replacements = replace
- def build(self, builder):
- """Calls the builder object's ``add_single_subst`` callback."""
- prefix = [p.glyphSet() for p in self.prefix]
- suffix = [s.glyphSet() for s in self.suffix]
- originals = self.glyphs[0].glyphSet()
- replaces = self.replacements[0].glyphSet()
- if len(replaces) == 1:
- replaces = replaces * len(originals)
- builder.add_single_subst(
- self.location,
- prefix,
- suffix,
- OrderedDict(zip(originals, replaces)),
- self.forceChain,
- )
- def asFea(self, indent=""):
- res = "sub "
- if len(self.prefix) or len(self.suffix) or self.forceChain:
- if len(self.prefix):
- res += " ".join(asFea(g) for g in self.prefix) + " "
- res += " ".join(asFea(g) + "'" for g in self.glyphs)
- if len(self.suffix):
- res += " " + " ".join(asFea(g) for g in self.suffix)
- else:
- res += " ".join(asFea(g) for g in self.glyphs)
- res += " by {};".format(" ".join(asFea(g) for g in self.replacements))
- return res
- class ScriptStatement(Statement):
- """A ``script`` statement."""
- def __init__(self, script, location=None):
- Statement.__init__(self, location)
- self.script = script #: the script code
- def build(self, builder):
- """Calls the builder's ``set_script`` callback."""
- builder.set_script(self.location, self.script)
- def asFea(self, indent=""):
- return "script {};".format(self.script.strip())
- class SinglePosStatement(Statement):
- """A single position statement. ``prefix`` and ``suffix`` should be
- lists of `glyph-containing objects`_.
- ``pos`` should be a one-element list containing a (`glyph-containing object`_,
- :class:`ValueRecord`) tuple."""
- def __init__(self, pos, prefix, suffix, forceChain, location=None):
- Statement.__init__(self, location)
- self.pos, self.prefix, self.suffix = pos, prefix, suffix
- self.forceChain = forceChain
- def build(self, builder):
- """Calls the builder object's ``add_single_pos`` callback."""
- prefix = [p.glyphSet() for p in self.prefix]
- suffix = [s.glyphSet() for s in self.suffix]
- pos = [(g.glyphSet(), value) for g, value in self.pos]
- builder.add_single_pos(self.location, prefix, suffix, pos, self.forceChain)
- def asFea(self, indent=""):
- res = "pos "
- if len(self.prefix) or len(self.suffix) or self.forceChain:
- if len(self.prefix):
- res += " ".join(map(asFea, self.prefix)) + " "
- res += " ".join(
- [
- asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "")
- for x in self.pos
- ]
- )
- if len(self.suffix):
- res += " " + " ".join(map(asFea, self.suffix))
- else:
- res += " ".join(
- [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos]
- )
- res += ";"
- return res
- class SubtableStatement(Statement):
- """Represents a subtable break."""
- def __init__(self, location=None):
- Statement.__init__(self, location)
- def build(self, builder):
- """Calls the builder objects's ``add_subtable_break`` callback."""
- builder.add_subtable_break(self.location)
- def asFea(self, indent=""):
- return "subtable;"
- class ValueRecord(Expression):
- """Represents a value record."""
- def __init__(
- self,
- xPlacement=None,
- yPlacement=None,
- xAdvance=None,
- yAdvance=None,
- xPlaDevice=None,
- yPlaDevice=None,
- xAdvDevice=None,
- yAdvDevice=None,
- vertical=False,
- location=None,
- ):
- Expression.__init__(self, location)
- self.xPlacement, self.yPlacement = (xPlacement, yPlacement)
- self.xAdvance, self.yAdvance = (xAdvance, yAdvance)
- self.xPlaDevice, self.yPlaDevice = (xPlaDevice, yPlaDevice)
- self.xAdvDevice, self.yAdvDevice = (xAdvDevice, yAdvDevice)
- self.vertical = vertical
- def __eq__(self, other):
- return (
- self.xPlacement == other.xPlacement
- and self.yPlacement == other.yPlacement
- and self.xAdvance == other.xAdvance
- and self.yAdvance == other.yAdvance
- and self.xPlaDevice == other.xPlaDevice
- and self.xAdvDevice == other.xAdvDevice
- )
- def __ne__(self, other):
- return not self.__eq__(other)
- def __hash__(self):
- return (
- hash(self.xPlacement)
- ^ hash(self.yPlacement)
- ^ hash(self.xAdvance)
- ^ hash(self.yAdvance)
- ^ hash(self.xPlaDevice)
- ^ hash(self.yPlaDevice)
- ^ hash(self.xAdvDevice)
- ^ hash(self.yAdvDevice)
- )
- def asFea(self, indent=""):
- if not self:
- return "<NULL>"
- x, y = self.xPlacement, self.yPlacement
- xAdvance, yAdvance = self.xAdvance, self.yAdvance
- xPlaDevice, yPlaDevice = self.xPlaDevice, self.yPlaDevice
- xAdvDevice, yAdvDevice = self.xAdvDevice, self.yAdvDevice
- vertical = self.vertical
- # Try format A, if possible.
- if x is None and y is None:
- if xAdvance is None and vertical:
- return str(yAdvance)
- elif yAdvance is None and not vertical:
- return str(xAdvance)
- # Make any remaining None value 0 to avoid generating invalid records.
- x = x or 0
- y = y or 0
- xAdvance = xAdvance or 0
- yAdvance = yAdvance or 0
- # Try format B, if possible.
- if (
- xPlaDevice is None
- and yPlaDevice is None
- and xAdvDevice is None
- and yAdvDevice is None
- ):
- return "<%s %s %s %s>" % (x, y, xAdvance, yAdvance)
- # Last resort is format C.
- return "<%s %s %s %s %s %s %s %s>" % (
- x,
- y,
- xAdvance,
- yAdvance,
- deviceToString(xPlaDevice),
- deviceToString(yPlaDevice),
- deviceToString(xAdvDevice),
- deviceToString(yAdvDevice),
- )
- def __bool__(self):
- return any(
- getattr(self, v) is not None
- for v in [
- "xPlacement",
- "yPlacement",
- "xAdvance",
- "yAdvance",
- "xPlaDevice",
- "yPlaDevice",
- "xAdvDevice",
- "yAdvDevice",
- ]
- )
- __nonzero__ = __bool__
- class ValueRecordDefinition(Statement):
- """Represents a named value record definition."""
- def __init__(self, name, value, location=None):
- Statement.__init__(self, location)
- self.name = name #: Value record name as string
- self.value = value #: :class:`ValueRecord` object
- def asFea(self, indent=""):
- return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
- def simplify_name_attributes(pid, eid, lid):
- if pid == 3 and eid == 1 and lid == 1033:
- return ""
- elif pid == 1 and eid == 0 and lid == 0:
- return "1"
- else:
- return "{} {} {}".format(pid, eid, lid)
- class NameRecord(Statement):
- """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
- def __init__(self, nameID, platformID, platEncID, langID, string, location=None):
- Statement.__init__(self, location)
- self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name)
- self.platformID = platformID #: Platform ID as integer
- self.platEncID = platEncID #: Platform encoding ID as integer
- self.langID = langID #: Language ID as integer
- self.string = string #: Name record value
- def build(self, builder):
- """Calls the builder object's ``add_name_record`` callback."""
- builder.add_name_record(
- self.location,
- self.nameID,
- self.platformID,
- self.platEncID,
- self.langID,
- self.string,
- )
- def asFea(self, indent=""):
- def escape(c, escape_pattern):
- # Also escape U+0022 QUOTATION MARK and U+005C REVERSE SOLIDUS
- if c >= 0x20 and c <= 0x7E and c not in (0x22, 0x5C):
- return chr(c)
- else:
- return escape_pattern % c
- encoding = getEncoding(self.platformID, self.platEncID, self.langID)
- if encoding is None:
- raise FeatureLibError("Unsupported encoding", self.location)
- s = tobytes(self.string, encoding=encoding)
- if encoding == "utf_16_be":
- escaped_string = "".join(
- [
- escape(byteord(s[i]) * 256 + byteord(s[i + 1]), r"\%04x")
- for i in range(0, len(s), 2)
- ]
- )
- else:
- escaped_string = "".join([escape(byteord(b), r"\%02x") for b in s])
- plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
- if plat != "":
- plat += " "
- return 'nameid {} {}"{}";'.format(self.nameID, plat, escaped_string)
- class FeatureNameStatement(NameRecord):
- """Represents a ``sizemenuname`` or ``name`` statement."""
- def build(self, builder):
- """Calls the builder object's ``add_featureName`` callback."""
- NameRecord.build(self, builder)
- builder.add_featureName(self.nameID)
- def asFea(self, indent=""):
- if self.nameID == "size":
- tag = "sizemenuname"
- else:
- tag = "name"
- plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
- if plat != "":
- plat += " "
- return '{} {}"{}";'.format(tag, plat, self.string)
- class STATNameStatement(NameRecord):
- """Represents a STAT table ``name`` statement."""
- def asFea(self, indent=""):
- plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
- if plat != "":
- plat += " "
- return 'name {}"{}";'.format(plat, self.string)
- class SizeParameters(Statement):
- """A ``parameters`` statement."""
- def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd, location=None):
- Statement.__init__(self, location)
- self.DesignSize = DesignSize
- self.SubfamilyID = SubfamilyID
- self.RangeStart = RangeStart
- self.RangeEnd = RangeEnd
- def build(self, builder):
- """Calls the builder object's ``set_size_parameters`` callback."""
- builder.set_size_parameters(
- self.location,
- self.DesignSize,
- self.SubfamilyID,
- self.RangeStart,
- self.RangeEnd,
- )
- def asFea(self, indent=""):
- res = "parameters {:.1f} {}".format(self.DesignSize, self.SubfamilyID)
- if self.RangeStart != 0 or self.RangeEnd != 0:
- res += " {} {}".format(int(self.RangeStart * 10), int(self.RangeEnd * 10))
- return res + ";"
- class CVParametersNameStatement(NameRecord):
- """Represent a name statement inside a ``cvParameters`` block."""
- def __init__(
- self, nameID, platformID, platEncID, langID, string, block_name, location=None
- ):
- NameRecord.__init__(
- self, nameID, platformID, platEncID, langID, string, location=location
- )
- self.block_name = block_name
- def build(self, builder):
- """Calls the builder object's ``add_cv_parameter`` callback."""
- item = ""
- if self.block_name == "ParamUILabelNameID":
- item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0))
- builder.add_cv_parameter(self.nameID)
- self.nameID = (self.nameID, self.block_name + item)
- NameRecord.build(self, builder)
- def asFea(self, indent=""):
- plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
- if plat != "":
- plat += " "
- return 'name {}"{}";'.format(plat, self.string)
- class CharacterStatement(Statement):
- """
- Statement used in cvParameters blocks of Character Variant features (cvXX).
- The Unicode value may be written with either decimal or hexadecimal
- notation. The value must be preceded by '0x' if it is a hexadecimal value.
- The largest Unicode value allowed is 0xFFFFFF.
- """
- def __init__(self, character, tag, location=None):
- Statement.__init__(self, location)
- self.character = character
- self.tag = tag
- def build(self, builder):
- """Calls the builder object's ``add_cv_character`` callback."""
- builder.add_cv_character(self.character, self.tag)
- def asFea(self, indent=""):
- return "Character {:#x};".format(self.character)
- class BaseAxis(Statement):
- """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
- pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
- def __init__(self, bases, scripts, vertical, location=None):
- Statement.__init__(self, location)
- self.bases = bases #: A list of baseline tag names as strings
- self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
- self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False
- def build(self, builder):
- """Calls the builder object's ``set_base_axis`` callback."""
- builder.set_base_axis(self.bases, self.scripts, self.vertical)
- def asFea(self, indent=""):
- direction = "Vert" if self.vertical else "Horiz"
- scripts = [
- "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2])))
- for a in self.scripts
- ]
- return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format(
- direction, " ".join(self.bases), indent, direction, ", ".join(scripts)
- )
- class OS2Field(Statement):
- """An entry in the ``OS/2`` table. Most ``values`` should be numbers or
- strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
- or ``Panose``, in which case it should be an array of integers."""
- def __init__(self, key, value, location=None):
- Statement.__init__(self, location)
- self.key = key
- self.value = value
- def build(self, builder):
- """Calls the builder object's ``add_os2_field`` callback."""
- builder.add_os2_field(self.key, self.value)
- def asFea(self, indent=""):
- def intarr2str(x):
- return " ".join(map(str, x))
- numbers = (
- "FSType",
- "TypoAscender",
- "TypoDescender",
- "TypoLineGap",
- "winAscent",
- "winDescent",
- "XHeight",
- "CapHeight",
- "WeightClass",
- "WidthClass",
- "LowerOpSize",
- "UpperOpSize",
- )
- ranges = ("UnicodeRange", "CodePageRange")
- keywords = dict([(x.lower(), [x, str]) for x in numbers])
- keywords.update([(x.lower(), [x, intarr2str]) for x in ranges])
- keywords["panose"] = ["Panose", intarr2str]
- keywords["vendor"] = ["Vendor", lambda y: '"{}"'.format(y)]
- if self.key in keywords:
- return "{} {};".format(
- keywords[self.key][0], keywords[self.key][1](self.value)
- )
- return "" # should raise exception
- class HheaField(Statement):
- """An entry in the ``hhea`` table."""
- def __init__(self, key, value, location=None):
- Statement.__init__(self, location)
- self.key = key
- self.value = value
- def build(self, builder):
- """Calls the builder object's ``add_hhea_field`` callback."""
- builder.add_hhea_field(self.key, self.value)
- def asFea(self, indent=""):
- fields = ("CaretOffset", "Ascender", "Descender", "LineGap")
- keywords = dict([(x.lower(), x) for x in fields])
- return "{} {};".format(keywords[self.key], self.value)
- class VheaField(Statement):
- """An entry in the ``vhea`` table."""
- def __init__(self, key, value, location=None):
- Statement.__init__(self, location)
- self.key = key
- self.value = value
- def build(self, builder):
- """Calls the builder object's ``add_vhea_field`` callback."""
- builder.add_vhea_field(self.key, self.value)
- def asFea(self, indent=""):
- fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
- keywords = dict([(x.lower(), x) for x in fields])
- return "{} {};".format(keywords[self.key], self.value)
- class STATDesignAxisStatement(Statement):
- """A STAT table Design Axis
- Args:
- tag (str): a 4 letter axis tag
- axisOrder (int): an int
- names (list): a list of :class:`STATNameStatement` objects
- """
- def __init__(self, tag, axisOrder, names, location=None):
- Statement.__init__(self, location)
- self.tag = tag
- self.axisOrder = axisOrder
- self.names = names
- self.location = location
- def build(self, builder):
- builder.addDesignAxis(self, self.location)
- def asFea(self, indent=""):
- indent += SHIFT
- res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
- res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
- res += "};"
- return res
- class ElidedFallbackName(Statement):
- """STAT table ElidedFallbackName
- Args:
- names: a list of :class:`STATNameStatement` objects
- """
- def __init__(self, names, location=None):
- Statement.__init__(self, location)
- self.names = names
- self.location = location
- def build(self, builder):
- builder.setElidedFallbackName(self.names, self.location)
- def asFea(self, indent=""):
- indent += SHIFT
- res = "ElidedFallbackName { \n"
- res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
- res += "};"
- return res
- class ElidedFallbackNameID(Statement):
- """STAT table ElidedFallbackNameID
- Args:
- value: an int pointing to an existing name table name ID
- """
- def __init__(self, value, location=None):
- Statement.__init__(self, location)
- self.value = value
- self.location = location
- def build(self, builder):
- builder.setElidedFallbackName(self.value, self.location)
- def asFea(self, indent=""):
- return f"ElidedFallbackNameID {self.value};"
- class STATAxisValueStatement(Statement):
- """A STAT table Axis Value Record
- Args:
- names (list): a list of :class:`STATNameStatement` objects
- locations (list): a list of :class:`AxisValueLocationStatement` objects
- flags (int): an int
- """
- def __init__(self, names, locations, flags, location=None):
- Statement.__init__(self, location)
- self.names = names
- self.locations = locations
- self.flags = flags
- def build(self, builder):
- builder.addAxisValueRecord(self, self.location)
- def asFea(self, indent=""):
- res = "AxisValue {\n"
- for location in self.locations:
- res += location.asFea()
- for nameRecord in self.names:
- res += nameRecord.asFea()
- res += "\n"
- if self.flags:
- flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
- flagStrings = []
- curr = 1
- for i in range(len(flags)):
- if self.flags & curr != 0:
- flagStrings.append(flags[i])
- curr = curr << 1
- res += f"flag {' '.join(flagStrings)};\n"
- res += "};"
- return res
- class AxisValueLocationStatement(Statement):
- """
- A STAT table Axis Value Location
- Args:
- tag (str): a 4 letter axis tag
- values (list): a list of ints and/or floats
- """
- def __init__(self, tag, values, location=None):
- Statement.__init__(self, location)
- self.tag = tag
- self.values = values
- def asFea(self, res=""):
- res += f"location {self.tag} "
- res += f"{' '.join(str(i) for i in self.values)};\n"
- return res
- class ConditionsetStatement(Statement):
- """
- A variable layout conditionset
- Args:
- name (str): the name of this conditionset
- conditions (dict): a dictionary mapping axis tags to a
- tuple of (min,max) userspace coordinates.
- """
- def __init__(self, name, conditions, location=None):
- Statement.__init__(self, location)
- self.name = name
- self.conditions = conditions
- def build(self, builder):
- builder.add_conditionset(self.location, self.name, self.conditions)
- def asFea(self, res="", indent=""):
- res += indent + f"conditionset {self.name} " + "{\n"
- for tag, (minvalue, maxvalue) in self.conditions.items():
- res += indent + SHIFT + f"{tag} {minvalue} {maxvalue};\n"
- res += indent + "}" + f" {self.name};\n"
- return res
- class VariationBlock(Block):
- """A variation feature block, applicable in a given set of conditions."""
- def __init__(self, name, conditionset, use_extension=False, location=None):
- Block.__init__(self, location)
- self.name, self.conditionset, self.use_extension = (
- name,
- conditionset,
- use_extension,
- )
- def build(self, builder):
- """Call the ``start_feature`` callback on the builder object, visit
- all the statements in this feature, and then call ``end_feature``."""
- builder.start_feature(self.location, self.name)
- if (
- self.conditionset != "NULL"
- and self.conditionset not in builder.conditionsets_
- ):
- raise FeatureLibError(
- f"variation block used undefined conditionset {self.conditionset}",
- self.location,
- )
- # language exclude_dflt statements modify builder.features_
- # limit them to this block with temporary builder.features_
- features = builder.features_
- builder.features_ = {}
- Block.build(self, builder)
- for key, value in builder.features_.items():
- items = builder.feature_variations_.setdefault(key, {}).setdefault(
- self.conditionset, []
- )
- items.extend(value)
- if key not in features:
- features[key] = [] # Ensure we make a feature record
- builder.features_ = features
- builder.end_feature()
- def asFea(self, indent=""):
- res = indent + "variation %s " % self.name.strip()
- res += self.conditionset + " "
- if self.use_extension:
- res += "useExtension "
- res += "{\n"
- res += Block.asFea(self, indent=indent)
- res += indent + "} %s;\n" % self.name.strip()
- return res
|