summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfbuildcomp.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/silfont/scripts/psfbuildcomp.py')
-rwxr-xr-xlib/silfont/scripts/psfbuildcomp.py309
1 files changed, 309 insertions, 0 deletions
diff --git a/lib/silfont/scripts/psfbuildcomp.py b/lib/silfont/scripts/psfbuildcomp.py
new file mode 100755
index 0000000..0c5d790
--- /dev/null
+++ b/lib/silfont/scripts/psfbuildcomp.py
@@ -0,0 +1,309 @@
+#!/usr/bin/env python
+__doc__ = '''Read Composite Definitions and add glyphs to a UFO font'''
+__url__ = 'http://github.com/silnrsi/pysilfont'
+__copyright__ = 'Copyright (c) 2015 SIL International (http://www.sil.org)'
+__license__ = 'Released under the MIT License (http://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 "(0.04,0.57,0.04,1)"
+ 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()