# Copyright 2000 by Robert Kern, Frederick MD, USA.
#
#                         All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Robert Kern not be used
# in advertising or publicity pertaining to distribution of the software
# without specific, written prior permission.
#
# ROBERT KERN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
# EVENT SHALL ROBERT KERN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.

"""\
PyFT.py -- An OO wrapper around the rest of PyFT.

The most common use of these FreeType bindings, to render a string
onto a PIL Image, is abstracted here.

Basically, one chooses a font:

  f = Font('times', size=12, bold=0, italic=1)

The appropriate TrueType file will be found given the data in the
dictionaries 'fontFiles', 'font_face_map', 'ps_font_map'.  For the
face name in the constructor, use a string that can be found in either
the keys or values of 'font_face_map'.  Please change these
dictionaries to match your system and font-naming preferences.

Then start an instance of BaseRenderer:

  rend = BaseRenderer(resolution=(96,96), aa=1)

Call the drawString method of the renderer to draw a string on a PIL
Image.  See the docstrings on the classes for more information.

2000-04-23
Robert Kern
kern@caltech.edu

"""
  
import string
from freetype import *
import Image

# Font-finding data
# Feel free to add and modify items to match your system esp. for Windows

# Map Postscript names to font files.
fontFiles = {'Times-Roman': '/usr/share/fonts/truetype/times.ttf',
             'Times-Bold': '/usr/share/fonts/truetype/timesbd.ttf',
             'Times-Italic': '/usr/share/fonts/truetype/timesi.ttf',
             'Times-BoldItalic': '/usr/share/fonts/truetype/timesbi.ttf',

             'Courier': '/usr/share/fonts/truetype/cour.ttf',
             'Courier-Bold': '/usr/share/fonts/truetype/courbd.ttf',
             'Courier-Oblique': '/usr/share/fonts/truetype/courbi.ttf',
             'Courier-BoldOblique': '/usr/share/fonts/truetype/couri.ttf',

             'Helvetica': '/usr/share/fonts/truetype/arial.ttf',
             'Helvetica-Bold': '/usr/share/fonts/truetype/arialbd.ttf',
             'Helvetica-Oblique': '/usr/share/fonts/truetype/ariali.ttf',
             'Helvetica-BoldOblique': '/usr/share/fonts/truetype/arialbi.ttf',

             'Symbol': '/usr/share/fonts/truetype/symbol.ttf',
             'ZapfDingbats': '/usr/share/fonts/truetype/wingding.ttf'
             }

# Map face names to other face names
font_face_map = {
    'serif':'times',
    'sansserif':'helvetica',
    'monospaced':'courier',
    'arial':'helvetica'
    }

# Map a (facename, bold, italic) tuple to a Postscript font name
ps_font_map = {
    #face, bold, italic -> ps name
    ('times', 0, 0) :'Times-Roman', 
    ('times', 1, 0) :'Times-Bold', 
    ('times', 0, 1) :'Times-Italic', 
    ('times', 1, 1) :'Times-BoldItalic', 
    
    ('courier', 0, 0) :'Courier', 
    ('courier', 1, 0) :'Courier-Bold', 
    ('courier', 0, 1) :'Courier-Oblique', 
    ('courier', 1, 1) :'Courier-BoldOblique', 
    
    ('helvetica', 0, 0) :'Helvetica', 
    ('helvetica', 1, 0) :'Helvetica-Bold', 
    ('helvetica', 0, 1) :'Helvetica-Oblique', 
    ('helvetica', 1, 1) :'Helvetica-BoldOblique', 
    
    # there is only one Symbol font            
    ('symbol', 0, 0) :'Symbol', 
    ('symbol', 1, 0) :'Symbol', 
    ('symbol', 0, 1) :'Symbol', 
    ('symbol', 1, 1) :'Symbol', 
    
    # ditto for dingbats
    ('zapfdingbats', 0, 0) :'ZapfDingbats', 
    ('zapfdingbats', 1, 0) :'ZapfDingbats', 
    ('zapfdingbats', 0, 1) :'ZapfDingbats', 
    ('zapfdingbats', 1, 1) :'ZapfDingbats', 
    }


# Okay, we need to add a __hash__ method to TT_Matrix if we want
# life to be seen later.

class TT_Matrix(TT_MatrixPtr):
    """ A simple 2x2 matrix used for transformations.
 You should use 16.16 fixed floats.           
                                            
   x' = xx*x + xy*y                            
   y' = yx*x + yy*y   
"""
    def __init__(self,*_args,**_kwargs):
        """"""
        self.this = apply(freetypec.new_TT_Matrix,_args,_kwargs)
        self.thisown = 1

    def __hash__(self):
        return hash((self.xx, self.xy, self.yx, self.yy))

#-------------------------------------------------------------------------
# Font
# -- Modified from PIDDLE (http://piddle.sourceforge.net)
#    Added a __hash__ method for use as dictionary keys
#    Eliminated stuff not useable here.
#-------------------------------------------------------------------------
class Font:
    "This class represents font typeface, size, and style."
        
    def __init__(self, face, size=12, bold=0, italic=0):
        # public mode variables
        self.bold = bold
        self.italic = italic
        self.underline = underline
                
        # public font size (points)
        self.size = size
        
        # typeface -- a name or set of names, interpreted by the Canvas,
        # or "None" to indicate the Canvas-specific default typeface
        self.face = face
        
    def __cmp__(self, other):
        "Compare two fonts to see if they're the same."
        if self.face == other.face and self.size == other.size and \
           self.bold == other.bold and self.italic == other.italic \
           and self.underline == other.underline:
            return 0
        else: return 1

    def __hash__(self):
        return hash((self.size, self.bold, self.italic, self.underline, self.face))

    def __repr__(self):
        return "Font(%d,%d,%d,%d,%s)" % (self.size, self.bold, self.italic, \
                                         self.underline, repr(self.face))

def _findFontFile(font):
    """Find a font given a Font instance"""
    
    face = string.lower(font.face)

    while font_face_map.has_key(face):
        face = font_face_map[face]

    try:
        psname = ps_font_map[(face, font.bold, font.italic)]
    except KeyError:
        # default to Helvetica
        return fontFiles['Helvetica']

    return fontFiles[psname]

class Face:
    """Face(engine, font)

    Class for internal use that holds FreeType TT_Face's and related data.

    Members:

      _filename -- filename of font file
      engine -- reference to TT_Engine
      face -- SWIG shadow class around the TT_Face pointer
      props -- SWIG shadow class around the TT_Face_Properties pointer
      cm -- SWIG shadow class around the current character map

    Methods:

      getCharMap(platform=3, encoding=1) -- returns the character map with
                                            the specified platform and
                                            encoding
      fontAscent() -- returns the font ascent
      fontDescent() -- returns the font descent
    """

    def __init__(self, engine, font):
        self._filename = _findFontFile(font)

        self.engine = engine
        self.face = TT_Open_Face(self.engine, self._filename)
        self.props = TT_Get_Face_Properties(self.face)

        self.cm = self.getCharMap(3, 1) # Windows Unicode

    def getCharMap(self, platform=3, encoding=1):
        for id in range(self.props.num_CharMaps):
            if TT_Get_CharMap_ID(self.face, id) == (platform, encoding):
                return TT_Get_CharMap(self.face, id)

        raise ValueError, "CharMap (%s, %s) does not exist" % (platform, encoding)

    def fontAscent(self):
        return self.props.os2.usWinAscent / 64

    def fontDescent(self):
        return self.props.os2.usWinDescent / 64

class GlyphCon:
    """GlyphCon(instance, transform=None)

    A container for caching glyphs and related data for a given
    instance and transformation matrix. As the user requests
    information, the relevant caches are checked. If the information
    is there, it is returned. Otherwise, it is calculated and cached
    for further use.

    __getitem__ returns a tuple of data:

      glyph, metrics, outline, bbox = glyphConInstance['a']

    Members:

      glyphCache   -- glyph objects
      metricCache  -- metrics
      outlineCache -- outlines
      bboxCache    -- bounding boxes
      advanceCache -- advancing vectors

    Methods:

      getGlyphs(text)   -- calculates data for each character in text
      stringWidth(text) -- calculates the width of text when rendered
                           in the instance.
    """
    
    def __init__(self, instance, transform=None):
        self.instance = instance
        self.transform = transform

        self.glyphCache = {}
        self.metricCache = {}
        self.outlineCache = {}
        self.bboxCache = {}
        self.advanceCache = {}

    def __getitem__(self, item):
        if not self.glyphCache.has_key(item):
            self.getGlyphs(item)
        
        return (self.glyphCache[item], self.metricCache[item],
                self.outlineCache[item], self.bboxCache[item])

    def getGlyphs(self, text):
        text = string.expandtabs(text)
        
        glyphIndices = pTT_String_Indices(self.instance.face.cm, text)

        for i in range(len(text)):
            if not self.glyphCache.has_key(text[i]):
                glyph = TT_New_Glyph(self.instance.face.face)
                TT_Load_Glyph(self.instance.instance, glyph, glyphIndices[i], TTLOAD_DEFAULT)
                outline = TT_Get_Glyph_Outline(glyph)
                metric = TT_Get_Glyph_Metrics(glyph)

                if self.transform is not None:
                    TT_Transform_Outline(outline, self.transform)

                bbox = TT_Get_Outline_BBox(outline)

                self.glyphCache[text[i]] = glyph
                self.outlineCache[text[i]] = outline
                self.metricCache[text[i]] = metric
                self.bboxCache[text[i]] = bbox

                if self.transform is not None:
                    advanceVec = TT_Transform_Vector(self.metricCache[text[i]].advance, 0., self.transform)
                else:
                    advanceVec = (self.metricCache[text[i]].advance, 0.)
        
                self.advanceCache[text[i]] = advanceVec

    def stringWidth(self, text):
        self.getGlyphs(text)

        w = 0

        for char in text:
            w = w + self.metricCache[char].advance

        return CEIL(w) / 64

