freetype矢量字体 —— 介绍篇

矢量字体

什么是矢量字体?

点阵字库显示英文字母和汉字时,大小固定,如果放大或缩小,会出现模糊、锯齿等现象。为了解决该问题,可以使用矢量字体(Vector font)。矢量字体又叫Outline font,即轮廓字体。

矢量字体形成有3步:
1)确定关键点;
2)使用数学曲线(贝塞尔曲线)连接关键点;
3)填充闭合曲线内部空间。

什么是字体的关键点?
以字母“A”为例,它的关键点如下图黄色点所示:
freetype矢量字体 —— 介绍篇

再用数学曲线(如贝塞尔曲线),将关键点连接起来,得到一系列封闭曲线,如下图所示:
freetype矢量字体 —— 介绍篇

最后,填充封闭闭合区间,就显示出字母“A”。如下图所示:
freetype矢量字体 —— 介绍篇

矢量字体的优势在于:
由于关键点的相对位置不变,即时放大或缩小字体,只要选取的数学曲线平滑,最终显示出的字体就不会变形,也不会出现模糊和锯齿现象。

字体分类

矢量字体主要包括 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:WindowsFonts目录下。

如何在字体文件中,找到字符的关键点?

先确定该字符的编码值,如ASCII码(ANSI编码)、GB2312码(GBK编码)、UNICODE码等。如果字体文件支持该编码格式(charset),就能用编码值去找到该字符的关键点(glyph)。有些字体文件支持多种编码格式(charset),在文件中称为charmaps(复数形式,表示可能支持多种charset)。

以simsun.ttc字体为例,用Windows字体查看器打开文件:
freetype矢量字体 —— 介绍篇

将其用表格形式抽象起来:
freetype矢量字体 —— 介绍篇

该字体文件格式组成:头部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"及其外框示意图如下:
freetype矢量字体 —— 介绍篇

LCD坐标系与笛卡尔坐标系

LCD坐标系中,原点在屏幕左上角。笛卡尔坐标系中,原点在左下角。freetype使用笛卡尔坐标系,LCD显示内容时,使用LCD坐标系,因此当显示内容时,需要转换为LCD坐标系。
两种坐标系下,坐标系换算方式如下图所示:
freetype矢量字体 —— 介绍篇

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(关键点度量):
freetype矢量字体 —— 介绍篇
注:笛卡尔坐标系

显示一行文字时,改行文字会基于同一个基线来绘制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矢量字体 —— 介绍篇

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_Charn");
            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 errorn");
            return -1;
        }

        /* get glyph image */
        err = FT_Get_Glyph(face->glyph, &glyph);
        if (err) {
            printf("FT_Get_Glyph errorn");
            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

原文链接: https://www.cnblogs.com/fortunely/p/16651504.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    freetype矢量字体 —— 介绍篇

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402474

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:05
下一篇 2023年4月21日 上午11:05

相关推荐