使用PyGtk Pixbuf及freetype-py 显示文本

使用PyGtk Pixbuf及freetype-py 显示文本

计算机上显示文本的过程大体上是,先将文本转换成一个一个的bitmap,然后再用图形系统将这些bitmap显示出来。freetype是一个open source的字体引擎,它完成的工作即是将字符转换成bitmap。freetype-py是一个freetype的python绑定,为我们在Python code中使用Freetype接口提供便利。

我们可以结合PyGtk所提供的Pixbuf对象,一些操作像素的方法,PyGtk所提供的窗口管理系统,及freetype转换字符到bitmap的方法,来显示字符。这个过程的code可以像下面这样:

#!/usr/bin/python
'''
author: Wolf-CS
website: http://my.oschina.net/wolfcs/blog
last edited: May 2013
'''

import gtk, gtk.gdk
import cairo
import freetype
import ctypes.util
from ctypes import *

class ColorMap:
    ''' A colormap is used to map scalar values to colors. It is build by
        adding couples of (value,color) where value must be between 0 and 1.
        The 'scale' method allows to specify the range of the colormap and
        the 'color' method then returns a color for any value. '''

    def __init__ (self, colors):
        self.colors = colors
        self.min    = 0
        self.max    = 1

    def scale (self, min, max):
        self.min, self.max = min,max

    def color (self, value):
        ''' Return the color corresponding to value. '''
        if not len(self.colors):
            return (0,0,0)
        elif len(self.colors) == 1:
            return self.colors[0][1]
        elif value < self.min:
            return self.colors[0][1]
        elif value > self.max:
            return self.colors[-1][1]
        value = (value-self.min)/(self.max-self.min)
        sup_color = self.colors[0]
        inf_color = self.colors[-1]
        for i in range (len(self.colors)-1):
            if value < self.colors[i+1][0]:
                inf_color = self.colors[i]
                sup_color = self.colors[i+1]
                break
        r = (value-inf_color[0]) / (sup_color[0] - inf_color[0])
        if r < 0: r = -r
        color = [sup_color[1][0]*r + inf_color[1][0]*(1-r),
                 sup_color[1][1]*r + inf_color[1][1]*(1-r),
                 sup_color[1][2]*r + inf_color[1][2]*(1-r)]
        return color

# Some colormaps
CM_IceAndFire = ColorMap([(0.00, (0.0, 0.0, 1.0)),
                         (0.25, (0.0, 0.5, 1.0)),
                         (0.50, (1.0, 1.0, 1.0)),
                         (0.75, (1.0, 1.0, 0.0)),
                         (1.00, (1.0, 0.0, 0.0))])
CM_Ice = ColorMap([(0.00, (0.0, 0.0, 1.0)),
                   (0.50, (0.5, 0.5, 1.0)),
                   (1.00, (1.0, 1.0, 1.0))])
CM_Fire = ColorMap([(0.00, (1.0, 1.0, 1.0)),
                    (0.50, (1.0, 1.0, 0.0)),
                    (1.00, (1.0, 0.0, 0.0))])
CM_Hot = ColorMap([(0.00, (0.0, 0.0, 0.0)),
                   (0.33, (1.0, 0.0, 0.0)),
                   (0.66, (1.0, 1.0, 0.0)),
                   (1.00, (1.0, 1.0, 1.0))])
CM_Grey = ColorMap([(0.00, (0.0, 0.0, 0.0)),
                    (1.00, (1.0, 1.0, 1.0))])

CM_MAPS = [CM_IceAndFire, CM_Ice, CM_Fire, CM_Hot, CM_Grey]

gcolor_map = CM_IceAndFire

def set_gcolor_map(color_map_index):
    index = color_map_index % len(CM_MAPS)
    global gcolor_map
    gcolor_map = CM_MAPS[index]

