예제 #1
0
    def test_insert_comment_outside_block(self, testufo):
        writer = MarkFeatureWriter()
        testufo.features.text = dedent("""\
            #
            # Automatic Code
            #
            """)
        feaFile = parseLayoutFeatures(testufo)

        assert writer.write(testufo, feaFile)

        testufo.features.text = dedent("""\
            #
            # Automatic Code
            #
            markClass acutecomb <anchor 100 200> @MC_top;
            feature mark {
                lookup mark1 {
                    pos base a <anchor 100 200> mark @MC_top;
                } mark1;

            } mark;
            """)
        feaFile = parseLayoutFeatures(testufo)

        assert writer.write(testufo, feaFile)

        # test append mode
        writer = MarkFeatureWriter(mode="append")
        assert writer.write(testufo, feaFile)
예제 #2
0
 def test_include_no_ufo_path(self, FontClass, tmpdir):
     ufo = FontClass()
     ufo.features.text = dedent("""\
         include(test.fea)
         """)
     with pushd(str(tmpdir)):
         with pytest.raises(IncludedFeaNotFound):
             parseLayoutFeatures(ufo)
예제 #3
0
    def test__makeMarkClassDefinitions_non_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
        })
        ufo.features.text = dedent("""\
            markClass cedilla <anchor 200 0> @MC_bottom;
            markClass grave <anchor 100 200> @MC_top;
            """)

        writer = MarkFeatureWriter()
        feaFile = parseLayoutFeatures(ufo)
        writer.setContext(ufo, feaFile)
        markClassDefs = writer._makeMarkClassDefinitions()

        assert len(markClassDefs) == 1
        assert len(feaFile.markClasses) == 3
        assert "MC_bottom" in feaFile.markClasses
        assert "MC_top" in feaFile.markClasses
        assert [str(mcd) for mcd in markClassDefs
                ] == ["markClass cedilla <anchor 100 0> @MC_bottom_1;"]
예제 #4
0
    def test__groupScriptsByTagAndDirection(self, FontClass):
        font = FontClass()
        font.features.text = dedent("""
            languagesystem DFLT dflt;
            languagesystem latn dflt;
            languagesystem latn TRK;
            languagesystem arab dflt;
            languagesystem arab URD;
            languagesystem deva dflt;
            languagesystem dev2 dflt;
            """)

        feaFile = parseLayoutFeatures(font)
        scripts = ast.getScriptLanguageSystems(feaFile)
        scriptGroups = KernFeatureWriter._groupScriptsByTagAndDirection(
            scripts)

        assert "kern" in scriptGroups
        assert list(scriptGroups["kern"]["LTR"]) == [("latn", ["dflt",
                                                               "TRK "])]
        assert list(scriptGroups["kern"]["RTL"]) == [("arab", ["dflt",
                                                               "URD "])]

        assert "dist" in scriptGroups
        assert list(scriptGroups["dist"]["LTR"]) == [
            ("deva", ["dflt"]),
            ("dev2", ["dflt"]),
        ]
예제 #5
0
    def test_getKerningClasses(self, FontClass):
        font = FontClass()
        for i in range(65, 65 + 6):  # A..F
            font.newGlyph(chr(i))
        font.groups.update({
            "public.kern1.A": ["A", "B"],
            "public.kern2.C": ["C", "D"]
        })
        # simulate a name clash between pre-existing class definitions in
        # feature file, and those generated by the feature writer
        font.features.text = "@kern1.A = [E F];"

        writer = KernFeatureWriter()
        feaFile = parseLayoutFeatures(font)
        side1Classes, side2Classes = KernFeatureWriter.getKerningClasses(
            font, feaFile)

        assert "public.kern1.A" in side1Classes
        # the new class gets a unique name
        assert side1Classes["public.kern1.A"].name == "kern1.A_1"
        assert getGlyphs(side1Classes["public.kern1.A"]) == ["A", "B"]

        assert "public.kern2.C" in side2Classes
        assert side2Classes["public.kern2.C"].name == "kern2.C"
        assert getGlyphs(side2Classes["public.kern2.C"]) == ["C", "D"]
