summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfsyncmasters.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/silfont/scripts/psfsyncmasters.py')
-rw-r--r--lib/silfont/scripts/psfsyncmasters.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/lib/silfont/scripts/psfsyncmasters.py b/lib/silfont/scripts/psfsyncmasters.py
new file mode 100644
index 0000000..4302d50
--- /dev/null
+++ b/lib/silfont/scripts/psfsyncmasters.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python
+__doc__ = '''Sync metadata across a family of fonts based on designspace files'''
+__url__ = 'http://github.com/silnrsi/pysilfont'
+__copyright__ = 'Copyright (c) 2018 SIL International (http://www.sil.org)'
+__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
+__author__ = 'David Raymond'
+
+from silfont.core import execute
+import silfont.ufo as UFO
+import silfont.etutil as ETU
+import os, datetime
+import fontTools.designspaceLib as DSD
+from xml.etree import ElementTree as ET
+
+argspec = [
+ ('primaryds', {'help': 'Primary design space file'}, {'type': 'filename'}),
+ ('secondds', {'help': 'Second design space file', 'nargs': '?', 'default': None}, {'type': 'filename', 'def': None}),
+ ('--complex', {'help': 'Indicates complex set of fonts rather than RIBBI', 'action': 'store_true', 'default': False},{}),
+ ('-l','--log', {'help': 'Log file'}, {'type': 'outfile', 'def': '_sync.log'}),
+ ('-n','--new', {'help': 'append "_new" to file names', 'action': 'store_true', 'default': False},{}) # For testing/debugging
+ ]
+
+def doit(args) :
+ ficopyreq = ("ascender", "copyright", "descender", "familyName", "openTypeHheaAscender",
+ "openTypeHheaDescender", "openTypeHheaLineGap", "openTypeNameDescription", "openTypeNameDesigner",
+ "openTypeNameDesignerURL", "openTypeNameLicense", "openTypeNameLicenseURL",
+ "openTypeNameManufacturer", "openTypeNameManufacturerURL", "openTypeNamePreferredFamilyName",
+ "openTypeNameVersion", "openTypeOS2CodePageRanges", "openTypeOS2TypoAscender",
+ "openTypeOS2TypoDescender", "openTypeOS2TypoLineGap", "openTypeOS2UnicodeRanges",
+ "openTypeOS2VendorID", "openTypeOS2WinAscent", "openTypeOS2WinDescent", "versionMajor",
+ "versionMinor")
+ ficopyopt = ("openTypeNameSampleText", "postscriptFamilyBlues", "postscriptFamilyOtherBlues", "trademark",
+ "woffMetadataCredits", "woffMetadataDescription")
+ fispecial = ("italicAngle", "openTypeOS2WeightClass", "openTypeNamePreferredSubfamilyName", "openTypeNameUniqueID",
+ "styleMapFamilyName", "styleMapStyleName", "styleName", "unitsPerEm")
+ fiall = sorted(set(ficopyreq) | set(ficopyopt) | set(fispecial))
+ required = ficopyreq + ("openTypeOS2WeightClass", "styleName", "unitsPerEm")
+ libcopy = ("com.schriftgestaltung.glyphOrder", "public.glyphOrder", "public.postscriptNames")
+ logger = args.logger
+ complex = args.complex
+
+ pds = DSD.DesignSpaceDocument()
+ pds.read(args.primaryds)
+ if args.secondds is not None:
+ sds = DSD.DesignSpaceDocument()
+ sds.read(args.secondds)
+ else:
+ sds = None
+
+ # Process all the sources
+ psource = None
+ dsources = []
+ for source in pds.sources:
+ if source.copyInfo:
+ if psource: logger.log('Multiple fonts with <info copy="1" />', "S")
+ psource = Dsource(pds, source, logger, frompds=True, psource = True, args = args)
+ else:
+ dsources.append(Dsource(pds, source, logger, frompds=True, psource = False, args = args))
+ if sds is not None:
+ for source in sds.sources:
+ dsources.append(Dsource(sds, source, logger, frompds=False, psource = False, args=args))
+
+ # Process values in psource
+ fipval = {}
+ libpval = {}
+ changes = False
+ reqmissing = False
+
+ for field in fiall:
+ pval = psource.fontinfo.getval(field) if field in psource.fontinfo else None
+ oval = pval
+ # Set values or do other checks for special cases
+ if field == "italicAngle":
+ if "italic" in psource.source.filename.lower():
+ if pval is None or pval == 0 :
+ logger.log("Primary font: Italic angle must be non-zero for italic fonts", "E")
+ else:
+ if pval is not None and pval != 0 :
+ logger.log("Primary font: Italic angle must be zero for non-italic fonts", "E")
+ pval = None
+ elif field == "openTypeOS2WeightClass":
+ pval = int(psource.source.location["weight"])
+ elif field == "styleMapFamilyName":
+ if not complex and pval is None: logger.log("styleMapFamilyName missing from primary font", "E")
+ elif field == "styleMapStyleName":
+ if not complex and pval not in ('regular', 'bold', 'italic', 'bold italic'):
+ logger.log("styleMapStyleName must be 'regular', 'bold', 'italic', 'bold italic'", "E")
+ elif field in ("styleName", "openTypeNamePreferredSubfamilyName"):
+ pval = psource.source.styleName
+ elif field == "openTypeNameUniqueID":
+ nm = str(fipval["openTypeNameManufacturer"]) # Need to wrap with str() just in case missing from
+ fn = str(fipval["familyName"]) # fontinfo so would have been set to None
+ sn = psource.source.styleName
+ pval = nm + ": " + fn + " " + sn + ": " + datetime.datetime.now().strftime("%Y")
+ elif field == "unitsperem":
+ if pval is None or pval <= 0: logger.log("unitsperem must be non-zero", "S")
+ # After processing special cases, all required fields should have values
+ if pval is None and field in required:
+ reqmissing = True
+ logger.log("Required fontinfo field " + field + " missing from " + psource.source.filename, "E")
+ elif oval != pval:
+ changes = True
+ if pval is None:
+ if field in psource.fontinfo: psource.fontinfo.remove(field)
+ else:
+ psource.fontinfo[field][1].text = str(pval)
+ logchange(logger, "Primary font: " + field + " updated:", oval, pval)
+ fipval[field] = pval
+ if reqmissing: logger.log("Required fontinfo fields missing from " + psource.source.filename, "S")
+ if changes:
+ psource.fontinfo.setval("openTypeHeadCreated", "string",
+ datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
+ psource.write("fontinfo")
+
+ for field in libcopy:
+ pval = psource.lib.getval(field) if field in psource.lib else None
+ if pval is None:
+ logtype = "W" if field[0:7] == "public." else "I"
+ logger.log("lib.plist field " + field + " missing from " + psource.source.filename, logtype)
+ libpval[field] = pval
+
+ # Now update values in other source fonts
+
+ for dsource in dsources:
+ logger.log("Processing " + dsource.ufodir, "I")
+ fchanges = False
+ for field in fiall:
+ sval = dsource.fontinfo.getval(field) if field in dsource.fontinfo else None
+ oval = sval
+ pval = fipval[field]
+ # Set values or do other checks for special cases
+ if field == "italicAngle":
+ if "italic" in dsource.source.filename.lower():
+ if sval is None or sval == 0:
+ logger.log(dsource.source.filename + ": Italic angle must be non-zero for italic fonts", "E")
+ else:
+ if sval is not None and sval != 0:
+ logger.log(dsource.source.filename + ": Italic angle must be zero for non-italic fonts", "E")
+ sval = None
+ elif field == "openTypeOS2WeightClass":
+ sval = int(dsource.source.location["weight"])
+ elif field == "styleMapStyleName":
+ if not complex and sval not in ('regular', 'bold', 'italic', 'bold italic'):
+ logger.log(dsource.source.filename + ": styleMapStyleName must be 'regular', 'bold', 'italic', 'bold italic'", "E")
+ elif field in ("styleName", "openTypeNamePreferredSubfamilyName"):
+ sval = dsource.source.styleName
+ elif field == "openTypeNameUniqueID":
+ sn = dsource.source.styleName
+ sval = nm + ": " + fn + " " + sn + ": " + datetime.datetime.now().strftime("%Y")
+ else:
+ sval = pval
+ if oval != sval:
+ if field == "unitsPerEm": logger.log("unitsPerEm inconsistent across fonts", "S")
+ fchanges = True
+ if sval is None:
+ dsource.fontinfo.remove(field)
+ logmess = " removed: "
+ else:
+ logmess = " added: " if oval is None else " updated: "
+ # Copy value from primary. This will add if missing.
+ dsource.fontinfo.setelem(field, ET.fromstring(ET.tostring(psource.fontinfo[field][1])))
+ # For fields where it is not a copy from primary...
+ if field in ("italicAngle", "openTypeNamePreferredSubfamilyName", "openTypeNameUniqueID",
+ "openTypeOS2WeightClass", "styleName"):
+ dsource.fontinfo[field][1].text = str(sval)
+
+ logchange(logger, dsource.source.filename + " " + field + logmess, oval, sval)
+
+ lchanges = False
+ for field in libcopy:
+ oval = dsource.lib.getval(field) if field in dsource.lib else None
+ pval = libpval[field]
+ if oval != pval:
+ lchanges = True
+ if pval is None:
+ dsource.lib.remove(field)
+ logmess = " removed: "
+ else:
+ dsource.lib.setelem(field, ET.fromstring(ET.tostring(psource.lib[field][1])))
+ logmess = " updated: "
+ logchange(logger, dsource.source.filename + " " + field + logmess, oval, pval)
+
+ if lchanges:
+ dsource.write("lib")
+ fchanges = True # Force fontinfo to update so openTypeHeadCreated is set
+ if fchanges:
+ dsource.fontinfo.setval("openTypeHeadCreated", "string",
+ datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
+ dsource.write("fontinfo")
+
+ logger.log("psfsyncmasters completed", "P")
+
+class Dsource(object):
+ def __init__(self, ds, source, logger, frompds, psource, args):
+ self.ds = ds
+ self.source = source
+ self.logger = logger
+ self.frompds = frompds # Boolean to say if came from pds
+ self.newfile = "_new" if args.new else ""
+ self.ufodir = source.path
+ if not os.path.isdir(self.ufodir): logger.log(self.ufodir + " in designspace doc does not exist", "S")
+ try:
+ self.fontinfo = UFO.Uplist(font=None, dirn=self.ufodir, filen="fontinfo.plist")
+ except Exception as e:
+ logger.log("Unable to open fontinfo.plist in " + self.ufodir, "S")
+ try:
+ self.lib = UFO.Uplist(font=None, dirn=self.ufodir, filen="lib.plist")
+ except Exception as e:
+ if psource:
+ logger.log("Unable to open lib.plist in " + self.ufodir, "E")
+ self.lib = {} # Just need empty dict, so all vals will be set to None
+ else:
+ logger.log("Unable to open lib.plist in " + self.ufodir + "; creating empty one", "E")
+ self.lib = UFO.Uplist()
+ self.lib.logger=logger
+ self.lib.etree = ET.fromstring("<plist>\n<dict/>\n</plist>")
+ self.lib.populate_dict()
+ self.lib.dirn = self.ufodir
+ self.lib.filen = "lib.plist"
+
+ # Process parameters with similar logic to that in ufo.py. primarily to create outparams for writeXMLobject
+ libparams = {}
+ params = args.paramsobj
+ if "org.sil.pysilfontparams" in self.lib:
+ elem = self.lib["org.sil.pysilfontparams"][1]
+ if elem.tag != "array":
+ logger.log("Invalid parameter XML lib.plist - org.sil.pysilfontparams must be an array", "S")
+ for param in elem:
+ parn = param.tag
+ if not (parn in params.paramclass) or params.paramclass[parn] not in ("outparams", "ufometadata"):
+ logger.log(
+ "lib.plist org.sil.pysilfontparams must only contain outparams or ufometadata values: " + parn + " invalid",
+ "S")
+ libparams[parn] = param.text
+ # Create font-specific parameter set (with updates from lib.plist) Prepend names with ufodir to ensure uniqueness if multiple fonts open
+ params.addset(self.ufodir + "lib", "lib.plist in " + self.ufodir, inputdict=libparams)
+ if "command line" in params.sets:
+ params.sets[self.ufodir + "lib"].updatewith("command line", log=False) # Command line parameters override lib.plist ones
+ copyset = "main" if "main" in params.sets else "default"
+ params.addset(self.ufodir, copyset=copyset)
+ params.sets[self.ufodir].updatewith(self.ufodir + "lib", sourcedesc="lib.plist")
+ self.paramset = params.sets[self.ufodir]
+ # Validate specific parameters
+ if sorted(self.paramset["glifElemOrder"]) != sorted(params.sets["default"]["glifElemOrder"]):
+ logger.log("Invalid values for glifElemOrder", "S")
+ # Create outparams based on values in paramset, building attriborders from separate attriborders.<type> parameters.
+ self.outparams = {"attribOrders": {}}
+ for parn in params.classes["outparams"]:
+ value = self.paramset[parn]
+ if parn[0:12] == 'attribOrders':
+ elemname = parn.split(".")[1]
+ self.outparams["attribOrders"][elemname] = ETU.makeAttribOrder(value)
+ else:
+ self.outparams[parn] = value
+ self.outparams["UFOversion"] = 9 # Dummy value since not currently needed
+
+ def write(self, plistn):
+ filen = plistn + self.newfile + ".plist"
+ self.logger.log("Writing updated " + plistn + ".plist to " + filen, "P")
+ exists = True if os.path.isfile(os.path.join(self.ufodir, filen)) else False
+ plist = getattr(self, plistn)
+ UFO.writeXMLobject(plist, self.outparams, self.ufodir, filen, exists, fobject=True)
+
+
+def logchange(logger, logmess, old, new):
+ oldstr = str(old) if len(str(old)) < 22 else str(old)[0:20] + "..."
+ newstr = str(new) if len(str(new)) < 22 else str(new)[0:20] + "..."
+ if old is None:
+ logmess = logmess + " New value: " + newstr
+ else:
+ if new is None:
+ logmess = logmess + " Old value: " + oldstr
+ else:
+ logmess = logmess + " Old value: " + oldstr + ", new value: " + newstr
+ logger.log(logmess, "W")
+ # Extra verbose logging
+ if len(str(old)) > 21 :
+ logger.log("Full old value: " + str(old), "V")
+ if len(str(new)) > 21 :
+ logger.log("Full new value: " + str(new), "V")
+ logger.log("Types: Old - " + str(type(old)) + ", New - " + str(type(new)), "V")
+
+
+def cmd() : execute(None,doit, argspec)
+if __name__ == "__main__": cmd()
+
+
+''' *** Code notes ***
+
+Does not check precision for float, since no float values are currently processed
+ - see processnum in psfsyncmeta if needed later
+
+'''