class MainWindow(gtk.Window):
    def __init__(self):
        super(self.__class__, self).__init__()
        self.init_ui()
        self.create_pixbuf()

    def init_ui(self):
        self.darea = gtk.DrawingArea()
        self.darea.connect("expose_event", self.expose)
        self.add(self.darea)

        self.set_title("JpegImage")
        self.resize(960, 480)
        self.set_position(gtk.WIN_POS_CENTER)
        self.connect("delete-event", gtk.main_quit)
        self.show_all()

    def create_pixbuf(self):
        width = 960
        height = 480
        self.datapb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
        self.clear_pixbuf(self.datapb, 0, 128, 255, 255)

    def expose(self, widget, event):
        self.context = widget.window.cairo_create()
        self.on_draw(300, self.context)

    def on_draw(self, wdith, cr):
        text = "A Quick Brown Fox Jumps Over The Lazy Dog 0123456789"
        face = freetype.Face("./Arial.ttf")
        text_size = 32
        face.set_char_size(text_size * 64)
        
        metrics = face.size
        self.ascender  = metrics.ascender/64.0
        self.descender = metrics.descender/64.0
        self.height    = metrics.height/64.0
        self.linegap   = self.height - self.ascender + self.descender
#        print "ascender = %d, descender = %d, height = %d" % (self.ascender, self.descender, self.height)
#        self.draw_char(self.datapb, 20, 20, 'S', face)
        ypos = int(self.ascender)
        color_map_index = 0
        while ypos + int(self.height) < 480:
            set_gcolor_map(color_map_index)
            self.draw_string(self.datapb, 5, int(ypos), text, face)
            color_map_index += 1
            ypos += int(self.ascender - self.descender)
        
        gtk.gdk.CairoContext.set_source_pixbuf(cr, self.datapb, 0, 0)
        cr.paint()

    def draw_ft_bitmap(self, pixbuf, bitmap, pen):
        x_pos = pen.x >> 6
        y_pos = pen.y >> 6
        width = bitmap.width
        rows = bitmap.rows

        pixbuf_width = pixbuf.get_width()
        pixbuf_height = pixbuf.get_height()
#        print "y_pos = %d, pixbuf_height = %d" % (y_pos, pixbuf_height)
        assert ((y_pos > 0) and (y_pos + rows < pixbuf_height))
        assert ((x_pos > 0) and (x_pos + width < pixbuf_width))

        glyph_pixels = bitmap.buffer

        for line in range(rows):
            for column in range(width):
                if glyph_pixels[line * width + column] != 0:
                    colors = gcolor_map.color(glyph_pixels[line * width + column] / 255)
                    self.put_pixel(pixbuf, y_pos + line, x_pos + column, 
                               colors[0] * 255, 
                               colors[1] * 255,
                               colors[2] * 255,
                               255)

    def draw_string(self, pixbuf, x_pos, y_pos, str, face):
        prev_char = 0;
        pen = freetype.Vector()
        pen.x = x_pos << 6
        pen.y = y_pos << 6

        ascender = face.ascender
        descender = face.descender
        height = face.height
