Exemplo n.º 1
0
def parseLayoutFeatures(font):
    """Parse OpenType layout features in the UFO and return a
    feaLib.ast.FeatureFile instance.
    """
    featxt = font.features.text or ""
    if not featxt:
        return ast.FeatureFile()
    buf = StringIO(featxt)
    ufoPath = font.path
    includeDir = None
    if ufoPath is not None:
        # The UFO v3 specification says "Any include() statements must be relative to
        # the UFO path, not to the features.fea file itself". We set the `name`
        # attribute on the buffer to the actual feature file path, which feaLib will
        # pick up and use to attribute errors to the correct file, and explicitly set
        # the include directory to the parent of the UFO.
        ufoPath = os.path.normpath(ufoPath)
        buf.name = os.path.join(ufoPath, "features.fea")
        includeDir = os.path.dirname(ufoPath)
    glyphNames = set(font.keys())
    try:
        parser = Parser(buf, glyphNames, includeDir=includeDir)
        doc = parser.parse()
    except IncludedFeaNotFound as e:
        if ufoPath and os.path.exists(os.path.join(ufoPath, e.args[0])):
            logger.warning("Please change the file name in the include(...); "
                           "statement to be relative to the UFO itself, "
                           "instead of relative to the 'features.fea' file "
                           "contained in it.")
        raise
    return doc
Exemplo n.º 2
0
def parseLayoutFeatures(font):
    """ Parse OpenType layout features in the UFO and return a
    feaLib.ast.FeatureFile instance.
    """
    featxt = tounicode(font.features.text or "", "utf-8")
    if not featxt:
        return ast.FeatureFile()
    buf = UnicodeIO(featxt)
    # the path is used by the lexer to resolve 'include' statements
    # and print filename in error messages. For the UFO spec, this
    # should be the path of the UFO, not the inner features.fea:
    # https://github.com/unified-font-object/ufo-spec/issues/55
    ufoPath = font.path
    if ufoPath is not None:
        buf.name = ufoPath
    glyphNames = set(font.keys())
    try:
        parser = Parser(buf, glyphNames)
        doc = parser.parse()
    except IncludedFeaNotFound as e:
        if ufoPath and os.path.exists(os.path.join(ufoPath, e.args[0])):
            logger.warning("Please change the file name in the include(...); "
                           "statement to be relative to the UFO itself, "
                           "instead of relative to the 'features.fea' file "
                           "contained in it.")
        raise
    return doc
Exemplo n.º 3
0
 def check_fea2fea_file(self, name):
     f = self.getpath("{}.fea".format(name))
     p = Parser(f)
     doc = p.parse()
     tlines = self.normal_fea(doc.asFea().split("\n"))
     with open(f, "r", encoding="utf-8") as ofile:
         olines = self.normal_fea(ofile.readlines())
     if olines != tlines:
         for line in difflib.unified_diff(olines, tlines):
             sys.stderr.write(line)
         self.fail("Fea2Fea output is different from expected")
Exemplo n.º 4
0
 def check_fea2fea_file(self, name):
     f = self.getpath("{}.fea".format(name))
     p = Parser(f)
     doc = p.parse()
     tlines = self.normal_fea(doc.asFea().split("\n"))
     with open(f, "r", encoding="utf-8") as ofile:
         olines = self.normal_fea(ofile.readlines())
     if olines != tlines:
         for line in difflib.unified_diff(olines, tlines):
             sys.stdout.write(line)
         self.fail("Fea2Fea output is different from expected")
Exemplo n.º 5
0
def _parseFeaSource(featureSource):
    pos = 0
    while True:
        m = _feaIncludePat.search(featureSource, pos)
        if m is None:
            break
        pos = m.end()

        lineStart = featureSource.rfind("\n", 0, m.start())
        lineEnd = featureSource.find("\n", m.end())
        if lineStart == -1:
            lineStart = 0
        if lineEnd == -1:
            lineEnd = len(featureSource)
        line = featureSource[lineStart:lineEnd]
        f = io.StringIO(line)
        p = FeatureParser(f, followIncludes=False)
        for st in p.parse().statements:
            if isinstance(st, IncludeStatement):
                yield st.filename
Exemplo n.º 6
0
reMarkClass = re.compile(r"markClass.*;")
markClassDefinitions = reMarkClass.findall(
    markFeatures)  # Save all the markClass definitions

# Read the input feature file
with open(in_path, "r") as input_fea:
    # Replace $markClasses with the generated markClassDefinitions
    if markClassDefinitions != []:
        features = input_fea.read().replace(
            "$markClasses", "\n".join(markClassDefinitions))
    else:
        features = input_fea.read().replace("$markClasses", "")

    if stylename:
        # Replace the matra I style-based include path for each style
        print("Replacing $stylename with '%s' if found in feature file" %
              stylename)
        features = features.replace("$stylename", stylename)

    # Write to a temporary file, which we can parse and expand all includes
    with open("production/features/tmp.fea", "w") as tmp:
        tmp.write(features)

