summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfmakefea.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/silfont/scripts/psfmakefea.py')
-rwxr-xr-xlib/silfont/scripts/psfmakefea.py369
1 files changed, 369 insertions, 0 deletions
diff --git a/lib/silfont/scripts/psfmakefea.py b/lib/silfont/scripts/psfmakefea.py
new file mode 100755
index 0000000..6ef0613
--- /dev/null
+++ b/lib/silfont/scripts/psfmakefea.py
@@ -0,0 +1,369 @@
+#!/usr/bin/python3
+__doc__ = 'Make features.fea file'
+# TODO: add conditional compilation, compare to fea, compile to ttf
+__url__ = 'http://github.com/silnrsi/pysilfont'
+__copyright__ = 'Copyright (c) 2017 SIL International (http://www.sil.org)'
+__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
+__author__ = 'Martin Hosken, Alan Ward'
+
+import silfont.ufo as ufo
+from collections import OrderedDict
+from silfont.feax_parser import feaplus_parser
+from xml.etree import ElementTree as et
+import re
+
+from silfont.core import execute
+
+def getbbox(g):
+ res = (65536, 65536, -65536, -65536)
+ if g['outline'] is None:
+ return (0, 0, 0, 0)
+ for c in g['outline'].contours:
+ for p in c['point']:
+ if 'type' in p.attrib: # any actual point counts
+ x = float(p.get('x', '0'))
+ y = float(p.get('y', '0'))
+ res = (min(x, res[0]), min(y, res[1]), max(x, res[2]), max(y, res[3]))
+ return res
+
+class Glyph(object) :
+ def __init__(self, name, advance=0, bbox=None) :
+ self.name = name
+ self.anchors = {}
+ self.is_mark = False
+ self.advance = int(float(advance))
+ self.bbox = bbox or (0, 0, 0, 0)
+
+ def add_anchor(self, info) :
+ self.anchors[info['name']] = (int(float(info['x'])), int(float(info['y'])))
+
+ def decide_if_mark(self) :
+ for a in self.anchors.keys() :
+ if a.startswith("_") :
+ self.is_mark = True
+ break
+
+def decode_element(e):
+ '''Convert plist element into python structures'''
+ res = None
+ if e.tag == 'string':
+ return e.text
+ elif e.tag == 'integer':
+ return int(e.text)
+ elif e.tag== 'real':
+ return float(e.text)
+ elif e.tag == 'array':
+ res = [decode_element(x) for x in e]
+ elif e.tag == 'dict':
+ res = {}
+ for p in zip(e[::2], e[1::2]):
+ res[p[0].text] = decode_element(p[1])
+ return res
+
+class Font(object) :
+ def __init__(self, defines = None):
+ self.glyphs = OrderedDict()
+ self.classes = OrderedDict()
+ self.all_aps = OrderedDict()
+ self.fontinfo = {}
+ self.kerns = {}
+ self.defines = {} if defines is None else defines
+
+ def readaps(self, filename, omitaps='', params = None) :
+ omittedaps = set(omitaps.replace(',',' ').split()) # allow comma- and/or space-separated list
+ if filename.endswith('.ufo') :
+ f = ufo.Ufont(filename, params = params)
+ self.fontinfo = {}
+ for k, v in f.fontinfo._contents.items():
+ self.fontinfo[k] = decode_element(v[1])
+ skipglyphs = set(f.lib.getval('public.skipExportGlyphs', []))
+ for g in f.deflayer :
+ if g in skipglyphs:
+ continue
+ ufo_g = f.deflayer[g]
+ advb = ufo_g['advance']
+ adv = advb.width if advb is not None else 0
+ bbox = getbbox(ufo_g)
+ glyph = Glyph(g, advance=adv, bbox=bbox)
+ self.glyphs[g] = glyph
+ if 'anchor' in ufo_g._contents :
+ for a in ufo_g._contents['anchor'] :
+ if a.element.attrib['name'] not in omittedaps:
+ glyph.add_anchor(a.element.attrib)
+ self.all_aps.setdefault(a.element.attrib['name'], []).append(glyph)
+ if hasattr(f, 'groups'):
+ for k, v in f.groups._contents.items():
+ self.classes[k.lstrip('@')] = decode_element(v[1])
+ if hasattr(f, 'kerning'):
+ for k, v in f.kerning._contents.items():
+ key = k.lstrip('@')
+ if key in self.classes:
+ key = "@" + key
+ subelements = decode_element(v[1])
+ kerndict = {}
+ for s, n in subelements.items():
+ skey = s.lstrip('@')
+ if skey in self.classes:
+ skey = "@" + skey
+ kerndict[skey] = n
+ self.kerns[key] = kerndict
+ elif filename.endswith('.xml') :
+ currGlyph = None
+ currPoint = None
+ self.fontinfo = {}
+ for event, elem in et.iterparse(filename, events=('start', 'end')):
+ if event == 'start':
+ if elem.tag == 'glyph':
+ name = elem.get('PSName', '')
+ if name:
+ currGlyph = Glyph(name)
+ self.glyphs[name] = currGlyph
+ currPoint = None
+ elif elem.tag == 'point':
+ currPoint = {'name' : elem.get('type', '')}
+ elif elem.tag == 'location' and currPoint is not None:
+ currPoint['x'] = int(elem.get('x', 0))
+ currPoint['y'] = int(elem.get('y', 0))
+ elif elem.tag == 'font':
+ n = elem.get('name', '')
+ x = n.split('-')
+ if len(x) == 2:
+ self.fontinfo['familyName'] = x[0]
+ self.fontinfo['openTypeNamePreferredFamilyName'] = x[0]
+ self.fontinfo['styleMapFamilyName'] = x[0]
+ self.fontinfo['styleName'] = x[1]
+ self.fontinfo['openTypeNamePreferredSubfamilyName'] = x[1]
+ self.fontinfo['postscriptFullName'] = "{0} {1}".format(*x)
+ self.fontinfo['postscriptFontName'] = n
+ elif event == 'end':
+ if elem.tag == 'point':
+ if currGlyph and currPoint['name'] not in omittedaps:
+ currGlyph.add_anchor(currPoint)
+ self.all_aps.setdefault(currPoint['name'], []).append(currGlyph)
+ currPoint = None
+ elif elem.tag == 'glyph':
+ currGlyph = None
+
+ def read_classes(self, fname, classproperties=False):
+ doc = et.parse(fname)
+ for c in doc.findall('.//class'):
+ class_name = c.get('name')
+ m = re.search('\[(\d+)\]$', class_name)
+ # support fixedclasses like make_gdl.pl via AP.pm
+ if m:
+ class_nm = class_name[0:m.start()]
+ ix = int(m.group(1))
+ else:
+ class_nm = class_name
+ ix = None
+ cl = self.classes.setdefault(class_nm, [])
+ for e in c.get('exts', '').split() + [""]:
+ for g in c.text.split():
+ if g+e in self.glyphs or (e == '' and g.startswith('@')):
+ if ix:
+ cl.insert(ix, g+e)
+ else:
+ cl.append(g+e)
+ if not classproperties:
+ return
+ for c in doc.findall('.//property'):
+ for e in c.get('exts', '').split() + [""]:
+ for g in c.text.split():
+ if g+e in self.glyphs:
+ cname = c.get('name') + "_" + c.get('value')
+ self.classes.setdefault(cname, []).append(g+e)
+
+ def make_classes(self, ligmode) :
+ for name, g in self.glyphs.items() :
+ # pull off suffix and make classes
+ # TODO: handle ligatures
+ base = name
+ if ligmode is None or 'comp' not in ligmode or "_" not in name:
+ pos = base.rfind('.')
+ while pos > 0 :
+ old_base = base
+ ext = base[pos+1:]
+ base = base[:pos]
+ ext_class_nm = "c_" + ext
+ if base in self.glyphs and old_base in self.glyphs:
+ glyph_lst = self.classes.setdefault(ext_class_nm, [])
+ if not old_base in glyph_lst:
+ glyph_lst.append(old_base)
+ self.classes.setdefault("cno_" + ext, []).append(base)
+ pos = base.rfind('.')
+ if ligmode is not None and "_" in name:
+ comps = name.split("_")
+ if "comp" in ligmode or "." not in comps[-1]:
+ base = comps.pop(-1 if "last" in ligmode else 0)
+ cname = base.replace(".", "_")
+ noname = "_".join(comps)
+ if base in self.glyphs and noname in self.glyphs:
+ glyph_lst = self.classes.setdefault("clig_"+cname, [])
+ if name not in glyph_lst:
+ glyph_lst.append(name)
+ self.classes.setdefault("cligno_"+cname, []).append(noname)
+ if g.is_mark :
+ self.classes.setdefault('GDEF_marks', []).append(name)
+ else :
+ self.classes.setdefault('GDEF_bases', []).append(name)
+
+ def make_marks(self) :
+ for name, g in self.glyphs.items() :
+ g.decide_if_mark()
+
+ def order_classes(self):
+ # return ordered list of classnames as desired for FEA
+
+ # Start with alphabetical then correct:
+ # 1. Put classes like "cno_whatever" adjacent to "c_whatever"
+ # 2. Classes can be defined in terms of other classes but FEA requires that
+ # classes be defined before they can be referenced.
+
+ def sortkey(x):
+ key1 = 'c_' + x[4:] if x.startswith('cno_') else x
+ return (key1, x)
+
+ classes = sorted(self.classes.keys(), key=sortkey)
+ links = {} # key = classname; value = list of other classes that include this one
+ counts = {} # key = classname; value = count of un-output classes that this class includes
+ for name in classes:
+ y = [c[1:] for c in self.classes[name] if c.startswith('@')] #list of included classes
+ counts[name] = len(y)
+ for c in y:
+ links.setdefault(c, []).append(name)
+
+ outclasses = []
+ while len(classes) > 0:
+ foundone = False
+ for name in classes:
+ if counts[name] == 0:
+ foundone = True
+ # output this class
+ outclasses.append(name)
+ classes.remove(name)
+ # adjust counts of classes that include this one
+ if name in links:
+ for n in links[name]:
+ counts[n] -= 1
+ # It may now be possible to output some we skipped earlier,
+ # so start over from the beginning of the list
+ break
+ if not foundone:
+ # all remaining classes include un-output classes and thus there is a loop somewhere
+ raise ValueError("Class reference loop(s) found: " + ", ".join(classes))
+ return outclasses
+
+ def addComment(self, parser, text):
+ cmt = parser.ast.Comment("# " + text, location=None)
+ cmt.pretext = "\n"
+ parser.add_statement(cmt)
+
+ def append_classes(self, parser) :
+ # normal glyph classes
+ self.addComment(parser, "Main Classes")
+ for name in self.order_classes():
+ gc = parser.ast.GlyphClass(None, location=None)
+ for g in self.classes[name] :
+ gc.append(g)
+ gcd = parser.ast.GlyphClassDefinition(name, gc, location=None)
+ parser.add_statement(gcd)
+ parser.define_glyphclass(name, gcd)
+
+ def _addGlyphsToClass(self, parser, glyphs, gc, anchor, definer):
+ if len(glyphs) > 1 :
+ val = parser.ast.GlyphClass(glyphs, location=None)
+ else :
+ val = parser.ast.GlyphName(glyphs[0], location=None)
+ classdef = definer(gc, anchor, val, location=None)
+ gc.addDefinition(classdef)
+ parser.add_statement(classdef)
+
+ def append_positions(self, parser):
+ # create base and mark classes, add to fea file dicts and parser symbol table
+ bclassdef_lst = []
+ mclassdef_lst = []
+ self.addComment(parser, "Positioning classes and statements")
+ for ap_nm, glyphs_w_ap in self.all_aps.items() :
+ self.addComment(parser, "AP: " + ap_nm)
+ # e.g. all glyphs with U AP
+ if not ap_nm.startswith("_"):
+ if any(not x.is_mark for x in glyphs_w_ap):
+ gcb = parser.set_baseclass(ap_nm)
+ parser.add_statement(gcb)
+ if any(x.is_mark for x in glyphs_w_ap):
+ gcm = parser.set_baseclass(ap_nm + "_MarkBase")
+ parser.add_statement(gcm)
+ else:
+ gc = parser.set_markclass(ap_nm)
+
+ # create lists of glyphs that use the same point (name and coordinates)
+ # that can share a class definition
+ anchor_cache = OrderedDict()
+ markanchor_cache = OrderedDict()
+ for g in glyphs_w_ap :
+ p = g.anchors[ap_nm]
+ if g.is_mark and not ap_nm.startswith("_"):
+ markanchor_cache.setdefault(p, []).append(g.name)
+ else:
+ anchor_cache.setdefault(p, []).append(g.name)
+
+ if ap_nm.startswith("_"):
+ for p, glyphs_w_pt in anchor_cache.items():
+ anchor = parser.ast.Anchor(p[0], p[1], location=None)
+ self._addGlyphsToClass(parser, glyphs_w_pt, gc, anchor, parser.ast.MarkClassDefinition)
+ else:
+ for p, glyphs_w_pt in anchor_cache.items():
+ anchor = parser.ast.Anchor(p[0], p[1], location=None)
+ self._addGlyphsToClass(parser, glyphs_w_pt, gcb, anchor, parser.ast.BaseClassDefinition)
+ for p, glyphs_w_pt in markanchor_cache.items():
+ anchor = parser.ast.Anchor(p[0], p[1], location=None)
+ self._addGlyphsToClass(parser, glyphs_w_pt, gcm, anchor, parser.ast.BaseClassDefinition)
+
+#TODO: provide more argument info
+argspec = [
+ ('infile', {'nargs': '?', 'help': 'Input UFO or file'}, {'def': None, 'type': 'filename'}),
+ ('-i', '--input', {'required': 'True', 'help': 'Fea file to merge'}, {}),
+ ('-o', '--output', {'help': 'Output fea file'}, {}),
+ ('-c', '--classfile', {'help': 'Classes file'}, {}),
+ ('-L', '--ligmode', {'help': 'Parse ligatures: last - use last element as class name, first - use first element as class name, lastcomp, firstcomp - final variants are part of the component not the whole ligature'}, {}),
+ ('-D', '--define', {'action': 'append', 'help': 'Add option definition to pass to fea code --define=var=val'}, {}),
+ # ('--debug', {'help': 'Drop into pdb', 'action': 'store_true'}, {}),
+ ('--classprops', {'help': 'Include property elements from classes file', 'action': 'store_true'}, {}),
+ ('--omitaps', {'help': 'names of attachment points to omit (comma- or space-separated)', 'default': '', 'action': 'store'}, {})
+]
+
+def doit(args) :
+ defines = dict(x.split('=') for x in args.define) if args.define else {}
+ font = Font(defines)
+ # if args.debug:
+ # import pdb; pdb.set_trace()
+ if "checkfix" not in args.params:
+ args.paramsobj.sets["main"]["checkfix"] = "None"
+ if args.infile is not None:
+ font.readaps(args.infile, args.omitaps, args.paramsobj)
+
+ font.make_marks()
+ font.make_classes(args.ligmode)
+ if args.classfile:
+ font.read_classes(args.classfile, classproperties = args.classprops)
+
+ p = feaplus_parser(None, font.glyphs, font.fontinfo, font.kerns, font.defines)
+ doc_ufo = p.parse() # returns an empty ast.FeatureFile
+
+ # Add goodies from the font
+ font.append_classes(p)
+ font.append_positions(p)
+
+ # parse the input fea file
+ if args.input :
+ doc_fea = p.parse(args.input)
+ else:
+ doc_fea = doc_ufo
+
+ # output as doc.asFea()
+ if args.output :
+ with open(args.output, "w") as of :
+ of.write(doc_fea.asFea())
+
+def cmd(): execute(None, doit, argspec)
+if __name__ == '__main__': cmd()