#        print "ascender = %d, descender = %d, height = %d" % (ascender, descender, height)

        hscale = 1.0
        matrix = freetype.Matrix(int((hscale) * 0x10000L), int((0.2) * 0x10000L),
                         int((0.0) * 0x10000L), int((1.1) * 0x10000L))

        cur_pen = freetype.Vector()
        pen_translate = freetype.Vector()
        for cur_char in str:
            face.set_transform(matrix, pen_translate)

            face.load_char(cur_char)
            kerning = face.get_kerning(prev_char, cur_char)
            pen.x += kerning.x
            slot = face.glyph
            bitmap = slot.bitmap

            cur_pen.x = pen.x
            cur_pen.y = pen.y - slot.bitmap_top * 64
            self.draw_ft_bitmap(pixbuf, bitmap, cur_pen)
            
            pen.x += slot.advance.x
            prev_char = cur_char

    def draw_char(self, pixbuf, x_pos, y_pos, char, face):
        face.load_char(char)
        slot = face.glyph
        bitmap = slot.bitmap

        pen = freetype.Vector()
        pen.x = x_pos << 6
        pen.y = y_pos << 6
        self.draw_ft_bitmap(pixbuf, bitmap, pen)

    def put_pixel(self, pixbuf, y_pos, x_pos, red, green, blue, alpha):
        n_channels = pixbuf.get_n_channels()
        width = pixbuf.get_width()
        height = pixbuf.get_height()
        assert (n_channels == 4)
        assert (y_pos >= 0 and y_pos < height)
        assert (x_pos >= 0 and x_pos < width)

        pixels = pixbuf.get_pixels_array()
        pixels[y_pos][x_pos][0] = red
        pixels[y_pos][x_pos][1] = green
        pixels[y_pos][x_pos][2] = blue
        pixels[y_pos][x_pos][3] = alpha

    def clear_pixbuf(self, pixbuf, red, green, blue, alpha):
        n_channels = pixbuf.get_n_channels()
        assert (n_channels == 4)

        width = pixbuf.get_width()
        height = pixbuf.get_height()

        pixels = pixbuf.get_pixels_array()
        for row in range(height):
            for column in range(width):
                pixels[row][column][0] = red
                pixels[row][column][1] = green
                pixels[row][column][2] = blue
                pixels[row][column][3] = alpha

def main():
    window = MainWindow()
    gtk.main()

if __name__ == "__main__":
    main()

首先,可以先来看一下上面那段code运行的结果。

使用PyGtk Pixbuf及freetype-py 显示文本_第1张图片

上面那段code的整体思路为,

  1. 创建一个Pixbuf,
  2. 向Pixbuf中写入文本的像素数据,由于文本的像素数据是单色的,即描述文本的bitmap的一个像素点,会是一个字节,因而,可以依据需要,将这种单色的数据映射到适当的颜色,如上面的图所示。
  3. 使用PyGtk的窗口系统来显示这个Pixbuf。

    def create_pixbuf(self):
        width = 700
        height = 480
        self.datapb = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height)
        self.clear_pixbuf(self.datapb, 0, 128, 255, 255)

创建一个Pixbuf。参数为色彩空间,是否需要alpha通道,每一个原色占用的位数,以及图片的宽和高,这些参数将决定着Pixbuf的像素缓冲区的大小。然后就是用一种蓝色来清空这个缓冲区。

    def clear_pixbuf(self, pixbuf, red, green, blue, alpha):
        n_channels = pixbuf.get_n_channels()
        assert (n_channels == 4)

        width = pixbuf.get_width()
        height = pixbuf.get_height()

        pixels = pixbuf.get_pixels_array()
        for row in range(height):
            for column in range(width):
                pixels[row][column][0] = red
                pixels[row][column][1] = green
                pixels[row][column][2] = blue
                pixels[row][column][3] = alpha

由这个函数,有两点值得我们关注。其一,是写入像素数据的方法。我们需要调到Pixbuf的get_pixels_array()函数,它会返回一个可供我们修改的像素缓冲区的引用。其二,在Pixbuf中,是用什么样的结构来存储像素数据的。尽管我们能看到array的字样,但它并不是用一个一维数组来存的,可以看到,它是用一个元组的二维数组来存储像素数据的。每一个元组描述一个像素点的色彩数据,有4个元素,分别表示R、G、B、A值。每一个行的像素数据存入一个元组数组中,所有的像素数据再组成一个元组数组的数组。这样可以让我们很方便的依据像素点的位置来定位某个像素点的像素数据。

    def on_draw(self, wdith, cr):
        text = "A Quick Brown Fox Jumps Over The Lazy Dog"
        face = freetype.Face("./Arial.ttf")
        text_size = 32
        face.set_char_size(text_size * 64)
        
        metrics = face.size
        self.ascender  = metrics.ascender/64.0
        self.descender = metrics.descender/64.0
        self.height    = metrics.height/64.0
        self.linegap   = self.height - self.ascender + self.descender
