2
0

otmod.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # ------------------------------------------------------------------------------
  4. # otmod.py
  5. # Copyright 2015 Christopher Simpkins
  6. # MIT license
  7. # ------------------------------------------------------------------------------
  8. import sys
  9. import os.path
  10. import codecs
  11. import unicodedata
  12. from fontTools import ttLib
  13. from yaml import load
  14. try:
  15. from yaml import CLoader as Loader
  16. except ImportError:
  17. from yaml import Loader
  18. # ------------------------------------------------------------------------------
  19. # [ Argument Class ]
  20. # all command line arguments (object inherited from Python list)
  21. # ------------------------------------------------------------------------------
  22. class Argument(list):
  23. """Argument class is a list for command line arguments. It provides methods for positional argument parsing"""
  24. def __init__(self, argv):
  25. self.argv = argv
  26. list.__init__(self, self.argv)
  27. # return argument at position specified by the 'position' parameter
  28. def get_arg(self, position):
  29. if self.argv and (len(self.argv) > position):
  30. return self.argv[position]
  31. else:
  32. return ""
  33. # return position of user specified argument in the argument list
  34. def get_arg_position(self, test_arg):
  35. if self.argv:
  36. if test_arg in self.argv:
  37. return self.argv.index(test_arg)
  38. else:
  39. return -1 # TODO: change the return code that indicates an error
  40. # return the argument at the next position following a user specified positional argument
  41. # (e.g. for argument to an option in cmd)
  42. def get_arg_next(self, position):
  43. if len(self.argv) > (position + 1):
  44. return self.argv[position + 1]
  45. else:
  46. return ""
  47. def generate_outfile_path(filepath):
  48. filepath_list = os.path.split(filepath)
  49. directory_path = filepath_list[0]
  50. basefile_path = filepath_list[1]
  51. basefile_list = basefile_path.split(".")
  52. new_basefile_name = basefile_list[0] + "-new." + basefile_list[1]
  53. outfile = os.path.join(directory_path, new_basefile_name)
  54. return outfile
  55. def read_utf8(filepath):
  56. """read_utf8() is a function that reads text in as UTF-8 NFKD normalized text strings from filepath
  57. :param filepath: the filepath to the text input file
  58. """
  59. try:
  60. f = codecs.open(filepath, encoding='utf_8', mode='r')
  61. except IOError as ioe:
  62. sys.stderr.write("[otmod.py] ERROR: Unable to open '" + filepath + "' for read.\n")
  63. raise ioe
  64. try:
  65. textstring = f.read()
  66. norm_text = unicodedata.normalize('NFKD', textstring) # NKFD normalization of the unicode data before returns
  67. return norm_text
  68. except Exception as e:
  69. sys.stderr.write("[otmod.py] ERROR: Unable to read " + filepath + " with UTF-8 encoding using the read_utf8() method.\n")
  70. raise e
  71. finally:
  72. f.close()
  73. def main(arguments):
  74. args = Argument(arguments)
  75. # Command line syntax + parsing
  76. # LONG OPTIONS: otmod.py --in <font infile path> --opentype <Open Type changes YAML path> --out <font outfile path> --quiet
  77. # SHORT OPTIONS: otmod.py -i <font infile path> -t <Open Type changes YAML path> -o <font outfile path> -q
  78. # Quiet flag (default to False, if set to True does not print changes that occurred to std output)
  79. quiet = False
  80. if "--quiet" in args.argv or "-q" in args.argv:
  81. quiet = True
  82. # font infile path
  83. if "--in" in args.argv:
  84. infile = args.get_arg_next(args.get_arg_position("--in"))
  85. if infile is "":
  86. sys.stderr.write("[otmod.py] ERROR: please define the font input file path as an argument to the --in command line option.\n")
  87. sys.exit(1)
  88. elif "-i" in args.argv:
  89. infile = args.get_arg_next(args.get_arg_position("-i"))
  90. if infile is "":
  91. sys.stderr.write("[otmod.py] ERROR: please define the font input file path as an argument to the -i command line option.\n")
  92. sys.exit(1)
  93. else:
  94. sys.stderr.write("[otmod.py] ERROR: Please include the `--in` option with an input font file defined as an argument.\n")
  95. sys.exit(1)
  96. # OpenType change YAML file path
  97. if "--opentype" in args.argv:
  98. otpath = args.get_arg_next(args.get_arg_position("--opentype"))
  99. if otpath is "":
  100. sys.stderr.write("[otmod.py] ERROR: please define the YAML OpenType changes file path as an argument to the --opentype command line option.\n")
  101. sys.exit(1)
  102. elif "-t" in args.argv:
  103. otpath = args.get_arg_next(args.get_arg_position("-t"))
  104. if otpath is "":
  105. sys.stderr.write("[otmod.py] ERROR: please define the YAML OpenType changes file path as an argument to the -t command line option.\n")
  106. sys.exit(1)
  107. else:
  108. 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")
  109. sys.exit(1)
  110. # font outfile path (allows for font name change in outfile)
  111. if "--out" in args.argv:
  112. outfile = args.get_arg_next(args.get_arg_position("--out"))
  113. if outfile is "":
  114. outfile = generate_outfile_path(infile)
  115. elif "-o" in args.argv:
  116. outfile = args.get_arg_next(args.get_arg_position("-o"))
  117. if outfile is "":
  118. outfile = generate_outfile_path(infile)
  119. else:
  120. outfile = generate_outfile_path(infile)
  121. # Test for existing file paths
  122. if not os.path.isfile(infile):
  123. sys.stderr.write("[otmod.py] ERROR: Unable to locate font at the infile path '" + infile + "'.\n")
  124. sys.exit(1)
  125. if not os.path.isfile(otpath):
  126. sys.stderr.write("[otmod.py] ERROR: Unable to locate the OpenType modification settings YAML file at '" + otpath + "'.\n")
  127. sys.exit(1)
  128. # Read YAML OT table changes settings file and convert to Python object
  129. try:
  130. yaml_text = read_utf8(otpath)
  131. # Python dictionary definitions with structure `otmods_obj['OS/2']['sTypoLineGap']`
  132. otmods_obj = load(yaml_text, Loader=Loader)
  133. except Exception as e:
  134. sys.stderr.write("[otmod.py] ERROR: There was an error during the attempt to parse the YAML file. " + str(e) + "\n")
  135. sys.exit(1)
  136. # Read font infile and create a fontTools OT table object
  137. try:
  138. tt = ttLib.TTFont(infile)
  139. except Exception as e:
  140. 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")
  141. sys.exit(1)
  142. # iterate through OT tables in the Python fonttools OT table object
  143. for ot_table in otmods_obj:
  144. # Confirm that the requested table name for a change is an actual table in the font
  145. if ot_table in tt.keys():
  146. # iterate through the items that require modification in the table
  147. for field in otmods_obj[ot_table]:
  148. # confirm that the field exists in the existing font table
  149. if field in tt[ot_table].__dict__.keys():
  150. # modify the field definition in memory
  151. tt[ot_table].__dict__[field] = otmods_obj[ot_table][field]
  152. # notify user if quiet flag is not set
  153. if not quiet:
  154. print("(" + infile + ")[" + ot_table + "][" + field + "] changed to " + str(tt[ot_table].__dict__[field]))
  155. else:
  156. 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.")
  157. else:
  158. print("[otmod.py] WARNING: '" + ot_table + "' was not a table found in the font '" + infile + "'. No change was made to this table.")
  159. # Write updated font to disk
  160. try:
  161. tt.save(outfile)
  162. if not quiet:
  163. print("[otmod.py] '" + infile + "' was updated and the new font write took place on the path '" + outfile + "'.")
  164. except Exception as e:
  165. sys.stderr.write("[otmod.py] ERROR: There was an error during the attempt to write the file '" + outfile + "' to disk. " + str(e) + "\n")
  166. sys.exit(1)
  167. if __name__ == '__main__':
  168. if len(sys.argv) > 1:
  169. main(sys.argv[1:])
  170. else:
  171. sys.stderr.write("[otmod.py] ERROR: no arguments detected in your command.\n")
  172. sys.exit(1)