1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
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()
|