Created
May 8, 2013 14:13
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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