#!/usr/bin/env python3 'General classes and functions for use in pysilfont scripts' __url__ = 'https://github.com/silnrsi/pysilfont' __copyright__ = 'Copyright (c) 2014 SIL International (https://www.sil.org)' __license__ = 'Released under the MIT License (https://opensource.org/licenses/MIT)' __author__ = 'David Raymond' import os, subprocess, difflib, sys, io, json from silfont.core import execute from pkg_resources import resource_filename from csv import reader as csvreader try: from fontTools.ttLib import TTFont except Exception as e: TTFont = None class dirTree(dict) : """ An object to hold list of all files and directories in a directory with option to read sub-directory contents into dirTree objects. Iterates through readSub levels of subfolders Flags to keep track of changes to files etc""" def __init__(self,dirn,readSub = 9999) : self.removedfiles = {} # List of files that have been renamed or deleted since reading from disk for name in os.listdir(dirn) : if name[-1:] == "~" : continue item=dirTreeItem() if os.path.isdir(os.path.join(dirn, name)) : item.type = "d" if readSub : item.dirtree = dirTree(os.path.join(dirn,name),readSub-1) self[name] = item def subTree(self,path) : # Returns dirTree object for a subtree based on subfolder name(s) # 'path' can be supplied as either a relative path (eg "subf/subsubf") or array (eg ['subf','subsubf'] if type(path) in (bytes, str): path = self._split(path) subf=path[0] if subf in self: dtree = self[subf].dirtree else : return None if len(path) == 1 : return dtree else : path.pop(0) return dtree.subTree(path) def _split(self,path) : # Turn a relative path into an array of subfolders npath = [os.path.split(path)[1]] while os.path.split(path)[0] : path = os.path.split(path)[0] npath.insert(0,os.path.split(path)[1]) return npath class dirTreeItem(object) : def __init__(self, type = "f", dirtree = None, read = False, added = False, changed = False, towrite = False, written = False, fileObject = None, fileType = None, flags = {}) : self.type = type # "d" or "f" self.dirtree = dirtree # dirtree for a sub-directory # Remaining properties are for calling scripts to use as they choose to track actions etc self.read = read # Item has been read by the script self.added = added # Item has been added to dirtree, so does not exist on disk self.changed = changed # Item has been changed, so may need updating on disk self.towrite = towrite # Item should be written out to disk self.written = written # Item has been written to disk self.fileObject = fileObject # An object representing the file self.fileType = fileType # The type of the file object self.flags = {} # Any other flags a script might need def setinfo(self, read = None, added = None, changed = None, towrite = None, written = None, fileObject = None, fileType = None, flags = None) : pass if read : self.read = read if added : self.added = added if changed : self.changed = changed if towrite: self.towrite = towrite if written : self.written = written if fileObject is not None : self.fileObject = fileObject if fileType : self.fileType = fileType if flags : self.flags = flags class ufo_diff(object): # For diffing 2 ufos as part of testing # returncodes: # 0 - ufos are the same # 1 - Differences were found # 2 - Errors running the difference (eg can't open file) # diff - text of the differences # errors - text of the errors def __init__(self, ufo1, ufo2, ignoreOHCtime=True): diffcommand = ["diff", "-r", "-c1", ufo1, ufo2] # By default, if only difference in fontinfo is the openTypeHeadCreated timestamp ignore that if ignoreOHCtime: # Exclude fontinfo if only diff is openTypeHeadCreated # Otherwise leave it in so differences are reported by main diff fi1 = os.path.join(ufo1,"fontinfo.plist") fi2 = os.path.join(ufo2, "fontinfo.plist") fitest = subprocess.Popen(["diff", fi1, fi2, "-c1"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) text = fitest.communicate() if fitest.returncode == 1: difftext = text[0].decode("utf-8").split("\n") if difftext[4].strip() == "openTypeHeadCreated" and len(difftext) == 12: diffcommand.append("--exclude=fontinfo.plist") # Now do the main diff test = subprocess.Popen(diffcommand, stdout=subprocess.PIPE, stderr=subprocess.PIPE) text = test.communicate() self.returncode = test.returncode self.diff = text[0].decode("utf-8") self.errors = text[1] def print_text(self): # Print diff info or errors from the diffcommand if self.returncode == 0: print("UFOs are the same") elif self.returncode == 1: print("UFOs are different") print(self.diff) elif self.returncode == 2: print("Failed to compare UFOs") print(self.errors) class text_diff(object): # For diffing 2 text files with option to ignore common timestamps # See ufo_diff for class attribute details def __init__(self, file1, file2, ignore_chars=0, ignore_firstlinechars = 0): # ignore_chars - characters to ignore from left of each line; typically 20 for timestamps # ignore_firstlinechars - as above, but just for first line, eg for initial comment in csv files, typically 22 errors = [] try: f1 = [x[ignore_chars:-1].replace('\\','/') for x in io.open(file1, "r", encoding="utf-8").readlines()] except IOError: errors.append("Can't open " + file1) try: f2 = [x[ignore_chars:-1].replace('\\','/') for x in io.open(file2, "r", encoding="utf-8").readlines()] except IOError: errors.append("Can't open " + file2) if errors == []: # Indicates both files were opened OK if ignore_firstlinechars: # Ignore first line for files with first line comment with timestamp f1[0] = f1[0][ignore_firstlinechars:-1] f2[0] = f2[0][ignore_firstlinechars:-1] self.errors = "" self.diff = "\n".join([x for x in difflib.unified_diff(f1, f2, file1, file2, n=0)]) self.returncode = 0 if self.diff == "" else 1 else: self.diff = "" self.errors = "\n".join(errors) self.returncode = 2 def print_text(self): # Print diff info or errors the unified_diff command if self.returncode == 0: print("Files are the same") elif self.returncode == 1: print("Files are different") print(self.diff) elif self.returncode == 2: print("Failed to compare Files") print(self.errors) class ttf_diff(object): # For diffing 2 ttf files. Differences are not listed # See ufo_diff for class attribute details def __init__(self, file1, file2): errors=[] if TTFont is None: self.diff="" self.errors="Testing failed - class ttf_diff requires fontTools to be installed" self.returncode = 2 return # Open the ttf files try: font1 = TTFont(file1) except Exception as e: errors.append("Can't open " + file1) errors.append(e.__str__()) try: font2 = TTFont(file2) except Exception as e: errors.append("Can't open " + file2) errors.append(e.__str__()) if errors: self.diff = "" self.errors = "\n".join(errors) self.returncode = 2 return # Create ttx xml strings from each font ttx1 = _ttx() ttx2 = _ttx() font1.saveXML(ttx1) font2.saveXML(ttx2) if ttx1.txt() == ttx2.txt(): self.diff = "" self.errors = "" self.returncode = 0 else: self.diff = file1 + " and " + file2 + " are different - compare with external tools" self.errors = "" self.returncode = 1 def print_text(self): # Print diff info or errors the unified_diff command if self.returncode == 0: print("Files are the same") elif self.returncode == 1: print("Files are different") print(self.diff) elif self.returncode == 2: print("Failed to compare Files") print(self.errors) def test_run(tool, commandline, testcommand, outfont, exp_errors, exp_warnings): # Used by tests to run commands sys.argv = commandline.split(" ") (args, font) = execute(tool, testcommand.doit, testcommand.argspec, chain="first") if outfont: if tool in ("FT", "FP"): font.save(outfont) else: # Must be Pyslifont Ufont font.write(outfont) args.logger.logfile.close() # Need to close the log so that the diff test can be run exp_counts = (exp_errors, exp_warnings) actual_counts = (args.logger.errorcount, args.logger.warningcount) result = exp_counts == actual_counts if not result: print("Mis-match of logger errors/warnings: " + str(exp_counts) + " vs " + str(actual_counts)) return result def test_diffs(dirname, testname, extensions): # Used by test to run diffs on results files based on extensions result = True for ext in extensions: resultfile = os.path.join("local/testresults", dirname, testname + ext) referencefile = os.path.join("tests/reference", dirname, testname + ext) if ext == ".ufo": diff = ufo_diff(resultfile, referencefile) elif ext == ".csv": diff = text_diff(resultfile, referencefile, ignore_firstlinechars=22) elif ext in (".log", ".lg"): diff = text_diff(resultfile, referencefile, ignore_chars=20) elif ext == ".ttf": diff = ttf_diff(resultfile, referencefile) else: diff = text_diff(resultfile, referencefile) if diff.returncode: diff.print_text() result = False return result class _ttx(object): # Used by ttf_diff() def __init__(self): self.lines = [] def write(self, line): if not(" 1: errormess = "RGBA values must be between 0 and 1" if values[0] + values[1] + values[2] == 0: errormess = "At lease one RGB value must be non-zero" if values[3] == 0: errormess = "With RGBA, A must not be zero" if errormess == "": for i in (0, 1, 2, 3): v = values[i] if v == int(v): v = int(v) # Convert integers to int type for correct formatting with str() RGBA += str(v) + "," RGBA = RGBA[0:-1] # Strip trialing comma name = colortoname(RGBA, "") else: name = splitcol RGBA = nametocolor(name) if RGBA is None: errormess = "Invalid color name" if errormess: logcolor = "Invalid color: " + splitcol + " - " + errormess RGBA = None name = None else: logcolor = RGBA if name: logcolor += " (" + name + ")" parsed.append((RGBA, name, logcolor,splitcol)) if single: parsed = parsed[0] return parsed # Provide dict of required characters which match the supplied list of sets - sets can be basic, rtl or sil def required_chars(sets="basic"): if type(sets) == str: sets = (sets,) # Convert single string to a tuple rcfile = open(resource_filename('silfont','data/required_chars.csv')) rcreader = csvreader(rcfile) next(rcreader) # Read fist line which is headers rcdict = {} for line in rcreader: unicode = line[0][2:] item = { "ps_name": line[1], "glyph_name": line[2], "sil_set": line[3], "rationale": line[4], "notes": line[5] } if item["sil_set"] in sets: rcdict[unicode] = item return rcdict # Pretty print routine for json files. def prettyjson(data, oneliners=None, indent="", inkey=None, oneline=False): # data - json data to format # oneliners - lists of keys for which data should be output on a single line # indent, inkey & oneline - used when prettyjson calls itself interactively for sub-values that are dicts res = ["{"] thisoneline = oneline and (oneliners is None or inkey not in oneliners) for key, value in sorted(data.items()): line = ("" if thisoneline else indent) + '"{}": '.format(key) if isinstance(value, dict): val = prettyjson(value, oneliners=oneliners, indent = indent + " ", inkey = key, oneline=(oneline if oneliners is None or key not in oneliners else True)) else: val = json.dumps(value, ensure_ascii=False) res.append(line + val + ",") res[-1] = res[-1][:-1] res.append(("" if thisoneline else indent[:-4]) + "}") return (" " if thisoneline else "\n").join(res)