Esempio n. 1
0
    def test_mark_mkmk_features(self, testufo):
        writer = MarkFeatureWriter()  # by default both mark + mkmk are built
        feaFile = ast.FeatureFile()
        assert writer.write(testufo, feaFile)

        assert str(feaFile) == dedent("""\
            markClass acutecomb <anchor 100 200> @MC_top;
            markClass tildecomb <anchor 100 200> @MC_top;

            feature mark {
                lookup mark2base {
                    pos base a <anchor 100 200> mark @MC_top;
                } mark2base;

                lookup mark2liga {
                    pos ligature f_i <anchor 100 500> mark @MC_top
                        ligComponent <anchor 600 500> mark @MC_top;
                } mark2liga;

            } mark;

            feature mkmk {
                lookup mark2mark_top {
                    @MFS_mark2mark_top = [acutecomb tildecomb];
                    lookupflag UseMarkFilteringSet @MFS_mark2mark_top;
                    pos mark tildecomb <anchor 100 300> mark @MC_top;
                } mark2mark_top;

            } mkmk;
            """)
Esempio n. 2
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
Esempio n. 3
0
    def test_write_only_one(self, testufo):
        writer = MarkFeatureWriter(features=["mkmk"])  # only builds "mkmk"
        feaFile = ast.FeatureFile()
        assert writer.write(testufo, feaFile)
        fea = str(feaFile)

        assert "feature mark" not in fea
        assert "feature mkmk" in fea

        writer = MarkFeatureWriter(features=["mark"])  # only builds "mark"
        feaFile = ast.FeatureFile()
        assert writer.write(testufo, feaFile)
        fea = str(feaFile)

        assert "feature mark" in fea
        assert "feature mkmk" not in fea
Esempio n. 4
0
    def test_warn_duplicate_anchor_names(self, FontClass, caplog):
        caplog.set_level(logging.ERROR)

        ufo = FontClass()
        ufo.newGlyph("a").anchors = [
            {
                "name": "top",
                "x": 100,
                "y": 200
            },
            {
                "name": "top",
                "x": 200,
                "y": 300
            },
        ]

        writer = MarkFeatureWriter()
        feaFile = ast.FeatureFile()

        logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter"
        with caplog.at_level(logging.WARNING, logger=logger):
            writer.setContext(ufo, feaFile)

        assert len(caplog.records) == 1
        assert "duplicate anchor 'top' in glyph 'a'" in caplog.text
Esempio n. 5
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
Esempio n. 6
0
 def writeFeatures(cls, ufo, **kwargs):
     """ Return a new FeatureFile object containing only the newly
     generated statements, or None if no new feature was generated.
     """
     writer = cls.FeatureWriter(**kwargs)
     feaFile = parseLayoutFeatures(ufo)
     n = len(feaFile.statements)
     if writer.write(ufo, feaFile):
         new = ast.FeatureFile()
         new.statements = feaFile.statements[n:]
         return new
Esempio n. 7
0
    def test_ignoreMarks(self, FontClass):
        font = FontClass()
        for name in ("one", "four", "six"):
            font.newGlyph(name)
        font.kerning.update({("four", "six"): -55.0, ("one", "six"): -30.0})
        # default is ignoreMarks=True
        writer = KernFeatureWriter()
        feaFile = ast.FeatureFile()
        assert writer.write(font, feaFile)

        assert str(feaFile) == dedent(
            """\
            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos four six -55;
                pos one six -30;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """
        )

        writer = KernFeatureWriter(ignoreMarks=False)
        feaFile = ast.FeatureFile()
        assert writer.write(font, feaFile)

        assert str(feaFile) == dedent(
            """\
            lookup kern_ltr {
                pos four six -55;
                pos one six -30;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """
        )
Esempio n. 8
0
    def test_skip_unnamed_anchors(self, FontClass, caplog):
        caplog.set_level(logging.ERROR)

        ufo = FontClass()
        ufo.newGlyph("a").appendAnchor({"x": 100, "y": 200})

        writer = MarkFeatureWriter()
        feaFile = ast.FeatureFile()

        logger = "ufo2ft.featureWriters.markFeatureWriter.MarkFeatureWriter"
        with caplog.at_level(logging.WARNING, logger=logger):
            writer.setContext(ufo, feaFile)

        assert len(caplog.records) == 1
        assert "unnamed anchor discarded in glyph 'a'" in caplog.text
