fons
clone your own copy | download snapshot

Snapshots | iceberg

Inside this repository

gtlib.py
text/x-python

Download raw (14.1 KB)

# -*- coding: utf-8 -*-

#    Glyphtracer
#    Copyright (C) 2010 Jussi Pakkanen
#
#    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 <http://www.gnu.org/licenses/>.

# Glyphtracer library files and stuff

import os, subprocess, tempfile

program_name = 'Glyphtracer'
program_version = '1.3'

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_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)]

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), ('periodcenter', 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), ('brokenbar', 166),\
               ('uniFFFD', 65533)]

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 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)]

sfd_header = """SplineFontDB: 3.0
FontName: %%s
FullName: %%s
FamilyName: %%s
Weight: Medium
Copyright: Originally traced with %s
UComments: "No comments" 
Version: 001.000
ItalicAngle: 0
UnderlinePosition: -100
UnderlineWidth: 50
Ascent: %%d
Descent: %%d
LayerCount: 2
Layer: 0 0 "Back"  1
Layer: 1 0 "Fore"  0
NeedsXUIDChange: 1
XUID: [1021 397 1238052781 15881202]
OS2Version: 0
OS2_WeightWidthSlopeOnly: 0
OS2_UseTypoMetrics: 1
CreationTime: 1270926697
ModificationTime: 1271540628
OS2TypoAscent: 0
OS2TypoAOffset: 1
OS2TypoDescent: 0
OS2TypoDOffset: 1
OS2TypoLinegap: 0
OS2WinAscent: 0
OS2WinAOffset: 1
OS2WinDescent: 0
OS2WinDOffset: 1
HheadAscent: 0
HheadAOffset: 1
HheadDescent: 0
HheadDOffset: 1
OS2Vendor: 'GlTr'
DEI: 91125
Encoding: UnicodeBmp
UnicodeInterp: none
NameList: Adobe Glyph List
DisplaySize: -36
AntiAlias: 1
FitToEm: 1
WinInfo: 57 19 19
BeginChars: 65536 %%d

""" % program_name

sfd_footer = """EndChars
EndSplineFont
"""

letter_header = """StartChar: %s
Encoding: %d %d %d
Width: %d
VWidth: 0
Flags: HW
LayerCount: 2
Fore
SplineSet
"""

letter_footer = """EndSplineSet
EndChar

"""

# Numerical constants
total_height = 2048 # By convention on Opentype Fonts
ascent = 1638
descent= total_height - ascent
height_ratio = 0.9
highest_y_coordinate = height_ratio * ascent
potrace_pixel_multiplier = 10
rbearing = 150

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):
    p = subprocess.Popen('potrace -c --eps -q ' + filename + ' -o -', shell=True, stdout=subprocess.PIPE)
    (so, se) = p.communicate()
    lines = so.split('\n')
    while not lines[0].endswith('moveto'):
        lines.pop(0)
    while not lines[-1].endswith('closepath'):
        lines.pop()
    pointset = parse_postscript(lines)
    pointset = map(convert_points, pointset)
    return pointset

def crop_and_trace(image, box):
    tfile = tempfile.NamedTemporaryFile(suffix='.pgm')
    tempname = tfile.name
    tfile.close()
    cropped = image.copy(box)
    if not cropped.save(tempname):
        raise RuntimeError('Could not save cropped image')
    points = potrace_image(tempname)
    os.unlink(tempname)
    return points
    

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(ofile, image, glyph, scale):
    if glyph.box is None:
        return
    width = glyph.box.r.width()*potrace_pixel_multiplier*scale + rbearing
    location3 = 0
    ofile.write(letter_header % (glyph.name, glyph.codepoint, glyph.codepoint, location3, width))
    points = crop_and_trace(image, glyph.box.r)
    for curve in points:
        fp = curve[0]
        assert(len(fp) == 2)
        ofile.write(pointlist_to_str(fp, scale))
        ofile.write(" m 0\n")

        for i in xrange(1, len(curve)):
            point = curve[i]
            ofile.write(' ')
            ofile.write(pointlist_to_str(point, scale))
            # Print move commands.
            if len(point) == 6:
                if i < len(curve)-1 and len(curve[i+1]) == 2:
                    flags = 2
                else:
                    flags = 0
                ofile.write(' c %d\n' % flags)
            elif len(point) == 2:
                if i < len(curve)-1 and len(curve[i+1]) == 2:
                    flags = 1
                else:
                    flags = 2
                ofile.write(' l %d\n' % flags)
            else:
                raise RuntimeError('Incorrect amount of points: %d' % len(point))
    ofile.write(letter_footer)
    

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 calculate_scale(glyphs):
    """Calculate multiplier to convert potrace's coordinates
    to font coordinates."""
    highest_box = max_y(glyphs)
    return highest_y_coordinate/(potrace_pixel_multiplier*highest_box)

def write_sfd(ofilename, fontname, image, glyphs):
    ofile = file(ofilename, 'w')
    font_name = fontname
    full_name = fontname
    family_name = fontname
    num_letters = len(glyphs)
    scale = calculate_scale(glyphs)
    
    ofile.write(sfd_header % (font_name, full_name, family_name, ascent, descent, num_letters))

    for glyph in glyphs:
        process_glyph(ofile, image, glyph, scale)
    
    ofile.write(sfd_footer)