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