예제 #6
0
    def test_mode(self, FontClass):
        ufo = FontClass()
        for name in ("one", "four", "six", "seven"):
            ufo.newGlyph(name)
        existing = dedent(
            """\
            feature kern {
                pos one four' -50 six;
            } kern;
            """
        )
        ufo.features.text = existing
        ufo.kerning.update({("seven", "six"): 25.0})

        writer = KernFeatureWriter()  # default mode="skip"
        feaFile = parseLayoutFeatures(ufo)
        assert not writer.write(ufo, feaFile)

        assert str(feaFile) == existing

        # pass optional "append" mode
        writer = KernFeatureWriter(mode="append")
        feaFile = parseLayoutFeatures(ufo)
        assert writer.write(ufo, feaFile)

        expected = existing + dedent(
            """

            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos seven six 25;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """
        )
        assert str(feaFile) == expected

        # pass "skip" mode explicitly
        writer = KernFeatureWriter(mode="skip")
        feaFile = parseLayoutFeatures(ufo)
        assert not writer.write(ufo, feaFile)

        assert str(feaFile) == existing
    def test_insert_comment_middle(self, testufo):
        writer = MarkFeatureWriter()
        testufo.features.text = dedent("""\
            markClass acutecomb <anchor 100 200> @MC_top;
            feature mark {
                lookup mark1 {
                    pos base a
                        <anchor 100 200> mark @MC_top;
                } mark1;
                #
                # Automatic Code
                #
                lookup mark2 {
                    pos base a
                        <anchor 150 250> mark @MC_top;
                } mark2;
            } mark;
            """)
        feaFile = parseLayoutFeatures(testufo)

        with pytest.raises(
                InvalidFeaturesData,
                match="Insert marker has rules before and after, feature mark "
                "cannot be inserted.",
        ):
            writer.write(testufo, feaFile)

        # test append mode ignores insert marker
        generated = self.writeFeatures(testufo, mode="append")
        assert str(generated) == dedent("""\
            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;
            """)
예제 #8
0
    def test_defs_and_lookups_first(self, testufo):
        testufo.newGlyph("circumflexcomb")
        writer = MarkFeatureWriter()
        testufo.features.text = dedent("""\
            feature mkmk {
                # Automatic Code
                # Move acutecomb down and right if preceded by circumflexcomb
                lookup move_acutecomb {
                    lookupflag UseMarkFilteringSet [acutecomb circumflexcomb];
                    pos circumflexcomb acutecomb' <0 20 0 20>;
                } move_acutecomb;
            } mkmk;
            """)
        feaFile = parseLayoutFeatures(testufo)

        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;

            feature mkmk {
                # Move acutecomb down and right if preceded by circumflexcomb
                lookup move_acutecomb {
                    lookupflag UseMarkFilteringSet [acutecomb circumflexcomb];
                    pos circumflexcomb acutecomb' <0 20 0 20>;
                } move_acutecomb;

            } mkmk;
            """)
예제 #9
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
예제 #10
0
    def test_include_not_found(self, FontClass, tmpdir, caplog):
        caplog.set_level(logging.ERROR)

        tmpdir.join("test.fea").write_text(
            dedent("""\
            # hello world
            """),
            encoding="utf-8",
        )
        ufo = FontClass()
        ufo.features.text = dedent("""\
            include(../test.fea)
            """)
        ufo.save(str(tmpdir.join("Test.ufo")))

        with caplog.at_level(logging.WARNING, logger=logger.name):
            with pytest.raises(IncludedFeaNotFound):
                parseLayoutFeatures(ufo)

        assert len(caplog.records) == 1
        assert "change the file name in the include" in caplog.text
예제 #11
0
 def bases(self):
     "Collect the base glyphs that need to have matching I matras."
     fonts = self.fonts
     bases = set()
     for font in fonts:
         fea = parseLayoutFeatures(font)
         for statement in fea.statements:
             if (hasattr(statement, "glyphs")
                     and hasattr(statement, "name")
                     and statement.name == "BASES_ALIVE"):
                 bases |= set(statement.glyphs.glyphs)
         if bases: break
     return bases
예제 #12
0
    def test_insert_comment_after(self, FontClass):
        ufo = FontClass()
        for name in ("one", "four", "six", "seven"):
            ufo.newGlyph(name)
        existing = dedent("""\
            feature kern {
                pos one four' -50 six;
                #
                # Automatic Code
                #
            } kern;
            """)
        ufo.features.text = existing
        ufo.kerning.update({("seven", "six"): 25.0})

        writer = KernFeatureWriter()
        feaFile = parseLayoutFeatures(ufo)
        assert writer.write(ufo, feaFile)

        expected = dedent("""\
            feature kern {
                pos one four' -50 six;
                #
                #
            } kern;


            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos seven six 25;
            } kern_ltr;

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

        assert str(feaFile) == expected

        # test append mode ignores insert marker
        generated = self.writeFeatures(ufo, mode="append")
        assert str(generated) == dedent("""
            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos seven six 25;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """)
예제 #13
0
    def test_include(self, FontClass, tmpdir):
        tmpdir.join("test.fea").write_text(
            dedent("""\
            # hello world
            """),
            encoding="utf-8",
        )
        ufo = FontClass()
        ufo.features.text = dedent("""\
            include(test.fea)
            """)
        ufo.save(str(tmpdir.join("Test.ufo")))

        fea = parseLayoutFeatures(ufo)

        assert "# hello world" in str(fea)
예제 #14
0
    def test_cleanup_missing_glyphs(self, FontClass):
        groups = {
            "public.kern1.A": ["A", "Aacute", "Abreve", "Acircumflex"],
            "public.kern2.B": ["B", "D", "E", "F"],
            "public.kern1.C": ["foobar"],
        }
        kerning = {
            ("public.kern1.A", "public.kern2.B"): 10,
            ("public.kern1.A", "baz"): -25,
            ("baz", "public.kern2.B"): -20,
            ("public.kern1.C", "public.kern2.B"): 20,
        }
        ufo = FontClass()
        exclude = {"Abreve", "D", "foobar"}
        for glyphs in groups.values():
            for glyph in glyphs:
                if glyph in exclude:
                    continue
                ufo.newGlyph(glyph)
        ufo.groups.update(groups)
        ufo.kerning.update(kerning)

        writer = KernFeatureWriter()
        feaFile = parseLayoutFeatures(ufo)
        writer.write(ufo, feaFile)

        classDefs = getClassDefs(feaFile)
        assert len(classDefs) == 2
        assert classDefs[0].name == "kern1.A"
        assert classDefs[1].name == "kern2.B"
        assert getGlyphs(classDefs[0]) == ["A", "Aacute", "Acircumflex"]
        assert getGlyphs(classDefs[1]) == ["B", "E", "F"]

        lookups = getLookups(feaFile)
        assert len(lookups) == 1
        kern_ltr = lookups[0]
        assert kern_ltr.name == "kern_ltr"
        rules = getPairPosRules(kern_ltr)
        assert len(rules) == 1
        assert str(rules[0]) == "pos @kern1.A @kern2.B 10;"
예제 #15
0
    def test_insert_comment_middle(self, FontClass):
        ufo = FontClass()
        for name in ("one", "four", "six", "seven"):
            ufo.newGlyph(name)
        existing = dedent("""\
            feature kern {
                pos one four' -50 six;
                #
                # Automatic Code
                #
                pos one six' -50 six;
            } kern;
            """)
        ufo.features.text = existing
        ufo.kerning.update({("seven", "six"): 25.0})

        writer = KernFeatureWriter()
        feaFile = parseLayoutFeatures(ufo)

        with pytest.raises(
                InvalidFeaturesData,
                match="Insert marker has rules before and after, feature kern "
                "cannot be inserted.",
        ):
            writer.write(ufo, feaFile)

        # test append mode ignores insert marker
        generated = self.writeFeatures(ufo, mode="append")
        assert str(generated) == dedent("""
            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos seven six 25;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;
            """)
