#!/usr/bin/env python # -*- coding: utf-8 -*- # ------------------------------------------------------------------------------ # otmod.py # Copyright 2015 Christopher Simpkins # MIT license # ------------------------------------------------------------------------------ import sys import os.path import codecs import unicodedata from fontTools import ttLib from yaml import load try: from yaml import CLoader as Loader except ImportError: from yaml import Loader # ------------------------------------------------------------------------------ # [ Argument Class ] # all command line arguments (object inherited from Python list) # ------------------------------------------------------------------------------ class Argument(list): """Argument class is a list for command line arguments. It provides methods for positional argument parsing""" def __init__(self, argv): self.argv = argv list.__init__(self, self.argv) # return argument at position specified by the 'position' parameter def get_arg(self, position): if self.argv and (len(self.argv) > position): return self.argv[position] else: return "" # return position of user specified argument in the argument list def get_arg_position(self, test_arg): if self.argv: if test_arg in self.argv: return self.argv.index(test_arg) else: return -1 # TODO: change the return code that indicates an error # return the argument at the next position following a user specified positional argument # (e.g. for argument to an option in cmd) def get_arg_next(self, position): if len(self.argv) > (position + 1): return self.argv[position + 1] else: return "" def generate_outfile_path(filepath): filepath_list = os.path.split(filepath) directory_path = filepath_list[0] basefile_path = filepath_list[1] basefile_list = basefile_path.split(".") new_basefile_name = basefile_list[0] + "-new." + basefile_list[1] outfile = os.path.join(directory_path, new_basefile_name) return outfile def read_utf8(filepath): """read_utf8() is a function that reads text in as UTF-8 NFKD normalized text strings from filepath :param filepath: the filepath to the text input file """ try: f = codecs.open(filepath, encoding='utf_8', mode='r') except IOError as ioe: sys.stderr.write("[otmod.py] ERROR: Unable to open '" + filepath + "' for read.\n") raise ioe try: textstring = f.read() norm_text = unicodedata.normalize('NFKD', textstring) # NKFD normalization of the unicode data before returns return norm_text except Exception as e: sys.stderr.write("[otmod.py] ERROR: Unable to read " + filepath + " with UTF-8 encoding using the read_utf8() method.\n") raise e finally: f.close() def main(arguments): args = Argument(arguments) # Command line syntax + parsing # LONG OPTIONS: otmod.py --in --opentype --out --quiet # SHORT OPTIONS: otmod.py -i -t -o -q # Quiet flag (default to False, if set to True does not print changes that occurred to std output) quiet = False if "--quiet" in args.argv or "-q" in args.argv: quiet = True # font infile path if "--in" in args.argv: infile = args.get_arg_next(args.get_arg_position("--in")) if infile is "": sys.stderr.write("[otmod.py] ERROR: please define the font input file path as an argument to the --in command line option.\n") sys.exit(1) elif "-i" in args.argv: infile = args.get_arg_next(args.get_arg_position("-i")) if infile is "": sys.stderr.write("[otmod.py] ERROR: please define the font input file path as an argument to the -i command line option.\n") sys.exit(1) else: sys.stderr.write("[otmod.py] ERROR: Please include the `--in` option with an input font file defined as an argument.\n") sys.exit(1) # OpenType change YAML file path if "--opentype" in args.argv: otpath = args.get_arg_next(args.get_arg_position("--opentype")) if otpath is "": sys.stderr.write("[otmod.py] ERROR: please define the YAML OpenType changes file path as an argument to the --opentype command line option.\n") sys.exit(1) elif "-t" in args.argv: otpath = args.get_arg_next(args.get_arg_position("-t")) if otpath is "": sys.stderr.write("[otmod.py] ERROR: please define the YAML OpenType changes file path as an argument to the -t command line option.\n") sys.exit(1) else: sys.stderr.write("[otmod.py] ERROR: Please include the `--opentype` option and define it with an path argument to the YAML formatted OpenType changes file.\n") sys.exit(1) # font outfile path (allows for font name change in outfile) if "--out" in args.argv: outfile = args.get_arg_next(args.get_arg_position("--out")) if outfile is "": outfile = generate_outfile_path(infile) elif "-o" in args.argv: outfile = args.get_arg_next(args.get_arg_position("-o")) if outfile is "": outfile = generate_outfile_path(infile) else: outfile = generate_outfile_path(infile) # Test for existing file paths if not os.path.isfile(infile): sys.stderr.write("[otmod.py] ERROR: Unable to locate font at the infile path '" + infile + "'.\n") sys.exit(1) if not os.path.isfile(otpath): sys.stderr.write("[otmod.py] ERROR: Unable to locate the OpenType modification settings YAML file at '" + otpath + "'.\n") sys.exit(1) # Read YAML OT table changes settings file and convert to Python object try: yaml_text = read_utf8(otpath) # Python dictionary definitions with structure `otmods_obj['OS/2']['sTypoLineGap']` otmods_obj = load(yaml_text, Loader=Loader) except Exception as e: sys.stderr.write("[otmod.py] ERROR: There was an error during the attempt to parse the YAML file. " + str(e) + "\n") sys.exit(1) # Read font infile and create a fontTools OT table object try: tt = ttLib.TTFont(infile) except Exception as e: sys.stderr.write("[otmod.py] ERROR: There was an error during the attempt to parse the OpenType tables in the font file '" + infile + "'. " + str(e) + "\n") sys.exit(1) # iterate through OT tables in the Python fonttools OT table object for ot_table in otmods_obj: # Confirm that the requested table name for a change is an actual table in the font if ot_table in tt.keys(): # iterate through the items that require modification in the table for field in otmods_obj[ot_table]: # confirm that the field exists in the existing font table if field in tt[ot_table].__dict__.keys(): # modify the field definition in memory tt[ot_table].__dict__[field] = otmods_obj[ot_table][field] # notify user if quiet flag is not set if not quiet: print("(" + infile + ")[" + ot_table + "][" + field + "] changed to " + str(tt[ot_table].__dict__[field])) else: print("[otmod.py] WARNING: '" + ot_table + "' table field '" + field + "' was not a table found in the font '" + infile + "'. No change was made to this table field.") else: print("[otmod.py] WARNING: '" + ot_table + "' was not a table found in the font '" + infile + "'. No change was made to this table.") # Write updated font to disk try: tt.save(outfile) if not quiet: print("[otmod.py] '" + infile + "' was updated and the new font write took place on the path '" + outfile + "'.") except Exception as e: sys.stderr.write("[otmod.py] ERROR: There was an error during the attempt to write the file '" + outfile + "' to disk. " + str(e) + "\n") sys.exit(1) if __name__ == '__main__': if len(sys.argv) > 1: main(sys.argv[1:]) else: sys.stderr.write("[otmod.py] ERROR: no arguments detected in your command.\n") sys.exit(1)