diff options
Diffstat (limited to 'src/silfont/scripts/psfsyncmeta.py')
-rw-r--r-- | src/silfont/scripts/psfsyncmeta.py | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/src/silfont/scripts/psfsyncmeta.py b/src/silfont/scripts/psfsyncmeta.py new file mode 100644 index 0000000..5366bc5 --- /dev/null +++ b/src/silfont/scripts/psfsyncmeta.py @@ -0,0 +1,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() |