2135 lines
72 KiB
Python
2135 lines
72 KiB
Python
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
|