【C++】字体文件解析(FreeType)

article/2025/8/30 14:16:55

目录

字体文件解析

一、前言

二、基本排版概念

1.字体文件

2.字符图像和字符表

3.字符和字体指标

三、字形轮廓

四、字形指标

1.基线、笔和布局

2.排版指标和边界框

3.方位与步进

4.网格拟合的效果

5.文本宽度与边界框

五、代码实现

六、使用实例

七、合并缓存优化


字体文件解析

一、前言

        要在应用里显示文本,一般有两个方案,其一是调用操作系统的接口;其二是解析字体文件获得字体图像,再间接显示。这里我们讨论第二种方案,这种方案的优点是不依赖操作系统,比较灵活。但缺点是,开发难度较高,管理字体图像需要开销。

        当然主流的字体解析库便是FreeType,官网如下:

The FreeType Project

        其中编译可以只依赖zlib,而不是有些人说的必须依赖pnglib。如果只考虑windows平台,它也提供了vs工程文件,可以方便的编译为动态链接库。

        从第二节到第四节是官网教程的少部分,可以简单浏览理解概念(如果需要更复杂的功能,可能需要仔细去官网阅读全文)。后面是代码实现。接下来开始吧!

二、基本排版概念

1.字体文件

        首先,在FreeType中的基本字体单位是Face,例如simkai.tff通过加载,变为一个FT_Face句柄。而广义上的字体可能是多个Face的组合,例如“Palatino”字体,包含“Palatino常规”、“Palatino斜体”两个不同的Face,它们是分离的文件。所以我们约定术语字体(font)为单个Face,而一套字体包含多个文件我们称为字体集合(font collection)

2.字符图像和字符表

        字符的图像称为字形(glyphs),而一个字符可以有多个字形。而一个字形,也可用于多个字符(不同的字符,写法可能一样)。

        我们可以只关注两个概念:一个字体文件包含多个字形,每一个都可以储存为位图、矢量、或其他任何方案。而我们通过字形索引访问。

        其二,字体文件包含一到多个表,称为字符表(character maps)。它可以将字符编码(ASCII、Unicode、GB2312、BIG5等)转为字形索引。(通过字形索引就能获取到字形,便可获得字符的图像)