#        print "ascender = %d, descender = %d, height = %d" % (self.ascender, self.descender, self.height)
#        self.draw_char(self.datapb, 20, 20, 'S', face)
        ypos = int(self.ascender)
        color_map_index = 0
        while ypos + int(self.height) < 480:
            set_gcolor_map(color_map_index)
            self.draw_string(self.datapb, 5, int(ypos), text, face)
            color_map_index += 1
            ypos += int(self.ascender - self.descender)
        
        gtk.gdk.CairoContext.set_source_pixbuf(cr, self.datapb, 0, 0)
        cr.paint()

freetype-py不仅仅是导出了freetype的C API,它还对其中的一些结构进行了封装,以方便我们的操作。Face即是对于FT_Face的一种封装,使得我们的操作可以更加的简便。我们可以简单的将字库文件的路径传给Face的构造函数,来创建一个Face对象,然后为它设置字体大小。由于freetype内部对字体大小的格式的要求,我们需要将pixel的单位乘上一个64。

换行时,要求得绘制下一行的纵坐标,我们可以Face中提供的一些信息来完成。ascender为一个正数,descender为一个负数,两者相减,可以求得字的高度,也就可以依据绘制当前行的纵坐标,来求得下一行绘制的纵坐标。

可以将子串内容,face,及绘制的位置传给draw_string()函数来进行绘制。

    def draw_string(self, pixbuf, x_pos, y_pos, str, face):
        prev_char = 0;
        pen = freetype.Vector()
        pen.x = x_pos << 6
        pen.y = y_pos << 6

        ascender = face.ascender
        descender = face.descender
        height = face.height
#        print "ascender = %d, descender = %d, height = %d" % (ascender, descender, height)

        hscale = 1.0
        matrix = freetype.Matrix(int((hscale) * 0x10000L), int((0.2) * 0x10000L),
                         int((0.0) * 0x10000L), int((1.1) * 0x10000L))

        cur_pen = freetype.Vector()
        pen_translate = freetype.Vector()
        for cur_char in str:
            face.set_transform(matrix, pen_translate)

            face.load_char(cur_char)
            kerning = face.get_kerning(prev_char, cur_char)
            pen.x += kerning.x
            slot = face.glyph
            bitmap = slot.bitmap

            cur_pen.x = pen.x
            cur_pen.y = pen.y - slot.bitmap_top * 64
            self.draw_ft_bitmap(pixbuf, bitmap, cur_pen)
            
            pen.x += slot.advance.x
            prev_char = cur_char

这个是draw_string()函数的实现。比较基本的freetype API的应用,设置transformation,load glyph,获取到kerning值以修正绘制字符的横坐标,然后便是抓取的glyph的bitmap,并绘制。

    def draw_ft_bitmap(self, pixbuf, bitmap, pen):
        x_pos = pen.x >> 6
        y_pos = pen.y >> 6
        width = bitmap.width
        rows = bitmap.rows

        pixbuf_width = pixbuf.get_width()
        pixbuf_height = pixbuf.get_height()
#        print "y_pos = %d, pixbuf_height = %d" % (y_pos, pixbuf_height)
        assert ((y_pos > 0) and (y_pos + rows < pixbuf_height))
        assert ((x_pos > 0) and (x_pos + width < pixbuf_width))

        glyph_pixels = bitmap.buffer

        for line in range(rows):
            for column in range(width):
                if glyph_pixels[line * width + column] != 0:
                    colors = gcolor_map.color(glyph_pixels[line * width + column] / 255)
                    self.put_pixel(pixbuf, y_pos + line, x_pos + column, 
                               colors[0] * 255, 
                               colors[1] * 255,
                               colors[2] * 255,
                               255)

这个函数就是将freetype的glyph bitmap,做色彩映射之后,依据Pixbuf中存储像素数据的格式,将像素数据存储进Pixbuf中。

关于freetype API更详细的使用方法,可以参考freetype的官方文档,那里介绍的比较详细,比较权威,也比较清晰。

http://www.freetype.org/freetype2/documentation.html

Done。

你可能感兴趣的:(使用PyGtk Pixbuf及freetype-py 显示文本)