Esempio n. 9
0
    def test_quantize(self, FontClass):
        font = FontClass()
        for name in ("one", "four", "six"):
            font.newGlyph(name)
        font.kerning.update({("four", "six"): -57.0, ("one", "six"): -24.0})
        writer = KernFeatureWriter(quantization=5)
        feaFile = ast.FeatureFile()
        assert writer.write(font, feaFile)

        assert str(feaFile) == dedent("""\
            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos four six -55;
                pos one six -25;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """)
Esempio n. 10
0
    def test__makeMarkClassDefinitions_empty(self, FontClass):
        ufo = FontClass()
        ufo.newGlyph("a").appendAnchor({"name": "top", "x": 250, "y": 500})
        ufo.newGlyph("c").appendAnchor({"name": "bottom", "x": 250, "y": -100})
        ufo.newGlyph("grave").appendAnchor({
            "name": "_top",
            "x": 100,
            "y": 200
        })
        ufo.newGlyph("cedilla").appendAnchor({
            "name": "_bottom",
            "x": 100,
            "y": 0
        })
        writer = MarkFeatureWriter()
        feaFile = ast.FeatureFile()
        writer.setContext(ufo, feaFile)
        markClassDefs = writer._makeMarkClassDefinitions()

        assert len(feaFile.markClasses) == 2
        assert [str(mcd) for mcd in markClassDefs] == [
            "markClass cedilla <anchor 100 0> @MC_bottom;",
            "markClass grave <anchor 100 200> @MC_top;",
        ]
Esempio n. 11
0
    def test_predefined_anchor_lists(self, FontClass):
        """ Roboto uses some weird anchor naming scheme, see:
        https://github.com/google/roboto/blob/
            5700de83856781fa0c097a349e46dbaae5792cb0/
            scripts/lib/fontbuild/markFeature.py#L41-L47
        """
        class RobotoMarkFeatureWriter(MarkFeatureWriter):
            class NamedAnchor(NamedAnchor):
                markPrefix = "_mark"
                ignoreRE = "(^mkmk|_acc$)"

        ufo = FontClass()
        a = ufo.newGlyph("a")
        a.anchors = [
            {
                "name": "top",
                "x": 250,
                "y": 600
            },
            {
                "name": "bottom",
                "x": 250,
                "y": -100
            },
        ]
        f_i = ufo.newGlyph("f_i")
        f_i.anchors = [
            {
                "name": "top_1",
                "x": 200,
                "y": 700
            },
            {
                "name": "top_2",
                "x": 500,
                "y": 700
            },
        ]
        gravecomb = ufo.newGlyph("gravecomb")
        gravecomb.anchors = [
            {
                "name": "_marktop",
                "x": 160,
                "y": 780
            },
            {
                "name": "mkmktop",
                "x": 150,
                "y": 800
            },
            {
                "name": "mkmkbottom_acc",
                "x": 150,
                "y": 600
            },
        ]
        ufo.newGlyph("cedillacomb").appendAnchor({
            "name": "_markbottom",
            "x": 200,
            "y": 0
        })
        ufo.newGlyph("ogonekcomb").appendAnchor({
            "name": "_bottom",
            "x": 180,
            "y": -10
        })

        writer = RobotoMarkFeatureWriter()
        feaFile = ast.FeatureFile()
        writer.write(ufo, feaFile)

        assert str(feaFile) == dedent("""\
            markClass cedillacomb <anchor 200 0> @MC_markbottom;
            markClass gravecomb <anchor 160 780> @MC_marktop;

            feature mark {
                lookup mark2base {
                    pos base a <anchor 250 -100> mark @MC_markbottom <anchor 250 600> mark @MC_marktop;
                } mark2base;

                lookup mark2liga {
                    pos ligature f_i <anchor 200 700> mark @MC_marktop
                        ligComponent <anchor 500 700> mark @MC_marktop;
                } mark2liga;

            } mark;

            feature mkmk {
                lookup mark2mark_bottom {
                    @MFS_mark2mark_bottom = [cedillacomb gravecomb];
                    lookupflag UseMarkFilteringSet @MFS_mark2mark_bottom;
                    pos mark gravecomb <anchor 150 600> mark @MC_markbottom;
                } mark2mark_bottom;

                lookup mark2mark_top {
                    @MFS_mark2mark_top = [gravecomb];
                    lookupflag UseMarkFilteringSet @MFS_mark2mark_top;
                    pos mark gravecomb <anchor 150 800> mark @MC_marktop;
                } mark2mark_top;

            } mkmk;
            """)