计算机上显示文本的过程大体上是,先将文本转换成一个一个的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运行的结果。
上面那段code的整体思路为,
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。