本章主要简单介绍FreeType,以及结合TrueType来写一个代码示例
你将学习到的知识点有
- 什么是FreeType,它和TrueType、OpenTrue有什么联系
- 写代码实现使用FreeType读取TrueType字体文件,生成位图数据,然后在屏幕上把位图数据显示出来。笔者使用Qt的窗口来模拟屏幕,实现真正的从字符串数据到可视的字符的效果。效果图如下:
TrueType和FreeType的简单应用
1. FreeType介绍
百度百科:FreeType库是一个完全免费(开源)的、高质量的且可移植的字体引擎,它提供统一的接口来访问多种字体格式文件,包括TrueType, OpenType, Type1, CID, CFF, Windows FON/FNT, X11 PCF等。支持单色位图、反走样位图的渲染。
直白地说,freetype就是用来生成字体的位图的一个开源函数库。它可以渲染truetype、opentype、type1等字体文件。truetype在上一篇博客已经介绍过,而opentype是truetype的一个升级版。opentype是Microsoft和Adobe之间竞争与合作的产物,它嵌入了PostScript字体,功能更加强大。
2.TrueType+FreeType生成位图数据
准备材料:
- truetype字体文件KhmerUI.ttf,这是高棉语的字体文件
- freetype字体引擎源码,这里使用的是freetype-2.10.1
- ubuntu16.04,我是虚拟机里面运行的
- 安装Qt Creator
以上材料的下载链接在这里。
ubuntu16.04安装freetype:
一般在ubuntu下,解压并进入源码根目录,直接:
./configure
make
sudo make install
三部曲,我选择最简单的安装方式。安装过程中可能会出现依赖问题,请自行查找资料解决。
ubuntu16.04安装Qt Creator
使用Qt是为了模拟屏幕来显示渲染出来的字符位图数据,我使用的Qt版本是Qt Creator 3.4.2 (opensource),读者可以去Qt官网上直接下载安装文件。
工作流程
回顾一下前一章的freetype加载字符到渲染出字符的点阵数据的流程
- 初始化freetype库
- 使用freetype库打开truetype文件(一般是.tty后缀的文件),加载字符的全部数据
- 设置字符的大小
- 根据要显示的字符设置字符的编码方式,freetype默认使用的是Unicode编码。一般情况是这样的:
(1)要显示的字符使用utf8格式编码
(2)使用freetype渲染前,需要将utf8转换Unicode编码,转换算法网上可以搜索到很多 - 渲染字符成为需要的点阵数据,然后一个个刷到屏幕上
- 字符的显示的位置要根据字符的度量调整,才能正常显示
开始写代码:
- 目录结构
.
├── contrib -----第三方库头文件
│ ├── freetype2 -------freetype2库的头文件
├── encoding_conv.c ----utf8转换成ucs2的函数接口
├── encoding_conv.h
├── font.qrc
├── fonts
│ └── KhmerUI.ttf -------高棉语truetype文件
├── lib
│ ├── freetype-2.10.1 -------freetype2的共享库文件
├── main.cpp
├── mainwin.cpp
├── mainwin.h
├── qt_font_freetype.pro ------Qt工程文件
└── qt_font_freetype.pro.user
首先使用Qt Creator创建工程,添加代码和相应的文件
- 主要代码分析
// mainwin.h
#ifndef MAINWIN_H
#define MAINWIN_H
#include <QWidget>
class MainWin : public QWidget
{
Q_OBJECT
public:
MainWin(QWidget *parent = 0);
~MainWin();
quint32 screen_width();
quint32 screen_height();
quint8 screen_pixel_mode();
bool set_simulator_screen_color(quint32 color);
qint32 draw_text(int x, int y, char *text, unsigned int color, int font_size);
protected:
void paintEvent(QPaintEvent *event);
private:
unsigned int* simulator_screen;
quint32 simulator_screen_width;
quint32 simulator_screen_height;
quint8 pixel_mode;
QPixmap* screen_pixmap;
// truetype文件的路径,需要根据自己的需要设置
char* ttf_file = "/home/chen/gui_workspace/qt_font_freetype/fonts/KhmerUI.ttf";
};
#endif // MAINWIN_H
//mainwin构造函数
MainWin::MainWin(QWidget *parent)
: QWidget(parent)
{
simulator_screen_width = 500;
simulator_screen_height = 300;
//设置窗口大小
resize(QSize(simulator_screen_width, simulator_screen_height));
//像素模式使用argb 4个字节
pixel_mode = 4;
//此处分配的内存用于模拟显存
simulator_screen = (unsigned int *)qMallocAligned(simulator_screen_width*simulator_screen_height*pixel_mode, 1);
if (simulator_screen == Q_NULLPTR)
{
qDebug("simulator_screen malloc failed\n");
}
set_simulator_screen_color(WHITE_COLOR);
//调用自己实现的draw_text把字符显示到窗口
draw_text(40, 40, "This is a FreeType test!", RED_COLOR, 27);
}
//draw_text函数实现,此处是本篇最重要的代码
qint32 MainWin::draw_text(int x, int y, char *utf8_text, unsigned int color, int font_size)
{
int i = 0;
int j = 0;
int a = 0;
int b = 0;
int k = 0;
int error;
int whcar_len;
int width = 0;
int m_font_size = font_size;
wchar_t *ucs2_text;
int length;
unsigned int draw_color;
int pen_x = x;
int pen_y = y;
FT_Face FTFace;
FT_Library library;
FT_UInt glyphIndex;
if (utf8_text == NULL)
{
return 0;
}
if (font_size < 0)
{
m_font_size = 18;
}
length = strlen(utf8_text) + 1;
ucs2_text = (wchar_t *)malloc(length * sizeof(ucs2_text));
if (!ucs2_text)
{
return 0;
}
utf8_to_ucs2(utf8_text, length, ucs2_text, length);
whcar_len = wcslen(ucs2_text);
if (FT_Init_FreeType(&library) != 0)
{
qDebug("error func:%s line:%d \r\n",__FUNCTION__,__LINE__);
return 0;
}
if (FT_New_Face(library, ttf_file, 0, &FTFace) != 0)
{
qDebug("error func:%s line:%d \r\n",__FUNCTION__,__LINE__);
return 0;
}
FT_Set_Char_Size(FTFace, 0, m_font_size * 64, 96, 96);
for (k = 0; k < whcar_len; k++)
{
glyphIndex = FT_Get_Char_Index(FTFace, ucs2_text[k]);
error = FT_Load_Glyph(FTFace, glyphIndex, FT_LOAD_DEFAULT);
if (error)
{
qDebug("error func:%s line:%d \r\n",__FUNCTION__,__LINE__);
continue;
}
error = FT_Render_Glyph(FTFace->glyph, ft_render_mode_normal);
if (error)
{
qDebug("error func:%s line:%d \r\n",__FUNCTION__,__LINE__);
continue;
}
FT_GlyphSlot slot = FTFace->glyph;
FT_Glyph_Metrics metrics = FTFace->glyph->metrics;
FT_Bitmap bitmap = slot->bitmap;
qDebug("slot->bitmap_left = %d, slot->bitmap_top = %d, bitmap.rows = %d, bitmap.width = %d\n", slot->bitmap_left, slot->bitmap_top, bitmap.rows, bitmap.width);
qDebug("metrics.height>>6 = %d, metrics.width>>6 = %d, metrics.vertBearingX>>6 = %d, metrics.vertBearingY>>6 = %d\n", metrics.height>>6, metrics.width>>6, metrics.vertBearingX>>6, metrics.vertBearingY>>6);
for (a = 0, j = pen_y; j < simulator_screen_height && a < bitmap.rows; j++, a++)
{
for (b = 0, i = pen_x; i < simulator_screen_width && b < bitmap.width; i++, b++)
{
draw_color = (color & 0x00ffffff) | (bitmap.buffer[ a*bitmap.width + b ] << 24);
//把字符的点阵数据放到显存中
simulator_screen[j*simulator_screen_width + i] = draw_color;
}
}
width += slot->advance.x >> 6;
pen_x += slot->advance.x >> 6; //增加水平方向上的画笔的位置
}
qDebug(" %s width = %d\n", __FUNCTION__, width);
//重新刷新窗口,也就是会自动调用窗口的paintEvent函数,重载了窗口的paintEvent函数,然在里面把显存的数据刷新出来
this->repaint();
return width;
}
//重载窗口的paintEvent函数
void MainWin::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QImage image((uchar *)simulator_screen, screen_width(), screen_height(), screen_width()*screen_pixel_mode(), QImage::Format_ARGB32);//data数组 //355宽度 //frame_len 高度//每行字节数//格式
QPixmap pixmap=QPixmap::fromImage(image);
painter.drawPixmap(this->rect(), pixmap);
}
- 显示效果1
由以上显示效果可以看出,字符串是显示出来了,但是好像有些奇怪,垂直方向的字距有问题。原因是垂直方向上没有进行字距调整,freetype渲染出来的位图大小不一定都是相同的,但是freetype有预设了全局的字符串大小,通过FTFace->size可以访问。预设全局大小意味着这是一个合适的大小值,而且全部的字符大小都不会超过它。以下是字距调整的代码:
//其实只需要再画每一个字符前,调整一下坐标就可以了
pen_x += slot->bitmap_left;
pen_y = (FTFace->size->metrics.ascender >> 6) - slot->bitmap_top;
for (a = 0, j = pen_y; j < simulator_screen_height && a < bitmap.rows; j++, a++)
{
for (b = 0, i = pen_x; i < simulator_screen_width && b < bitmap.width; i++, b++)
{
draw_color = (color & 0x00ffffff) | (bitmap.buffer[ a*bitmap.width + b ] << 24);
simulator_screen[j*simulator_screen_width + i] = draw_color;
}
}
以下是垂直字距调整后的显示效果:
- 显示效果2
对于垂直字距调整的代码:
pen_x += slot->bitmap_left;
pen_y = (FTFace->size->metrics.ascender >> 6) - slot->bitmap_top;
为什么这样写?以下是答案:
我们的屏幕坐标轴使用的是如下图所示的坐标轴,Y轴向下增长:
而freetype渲染出来的位图的坐标是和笛卡尔坐标,即x轴水平向右增长,y轴垂直向上增长。
所以对应于 slot->bitmap_left,slot->bitmap_top,FTFace->size->metrics.ascender就很好理解。如下图所示:
黑色的矩形框是预设的全局字符大小,是固定的,显示的字符一般不能超过它的大小。但是渲染出来的点阵数据的bitmap_left和bitmap_top不是固定的,所以如果不进行字距调整的话,就会显示地很奇怪。其中FTFace->size->metrics.ascender >> 6,>>6表示这个数是26.6格式数,所以要除以64
总结
- truetype font技术描述了字体的各种规则方式,如点和线构成了笔画(Contour),其中曲线使用二阶贝塞尔样条(Bezier-spline)来描述;Contour又组成了最小的描画单位glyph;还有虚拟框EM、坐标空间、转换规则等等。
- truptype文件是truetype font技术的具体的实现载体
- freetype库是truetype的字体引擎,说白了使用freetype就可以翻译truetype描述的各种规则,然后根据这些规则来获得想要的点阵数据
- 笔者搜索了很多网上关于freetype技术的博客,发现几乎都是给出了几行实例的代码,还有一些介绍性的文字,没有让人可以真正得看到一些实际的效果,或者实际的可操作性也不强。本文使用QT的窗口来实现字符的显示效果,能让人从抽象都具体,由浅入深地理解freetype和truetype相关的技术
拓展
freetype一般情况下只能按照给定字符顺序一个个地把字符渲染成点阵数据,但是在某些语言中,如果某两个字符相邻组合在一起可能需要变化成另外一个字符,如"fi"两个相邻组合到一起可能要写成:
可能语义也会发生改变。这些都是一些特殊的规则,可能这些规则在truetype font中属于一种自定义的规则,因为它是属于某一种语言的,它不是通用的,它是由具体的字体设计厂商规定的。能解决这种问题的一个方法是使用harfbuzz整形引擎,这将在下一篇博客介绍。
他时若遂凌云志, 敢笑黄巢不丈夫