3.字符和字体指标

        每个字形图像都含有各种指标(Metrics ,这些指标描述了在呈现文本时如何放置和管理。指标包含字形位置光标前进文本布局

        可拓展格式还包含全局指标,以字体单位,描述同一Face所有字形的属性。例如最大字形边框、字体的上升、下降和文本高度。

        不可拓展的格式,也包含一些指标。仅适用于一组给定字符的尺寸、分辨率。一般以像素为单位。

三、字形轮廓

        Freetype不是通过像素来存储字形,而是通过字符的形状,我们称之为轮廓(outlines)。它使用点为单位,以下公式计算转换到像素单位:

        pixel_size = point_size * resolution / 72

        其中resolution为分辨率,以dpi(每英寸点数)为单位。

        存储在文件内的数据称为主轮廓,以点作为单位。在转换为位图时,需要进行缩放,这个步骤需要进行网格拟合(grid-fitting)得到图像,它有几种不同的算法,不过我们简单理解一下概念即可。

四、字形指标

1.基线、笔和布局

        基线(baseline)是一条假象的线,比如作业本上的横线,使我们方便对齐位置。它可以是横的,也可以是竖的。而笔尖位于基线上的一点,用于定位字形。

        水平布局:字形在基线之上(有可能超过基线下方,比如字母q),通过向左或向右增加笔的位置来定位字形。

        两个连续笔尖位置(下图线上的小黑方块)的距离与字形有关,称为步进宽度(advance width)。它始终是正数,即使阿拉伯语是从右往左写的(我们排版的时候再进行处理)。

        另外笔的位置始终在基线上。

        而垂直布局基线在字形的中央,如下图所示:

2.排版指标和边界框

        为字体所有字形定义的各种Face指标:

  • Ascent:从基线到最高轮廓点的距离。正值,因为Y轴向上
  • Descent:从基线到最低轮廓点的距离。负值,但有些字体是正值。
  • Linegap:必须放置在两行文本之间的距离。

        两条基线之间的距离(标准行间距):linespace = ascent - descent + linegap

  • 边界框(bounding box):由xMin、yMin、xMax、yMax表示的包围盒,能够包含所有字形。简写为“bbox”。
  • Internal leading:用于传统排版,计算公式为:internal leading = ascent - descent - EM_size
  • External leading:与Linegap相同。

        (注意这里的大小均是字点单位,通过face可以访问,与我们设置的像素大小无关。要获得指定字体像素大小相关的数据,需要先调用FT_Set_Char_Size,然后再通过ft_face->size->metrics获得FT_Size_Metrics。不过最终我没有这么做,而是简单使用字体像素大小作为基本行间距)

3.方位与步进

        每个字形有属于自己的方位(bearing)步进(advance)。实际值和布局有关,水平和垂直布局是不同的值。

  • 左侧方位:笔尖到字形左侧bbox的水平距离。通常水平布局才存在。在FreeType中叫bearingX,简称“lsb”。
  • 顶侧方位:基线到字形bbox顶部的垂直距离。通常水平布局为正,垂直布局为负。在FreeType中叫bearingY
  • 步进宽度:渲染自身后,笔尖应该偏移的水平距离(从右向左则是减它)。垂直布局它始终为0。在FreeType中叫advanceX
  • 步进高度:渲染自身后,笔尖应该减少的垂直距离(它为正值,因为Y轴向上,而写字是向下)。水平布局为0。在FreeType中叫advanceY
  • 字形宽度:glyph width = bbox.xMax - bbox.xMin
  • 字形高度:glyph height = bbox.yMax - bbox.yMin
  • 右侧方位:步进到bbox右侧的距离,仅用于水平布局,一般为正值。缩写为“rsb”。

        大家可以仔细对照下图,以便写出正确的代码来实现预期的排版:

水平布局
垂直布局

4.网格拟合的效果

        网格拟合为了使字形的控制点与像素对齐,可能会修改调整字符图像的尺寸,从而影响字形指标。

5.文本宽度与边界框

        字形的对齐(origin)点即是笔尖在基线的位置。此对齐点通常不在字形的bbox上。而步进宽度与字形宽度也不是一回事。

        对于整个字符串来说:

  • 整个字符串包围盒不包含文本光标,并且它也不会在角上。
  • 字符串的步进宽度与包围盒无关。特别的是,前后存在空格、制表符。
  • 类似字距调整等附加处理,会使整体尺寸与单个字形指标无关。

五、代码实现

        包含头文件。如果需要获得字体描边图像,还需包含FT_STROKER

#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_STROKER_H

        定义封装类FreeType,其中FT_Library为基本句柄,FT_Face为单个文件的句柄:

class FreeType
{
public:private:FT_Library _library;vector<FT_Face> _vecFace;
};

         初始化与释放,使用FT_Init_FreeTypeFT_Done_FreeType,FT_Face通过FT_Done_Face释放(如果你不再使用某字体,则可以提前释放FT_Face):

FreeType()
{if (FT_Init_FreeType(&_library)){debug_err("FreeType初始化失败!");_library = nullptr;}
}~FreeType()
{for (auto& iter : _vecFace)FT_Done_Face(iter);FT_Done_FreeType(_library);
}

        加载字体文件,使用FT_New_Face,并保存FT_Face到容器,它所在的位置即是它的id:

//! 加载字体文件,返回编号,-1为失败
size_t LoadFace(string_view path_name)
{FT_Face face;if (FT_New_Face(_library, String::cvt_u8_mb(path_name).c_str(), 0, &face)){debug_err("字体文件加载失败: " + string{path_name});return -1;}//从内存加载//FT_New_Memory_Face(library, (FT_Byte*)buffer, size, 0, &face.face)_vecFace.push_back(face);return _vecFace.size() - 1;
}

        我们定义接口的两个类,一个输入CharInfo、一个输出CharImage,通过CharInfoHash可以定义哈希表,用于保存到CharSprite对应关系,避免重复生成精灵(不过我们这里暂时不需要用到CharInfoHash和CharSprite):

unordered_map<CharInfo, CharSprite, CharInfoHash> _hashCharImage;
//! 字符信息
struct CharInfo
{size_t _font;//字体idutf_char _ch;//我这里是char32_t,可以替换为wchar_tsize_t _size;//字体大小size_t _outline;//描边大小bool operator==(const CharInfo& b) const{return _font == b._font&& _ch == b._ch&& _size == b._size&& _outline == b._outline;}
};//! CharInfo的哈希函数
struct CharInfoHash
{//8 + 16 + 8 + 32size_t operator()(const CharInfo& info) const{uint64_t n = ((uint64_t)info._font << 56)| ((uint64_t)info._size << 40)| ((uint64_t)info._outline << 32)| ((uint64_t)info._ch);return std::hash<uint64_t>()(n);}
};//! 字符图像数据
struct CharImage
{Image* _image;Image* _imageOutline;Vector2 _pos;			//! 锚点float _advance;			//! 水平步进~CharImage();
};

        所以核心的封装函数即是GetChar,由于我们要取得字体描边图像,代码便会复杂许多(官方代码修改而来):

CharImage* GetChar(const CharInfo& info);

        首先检查传入参数,并给传入的参数取个简化名字:

//错误输出
auto fn_debug = [&](string_view str)
{debug_err(format("{}:{},{},{},{}",str, info._font, to_string(info._ch), info._size, info._outline));
};if (info._font >= _vecFace.size())
{fn_debug("字体id越界");return nullptr;
}const char32_t& ch = info._ch;
const size_t& size = info._size;
const size_t& outline = info._outline;

        设置编码表,通常设为FT_ENCODING_UNICODE,即unicode编码:

FT_Face ft_face = _vecFace[info._font];
if (FT_Select_Charmap(ft_face, FT_ENCODING_UNICODE))
{fn_debug("设置编码失败");return nullptr;
}

        设置字体大小,我们的size单位是像素,需要如下转换:

if (FT_Set_Char_Size(ft_face, FT_F26Dot6(size << 6), FT_F26Dot6(size << 6), 72, 72))
{fn_debug("设置字体大小失败");return nullptr;
}

        获取字形,标记FT_LOAD_NO_BITMAP设置不生成位图(后面我们再生成):

FT_UInt gindex = FT_Get_Char_Index(ft_face, ch);
if (FT_Load_Glyph(ft_face, gindex, FT_LOAD_NO_BITMAP))
{fn_debug("字形加载失败");return nullptr;
}

        获取字形属性是否支持描边,没有我们简单返回错误(这里可以自行改进,一般来说都支持):

if (ft_face->glyph->format != FT_GLYPH_FORMAT_OUTLINE)
{fn_debug("不支持描边");return nullptr;
}

        接下来比较麻烦,我们需要示例代码定义的4个东西,首先是Span类,表示水平连续且颜色相同的一段像素,其中xy是位置,w是宽度,而coverage是颜色。coverage的范围是[0, 255],表示透明度。最终我们需要生成一个白色的带透明通道的字符图像,进行染色便可实现不同颜色的字体:

//表示水平的一段连续数据
struct Span
{int _x;int _y;int _w;int _coverage;//为uint8_t透明度Span(){}Span(int x, int y, int w, int coverage): _x(x), _y(y), _w(w), _coverage(coverage){}
};

        还需要一个函数与回调,后面我们调用两次RenderSpans会生成两个Spans,一个是普通图像,一个是描边图像:

//渲染器回调,写入Span
static void RasterCallback(int y, int count, const FT_Span* spans, void* user)
{vector<Span>* sptr = (vector<Span>*)user;for (int i = 0; i < count; ++i)sptr->push_back(Span(spans[i].x, y, spans[i].len, spans[i].coverage));
}
// 设置光栅参数,且渲染描边
void RenderSpans(FT_Library& library, FT_Outline* outline, vector<Span>* spans)
{FT_Raster_Params params;memset(&params, 0, sizeof(params));params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;params.gray_spans = RasterCallback;params.user = spans;FT_Outline_Render(library, outline, &params);
}

        最后一个类是类似于Rect的概念,用于计算两个图像公用的包围盒:

struct CharRect
{float _xMin;float _xMax;float _yMin;float _yMax;CharRect() {}CharRect(float left, float top, float right, float bottom): _xMin(left), _xMax(right), _yMin(top), _yMax(bottom) {}void Include(int x, int y){_xMin = min(_xMin, float(x));_yMin = min(_yMin, float(y));_xMax = max(_xMax, float(x));_yMax = max(_yMax, float(y));}float Width() { return _xMax - _xMin + 1; }float Height() { return _yMax - _yMin + 1; }
};

        好了,终于可以回到我们的GetChar函数了,首先渲染到普通spans

//渲染到 spans
vector<Span> spans;
RenderSpans(_library, &ft_face->glyph->outline, &spans);

        然后设置画笔,并渲染到spans_outline,其中outline是描边的像素大小:

//接下来渲染到 spans_outline
vector<Span> spans_outline;//设置画笔
FT_Stroker stroker;
FT_Stroker_New(_library, &stroker);
FT_Stroker_Set(stroker,(int)(outline * 64),FT_STROKER_LINECAP_ROUND,FT_STROKER_LINEJOIN_ROUND,0);FT_Glyph glyph;
if (FT_Get_Glyph(ft_face->glyph, &glyph))
{fn_debug("获取字形失败");return nullptr;
}FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1);
if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
{//绘制outline到 outline_spansFT_Outline* o =&reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;RenderSpans(_library, o, &spans_outline);
}

        现在数据已经保存到spans中,可以清理资源了,然后我们检查一下spans是否为空,有些情况会返回空(例如空白字符):

