[电子书]项目储备二:用FreeType2绘制矢量字体、多行文字
0 矢量字体 VS 点阵字体
点阵字体:每一个字符都用点阵(8x8 / 8x16/ 16x16等)表示,然后用每个点的虚实来表示字符的轮廓
矢量字体:矢量字体(Vector font)中每一个字形是通过数学曲线来描述的,它包含了字形边界上的关键点,连线的导数信息等,
字体的渲染引擎(如FreeType2)通过读取这些数学矢量,然后进行一定的数学运算来进行渲染。
矢量字体主要包括 Type1、 TrueType、OpenType等几类。
两者的区别:
1)拓展名:矢量字体扩展名ttf。点阵字体的扩展名是fon.(在window10下,C盘windows/fonts/目录下有许多字体文件)
2)点阵字体组成的点阵字库 常用来保存显示字符点阵,这类点阵字库汉字最大的缺点是不能放大,一旦放大后就会
发现文字边缘的锯齿。而 矢量字体组成的矢量字库只保存该字符关键点和其它一些描述信息
(比如一个笔划的起始、终止坐标,半径、弧度等)。在显示、打印这一类字库时,要经过一系列的数学运算才能
输出结果,但是这一类字库保存的汉字理论上可以被无限地放大,笔划轮廓仍然能保持圆滑,打印时使用的字库均为此类字库
两者的优劣势:
①点阵字体: 点阵字体优点是显示速度快,不像矢量字体需要计算;其最大的缺点是不能放大,一旦放大后就会发现文字边缘的锯齿。
②矢量字体:优点很明显显示效果比点阵字体好,且随意变换(放大、缩小、旋转等)不失真。缺点需要渲染方能显示、打印,
因此相比点阵字体肯定要慢。
矢量字体是后文要实现绘制,那如何使用矢量字体呢?
需要从矢量字库从提取字符的关键点,然后用若干条数学曲线(贝塞尔曲线)连接绘制(由FreeType2渲染完成)。
因此,实际编程时,只需要使用FreType2提供的高层API即可,此外,难点在于需要我们
把转换后的位图绘制到LCD屏幕上。
1 FreeType2的介绍与基本使用
FreeType库是一个完全免费(开源)的、高质量的且可移植的字体引擎,它提供统一的接口来访问多种字体格式文件,
包括TrueType, OpenType, Type1, CID, CFF, Windows FON/FNT, X11 PCF等
FreeType2的基本使用如下图
后面绘制矢量字体、多行文字也是按此流程编程实现
用文字来描述FreeType的基本使用,如下所述
1)给定一个文字,可得到确定它的字符编码值
2)根据字符“编码值”从矢量字体文件中找到“glyph(字形/字的轮廓,其实就是字形的一些关键点)”
3)设置字体大小(因为是矢量字体,大小随意,固然需确定显示字符大小啦)
4)用某些函数吧Glyph里的关键点编码为指定字体大小
5)转换为位图点阵
6)在LCD上显示出来
废话不多说,先看看如果用FreeType2渲染一个矢量字体,并能随意旋转
2 在LCD显示一个矢量字体,且可旋转
方便起见,在LCD中央显示一个“繁”字,并根据命令行参数控制旋转角度
show_vector_font.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <math.h>
#include <wchar.h>
#include <ft2build.h>
#include FT_FREETYPE_H // freetype/freetype.h
#include FT_GLYPH_H
extern const unsigned char fontdata_8x16[];
int fd_fb;
static line_width, pixel_width, screen_size;
static unsigned char* fbmem;
static struct fb_var_screeninfo var;
static struct fb_fix_screeninfo fix;
int error;
double angle;
FT_Library library;
FT_Face face; /* handle to face object */
FT_GlyphSlot slot;
FT_Vector pen; /* untransformed origin */
FT_Matrix matrix;
void lcd_put_pixel(unsigned int x, unsigned y, unsigned int color);
void my_draw_bitmap( FT_Bitmap* bitmap, FT_Int x, FT_Int y);
int main(int argc, char** argv)
{
wchar_t *str1 = L"繁";
if(argc != 3)
{
printf("Usage: ");
printf("%s <font_file> <angle>\n", argv[0]);
return 0;
}
fd_fb = open("/dev/fb0", O_RDWR);
if(fd_fb < 0)
{
printf("can't open /dev/fb0\n");
return -1;
}
if(ioctl(fd_fb, FBIOGET_VSCREENINFO, &var))
{
printf("can't get var\n");
return -1;
}
if(ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix))
{
printf("can't get fix\n");
return -1;
}
line_width = var.xres * var.bits_per_pixel /8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * pixel_width;
fbmem=(unsigned char*)mmap(NULL, screen_size,PROT_READ | PROT_WRITE, MAP_SHARED,fd_fb, 0);
if(fbmem == (unsigned char*)-1)
{
printf("can't map fbmem\n");b return -1;
}
memset(fbmem, 0, screen_size); // clear screen
// show a vector font
error = FT_Init_FreeType(&library);
error = FT_New_Face( library, argv[1], 0, &face );
slot = face->glyph;
error = FT_Set_Pixel_Sizes(
face, /* handle to face object */
24, /* pixel_width */
0); /* pixel_height */
angle = ( 1.0 * strtoul(argv[2], NULL, 0) / 360 ) * 3.14159 * 2; /* use 25 degrees */
matrix.xx = (FT_Fixed)( cos( angle ) * 0x10000L );
matrix.xy = (FT_Fixed)(-sin( angle ) * 0x10000L );
matrix.yx = (FT_Fixed)( sin( angle ) * 0x10000L );
matrix.yy = (FT_Fixed)( cos( angle ) * 0x10000L );
pen.x = (var.xres/2 ) * 64;
pen.y = (var.yres/2 ) * 64;
FT_Set_Transform(
face, /* target face object */
&matrix, /* pointer to 2x2 matrix */
&pen); /* pointer to 2d vector */
'aT_Load_Char( face, str1[0], FT_LOAD_RENDER );
my_draw_bitmap( &slot->bitmap,
slot->bitmap_left,
var.yres - slot->bitmap_top );
// 记得释放
munmap(fbmem, screen_size);
FT_Done_Face(face);
FT_Done_FreeType(library);
return 0;
}
void lcd_put_pixel(unsigned int x, unsigned y, unsigned int color)
{
unsigned char* pen_8 = fbmem + y*line_width + x*pixel_width;
unsigned ort* pen_16 = NULL;
unsigned int* pen_32 = NULL;
unsigned int red, green, blue;
pen_16 = (unsigned short*)pen_8;
pen_32 = (unsigned int*)pen_8;
switch(var.bits_per_pixel)
{
case 8:
{
*pen_8 = color;
break;
}
case 16:
{
red = (color >> 16) & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 0) & 0xff;
color = ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
*pen_16 = color;
break;
}
case 32'z:
{
*pen_32 = color ;
break;
}
default:
{
printf("can't surport %dbpp\n",var.bits_per_pixel);
return ;
}
}
}
void my_draw_bitmap( FT_Bitmap* bitmap, FT_Int x, FT_Int y)
{
printf("x:%d, y:%d\n", x, y);
FT_Int i, j, p, q;
FT_Int x_max = x + bitmap->width;
FT_Int y_max = y + bitmap->rows;
for ( i = x, p = 0; i < x_max; i++, p++ )
{
for ( j = y, q = 0; j < y_max; j++, q++ )
{
if ( i < 0 || j < 0 ||
ao i >= var.xres || j >= var.yres )
continue;
lcd_put_pixel(i, j, bitmap->buffer[q * bitmap->width + p]);
}
}
}
好了,如何交叉编译并测试呢?
- tar xjf freetype-2.4.10.tar bz2
- .configure --host=arm-linux --prefix=xxxx
(xxx是存放交叉编译工具链头文件和库的目录,一般来说是目录是相同的,我的就是。
如果不是,则不需–prefix=xxx,在后面make install DESTDIR=$PWD/tmp 安装到临时目录
然后再把freetype的库文件和库复制到交叉编译工具链对应的头文件和库目录下
) - make
- make DESTDIR=$PWD/tmp install (如果目录相同,DESTDIR不需要)
最后编译:
arm-linux-gcc -finput-charset=GBK -fexec-charset=GBK -o show_vector_font show_vector_font.c
-lfreetype -lm
将生成的show_vector_font测试文件和字体文件(可从C盘windows/fonts/目录下随便获取一个)复制
到开发板的根文件系统
另外,记得也要将freetype的动态库文件复制到根文件系统的/lib目录下
然后测试
./show_vector_font simsun.ttc 45
最后,看下面的测试结果图.
ok,这个简单例子只是掌握FreeType2的基本用法
下面来个更高级一些的用法,在LCD任意位置显示多行文字
测试效果:以逆时针旋转45°为例,其它角度也OK。要顺时针的话,要调整那个变换矩阵(我数学渣渣,留给你们:)
3 在LCD任意位置显示多行文字
有了前面显示单个矢量字体,对于显示多行文字就简单了,直接贴出代码,如下
备注:全部贴出太占篇幅了,也没必要。因此贴出关键代码,其它和显示单个矢量字体一样
完整源码可从我的代码集合那篇博文获取
int main(int argc, char** argv)
{
wchar_t *str = L"中国我爱你\n我爱你中国";
/* ... */
draw_lines(200, 100, str);
}
int draw_lines(unsigned int x, unsigned int y, wchar_t* str)
{
if(x > var.xres || y > var.yres || !str)
return -1;
printf("xres:%d yres:%d\n",var.xres, var.yres);
pen.x = x * 64;
pen.y = (var.yres - y) * 64;
int n;
for(n = 0; n < wcslen(str)i; ++n)
{
FT_Set_Transform(
face, /* target face object */
0,
&pen); /* pointer to 2d vector */
FT_Load_Char( face, str[n], FT_LOAD_RENDER );
my_draw_bitmap( &slot->bitmap,
slot->bitmap_left,
var.yres - slot->bitmap_top );
pen.x += slot->advance.x;
if(str[n] == '\n')
{
pen.y = (var.yres - y - 24)*64;
pen.x = x * 64;
b }
}
}
测试效果:
细心的朋友会发现,其实换行的时候,我给定了下一行文字的起点(即设置的pen.x pen.y)
打印汉字没问题,但是如果中英文参杂时,比如g,会不会出现混叠呢?
其实会的,这里有个解决办法,也是要讲解的重点——提取计算下一行(整行, 而不是单个字体)的最大宽和高
修改后的代码如下:
typedef struct TGlyph {
FT_UInt index; /* glyph index */
FT_Vector pos; /* glyph origin on the baseline */
FT_Glyph image; /* glyph image */
} TGlyph, *PGlyph;
/* 从一行字符串中获取glyph保存到glyphs[]数组的image */
int Get_Glyphs_Frm_Wstr(FT_Face face, wchar_t *wstr, TGlyph glyphs[])
{
int n;
PGlyph glyph = glyphs;
int pen_x = 0;
int pen_y = 0;
int error;
FT_GlyphSlot slot = face->glyph;
for(n = 0; n < wcslen(wstr); n++)
{
glyph->index = FT_Get_Char_Index(face, wstr[n]);
glyph->pos.x = pen_x;
glyph->pos.y = pen_y;
if(wstr[n] == '\n') // 计算的字符串以换行符作为一行结束的标志
break;
error = FT_Load_Glyph(face, glyph->index, FT_LOAD_DEFAULT);
if(error)
continue;
error = FT_Get_Glyph(face->glyph, &glyph->image);
if(error)
continue;
FT_Glyph_Transform(glyph->image, 0, &glyph->pos);
pen_x += slot->advance.x;
glyph++;
}
return (glyph - glyphs);
}
/* 根据glyphs[]数组保存的glyph计算每个glyph的cbox,cbox里存有xMin xMax yMin yMax */
/* 然后依次比较每个glyph的cbox以获得xMin xMax yMin yMax,并把最终结果保存到abbox */
void compute_string_bbox(TGlyph glyphs[], FT_UInt num_glyphs, FT_BBox *abbox)
{
FT_BBox bbox;
int n;
bbox.xMin = bbox.yMin = 32000;
bbox.xMax = bbox.yMax = -32000;
for(n = 0; n < num_glyphs; n++)
{
FT_BBox glyphs_bbox;
FT_Glyph_Get_CBox(glyphs[n].image, FT_GLYPH_BBOX_TRUNCATE, &glyphs_bbox);
if(glyphs_bbox.xMin < bbox.xMin)
bbox.xMin = glyphs_bbox.xMin;
if(glyphs_bbox.yMin < bbox.yMin)
bbox.yMin = glyphs_bbox.yMin;
if(glyphs_bbox.xMax > bbox.xMax)
bbox.xMax = glyphs_bbox.xMax;
if(glyphs_bbox.yMax > bbox.yMax)
bbox.yMax = glyphs_bbox.yMax;
}
*abbox = bbox;
}
int draw_lines(unsigned int x, unsigned int y, wchar_t* str)
{
FT_UInt num_glyphs;
TGlyph glyphs[MAX_GLYPHS]; /* glyphs table */
FT_BBox bbox;
int line_box_width;
int line_box_height;
int count;
if(x > var.xres || y > var.yres || !str)
return -1;
printf("xres:%d yres:%d\n",var.xres, var.yres);
pen.x = x * 64;
pen.y = (var.yres - y) * 64;
int n;
for(n = 0; n < wcslen(str); ++n)
{
if(str[n] == '\n')
{
n += 1;
// 注意要获取上行的宽、高
num_glyphs = Get_Glyphs_Frm_Wstr(face, &str[count], glyphs);
compute_string_bbox(glyphs, num_glyphs, &bbox);
line_box_height = bbox.yMax - bbox.yMin;
line_box_width = bbox.xMax - bbox.xMin;
pen.y = pen.y -(line_box_height + 1) * 64; // 注意 -1
pen.x = x * 64;
count = num_glyphs;
}
FT_Set_Transform(
face, /* target face object */
0,
&pen); /* pointer to 2d vector */
FT_Load_Char( face, str[n], FT_LOAD_RENDER );
my_draw_bitmap( &slot->bitmap,
slot->bitmap_left,
var.yres - slot->bitmap_top );
pen.x += slot->advance.x;
}
}
注意在计算得到行高后,记得-1,不然会出现覆盖现象,如下图所示
-1之后,最终效果:
至于为什么-1呢? 因为,比如说 23 至 8 差多少, 你不能说是 23-8=15吧,而应该是 23 -8 + 1 = 16才对,包括 8 本身。
因此,这里需要减一操作,不然覆盖了呀
再来看看,如果显示四行内容,能成功吗?看下图
好了,这篇文章告一段落,原本打算绘制图片的,考虑到篇幅问题,留给下篇博文吧 :)
如果有任何疑问、错误,欢迎指出来噢 :)
一起进步呀,加油!