Changeset 249

Show
Ignore:
Timestamp:
08/29/08 15:31:56 (4 months ago)
Author:
tal
Message:
Tons of fixes and a complete refactoring. It is now possible to subclass the compiler to modify certain parts of the process. Needs documentation.
Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • packages/ufo2fdk/trunk/Lib/ufo2fdk/outlineOTF.py

    r152 r249  
    55from fontTools.cffLib import TopDictIndex, TopDict, CharStrings, SubrsIndex, GlobalSubrsIndex, PrivateDict, IndexedStrings 
    66from fontTools.ttLib.tables.O_S_2f_2 import Panose 
     7from fontTools.ttLib.tables._h_e_a_d import parse_date 
    78from charstringPen import T2CharStringPen 
    89 
    910 
    10 def makeOutlineOTF(font, path, glyphOrder=None): 
    11     otf = TTFont(sfntVersion="OTTO") 
    12     # populate default values 
    13     _setupTable_head(otf, font) 
    14     _setupTable_hhea(otf, font) 
    15     _setupTable_hmtx(otf, font) 
    16     _setupTable_name(otf, font) 
    17     _setupTable_maxp(otf, font) 
    18     _setupTable_cmap(otf, font) 
    19     _setupTable_OS2(otf, font) 
    20     _setupTable_post(otf, font) 
    21     _setupTable_CFF(otf, font) 
    22     # populate the outlines 
    23     if glyphOrder is None: 
    24         glyphOrder = sorted(font.keys()) 
    25     _populate_glyphs(otf, font, glyphOrder) 
    26     # write the file 
    27     otf.save(path) 
    28     # discard the object 
    29     otf.close() 
    30  
    31 def _setupTable_head(otf, font): 
    32     otf["head"] = head = newTable("head") 
    33     head.checkSumAdjustment = 0 # XXX this is a guess 
    34     head.tableVersion = 1.0 
    35     head.fontRevision = 1.0 
    36     head.magicNumber = 0x5F0F3CF5 
    37     head.flags = 0 # XXX this is a guess 
    38     head.unitsPerEm = int(font.info.unitsPerEm) 
    39     rightNow = long(time.time() - time.timezone) 
    40     head.created = rightNow 
    41     head.modified = rightNow 
    42     head.xMin = 0 
    43     head.yMin = 0 
    44     head.xMax = 0 
    45     head.yMax = 0 
    46     head.macStyle = 0 # XXX this is a guess 
    47     head.lowestRecPPEM = 3 # XXX FontValidator describes this as "unreasonably small" 
    48     head.fontDirectionHint = 2 # XXX this is a guess 
    49     head.indexToLocFormat = 0 # XXX this is a guess 
    50     head.glyphDataFormat = 0 
    51  
    52 def _setupTable_name(otf, font): 
    53     # this table must exist, but it can be empty. 
    54     otf["name"] = newTable("name") 
    55  
    56 def _setupTable_maxp(otf, font): 
    57     otf["maxp"] = maxp = newTable("maxp") 
    58     maxp.tableVersion = 0x00005000 
    59  
    60 def _setupTable_cmap(otf, font): 
    61     # XXX is this necessary for the outline source? 
    62     # XXX need to make sure that these are the proper tables to write 
    63     from fontTools.ttLib.tables._c_m_a_p import cmap_format_4, cmap_format_6 
    64     cmap4_0_3 = cmap_format_4(4) 
    65     cmap4_0_3.platformID = 0 
    66     cmap4_0_3.platEncID = 3 
    67     cmap4_0_3.language = 0 
    68     cmap4_0_3.cmap = {} 
    69  
    70     cmap6_1_0 = cmap_format_4(6) 
    71     cmap6_1_0.platformID = 1 
    72     cmap6_1_0.platEncID = 0 
    73     cmap6_1_0.language = 0 
    74     cmap6_1_0.cmap = {} 
    75  
    76     cmap4_3_1 = cmap_format_4(4) 
    77     cmap4_3_1.platformID = 3 
    78     cmap4_3_1.platEncID = 1 
    79     cmap4_3_1.language = 0 
    80     cmap4_3_1.cmap = {} 
    81  
    82     otf["cmap"] = cmap = newTable("cmap") 
    83     cmap.tableVersion = 0 
    84     cmap.tables = [cmap4_0_3]#, cmap6_1_0, cmap4_3_1] # XXX more tables? one of this is preventing compile 
    85  
    86 def _setupTable_OS2(otf, font): 
    87     otf["OS/2"] = os2 = newTable("OS/2") 
    88     os2.version = 0x0003 # XXX has this been bumped up? 
    89     os2.xAvgCharWidth = int(round(font.info.unitsPerEm * 0.5)) # XXX calculate? 
    90     os2.usWeightClass = 400 
    91     os2.usWidthClass = 5 
    92     os2.fsType = 0x0000 
    93     superAndSubscriptSize = int(round(font.info.ascender * 0.85)) # XXX what should the default be? 
    94     os2.ySubscriptXSize = superAndSubscriptSize 
    95     os2.ySubscriptYSize = superAndSubscriptSize 
    96     os2.ySubscriptXOffset = 0 # XXX what should the default be? 
    97     os2.ySubscriptYOffset = int(round(font.info.descender * 0.5)) # XXX what should the default be? 
    98     os2.ySuperscriptXSize = superAndSubscriptSize 
    99     os2.ySuperscriptYSize = superAndSubscriptSize 
    100     os2.ySuperscriptXOffset = 0 # XXX what should the default be? 
    101     os2.ySuperscriptYOffset = font.info.ascender - superAndSubscriptSize # XXX what should the default be? 
    102     os2.yStrikeoutSize = int(round(font.info.unitsPerEm * 0.05)) # XXX what should the default be? 
    103     os2.yStrikeoutPosition = int(round(font.info.unitsPerEm * .23)) # XXX what should the default be? 
    104     os2.sFamilyClass = 0 
    105     panose = Panose() 
    106     panose.bFamilyType = 2 
    107     panose.bSerifStyle = 0 
    108     panose.bWeight = 0 
    109     panose.bProportion = 0 
    110     panose.bContrast = 0 
    111     panose.bStrokeVariation = 0 
    112     panose.bArmStyle = 0 
    113     panose.bLetterForm = 0 
    114     panose.bMidline = 0 
    115     panose.bXHeight = 0 
    116     os2.panose = panose 
    117     os2.ulUnicodeRange1 = 0 
    118     os2.ulUnicodeRange2 = 0 
    119     os2.ulUnicodeRange3 = 0 
    120     os2.ulUnicodeRange4 = 0 
    121     os2.achVendID = "None" # XXX get vendor code from font 
    122     os2.fsSelection = 64 # XXX this is a guess 
    123     os2.fsFirstCharIndex = 0 # usFirstCharIndex 
    124     os2.fsLastCharIndex = 0 # usLastCharIndex 
    125     os2.sTypoAscender = font.info.ascender 
    126     os2.sTypoDescender = -font.info.descender 
    127     os2.sTypoLineGap = 0 
    128     os2.usWinAscent = font.info.ascender 
    129     os2.usWinDescent = font.info.descender 
    130     os2.ulCodePageRange1 = 0 
    131     os2.ulCodePageRange2 = 0 
    132     os2.sxHeight = int(round(font.info.ascender * 0.5)) 
    133     os2.sCapHeight = font.info.ascender 
    134     os2.usDefaultChar = 0 
    135     os2.usBreakChar = 1 
    136     os2.usMaxContex = 0 # usMaxContext 
    137  
    138 def _setupTable_hmtx(otf,  font): 
    139     # this is required, but it can be empty 
    140     otf["hmtx"] = hmtx = newTable("hmtx") 
    141     hmtx.metrics = {} 
    142  
    143 def _setupTable_hhea(otf, font): 
    144     otf["hhea"] = hhea = newTable("hhea") 
    145     hhea.tableVersion = 1.0 
    146     hhea.ascent = int(font.info.ascender) 
    147     hhea.descent = -int(font.info.descender) 
    148     hhea.lineGap = 0 
    149     hhea.advanceWidthMax = 0 
    150     hhea.minLeftSideBearing = 0 
    151     hhea.minRightSideBearing = 0 
    152     hhea.xMaxExtent = 0 
    153     hhea.caretSlopeRise = 1 
    154     hhea.caretSlopeRun = 0 
    155     hhea.caretOffset = 0 # XXX this is a guess 
    156     hhea.reserved0 = 0 
    157     hhea.reserved1 = 0 
    158     hhea.reserved2 = 0 
    159     hhea.reserved3 = 0 
    160     hhea.metricDataFormat = 0 
    161     hhea.numberOfHMetrics = 0 
    162  
    163 def _setupTable_post(otf, font): 
    164     otf["post"] = post = newTable("post") 
    165     post.formatType = 3.0 
    166     italicAngle = font.info.italicAngle 
    167     if italicAngle is None: 
    168         italicAngle = 0 
    169     post.italicAngle = italicAngle 
    170     post.underlinePosition = -int(round(font.info.descender * 0.3)) # XXX this is a guess 
    171     post.underlineThickness = int(round(font.info.unitsPerEm * .05)) # XXX this is a guess 
    172     post.isFixedPitch = 0 
    173     post.minMemType42 = 0 # XXX this is a guess 
    174     post.maxMemType42 = 0 # XXX this is a guess 
    175     post.minMemType1 = 0 # XXX this is a guess 
    176     post.maxMemType1 = 0 # XXX this is a guess 
    177  
    178 def _setupTable_CFF(otf, font): 
    179     otf["CFF "] = cff = newTable("CFF ") 
    180     cff = cff.cff 
    181     cff.major = 1 
    182     cff.minor = 0 
    183     cff.hdrSize = 4 
    184     cff.offSize = 4 
    185     cff.fontNames = [] # XXX need a real font name! 
    186     strings = IndexedStrings() 
    187     cff.strings = strings 
    188     private = PrivateDict(strings=strings) 
    189     private.rawDict.update(private.defaults) 
    190     globalSubrs = GlobalSubrsIndex(private=private) 
    191     topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) 
    192     topDict.Private = private 
    193     topDict.CharStrings = CharStrings(file=None, charset=None, 
    194         globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) 
    195     topDict.CharStrings.charStringsAreIndexed = True 
    196     topDict.charset = [] 
    197     topDict.CharStrings.charStringsIndex = SubrsIndex(private=private, globalSubrs=globalSubrs) 
    198     cff.topDictIndex = topDictIndex = TopDictIndex() 
    199     topDictIndex.append(topDict) 
    200     topDictIndex.strings = strings 
    201     cff.GlobalSubrs = globalSubrs 
    202     # populate data from the font. 
    203     # this is required for a basic CFF table. 
    204     info = font.info 
    205     cff.fontNames.append(info.fontName) 
    206     topDict = cff.topDictIndex[0] 
    207     if hasattr(info, "fullName"): 
    208         topDict.FullName = makePSName(font) # XXX this should probably draw from a real value 
    209     if hasattr(info, "familyName"): 
    210         topDict.FamilyName = info.familyName 
    211     if hasattr(info, "styleName"): 
    212         topDict.Weight = info.styleName 
    213     if hasattr(info, "fontName"): 
    214         topDict.FontName = makePSName(font) # XXX this should probably draw from a real value 
    215  
    216 def _populate_glyphs(otf, font, glyphOrder): 
    217     mapping, widths, lefts, rights, fontBBox = _populate_CFF(otf, font, glyphOrder) 
    218     glyphCount = len(widths) 
    219     # populate the cmap table 
    220     # XXX do we need to do this for the outline source? 
    221     cmap = otf["cmap"] 
    222     for unicodeValue, glyphName in mapping.items(): 
    223         for table in cmap.tables: 
    224             pID = table.platformID 
    225             eID = table.platEncID 
    226             # XXX are these the only valid tables? 
    227             if (pID, eID) in [(0, 3), (3, 1), (0, 4), (3, 10)]: 
    228                 table.cmap[unicodeValue] = glyphName 
    229     # populate the hmtx table 
    230     hmtx = otf["hmtx"] 
    231     for glyphName, width in widths.items(): 
    232         left = lefts[glyphName] 
    233         right = rights[glyphName] 
    234         hmtx[glyphName] = (width, left) 
    235     # update the OS/2 table 
    236     os2 = otf["OS/2"] 
    237     # the OS/2 doc states: 
    238     # """ 
    239     # The value for xAvgCharWidth is calculated by obtaining the arithmetic 
    240     # average of the width of all non-zero width glyphs in the font. 
    241     # """ 
    242     # "non-zero width glyphs"? does that mean that glyphs with 
    243     # a width of zero should not be counted? that doesn't seem right. 
    244     avgWidth = int(round(sum(widths.values()) / glyphCount)) 
    245     os2.xAvgCharWidth = avgWidth 
    246     minIndex = 32 # it is safe to asume that this is present since a space glyph is inserted 
    247     maxIndex = max(mapping.keys()) 
    248     if maxIndex >= 0xFFFF: 
    249         # see OS/2 docs 
    250         # need to find a value lower than 0xFFFF. 
    251         # shouldn't get to this point though. 
    252         raise NotImplementedError 
    253     os2.fsFirstCharIndex = minIndex 
    254     os2.fsLastCharIndex = maxIndex 
    255     os2.usBreakChar = 32 
    256     os2.usDefaultChar = 32 
    257     # update the hhea table 
    258     hhea = otf["hhea"] 
    259     hhea.advanceWidthMax = max(widths.values()) 
    260     hhea.minLeftSideBearing = min(lefts.values()) 
    261     rightSidebearing = [widths[glyphName] - rights[glyphName] for glyphName in widths.keys()] 
    262     hhea.minRightSideBearing = min(rightSidebearing) 
    263     hhea.xMaxExtent = max(rights.values()) # XXX the docs give an equation that is murky: Max(lsb + (xMax - xMin)) 
    264     hhea.numberOfHMetrics = glyphCount 
    265     # update the head table 
    266     head = otf["head"] 
    267     head.xMin, head.yMin, head.xMax, head.yMax = fontBBox 
    268     # update the post table 
    269     post = otf["post"] 
    270     # XXX make a guess at this? 
    271     isFixedPitch = True 
    272     testWidth = None 
    273     for width in widths.values(): 
    274         if testWidth is None: 
    275             testWidth = width 
    276             continue 
    277         if width != testWidth: 
    278             isFixedPitch = False 
    279             break 
    280     post.isFixedPitch = isFixedPitch 
    281  
    282 def _populate_CFF(otf, font, glyphOrder): 
    283     orderedGlyphs = _getOrderedGlyphs(font, glyphOrder) 
    284     # as the CFF table is built, a bunch of info 
    285     # for the other tables is stored. 
    286     mapping = {} 
    287     widths = {} 
    288     lefts = {} 
    289     rights = {} 
    290     fontBBox = getFontBBox(font) 
    291     # build the CFF table 
    292     cff = otf["CFF "].cff 
    293     topDict = cff.topDictIndex[0] 
    294     charStrings = topDict.CharStrings 
    295     charStringsIndex = charStrings.charStringsIndex 
    296     private = charStringsIndex.private 
    297     globalSubrs = charStringsIndex.globalSubrs 
    298     for glyph in orderedGlyphs: 
    299         glyphName = glyph.name 
    300         glyphWidth = glyph.width 
    301         unicodes = glyph.unicodes 
    302         if hasattr(glyph, "box"): 
    303             bounds = glyph.box 
    304         else: 
    305             bounds = glyph.bounds 
    306         if bounds is not None: 
    307             xMin, yMin, xMax, yMax = bounds 
    308         else: 
    309             xMin = 0 
    310             xMax = 0 
    311         # write the char string 
    312         pen = T2CharStringPen(glyphWidth, font) 
     11class OutlineOTFCompiler(object): 
     12 
     13    def __init__(self, font, path, glyphOrder=None): 
     14        self.ufo = font 
     15        self.path = path 
     16        # make any missing glyphs and store them locally 
     17        missingRequiredGlyphs = self.makeMissingRequiredGlyphs() 
     18        # make a dict of all glyphs 
     19        self.allGlyphs = {} 
     20        for glyph in font: 
     21            self.allGlyphs[glyph.name] = glyph 
     22        self.allGlyphs.update(missingRequiredGlyphs) 
     23        # store the glyph order 
     24        if glyphOrder is None: 
     25            glyphOrder = sorted(self.allGlyphs.keys()) 
     26        self.glyphOrder = self.makeOfficialGlyphOrder(glyphOrder) 
     27        # make a reusable bounding box 
     28        self.fontBoundingBox = self.makeFontBoundingBox() 
     29        # make a reusable character mapping 
     30        self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() 
     31 
     32    # ----------- 
     33    # Main Method 
     34    # ----------- 
     35 
     36    def compile(self): 
     37        self.otf = TTFont(sfntVersion="OTTO") 
     38        # populate basic tables 
     39        self.setupTable_head() 
     40        self.setupTable_hhea() 
     41        self.setupTable_hmtx() 
     42        self.setupTable_name() 
     43        self.setupTable_maxp() 
     44        self.setupTable_cmap() 
     45        self.setupTable_OS2() 
     46        self.setupTable_post() 
     47        self.setupTable_CFF() 
     48        self.setupOtherTables() 
     49        # write the file 
     50        self.otf.save(self.path) 
     51        # discard the object 
     52        self.otf.close() 
     53        del self.otf 
     54 
     55    # ----- 
     56    # Tools 
     57    # ----- 
     58 
     59    def makeFontBoundingBox(self): 
     60        return getFontBBox(self.allGlyphs.values()) 
     61 
     62    def makeUnicodeToGlyphNameMapping(self): 
     63        mapping = {} 
     64        for glyphName, glyph in self.allGlyphs.items(): 
     65            unicodes = glyph.unicodes 
     66            for uni in unicodes: 
     67                mapping[uni] = glyphName 
     68        return mapping 
     69 
     70    def makeMissingRequiredGlyphs(self): 
     71        glyphs = {} 
     72        defaultWidth = int(round(self.ufo.info.unitsPerEm * 0.5)) 
     73        if ".notdef" not in self.ufo: 
     74            glyphs[".notdef"] = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=self.ufo.info.unitsPerEm, ascender=self.ufo.info.ascender, descender=self.ufo.info.descender) 
     75        if "space" not in self.ufo: 
     76            glyphs["space"] = StubGlyph(name="space", width=defaultWidth, unitsPerEm=self.ufo.info.unitsPerEm, ascender=self.ufo.info.ascender, descender=self.ufo.info.descender, unicodes=[32]) 
     77        return glyphs 
     78 
     79    def makeOfficialGlyphOrder(self, glyphOrder): 
     80        allGlyphs = self.allGlyphs 
     81        orderedGlyphs = [".notdef", "space"] 
     82        for glyphName in glyphOrder: 
     83            if glyphName in [".notdef", "space"]: 
     84                continue 
     85            orderedGlyphs.append(glyphName) 
     86        for glyphName in sorted(self.allGlyphs.keys()): 
     87            if glyphName not in orderedGlyphs: 
     88                orderedGlyphs.append(glyphName) 
     89        return orderedGlyphs 
     90 
     91    def getCharStringForGlyph(self, glyph, private, globalSubrs): 
     92        pen = T2CharStringPen(glyph.width, self.allGlyphs) 
    31393        glyph.draw(pen) 
    31494        charString = pen.getCharString(private, globalSubrs) 
    315         exists = charStrings.has_key(glyphName) 
    316         if exists: 
    317             # XXX a glyph already has this name. should we choke? 
    318             glyphID = charStrings.charStrings[glyphName] 
    319             charStringsIndex.items[glyphID] = charString 
    320         else: 
    321             charStringsIndex.append(charString) 
    322             glyphID = len(topDict.charset) 
    323             charStrings.charStrings[glyphName] = glyphID 
    324             topDict.charset.append(glyphName) 
    325         # store needed values 
    326         if unicodes: 
    327             for unicodeValue in unicodes: 
    328                 mapping[unicodeValue] = glyphName 
    329         widths[glyphName] = glyphWidth 
    330         lefts[glyphName] = xMin 
    331         rights[glyphName] = xMax 
    332     topDict.FontBBox = fontBBox 
    333     # write the glyph order 
    334     glyphOrder = [glyph.name for glyph in orderedGlyphs] 
    335     otf.setGlyphOrder(glyphOrder) 
    336     # return the saved data 
    337     return mapping, widths, lefts, rights, fontBBox 
    338  
    339 def _getOrderedGlyphs(font, glyphOrder): 
    340     orderedGlyphs = [] 
    341     defaultWidth = int(round(font.info.unitsPerEm * 0.5)) 
    342     glyphOrder = list(glyphOrder) 
    343     # .notdef should be the first glyph. create it if it does not exist. 
    344     if ".notdef" not in font: 
    345         notdef = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=font.info.unitsPerEm, ascender=font.info.ascender, descender=font.info.descender) 
    346     else: 
    347         notdef = font[".notdef"] 
    348     orderedGlyphs.append(notdef) 
    349     # space should be the second glyph. create it if it does not exist. 
    350     if "space" not in font: 
    351         space = StubGlyph(name="space", width=defaultWidth, unitsPerEm=font.info.unitsPerEm, ascender=font.info.ascender, descender=font.info.descender, unicodes=[32]) 
    352     else: 
    353         space = font["space"] 
    354     orderedGlyphs.append(space) 
    355     # make sure no glyphs are missing from the order 
    356     for glyphName in sorted(font.keys()): 
    357         if glyphName not in glyphOrder: 
    358             glyphOrder.append(glyphName) 
    359     # now gather the glyphs 
    360     for glyphName in glyphOrder: 
    361         if glyphName in [".notdef", "space"]: 
    362             continue 
    363         orderedGlyphs.append(font[glyphName]) 
    364     # done. 
    365     return orderedGlyphs 
     95        return charString 
     96 
     97    # -------------- 
     98    # Table Builders 
     99    # -------------- 
     100 
     101    def setupTable_head(self): 
     102        self.otf["head"] = head = newTable("head") 
     103        head.checkSumAdjustment = 0 # XXX this is a guess 
     104        head.tableVersion = 1.0 
     105        head.fontRevision = 1.0 
     106        head.magicNumber = 0x5F0F3CF5 
     107        # upm 
     108        head.unitsPerEm = int(self.ufo.info.unitsPerEm) 
     109        # times 
     110        rightNow = parse_date(time.asctime(time.gmtime())) 
     111        head.created = rightNow 
     112        head.modified = rightNow 
     113        # bounding box 
     114        xMin, yMin, xMax, yMax = self.fontBoundingBox 
     115        head.xMin = xMin 
     116        head.yMin = yMin 
     117        head.xMax = xMax 
     118        head.yMax = yMax 
     119        # style mapping 
     120        head.macStyle = 0 # XXX this is a guess 
     121        # misc 
     122        head.flags = 3 # XXX this is a guess 
     123        head.lowestRecPPEM = 3 # XXX FontValidator describes this as "unreasonably small" 
     124        head.fontDirectionHint = 2 # XXX this is a guess 
     125        head.indexToLocFormat = 0 # XXX this is a guess 
     126        head.glyphDataFormat = 0 
     127 
     128    def setupTable_name(self): 
     129        self.otf["name"] = newTable("name") 
     130 
     131    def setupTable_maxp(self): 
     132        self.otf["maxp"] = maxp = newTable("maxp") 
     133        maxp.tableVersion = 0x00005000 
     134 
     135    def setupTable_cmap(self): 
     136        from fontTools.ttLib.tables._c_m_a_p import cmap_format_4 
     137        # mac 
     138        cmap4_0_3 = cmap_format_4(4) 
     139        cmap4_0_3.platformID = 0 
     140        cmap4_0_3.platEncID = 3 
     141        cmap4_0_3.language = 0 
     142        cmap4_0_3.cmap = dict(self.unicodeToGlyphNameMapping) 
     143        # windows 
     144        cmap4_3_1 = cmap_format_4(4) 
     145        cmap4_3_1.platformID = 3 
     146        cmap4_3_1.platEncID = 1 
     147        cmap4_3_1.language = 0 
     148        cmap4_3_1.cmap = dict(self.unicodeToGlyphNameMapping) 
     149        # store 
     150        self.otf["cmap"] = cmap = newTable("cmap") 
     151        cmap.tableVersion = 0 
     152        cmap.tables = [cmap4_0_3, cmap4_3_1] 
     153 
     154    def setupTable_OS2(self): 
     155        self.otf["OS/2"] = os2 = newTable("OS/2") 
     156        os2.version = 0x0003 # XXX has this been bumped up? 
     157        # average glyph width 
     158        widths = [glyph.width for glyph in self.allGlyphs.values() if glyph.width > 0] 
     159        os2.xAvgCharWidth = int(round(sum(widths) / len(widths))) 
     160        # weight and width classes 
     161        os2.usWeightClass = 400 
     162        os2.usWidthClass = 5 
     163        # embedding 
     164        os2.fsType = 0 
     165        # superscript and subscript 
     166        superAndSubscriptSize = int(round(self.ufo.info.ascender * 0.85)) # XXX what should the default be? 
     167        os2.ySubscriptXSize = superAndSubscriptSize 
     168        os2.ySubscriptYSize = superAndSubscriptSize 
     169        os2.ySubscriptXOffset = 0 # XXX what should the default be? 
     170        os2.ySubscriptYOffset = int(round(self.ufo.info.descender * 0.5)) # XXX what should the default be? 
     171        os2.ySuperscriptXSize = superAndSubscriptSize 
     172        os2.ySuperscriptYSize = superAndSubscriptSize 
     173        os2.ySuperscriptXOffset = 0 # XXX what should the default be? 
     174        os2.ySuperscriptYOffset = self.ufo.info.ascender - superAndSubscriptSize # XXX what should the default be? 
     175        os2.yStrikeoutSize = int(round(self.ufo.info.unitsPerEm * 0.05)) # XXX what should the default be? 
     176        os2.yStrikeoutPosition = int(round(self.ufo.info.unitsPerEm * .23)) # XXX what should the default be? 
     177        os2.sFamilyClass = 0 
     178        # Panose 
     179        panose = Panose() 
     180        panose.bFamilyType = 0 
     181        panose.bSerifStyle = 0 
     182        panose.bWeight = 0 
     183        panose.bProportion = 0 
     184        panose.bContrast = 0 
     185        panose.bStrokeVariation = 0 
     186        panose.bArmStyle = 0 
     187        panose.bLetterForm = 0 
     188        panose.bMidline = 0 
     189        panose.bXHeight = 0 
     190        os2.panose = panose 
     191        # Unicode and code page ranges 
     192        os2.ulUnicodeRange1 = 0 
     193        os2.ulUnicodeRange2 = 0 
     194        os2.ulUnicodeRange3 = 0 
     195        os2.ulUnicodeRange4 = 0 
     196        os2.ulCodePageRange1 = 0 
     197        os2.ulCodePageRange2 = 0 
     198        # vendor id 
     199        os2.achVendID = "None" # XXX get vendor code from font 
     200        # vertical metrics 
     201        os2.sxHeight = int(round(self.ufo.info.ascender * 0.5)) 
     202        os2.sCapHeight = self.ufo.info.ascender 
     203        os2.sTypoAscender = self.ufo.info.unitsPerEm + self.ufo.info.descender 
     204        os2.sTypoDescender = self.ufo.info.descender 
     205        os2.sTypoLineGap = 50 
     206        os2.usWinAscent = self.fontBoundingBox[3] 
     207        os2.usWinDescent = self.fontBoundingBox[1] 
     208        # style mapping 
     209        os2.fsSelection = 0 # XXX this is a guess 
     210        # characetr indexes 
     211        unicodes = [i for i in self.unicodeToGlyphNameMapping.keys() if i is not None] 
     212        minIndex = min(unicodes) 
     213        maxIndex = max(unicodes) 
     214        if maxIndex >= 0xFFFF: 
     215            # see OS/2 docs 
     216            # need to find a value lower than 0xFFFF. 
     217            # shouldn't get to this point though. 
     218            raise NotImplementedError 
     219        os2.fsFirstCharIndex = minIndex 
     220        os2.fsLastCharIndex = maxIndex 
     221        os2.usBreakChar = 32 
     222        os2.usDefaultChar = 0 
     223        # maximum contextual lookup length 
     224        os2.usMaxContex = 0 
     225 
     226    def setupTable_hmtx(self): 
     227        self.otf["hmtx"] = hmtx = newTable("hmtx") 
     228        hmtx.metrics = {} 
     229        for glyphName, glyph in self.allGlyphs.items(): 
     230            width = glyph.width 
     231            left = 0 
     232            if len(glyph) or len(glyph.components): 
     233                left = glyph.leftMargin 
     234            hmtx[glyphName] = (width, left) 
     235 
     236    def setupTable_hhea(self): 
     237        self.otf["hhea"] = hhea = newTable("hhea") 
     238        hhea.tableVersion = 1.0 
     239        # vertical metrics 
     240        hhea.ascent = int(self.ufo.info.unitsPerEm + self.ufo.info.descender) 
     241        hhea.descent = int(self.ufo.info.descender) 
     242        hhea.lineGap = 50 
     243        # horizontal metrics 
     244        widths = [] 
     245        lefts = [] 
     246        rights = [] 
     247        extents = [] 
     248        for glyph in self.allGlyphs.values(): 
     249            left = glyph.leftMargin 
     250            right = glyph.rightMargin 
     251            if left is None: 
     252                left = 0 
     253            if right is None: 
     254                right = 0 
     255            widths.append(glyph.width) 
     256            lefts.append(left) 
     257            rights.append(right) 
     258            bounds = glyph.bounds 
     259            if bounds is not None: 
     260                xMin, yMin, xMax, yMax = glyph.bounds 
     261            else: 
     262                xMin = 0 
     263                xMax = 0 
     264            extent = left + (xMax - xMin) # equation from spec for calculating xMaxExtent: Max(lsb + (xMax - xMin)) 
     265            extents.append(extent) 
     266        hhea.advanceWidthMax = max(widths) 
     267        hhea.minLeftSideBearing = min(lefts) 
     268        hhea.minRightSideBearing = min(rights) 
     269        hhea.xMaxExtent = max(extents) 
     270        # misc 
     271        hhea.caretSlopeRise = 1 
     272        hhea.caretSlopeRun = 0 
     273        hhea.caretOffset = 0 # XXX this is a guess 
     274        hhea.reserved0 = 0 
     275        hhea.reserved1 = 0 
     276        hhea.reserved2 = 0 
     277        hhea.reserved3 = 0 
     278        hhea.metricDataFormat = 0 
     279        # glyph count 
     280        hhea.numberOfHMetrics = len(self.allGlyphs) 
     281 
     282    def setupTable_post(self): 
     283        self.otf["post"] = post = newTable("post") 
     284        post.formatType = 3.0 
     285        # italic angle 
     286        italicAngle = self.ufo.info.italicAngle 
     287        if italicAngle is None: 
     288            italicAngle = 0 
     289        post.italicAngle = italicAngle 
     290        # underline 
     291        post.underlinePosition = int(round(self.ufo.info.descender * 0.3)) # XXX this is a guess 
     292        post.underlineThickness = int(round(self.ufo.info.unitsPerEm * .05)) # XXX this is a guess 
     293        # determine if the font has a fixed width 
     294        widths = set([glyph.width for glyph in self.allGlyphs.values()]) 
     295        post.isFixedPitch = bool(len(widths) == 1) 
     296        # misc 
     297        post.minMemType42 = 0 # XXX this is a guess 
     298        post.maxMemType42 = 0 # XXX this is a guess 
     299        post.minMemType1 = 0 # XXX this is a guess 
     300        post.maxMemType1 = 0 # XXX this is a guess 
     301 
     302    def setupTable_CFF(self): 
     303        self.otf["CFF "] = cff = newTable("CFF ") 
     304        cff = cff.cff 
     305        # set up the basics 
     306        cff.major = 1 
     307        cff.minor = 0 
     308        cff.hdrSize = 4 
     309        cff.offSize = 4 
     310        cff.fontNames = [] 
     311        strings = IndexedStrings() 
     312        cff.strings = strings 
     313        private = PrivateDict(strings=strings) 
     314        private.rawDict.update(private.defaults) 
     315        globalSubrs = GlobalSubrsIndex(private=private) 
     316        topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) 
     317        topDict.Private = private 
     318        charStrings = topDict.CharStrings = CharStrings(file=None, charset=None, 
     319            globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) 
     320        charStrings.charStringsAreIndexed = True 
     321        topDict.charset = [] 
     322        charStringsIndex = charStrings.charStringsIndex = SubrsIndex(private=private, globalSubrs=globalSubrs) 
     323        cff.topDictIndex = topDictIndex = TopDictIndex() 
     324        topDictIndex.append(topDict) 
     325        topDictIndex.strings = strings 
     326        cff.GlobalSubrs = globalSubrs 
     327        # populate naming data 
     328        info = self.ufo.info 
     329        psName = makePSName(self.ufo) 
     330        cff.fontNames.append(psName) 
     331        topDict = cff.topDictIndex[0] 
     332        topDict.FullName = "%s %s" % (info.familyName, info.styleName) 
     333        topDict.FamilyName = info.familyName 
     334        topDict.Weight = info.styleName 
     335        topDict.FontName = psName 
     336        # populate glyphs 
     337        for glyphName in self.glyphOrder: 
     338            glyph = self.allGlyphs[glyphName] 
     339            glyphWidth = glyph.width 
     340            unicodes = glyph.unicodes 
     341            bounds = glyph.bounds 
     342            if bounds is not None: 
     343                xMin, yMin, xMax, yMax = bounds 
     344            else: 
     345                xMin = 0 
     346                xMax = 0 
     347            charString = self.getCharStringForGlyph(glyph, private, globalSubrs) 
     348            # add to the font 
     349            exists = charStrings.has_key(glyphName) 
     350            if exists: 
     351                # XXX a glyph already has this name. should we choke? 
     352                glyphID = charStrings.charStrings[glyphName] 
     353                charStringsIndex.items[glyphID] = charString 
     354            else: 
     355                charStringsIndex.append(charString) 
     356                glyphID = len(topDict.charset) 
     357                charStrings.charStrings[glyphName] = glyphID 
     358                topDict.charset.append(glyphName) 
     359        topDict.FontBBox = self.fontBoundingBox 
     360        # write the glyph order 
     361        self.otf.setGlyphOrder(self.glyphOrder) 
     362 
     363    def setupOtherTables(self): 
     364        pass 
     365 
    366366 
    367367def makePSName(font): 
     
    374374    rect = None 
    375375    for glyph in font: 
    376         if hasattr(glyph, "box"): 
    377             bounds = glyph.box 
    378         else: 
    379             bounds = glyph.bounds 
     376        bounds = glyph.bounds 
    380377        if rect is None: 
    381378            rect = bounds 
     
    403400        self.descender = descender 
    404401        self.unicodes = unicodes 
     402        self.components = [] 
    405403        if unicodes: 
    406404            self.unicode = unicodes[0] 
     
    409407        if name == ".notdef": 
    410408            self.draw = self._drawDefaultNotdef 
     409 
     410    def __len__(self): 
     411        if self.name == ".notdef": 
     412            return 1 
     413        return 0 
     414 
     415    def _get_leftMargin(self): 
     416        return self.bounds[0] 
     417 
     418    leftMargin = property(_get_leftMargin) 
     419 
     420    def _get_rightMargin(self): 
     421        bounds = self.bounds 
     422        if bounds is None: 
     423            return 0 
     424        xMin, yMin, xMax, yMax = bounds 
     425        return self.width - bounds[2] 
     426 
     427    rightMargin = property(_get_rightMargin) 
    411428 
    412429    def draw(self, pen): 
     
    446463 
    447464    bounds = property(_get_bounds) 
    448