//清理后面无需用到的资源
FT_Stroker_Done(stroker);
FT_Done_Glyph(glyph);if (spans.empty())
{fn_debug("spans为空(或许打印了控制字符)");return nullptr;
}

        然后如下计算包围盒:

//计算二者包围盒(描边更大)
CharRect rect(float(spans.front()._x),float(spans.front()._y),float(spans.front()._x),float(spans.front()._y));
for (Span& s : spans)
{rect.Include(s._x, s._y);rect.Include(s._x + s._w - 1, s._y);
}
for (Span& s : spans_outline)
{rect.Include(s._x, s._y);rect.Include(s._x + s._w - 1, s._y);
}

        然后复制数据到Image,首先以包围盒大小生成两个无色的图像(一个用于正常、一个用于描边),然后以白色+透明度写入对应的数据,如下所示:

//获得必要的属性
unsigned img_w = (unsigned)rect.Width();
unsigned img_h = (unsigned)rect.Height();//分配图像内存,以0颜色
Image* img_outline = Image::Create({ img_w ,img_h}, ColorDef::NONE);
Image* img = Image::Create({ img_w ,img_h }, ColorDef::NONE);//这里取得image buffer指针来赋值
uint32_t* p_lock = img_outline->GetData();//复制到img_outline
for (Span& s : spans_outline)
{for (int w = 0; w < s._w; ++w){size_t y = img_h - 1 - (s._y - rect._yMin);size_t index = y * img_w + s._x - rect._xMin + w;p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;}
}
//复制到img
p_lock = img->GetData();
for (Span& s : spans)
{for (int w = 0; w < s._w; ++w){size_t y = img_h - 1 - (s._y - rect._yMin);size_t index = y * img_w + s._x - rect._xMin + w;p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;}
}

        最后返回必要的数据,完成了最后的操作:

