freetype矢量字体 —— 介绍篇
矢量字体
什么是矢量字体?
点阵字库显示英文字母和汉字时,大小固定,如果放大或缩小,会出现模糊、锯齿等现象。为了解决该问题,可以使用矢量字体(Vector font)。矢量字体又叫Outline font,即轮廓字体。
矢量字体形成有3步:
1)确定关键点;
2)使用数学曲线(贝塞尔曲线)连接关键点;
3)填充闭合曲线内部空间。
什么是字体的关键点?
以字母“A”为例,它的关键点如下图黄色点所示:
再用数学曲线(如贝塞尔曲线),将关键点连接起来,得到一系列封闭曲线,如下图所示:
最后,填充封闭闭合区间,就显示出字母“A”。如下图所示:
矢量字体的优势在于:
由于关键点的相对位置不变,即时放大或缩小字体,只要选取的数学曲线平滑,最终显示出的字体就不会变形,也不会出现模糊和锯齿现象。
字体分类
矢量字体主要包括 Type1、TrueType、OpenType等几类。
-
Type1全程PostScript Type1,由Adobe公司于1985年提出的一套矢量字体标准。Type1基于PDL标准作为打印描述语言,可用于高端打印机,特点是打印时不会产生变形、速度快。Type1是非开放字体,Adobe对其征收高额使用费。Type1使用三次贝塞尔曲线来描述字形。
-
TrueType是Apple与Microsoft公司于1991年联合提出的另一套矢量字标准。TrueType使用二次贝塞尔曲线来描述字形,Type1字体比TrueType字体更精确美观。打印时,TrueType会先翻译为PDL,会产生一定的形变,不如Type1美观。
-
OpenType是Type1与TrueType之争的最终产物,由Adobe与Microsoft公司于1995年联手开发的一种兼容Type1和TrueType,并且支持Unicode的字体。OpenType兼具Type1和OpenType的特点。
字体文件扩展名
矢量字体文件扩展名ttf,点阵字体文件的扩展名是fon。
ttc(全称TrueType Collection)字体,是TrueType字体集成文件,在一个单独文件结构中包含多种字体,以便有效地共享轮廓数据,当多种字体共享同一笔画时,TTC技术可以有效地减少字体文件的大小。
ttc是几个ttf合成的字库,安装后字体列表中可以看到2个以上字体。ttf字体只包含一种字型。
freetype字体引擎
freetype库是一个免费的、可移植的字体引擎开源库,提供统一的接口来访问多种字体格式文字。
官网:https://freetype.org/
字符图像(char image)称为关键点(glyph),单个字符能拥有几个不同的图像,存在于字体文件中。Windows使用的字体文件中c:\Windows\Fonts目录下。
如何在字体文件中,找到字符的关键点?
先确定该字符的编码值,如ASCII码(ANSI编码)、GB2312码(GBK编码)、UNICODE码等。如果字体文件支持该编码格式(charset),就能用编码值去找到该字符的关键点(glyph)。有些字体文件支持多种编码格式(charset),在文件中称为charmaps(复数形式,表示可能支持多种charset)。
以simsun.ttc字体为例,用Windows字体查看器打开文件:
将其用表格形式抽象起来:
该字体文件格式组成:头部charmaps + 后面的“A B 中 国”等glyph(关键点/字符图像)示意图。
charmaps 表示字符映射表,包含支持哪些编码(ASCII/GB2312/UNICODE/BIG5等)信息。如果字体文件支持该编码,使用编码值可以通过charmap找到对应的glyph(关键点)。一般而言,都支持UNICODE码。
程序编码上,freetype引擎提供了从字体文件中提取glyph的功能。
文字的显示过程
1)给定一个字符,确定其编码值(ASCII/GB2312/UNICODE等);
2)设置字体大小;
3)根据编码值,从字体文件头部通过charmap找到对应的关键点(glyph),freetype会根据字体大小调整关键点;
4)把关键点转换为位图点阵;
5)LCD上显示文字对应的位图点阵。
freetype显示文字流程
参考freetype官方帮助文档(freetype-doc-2.10.2/docs/tutorial中的例程和step1/step2/step3文档),可以知道程序中,使用freetype库显示文字流程为:
1)包含头文件及API头文件:ft2build.h;
#include <ft2build.h>
#include FT_FREETYPE_H // 基础的FreeType 2 API
#include FT_GLYPH_H // 管理Glyph Images
2)初始化: FT_InitFreetype
初始化FreeType库
3)加载(打开)字体Face: FT_New_Face
face描述了一个给定(印刷)字体和样式
4)设置字体大小:FT_Set_Char_Size或FT_Set_Pixel_Size
FT_Set_Char_Size:设置标称尺寸(nominal size in points,1pt=0.376mm);
FT_Set_Pixel_Size:设置标称尺寸(nominal size (in pixels),单位是像素点);
5)(可选)选择charmap: FT_Select_Charmap
通过编码标记,选择一个给定的charmap。face对象创建时,默认从字体文件中查找Unicode charmap并且选择之。如果想要改变,可以调用该函数。
当前charmap可以通过face->charmap来访问。
6)根据编码值charcode找到glyph_index: glyph_index = FT_Get_Char_Index(face, charcode)
获取指定字符编码charcode的glyph索引。加载glyph图像到slot的函数FT_Load_Glyph,会用到该索引。
7)根据glyph_index取出glyph: FT_Load_Glyph(face, glyph_index, load_flags)
8)转换为位图:FT_Render_Glyph
将给定的glyph 图像转换为bitmap(位图)
9)移动或旋转:FT_Set_Transform
有移动或旋转glyph 图像需求时,调用该函数。
10)最后显示出来
将得到的最终glyph对应的bitmap,传递给framebuffer,通过LCD显示出来。
上面步骤6、7、8可以用一个函数替换:FT_Load_Char(face, charcode, FT_LOAD_RENDER),可以直接得到glyph对应的bitmap。
使用freetype显示单个文字
使用wchar_t表示字符的UNICODE值
如果要在LCD上显示一个矢量字体,首先要确定其编码值。常用UNICODE编码,在程序里使用UNICODE字符串时,需要使用wchar_t(宽字符))来表示。
控制台打印宽字符串对应的UNICODE值示例:
// test_wchar.c, 要求以UTF-8编码保存
#include <stdio.h>
#include <string.h>
#include <wchar.h>
/*
编译命令:
gcc -o test_wchar test_wchar.c
运行命令(PC运行):
./test_wchar
*/
int main(int argc, char *argv[])
{
wchar_t *chinese_str = L"中gif";
unsinged int *p = (wchar_t *)chinese_str;
int i;
printf("sizeof(wchar_t) = %d, str's Unicode: \n", sizeof(wchar_t));
for (i = 0; i < wcslen(chinese_str); i++) {
printf("0x%x ", p[i]);
}
printf("\n");
return 0;
}
运行结果:
sizeof(wchar_t) = 4, str's Unicode:
0x4e2d 0x67 0x69 0x66
每个wchar_t代表一个UNICODE,而UNICODE用4byte保存,为何后面3个英文字符对应的UNICODE值"0x67 0x69 0x66"都是1byte呢?
因为打印的时候,忽略了0,即0x67 <=> 0x0067, 0x69 <=> 0x0069, 0x66 <=> 0x0066
注:如果test_wchar.c以ANSI(GB2312)格式保存,那么需要用下面命令编译:
$ gcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_wchar.c
使用freetype得到位图
参考freetype-2.10.2/docs/tutorial/example1.c 。
使用freetype得到一个字符的bitmap,需要4个步骤:
1)初始化freetype库
err = FT_Init_FreeType(&library); /* initialize library */
2)加载字体文件,保存在&face中
err = FT_New_Face(library, argv[1], 0, &face); /* create face object */
/* error handling omitted */
slot = face->glyph; // 从face中获取FT_GlyphSlot, 代表一个字符的glyph image. 文字的bigmap就保存在slot中
3)设置字体大小
FT_Set_Pixel_Sizes(face, font_size, 0); /* set font size in pixel */
4)根据字符编码值得到bitmap
FT_Load_Char 等于这3个函数功能:
- 根据编码值获取glyph_index: FT_Get_Char_Index
- 根据glyph_index取出glyph: FT_Load_Glyph
- 渲染出bitmap: FT_Render_Glyph
wchar_t *chinese_str = L"繁";
/* load glyph image into the slot (erase previous one) */
err = FT_Load_Char(face, chinese_str[0], FT_LOAD_RENDER);
执行FT_Load_Char后,字符的bitmap保存到slot->bitmap中(即face->glyph->bitmap)。
LCD上显示bitmap
应用编程的角度看,要在LCD上显示内容,就要获取LCD对应的Framebuffer缓存,然后修改对应位置的像素点的RGB值。
每个字符对应的bitmap里的数据格式是怎样的?
参考example1.c,可以得到
FT_GlyphSlot slot = face->glyph;
unsinged char buffer = slot->bitmap.buffer; // 提取字符对应的bitmap的buffer
// bitmap的buffer, 可以理解为一个长为width, 宽为rows的二维像素矩阵, 每个像素点占用1byte
int width = slot->bitmap.width;
int rows = slot->bitmap.rows;
调用draw_bitmap显示字符(bitmap)
// 以屏幕中央位置为起始点, 显示字符对应的bitmap
draw_bitmap(&slot->bitmap, var.xres/2, var.yres/2);
自定义draw_bitmap的实现如下:
// LCD坐标(x, y)为起始位置, 显示bitmap
void draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y)
{
FT_Int i, j, p, q;
FT_Int x_max = x + bitmap->width;
FT_Int y_max = y + bitmap->rows;
for (j = y, q = 0; j < y_max; j++, q++) { // y轴方向(纵坐标)
for (i = x, p = 0; i < x_max; i++, p++) { // x轴方向(横坐标)
if (i < 0 || j < 0 || i >= var.xres || j >= var.yres)
continue;
// 设置lcd 位置(i, j)像素点值(只置1, 不清除)
// image[j][i] |= bitmap->buffer[q * bitmap->width + p];
lcd_put_pixel(i, j, bitmap->buffer[q * bitmap->width + p]);
}
}
}
注意:由于bitmap中每个像素用1byte表示,而0x00RRGGBB的颜色格式中,只能表示蓝色(最后一个byte),因此draw_bitmap在LCD上显示的文字是蓝色的。
完整源代码参见:freetype_show_font.c | gitee
编译
编译命令:
$ arm-linux-gnuabihf-gcc -o freetype_show_font freetype_show_font.c -lfreetype
注意:用"-lfreetype"链接freetype库,前提是已经为交叉编译工具链安装freetype库。
运行
将目标程序与字体文件simsun.ttc拷贝至开发板,置于同一目录。执行运行命令:
# ./freetype_show_font ./simsun.ttc
或者同时加上设置字体大小300(in pixel)
# ./freetype_show_font ./simsun.ttc 300
实验成功的话,可以在LCD中间看到一个“繁”字(蓝色)。
使用freetype显示一行文字
LCD上指定位置显示一行文字,就需要在LCD坐标系下,考虑该行文字各个文字的位置。
记文字外框左上角坐标(x, y),那么在LCD坐标系下,字符串"你好baidu.com"及其外框示意图如下:
LCD坐标系与笛卡尔坐标系
LCD坐标系中,原点在屏幕左上角。笛卡尔坐标系中,原点在左下角。freetype使用笛卡尔坐标系,LCD显示内容时,使用LCD坐标系,因此当显示内容时,需要转换为LCD坐标系。
两种坐标系下,坐标系换算方式如下图所示:
LCD中的(x,y),在笛卡尔坐标系中,x坐标是一样的,只需要转换y坐标。其中,H表示LCD宽度,即横向像素点个数;V表示LCD高度,即纵向像素点个数。
设笛卡尔坐标系下,纵坐标y' + y = V => y' = V - y
也就是说,LCD坐标系中(x, y) => 笛卡尔坐标系中(x, V-y)
每个字符大小可能不同
FT_Set_Pixel_Sizes设置字体大小时,只是“期望值”(请求值),而非最终值。比如,"你好baidu.com",如果把"."显示成与其他汉字一样大,就不好看。
显示一行文字时,后面的文字的位置,会受到前面文字的影响。而freetype也考虑到了这点,详细可参考freetype-doc-2.10.2/docs/tutorial/step2.html
对于字符g的glyph metrics(关键点度量):
注:笛卡尔坐标系
显示一行文字时,改行文字会基于同一个基线来绘制bitmap,我们把这条基线称为baseline,也就是上图中经过origin平行于x轴的直线。
在baseline上,每个字符都有一个原点(origin),当前字符的origin + advacen 可得到下一个(相邻)字符的origin。一个字符的bitmap占据的四个角落,不一定与原点重合。
如上图所示,用xMin、xMax、yMin、yMax表示字符的bitmap占据的位置对应外框边界值。但如何获取这4个边界值呢?可以使用FT_Glyph_Get_CBox,将这4个值保存到FT_BBox结构体中。
FT_BBox结构体定义:
typedef struct FT_BBox_
{
FT_Pos xMin, yMin;
FT_Pos xMax, yMax;
} FT_BBox;
怎么在指定位置显示一行文字?
对于一行文字,每个字符都有各自的外框(xMin, xMax, yMin, yMax)。
笛卡尔坐标系下,如果想要在指定位置(x, y)显示一行文字,那么(x, y)作为第一个字符左上角坐标。
1)先指定第1个字符的origin坐标为(0, 0),调用FT_Glyph_Get_CBox计算出该字符的外框;
2)再通过当前字符的origin + advance,计算出右边相邻字符的origin,也调用FT_Glyph_Get_CBox计算出相邻字符的外框;
3)想在(x,y)处(以改点为字符外框左上角)显示一行文字,需要调整字符的pen坐标。pen坐标定义为字符的origin原点,在绝对坐标系下坐标是什么呢?这是调整pen坐标关键。
假设在单个字符的glyph metrics对应的相对坐标系下,此时字符origin坐标(0, 0),字符外框左上角坐标(x', y');
那么,在绝对坐标系下,要显示字符的起始位置,即字符外框左上角坐标(x, y),可以得到pen坐标为(x-x', y-y')。
也就是说,想在(x,y)处显示一个字符,那么只需要将其origin移动到pen (x-x', y-y')即可。其中, (x', y')是该字符在相对坐标系下,左上角坐标,可通过FT_Glyph_Get_CBox求的。
freetype重要数据结构
1.FT_Library
FT_Library代表freetype库。使用freetype,先要初始化freetype库。
示例代码:
FT_Library library; /* freetype library */
int err = FT_Init_FreeType(&library); /* initialize freetype library */
2.FT_Face
代表一个矢量字体文件,源码中打开字体文件后,可以得到一个face object
err = FT_New_Face(library, font_file_path, 0, &face); /* create face object */
3.FT_GlyphSlot
slot用于保存字符的处理后的结果,如旋转后的glyph image,bitmap。
一个face对象中有很多字符,生成字符点阵bitmap时,新生成的bitmap保存在哪儿?
答案是保存在插槽中:slot = face->glyph。
既然face中有多个字符,那后面生成的字符bitmap时,存放在哪儿呢?
答案是也保存在face->glyph中,会覆盖前1个字符的bitmap。
FT_GlyphSlot slot = face->glyph; /* slot: 保存字体的处理结果 */
4.FT_Glyph
字体文件中包含了字符的原始关键点信息(glyph image),调用freetype函数可以对其进行放大、缩小、旋转等操作。这些新的关键点保存在插槽中(slot = face->glyph)
新的关键点用FT_Glyph表示,可用下面方式从slot获取glyph:
err = FT_Get_Glyph(slot, &glyph);
5.FT_BBox
FT_BBox表示一个字符的外框,包含字符的边界信息xMin, yMin, xMax, yMax,即glyph的外框。结构体定义:
typedef struct FT_BBox_
{
FT_Pos xMin, yMin;
FT_Pos xMax, yMax;
} FT_BBox;
如何从glyph获取外框信息?
可以调用FT_Glyph_Get_CBox:
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &bbox);
利用上述数据结构及其API,进行文字显示的通用流程如下:
FT_Library library; /* 对应freetype库 */
FT_Face face; /* 对应字体文件 */
FT_GlyphSlot slot; /* 存放字符的处理结果, 包含glyph和bitmap */
FT_Glyph glyph; /* 字符处理后的glyph image */
FT_BBox bbox; /* 字符外框 */
FT_Vector pen; /* 字符的原点 */
err = FT_Init_FreeType(&library); /* 初始化freetype库 */
err = FT_New_face(library, font_file_path, 0, &face); /* 加载字体文件, 创建face object */
FT_Set_Pixel_Sizes(face, 24, 0); /* 设置字体大小(in pixel) */
/* 确定坐标 */
// 注意这里的pen坐标 (0,0)只是示例
pen.x = 0;
pen.y = 0;
FT_Set_Transform(face, 0, &pen); /* 旋转glyph image, 移动origin到pen */
/* 根据font_code加载字符, 得到新的glyph和bitmap, 保存在slot中 */
err = FT_Load_Char(face, font_code, FT_LOAD_RENDER);
err = FT_Get_Glyph(slot, &glyph); /* 从slot中得到新的glyph */
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &bbox); /* 从glyph得到字符外框bbox */
draw_bitmap(&slot->bitmap, lcd_x, lcd_y); /* 绘制字符bitmap, 字符bitmap保存在slot->bitmap中 */
显示一行字符串
显示一行字符的关键,是得到改行字符串的外框(compute_string_bbox),然后以外框左上角反推出第一个字符origin的位置。而有了第一个字符origin后,可以推算出后一个字符的origin = 前一个字符origin + advance。
这样,就可以按顺序依次绘制出每个字符的bitmap,从而显示文字。
int display_string(FT_Face face, wchar_t *wstr, int lcd_x, int lcd_y)
{
int i, err;
FT_BBox bbox; // 整行文字的外框
FT_Vector pen; // 字符的origin要移动到的位置
FT_Glyph glyph;
FT_GlyphSlot slot = face->glyph;
// 把输入的LCD坐标转换为笛卡尔坐标
int x = lcd_x;
int y = var.yres - lcd_y; // var.yres是通过ioctl获取的屏幕纵向分辨率(像素个数)
/* 自定义函数计算外框 */
compute_string_bbox(face, wstr, &bbox);
/* 反推出第一个字符的origin */
// 字符外框左上角相对坐标(x', y') = (xMin, yMax)
// 因此, origin在绝对坐标系下, 坐标为(x - x', y - y') = (x - xMin, y - yMax)
// 乘以64是因为FT_Set_Transform要求坐标单位是1/64像素
pen.x = (x - bbox.xMin) * 64; // 单位: 1/64像素
pen.y = (y - bbox.yMax) * 64;
/* 处理并显示每个字符 */
for (i = 0; i < wcslen(wstr); i++) {
/* 旋转、移动 */
FT_Set_Transform(face, 0, &pen);
/* 加载bitmap: load glyph image into slot, erase previous one */
err = FT_Load_Char(face, wcstr[i], FT_LOAD_RENDER);
if (err) {
printf("FT_Load_Char\n");
return -1;
}
/* 在LCD上绘制: 使用LCD坐标 */
// slot中保存的坐标是笛卡尔坐标, 字符外框左上角坐标(bitmap_left, bitmap_top)
// 转换为LCD坐标 (bitmap_left, var.yres - bitmap_top)
draw_bitmap(&slot->bitmap, slot->bitmap_left, var.yres - slot->bitmap_top);
/* 同一行下一个字符的origin */
pen.x += slot->advance.x;
pen.y += slot->advance.y;
}
return 0;
}
计算一行文字的外框
由于每个字符的外框都不一样,遍历每个文字的外框,然后更新整行的外框。整行外框一定是包含该行所有字符。
每个字符的外框 => 整行外框
// 计算字符串wstr外框, 结果保存到abbox中
// @param face face对象, 保存了字体信息
// @param wstr 要显示的一行宽字符串
// @param abbox [out] 保存改行字符串的外框计算结果
// @return 0表示计算成功; -1表示失败
//
int compute_string_bbox(FT_Face face, wchar_t *wstr, FT_BBox *abbox)
{
int i, err;
FT_BBox bbox; /* 当前整行外框 */
FT_BBox glyph_bbox; /* 当前字符外框 */
FT_Vector pen; /* 当前字符glyph image的origin在笛卡尔坐标系下的位置 */
FT_Glyph glyph;
FT_GlyphSlot slot = face->glyph; /* 插槽slot */
/* 指定第1个字符的origin为(0,0), 方便计算外框, 无需考虑原点的影响 */
pen.x = 0;
pen.y = 0;
/* 循环计算每个字符的外框(bounding box), 然后更新整行的外框 */
for (i = 0; i < wcslen(wstr); i++) {
/* 将当前字符的origin移动到pen */
FT_Set_Transform(face, 0, &pen); // 矩阵传入0, 表示不旋转
/* 加载bitmap: load glyph image into the slot, erase previous one */
err = FT_Load_Char(face, wstr[i], FT_LOAD_RENDER);
if (err) {
printf("FT_Load_Char error\n");
return -1;
}
/* get glyph image */
err = FT_Get_Glyph(face->glyph, &glyph);
if (err) {
printf("FT_Get_Glyph error\n");
return -1;
}
/* 从当前字符对应的glyph image得到当前字符的外框glyph_bbox */
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &glyph_bbox);
/* 更新整行外框bbox */
bbox.xMin = min(glyph_bbox.xMin, bbox.xMin);
bbox.yMin = min(glyph_bbox.yMin, bbox.yMin);
bbox.xMax = max(glyph_bbox.xMax, bbox.xMax);
bbox.yMax = max(glyph_bbox.yMax, bbox.yMax);
/* 计算同一行下个字符的origin位置: + advance */
pen.x += slot->advance.x;
pen.y += slot->advance.y;
}
/* 保存结果: 整行外框 */
if (abbox) *abbox = bbox;
return 0;
}
编译、运行
# PC交叉编译
$ arm-linux-gnueabihf-gcc -o show_line show_line.c -lfreetype
# 开发板运行
# 5个参数分别表示: 目标程序, 字体文件, 要显示一行字符的LCD x坐标, y坐标, 字体大小
# ./show_line ./simsun.ttc 100 200 50
完整源码,参见:show_line.c | gitee
参考
韦东山. 嵌入式Linux应用开发完全手册V4.0
freetype官方帮助文档:https://freetype.org/freetype2/docs/tutorial/index.html