class Instance:
    """Instance(face, size=12, resolution=(96,96))

    Holds data on a given instance.

    Members:

      instance -- SWIG shadow class of the TT_Instance pointer
      pointsize -- size of the font in typographic points
      resolution -- resolution of the instance
      face -- an instance of the class Face

    Methods:

      getGlyphCon(transform=None) -- returns a GlyphCon instance for
                                     the given transformation
      getRotGlyphCon(theta=0.)    -- returns a GlyphCon instance rotated
                                     by theta degrees
      getSkewGlyphCon(alpha=0., beta=0.) -- returns a GlyphCon instance
                                     appropriately skewed
    """
    def __init__(self, face, size=12, resolution=(96,96)):
        self.instance = TT_New_Instance(face.face)
        self.pointsize = size
        self.resolution = resolution
        self.face = face
        
        TT_Set_Instance_Resolutions(self.instance,
                                    resolution[0], resolution[1])
        TT_Set_Instance_PointSize(self.instance, size)

    def getGlyphCon(self, transform=None):
        return GlyphCon(self, transform)

    def getRotGlyphCon(self, theta=0.):
        return GlyphCon(self, rotation(theta))

    def getSkewGlyphCon(self, alpha=0., beta=0.):
        return GlyphCon(self, skew(alpha, beta))
        