float bearingX = float(ft_face->glyph->metrics.horiBearingX >> 6);
float bearingY = float(ft_face->glyph->metrics.horiBearingY >> 6);
float advance = float(ft_face->glyph->advance.x >> 6);CharImage* ret = new CharImage;ret->_advance = advance;
ret->_pos[0] = -bearingX;
ret->_pos[1] = bearingY;
ret->_image = img;
ret->_imageOutline = img_outline;return ret;

        完整代码最后列出

六、使用实例

        首先加载字体,然后调用GetChar返回指定大小的字符图像:

g_factory->LoadFont("font/syht.otf");CharImage* ci = g_factory->GetChar({0, U'中', 72, 2});
ci->_image->SaveToFile("temp/ch.png");
ci->_imageOutline->SaveToFile("temp/ch_outline.png");

        为了方便观察,我在ps内加以黑色背景,并放置在一起对比:

七、合并缓存优化

        当然,我们需要反复的使用同一个字符图像,实际工程我们需要进行缓存并且合并到一张纹理上。

        我使用以下动态装箱算法,源码文件为DND.TexturePack.ixx,其中还有一个静态装箱算法,可以用于已知所有图片合并为一张大图。而我们这里使用动态装箱,当取得一个字符图像时,就放入合适的位置,当然不能提前知道所有的字符:

//! 动态装箱(DH为2时,32、31、30会放到同一行)
template<unsigned DH>
class Dynamic
{
public://! 按高度存储 每一行struct Line{unsigned _y;//! 所在yunsigned _h;//! 最大高度unsigned _x;//! 当前xbool _free;//! 是否有空位标记};/*** @brief 清空初始化 或 再次使用* @param[in] size 箱子大小*/void Reset(const Size& size){_size = size;_regY = 0;_bFull = false;_vecLine.clear();}/*** @brief 添加一个,失败返回false* @param[in] size 装入大小* @param[out] rect 成功则返回位置*/bool Add(const Size& size, RectU& rect){if (_bFull)return false;unsigned w = size[0];unsigned h = size[1];//找到一个h比自己大的,但又不能超过DH(然后还能放得下x)auto iter = find_if(_vecLine.begin(), _vecLine.end(), [&](Line& line){if (line._free&& line._h >= h // 30 >= 28&& line._h <= h + DH) // 30 <= 28 + 2   {28,29,30}{if (line._x + w > _size[0]){line._free = false;return false;}elsereturn true;}return false;});if (iter == _vecLine.end()){//没有就创建一个if (h + _regY > _size[1]){_bFull = true;return false;}Line line;line._h = h;line._y = _regY;line._x = 0;line._free = true;_regY += h;_vecLine.push_back(line);iter = _vecLine.end() - 1;}rect = { iter->_x, iter->_y, iter->_x + w , iter->_y + h };iter->_x += w;//越界检查assert(rect[2] <= _size[0] && rect[3] <= _size[1]);return true;}private:Size _size;unsigned _regY;//当前ybool _bFull;//满了标记vector<Line> _vecLine;
};

        最后会产生如下纹理(我在ps内添加了黑色背景方便观察):

         从FreeType取得字符图像后,再写入到纹理,我们便能显示文本了。对文本进行排版是另外一件事,这里我就不详细说明了,也比较麻烦,可以参考我的源码DND.Text.ixx,最后效果如下:

        其中DND.FactoryImp.ixx有以下成员,思路即是用到哪个字符,就生成字符图像,然后reg_image注册到纹理,生成uv,成为精灵。通过Text类管理多个字符精灵,进行布局:

//字符精灵
struct CharSprite
{struct{size_t _idTex;RectU _rect;//在大图区域Vector2 _uv[4];//计算出的uv}_data[2];//非描边 和 描边Vector2 _anchor;//锚点(相对于基线)float _advance;//步进
};
//CharInfo -> CharSprite
unordered_map<CharInfo, CharSprite, CharInfoHash> _hashCharImage;using TexPack = TexturePack::Dynamic<2>;
//动态纹理使用区域
vector<TexPack> _allTexPack;//注册一个图像(内部使用)
//返回tex_id和纹理区域,不存在rect_id
//绘制之前批量提交
bool _reg_image(Image* image, size_t& id_tex, RectU& rect);

        源码位置:DND: 应用程序框架。

        觉得有用,点赞、收藏、关注一下吧。 

