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


    目录

    字体文件解析

    一、前言

    二、基本排版概念

    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

    1. #include
    2. #include FT_FREETYPE_H
    3. #include FT_STROKER_H

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

    1. class FreeType
    2. {
    3. public:
    4. private:
    5. FT_Library _library;
    6. vector _vecFace;
    7. };

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

    1. FreeType()
    2. {
    3. if (FT_Init_FreeType(&_library))
    4. {
    5. debug_err("FreeType初始化失败!");
    6. _library = nullptr;
    7. }
    8. }
    9. ~FreeType()
    10. {
    11. for (auto& iter : _vecFace)
    12. FT_Done_Face(iter);
    13. FT_Done_FreeType(_library);
    14. }

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

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

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

    unordered_map _hashCharImage;
    1. //! 字符信息
    2. struct CharInfo
    3. {
    4. size_t _font;//字体id
    5. utf_char _ch;//我这里是char32_t,可以替换为wchar_t
    6. size_t _size;//字体大小
    7. size_t _outline;//描边大小
    8. bool operator==(const CharInfo& b) const
    9. {
    10. return _font == b._font
    11. && _ch == b._ch
    12. && _size == b._size
    13. && _outline == b._outline;
    14. }
    15. };
    16. //! CharInfo的哈希函数
    17. struct CharInfoHash
    18. {
    19. //8 + 16 + 8 + 32
    20. size_t operator()(const CharInfo& info) const
    21. {
    22. uint64_t n = ((uint64_t)info._font << 56)
    23. | ((uint64_t)info._size << 40)
    24. | ((uint64_t)info._outline << 32)
    25. | ((uint64_t)info._ch);
    26. return std::hash<uint64_t>()(n);
    27. }
    28. };
    29. //! 字符图像数据
    30. struct CharImage
    31. {
    32. Image* _image;
    33. Image* _imageOutline;
    34. Vector2 _pos; //! 锚点
    35. float _advance; //! 水平步进
    36. ~CharImage();
    37. };

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

    CharImage* GetChar(const CharInfo& info);

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

    1. //错误输出
    2. auto fn_debug = [&](string_view str)
    3. {
    4. debug_err(format("{}:{},{},{},{}",
    5. str, info._font, to_string(info._ch), info._size, info._outline));
    6. };
    7. if (info._font >= _vecFace.size())
    8. {
    9. fn_debug("字体id越界");
    10. return nullptr;
    11. }
    12. const char32_t& ch = info._ch;
    13. const size_t& size = info._size;
    14. const size_t& outline = info._outline;

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

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

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

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

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

    1. FT_UInt gindex = FT_Get_Char_Index(ft_face, ch);
    2. if (FT_Load_Glyph(ft_face, gindex, FT_LOAD_NO_BITMAP))
    3. {
    4. fn_debug("字形加载失败");
    5. return nullptr;
    6. }

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

    1. if (ft_face->glyph->format != FT_GLYPH_FORMAT_OUTLINE)
    2. {
    3. fn_debug("不支持描边");
    4. return nullptr;
    5. }

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

    1. //表示水平的一段连续数据
    2. struct Span
    3. {
    4. int _x;
    5. int _y;
    6. int _w;
    7. int _coverage;//为uint8_t透明度
    8. Span(){}
    9. Span(int x, int y, int w, int coverage):
    10. _x(x), _y(y), _w(w), _coverage(coverage)
    11. {}
    12. };

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

    1. //渲染器回调,写入Span
    2. static void RasterCallback(int y, int count, const FT_Span* spans, void* user)
    3. {
    4. vector* sptr = (vector*)user;
    5. for (int i = 0; i < count; ++i)
    6. sptr->push_back(Span(spans[i].x, y, spans[i].len, spans[i].coverage));
    7. }
    8. // 设置光栅参数,且渲染描边
    9. void RenderSpans(FT_Library& library, FT_Outline* outline, vector* spans)
    10. {
    11. FT_Raster_Params params;
    12. memset(¶ms, 0, sizeof(params));
    13. params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;
    14. params.gray_spans = RasterCallback;
    15. params.user = spans;
    16. FT_Outline_Render(library, outline, ¶ms);
    17. }

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

    1. struct CharRect
    2. {
    3. float _xMin;
    4. float _xMax;
    5. float _yMin;
    6. float _yMax;
    7. CharRect() {}
    8. CharRect(float left, float top, float right, float bottom):
    9. _xMin(left), _xMax(right), _yMin(top), _yMax(bottom)
    10. {}
    11. void Include(int x, int y)
    12. {
    13. _xMin = min(_xMin, float(x));
    14. _yMin = min(_yMin, float(y));
    15. _xMax = max(_xMax, float(x));
    16. _yMax = max(_yMax, float(y));
    17. }
    18. float Width() { return _xMax - _xMin + 1; }
    19. float Height() { return _yMax - _yMin + 1; }
    20. };

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

    1. //渲染到 spans
    2. vector spans;
    3. RenderSpans(_library, &ft_face->glyph->outline, &spans);

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

    1. //接下来渲染到 spans_outline
    2. vector spans_outline;
    3. //设置画笔
    4. FT_Stroker stroker;
    5. FT_Stroker_New(_library, &stroker);
    6. FT_Stroker_Set(stroker,
    7. (int)(outline * 64),
    8. FT_STROKER_LINECAP_ROUND,
    9. FT_STROKER_LINEJOIN_ROUND,
    10. 0);
    11. FT_Glyph glyph;
    12. if (FT_Get_Glyph(ft_face->glyph, &glyph))
    13. {
    14. fn_debug("获取字形失败");
    15. return nullptr;
    16. }
    17. FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1);
    18. if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
    19. {
    20. //绘制outline到 outline_spans
    21. FT_Outline* o =
    22. &reinterpret_cast(glyph)->outline;
    23. RenderSpans(_library, o, &spans_outline);
    24. }

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

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

            然后如下计算包围盒:

    1. //计算二者包围盒(描边更大)
    2. CharRect rect(
    3. float(spans.front()._x),
    4. float(spans.front()._y),
    5. float(spans.front()._x),
    6. float(spans.front()._y));
    7. for (Span& s : spans)
    8. {
    9. rect.Include(s._x, s._y);
    10. rect.Include(s._x + s._w - 1, s._y);
    11. }
    12. for (Span& s : spans_outline)
    13. {
    14. rect.Include(s._x, s._y);
    15. rect.Include(s._x + s._w - 1, s._y);
    16. }

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

    1. //获得必要的属性
    2. unsigned img_w = (unsigned)rect.Width();
    3. unsigned img_h = (unsigned)rect.Height();
    4. //分配图像内存,以0颜色
    5. Image* img_outline = Image::Create({ img_w ,img_h}, ColorDef::NONE);
    6. Image* img = Image::Create({ img_w ,img_h }, ColorDef::NONE);
    7. //这里取得image buffer指针来赋值
    8. uint32_t* p_lock = img_outline->GetData();
    9. //复制到img_outline
    10. for (Span& s : spans_outline)
    11. {
    12. for (int w = 0; w < s._w; ++w)
    13. {
    14. size_t y = img_h - 1 - (s._y - rect._yMin);
    15. size_t index = y * img_w + s._x - rect._xMin + w;
    16. p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;
    17. }
    18. }
    19. //复制到img
    20. p_lock = img->GetData();
    21. for (Span& s : spans)
    22. {
    23. for (int w = 0; w < s._w; ++w)
    24. {
    25. size_t y = img_h - 1 - (s._y - rect._yMin);
    26. size_t index = y * img_w + s._x - rect._xMin + w;
    27. p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;
    28. }
    29. }

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

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

            完整代码最后列出

    六、使用实例

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

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

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

    七、合并缓存优化

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

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

    1. //! 动态装箱(DH为2时,32、31、30会放到同一行)
    2. template<unsigned DH>
    3. class Dynamic
    4. {
    5. public:
    6. //! 按高度存储 每一行
    7. struct Line
    8. {
    9. unsigned _y;//! 所在y
    10. unsigned _h;//! 最大高度
    11. unsigned _x;//! 当前x
    12. bool _free;//! 是否有空位标记
    13. };
    14. /**
    15. * @brief 清空初始化 或 再次使用
    16. * @param[in] size 箱子大小
    17. */
    18. void Reset(const Size& size)
    19. {
    20. _size = size;
    21. _regY = 0;
    22. _bFull = false;
    23. _vecLine.clear();
    24. }
    25. /**
    26. * @brief 添加一个,失败返回false
    27. * @param[in] size 装入大小
    28. * @param[out] rect 成功则返回位置
    29. */
    30. bool Add(const Size& size, RectU& rect)
    31. {
    32. if (_bFull)
    33. return false;
    34. unsigned w = size[0];
    35. unsigned h = size[1];
    36. //找到一个h比自己大的,但又不能超过DH(然后还能放得下x)
    37. auto iter = find_if(_vecLine.begin(), _vecLine.end(), [&](Line& line)
    38. {
    39. if (line._free
    40. && line._h >= h // 30 >= 28
    41. && line._h <= h + DH) // 30 <= 28 + 2 {28,29,30}
    42. {
    43. if (line._x + w > _size[0])
    44. {
    45. line._free = false;
    46. return false;
    47. }
    48. else
    49. return true;
    50. }
    51. return false;
    52. });
    53. if (iter == _vecLine.end())
    54. {//没有就创建一个
    55. if (h + _regY > _size[1])
    56. {
    57. _bFull = true;
    58. return false;
    59. }
    60. Line line;
    61. line._h = h;
    62. line._y = _regY;
    63. line._x = 0;
    64. line._free = true;
    65. _regY += h;
    66. _vecLine.push_back(line);
    67. iter = _vecLine.end() - 1;
    68. }
    69. rect = { iter->_x, iter->_y, iter->_x + w , iter->_y + h };
    70. iter->_x += w;
    71. //越界检查
    72. assert(rect[2] <= _size[0] && rect[3] <= _size[1]);
    73. return true;
    74. }
    75. private:
    76. Size _size;
    77. unsigned _regY;//当前y
    78. bool _bFull;//满了标记
    79. vector _vecLine;
    80. };

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

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

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

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

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

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

            与FreeType相关的完整代码:

    1. /**
    2. * @file DND.FreeType.ixx
    3. * @brief 基于Freetype2的字体解析
    4. *
    5. *
    6. * @version 1.0
    7. * @author lveyou
    8. * @date 22-09-10
    9. *
    10. */
    11. module;
    12. #include
    13. #include FT_FREETYPE_H
    14. #include FT_STROKER_H
    15. export module DND.FreeType;
    16. import DND.Std;
    17. import DND.Debug;
    18. import DND.Color;
    19. import DND.CodeCvt;
    20. export import DND.Font;
    21. export namespace dnd
    22. {
    23. class FreeType
    24. {
    25. public:
    26. //表示水平的一段连续数据
    27. struct Span
    28. {
    29. int _x;
    30. int _y;
    31. int _w;
    32. int _coverage;//为uint8_t透明度
    33. Span(){}
    34. Span(int x, int y, int w, int coverage):
    35. _x(x), _y(y), _w(w), _coverage(coverage)
    36. {}
    37. };
    38. //渲染器回调,写入Span
    39. static void RasterCallback(int y, int count, const FT_Span* spans, void* user)
    40. {
    41. vector* sptr = (vector*)user;
    42. for (int i = 0; i < count; ++i)
    43. sptr->push_back(Span(spans[i].x, y, spans[i].len, spans[i].coverage));
    44. }
    45. // 设置光栅参数,且渲染描边
    46. void RenderSpans(FT_Library& library, FT_Outline* outline, vector* spans)
    47. {
    48. FT_Raster_Params params;
    49. memset(¶ms, 0, sizeof(params));
    50. params.flags = FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT;
    51. params.gray_spans = RasterCallback;
    52. params.user = spans;
    53. FT_Outline_Render(library, outline, ¶ms);
    54. }
    55. struct CharRect
    56. {
    57. float _xMin;
    58. float _xMax;
    59. float _yMin;
    60. float _yMax;
    61. CharRect() {}
    62. CharRect(float left, float top, float right, float bottom):
    63. _xMin(left), _xMax(right), _yMin(top), _yMax(bottom)
    64. {}
    65. void Include(int x, int y)
    66. {
    67. _xMin = min(_xMin, float(x));
    68. _yMin = min(_yMin, float(y));
    69. _xMax = max(_xMax, float(x));
    70. _yMax = max(_yMax, float(y));
    71. }
    72. float Width() { return _xMax - _xMin + 1; }
    73. float Height() { return _yMax - _yMin + 1; }
    74. };
    75. FreeType()
    76. {
    77. if (FT_Init_FreeType(&_library))
    78. {
    79. debug_err("FreeType初始化失败!");
    80. _library = nullptr;
    81. }
    82. }
    83. //! 加载字体文件,返回编号,-1为失败
    84. size_t LoadFace(string_view path_name)
    85. {
    86. FT_Face face;
    87. if (FT_New_Face(_library, CodeCvt::cvt_u8_mb(path_name).c_str(), 0, &face))
    88. {
    89. debug_err("字体文件加载失败: " + string{path_name});
    90. return -1;
    91. }
    92. assert(FT_IS_SCALABLE(face));
    93. //从内存加载
    94. //FT_New_Memory_Face(library, (FT_Byte*)buffer, size, 0, &face.face)
    95. //打印一些属性
    96. debug_msg(format("成功加载一个字体:{},{}", _vecFace.size(), path_name));
    97. debug(format("名称:{}", face->family_name));
    98. debug(format("字形数:{}", face->num_glyphs));
    99. //debug(format("包围盒:({}, {}),({}, {})",
    100. // face->bbox.xMin, face->bbox.xMax,
    101. // face->bbox.yMin, face->bbox.yMax));
    102. _vecFace.push_back(face);
    103. return _vecFace.size() - 1;
    104. }
    105. CharImage* GetChar(const CharInfo& info)
    106. {
    107. //错误输出
    108. auto fn_debug = [&](string_view str)
    109. {
    110. debug_err(format("{}:{},{},{},{}",
    111. str, info._font, to_string(info._ch), info._size, info._outline));
    112. };
    113. if (info._font >= _vecFace.size())
    114. {
    115. fn_debug("字体id越界");
    116. return nullptr;
    117. }
    118. const char32_t& ch = info._ch;
    119. const size_t& size = info._size;
    120. const size_t& outline = info._outline;
    121. FT_Face ft_face = _vecFace[info._font];
    122. if (FT_Select_Charmap(ft_face, FT_ENCODING_UNICODE))
    123. {
    124. fn_debug("设置编码失败");
    125. return nullptr;
    126. }
    127. if (FT_Set_Char_Size(ft_face, FT_F26Dot6(size << 6), FT_F26Dot6(size << 6), 72, 72))
    128. {
    129. fn_debug("设置字体大小失败");
    130. return nullptr;
    131. }
    132. FT_UInt gindex = FT_Get_Char_Index(ft_face, ch);
    133. if (FT_Load_Glyph(ft_face, gindex, FT_LOAD_NO_BITMAP))
    134. {
    135. fn_debug("字形加载失败");
    136. return nullptr;
    137. }
    138. if (ft_face->glyph->format != FT_GLYPH_FORMAT_OUTLINE)
    139. {
    140. fn_debug("不支持描边");
    141. return nullptr;
    142. }
    143. //渲染到 spans
    144. vector spans;
    145. RenderSpans(_library, &ft_face->glyph->outline, &spans);
    146. //接下来渲染到 spans_outline
    147. vector spans_outline;
    148. //设置画笔
    149. FT_Stroker stroker;
    150. FT_Stroker_New(_library, &stroker);
    151. FT_Stroker_Set(stroker,
    152. (int)(outline * 64),
    153. FT_STROKER_LINECAP_ROUND,
    154. FT_STROKER_LINEJOIN_ROUND,
    155. 0);
    156. FT_Glyph glyph;
    157. if (FT_Get_Glyph(ft_face->glyph, &glyph))
    158. {
    159. fn_debug("获取字形失败");
    160. return nullptr;
    161. }
    162. FT_Glyph_StrokeBorder(&glyph, stroker, 0, 1);
    163. if (glyph->format == FT_GLYPH_FORMAT_OUTLINE)
    164. {
    165. //绘制outline到 outline_spans
    166. FT_Outline* o =
    167. &reinterpret_cast(glyph)->outline;
    168. RenderSpans(_library, o, &spans_outline);
    169. }
    170. //清理后面无需用到的资源
    171. FT_Stroker_Done(stroker);
    172. FT_Done_Glyph(glyph);
    173. if (spans.empty())
    174. {
    175. fn_debug("spans为空(或许打印了控制字符)");
    176. return nullptr;
    177. }
    178. //计算二者包围盒(描边更大)
    179. CharRect rect(
    180. float(spans.front()._x),
    181. float(spans.front()._y),
    182. float(spans.front()._x),
    183. float(spans.front()._y));
    184. for (Span& s : spans)
    185. {
    186. rect.Include(s._x, s._y);
    187. rect.Include(s._x + s._w - 1, s._y);
    188. }
    189. for (Span& s : spans_outline)
    190. {
    191. rect.Include(s._x, s._y);
    192. rect.Include(s._x + s._w - 1, s._y);
    193. }
    194. //获得必要的属性
    195. unsigned img_w = (unsigned)rect.Width();
    196. unsigned img_h = (unsigned)rect.Height();
    197. //分配图像内存,以0颜色
    198. Image* img_outline = Image::Create({ img_w ,img_h}, ColorDef::NONE);
    199. Image* img = Image::Create({ img_w ,img_h }, ColorDef::NONE);
    200. //这里取得image buffer指针来赋值
    201. uint32_t* p_lock = img_outline->GetData();
    202. //复制到img_outline
    203. for (Span& s : spans_outline)
    204. {
    205. for (int w = 0; w < s._w; ++w)
    206. {
    207. size_t y = img_h - 1 - (s._y - rect._yMin);
    208. size_t index = y * img_w + s._x - rect._xMin + w;
    209. p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;
    210. }
    211. }
    212. //复制到img
    213. p_lock = img->GetData();
    214. for (Span& s : spans)
    215. {
    216. for (int w = 0; w < s._w; ++w)
    217. {
    218. size_t y = img_h - 1 - (s._y - rect._yMin);
    219. size_t index = y * img_w + s._x - rect._xMin + w;
    220. p_lock[index] = uint32_t(s._coverage) << 24 | 0x00ffffff;
    221. }
    222. }
    223. float bearingX = float(ft_face->glyph->metrics.horiBearingX >> 6);
    224. float bearingY = float(ft_face->glyph->metrics.horiBearingY >> 6);
    225. float advance = float(ft_face->glyph->advance.x >> 6);
    226. CharImage* ret = new CharImage;
    227. ret->_advance = advance;
    228. ret->_bearingX = bearingX;
    229. ret->_bearingY = bearingY;
    230. ret->_image = img;
    231. ret->_imageOutline = img_outline;
    232. return ret;
    233. }
    234. size_t GetFontSize()
    235. {
    236. return _vecFace.size();
    237. }
    238. ~FreeType()
    239. {
    240. for (auto& iter : _vecFace)
    241. FT_Done_Face(iter);
    242. FT_Done_FreeType(_library);
    243. }
    244. private:
    245. FT_Library _library;
    246. vector _vecFace;
    247. };
    248. FreeType* g_freetype;
    249. }

  • 相关阅读:
    快速入门:如何使用HTTP代理进行网络请求
    四步手把手教你实现扫雷游戏(c语言)
    项目背景以及游戏平台简介
    Arduino安装全流程(包括ESP32离线包安装)
    MySQL备份与恢复
    R语言分析糖尿病数据:多元线性模型、MANOVA、决策树、典型判别分析、HE图、Box's M检验可视化...
    CleanMyMac X4最新版测评效果及功能下载
    卷积神经网络(CNN)识别验证码
    【KD】Transformer在各个研究领域的轻量化研究进展
    开源安全的危机在于太相信 GitHub?——专访Apache之父&OpenSSF基金会总经理Brain Behlendorf...
  • 原文地址:https://blog.csdn.net/u014629601/article/details/126807571