class BaseRenderer:
    """BaseRenderer(resolution=(96,96), aa=1)

    Main rendering class.  Takes care of most of your dirty work of
    initializing the FreeType engine and rendering text.

    Members:

      engine -- SWIG shadow class around a TT_Engine pointer
      resolution -- resolution
      aa -- boolean determining anti-aliasing (1 for yes, 0 for no)
      faces -- dictionary of faces keyed by (facestring, bold, italic) tuples
      instances -- dict of FreeType instanceskeyed by
                   (facestring, bold, italic, size) tuples
      glyphs -- dict of GlyphCon instances keyed by (font, transform) tuples
      aaPalette -- tuple of integers that defines the anti-aliasing palette
                   used by FreeType.  See the FreeType documentation.

    Methods:

      openFont(font) -- loads a font face and instance into the
                        appropriate member dicts if not already there
                        
      getInstance(font) -- opens a font and returns the instance
      renderString(text, font, transform=None) -- renders a string on a
                        TT_Raster buffer with the given font and transform
      raster2Image(raster) -- convert a TT_Raster to a PIL Image
                        (black-on-white)
      raster2Mask(raster) -- convert a TT_Raster to a PIL Image suitable
                        for using as an alpha-blending mask (white-on-black)

      drawString(image, text, x, y, font, color=(0,0,0), transform=None)
                        -- draw a string onto a PIL Image at position (x,y)
                        with the given font, RGB color, and transformation
    """
    def __init__(self, resolution=(96,96), aa=1):
        self.engine = TT_Init_FreeType()
        self.resolution = resolution
        self.aa = aa

        self.faces = {}
        self.instances = {}
        self.glyphs = {}

        self.aaPalette = (0, 64, 128, 192, 255)

    def openFont(self, font):
        if not self.faces.has_key((font.face, font.bold, font.italic)):
            face = Face(self.engine, font)

            self.faces[font.face, font.bold, font.italic] = face

        if not self.instances.has_key((font.face, font.bold, font.italic, font.size)):
            instance = Instance(self.faces[font.face, font.bold, font.italic], font.size, self.resolution)

            self.instances[font.face, font.bold, font.italic, font.size] = instance

    def getInstance(self, font):
        self.openFont(font)

        return self.instances[font.face, font.bold, font.italic, font.size]

    def renderString(self, text, font, transform=None):
        assert self.aa in (0, 1), "self.aa must be either 0 or 1"

        minyMin = 0
        maxyMax = 0
        minxMin = 0
        maxxMax = 0

        x = 0
        y = 0

        offsets = []

        inst = self.getInstance(font)
        glyphs = inst.getGlyphCon(transform)
        glyphs.getGlyphs(text)

        self.glyphs[font, transform] = glyphs

        for i in range(len(text)):
            maxyMax = max(maxyMax, glyphs.bboxCache[text[i]].yMax + y)
            maxxMax = max(maxxMax, glyphs.bboxCache[text[i]].xMax + x)

            if i != 0:
                minyMin = min(minyMin, glyphs.bboxCache[text[i]].yMin + y)
                minxMin = min(minxMin, glyphs.bboxCache[text[i]].xMin + x)
            else:
                minyMin = glyphs.bboxCache[text[i]].yMin + y
                minxMin = glyphs.bboxCache[text[i]].xMin + x

            offsets.append((x, y))

            x = x + glyphs.advanceCache[text[i]][0]
            y = y + glyphs.advanceCache[text[i]][1]

        width = (maxxMax - minxMin) / 64
        height = (maxyMax - minyMin) / 64

        raster = TT_Raster_Map(width, height, ('bit', 'pix')[self.aa])

        if not self.aa:
            for i in range(len(text)):
                TT_Translate_Outline(glyphs.outlineCache[text[i]],
                                     offsets[i][0] - minxMin,
                                     offsets[i][1] - minyMin)
                TT_Get_Outline_Bitmap(self.engine, glyphs.outlineCache[text[i]], raster)
                TT_Translate_Outline(glyphs.outlineCache[text[i]],
                                     -offsets[i][0] + minxMin,
                                     -offsets[i][1] + minyMin)
        else:
            TT_Set_Raster_Gray_Palette(self.engine, self.aaPalette)

            for i in range(len(text)):
                TT_Translate_Outline(glyphs.outlineCache[text[i]],
                                     offsets[i][0] - minxMin,
                                     offsets[i][1] - minyMin)
                TT_Get_Outline_Pixmap(self.engine, glyphs.outlineCache[text[i]], raster)
                TT_Translate_Outline(glyphs.outlineCache[text[i]],
                                     -offsets[i][0] + minxMin,
                                     -offsets[i][1] + minyMin)

        return raster

    def raster2Image(self, raster):
        if self.aa == 0:
            im = Image.fromstring("1", (raster.width, raster.rows),
                                  raster.tostring(), "raw", "1;I")
        elif self.aa == 1:
            im = Image.fromstring("L", (raster.cols, raster.rows),
                                  raster.tostring(), "raw", "L;I")
        return im

    def raster2Mask(self, raster):
        if self.aa == 0:
            im = Image.fromstring("1", (raster.width, raster.rows),
                                  raster.tostring(), "raw", "1")
        elif self.aa == 1:
            im = Image.fromstring("L", (raster.cols, raster.rows),
                                  raster.tostring(), "raw", "L")
        return im

    def drawString(self, image, text, x, y, font, color=(0,0,0), transform=None):

        text = string.expandtabs(text)
        
        if color == (-1, -1, -1):
            return
        
        raster = self.renderString(text, font, transform)

        mask = self.raster2Mask(raster)

        image.paste(Image.new('RGB', mask.size, color), (x, y - mask.size[1]), mask)

        if font.underline:
            import ImageDraw

            pen = ImageDraw.ImageDraw(image)
            pen.setink(color)

            dy = .5 * self.faces[font.face, font.bold, font.italic].fontDescent()
            width = self.glyphs[font, transform].stringWidth(text)

            if transform is not None:
                x1, x2 = TT_Transform_Vector(x, y+dy, transform)
                y1, y2 = TT_Transform_Vector(x+width, y+dy, transform)

                pen.line(x1,y1, x2, y2)
            else:
                pen.line(x, y+dy, x+width,y+dy)