예제 #16
0
    def test_insert_comment_before_extended(self, FontClass):
        ufo = FontClass()
        for name in ("one", "four", "six", "seven"):
            ufo.newGlyph(name)
        existing = dedent("""\
            feature kern {
                #
                # Automatic Code End
                #
                pos one four' -50 six;
            } kern;
            """)
        ufo.features.text = existing
        ufo.kerning.update({("seven", "six"): 25.0})

        writer = KernFeatureWriter()
        feaFile = parseLayoutFeatures(ufo)
        assert writer.write(ufo, feaFile)

        expected = dedent("""\
            lookup kern_ltr {
                lookupflag IgnoreMarks;
                pos seven six 25;
            } kern_ltr;

            feature kern {
                lookup kern_ltr;
            } kern;

            feature kern {
                #
                #
                pos one four' -50 six;
            } kern;
            """)

        assert str(feaFile).strip() == expected.strip()
예제 #17
0
    def test_insert_comment_after(self, testufo):
        writer = MarkFeatureWriter()
        testufo.features.text = dedent("""\
            markClass acutecomb <anchor 100 200> @MC_top;
            feature mark {
                lookup mark1 {
                    pos base a <anchor 100 200> mark @MC_top;
                } mark1;
                #
                # Automatic Code
                #
            } mark;
            """)
        feaFile = parseLayoutFeatures(testufo)

        assert writer.write(testufo, feaFile)

        assert str(feaFile) == dedent("""\
            markClass acutecomb <anchor 100 200> @MC_top;
            feature mark {
                lookup mark1 {
                    pos base a <anchor 100 200> mark @MC_top;
                } mark1;

                #
                #
            } mark;

            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;
            """)

        # test append mode ignores insert marker
        generated = self.writeFeatures(testufo, mode="append")
        assert str(generated) == dedent("""\
            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;
            """)
예제 #18
0
    def interpolate_instance_ufos_mutatormath(
        self,
        designspace,
        include=None,
        round_instances=False,
        expand_features_to_instances=False,
    ):
        """Interpolate master UFOs with MutatorMath and return instance UFOs.

        Args:
            designspace: a DesignSpaceDocument object containing sources and
                instances.
            include (str): optional regular expression pattern to match the
                DS instance 'name' attribute and only interpolate the matching
                instances.
            round_instances (bool): round instances' coordinates to integer.
            expand_features_to_instances: parses the master feature file, expands all
                include()s and writes the resulting full feature file to all instance
                UFOs. Use this if you share feature files among masters in external
                files. Otherwise, the relative include paths can break as instances
                may end up elsewhere. Only done on interpolation.
        Returns:
            list of defcon.Font objects corresponding to the UFO instances.
        Raises:
            FontmakeError: if any of the sources defines a custom 'layer', for
                this is not supported by MutatorMath.
            ValueError: "expand_features_to_instances" is True but no source in the
                designspace document is designated with '<features copy="1"/>'.
        """
        from glyphsLib.interpolation import apply_instance_data
        from mutatorMath.ufo.document import DesignSpaceDocumentReader

        if any(source.layerName is not None for source in designspace.sources):
            raise FontmakeError(
                "MutatorMath doesn't support DesignSpace sources with 'layer' "
                "attribute")

        with temporarily_disabling_axis_maps(
                designspace.path) as temp_designspace_path:
            builder = DesignSpaceDocumentReader(
                temp_designspace_path,
                ufoVersion=3,
                roundGeometry=round_instances,
                verbose=True,
            )
            logger.info("Interpolating master UFOs from designspace")
            if include is not None:
                instances = self._search_instances(designspace,
                                                   pattern=include)
                for instance_name in instances:
                    builder.readInstance(("name", instance_name))
                filenames = set(instances.values())
            else:
                builder.readInstances()
                filenames = None  # will include all instances

        logger.info("Applying instance data from designspace")
        instance_ufos = apply_instance_data(designspace,
                                            include_filenames=filenames)

        if expand_features_to_instances:
            logger.debug("Expanding features to instance UFOs")
            master_source = next(
                (s for s in designspace.sources if s.copyFeatures), None)
            if not master_source:
                raise ValueError(
                    "No source is designated as the master for features.")
            else:
                master_source_font = builder.sources[master_source.name][0]
                master_source_features = parseLayoutFeatures(
                    master_source_font).asFea()
                for instance_ufo in instance_ufos:
                    instance_ufo.features.text = master_source_features
                    instance_ufo.save()

        return instance_ufos
