diff options
Diffstat (limited to 'src/silfont/scripts/psfbuildcomp.py')
-rw-r--r-- | src/silfont/scripts/psfbuildcomp.py | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/src/silfont/scripts/psfbuildcomp.py b/src/silfont/scripts/psfbuildcomp.py new file mode 100644 index 0000000..48aa5c6 --- /dev/null +++ b/src/silfont/scripts/psfbuildcomp.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +__doc__ = '''Read Composite Definitions and add glyphs to a UFO font''' +__url__ = 'https://github.com/silnrsi/pysilfont' +__copyright__ = 'Copyright (c) 2015 SIL International (https://www.sil.org)' +__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)' +__author__ = 'David Rowe' + +try: + xrange +except NameError: + xrange = range +from xml.etree import ElementTree as ET +import re +from silfont.core import execute +import silfont.ufo as ufo +from silfont.comp import CompGlyph +from silfont.etutil import ETWriter +from silfont.util import parsecolors + +argspec = [ + ('ifont',{'help': 'Input UFO'}, {'type': 'infont'}), + ('ofont',{'help': 'Output UFO','nargs': '?' }, {'type': 'outfont'}), + ('-i','--cdfile',{'help': 'Composite Definitions input file'}, {'type': 'infile', 'def': '_CD.txt'}), + ('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_CD.log'}), + ('-a','--analysis',{'help': 'Analysis only; no output font generated', 'action': 'store_true'},{}), + ('-c','--color',{'help': 'Color cells of generated glyphs', 'action': 'store_true'},{}), + ('--colors', {'help': 'Color(s) to use when marking generated glyphs'},{}), + ('-f','--force',{'help': 'Force overwrite of glyphs having outlines', 'action': 'store_true'},{}), + ('-n','--noflatten',{'help': 'Do not flatten component references', 'action': 'store_true'},{}), + ('--remove',{'help': 'a regex matching anchor names that should always be removed from composites'},{}), + ('--preserve', {'help': 'a regex matching anchor names that, if present in glyphs about to be replace, should not be overwritten'}, {}) + ] + +glyphlist = [] # accessed as global by recursive function addtolist() and main function doit() + +def doit(args): + global glyphlist + infont = args.ifont + logger = args.logger + params = infont.outparams + + removeRE = re.compile(args.remove) if args.remove else None + preserveRE = re.compile(args.preserve) if args.preserve else None + + colors = None + if args.color or args.colors: + colors = args.colors if args.colors else "g_blue,g_purple" + colors = parsecolors(colors, allowspecial=True) + invalid = False + for color in colors: + if color[0] is None: + invalid = True + logger.log(color[2], "E") + if len(colors) > 3: + logger.log("A maximum of three colors can be supplied: " + str(len(colors)) + " supplied", "E") + invalid = True + if invalid: logger.log("Re-run with valid colors", "S") + if len(colors) == 1: colors.append(colors[0]) + if len(colors) == 2: colors.append(colors[1]) + logstatuses = ("Glyph unchanged", "Glyph changed", "New glyph") + + ### temp section (these may someday be passed as optional parameters) + RemoveUsedAnchors = True + ### end of temp section + + cgobj = CompGlyph() + + for linenum, rawCDline in enumerate(args.cdfile): + CDline=rawCDline.strip() + if len(CDline) == 0 or CDline[0] == "#": continue + logger.log("Processing line " + str(linenum+1) + ": " + CDline,"I") + cgobj.CDline=CDline + try: + cgobj.parsefromCDline() + except ValueError as mess: + logger.log("Parsing error: " + str(mess), "E") + continue + g = cgobj.CDelement + + # Collect target glyph information and construct list of component glyphs + targetglyphname = g.get("PSName") + targetglyphunicode = g.get("UID") + glyphlist = [] # list of component glyphs + lsb = rsb = 0 + adv = None + for e in g: + if e.tag == 'note': pass + elif e.tag == 'property': pass # ignore mark info + elif e.tag == 'lsb': lsb = int(e.get('width')) + elif e.tag == 'rsb': rsb = int(e.get('width')) + elif e.tag == 'advance': adv = int(e.get('width')) + elif e.tag == 'base': + addtolist(e,None) + logger.log(str(glyphlist),"V") + + # find each component glyph and compute x,y position + xadvance = lsb + componentlist = [] + targetglyphanchors = {} # dictionary of {name: (xOffset,yOffset)} + for currglyph, prevglyph, baseAP, diacAP, shiftx, shifty in glyphlist: + # get current glyph and its anchor names from font + if currglyph not in infont.deflayer: + logger.log(currglyph + " not found in font", "E") + continue + cg = infont.deflayer[currglyph] + cganc = [x.element.get('name') for x in cg['anchor']] + diacAPx = diacAPy = 0 + baseAPx = baseAPy = 0 + if prevglyph is None: # this is new 'base' + xOffset = xadvance + yOffset = 0 + # Find advance width of currglyph and add to xadvance + if 'advance' in cg: + cgadvance = cg['advance'] + if cgadvance is not None and cgadvance.element.get('width') is not None: + xadvance += int(float(cgadvance.element.get('width'))) + else: # this is 'attach' + if diacAP is not None: # find diacritic Attachment Point in currglyph + if diacAP not in cganc: + logger.log("The AP '" + diacAP + "' does not exist on diacritic glyph " + currglyph, "E") + else: + i = cganc.index(diacAP) + diacAPx = int(float(cg['anchor'][i].element.get('x'))) + diacAPy = int(float(cg['anchor'][i].element.get('y'))) + else: + logger.log("No AP specified for diacritic " + currglyph, "E") + if baseAP is not None: # find base character Attachment Point in targetglyph + if baseAP not in targetglyphanchors.keys(): + logger.log("The AP '" + baseAP + "' does not exist on base glyph when building " + targetglyphname, "E") + else: + baseAPx = targetglyphanchors[baseAP][0] + baseAPy = targetglyphanchors[baseAP][1] + if RemoveUsedAnchors: + logger.log("Removing used anchor " + baseAP, "V") + del targetglyphanchors[baseAP] + xOffset = baseAPx - diacAPx + yOffset = baseAPy - diacAPy + + if shiftx is not None: xOffset += int(shiftx) + if shifty is not None: yOffset += int(shifty) + + componentdic = {'base': currglyph} + if xOffset != 0: componentdic['xOffset'] = str(xOffset) + if yOffset != 0: componentdic['yOffset'] = str(yOffset) + componentlist.append( componentdic ) + + # Move anchor information to targetglyphanchors + for a in cg['anchor']: + dic = a.element.attrib + thisanchorname = dic['name'] + if RemoveUsedAnchors and thisanchorname == diacAP: + logger.log("Skipping used anchor " + diacAP, "V") + continue # skip this anchor + # add anchor (adjusted for position in targetglyph) + targetglyphanchors[thisanchorname] = ( int( dic['x'] ) + xOffset, int( dic['y'] ) + yOffset ) + logger.log("Adding anchor " + thisanchorname + ": " + str(targetglyphanchors[thisanchorname]), "V") + logger.log(str(targetglyphanchors),"V") + + if adv is not None: + xadvance = adv ### if adv specified, then this advance value overrides calculated value + else: + xadvance += rsb ### adjust with rsb + + logger.log("Glyph: " + targetglyphname + ", " + str(targetglyphunicode) + ", " + str(xadvance), "V") + for c in componentlist: + logger.log(str(c), "V") + + # Flatten components unless -n set + if not args.noflatten: + newcomponentlist = [] + for compdic in componentlist: + c = compdic['base'] + x = compdic.get('xOffset') + y = compdic.get('yOffset') + # look up component glyph + g=infont.deflayer[c] + # check if it has only components (that is, no contours) in outline + if g['outline'] and g['outline'].components and not g['outline'].contours: + # for each component, get base, x1, y1 and create new entry with base, x+x1, y+y1 + for subcomp in g['outline'].components: + componentdic = subcomp.element.attrib.copy() + x1 = componentdic.pop('xOffset', 0) + y1 = componentdic.pop('yOffset', 0) + xOffset = addtwo(x, x1) + yOffset = addtwo(y, y1) + if xOffset != 0: componentdic['xOffset'] = str(xOffset) + if yOffset != 0: componentdic['yOffset'] = str(yOffset) + newcomponentlist.append( componentdic ) + else: + newcomponentlist.append( compdic ) + if componentlist == newcomponentlist: + logger.log("No changes to flatten components", "V") + else: + componentlist = newcomponentlist + logger.log("Components flattened", "V") + for c in componentlist: + logger.log(str(c), "V") + + # Check if this new glyph exists in the font already; if so, decide whether to replace, or issue warning + preservedAPs = set() + if targetglyphname in infont.deflayer.keys(): + logger.log("Target glyph, " + targetglyphname + ", already exists in font.", "V") + targetglyph = infont.deflayer[targetglyphname] + if targetglyph['outline'] and targetglyph['outline'].contours and not args.force: # don't replace glyph with contours, unless -f set + logger.log("Not replacing existing glyph, " + targetglyphname + ", because it has contours.", "W") + continue + else: + logger.log("Replacing information in existing glyph, " + targetglyphname, "I") + glyphstatus = "Replace" + # delete information from existing glyph + targetglyph.remove('outline') + targetglyph.remove('advance') + for i in xrange(len(targetglyph['anchor'])-1,-1,-1): + aname = targetglyph['anchor'][i].element.attrib['name'] + if preserveRE is not None and preserveRE.match(aname): + preservedAPs.add(aname) + logger.log("Preserving anchor " + aname, "V") + else: + targetglyph.remove('anchor',index=i) + else: + logger.log("Adding new glyph, " + targetglyphname, "I") + glyphstatus = "New" + # create glyph, using targetglyphname, targetglyphunicode + targetglyph = ufo.Uglif(layer=infont.deflayer, name=targetglyphname) + # actually add the glyph to the font + infont.deflayer.addGlyph(targetglyph) + + if xadvance != 0: targetglyph.add('advance',{'width': str(xadvance)} ) + if targetglyphunicode: # remove any existing unicode value(s) before adding unicode value + for i in xrange(len(targetglyph['unicode'])-1,-1,-1): + targetglyph.remove('unicode',index=i) + targetglyph.add('unicode',{'hex': targetglyphunicode} ) + targetglyph.add('outline') + # to the outline element, add a component element for every entry in componentlist + for compdic in componentlist: + comp = ufo.Ucomponent(targetglyph['outline'],ET.Element('component',compdic)) + targetglyph['outline'].appendobject(comp,'component') + # copy anchors to new glyph from targetglyphanchors which has format {'U': (500,1000), 'L': (500,0)} + for a in sorted(targetglyphanchors): + if removeRE is not None and removeRE.match(a): + logger.log("Skipping unwanted anchor " + a, "V") + continue # skip this anchor + if a not in preservedAPs: + targetglyph.add('anchor', {'name': a, 'x': str(targetglyphanchors[a][0]), 'y': str(targetglyphanchors[a][1])} ) + # mark glyphs as being generated by setting cell mark color if -c or --colors set + if colors: + # Need to see if the target glyph has changed. + if glyphstatus == "Replace": + # Need to recreate the xml element then normalize it for comparison with original + targetglyph["anchor"].sort(key=lambda anchor: anchor.element.get("name")) + targetglyph.rebuildET() + attribOrder = params['attribOrders']['glif'] if 'glif' in params['attribOrders'] else {} + if params["sortDicts"] or params["precision"] is not None: ufo.normETdata(targetglyph.etree, params, 'glif') + etw = ETWriter(targetglyph.etree, attributeOrder=attribOrder, indentIncr=params["indentIncr"], + indentFirst=params["indentFirst"], indentML=params["indentML"], precision=params["precision"], + floatAttribs=params["floatAttribs"], intAttribs=params["intAttribs"]) + newxml = etw.serialize_xml() + if newxml == targetglyph.inxmlstr: glyphstatus = 'Unchanged' + + x = 0 if glyphstatus == "Unchanged" else 1 if glyphstatus == "Replace" else 2 + + color = colors[x] + lib = targetglyph["lib"] + if color[0]: # Need to set actual color + if lib is None: targetglyph.add("lib") + targetglyph["lib"].setval("public.markColor", "string", color[0]) + logger.log(logstatuses[x] + " - setting markColor to " + color[2], "I") + elif x < 2: # No need to log for new glyphs + if color[1] == "none": # Remove existing color + if lib is not None and "public.markColor" in lib: lib.remove("public.markColor") + logger.log(logstatuses[x] + " - Removing existing markColor", "I") + else: + logger.log(logstatuses[x] + " - Leaving existing markColor (if any)", "I") + + # If analysis only, return without writing output font + if args.analysis: return + # Return changed font and let execute() write it out + return infont + +def addtolist(e, prevglyph): + """Given an element ('base' or 'attach') and the name of previous glyph, + add a tuple to the list of glyphs in this composite, including + "at" and "with" attachment point information, and x and y shift values + """ + global glyphlist + subelementlist = [] + thisglyphname = e.get('PSName') + atvalue = e.get("at") + withvalue = e.get("with") + shiftx = shifty = None + for se in e: + if se.tag == 'property': pass + elif se.tag == 'shift': + shiftx = se.get('x') + shifty = se.get('y') + elif se.tag == 'attach': + subelementlist.append( se ) + glyphlist.append( ( thisglyphname, prevglyph, atvalue, withvalue, shiftx, shifty ) ) + for se in subelementlist: + addtolist(se, thisglyphname) + +def addtwo(a1, a2): + """Take two items (string, number or None), convert to integer and return sum""" + b1 = int(a1) if a1 is not None else 0 + b2 = int(a2) if a2 is not None else 0 + return b1 + b2 + +def cmd() : execute("UFO",doit,argspec) +if __name__ == "__main__": cmd() |