Source code for fontinfo

"""
Copyright @ 2013 Mathias Westerdahl

The font info reads the .fontinfo and creates the layers, effects, and blends

.. py:attribute:: name = ''

    The path of the font file. Relative paths are relative to the .fontinfo file. Supports .ttf files.
    For a complete list of supported formats, see `Freetype2 <http://www.freetype.org/freetype2/index.html>`_

.. py:attribute:: size = 32

    The size of the font (in pixels)
    
.. py:attribute:: leading = 0.0

    The leading of the font.
    
.. py:attribute:: tracking = 0.0

    The tracking of the font.

.. py:attribute:: dpi = 72

    The resolution of the font when being rendered.

.. py:attribute:: padding = 0

    The amount of padding that is inserted between two glyphs when rendered to the bitmap.

.. py:attribute:: internalpadding = 0, 0

    The amount of padding (x, y) that is added *internally* for a glyph.
    E.g. making room for manual fixup in a image editing program.

.. py:attribute:: useadvanceaswidth = 0

    For japanese fonts, the space in the glyph is important.
    Therefore, this flag was introduced to respect the space of a glyph.
    Set to 1 for Japanese fonts.
    
.. py:attribute:: usepairkernings = 1

    Enables the collecting of the pair kernings table.

.. py:attribute:: letters = 20-7e

    Specifies what letters should be included in the font. It can hold these formats:
    
    #. A list of hexadecimal digits. The list is comma separated and can specify ranges with the '-' character::
    
        letters = 20-7c,7d,7e
        
    #. A filename. The file contains the actual letters.

.. py:attribute:: bgcolor = 0, 0, 0

    The background color of the texture

.. py:attribute:: fgcolor = 1, 1, 1
    
    The color of the glyph
        
.. py:attribute:: antialias = normal

    Enables antialiasing for the glyph rendering (the first pass).
    Possible values are **'none', 'light' and 'normal'**.
    
.. py:attribute:: texturerender = fonttex_bitmap
    
    A path to the module that renders the glyphs to the texture. Must implement a *image = render(info)* function.
    The default is :doc:`fonttex_bitmap`

.. py:attribute:: texturewriter = fonttexout_pil

    A path to the module that writes the texture to disc. Must implement a *write(options, info, image)* function.
    The default is :doc:`fonttexout_pil`

.. py:attribute:: writer = fontout_json
    
    A path to the module that writes the glyph info. Must implement a *write(options, info, pairkernings)* function.
    The default is :doc:`fontout_json`
    
.. py:attribute:: layers = ''

    A list of layers (:py:class:`fonteffects.Layer`) that will be applied in order.
        
.. py:attribute:: effects = ''

    A list of post effects that will be applied in order, after the layers have been rendered.


"""
import os, sys, struct
from ConfigParser import SafeConfigParser
from fontutils import FontException
import fontblend
import fonteffects


def GetDefaultOptions():
    defaults = dict()
    defaults['name'] = 'not set'
    defaults['size'] = '32'
    defaults['leading'] = '0.0'
    defaults['tracking'] = '0.0'
    defaults['dpi'] = '72'
    defaults['padding'] = '0'
    defaults['internalpadding'] = '0, 0'
    defaults['useadvanceaswidth'] = '0'  # E.g. set to 1 for japanese fonts
    defaults['usepairkernings'] = '1'
    
    defaults['letters'] = '20-7e'
    defaults['bgcolor'] = '0, 0, 0'
    defaults['fgcolor'] = '1, 1, 1'
    defaults['antialias'] = 'normal'

    defaults['writer'] = 'fontout_json'
    
    defaults['texturerender'] = 'fonttex_bitmap'
    defaults['texturesize'] = '512, 512'
    defaults['textureoffset'] = '0, 0'
    defaults['usepremultipliedalpha'] = '0'
    
    defaults['texturewriter'] = 'fonttexout_pil'
    defaults['textureformat'] = '.png'
    defaults['texturechannels'] = 'RGBA'
    
    defaults['layers'] = ''
    defaults['posteffects'] = ''
    return defaults


