Skip to content

Instantly share code, notes, and snippets.

@rjmoggach
Created May 8, 2013 14:13
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rjmoggach/5540711 to your computer and use it in GitHub Desktop.
Save rjmoggach/5540711 to your computer and use it in GitHub Desktop.
Python script to convert a hierarchy of fonts to OTF format This is useful if you have a huge collection of mac fonts that use resource forks and want cross platform fonts. Use at your own risk. It's not fully tested. Backup your originals before you run the script to be safe. It requires the fontforge python library.
#!/usr/bin/python
import os, sys, string, re, MacOS
from string import capwords
import fontforge as ff
from optparse import OptionParser
TEST = False
SKIP_FILES = ['.DS_Store', '.AppleDB', 'convert_font.log', 'Icon?']
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALPHABET_IGNORE = 'BCDEFGHIJKLMNOQRSVWYZ'
#DESTINATION = '/Users/rob/Fonts'
IGNORE_PRE = ['AG', 'AT', 'AU', 'Adobe', 'Avant_Garde', 'BI', 'Berthold', 'Bitstream', 'Cg', 'DF', 'FF', 'HTF', 'ITC', 'Linotype', 'Monotype', 'Stempel', 'The']
IGNORE_POST = ['', 'A', 'AG', 'AMP', 'AT', 'Alt', 'Alternate', 'Alternates', 'Antiqua', 'Antique', 'B', 'B', 'BC', 'BE', 'BQ', 'BT', 'BTN', 'Bd', 'Bk', 'Bl', 'Bla', 'Black', 'Bld', 'Blk', 'Bol', 'Bold', 'Boo', 'Book', 'Broad', 'Bt', 'C', 'Cameo', 'Cap', 'Caps', 'Cd', 'Cm', 'Cmp', 'Cn', 'Com', 'Compressed', 'Con', 'Cond', 'Condensed', 'Cp', 'Cursive', 'Cy', 'Cyr', 'Cyrillic', 'D', 'DCD', 'Dark', 'Dem', 'Demi', 'Demibold', 'Dfr', 'Dingbats', 'Dis', 'Dm', 'Double', 'EF', 'Eight', 'Engraved', 'Ep', 'Ex', 'Exb', 'Exp', 'Expanded', 'Expert', 'Ext', 'Extended', 'Extension', 'Extra', 'Extras', 'F', 'Face', 'Figs', 'Five', 'Font', 'Fortytwo', 'Four', 'Fractions', 'Fractions', 'Got', 'Gothic', 'Greek', 'Gth', 'HTF', 'Hand', 'Handtled', 'Handtooled', 'Headline', 'Heavy', 'Heavyface', 'Hundred', 'Hv', 'I', 'ICG', 'IIA', 'ITC', 'ITCSC', 'ITCTT', 'Initials', 'Inline', 'Inserat', 'It', 'Ita', 'Ital', 'Italic', 'Itc', 'Kursiv', 'LF', 'LF', 'LH', 'LL', 'LP', 'LT', 'Lf', 'Lift', 'Lig', 'Light', 'Lined', 'Lining', 'Lit', 'Lt', 'MM', 'MT', 'MT', 'Math', 'Md', 'Md', 'Med', 'Medium', 'Medium', 'Modern', 'Mono', 'Mt', 'NPL', 'Narrow', 'Negative', 'Nine', 'No', 'No', 'Normal', 'OS', 'OSITC', 'Obl', 'Oblique', 'Old', 'Oldstyle', 'One', 'Open', 'Open', 'Openface', 'Ornamental', 'Ornaments', 'Os', 'Ou', 'Out', 'Outline', 'Outline', 'Outlined', 'P', 'PI', 'Petite', 'Pi', 'Plain', 'Positive', 'Poster', 'Poster', 'Pro', 'Reg', 'Regular', 'Rev', 'Reversed', 'Rg', 'Roman', 'Rough', 'Round', 'Rounded', 'SC', 'SC', 'SCEF', 'SCITC', 'SCITCTT', 'SCT', 'Sans', 'Script', 'Semi', 'SemiSans', 'Semibold', 'Semisans', 'Serif', 'Seven', 'Shaded', 'Shadow', 'Six', 'Slab', 'Slabserif', 'Small', 'Smallcaps', 'Split', 'Split', 'Sq', 'Square', 'Standard', 'Std', 'Striped', 'Style', 'Sup', 'Super', 'Swash', 'Swashes', 'Symbol', 'T', 'Tab', 'Text', 'Tf', 'Th', 'Th', 'Thin', 'Three', 'Titling', 'Twe', 'Twelve', 'Two', 'Type', 'Ult', 'Ultra', 'Uncial', 'Unicase', 'Wide', 'X', 'XXX', 'with']
REPLACE_POST = {'Grot': 'Grotesque', 'Ext': 'Extended', 'Bd': 'Bold', 'Std': 'Standard', 'Obl':'Oblique','Rg': 'Regular', 'Ult': 'Ultra', 'Exp': 'Expanded'}
STRIP_CHARS = ['', ' ', ' ', ' ', ' ', '\'']
def unique(a):
return list(set(a))
def intersect(a, b):
return list(set(a) & set(b))
def union(a, b):
return list(set(a) | set(b))
def difference(a, b):
return list(set(b).difference(set(a)))
def clean_spaces(text):
while ' ' in text: text = text.replace(' ',' ')
text = text.strip()
return text
class DirectoryWalker:
# a forward iterator that traverses a directory tree
def __init__(self, directory):
self.stack = [directory]
self.files = []
self.index = 0
def __getitem__(self, index):
while 1:
try:
file = self.files[self.index]
self.index = self.index + 1
except IndexError:
# pop next directory from stack
self.directory = self.stack.pop()
self.files = os.listdir(self.directory)
self.index = 0
else:
# got a filename
fullname = os.path.join(self.directory, file)
if os.path.isdir(fullname) and not os.path.islink(fullname):
self.stack.append(fullname)
return fullname
def return_stripped_set(name):
# strip weird characters and return a split set
name = re.sub('[\W_]+',' ', name)
name_set = re.split('([A-Z][a-z]+|[\d]+)|[\W]+', name)
procd_set = name_set
i=0
l=len(procd_set)
while i != l:
if not procd_set[i] or procd_set[i] is None:
procd_set.pop(i)
l=len(procd_set)
continue
i += 1
return procd_set
def return_font_file_name(fontname):
# return the font name from the font embedded name
if intersect(fontname,ALPHABET):
fn_set = return_stripped_set(fontname)
name = ''
for i in fn_set:
i = i.strip()
if i in ALPHABET:
name = string.join((name, i))
elif i in IGNORE_PRE:
name = string.join((name,i))
elif i[:-1] in IGNORE_PRE:
name = string.join((name,i[:-1]))
name = string.join((name,i[-1:]))
elif i in STRIP_CHARS:
continue
else:
try:
name = string.join((name,REPLACE_POST[i]))
except KeyError:
name = string.join((name,i))
else:
name = fontname
name = clean_spaces(name).strip('-_ ').replace('-',' ').replace('_',' ')
name = name.replace(' ','_')
return name
def replace_words(name_set):
# look for short forms we want to expand in a set and replace them
procd_set = name_set
i = 1
l=len(procd_set)
while i != l:
try:
procd_set[i] = REPLACE_POST[procd_set[i]]
i += 1
except KeyError:
i += 1
return procd_set
def return_dest_dir(familyname, fontname, DESTINATION):
# build the destination directory from the familyname or fontname and root destination dir
sort_dir = '0'
if not len(familyname) is 1:
dir_name_set = return_stripped_set(familyname)
else:
dir_name_set = return_stripped_set(fontname)
if not len(dir_name_set) is 1:
while dir_name_set[-1].strip() in IGNORE_POST or not dir_name_set[-1].strip().isalpha():
if not len(dir_name_set) is 1:
dropped = dir_name_set.pop()
else:
break
if not len(dir_name_set) is 1:
while dir_name_set[0].strip() in IGNORE_PRE or dir_name_set[0].strip()[:-1] in IGNORE_PRE or not re.search('\D',dir_name_set[0]) or dir_name_set[0] in ALPHABET_IGNORE:
dropped = dir_name_set.pop(0)
dir_name_set = replace_words(dir_name_set)
dir_name = string.join(dir_name_set)
dir_name = clean_spaces(dir_name).replace(' ','_')
if str(dir_name[:1]).upper() in ALPHABET: sort_dir = str(dir_name[:1]).upper()
return os.path.join(DESTINATION, sort_dir, dir_name)
def main():
DEFAULT_FONT_DIR = os.path.join(os.path.expanduser('~'), 'FontsConverted')
parser=OptionParser()
usage = """
%prog [options]
Default source dir is current directory.
Default destination dir is 'FontsConverted' in your home dir.
***WARNING***
Split up the tasks.
It will crash if you try more than 15000 fonts.
"""
parser = OptionParser(usage=usage)
parser.add_option("-s", dest="SOURCE", default=".", help="source directory of fonts to convert")
parser.add_option("-d", dest="DESTINATION", default=DEFAULT_FONT_DIR, help="destination directory to generate fonts in")
parser.add_option("-t", dest="TEST", action='store_true', default=False, help="test the conversion without creating any files")
parser.add_option("-k", dest="SKIP_FOLDERS", help="skip these directories (comma-separated NO SPACES)")
(options, args) = parser.parse_args()
SKIP_FOLDERS = str(options.SKIP_FOLDERS).replace('\\','')
SKIP_FOLDERS = SKIP_FOLDERS.split(',')
SOURCE = str(options.SOURCE).replace('\\','')
DESTINATION = str(options.DESTINATION).replace('\\','')
TEST = options.TEST
SKIPPED_FILES = []
if TEST:
print '\n------ TESTING ONLY --------\n'
for file in DirectoryWalker(SOURCE):
head,tail=os.path.split(file)
if not tail in SKIP_FILES:
if os.path.isfile(file):
try:
f=ff.open(file)
print " OPEN:", f.fontname
f.close()
print " OK:", file
except EnvironmentError:
print "Error:", file
print '\n------ TESTING COMPLETE --------\n'
else:
print '\n------ STARTING CONVERSION --------\n'
for file in DirectoryWalker(SOURCE):
file = file.decode("utf-8")
head,tail=os.path.split(file)
if not head is '.':
skip_head_string = head.strip('/.').strip().split('/')[0]
#print '\nSKIP STRING:', skip_head_string
#print skip_head_string in SKIP_FOLDERS
if not skip_head_string in SKIP_FOLDERS and not head.strip('.') in SKIP_FOLDERS:
if not tail in SKIP_FILES:
if os.path.isfile(file):
try:
macfiletype = MacOS.GetCreatorAndType(file)
print 'DEBUG MAC CREATOR:', macfiletype
except: pass
# skip suitcases that crash fontforge
if list(macfiletype)[0] in ['2T2L', '2t2l','VOMD', 'vomd', 'RVOM', 'rvom','\xbfSCC'] and list(macfiletype)[1] in ['LIFF','liff']: continue
try:
print '\nOPEN: %s\n--------------------------------------------------------------------------' % file
f=ff.open(file)
if not file[-4:] in '.otf':
valid_font = f.validate()
else:
valid_font = True
if valid_font:
#print>>sys.stdout, ' OPENED: "%s"' % file
font_fullname, font_family = f.fontname, f.familyname
if f.is_cid: font_fullname, font_family = f.cidfontname, f.cidfamilyname
font_file_name = return_font_file_name(font_fullname)
dest_file = string.join((font_file_name,'.otf'),'')
dest_dir = return_dest_dir(font_family, font_fullname, DESTINATION)
dest_font = os.path.join(DESTINATION, dest_dir, dest_file)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
if not os.path.exists(dest_font):
pass
f.generate(dest_font)
f.close()
if not os.path.exists(dest_font):
os.rmdir(os.path.join(DESTINATION, dest_dir))
else:
print>>sys.stdout, '\tOUTPUT: "%s" --> %s' % (file, dest_font)
print>>sys.stdout, "\t SRC: %s" % file
print>>sys.stdout, "\t FONT: %s (%s)" % (font_fullname, font_file_name)
print>>sys.stdout, "\tFAMILY: %s (%s)" % (font_family, return_font_file_name(font_family))
print>>sys.stdout, "\t FILE: %s" % dest_font
else:
f.close()
print>>sys.stderr, ' INVALID FONT'
except EnvironmentError, UnicodeDecodeError:
SKIPPED_FILES.append(file)
else:
print "Skipping Directory: ", file
else:
pass
print '\n------ FINISHED CONVERSION --------\n'
for entry in SKIPPED_FILES:
print>>sys.stdout, "SKIPPED:", entry
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment