def arrange_by_language(self): from fontFeatures import Routine if not self.languages: return languages = {} def add_lang(p, r): nonlocal languages if not p in languages: languages[p] = [] languages[p].extend(r) for s, l in self.languages: if l == "*": add_lang((s, "dflt"), self.rules) else: add_lang((s, l), self.rules) if len(languages.keys()) < 2: return routines = [] for k, v in languages.items(): r = Routine(rules=v, languages=[k], flags=self.flags) if self.name: r.name = self.name + "_" + k[0] + "_" + k[1] routines.append(r) return routines
def test_add(): r1 = Routine(name="One") r2 = Routine(name="Two") f1 = FontFeatures() f2 = FontFeatures() f1.namedClasses["One"] = ["A", "B", "C"] f1.glyphclasses["a"] = "base" f1.addFeature("onex", [r1]) f2.namedClasses["Two"] = ["d", "e", "f"] f2.addFeature("twox", [r2]) f2.glyphclasses["d"] = "mark" f2.anchors["a"] = {"top": (300, 200)} combined = f1 + f2 assert combined.namedClasses["One"] == ["A", "B", "C"] assert combined.namedClasses["Two"] == ["d", "e", "f"] assert "onex" in combined.features assert combined.features["onex"][0].routine == r1 assert "twox" in combined.features assert combined.features["twox"][0].routine == r2 assert combined.glyphclasses["a"] == "base" assert combined.glyphclasses["d"] == "mark" el = combined.toXML() assert etree.tostring(el).decode() == expected
def arrange_by_language(self): from fontFeatures import Routine languages = {} def add_lang(p, r): nonlocal languages if not p in languages: languages[p] = [] languages[p].append(r) for r in self.rules: if not r.languages: add_lang(("DFLT", "dflt"), r) else: for lang in r.languages: newrule = copy.deepcopy(r) newrule.languages = [lang] add_lang(lang, newrule) if len(languages.keys()) < 2: return routines = [] for k, v in languages.items(): r = Routine(rules=v, languages=[k], flags=self.flags) if self.name: r.name = self.name + "_" + k[0] + "_" + k[1] routines.append(r) return routines
def test_MergeMultipleSingleSubstitutions_1(self): r1 = Routine(rules=[ Substitution([["a", "y"]], [["b", "z"]]), Substitution([["b", "d", "a"]], [["c", "e", "f"]]), ]) Optimizer(FontFeatures()).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub [a y b d] by [c z c e];\n")
def makeBuffer(self, before_after="before"): buf = VariationAwareBuffer( self.project.font, direction=self.buffer_direction, ) if self.project.variations: buf.location = self.location buf.vf = self.project.variations buf.items = [ VariationAwareBufferItem.new_glyph(g, self.project.font, buf) for g in self.representative_string ] shaper = Shaper(self.project.fontfeatures, self.project.font) shaper.execute(buf, features=self.makeShaperFeatureArray()) routine = Routine(rules=[self.rule]) # print("Before shaping: ", buf.serialize()) if before_after == "after" and self.rule: print("Before application: ", buf.serialize()) print(self.rule.asFea()) buf.clear_mask() # XXX try: routine.apply_to_buffer(buf) except Exception as e: print("Couldn't shape: " + str(e)) print("After application: ", buf.serialize()) return buf
def test_MergeMultipleSingleSubstitutions_2(self): r1 = Routine(rules=[ Substitution([["a"]], [["b"]]), Substitution([["b"]], [["c"]]), Substitution([["d"]], [["e"]]), Substitution([["y"]], [["z"]]), ]) Optimizer(FontFeatures()).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub [a b d y] by [c c e z];\n")
def test_GlyphClasses(self): r1 = Routine(rules=[ Substitution([["a", "b", "c", "d", "e", "f", "g", "h"]], [["z"]]), ]) ff = FontFeatures() Optimizer(ff).optimize_routine(r1, level=1) self.assertEqual(r1.asFea(), " sub @class1 by z;\n") self.assertEqual(ff.namedClasses["class1"], ("a", "b", "c", "d", "e", "f", "g", "h"))
def test_complex(self): pos1 = Substitution(["a"], ["b"]) pos2 = Substitution(["b"], ["c"]) r1 = Routine(rules=[pos1], name="dummy1") r2 = Routine(rules=[pos2], name="dummy2") rr1 = RoutineReference(routine=r1) rr2 = RoutineReference(routine=r2) c = Chaining([["a"], ["b"]], lookups=[[rr1, rr2], None]) self.assertEqual(c.asFea(), "sub a' lookup dummy1 lookup dummy2 b';")
def test_routine_partition_not_needed(): f = FontFeatures() s1 = Substitution([["A"]], [["A.grk"]], languages=["grek/*"]) s2 = Substitution([["A"]], [["A.esp"]], languages=["grek/*"]) r = Routine(rules=[s1, s2], flags=0x2) f.routines.append(r) f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 1 r.rules = [] f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 1
def roundTrip(self, thing, dependencies): f = FontFeatures() f.routines.extend(dependencies) rt = thing.__class__.fromXML(thing.toXML()) f.routines.append(Routine(rules=[rt])) f.resolveAllRoutines() self.assertEqual(rt.asFea(), thing.asFea())
def xmlToFontFeatures(self): routines = {} warnings = [] for xmlroutine in self.xml.find("routines"): if "computed" in xmlroutine.attrib: r = ComputedRoutine.fromXML(xmlroutine) r.project = self elif "divider" in xmlroutine.attrib: r = DividerRoutine.fromXML(xmlroutine) else: r = Routine.fromXML(xmlroutine) routines[r.name] = r self.fontfeatures.routines.append(r) for xmlfeature in self.xml.find("features"): # Temporary until we refactor fontfeatures featurename = xmlfeature.get("name") self.fontfeatures.features[featurename] = [] for r in xmlfeature: routinename = r.get("name") if routinename in routines: self.fontfeatures.addFeature(featurename, [routines[routinename]]) else: warnings.append( "Lost routine %s referenced in feature %s" % (routinename, featurename)) return warnings # We don't do anything with them yet
def fromXML(klass, el): """Creates a FontFeatures object from a lxml Element object.""" f = klass() from fontFeatures import RoutineReference, Routine for part in el: if part.tag == "namedclasses": for cl_el in part: f.namedClasses[cl_el.get("name")] = [g.text for g in cl_el] elif part.tag == "routines": f.routines = [Routine.fromXML(r) for r in part] elif part.tag == "features": for feat in part: f.features[feat.get("name")] = [ RoutineReference.fromXML(x) for x in feat ] elif part.tag == "anchors": for glyph in part: f.anchors[glyph.get("name")] = { el.get("name"): (int(el.get("x")), int(el.get("y"))) for el in glyph } elif part.tag == "glyphclasses": for cl_el in part: f.glyphclasses[cl_el.get("name")] = cl_el.get("class") for refs in f.features.values(): for ref in refs: ref.resolve(f) return f
def test_simple_sub(self): pos = Substitution(["a"], ["b"]) r = Routine(rules=[pos], name="dummy") rr = RoutineReference(routine=r) c = Chaining([["a"], ["b"]], lookups=[[rr], None]) self.assertEqual(c.asFea(), "sub a' lookup dummy b';")
def test_multiple_languages_routine(): f = FontFeatures() s1 = Substitution([["a"]], ["b"]) expected = """languagesystem arab URD; languagesystem arab FAR; lookup Routine_1 { lookupflag 0; ; sub a by b; } Routine_1; feature locl { script arab; language URD; lookup Routine_1; } locl; feature locl { script arab; language FAR; lookup Routine_1; } locl; """ f = FontFeatures() r1 = Routine(rules=[s1], languages=[("arab", "URD "), ("arab", "FAR ")]) f.addFeature("locl", [r1]) assert f.asFea(do_gdef=False) == expected
def insertRows(self, position, rows=1, parent=QModelIndex()): """ Insert a row into the model. """ assert(rows == 1) self.beginInsertRows(QModelIndex(), position, position + rows - 1) self.project.fontfeatures.routines.insert(position,Routine(name="", rules=[])) self.endInsertRows() return True
def arrange_by_type(self): from fontFeatures import Routine # Arrange into rules of similar type (Substitution/Positioning) ruleTypes = {} for r in self.rules: if not type(r) in ruleTypes: ruleTypes[type(r)] = [] ruleTypes[type(r)].append(r) if len(ruleTypes.keys()) == 1: return routines = [] for k, v in ruleTypes.items(): r = Routine(rules=v, flags=self.flags) if self.name: r.name = self.name + "_" + str(k) return routines
def test_complex_pos(self): v = ValueRecord(xAdvance=120) pos1 = Positioning(["a"], [v]) r1 = Routine(rules=[pos1]) rr1 = RoutineReference(routine=r1) c = Chaining([["a"], ["b"]], lookups=[[rr1], None]) c.feaPreamble(FontFeatures()) self.assertEqual(c.asFea(), "pos a' lookup ChainedRoutine1 b';")
def wrap_and_flush(): nonlocal oddStatements if len(oddStatements) > 0: self.parser.fontfeatures.addFeature(featurename, [ Routine(rules=[ r for r in oddStatements if isinstance(r, Rule) ]) ]) oddStatements = []
def test_featurelist(qtmodeltester): proj = FluxProject() proj.fontfeatures = FontFeatures() r1 = Routine(name="routine1") r2 = Routine(name="routine2") proj.fontfeatures.features["test"] = [r1, r2] proj = FluxProject("Hind.fluxml") model = FeatureListModel(proj) root = model.index(-1, -1) assert (model.describeIndex(root) == "root of tree") feature1 = model.index(0, 0) assert (model.describeIndex(feature1) == "feature at row 0") child1 = model.index(0, 0, feature1) assert (child1.parent() == feature1) assert (model.index(0, 0, feature1) == model.index(0, 0, feature1)) # import code; code.interact(local=locals()) qtmodeltester.check(model, force_py=True) qtmodeltester.check(model)
def test_simple_pos(self): v = ValueRecord(xAdvance=120) pos = Positioning(["a"], [v]) r = Routine(rules=[pos], name="dummy") rr = RoutineReference(routine=r) c = Chaining([["a"], ["b"]], lookups=[[rr], None]) self.assertEqual(c.asFea(), "pos a' lookup dummy b';") self.assertEqual( etree.tostring(c.toXML()), '<chaining><lookups><slot><routinereference name="dummy"/></slot><slot><lookup/></slot></lookups><input><slot><glyph>a</glyph></slot><slot><glyph>b</glyph></slot></input></chaining>' .encode("utf-8"))
def test_namedclass_in_slot(): font = Babelfont.load("tests/data/LibertinusSans-Regular.otf") buf = Buffer(font, glyphs=["A", "B", "C"]) r = Routine() r.addRule(Substitution([["G", "@AB"]], [["X"]])) r.apply_to_buffer(buf, namedclasses={"AB": ["A", "B"]}) assert buf.serialize(position=False) == "X|X|C"
def arrange_by_flags(self): from fontFeatures import Routine flagTypes = {} for r in self.rules: if not r.flags: r.flags = 0 if not r.flags in flagTypes: flagTypes[r.flags] = [] flagTypes[r.flags].append(r) if len(flagTypes.keys()) == 1: if not self.flags: self.flags = 0 self.flags = self.flags | list(flagTypes.keys())[0] return routines = [] for k, v in flagTypes.items(): r = Routine(rules=v, flags=k) if self.name: r.name = self.name + "_" + str(k) routines.append(r) return routines
def arrange_by_lookup_type(self): from fontFeatures import Routine ruleTypes = {} for r in self.rules: if not lookup_type(r) in ruleTypes: ruleTypes[lookup_type(r)] = [] ruleTypes[lookup_type(r)].append(r) if len(ruleTypes.keys()) == 1: return # Special case the fact that a single sub can be expressed as part of a # multiple sub if needed if tuple(sorted(ruleTypes.keys())) == (1, 2) or tuple( sorted(ruleTypes.keys())) == (1, 8): return routines = [] for k, v in ruleTypes.items(): r = Routine(rules=v, flags=self.flags) if self.name: r.name = self.name + "_" + str(k) routines.append(r) return routines
def test_multiple_applications(): font = Babelfont.load("tests/data/LibertinusSans-Regular.otf") buf = Buffer(font, glyphs=["A", "B", "C"]) r = Routine() r.addRule(Substitution([["A"]], [["X"]])) r.addRule(Substitution([["B"]], [["Y"]])) r.apply_to_buffer(buf) assert buf.serialize(position=False) == "X|Y|C"
def test_routine_partition(): f = FontFeatures() s1 = Substitution([["A"]], [["A.grk"]], languages=["grek/*"]) s2 = Substitution([["A"]], [["A.esp"]], languages=["latn/ESP "]) r = Routine(rules=[s1, s2], flags=0x2) f.routines.append(r) dummy = Routine(rules=[Substitution([["G"]], [["G"]])]) f.routines.append(dummy) c = Chaining( [["A"], ["V"]], lookups=[ [RoutineReference(routine=dummy), RoutineReference(routine=r)], [RoutineReference(routine=r), RoutineReference(routine=dummy)], ]) r2 = Routine(rules=[c]) f.routines.append(r2) f.addFeature("locl", [r]) f.partitionRoutine(r, lambda rule: tuple(rule.languages or [])) assert len(f.routines) == 4 assert f.routines[0].flags == f.routines[1].flags assert len(f.routines[0].rules) == 1 assert len(f.routines[1].rules) == 1 assert f.routines[0].rules[0].replacement[0][0] == "A.grk" assert f.routines[1].rules[0].replacement[0][0] == "A.esp" assert len(c.lookups[0]) == 3 assert len(f.features["locl"]) == 2
def test_script_language_split(): f = FontFeatures() s1 = Substitution([["question"]], [["questiongreek"]], languages=[("grek", "*")]) s2 = Substitution([["A"]], [["alpha"]], languages=[("grek", "ELL ")]) s3 = Substitution([["question"]], [["questiondown"]], languages=[("latn", "ESP ")]) s4 = Substitution([["question"]], [["B"]], languages=[("latn", "POL ")]) s5 = Substitution([["X"]], [["Y"]]) r = Routine(rules=[s1, s2, s3, s4, s5]) f.addFeature("locl", [r]) f.buildBinaryFeatures(font) gsub = "\n".join(getXML(font["GSUB"].toXML)) assert gsub == """<Version value="0x00010000"/>
def compatibleRules(self, l, r): from fontFeatures.feaLib.Routine import ( arrange_by_type, arrange_by_lookup_type, arrange_by_flags, ) testRoutine = Routine() testRoutine.rules.extend(l.rules) testRoutine.rules.extend(r.rules) if arrange_by_type(testRoutine): return False if arrange_by_lookup_type(testRoutine): return False if arrange_by_flags(testRoutine): return False return True
def fromXML(klass, el): from fontFeatures import Routine, RoutineReference rule = klass(klass._slotArray(klass, el.find("input")), precontext=klass._slotArray(klass, el.find("precontext")), postcontext=klass._slotArray(klass, el.find("postcontext")), address=el.get("address"), languages=el.get("languages"), flags=int(el.get("flags") or 0)) lookupsxml = el.find("lookups") rule.lookups = [] for slot in lookupsxml: routines = [] for lu in slot: if lu.tag == "routinereference": routines.append(RoutineReference.fromXML(lu)) elif lu.tag == "routine": routines.append(Routine.fromXML(lu)) rule.lookups.append(routines) return rule
def apply(self, ff): logger = logging.getLogger("fontFeatures") deadRoutine = Routine(name="dead") def haschains(routine): for r in routine.rules: if isinstance(r, Chaining): return True routinelist = ff.routines ff.markRoutineUseInChains() for lix, l in enumerate(routinelist): if haschains(l) or l == deadRoutine: continue for rix in range(lix + 1, len(routinelist)): r = routinelist[rix] if haschains(r) or r == deadRoutine: continue if self.nonOverlapping(ff, l, r): logger.info("Merging nonoverlapping routines %s , %s" % (l.name, r.name)) self.merge(l, r) routinelist[rix] = deadRoutine ff.routines = list(filter(lambda r: r != deadRoutine, routinelist))
def test_routine_named(): r1 = Routine(name="One") f1 = FontFeatures() f1.addFeature("onex", [r1]) assert f1.routineNamed("One") == r1