310 lines
15 KiB
Python
310 lines
15 KiB
Python
|
#!/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()
|