parser = Parser("production/features/tmp.fea")
parsed = parser.parse()
os.remove("production/features/tmp.fea")

# Write the parsed and substituted features to the UFO features
with open(out_path + "/features.fea", "w") as fea:
    fea.write(str(parsed))
Exemplo n.º 7
0
class FeaParser:
    """Turns a AFDKO feature file or string into a FontFeatures object.

    Args:
        featurefile: File object or string.
        font: Optionally, a TTFont object.
    """
    def __init__(self, featurefile, font=None):

        self.ff = fontFeatures.FontFeatures()
        self.markclasses = {}
        self.currentFeature = None
        self.currentRoutine = None
        self.gensym = 1
        self.glyphmap = ()
        self.currentLanguage = None
        if font:
            self.glyphmap = font.getReverseGlyphMap()
        if isinstance(featurefile, str):
            featurefile = io.StringIO(featurefile)
        self.featurefile = featurefile
        self.parser = Parser(self.featurefile, self.glyphmap)
        self.parser.ast.ValueRecord = fontFeatures.ValueRecord

    def parse(self):
        """Parse the feature code.

        Returns:
            A ``FontFeatures`` object containing the rules of this file.
        """

        # Borrow glyph classes
        for name, members in self.ff.namedClasses.items():
            glyphclass = ast.GlyphClassDefinition(
                name, ast.GlyphClass([m for m in members]))
            self.parser.glyphclasses_.define(name, glyphclass)

        parsetree = self.parser.parse()

        # Return glyph classes
        if len(self.parser.glyphclasses_.scopes_):
            for name, definition in self.parser.glyphclasses_.scopes_[
                    -1].items():
                if isinstance(definition, ast.MarkClass):
                    pass
                    # self.ff.namedClasses[name] = list(definition.glyphs.keys())
                else:
                    self.ff.namedClasses[name] = definition.glyphs.glyphs

        self.features_ = {}
        parsetree.build(self)
        return self.ff

    def _start_routine_if_necessary(self, location):
        if not self.currentRoutine:
            self._start_routine(location, "")

    def _start_routine(self, location, name):
        location = "%s:%i:%i" % (location)
        # print("Starting routine at "+location)
        self._discard_empty_routine()
        self.currentRoutine = fontFeatures.Routine(name=name, address=location)
        if not name:
            self.currentRoutine.name = "unnamed_routine_%i" % self.gensym
            self.gensym = self.gensym + 1
        self.currentRoutineFlag = 0
        if self.currentFeature:
            reference = self.ff.referenceRoutine(self.currentRoutine)
            self.ff.addFeature(self.currentFeature, [reference])
        else:
            self.ff.routines.append(self.currentRoutine)

    def start_lookup_block(self, location, name):
        self._start_routine(location, name)

    def start_feature(self, location, name):
        self.currentFeature = name

    def set_font_revision(self, location, revision):
        pass

    def add_name_record(self, *args):
        pass

    def add_featureName(self, tag):
        # XXX support this
        pass

    def set_script(self, location, script):
        self.currentLanguage = [(script, "*")]

    def set_language(self, location, language, include_default, required):
        self.currentLanguage = [(self.currentLanguage[0][0], language)]

    def add_single_subst(self, location, prefix, suffix, mapping, forceChain):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Substitution(input_=[list(mapping.keys())],
                                      replacement=[list(mapping.values())],
                                      precontext=[[str(g) for g in group]
                                                  for group in prefix],
                                      postcontext=[[str(g) for g in group]
                                                   for group in suffix],
                                      address=location,
                                      languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_reverse_chain_single_subst(self, location, prefix, suffix,
                                       mapping):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Substitution(input_=[list(mapping.keys())],
                                      replacement=[list(mapping.values())],
                                      precontext=[[str(g) for g in group]
                                                  for group in prefix],
                                      postcontext=[[str(g) for g in group]
                                                   for group in suffix],
                                      address=location,
                                      languages=self.currentLanguage,
                                      reverse=True)
        self.currentRoutine.addRule(s)

    def add_multiple_subst(self, location, prefix, glyph, suffix, replacements,
                           forceChain):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Substitution(input_=[[glyph]],
                                      replacement=[[g] for g in replacements],
                                      precontext=[[str(g) for g in group]
                                                  for group in prefix],
                                      postcontext=[[str(g) for g in group]
                                                   for group in suffix],
                                      address=location,
                                      languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_alternate_subst(self, location, prefix, glyph, suffix,
                            replacement):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Substitution(input_=[[glyph]],
                                      replacement=[replacement],
                                      precontext=[[str(g) for g in group]
                                                  for group in prefix],
                                      postcontext=[[str(g) for g in group]
                                                   for group in suffix],
                                      address=location,
                                      languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_ligature_subst(self, location, prefix, glyphs, suffix, replacement,
                           forceChain):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Substitution(input_=[list(x) for x in glyphs],
                                      replacement=[[replacement]],
                                      precontext=[[str(g) for g in group]
                                                  for group in prefix],
                                      postcontext=[[str(g) for g in group]
                                                   for group in suffix],
                                      address=location,
                                      languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_chain_context_subst(self, location, prefix, glyphs, suffix,
                                lookups):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        # Find named feature
        mylookups = []
        for x in lookups:
            if x:
                mylookups.append([self.ff.routineNamed(y.name) for y in x])
            else:
                mylookups.append(None)
        s = fontFeatures.Chaining(input_=[list(x) for x in glyphs],
                                  precontext=[[str(g) for g in group]
                                              for group in prefix],
                                  postcontext=[[str(g) for g in group]
                                               for group in suffix],
                                  lookups=mylookups,
                                  address=location,
                                  languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    add_chain_context_pos = add_chain_context_subst

    def add_single_pos(self, location, prefix, suffix, pos, forceChain):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Positioning(glyphs=[p[0] for p in pos],
                                     valuerecords=[p[1] for p in pos],
                                     precontext=[[str(g) for g in group]
                                                 for group in prefix],
                                     postcontext=[[str(g) for g in group]
                                                  for group in suffix],
                                     address=location,
                                     languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Positioning(glyphs=[[glyph1], [glyph2]],
                                     valuerecords=[value1, value2],
                                     address=location,
                                     languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2,
                           value2):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        s = fontFeatures.Positioning(glyphs=[glyphclass1, glyphclass2],
                                     valuerecords=[value1, value2],
                                     address=location,
                                     languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_cursive_pos(self, location, glyphclass, entryAnchor, exitAnchor):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        basedict, markdict = {}, {}
        if entryAnchor:
            basedict = {g: (entryAnchor.x, entryAnchor.y) for g in glyphclass}
        if exitAnchor:
            markdict = {g: (exitAnchor.x, exitAnchor.y) for g in glyphclass}
        s = fontFeatures.Attachment(base_name="cursive_entry",
                                    mark_name="cursive_exit",
                                    bases=basedict,
                                    marks=markdict,
                                    address=location,
                                    languages=self.currentLanguage)
        self.currentRoutine.addRule(s)

    def add_mark_base_pos(self, location, bases, marks):
        self._start_routine_if_necessary(location)
        location = "%s:%i:%i" % (location)
        for baseanchor, markclass in marks:
            assert len(markclass.definitions) == 1
            markanchor = markclass.definitions[0].anchor
            s = fontFeatures.Attachment(
                base_name=markclass.name,
                mark_name=markclass.name,
                bases={g: (baseanchor.x, baseanchor.y)
                       for g in bases},
                marks={
                    g: (markanchor.x, markanchor.y)
                    for g in markclass.glyphs.keys()
                },
                address=location,
                languages=self.currentLanguage)
            s.fontfeatures = self.ff
        self.currentRoutine.addRule(s)

    add_mark_mark_pos = add_mark_base_pos

    def set_lookup_flag(self, location, value, markAttach, markFilter):
        if self.currentRoutine and value == self.currentRoutineFlag:
            return
        # If we're mid-feature, start a new routine here
        if self.currentFeature:
            self.end_lookup_block()
            self._discard_empty_routine()
            self._start_routine(location, None)
        self.currentRoutineFlag = value

    def add_language_system(self, location, script, language):
        pass

    def add_lookup_call(self, lookup_name):

        routine = self.ff.routineNamed(lookup_name)
        if self.currentFeature:
            self._discard_empty_routine()
            self.ff.addFeature(self.currentFeature, [routine])
        else:
            raise ValueError("Huh?")

    def end_lookup_block(self):
        if self.currentRoutine:
            for rule in self.currentRoutine.rules:
                rule.flags = self.currentRoutineFlag

    def end_feature(self):
        self._discard_empty_routine()
        self.currentFeature = None
        self.currentLanguage = None
        if self.currentRoutine:
            for rule in self.currentRoutine.rules:
                rule.flags = self.currentRoutineFlag
            self.currentRoutine = None

    def _discard_empty_routine(self):
        if not self.currentFeature:
            return
        if self.currentRoutine and not self.currentRoutine.rules:
            if self.currentRoutine not in self.ff.routines:
                # print("%s escaped!" % self.currentRoutine.name)
                return
            del (self.ff.routines[self.ff.routines.index(self.currentRoutine)])
            if self.currentFeature in self.ff.features:
                del (self.ff.features[self.currentFeature][-1])
        pass

    def add_feature_reference(self, location, featurename):
        # XXX
        pass

    def add_glyphClassDef(self, location, base, ligature, mark, component):
        for g in base:
            self.ff.glyphclasses[g] = "base"
        for g in ligature:
            self.ff.glyphclasses[g] = "ligature"
        for g in mark:
            self.ff.glyphclasses[g] = "mark"
        for g in component:
            self.ff.glyphclasses[g] = "component"