[docs]class SFontInfo(object): """ The input format (.fontinfo) """ def __init__(self, options): """ Reads the font info and converts the info into usable members in this struct """ assert os.path.exists(options.input), "The file doesn't exist: %s\nCwd: %s" % (options.input, os.getcwd()) self._init(options) def _init(self, options): defaults = GetDefaultOptions() cfg = SafeConfigParser() cfg.read(options.input) for name in defaults.iterkeys(): if cfg.has_option('default', name): setattr(self, name, cfg.get('default', name) ) else: setattr(self, name, defaults[name] ) self.functionlist = dict() for section in cfg.sections(): if section in ['default']: continue vars = dict(cfg.items(section)) vars['section'] = section typename = vars.get('type', None) if typename is None: raise FontException("%s: There is no type specified" % section) try: cls = fonteffects.GetClassByName(typename) except KeyError: raise FontException("%s: There is no function type named %s" % (section, typename)) self.functionlist[section] = cls(options, **vars) self.functionlist[section].name = section d = dict() d.update( self.functionlist ) d.update( fontblend.BLENDFUNCTIONS ) d['Layer'] = fonteffects.Layer if not cfg.has_option('default', 'layers'): raise FontException("A font must have at least one layer") self.layers = eval( cfg.get("default", "layers"), d, d ) if cfg.has_option('default', 'posteffects'): self.posteffects = eval( cfg.get("default", "posteffects"), d, d ) else: self.posteffects = [] if not os.path.isabs(self.name): self.name = os.path.join( os.path.dirname(options.input), self.name ) self.leading = float(eval(self.leading)) self.tracking = float(eval(self.tracking)) self.size = int(eval(self.size)) self.dpi = int(self.dpi) self.texturesize = tuple( map( int, self.texturesize.split(',') ) ) self.textureoffset = tuple( map( int, self.textureoffset.split(',') ) ) texturerender = __import__(self.texturerender) texturerender = getattr(texturerender, 'render', None) if not texturerender: raise FontException("The module %s doesn't implement render()" % self.texturerender) self.texturerender = texturerender texturewriter = __import__(self.texturewriter) texturewriter = getattr(texturewriter, 'write', None) if not texturewriter: raise FontException("The module %s doesn't implement write()" % self.texturewriter) self.texturewriter = texturewriter basedir, basename = os.path.split(self.writer) if basedir and not basedir in sys.path: sys.path.append(basedir) basename, ext = os.path.splitext(basename) writer = __import__(basename) if not getattr(writer, 'write', None): raise FontException("The module %s doesn't implement write()" % self.writer) self.writer = writer self.padding = int(self.padding) self.internalpadding = tuple( map( int, self.internalpadding.split(',') ) ) self.useadvanceaswidth = int(self.useadvanceaswidth) self.usepairkernings = int(self.usepairkernings) self.usepremultipliedalpha = int(self.usepremultipliedalpha) self.bgcolor = eval(self.bgcolor) self.fgcolor = eval(self.fgcolor) self.bgcolor = map( lambda x: float(x)/255.0 if isinstance(x, int) else x, self.bgcolor ) self.fgcolor = map( lambda x: float(x)/255.0 if isinstance(x, int) else x, self.fgcolor ) letters_path = self.letters if not os.path.isabs(self.letters): letters_path = os.path.join( os.path.dirname(options.input), self.letters ) if os.path.exists(letters_path): with open(letters_path, 'rb') as f: data = f.read() data = data.replace('\n', '').replace('\r', '') if not data: raise f.FontException("The file '%s' was empty" % letters_path) try: letters = [ ord(c) for c in str(data) ] except UnicodeEncodeError: letters = [ ord(c) for c in unicode(data) ] else: letters = [] for token in self.letters.split(','): if not token: continue limits = token.split('-') if len(limits) == 1: letters.append( int(limits[0], 16) ) else: letters += range( int(limits[0], 16), int(limits[1], 16) + 1 ) self.letters = letters if not self.letters: raise FontException("There were no letters specified!")
def ReadFormat(f, format): return struct.unpack(format, f.read( struct.calcsize(format) ) ) class SPackedFontInfo(object): def __init__(self, texturename='', info=None): self.texture = texturename if info: self.name = info.name self.sizepixels = info.size # the size in pixels self.sizefont = info.fontsize # the size of the font self.spacing = info.padding self.texturesize = info.texturesize self.ascender = info.ascender self.descender = info.descender self.maxsize = info.maxsize else: self.name = '' self.texturesize = [0, 0] self.sizepixels = 0 self.sizefont = 0 self.spacing = 0 self.ascender = 0 self.descender = 0 self.maxsize = [0, 0] def __str__(self): s = '' for n, v in self.__dict__.iteritems(): if n.startswith('_'): continue s += '%s = %s\n' % (n, str(v)) return s def write(self, f, endian): s = struct.pack(endian + '64s256s', self.name, self.texture ) # 16 bytes s += struct.pack(endian + 'BBBBbBBxHHxxxx', self.sizepixels, self.sizefont, self.spacing, self.ascender, self.descender, self.maxsize[0], self.maxsize[1], self.texturesize[0], self.texturesize[1] ) f.write( s ) def read(self, f, endian): self.name, self.texture = ReadFormat(f, endian + '64s256s') self.name = self.name[:self.name.find('\0')] self.texture = self.texture[:self.texture.find('\0')] self.size, self.fontsize, self.spacing, self.ascender, self.descender, self.maxsize[0], self.maxsize[1], self.texturesize[0], self.texturesize[1] = ReadFormat(f, endian + 'BBBBbBBxHHxxxx') class SFontChar(object): format = 'HHBBbb' def __init__(self, glyph=None): if glyph: self.x, self.y, self.width, self.height = getattr(glyph, 'bitmapbox', (0, 0, 0, 0) ) self.utf8 = glyph.utf8 self.advanceX = glyph.info.advance self.bearingY = glyph.info.bearingY else: assert( False ) self.utf8 = 0 self.x = 0 self.y = 0 self.width = 0 self.height = 0 self.advanceX = 0 self.bearingY = 0 def __str__(self): s = '' for n, v in self.__dict__.iteritems(): if n.startswith('_'): continue s += '%s = %s\n' % (n, v) return s def write(self, f, endian): s = struct.pack( endian + SFontChar.format, self.x, self.y, self.width, self.height, self.advanceX, self.bearingY ) f.write( s ) def read(self, f, endian='='): self.x, self.y, self.width, self.height, self.advanceX, self.bearingY = ReadFormat(f, endian + SFontChar.format) class SFont(object): def __init__(self, texture='', info=None, glyphs=None, kernings=None): if texture and info is not None: self.info = SPackedFontInfo(texture, info) if glyphs is not None: self.chars = [ SFontChar(glyph) for glyph in glyphs ] self.kernings = kernings if kernings else [] def __str__(self): s = '' for n, v in self.__dict__.iteritems(): if n.startswith('_'): continue s += '%s = %s\n' % (n, v) return s def write(self, f, endian): self.info.write( f, endian ) s = struct.pack( endian + 'HH', len(self.chars), len(self.kernings) ) f.write( s ) pointeroffset_chars = f.tell() + struct.calcsize('IIII') pointeroffset_chars_glyphs = pointeroffset_chars + struct.calcsize('H') * len(self.chars) pointeroffset_kerning_chars = pointeroffset_chars_glyphs + struct.calcsize(SFontChar.format) * len(self.chars) pointeroffset_kerning_values = pointeroffset_kerning_chars + struct.calcsize('I') * len(self.kernings) #print self.info.texture, pointeroffset_chars, pointeroffset_chars_glyphs, pointeroffset_kerning_chars, pointeroffset_kerning_values s = struct.pack( endian + 'IIII', pointeroffset_chars, pointeroffset_chars_glyphs, pointeroffset_kerning_chars, pointeroffset_kerning_values ) f.write( s ) chars, glyphs = zip( *[ (c.utf8, c) for c in self.chars] ) s = '' for char in chars: s += struct.pack( endian + 'H', char ) f.write( s ) for glyph in glyphs: glyph.write( f, endian ) if self.kernings: chars, kernings = zip(*sorted(self.kernings.items())) s = '' for char in chars: s += struct.pack( endian + 'I', char ) for kerning in kernings: s += struct.pack( endian + 'i', kerning ) f.write( s ) def read(self, f, endian='='): """ Reads a font from a .fntb file. This implementation is very Python specific, do not use it in your c/c++ code. Instead, do a single read of the file into a sufficient char* buffer, and then cast the buffer into a SFont object. Next, you should patch the pointers (to the glyphs and kerning chars/values) """ self.info = SPackedFontInfo() self.info.read( f, endian ) len_chars, len_kernings = ReadFormat(f, endian + 'HH') pointeroffset_chars, pointeroffset_chars_glyphs, pointeroffset_kerning_chars, pointeroffset_kerning_values = ReadFormat(f, endian + 'IIII') chars = [0] * len_chars for i in xrange(len_chars): chars[i] = ReadFormat(f, endian + 'H')[0] glyphs = [] for i in xrange(len_chars): char = SFontChar() char.read(f, endian) glyphs.append(char) """ print self.info.name for index, (char, glyph) in enumerate( zip(chars, glyphs) ): print "%d: char %c %d x,y: %d, %d w,h: %d, %d adv: %d" % ( index, char, int(char), glyph.x, glyph.y, glyph.width, glyph.height, glyph.advanceX ) print "" print "" """ self.chars = dict( zip(chars, glyphs) ) kerning_chars = list( ReadFormat(f, endian + 'I' * len_kernings)) kerning_values = list( ReadFormat(f, endian + 'i' * len_kernings)) self.kernings = dict( zip(kerning_chars, kerning_values) ) def Extension(): return '.fntb' def Write(options, info, glyphs, pairkernings): font = SFont( options.texturename, info, glyphs, pairkernings ) f = open( options.output, 'wb') font.write( f, options.endian ) f.close() return 0