# -*- coding: utf-8 -*- # Glyphtracer # Copyright (C) 2010 Jussi Pakkanen # version 1.4 (c) 2015 Stéphanie Vilayphiou # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Glyphtracer library files and stuff import os, subprocess, tempfile import fontforge, psMat BLANK_FONT = "../utils/blank.sfd" font = fontforge.open(BLANK_FONT) program_name = 'Glyphtracer' program_version = '1.4' def entry_to_upper(e): return (e[0].capitalize(), e[1]-32) # The format of letter lists is as follows: # # Each element is a tuple. The first element is a string with the # glyph's name *as used by Fontforge*. Not the unicode name # or anything else. It is always in pure ASCII. The # second element is the corresponding Unicode code point. # These are read only lists that define different glyph groups. # In the future they may be parsed from a conf file. latin_lowercase_list = [('a', 97), ('b', 98), ('c', 99), ('d', 100), ('e', 101), ('f', 102),\ ('g', 103), ('h', 104), ('i', 105), ('j', 106), ('k', 107), ('l', 108),\ ('m', 109), ('n', 110), ('o', 111), ('p', 112), ('q', 113), ('r', 114),\ ('s', 115), ('t', 116), ('u', 117), ('v', 118), ('w', 119), ('x', 120),\ ('y', 121), ('z', 122)] latin_uppercase_list = [entry_to_upper(x) for x in latin_lowercase_list] latin_accented_lower_list = [('agrave', 224), ('aacute', 225), ('acircumflex', 226),\ ('atilde', 227), ('adieresis', 228), ('aring', 229),\ ('egrave', 232), ('eacute', 233), ('ecircumflex', 234),\ ('edieresis', 235), ('igrave', 236), ('iacute', 237),\ ('icircumflex', 238), ('idieresis', 239), ('ntilde', 241),\ ('ograve', 242), ('oacute', 243), ('ocircumflex', 244),\ ('otilde', 245), ('odieresis', 246), ('ugrave', 249),\ ('uacute', 250), ('ucircumflex', 251), ('udieresis', 252),\ ('yacute', 253), ('ydieresis', 255)] # Most lower case letters are at a fixed distance from their upper case variants. # Some are not, thus some of these lists will appear a bit messy. latin_accented_upper_list = [entry_to_upper(x) for x in latin_accented_lower_list[:-1]] \ + [('Ydieresis', 376)] latin_diacritics_list = [('asciicircum', 94), ('grave', 96), ('dieresis', 168), ('macron', 175), \ ('acute', 180), ('cedilla', 184)] latin_extra_lower_list = [('ccedilla', 231), ('eth', 240), ('oslash', 248),\ ('thorn', 254), ('ae', 230), ('oe', 339), ('germandbls', 223)] latin_extra_upper_list = [entry_to_upper(x) for x in latin_extra_lower_list[:-3]]\ + [('AE', 198), ('OE', 338)] number_list = [('zero', 48), ('one', 49), ('two', 50), ('three', 51), ('four', 52), ('five', 53),\ ('six', 54), ('seven', 55), ('eight', 56), ('nine', 57), ('onequarter', 188), ('onehalf', 189), ('threequarters', 190),\ ('uni2160', 8544), ('uni2161', 8545),\ ('uni2162', 8546), ('uni2163', 8547), ('uni2164', 8548), ('uni2165', 8549), ('uni2166', 8550), ('uni2167', 8551),\ ('uni2168', 8552), ('uni2169', 8553), ('uni216A', 8554), ('uni216B', 8555), ('uni216C', 8556), ('uni216D', 8557), ('uni216E', 8558), ('uni216F', 8559)] punctuation_list = [('exclam', 33), ('exclamdown', 161), ('question', 63), ('questiondown', 191),\ ('period', 46), ('comma', 44), ('colon', 58), ('semicolon', 59),\ ('slash', 47), ('backslash', 92), ('hyphen', 45), ('underscore', 95),\ ('endash', 8211), ('emdash', 8212), ('ellipsis', 8230), ('periodcentered', 183)] brackets_list = [('parenleft', 40), ('parenright', 41), ('bracketleft', 91), ('bracketright', 93),\ ('braceleft', 123), ('braceright', 125), ('less', 60), ('greater', 62)] quotation_list = [('quotesingle', 39,), ('quotedbl', 34), ('quoteleft', 8216), ('quoteright', 8217),\ ('quotesinglbase', 8218), ('quotedblleft', 8220), ('quotedblright', 8221),\ ('quotedblbase', 8222), ('guillemotleft', 171), ('guillemotright', 187),\ ('guilsinglleft', 8249), ('guilsinglright', 8250),] symbol_list = [('numbersign', 35), ('percent', 37), ('ampersand', 38), ('asterisk', 42),\ ('plus', 43), ('multiply', 215), ('divide', 247), ('equal', 61), ('at', 64),\ ('asciitilde', 126), ('copyright', 169), ('registered', 174),\ ('trademark', 8482), ('paragraph', 182), ('section', 167), ('bar', 124), ('brokenbar', 166),\ ('uniFFFD', 65533), ('ordfeminine', 170), ('logicalnot', 172), ('degree', 176),\ ('plusminus', 177), ('uni00B2', 178), ('uni00B3', 179), ('mu', 181), ('uni00B9', 185), ('uni00BA', 186)] currency_list = [('dollar', 36), ('cent', 162), ('Euro', 8364), ('sterling', 163),\ ('yen', 165), ('currency', 164)] cyrillic_upper = [('afii10017', 1040), ('afii10018', 1041), ('afii10019', 1042),\ ('afii10020', 1043), ('afii10021', 1044), ('afii10022', 1045),\ ('afii10024', 1046), ('afii10025', 1047), ('afii10026', 1048),\ ('afii10027', 1049), ('afii10028', 1050), ('afii10029', 1051),\ ('afii10030', 1052), ('afii10031', 1053), ('afii10032', 1054),\ ('afii10033', 1055), ('afii10034', 1056), ('afii10035', 1057),\ ('afii10036', 1058), ('afii10037', 1059), ('afii10038', 1060),\ ('afii10039', 1061), ('afii10040', 1062), ('afii10041', 1063),\ ('afii10042', 1064), ('afii10043', 1065), ('afii10044', 1066),\ ('afii10045', 1067), ('afii10046', 1068), ('afii10047', 1169),\ ('afii10048', 1070), ('afii10049', 1071)] cyrillic_lower = [('afii10065', 1072), ('afii10066', 1073), ('afii10067', 1074),\ ('afii10068', 1075), ('afii10069', 1076), ('afii10070', 1077),\ ('afii10072', 1078), ('afii10073', 1079), ('afii10073', 1080),\ ('afii10075', 1081), ('afii10076', 1082), ('afii10077', 1083),\ ('afii10078', 1084), ('afii10079', 1085), ('afii10080', 1086),\ ('afii10081', 1087), ('afii10082', 1088), ('afii10083', 1089),\ ('afii10084', 1090), ('afii10085', 1091), ('afii10086', 1092),\ ('afii10089', 1093), ('afii10088', 1094), ('afii10089', 1095),\ ('afii10090', 1096), ('afii10091', 1097), ('afii10092', 1098),\ ('afii10093', 1099), ('afii10094', 1100), ('afii10095', 1101),\ ('afii10096', 1102), ('afii10097', 1103)] glyph_groups = [('latin lower case', latin_lowercase_list),\ ('latin upper case', latin_uppercase_list),\ ('latin accented lower case', latin_accented_lower_list),\ ('latin accented upper case', latin_accented_upper_list),\ ('latin diacritics', latin_diacritics_list),\ ('latin extra lower case', latin_extra_lower_list), ('latin extra upper case', latin_extra_upper_list), ('numbers', number_list),\ ('brackets', brackets_list),\ ('punctuation', punctuation_list),\ ('quotation', quotation_list),\ ('symbols', symbol_list),\ ('currency', currency_list),\ ('cyrillic lowercase', cyrillic_lower),\ ('cyrillic uppercase', cyrillic_upper)] class LetterBox(object): def __init__(self, rectangle): self.r = rectangle self.taken = False def contains(self, x, y): return self.r.contains(x, y) class GlyphInfo(object): def __init__(self, name, codepoint): self.name = name self.codepoint = codepoint self.box = None def i_haz_potrace(): p = subprocess.Popen('potrace -h', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.wait() return p.returncode == 0 def data_to_glyphinfo(data): return GlyphInfo(data[0], data[1]) def integerise(command_line): return [int(x) for x in command_line.split()[0:-1]] def parse_postscript(commands): point_sets = [] points = [] assert(commands[0].endswith('moveto')) for cmd in commands: if cmd.endswith('moveto'): assert(len(points) == 0) points.append(integerise(cmd)) elif cmd.endswith('rcurveto'): points.append(integerise(cmd)) elif cmd.endswith('rlineto'): points.append(integerise(cmd)) elif cmd.endswith('closepath'): point_sets.append(points) points = [] elif cmd == 'fill': pass # There is more than one blob in the image. But that's ok. else: raise RuntimeError('Unknown PostScript command: ' + cmd) assert(len(points) == 0) return point_sets def potrace_image(filename): svg = filename + ".svg" p = subprocess.Popen('autotrace -output-file=' + svg + ' ' + filename, shell=True, stdout=subprocess.PIPE) (so, se) = p.communicate() return svg def crop_and_trace(image, box, codepoint): tfile = tempfile.NamedTemporaryFile(suffix='.png') tempname = tfile.name cropped = image.copy(box) if not cropped.save(tempname): raise RuntimeError('Could not save cropped image') #svg = potrace_image(tempname) font[codepoint].importOutlines(tempname) font[codepoint].autoTrace() #os.unlink(tempname) tfile.close() return def convert_points(pointlist): pointlist = to_absolute(pointlist) return flip_curve(pointlist) def to_absolute(pointlist): starting_point = pointlist[0] assert(len(starting_point) == 2) converted = [starting_point] current_point = starting_point for p in pointlist[1:]: if len(p) == 2: newp = [current_point[0] + p[0], current_point[1] + p[1]] elif len(p) == 6: newp = [0]*6 newp[0] = current_point[0] + p[0] newp[1] = current_point[1] + p[1] newp[2] = current_point[0] + p[2] newp[3] = current_point[1] + p[3] newp[4] = current_point[0] + p[4] newp[5] = current_point[1] + p[5] else: raise RuntimeError('Unknown point size error.') converted.append(newp) current_point = [newp[-2], newp[-1]] return converted def flip_curve(curve): first = curve[0] last = curve[-1] assert(first[0] == last[-2]) assert(first[1] == last[-1]) flipped = [first] for i in range(len(curve))[:0:-1]: curp = curve[i] if i == 0: prevp = first else: prevp = curve[i-1] if len(curp) == 6: newp = curp[2:4] + curp[0:2] + prevp[-2:] elif len(curp) == 2: newp = prevp[-2:] flipped.append(newp) return flipped def pointlist_to_str(points, scale): return ' '.join([str(scale*p) for p in points]) def process_glyph(image, glyph, font_height): font.createMappedChar(glyph.name) font.createChar(glyph.codepoint) font[glyph.name].vwidth = font_height crop_and_trace(image, glyph.box.r, glyph.codepoint) def max_y(glyphs): """Return the the height of the tallest letter box.""" return reduce(lambda x, y: max(x, y.box.r.height()), glyphs, 0) def createSpaceGlyphs(name, codepoint, value): font.createMappedChar(name) font.createChar(codepoint) font[name].width = value; def write_sfd(ofilename, fontname, image, glyphs, font_height, font_ascent): font_height = int(font_height) font.ascent = int(font_ascent) font.descent = font_height - int(font_ascent) print("Generating SFD") for glyph in glyphs: print(glyph.name, glyph.box.r) process_glyph(image, glyph, int()) font.selection.all() font.autoWidth(100, 30) font.autoHint() # CREATE SPACE CHARACTERS createSpaceGlyphs('space', 32, int(font_height / 4)) createSpaceGlyphs('uni2000', 8192, int(font_height / 2)) # en-quad createSpaceGlyphs('uni2001', 8193, font_height) # em-quad createSpaceGlyphs('uni2002', 8194, int(font_height / 2)) # en-space createSpaceGlyphs('uni2003', 8195, font_height) # em-space createSpaceGlyphs('uni2004', 8196, int(font_height / 3)) # three per em space createSpaceGlyphs('uni2005', 8197, int(font_height / 4)) # four per em space createSpaceGlyphs('uni2006', 8198, int(font_height / 6)) # six per em space #createSpaceGlyphs('uni2007', 8199, int(font['five'].width)) # figure space #createSpaceGlyphs('uni2008', 8200, int(font['period'].width)) # punctuation space createSpaceGlyphs('uni2009', 8201, int(font_height / 5)) # thin space createSpaceGlyphs('uni200A', 8202, int(font_height / 6)) # hair space createSpaceGlyphs('uni200B', 8203, 0) # zero-width space createSpaceGlyphs('uni202F', 8239, int(font_height / 5)) # narrow no-break space createSpaceGlyphs('uni205F', 8287, int(font_height / 18 * 4)) # mathematical space createSpaceGlyphs('uniFEFF', 65279, 0) # zero width no-break space print("save") font.save(ofilename)