        与FreeType相关的完整代码:

/**
* @file		DND.FreeType.ixx
* @brief	基于Freetype2的字体解析
*
*
* @version	1.0
* @author	lveyou
* @date		22-09-10
*
*/
module;
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_STROKER_H
export module DND.FreeType;import DND.Std;
import DND.Debug;
import DND.Color;
import DND.CodeCvt;
export import DND.Font;export namespace dnd
{ class FreeType
{
public://表示水平的一段连续数据struct Span{int _x;int _y;int _w;int _coverage;//为uint8_t透明度Span(){}Span(int x, int y, int w, int coverage): _x(x), _y(y), _w(w), _coverage(coverage){}};//渲染器回调,写入Spanstatic void RasterCallback(int y, int count, const FT_Span* spans, void* user){vector<Span>* sptr = (vector<Span>*)user;for (int i = 0; i < count; ++i)sptr->push_back(Span(spans[i].x, y, spans[i].len, spans[i].coverage));}// 设置光栅参数,且渲染描边void RenderSpans(FT_Library& library, FT_Outline* outline, vector<Span>* spans){FT_Raster_Params params;memset(&params, 0, sizeof(params));params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;params.gray_spans = RasterCallback;params.user = spans;FT_Outline_Render(library, outline, &params);}struct CharRect{float _xMin;float _xMax;float _yMin;float _yMax;CharRect() {}CharRect(float left, float top, float right, float bottom): _xMin(left), _xMax(right), _yMin(top), _yMax(bottom) {}void Include(int x, int y){_xMin = min(_xMin, float(x));_yMin = min(_yMin, float(y));_xMax = max(_xMax, float(x));_yMax = max(_yMax, float(y));}float Width() { return _xMax - _xMin + 1; }float Height() { return _yMax - _yMin + 1; }};FreeType(){if (FT_Init_FreeType(&_library)){debug_err("FreeType初始化失败!");_library = nullptr;}}//! 加载字体文件,返回编号,-1为失败size_t LoadFace(string_view path_name){FT_Face face;if (FT_New_Face(_library, CodeCvt::cvt_u8_mb(path_name).c_str(), 0, &face)){debug_err("字体文件加载失败: " + string{path_name});return -1;}assert(FT_IS_SCALABLE(face));//从内存加载//FT_New_Memory_Face(library, (FT_Byte*)buffer, size, 0, &face.face)//打印一些属性debug_msg(format("成功加载一个字体:{},{}", _vecFace.size(), path_name));debug(format("名称:{}", face->family_name));debug(format("字形数:{}", face->num_glyphs));//debug(format("包围盒:({}, {}),({}, {})",//	face->bbox.xMin, face->bbox.xMax,//	face->bbox.yMin, face->bbox.yMax));_vecFace.push_back(face);return _vecFace.size() - 1;}CharImage* GetChar(const CharInfo& info){//错误输出auto fn_debug = [&](string_view str){debug_err(format("{}:{},{},{},{}",str, info._font, to_string(info._ch), info._size, info._outline));};if (info._font >= _vecFace.size()){fn_debug("字体id越界");return nullptr;}const char32_t& ch = info._ch;const size_t& size = info._size;const size_t& outline = info._outline;FT_Face ft_face = _vecFace[info._font];if (FT_Select_Charmap(ft_face, FT_ENCODING_UNICODE)){fn_debug("设置编码失败");return nullptr;}if (FT_Set_Char_Size(ft_face, FT_F26Dot6(size << 6), FT_F26Dot6(size << 6), 72, 72)){fn_debug("设置字体大小失败");return nullptr;}FT_UInt gindex = FT_Get_Char_Index(ft_face, ch);if (FT_Load_Glyph(ft_face, gindex, FT_LOAD_NO_BITMAP)){fn_debug("字形加载失败");return nullptr;}if (ft_face->glyph->format != FT_GLYPH_FORMAT_OUTLINE){fn_debug("不支持描边");return nullptr;}//渲染到 spansvector<Span> spans;RenderSpans(_library, &ft_face->glyph->outline, &spans);//接下来渲染到 spans_outlinevector<Span> spans_outline;//设置画笔FT_Stroker stroker;FT_Stroker_New(_library, &stroker);FT_Stroker_Set(stroker,(int)(outline * 64),FT_STROKER_LINECAP_ROUND,FT_STROKER_LINEJOIN_ROUND,0);FT_Glyph glyph;if (FT_Get_Glyph(ft_face->glyph, &glyph)){fn_debug("获取字形失败");return nullptr;}FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1);if (glyph->format == FT_GLYPH_FORMAT_OUTLINE){//绘制outline到 outline_spansFT_Outline* o =&reinterpret_cast<FT_OutlineGlyph>(glyph)->outline;RenderSpans(_library, o, &spans_outline);}//清理后面无需用到的资源FT_Stroker_Done(stroker);FT_Done_Glyph(glyph);if (spans.empty()){fn_debug("spans为空(或许打印了控制字符)");return nullptr;}//计算二者包围盒(描边更大)CharRect rect(float(spans.front()._x),float(spans.front()._y),float(spans.front()._x),float(spans.front()._y));for (Span& s : spans){rect.Include(s._x, s._y);rect.Include(s._x + s._w - 1, s._y);}for (Span& s : spans_outline){rect.Include(s._x, s._y);rect.Include(s._x + s._w - 1, s._y);}//获得必要的属性unsigned img_w = (unsigned)rect.Width();unsigned img_h = (unsigned)rect.Height();//分配图像内存,以0颜色Image* img_outline = Image::Create({ img_w ,img_h}, ColorDef::NONE);Image* img = Image::Create({ img_w ,img_h }, ColorDef::NONE);//这里取得image buffer指针来赋值uint32_t* p_lock = img_outline->GetData();//复制到img_outlinefor (Span& s : spans_outline){for (int w = 0; w < s._w; ++w){size_t y = img_h - 1 - (s._y - rect._yMin);size_t index = y * img_w + s._x - rect._xMin + w;p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;}}//复制到imgp_lock = img->GetData();for (Span& s : spans){for (int w = 0; w < s._w; ++w){size_t y = img_h - 1 - (s._y - rect._yMin);size_t index = y * img_w + s._x - rect._xMin + w;p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;}}float bearingX = float(ft_face->glyph->metrics.horiBearingX >> 6);float bearingY = float(ft_face->glyph->metrics.horiBearingY >> 6);float advance = float(ft_face->glyph->advance.x >> 6);CharImage* ret = new CharImage;ret->_advance = advance;ret->_bearingX = bearingX;ret->_bearingY = bearingY;ret->_image = img;ret->_imageOutline = img_outline;return ret;}size_t GetFontSize(){return _vecFace.size();}~FreeType(){for (auto& iter : _vecFace)FT_Done_Face(iter);FT_Done_FreeType(_library);}
private:FT_Library _library;vector<FT_Face> _vecFace;
};FreeType* g_freetype;}


http://chatgpt.dhexx.cn/article/GBGy6DY7.shtml

相关文章

FreeType 用法

Freetype是一个跨平台、开源的字体渲染器&#xff0c;网上很多文章介绍&#xff0c;本人就不啰嗦了。本文重点在于实现文章标题所属的各种效果&#xff0c;不是Freetype的基本使用方法介绍文档&#xff0c;所以对于Freetype不熟悉的同学们请先学习下Freetype的基本用法&#xf…

freetype 使用解析---矢量字体

屏幕显示字体(字体点阵)&#xff1a; 在内核中有对应的文件fontdata_8x16.c&#xff0c;将字体通过数组8*16来描述&#xff0c;0表示为空&#xff0c;1表示描点&#xff0c;通过通过各个点形成一个字体点阵显示在屏幕上&#xff0c;而在我们所有终端中显示的字体也可以是通过点…

freetype简介与测试

目录 一、简要 二、文字显示过程 三、freetype中的step1 四、在PC上测试freetype 4.1 分析main函数 4.2 修改main函数 4.3 在PC上测试 4.4 得到CBox 一、简要 从点阵文件中把字母或者汉字的字模取出来在LCD上显示这个方式有个缺点&#xff0c;这个文字的大小就固定了不…

Freetype 的安装与使用

4 交叉编译程序 4.1 程序运行的一些基础知识 编译程序时去哪找头文件&#xff1f; 系统目录&#xff1a;就是交叉编译工具链里的某个 include 目录&#xff1b; 也可以自己指定&#xff1a;编译时用 “ -I dir ” 选项指定。 链接时去哪找库文件&#xff1f; 系统目录&#x…

java和web哪个难_web前端和java哪个难学?哪个简单?

说到java很多都人都熟知&#xff0c;但是说到web前端可能是很多人并不知道&#xff0c;随着最近几年的发展&#xff0c;web前端开发人越来越吃香&#xff0c;无论是薪资待遇还是岗位的招聘数量比java差不了多少&#xff0c;最近有同学在咨询web前端和java哪个难学这个问题&…

js和java那个难_javascript与java哪个难?

javascript与java哪个难&#xff1f;答案是&#xff1a;JavaScript比Java更难。那么这是为什么&#xff1f;下面本篇文章就来给大家介绍一下&#xff0c;希望对大家有所帮助。 原因&#xff1a; JavaScript有太多东西需要你自己去理解&#xff0c;这些东西里有很多要么Java已经…

没学历学java很难找工作吗

说实话不是学Java很难找工作&#xff0c;是以后找工作基本都会看学历&#xff0c;学历这个东西说重要也重要&#xff0c;因为它决定了你的简历能不能呈现到面试官面前&#xff0c;如果你的学历不够的话很有可能在第一轮简历就被刷下去了&#xff0c;它根本就不会呈现在面试官面…

java最难的部分_java最难学的是那一块?

原标题&#xff1a;java最难学的是那一块&#xff1f; 对于那些打算通过学习java来找到一份工作的同学来说&#xff0c;可以分为三个学习阶段&#xff0c;初级&#xff0c;中级&#xff0c;高级。不同的学习阶段有不同的难点&#xff0c;下来我就简单的和大家仔细的聊聊&#x…

学习Java开发难不难?好学吗?

学习Java难不难&#xff1f;这是很多希望学习Java的人比较纠结的问题。实际上&#xff0c;Java语言是非常易学的&#xff0c;Java语言机遇&#xff23;语言&#xff0c;却又高于&#xff23;语言。Java语言简单易学的特性使得大多数程序员很容易学习和使用Java。 Java是个简单…

零基础Java难学吗?自学怎么样?

在零基础上学习Java难吗?自学呢?要回答这个问题,我们应该从多方面来回答。首先,谁更适合学习Java?   如果仅仅从兴趣上说那么人人都可以胜任,那就像姜子牙70多年的探险生涯。47岁的刘邦在沛县召集民众响应陈胜武广起义。古代的年龄相当于我们现在的六十岁。齐白石,一位…

java学习路线,一个初中生学java要多久,java难学吗

一门永不过时的编程语言——Java 软件开发 java难学吗 java不难&#xff0c;你可以做如下学习 一、到相应的培训机构付费学习 别在这说培训机构没用什么的&#xff0c;不过一定要找正规的培训机构&#xff0c;不然容易被坑。培训机构里面的课程都是现在工作中需要用到的&am…

学Java难吗

Java这门语言你要是单独拎出来&#xff0c;那还是挺复杂的&#xff0c;但你要是把它放到编程里面&#xff0c;那算不上难&#xff0c;因为编程这门行业本来就难&#xff0c;而且&#xff0c;现在大学计算机专业的学生&#xff0c;大学期间&#xff0c;会设置Java这门课程&#…

c语言难还是java难_C语言真的比Java难学吗?

原标题&#xff1a; C语言真的比Java难学吗&#xff1f; 千锋小编觉得C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。C语言语法比较简单便捷&#xff0c;而且性能快速&#xff0c;只比汇编语言…

学习Java很难吗

没有简单的语言,编程语言各有各的优势,同时也各有各的难点,不过这些难点也可以是他们的特点。就像C++难的是语言的细节,Java难的是各种库,各种函数的调用,它的基本语法很简单,但只靠基础是做不出什么东西的,必须去熟悉各种轮子才能进行开发。 第一、 Java入门很简单…

Java难学吗

学习Java说难其实也不难&#xff01; 毕竟世上无难事&#xff0c;只怕有心人。 Java是编程语言中比较难学的一门语言&#xff0c;它的难度并不低&#xff0c;相对比于C语言来说&#xff0c;Java的学习难度要小一些。 Java的学习中最难得就是&#xff0c;各种各样的框架&#x…

java学起来难不难?

同学们问的最多的一个关于Java的问题就是java学起来到底难不难&#xff1f;java好不好学&#xff1f;小千综合过去毕业学员给大家介绍一下学Java到底难不难。 java语言 单单来说学Java这件事的话&#xff0c;小千认为是不难的&#xff0c;实际上Java经常会用到的代码结构无非就…

关于 高内聚 和 低耦合 的理解

随着一个程序逻辑越来越多&#xff0c;就难免需要微服务来保证程序的高可用性。一个服务宕机或者出问题了&#xff0c;不影响其他的服务。 只要有微服务的存在&#xff0c;两个服务之间有许多代码需要共用。 高内聚低耦合是一种程序设计的思想&#xff0c;高内聚的本质也就抽…

【架构基础】高内聚低耦合

软件设计目标&#xff1a;实现需求、易于重用、易于理解、没有冗余。 Dont reinvent the wheel, just realign it. --Anthony J D’ Angelo 高内聚低耦合&#xff0c;是软件工程中判断软件设计好坏的标准。主要评判模块或类的内聚性是否高&#xff0c;耦合度是否低。目的是使…

神秘的高内聚与低耦合举例

内聚类型&#xff1a; 1.巧合内聚 / 偶然内聚 模块的各成分之间没有关联&#xff0c;只是把分散的功能合并在一起。 例&#xff1a;A模块中有三条语句&#xff08;一条赋值&#xff0c;一条求和&#xff0c;一条传参&#xff09;&#xff0c;表面上看不出任何联系&#xff0c;但…

高内聚,低耦合的实现方式

高内聚低耦合&#xff0c;是软件工程中的概念&#xff0c;是判断软件设计好坏的标准&#xff0c;主要用于程序的面向对象的设计&#xff0c;主要看类的内聚性是否高&#xff0c;耦合度是否低。 目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越…