def action(self, args): (pre, l, r, post) = args[0] left = l.resolve(self.parser.fontfeatures, self.parser.font) right = r.resolve(self.parser.fontfeatures, self.parser.font) precontext = None postcontext = None if pre: precontext = [ g.resolve(self.parser.fontfeatures, self.parser.font) for g in pre ] if post: postcontext = [ g.resolve(self.parser.fontfeatures, self.parser.font) for g in post ] lefttoright = fontFeatures.Routine( rules=[fontFeatures.Substitution([left], [right])]) righttoleft = fontFeatures.Routine( rules=[fontFeatures.Substitution([right], [left])]) return [ fontFeatures.Chaining( [left, right], lookups=[[lefttoright], [righttoleft]], precontext=precontext, postcontext=postcontext, ) ]
def store(self, parser, tokens): from glyphtools import determine_kern, bin_glyphs_by_metric import fontFeatures import itertools units = int(tokens[-1].token) kerns = [] bincount = 5 lefts = parser.expandGlyphOrClassName(tokens[0].token) rights = parser.expandGlyphOrClassName(tokens[1].token) def make_kerns(rise=0, context=[], direction="LTR"): kerns = [] for l in lefts: for r in rights: if direction == "LTR": kern = determine_kern(parser.font, l, r, units, offset1=(0, rise)) else: kern = determine_kern(parser.font, r, l, units, offset1=(0, rise)) if abs(kern) < 5: continue v = fontFeatures.ValueRecord(xAdvance=kern) kerns.append( fontFeatures.Positioning([[r]], [v], precontext=[[l]], postcontext=context)) return kerns context = [ parser.expandGlyphOrClassName(x.token) for x in tokens[2:-1] ] if len(context) == 0: return [fontFeatures.Routine(rules=make_kerns())] kerns = [] binned_contexts = [ bin_glyphs_by_metric(parser.font, glyphs, "rise", bincount=bincount) for glyphs in context ] for c in itertools.product(*binned_contexts): totalrise = sum([x[1] for x in c]) precontext = [x[0] for x in c] kerns.extend( make_kerns(totalrise, context=precontext, direction="RTL")) return [fontFeatures.Routine(rules=kerns)]
def action(self, args): parser = self.parser bincount = 5 lefts, rights, units, pre = args[0] lefts = lefts.resolve(parser.fontfeatures, parser.font) rights = rights.resolve(parser.fontfeatures, parser.font) pre = [g.resolve(parser.fontfeatures, parser.font) for g in pre] def make_kerns(rise=0, context=[], direction="LTR"): kerns = [] for l in lefts: for r in rights: if direction == "LTR": kern = determine_kern(parser.font, l, r, units, offset1=(0, rise)) else: kern = determine_kern(parser.font, r, l, units, offset1=(0, rise)) if abs(kern) < 5: continue v = fontFeatures.ValueRecord(xAdvance=kern) kerns.append( fontFeatures.Positioning([[r]], [v], precontext=[[l]], postcontext=context)) return kerns if not pre: return [fontFeatures.Routine(rules=make_kerns())] kerns = [] binned_contexts = [ bin_glyphs_by_metric(parser.font, glyphs, "rise", bincount=bincount) for glyphs in pre ] for c in itertools.product(*binned_contexts): totalrise = sum([x[1] for x in c]) precontext = [x[0] for x in c] kerns.extend( make_kerns(totalrise, context=precontext, direction="RTL")) return [fontFeatures.Routine(rules=kerns)]
def action(self, args): (routinename, statements, flags_languages) = args flags, languages = flags_languages if routinename is not None: routinename = routinename[0].value if flags is None: flags = [] if not statements: rr = fontFeatures.RoutineReference(name = routinename) return [rr] r = fontFeatures.Routine() if routinename: r.name = routinename r.rules = [] for res in self.parser.filterResults(statements): if isinstance(res, fontFeatures.Routine): r.rules.extend(res.rules) else: r.rules.append(res) r.flags = 0 for f in flags: if isinstance(f, tuple): r.flags |= f[0] if f[0] == 0x10: r.markFilteringSet = f[1].resolve(self.parser.fontfeatures, self.parser.font) elif f[0] == 0xFF00: r.markAttachmentSet = f[1].resolve(self.parser.fontfeatures, self.parser.font) else: r.flags |= f r.languages = languages if not self.parser.current_feature: self.parser.fontfeatures.routines.append(r) return [r]
def unparseMarkToLigature(self, lookup): """Turn a GPOS5 (mark to ligature) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("MarkToLigature" + self.gensym())) self._fix_flags(b, lookup) self.unparsable(b, "Mark to lig pos", lookup) return b, []
def unparseCursiveAttachment(self, lookup): """Turn a GPOS3 (cursive attachment) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("CursiveAttachment" + self.gensym())) self._fix_flags(b, lookup) entries = {} exits = {} for s in lookup.SubTable: assert s.Format == 1 for glyph, record in zip(s.Coverage.glyphs, s.EntryExitRecord): if record.EntryAnchor: entries[glyph] = ( record.EntryAnchor.XCoordinate, record.EntryAnchor.YCoordinate, ) if record.ExitAnchor: exits[glyph] = ( record.ExitAnchor.XCoordinate, record.ExitAnchor.YCoordinate, ) b.addRule( fontFeatures.Attachment( "cursive_entry", "cursive_exit", entries, exits, flags=lookup.LookupFlag, address=self.currentLookup, ) ) return b, []
def action(self, parser, routinename, tail): if not tail: rr = fontFeatures.RoutineReference(name=routinename) return [rr] statements, flags = tail r = fontFeatures.Routine() if routinename: r.name = routinename r.rules = [] for res in parser.filterResults(statements): if isinstance(res, fontFeatures.Routine): r.rules.extend(res.rules) else: r.rules.append(res) r.flags = 0 for f in flags: if isinstance(f, tuple): r.flags |= f[0] if f[0] == 0x10: r.markFilteringSet = f[1].resolve(parser.fontfeatures, parser.font) elif f[0] == 0xFF00: r.markAttachmentSet = f[1].resolve(parser.fontfeatures, parser.font) else: r.flags |= f if not parser.current_feature: parser.fontfeatures.routines.append(r) return [r]
def unparseMarkToMark(self, lookup): """Turn a GPOS6 (mark to mark) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("MarkToMark" + self.gensym())) self._fix_flags(b, lookup) for subtable in lookup.SubTable: # fontTools.ttLib.tables.otTables.MarkBasePos assert subtable.Format == 1 for classId in range(0, subtable.ClassCount): anchorClassPrefix = "Anchor" + self.gensym() marks = self._formatMarkArray( subtable.Mark1Array, subtable.Mark1Coverage, classId ) bases = self._formatMark2Array( subtable.Mark2Array, subtable.Mark2Coverage, classId ) b.addRule( fontFeatures.Attachment( anchorClassPrefix, anchorClassPrefix + "_", bases, marks, font=self.font, address=self.currentLookup, flags=lookup.LookupFlag, force_markmark=True ) ) return b, []
def unparseSinglePositioning(self, lookup): """Turn a GPOS1 (single positioning) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("SinglePositioning" + self.gensym())) self._fix_flags(b, lookup) for subtable in lookup.SubTable: if subtable.Format == 1: spos = fontFeatures.Positioning( [subtable.Coverage.glyphs], [self.makeValueRecord(subtable.Value)], address=self.currentLookup, flags=lookup.LookupFlag, ) b.addRule(spos) else: # Optimize it later for g, v in zip(subtable.Coverage.glyphs, subtable.Value): spos = fontFeatures.Positioning( [[g]], [self.makeValueRecord(v)], address=self.currentLookup, flags=lookup.LookupFlag, ) b.addRule(spos) return b, []
def unparseSingleSubstitution(self, lookup): b = fontFeatures.Routine( name=self.getname("SingleSubstitution" + self.gensym()) ) self._fix_flags(b, lookup) for sub in lookup.SubTable: if len(sub.mapping) > 5: k = list(sub.mapping.keys()) v = list(sub.mapping.values()) b.addRule( fontFeatures.Substitution( [k], [v], address=self.currentLookup, flags=lookup.LookupFlag ) ) else: for k, v in sub.mapping.items(): b.addRule( fontFeatures.Substitution( [[k]], [[v]], address=self.currentLookup, flags=lookup.LookupFlag, ) ) return b, []
def unparseReverseContextualSubstitution(self, lookup): b = fontFeatures.Routine( name=self.getname("ReverseContextualSubstitution" + self.gensym()) ) self._fix_flags(b, lookup) for sub in lookup.SubTable: prefix = [] outputs = [] suffix = [] if hasattr(sub, "BacktrackCoverage"): for coverage in reversed(sub.BacktrackCoverage): prefix.append(coverage.glyphs) if hasattr(sub, "LookAheadCoverage"): for i, coverage in enumerate(sub.LookAheadCoverage): suffix.append(coverage.glyphs) outputs = [ sub.Substitute ] inputs = [ sub.Coverage.glyphs ] b.addRule( fontFeatures.Substitution( inputs, outputs, prefix, suffix, flags=lookup.LookupFlag, reverse=True ) ) return b,[]
def action(self, parser, aFrom, aTo, attachtype): bases = {} marks = {} def _category(k): return parser.fontfeatures.glyphclasses.get( k, parser.font[k].category) for k, v in parser.fontfeatures.anchors.items(): if aFrom in v: bases[k] = v[aFrom] if aTo in v: marks[k] = v[aTo] if attachtype == "marks": bases = { k: v for k, v in bases.items() if _category(k) == "mark" } else: bases = { k: v for k, v in bases.items() if _category(k) == "base" } if attachtype != "cursive": marks = { k: v for k, v in marks.items() if _category(k) == "mark" } return [ fontFeatures.Routine(rules=[ fontFeatures.Attachment( aFrom, aTo, bases, marks, font=parser.font) ]) ]
def action(self, args): parser = self.parser (bases, matra, matras) = args bases = bases.resolve(parser.fontfeatures, parser.font) matra = matra.resolve(parser.fontfeatures, parser.font) matras = matras.resolve(parser.fontfeatures, parser.font) # Organise matras into overhang widths matras2bases = {} matrasAndOverhangs = [(m, -parser.font[m].rightMargin) for m in matras] for b in bases: w = self.find_stem(parser.font, b) warnings.warn("Stem location for %s: %s" % (b, w)) (bestMatra, _) = min(matrasAndOverhangs, key=lambda s: abs(s[1] - w)) if not bestMatra in matras2bases: matras2bases[bestMatra] = [] matras2bases[bestMatra].append(b) rv = [] for bestMatra, basesForThisMatra in matras2bases.items(): rv.append( fontFeatures.Substitution([matra], postcontext=[basesForThisMatra], replacement=[[bestMatra]])) return [fontFeatures.Routine(rules=rv)]
def action(self, args): parser = self.parser sequence = args[:-1] routine = args[-1] combinations = [ g.resolve(parser.fontfeatures, parser.font) for g in sequence ] named = [x for x in parser.fontfeatures.routines if x.name == routine] if len(named) != 1: raise ValueError("Could not find routine called %s" % routine) routine = named[0] janky = fontFeatures.jankyPOS.JankyPos(parser.font, direction="RTL") col = collidoscope.Collidoscope( None, { "cursive": False, "faraway": True, "area": 0.1 }, ttFont=parser.font, ) col.direction = "RTL" r = [] clashes = 0 total = reduce(lambda x, y: x * y, [len(x) for x in combinations], 1) warnings.warn("Enumerating %i sequences" % total) for element in itertools.product(*combinations): buf = janky.positioning_buffer(element) buf = janky.process_fontfeatures(buf, parser.fontfeatures) glyphs = [] cursor = 0 for info in buf: g, vr = info["glyph"], info["position"] offset = beziers.point.Point(cursor + (vr.xPlacement or 0), vr.yPlacement or 0) glyphs.append(col.get_positioned_glyph(g, offset)) glyphs[-1]["advance"] = vr.xAdvance cursor = cursor + vr.xAdvance overlaps = col.has_collisions(glyphs) if not overlaps: continue clashes = clashes + 1 if clashes >= 0.6 * total: break # warnings.warn("Overlap found in glyph sequence %s" % (" ".join(element))) r.append( fontFeatures.Chaining([[x] for x in element], lookups=[[routine] for x in element])) if clashes >= 0.6 * total: warnings.warn( "Most enumerations of sequence overlapped, calling routine on whole sequence instead. This may not be what you want!" ) r = [ fontFeatures.Chaining(combinations, lookups=[[routine] for x in combinations]) ] return [fontFeatures.Routine(rules=r)]
def action(self, args): overhang_padding, glyphs = args parser = self.parser for c in ["inits", "medis"]: if c not in parser.fontfeatures.namedClasses: raise ValueError("Please define @%s class before calling") medis = parser.fontfeatures.namedClasses["medis"] inits = parser.fontfeatures.namedClasses["inits"] overhangers = glyphs.resolve(parser.fontfeatures, parser.font) binned_medis = bin_glyphs_by_metric(parser.font, medis, "run", bincount=8) binned_inits = bin_glyphs_by_metric(parser.font, inits, "run", bincount=8) rules = [] maxchainlength = 0 longeststring = [] for yb in overhangers: entry_anchor = parser.fontfeatures.anchors[yb]["entry"] overhang = max( -get_glyph_metrics(parser.font, yb)["rsb"], get_glyph_metrics(parser.font, yb)["xMax"] - entry_anchor[0], ) workqueue = [[x] for x in binned_inits] while workqueue: string = workqueue.pop(0) totalwidth = sum([max(x[1], failsafe_min_run) for x in string]) if totalwidth > overhang or len(string) > failsafe_max_length: continue adjustment = overhang - totalwidth + int(overhang_padding) postcontext = [x[0] for x in string[:-1]] + [[yb]] input_ = string[-1] example = [input_[0][0]] + [x[0] for x in postcontext] warnings.warn( "For glyphs in %s, overhang=%i totalwidth=%i adjustment=%i" % (example, overhang, totalwidth, adjustment)) maxchainlength = max(maxchainlength, len(string)) rules.append( fontFeatures.Positioning( [input_[0]], [fontFeatures.ValueRecord(xAdvance=int(adjustment))], postcontext=postcontext, )) for medi in binned_medis: workqueue.append([medi] + string) warnings.warn("Bari Ye collision maximum chain length was %i glyphs" % maxchainlength) return [fontFeatures.Routine(rules=rules, flags=8)]
def apply(self): right = self.matches() left = self.transform(right) left2, right2 = [], [] routine = fontFeatures.Routine(name=self.name.title().replace(" ", "")) for l, r in zip(left, right): if l in self.glyphnames and r in self.glyphnames: routine.addRule(fontFeatures.Substitution([[l]], [[r]])) self.project.fontfeatures.routines.extend([routine]) self.project.fontfeatures.addFeature(self.feature, [routine])
def apply(self): right = self.matches() routine = fontFeatures.Routine(name=self.name.title().replace(" ", "")) for r in right: left = r.replace(".liga","").split("_") if r == "fl" or r == "fi": left = list(r) if all(l in self.glyphnames for l in left) and r in self.glyphnames: routine.addRule(fontFeatures.Substitution([ [l] for l in left], [[r]])) self.project.fontfeatures.routines.extend([routine]) self.project.fontfeatures.addFeature(self.feature, [routine])
def _start_routine(self, location, name): location = "%s:%i:%i" % (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 unparseContextual(self, lookup): b = fontFeatures.Routine(name=self.getname("Contextual" + self._table + self.gensym())) self._fix_flags(b, lookup) for sub in lookup.SubTable: if sub.Format == 1: self._unparse_contextual_format1(sub, b, lookup) elif sub.Format == 2: self._unparse_contextual_format2(sub, b, lookup) elif sub.Format == 3: self._unparse_contextual_format3(sub, b, lookup) else: raise ValueError return b, []
def unparseSingleSubstitution(self, lookup): """Turn a GPOS1 (single substitution) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("SingleSubstitution" + self.gensym())) self._fix_flags(b, lookup) for sub in lookup.SubTable: for k, v in sub.mapping.items(): b.addRule( fontFeatures.Substitution( [[k]], [[v]], address=self.currentLookup, flags=lookup.LookupFlag, )) return b, []
def unparseChainedContextual(self, lookup): """Handles a generic chained contextual lookup, in various formats.""" b = fontFeatures.Routine(name=self.getname("ChainedContextual" + self._table + self.gensym())) self._fix_flags(b, lookup) for sub in lookup.SubTable: if sub.Format == 1: self._unparse_contextual_chain_format1(sub, b, lookup) elif sub.Format == 2: self._unparse_contextual_chain_format2(sub, b, lookup) elif sub.Format == 3: self._unparse_contextual_chain_format3(sub, b, lookup) else: raise ValueError return b, []
def unparseAlternateSubstitution(self, lookup): b = fontFeatures.Routine( name=self.getname("AlternateSubstitution" + self.gensym()) ) self._fix_flags(b, lookup) for sub in lookup.SubTable: for in_glyph, out_glyphs in sub.alternates.items(): b.addRule( fontFeatures.Substitution( singleglyph(in_glyph), [out_glyphs], address=self.currentLookup, flags=lookup.LookupFlag, ) ) return b, []
def unparseMultipleSubstitution(self, lookup): """Turn a GPOS2 (multiple substitution) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("MultipleSubstitution" + self.gensym())) self._fix_flags(b, lookup) for sub in lookup.SubTable: for in_glyph, out_glyphs in sub.mapping.items(): b.addRule( fontFeatures.Substitution( singleglyph(in_glyph), [glyph(x) for x in out_glyphs], address=self.currentLookup, flags=lookup.LookupFlag, )) return b, []
def action(cls, parser, ligatures): ligatures = sorted(ligatures.resolve(parser.fontfeatures, parser.font), key=lambda a: len(a)) rv = [] glyphnames = "|".join(sorted(parser.font.keys(), key=lambda a: len(a))) for l in ligatures: for liglen in range(5, 0, -1): ligre = "^" + ("(" + glyphnames + ")_") * liglen + "(" + glyphnames + ")$" m = re.match(ligre, l) if m: rv.append( fontFeatures.Substitution([[c] for c in m.groups()], replacement=[[l]])) break return [fontFeatures.Routine(rules=rv)]
def unparsePairPositioning(self, lookup): b = fontFeatures.Routine(name=self.getname("PairPositioning" + self.gensym())) self._fix_flags(b, lookup) for subtable in lookup.SubTable: if subtable.Format == 1: for g, pair in zip(subtable.Coverage.glyphs, subtable.PairSet): for vr in pair.PairValueRecord: spos = fontFeatures.Positioning( [[g], [vr.SecondGlyph]], [ self.makeValueRecord(vr.Value1, subtable.ValueFormat1), self.makeValueRecord(vr.Value2, subtable.ValueFormat2), ], address=self.currentLookup, flags=lookup.LookupFlag, ) b.addRule(spos) else: class1 = self._invertClassDef(subtable.ClassDef1.classDefs, self.font) class2 = self._invertClassDef(subtable.ClassDef2.classDefs, self.font) for ix1, c1 in enumerate(subtable.Class1Record): if ix1 not in class1: continue # XXX for ix2, c2 in enumerate(c1.Class2Record): if ix2 not in class2: continue # XXX vr1 = self.makeValueRecord(c2.Value1, subtable.ValueFormat1) vr2 = self.makeValueRecord(c2.Value2, subtable.ValueFormat2) if not vr1 and not vr2: continue firstClass = list( set(class1[ix1]) & set(subtable.Coverage.glyphs)) spos = fontFeatures.Positioning( [firstClass, class2[ix2]], [vr1, vr2], address=self.currentLookup, flags=lookup.LookupFlag, ) b.addRule(spos) return b, []
def unparseLookups(self): if not self.table.LookupList: return # Create a dummy list first, to allow resolving chained lookups for _ in self.table.LookupList.Lookup: r = fontFeatures.Routine() self.lookups.append(r) self.fontFeatures.routines.append(r) for lookupIdx, lookup in enumerate(self.table.LookupList.Lookup): routine, deps = self.unparseLookup(lookup, lookupIdx) debug = self.getDebugInfo(self._table, lookupIdx) if debug: routine.address = (self._table, lookupIdx, *debug) if debug[1]: routine.name = debug[1] self.copyRoutineToRoutine(routine, self.lookups[lookupIdx])
def action(self, parser, filename): rules = {} reachable = set([]) basedir = os.path.dirname(parser.current_file) trypath = os.path.join(basedir, filename) if not os.path.exists(trypath): trypath = filename if not os.path.exists(trypath): raise ValueError("Couldn't find connections file %s" % trypath) with open(trypath) as csvfile: reader = csv.DictReader(csvfile) for line in reader: left_glyph = line["Left Glyph"] remainder = list(line.items())[1:] for (g, v) in remainder: old = g + "1" if v == "1" or v == 1 or not v: continue replacement = g + str(v) if not old in rules: rules[old] = {} if not replacement in rules[old]: rules[old][replacement] = [] rules[old][replacement].append(left_glyph) r = fontFeatures.Routine(name="connections", flags=0x8) for oldglyph in rules: for replacement in rules[oldglyph]: context = rules[oldglyph][replacement] reachable |= set(context) reachable |= set([oldglyph, replacement]) r.addRule( fontFeatures.Substitution( [[oldglyph]], [[replacement]], postcontext=[context], reverse=True, )) parser.fontfeatures.namedClasses["reachable_glyphs"] = tuple( sorted(reachable)) return [r]
def unparseLigatureSubstitution(self, lookup): """Turn a GPOS4 (ligature substitution) subtable into a fontFeatures Routine.""" b = fontFeatures.Routine(name=self.getname("LigatureSubstitution" + self.gensym())) self._fix_flags(b, lookup) for sub in lookup.SubTable: for first, ligatures in sub.ligatures.items(): for lig in ligatures: substarray = [glyph(first)] for x in lig.Component: substarray.append(glyph(x)) b.addRule( fontFeatures.Substitution( substarray, singleglyph(lig.LigGlyph), address=self.currentLookup, flags=lookup.LookupFlag, )) return b, []
def action(self, args): parser = self.parser bases = args[0].resolve(parser.fontfeatures, parser.font) medialra = args[1].resolve(parser.fontfeatures, parser.font) if len(args) == 3: otherras = args[2].resolve(parser.fontfeatures, parser.font) overshoot = 0 else: overshoot = args[2] otherras = args[3].resolve(parser.fontfeatures, parser.font) # Chuck the original ra in the basket if overshoot is None: overshoot = 0 ras = set(otherras + medialra) # Organise ras into overhang widths ras2bases = {} rasAndOverhangs = [(m, -parser.font[m].rightMargin + overshoot) for m in ras] rasAndOverhangs = list(reversed(sorted(rasAndOverhangs))) for b in bases: w = parser.font[b].width - ((parser.font[b].rightMargin or 0) + (parser.font[b].leftMargin or 0)) bestRa = None for ra, overhang in rasAndOverhangs: if overhang <= w: bestRa = ra break if not bestRa: continue if bestRa not in ras2bases: ras2bases[bestRa] = [] ras2bases[bestRa].append(b) rv = [] for bestRa, basesForThisRa in ras2bases.items(): rv.append( fontFeatures.Substitution([medialra], postcontext=[basesForThisRa], replacement=[[bestRa]])) return [fontFeatures.Routine(rules=rv)]
def action(self, parser): glyphs = FontProxy(parser.font).glyphs rules = [] for g in glyphs: m = re.match(self.ligature, g) if not m: continue first, second, position = m[1], m[2], m[3] if position == "u": first = first + "i1" second = second + "f1" rules.append( fontFeatures.Substitution([[first], [second]], [[g]])) if position == "f": first = first + "m1" second = second + "f1" rules.append( fontFeatures.Substitution([[first], [second]], [[g]])) return [fontFeatures.Routine(name="Ligatures", rules=rules, flags=0x8)]