예제 #19
0
    def interpolate_instance_ufos(
        self,
        designspace,
        include=None,
        round_instances=False,
        expand_features_to_instances=False,
    ):
        """Interpolate master UFOs with Instantiator and return instance UFOs.

        Args:
            designspace: a DesignSpaceDocument object containing sources and
                instances.
            include (str): optional regular expression pattern to match the
                DS instance 'name' attribute and only interpolate the matching
                instances.
            round_instances (bool): round instances' coordinates to integer.
            expand_features_to_instances: parses the master feature file, expands all
                include()s and writes the resulting full feature file to all instance
                UFOs. Use this if you share feature files among masters in external
                files. Otherwise, the relative include paths can break as instances
                may end up elsewhere. Only done on interpolation.
        Returns:
            generator of ufoLib2.Font objects corresponding to the UFO instances.
        Raises:
            ValueError: The Designspace has no default source or contains incompatible
                glyphs or contains anisotropic (MutatorMath) locations or contains a
                <rules> substitution for a non-existant glyph.
        """
        from glyphsLib.interpolation import apply_instance_data_to_ufo

        logger.info("Interpolating master UFOs from designspace")
        designspace = designspaceLib.DesignSpaceDocument.fromfile(
            designspace.path)
        generator = instantiator.Instantiator.from_designspace(
            designspace, round_geometry=round_instances)

        if expand_features_to_instances:
            logger.debug("Expanding features to instance UFOs")
            fea_txt = parseLayoutFeatures(designspace.default.font).asFea()
            generator = attr.evolve(generator, copy_feature_text=fea_txt)

        for instance in designspace.instances:
            # Skip instances that have been set to non-export in Glyphs, stored as the
            # instance's `com.schriftgestaltung.export` lib key.
            if not instance.lib.get("com.schriftgestaltung.export", True):
                continue

            # Skip instances that do not match the user's inclusion regex if given.
            if include is not None and not fullmatch(include, instance.name):
                continue

            logger.info(f'Generating instance UFO for "{instance.name}"')

            instance.font = generator.generate_instance(instance)

            apply_instance_data_to_ufo(instance.font, instance, designspace)

            # TODO: Making filenames up on the spot is complicated, ideally don't save
            # anything if filename is not set, but make something up when "ufo" is in
            # output formats, but also consider output_path.
            if instance.filename is None:
                raise ValueError(
                    "It is currently required that instances have filenames set."
                )
            ufo_path = os.path.join(os.path.dirname(designspace.path),
                                    instance.filename)
            os.makedirs(os.path.dirname(ufo_path), exist_ok=True)
            instance.font.save(ufo_path, overwrite=True)

            yield instance.font
예제 #20
0
 def writeGDEF(cls, ufo, **kwargs):
     writer = cls.FeatureWriter(**kwargs)
     feaFile = parseLayoutFeatures(ufo)
     if writer.write(ufo, feaFile):
         return feaFile