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
|
#!/usr/bin/env python3
__doc__ = '''Sync metadata across a family of fonts assuming standard UFO file naming'''
__url__ = 'https://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2017 SIL International (https://www.sil.org)'
__license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)'
__author__ = 'David Raymond'
from silfont.core import execute
from datetime import datetime
import silfont.ufo as UFO
import os
from xml.etree import ElementTree as ET
argspec = [
('ifont',{'help': 'Input font file'}, {'type': 'infont'}),
('-l','--log',{'help': 'Log file'}, {'type': 'outfile', 'def': '_sync.log'}),
('-s','--single', {'help': 'Sync single UFO against master', 'action': 'store_true', 'default': False},{}),
('-m','--master', {'help': 'Master UFO to sync single UFO against', 'nargs': '?' },{'type': 'infont', 'def': None}),
('-r','--reportonly', {'help': 'Report issues but no updating', 'action': 'store_true', 'default': False},{}),
('-n','--new', {'help': 'append "_new" to file/ufo names', 'action': 'store_true', 'default': False},{}),
('--normalize', {'help': 'output all the fonts to normalize them', 'action': 'store_true', 'default': False},{}),
]
def doit(args) :
standardstyles = ["Regular", "Italic", "Bold", "BoldItalic"]
finfoignore = ["openTypeHeadCreated", "openTypeOS2Panose", "postscriptBlueScale", "postscriptBlueShift",
"postscriptBlueValues", "postscriptOtherBlues", "postscriptStemSnapH", "postscriptStemSnapV", "postscriptForceBold"]
libfields = ["public.postscriptNames", "public.glyphOrder", "com.schriftgestaltung.glyphOrder"]
font = args.ifont
logger = args.logger
singlefont = args.single
mfont = args.master
newfile = "_new" if args.new else ""
reportonly = args.reportonly
updatemessage = " to be updated: " if reportonly else " updated: "
params = args.paramsobj
precision = font.paramset["precision"]
# Increase screen logging level to W unless specific level supplied on command-line
if not(args.quiet or "scrlevel" in params.sets["command line"]) : logger.scrlevel = "W"
# Process UFO name
(path,base) = os.path.split(font.ufodir)
(base,ext) = os.path.splitext(base)
if '-' not in base : logger.log("Non-standard UFO name - must be <family>-<style>", "S")
(family,style) = base.split('-')
styles = [style]
fonts = {}
fonts[style] = font
# Process single and master settings
if singlefont :
if mfont :
mastertext = "Master" # Used in log messages
else : # Check against Regular font from same family
mfont = openfont(params, path, family, "Regular")
if mfont is None : logger.log("No regular font to check against - use -m to specify master font", "S")
mastertext = "Regular"
fonts["Regular"] =mfont
else : # Supplied font must be Regular
if mfont : logger.log("-m --master must only be used with -s --single", "S")
if style != "Regular" : logger.log("Must specify a Regular font unless -s is used", "S")
mastertext = "Regular"
mfont = font
# Check for required fields in master font
mfinfo = mfont.fontinfo
if "familyName" in mfinfo :
spacedfamily = mfinfo["familyName"][1].text
else:
logger.log("No familyName field in " + mastertext, "S")
if "openTypeNameManufacturer" in mfinfo :
manufacturer = mfinfo["openTypeNameManufacturer"][1].text
else:
logger.log("No openTypeNameManufacturer field in " + mastertext, "S")
mlib = mfont.lib
# Open the remaining fonts in the family
if not singlefont :
for style in standardstyles :
if not style in fonts :
fonts[style] = openfont(params, path, family, style) # Will return None if font does not exist
if fonts[style] is not None : styles.append(style)
# Process fonts
psuniqueidlist = []
fieldscopied = False
for style in styles :
font = fonts[style]
if font.UFOversion != "2" : logger.log("This script only works with UFO 2 format fonts","S")
fontname = family + "-" + style
spacedstyle = "Bold Italic" if style == "BoldItalic" else style
spacedname = spacedfamily + " " + spacedstyle
logger.log("************ Processing " + fontname, "P")
ital = True if "Italic" in style else False
bold = True if "Bold" in style else False
# Process fontinfo.plist
finfo=font.fontinfo
fieldlist = list(set(finfo) | set(mfinfo)) # Need all fields from both to detect missing fields
fchanged = False
for field in fieldlist:
action = None; issue = ""; newval = ""
if field in finfo :
elem = finfo[field][1]
tag = elem.tag
text = elem.text
if text is None : text = ""
if tag == "real" : text = processnum(text,precision)
# Field-specific actions
if field not in finfo :
if field not in finfoignore : action = "Copyfield"
elif field == "italicAngle" :
if ital and text == "0" :
issue = "is zero"
action = "Warn"
if not ital and text != "0" :
issue = "is non-zero"
newval = 0
action = "Update"
elif field == "openTypeNameUniqueID" :
newval = manufacturer + ": " + spacedname + ": " + datetime.now().strftime("%Y")
if text != newval :
issue = "Incorrect value"
action = "Update"
elif field == "openTypeOS2WeightClass" :
if bold and text != "700" :
issue = "is not 700"
newval = 700
action = "Update"
if not bold and text != "400" :
issue = "is not 400"
newval = 400
action = "Update"
elif field == "postscriptFontName" :
if text != fontname :
newval = fontname
issue = "Incorrect value"
action = "Update"
elif field == "postscriptFullName" :
if text != spacedname :
newval = spacedname
issue = "Incorrect value"
action = "Update"
elif field == "postscriptUniqueID" :
if text in psuniqueidlist :
issue = "has same value as another font: " + text
action = "Warn"
else :
psuniqueidlist.append(text)
elif field == "postscriptWeightName" :
newval = 'bold' if bold else 'regular'
if text != newval :
issue = "Incorrect value"
action = 'Update'
elif field == "styleMapStyleName" :
if text != spacedstyle.lower() :
newval = spacedstyle.lower()
issue = "Incorrect value"
action = "Update"
elif field in ("styleName", "openTypeNamePreferredSubfamilyName") :
if text != spacedstyle :
newval = spacedstyle
issue = "Incorrect value"
action = "Update"
elif field in finfoignore :
action = "Ignore"
# Warn for fields in this font but not master
elif field not in mfinfo :
issue = "is in " + spacedstyle + " but not in " + mastertext
action = "Warn"
# for all other fields, sync values from master
else :
melem = mfinfo[field][1]
mtag = melem.tag
mtext = melem.text
if mtext is None : mtext = ""
if mtag == 'real' : mtext = processnum(mtext,precision)
if tag in ("real", "integer", "string") :
if mtext != text :
issue = "does not match " + mastertext + " value"
newval = mtext
action = "Update"
elif tag in ("true, false") :
if tag != mtag :
issue = "does not match " + mastertext + " value"
action = "FlipBoolean"
elif tag == "array" : # Assume simple array with just values to compare
marray = mfinfo.getval(field)
array = finfo.getval(field)
if array != marray: action = "CopyArray"
else : logger.log("Non-standard fontinfo field type in " + fontname, "X")
# Now process the actions, create log messages etc
if action is None or action == "Ignore" :
pass
elif action == "Warn" :
logger.log(field + " needs manual correction: " + issue, "W")
elif action == "Error" :
logger.log(field + " needs manual correction: " + issue, "E")
elif action in ("Update", "FlipBoolean", "Copyfield", "CopyArray") : # Updating actions
fchanged = True
message = field + updatemessage
if action == "Update" :
message = message + issue + " Old: '" + text + "' New: '" + str(newval) + "'"
elem.text = newval
elif action == "FlipBoolean" :
newval = "true" if tag == "false" else "false"
message = message + issue + " Old: '" + tag + "' New: '" + newval + "'"
finfo.setelem(field, ET.fromstring("<" + newval + "/>"))
elif action == "Copyfield" :
message = message + "is missing so will be copied from " + mastertext
fieldscopied = True
finfo.addelem(field, ET.fromstring(ET.tostring(mfinfo[field][1])))
elif action == "CopyArray" :
message = message + "Some values different Old: " + str(array) + " New: " + str(marray)
finfo.setelem(field, ET.fromstring(ET.tostring(melem)))
logger.log(message, "W")
else:
logger.log("Uncoded action: " + action + " - oops", "X")
# Process lib.plist - currently just public.postscriptNames and glyph order fields which are all simple dicts or arrays
lib = font.lib
lchanged = False
for field in libfields:
# Check the values
action = None; issue = ""; newval = ""
if field in mlib:
if field in lib:
if lib.getval(field) != mlib.getval(field): # will only work for arrays or dicts with simple values
action = "Updatefield"
else:
action = "Copyfield"
else:
action = "Error" if field == ("public.GlyphOrder", "public.postscriptNames") else "Warn"
issue = field + " not in " + mastertext + " lib.plist"
# Process the actions, create log messages etc
if action is None or action == "Ignore":
pass
elif action == "Warn":
logger.log(field + " needs manual correction: " + issue, "W")
elif action == "Error":
logger.log(field + " needs manual correction: " + issue, "E")
elif action in ("Updatefield", "Copyfield"): # Updating actions
lchanged = True
message = field + updatemessage
if action == "Copyfield":
message = message + "is missing so will be copied from " + mastertext
lib.addelem(field, ET.fromstring(ET.tostring(mlib[field][1])))
elif action == "Updatefield":
message = message + "Some values different"
lib.setelem(field, ET.fromstring(ET.tostring(mlib[field][1])))
logger.log(message, "W")
else:
logger.log("Uncoded action: " + action + " - oops", "X")
# Now update on disk
if not reportonly:
if args.normalize:
font.write(os.path.join(path, family + "-" + style + newfile + ".ufo"))
else: # Just update fontinfo and lib
if fchanged:
filen = "fontinfo" + newfile + ".plist"
logger.log("Writing updated fontinfo to " + filen, "P")
exists = True if os.path.isfile(os.path.join(font.ufodir, filen)) else False
UFO.writeXMLobject(finfo, font.outparams, font.ufodir, filen, exists, fobject=True)
if lchanged:
filen = "lib" + newfile + ".plist"
logger.log("Writing updated lib.plist to " + filen, "P")
exists = True if os.path.isfile(os.path.join(font.ufodir, filen)) else False
UFO.writeXMLobject(lib, font.outparams, font.ufodir, filen, exists, fobject=True)
if fieldscopied :
message = "After updating, UFOsyncMeta will need to be re-run to validate these fields" if reportonly else "Re-run UFOsyncMeta to validate these fields"
logger.log("*** Some fields were missing and so copied from " + mastertext + ". " + message, "P")
return
def openfont(params, path, family, style) : # Only try if directory exists
ufodir = os.path.join(path,family+"-"+style+".ufo")
font = UFO.Ufont(ufodir, params=params) if os.path.isdir(ufodir) else None
return font
def processnum(text, precision) : # Apply same processing to numbers that normalization will
if precision is not None:
val = round(float(text), precision)
if val == int(val) : val = int(val) # Removed trailing decimal .0
text = str(val)
return text
def cmd() : execute("UFO",doit, argspec)
if __name__ == "__main__": cmd()
|