ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FreeType 字体引擎入门指南:从编译到字符像素转换

FreeType 字体引擎入门指南:从编译到字符像素转换 目录一、FreeType的定义及作用二、FreeType的源码下载与编译环境源码下载源码编译三、FreeType转换实战初始化字体库单个字符的转换字符串的转换转换思路示例1示例2示例3示例4示例5四、释放五、结语一、FreeType的定义及作用百度百科FreeType是字体引擎可以将字体解析后渲染成字形而不依赖操作系统的字体渲染机制。翻译一下就是FreeType不依赖系统就能通过字体文件(ttf、otf...)把文字的位图绘制出来。用更加白话的表达FreeType可以把字体文件(ttf、otf...)里面的文字转换为单通道灰度图的内存数据单通道灰度图是每个像素点用一个字节表示传统的黑白照片就只有一个通道表示亮度。手机、电脑中也有很多字体文件(最直观的体现WPS办公软件中的字体都来源与系统安装的字体)。windows中的字体C:\Windows\Fonts 这里看起来只是预览图你复制出来其实是.ttf类似的文件​android中的字体/system/fonts 也是一些ttf、otf等格式的字体文件​我们打开一个文件看看内容​里面存储的一串数字并不是我们想要的字符。为何这样我们千万不要搞混了依赖关系是有了字体才能显示出文字来也就是字体是描述文字轮廓的数学描述你想要画一张脸、不会直接给你一张脸而是告诉你脸的大小、耳朵的大小、头发的长短、眼睛的大小......FreeType的作用就是把字体文件中对文字的数学描述转换为位图数据描述。我们拿到这个位图数据后可以进一步加工封装成图片、也可以作为opengl着色器的纹理输入进行渲染。二、FreeType的源码下载与编译环境linux NDK (需要编译为适用于Android平台的库时使用)源码下载方式1FreeType Downloads 访问网站下载方式2wget https://mirrors.tuna.tsinghua.edu.cn/slackware/slackware-current/source/l/freetype/freetype-2.14.3.tar.xz执行命令、使用清华源下载推荐使用方式2、方式1经常无法访问、下载报错。源码编译tar -xvf freetype-2.14.3.tar.xz cd freetype-2.14.3.tar.xz mkdir build cd build export NDK/mnt/d/Android/Sdk/ndk/27.0.12077973 路径是自己的NDK路径 cmake \ -DCMAKE_TOOLCHAIN_FILE${NDK}/build/cmake/android.toolchain.cmake \ -DANDROID_ABIarm64-v8a \ -DANDROID_PLATFORMandroid-30 \ -DCMAKE_BUILD_TYPERelease \ -DCMAKE_INSTALL_PREFIX$(pwd)/install \ .. 编译为Linux平台去掉DCMAKE_TOOLCHAIN_FILE、DANDROID_PLATFORM、DANDROID_ABI、参数即可、要编译动态库添加-DBUILD_SHARED_LIBSON参数 make -j16 make install install目录下会生成头文件和库文件三、FreeType转换实战这一章节中只会讲解如何将字符、字符串转换为我们对应的RGBA内存数据至于转换后RGBA数据如何显示这里并不涉及笔者是在使用Opengl作为shader的纹理输入进行渲染的。初始化字体库需要先准备好字体文件或者从阿里巴巴普惠体下载新的字体这里我下载的是AlimamaFangYuanTiVF-Thin.ttf(阿里妈妈方圆体支持简体汉字、数字、英文、常用标点、非等宽字体)#include codecvt #include locale #include ft2build.h #include FT_FREETYPE_H //字体库实例 FT_Library mFtLibrary; //字体的脸所有对字体的操作都是通过FT_Face 完成的 FT_Face mFtFace; /* 字体文件原始数据指针使用过程中不可释放 这里访问的字体文件在Anroid工程的assets路径下无法通过路径访问 所以需要将内容读取到mFontData中如果你的项目可以通过路径访问字体文件可以不用声明。 但你坚持声明时请通过标准IO打开字体文件并读取数据到mFontData中。 / unsigned char mFontData;/* 字体在Android工程的assets下只能通过AAssetManager访问。 如果字体文件可以通过绝对路径访问则通过标准IO读取或直接使用FreeType提供的路径加载函数即可。 */ bool RenderManager::initFreeType(AAssetManager* mgr) { //初始化 if (FT_Init_FreeType(mFtLibrary) ! 0) { return false; } //打开文件 AAsset* asset AAssetManager_open(mgr, AlimamaFangYuanTiVF-Thin.ttf, AASSET_MODE_BUFFER); if (!asset) { release(); return false; } int64_t mFontDataSize AAsset_getLength(asset); mFontData (unsigned char *) malloc(mFontDataSize); //读取数据 int readBytes AAsset_read(asset, mFontData, mFontDataSize); AAsset_close(asset); if (readBytes ! mFontDataSize) { release(); return false; } /* 字体加载 FT_Face脸就是目录索引的意思后面就拿着这个对象像查字典一样查找字符 FT_New_Face根据路径加载使用过程中文件不可删除 FT_New_Memory_Face根据内存数据加载使用过程中内存不可释放 两种方式均要求使用过程中数据不可以被关闭/释放因为FT_Face后续查找字符时还需要继续访问 */ if (FT_New_Memory_Face(mFtLibrary, mFontData, mFontDataSize, 0/*加载第几套字体ttf/otf只有一套必须为0ttc多套0常规, 1粗体, 2初斜体 传递-1时查询face数量*/, amp;mFtFace) ! 0) { release(); return false; } /* 字体像素设置(行高而不是字符本身的高度字符的高度是和字符本身相关的比如字符1和。) 只指定高度宽度会自适应不会拉伸变形标准做法。 只是决定占用高度不是最终显示的高度显示时可以自己进行缩放。 比如抄别人作业时别人的一行文字占用高度和你自己想占用的高度是无关的 但是别人写得太小看不清楚时你肯定也是看不清楚的所以清晰度是有关联的 */ FT_Set_Pixel_Sizes(mFtFace, 0, 36); return true; //如果需要适配多套字体(切换字体风格或语言)建议提前加载好不同FT_Face保存便于切换。 }单个字符的转换/** * brief 将单个字符转换为RGBA位图数据 * param[in] c 输入字符 * param[out] outWidth 输出位图的宽度 * param[out] outHeight 输出位图的高度 * return 转换后的RGBA位图缓冲区 * warning 返回缓冲区不使用时必须free */ unsigned char* RenderManager::charToRGBA(char c, int* outWidth, int* outHeight) { //加载字符 if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) { *outWidth 0; *outHeight 0; return nullptr; } //位图数据获取bitmap这个bitmap是单通道的灰度图 FT_GlyphSlot slot mFtFace-gt;glyph; FT_Bitmapamp; bitmap slot-gt;bitmap; //位图宽高获取 int width bitmap.width; int height bitmap.rows; if (width 0 || height 0) { *outWidth *outHeight 0; return nullptr; } //以位图宽高进行申请RGBA空间因为是RGBA 4通道所以每个像素大小需要4个字节 unsigned char* rgba (unsigned char*)calloc(width * height, 4); if (!rgba) { *outWidth *outHeight 0; return nullptr; } //灰度图--gt;RGBA位图、逐个像素进行拷贝 for (int y 0; y lt; height; y) { for (int x 0; x lt; width; x) { unsigned char gray bitmap.buffer[y * width x]; /* 灰度值为0为纯黑不用填充数据calloc申请时就已经全部重置为0 所以只需要将非纯黑色区域说明就是文字的内容转换为纯白色就行 */ if (gray 0) continue; int idx (y * width x) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] 255; // A } } *outWidth width; *outHeight height; return rgba; }字符A的单通道灰度图 转换为RGBA的字符A非常重要的是通过FT_Load_Char加载的灰度图Y通道实际上并不是一个固定的亮度就像黑白照片一样有的部位更亮、有的地方更弱如果只有一个亮度你只能看到轮廓不能看到细节想象一下一张黑白照片的人脸比如人脸区域都是一个亮度那不是只知道大概是一张脸不是吗 这里的字符A白色是一个亮度是因为我为了演示方便渐变亮度的A不好演示。此外FT_Load_Char为了更加节省空间、生成的灰度图大小总是一个紧紧包裹字符边缘的矩形。转换后的效果取决于自己的处理我通过Opengl渲染进行了一定程度的缩放且截图时图片进行了放大所以看起来更加的模糊这里第二张图片的背景并不是从字符A单通道中采样的结果因为我们对黑色部分也就是灰度为0纯黑时RGBA的A通道没有进行赋值操作默认为0表示全透明也就是渲染时实际上只有白色的字符A, 背景区域是透明的这里的背景是我的底图而已。最后字符A成功的进行了转换但是字符A很明显过渡生硬、出现了严重的锯齿状这是为什么rgba[idx 3] 255; // A因为我们给灰度图的非黑色区域把alpha通道统设置成了255(完全不透明)这相当于某一个像素原本不是纯黑只有一点点白色但不是纯白时你强行设置了一个纯白色因为透明度是完全不透明的所以像素与像素之间过度生硬原本是渐变的像素变得更加突兀看起来有锯齿感。当我们把亮度值作为Alpha通道时就能完美的解决这一问题。亮度值越低(越接近0)需要透明度越高越接近0也就是一个像素越接近纯黑时这个像素点的透明度需要越高才能够看到后面的黑色区域如果为纯白时透明度应该完全不透明不应该看到后面的黑色。rgba[idx 0] 255; // R rgba[idx 1] 0; // G rgba[idx 2] 0; // B rgba[idx 3] gray; // A 灰度值当作透明度灰度作为透明的修改颜色为红色这样看起来好了很多通过以上的操作你以为你可以成功的转换单个字符了还是高兴太早了。那我想要转换一个中文字符怎么办请先思考2秒。unsigned char* RenderManager::charToRGBA(char c, int* outWidth, int* outHeight)实际上我们的函数已经很明确了我们接收的是一个字节的字符当我们传入的是一些常用的符号以及英文字符时默认以utf-8编码这些字符都是占用一个字节的ASCII码占用一个字节。当我们需要接受中文或者其他国家的字符时char是装不下的我们需要换一个更宽的字符变量来接受这里我们为了兼容性选取目前可以容纳所有字符的最大宽度的类型 char32_t。unsigned char* RenderManager::charToRGBA(char32_t c, int* outWidth, int* outHeight)charToRGBA(U国, width, height);使用字面量U指定字符类型为char32_tC11引入无论是一个字节还是四个字节的字符都可以容纳只能说非常完美前提是你的字体文件是支持中文的否则FT_Load_Char会失败有一点需要注意的是如果我们传入的字符并不能在传入时确定你必须要知道这个字符的编码格式然后根据编码格式获取到该字符对应的码点然后作为参数传入。下一章节中有介绍如何将字符串转换为char32_t的定长编码格式然后通过遍历的方式就可以获取到每个字符对应char32_t类型的码点。字符串的转换FT_Load_Char可以从字体中解析出字符的灰度图、那如果是字符串呐很遗憾FreeType并没有提供字符串的解析就像查字典一样你可以查出一个文字但是你无法查一段文字所以需要自行处理字符串的转换。转换思路我们之前在单个字符的转换中已经说过FT_Load_Char为了更加节省空间、生成的灰度图大小总是一个紧紧包裹字符边缘的矩形那现在如果要做字符串转换我们就需要手动的拼接字符串中的每一个字符比如: Hello上面就是通过FT_Load_Char转换Hello字符串中每一个字符的示例他总是生成一个紧紧包裹住字符边缘的矩形(实际生成的灰度图边缘没有黑色这里是为了对比明显纯白色看不清边缘)。所以我们的思路就是将字符串逐个字符分解转换完成后重新拼接起来。示例1/** * brief 将字符串转换为RGBA位图数据 * param[in] text 输入的字符串 * param[out] outWidth 输出位图的宽度 * param[out] outHeight 输出位图的高度 * return 转换后的RGBA位图缓冲区 * warning 返回缓冲区不使用时必须free */ unsigned char* RenderManager::textToRGBA(const std::string text, int* outWidth, int* outHeight) { //字符串的总宽度amp;高度 int totalWidth 0; int maxHeight 0; //计算字符串的总宽度及字符的最大高度 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; totalWidth slot-gt;bitmap.width; if (slot-gt;bitmap.rows gt; maxHeight) { maxHeight slot-gt;bitmap.rows; } } if (totalWidth 0 || maxHeight 0) { *outWidth 0; *outHeight 0; return nullptr; } //RGBA位图空间申请 unsigned char* rgba (unsigned char*)calloc(totalWidth * maxHeight, 4); //字符的起始位置因为一个一个拼接字符所以需要记录当前拼接字符的起始位置 int penX 0; //字符拼接 一个一个字符遍历 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; FT_Bitmapamp; bitmap slot-gt;bitmap; //一个一个像素点进行遍历拷贝到rgba中 for (int y 0; y lt; (int)bitmap.rows; y) { for (int x 0; x lt; (int)bitmap.width; x) { unsigned char gray bitmap.buffer[y * bitmap.width x]; if (gray 0) continue; //计算在RGBA新内存中的x坐标和y坐标 int dstX penX x; int dstY y; //坐标校验 不在范围内的不处理 if (dstX lt; 0 || dstX gt; totalWidth || dstY lt; 0 || dstY gt; maxHeight) continue; int idx (dstY * totalWidth dstX) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] gray; // A } } //更新下一个字符的起始坐标 penX slot-gt;bitmap.width; } *outWidth totalWidth; *outHeight maxHeight; return rgba; }我们渲染了一个日期的字符串可以看到字符串已经被显示了出来。主要步骤分析1.获取每个字符的宽度、累加作为字符串的总宽度、高度以最搞的字符为准totalWidth slot-bitmap.width; if (slot-bitmap.rows maxHeight) { maxHeight slot-bitmap.rows; }2.申请相应的空间用于存储转换后的RGBA字符串的存储unsigned char* rgba (unsigned char*)calloc(totalWidth * maxHeight, 4);3.定义字符当前坐标 penx 0,表示从字符坐标从 x0 处开始拼接字符串int penX 0;4.灰度图字符的坐标转换为RGBA像素中的坐标//灰度图中的像素坐标x 字符起始位置作为作为RGBA像素中的x坐标 int dstX penX x; //灰度图中的像素坐标y, 作为RGBA像素中的y坐标 int dstY y;5.计算灰度图中每一个像素对应RGBA像素中的每一个位置起始坐标并给RGBA通道赋值/* 灰度图中的某一个需要插入到RGBA像素的位置: (当前像素的高度 * RGBA像素的宽度) * 4 灰度图中像素的位置对应RGBA像素中的起始位置 */ int idx (dstY * totalWidth dstX) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] gray; // A6.更新下一个字符的起始坐标//下一个字符的起始坐标为当前坐标当前字符宽度 penX slot-bitmap.width;通过上面的步骤我们虽然转换出来了字符但是我们可以明显的看到年-月-日中间的分割符被转换到了RGBA中的顶部这是为什么那是因为 FT_Load_Char转换字符 - 时 转换后的灰度图高度就只有字符 - 的原始高度。当我们在第4步中将灰度图中的像素位置y直接作为了RGBA像素中新像素的y坐标导致了字符串位置没有被对齐这里恰好其他字符的高度是一致的所以看着没有问题当其他字符高度不一致时一样会存在问题同理除了垂直对齐水平方向上因为字符宽度的不同也会导致水平方向上排布不均。那么如何解决FT_Load_Char字符串转换中带来的字符对齐问题呢不用慌虽然FreeType总是生成包裹字符的最小灰度图但是并不意味着我们无法还原这个字符的相对位置。//字符的离地高度 int top slot-bitmap_top; //字符到左边的像素距离 int left slot-bitmap_left;通过上面的方式我们可以获取到字符原始的排布参数用这个参数我们就可以解决字符对齐问题了这两个参数是由字体文件本身和初始化字体时的像素大小决定的也就是每一套字体根据像素大小的不同每一个字符的离地高度和左边距都是已知的举个例子如字符.我们最开始设置了一定的字体像素大小但是生成对应灰度图的宽高并不是按照我们设置的像素值来生成原因就是FreeType是以内存最小的方式来生成的但是FreeType提供了额外的参数以便我们还原原始的像素排布。用一个比喻来说就是假如你有一些透明篮子装了一些不同大小的萝卜FreeType不会把装了萝卜的透明篮子一起给你只会给你萝卜但是这个萝卜在篮子里面怎么放就是通过额外的信息给你甚至有些萝卜可以悬浮在篮子中实际上透明篮子的大小也不是固定的只是为了更好的理解top和left引出灰度图外部有一个透明篮子在容纳。需要注意的是这里的slot-bitmap_top和bitmap.rows不同rows表示像素的行高是包裹像素内容的高度而top是字符顶部的离地高度表示的是 顶部到地面的距离比如字符 -他的行高虽然可能只有几个像素但是它是悬浮在空中此时这个后面我们会进一步介绍。示例2示例1中虽然已经显示了字符但是存在字体无法对齐的问题我们也分析了应该如何实现字体对齐现在我们对示例1中的代码进行修改/** * brief 将字符串转换为RGBA位图数据 * param[in] text 输入的字符串 * param[out] outWidth 输出位图的宽度 * param[out] outHeight 输出位图的高度 * return 转换后的RGBA位图缓冲区 * warning 返回缓冲区不使用时必须free */ unsigned char* RenderManager::textToRGBA(const std::string text, int* outWidth, int* outHeight) { //字符串的总宽度amp;高度 int totalWidth 0; int maxHeight 0; //计算字符串的总宽度及字符的最大高度 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; totalWidth slot-gt;bitmap.width; if (slot-gt;bitmap.rows gt; maxHeight) { maxHeight slot-gt;bitmap.rows; } } if (totalWidth 0 || maxHeight 0) { *outWidth 0; *outHeight 0; return nullptr; } //RGBA位图空间申请 unsigned char* rgba (unsigned char*)calloc(totalWidth * maxHeight, 4); //字符的起始位置因为一个一个拼接字符所以需要记录当前拼接字符的起始位置 int penX 0; //字符拼接 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; FT_Bitmapamp; bitmap slot-gt;bitmap; int top slot-gt;bitmap_top; int left slot-gt;bitmap_left; //一个一个像素点进行遍历拷贝到rgba中 for (int y 0; y lt; (int)bitmap.rows; y) { for (int x 0; x lt; (int)bitmap.width; x) { unsigned char gray bitmap.buffer[y * bitmap.width x]; if (gray 0) continue; int dstX penX left x; int dstY (maxHeight - top) y; //坐标校验 不在范围内的不处理 if (dstX lt; 0 || dstX gt; totalWidth || dstY lt; 0 || dstY gt; maxHeight) continue; int idx (dstY * totalWidth dstX) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] gray; // A } } //更新下一个字符的起始坐标 penX bitmap.width; } *outWidth totalWidth; *outHeight maxHeight; return rgba; }这次看着顺眼多了1.获取字符离地高度和左边距int top slot-bitmap_top; int left slot-bitmap_left;2.修改灰度图字符的坐标转换为RGBA像素中的坐标x和y都加上偏移//加上左边距 int dstX penX left x; //因为当前申请的是以最大字符高度进行申请的空间所以最大高度- 离地高度 像素自身y 距离顶部的高度 int dstY (maxHeight - top) y;通过上面的修改字符看似都对齐了不知细心的你还有没有发现每个字符是不是都挨得太紧密了我们是不是应该设置一下字符与字符的间隔以便看起来更加美观。于是你做了这样的修改让每个字符都间隔固定的位置1.计算总宽度时每个字符都4totalWidth totalWidth slot-bitmap.width 4;2.每个字符的间距都偏移 4像素penX bitmap.width 4;这一次似乎更加完美示例3当你还在因为示例2中展现出来的完美结果而暗喜时我不得不告诉你不要使用固定的偏移你只是强行把字符与字符之间用固定的宽度隔离是错误的。比如你想转换 are you ok!那么转换结果的!号一定和字母k至少有4像素的间隔那并不是我们想要的结果。所以不要以固定值来设置字符间隔因为不同的字体字符与字符之间应该以何种间隔展示时是在字体设计阶段就完成的。等宽字体与非等宽字体图片来源比例字体等宽字体-腾讯云开发者社区-腾讯云通过以下方式可以获取到该字符到下一个字符的偏移static_castint(slot-advance.x 6);我们对程序再一次加以修正只修改两个地方1.计算总宽度是以字符偏移间距进行累加的totalWidth static_castint(slot-advance.x 6);2.下一个字符的偏移像素penX static_castint(slot-advance.x 6);这一次似乎是真的完美了既对齐了字符也设置了合适的间距还能有什么问题呢示例4当你兴高采烈的以为程序很完美时是的你一定是忘记了什么才导致forget 的g被截取了一半。我们回顾一下代码if (slot-bitmap.rows maxHeight) { maxHeight slot-bitmap.rows; }我们申请高度是按照最大高度申请的为什么会装不下呐请思考2S ......聪明的你一下就知道了问题所在虽然是按照最大高度申请理论上申请的空间可以容纳最高的那个字符串但是这种情况仅适用于方方正正的字符(典型的汉字就是方块字类型)当遇到比如英文字符时这种情况就不再适用。我们观察一下 forget, 很快就意识到仅靠文字的高度无法申请正确的高度。我们必须申请字符串的顶部到字符串底部的这段最大距离作为高度。那么字符串顶部到字符串底部的最大距离我们应该如何计算呐聪明的字体设计师早就给我们想好了这一点看下面这副图你一定很熟悉这不就是我们的英语作业本吗图片来源dotnet 简单聊聊 Skia 里的 SKFontMetrics 的各项属性作用-腾讯云开发者社区-腾讯云基线颜色最深的那条绿色线条是我们排布的基准top字符顶部到基线的距离rows: 字符的高度所有字母都是以基线为基准rows是字符像素本身的高度top是基线到字符顶部的距离。这里的top就是我们之前说的字符的 离地高度地面就是基线。有的人规规矩矩的站在基线上有的人却把脚伸进了基线下。所以我们最终要申请的高度是 基线上半部分的最大top高度 基线以下部分最深的高度绝不能用单个字符的高度来作为最后的高度。计算最大高度// 基线以上最大高度 int maxAscent 0; // 基线以下最大深度 int maxDescent 0; //遍历每个字符计算 for (char c : text) { FT_Load_Char(mFtFace, c, FT_LOAD_RENDER); FT_GlyphSlot slot mFtFace-glyph; int ascent slot-gt;bitmap_top; int descent slot-gt;bitmap.rows - slot-gt;bitmap_top; if (ascent gt; maxAscent) maxAscent ascent; if (descent gt; maxDescent) maxDescent descent; } //最大高度 int maxHeight maxAscent maxDescent;修改后的代码为/** * brief 将字符串转换为RGBA位图数据 * param[in] text 输入的字符串 * param[out] outWidth 输出位图的宽度 * param[out] outHeight 输出位图的高度 * return 转换后的RGBA位图缓冲区 * warning 返回缓冲区不使用时必须free */ unsigned char* RenderManager::textToRGBA(const std::string text, int* outWidth, int* outHeight) { int totalWidth 0; //遍历字符串获取需要申请的数据宽高 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; totalWidth static_castlt;intgt;(slot-gt;advance.x gt;gt; 6); } //计算要申请的最大高度 int maxAscent 0; int maxDescent 0; for (char c : text) { FT_Load_Char(mFtFace, c, FT_LOAD_RENDER); FT_GlyphSlot slot mFtFace-gt;glyph; int ascent slot-gt;bitmap_top; int descent slot-gt;bitmap.rows - slot-gt;bitmap_top; if (ascent gt; maxAscent) maxAscent ascent; if (descent gt; maxDescent) maxDescent descent; } int maxHeight maxAscent maxDescent; if (totalWidth 0 || maxHeight 0) { *outWidth *outHeight 0; return nullptr; } //分配 RGBA 内存全透明 unsigned char* rgba (unsigned char*)calloc(totalWidth * maxHeight, 4); //字符的起始位置因为一个一个拼接字符所以需要记录当前拼接字符的起始位置 int penX 0; //字符拼接 for (char c : text) { if (FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; FT_Bitmapamp; bitmap slot-gt;bitmap; int top slot-gt;bitmap_top; int left slot-gt;bitmap_left; //一个一个像素点进行遍历拷贝到rgba中 for (int y 0; y lt; (int)bitmap.rows; y) { for (int x 0; x lt; (int)bitmap.width; x) { unsigned char alpha bitmap.buffer[y * bitmap.width x]; if (alpha 0) continue; int dstX penX left x; int dstY (maxAscent - top) y; if (dstX lt; 0 || dstX gt; totalWidth || dstY lt; 0 || dstY gt; maxHeight) continue; int idx (dstY * totalWidth dstX) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] alpha; // A } } //更新下一个字符的起始坐标 penX static_castlt;intgt;(slot-gt;advance.x gt;gt; 6); } *outWidth totalWidth; *outHeight maxHeight; return rgba; }这次我觉得真没问题了但是经过前面的教训还有示例5那大概率还有问题。示例5在前面的示例中我们先后解决了字符锯齿、字符串水平和垂直对齐、字符间距、字符截取问题现在想想还有什么问题没有解决是的还剩中文字符串不出所料当我们传入中文字符串我爱中国结果没有正确显示。早在单个字符串的转换的讲解中我们就知道了如何显示中文的单个字符我们需要确保传入给FT_Load_Char(mFtFace, c, FT_LOAD_RENDER)的字符编码c的码点为char32_t类型为什么是char32_t而不是char、char16_t ? char32_t的宽度足够容纳当前最大的字符编码。我们熟悉的常用编码utf-8是变长编码一个字符占用1-4个字节utf-16也是变长编码一个字符占用2-4个字节utf-32是定长编码每个字符都占用四个字节。如果我们把输入的字符串转换为utf-32编码我们就可以遍历字符串了如果是变长编码我们无法遍历字符串就无法循环加载单个字符的灰度图了。1.新增字符串编码转换函数std::u32string RenderManager::utf8_to_utf32(const std::string utf8) { try { std::wstring_convertstd::codecvt_utf8char32_t, char32_t converter; return converter.from_bytes(utf8); } catch (const std::exception e) { //转换失败返回空字符串 u8:utf-8 u:utf-16 U:utf-32 LOGD(utf8_to_utf32 failed); return U; } }2.遍历字符串时要以char32_t类型遍历for (char32_t cp : u32Text)通过上面的方式utf-8就转换为utf-32的编码格式这里存在新的问题假如你的字符串是来源于外部必须要想办法解析出编码格式才能正确转换有可能输入字符串并非utf-8。修改后/** * brief 将字符串转换为RGBA位图数据 * param[in] text 输入的字符串 * param[out] outWidth 输出位图的宽度 * param[out] outHeight 输出位图的高度 * return 转换后的RGBA位图缓冲区 * warning 返回缓冲区不使用时必须free */ unsigned char* RenderManager::textToRGBA(const std::string text, int* outWidth, int* outHeight) { std::u32string u32Text utf8_to_utf32(text); if (u32Text.empty()) { *outWidth *outHeight 0; return nullptr; } int totalWidth 0; //遍历字符串获取需要申请的数据宽高 for (char32_t cp : u32Text) { if (FT_Load_Char(mFtFace, cp, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; totalWidth static_castlt;intgt;(slot-gt;advance.x gt;gt; 6); } //计算要申请的最大高度 int maxAscent 0; int maxDescent 0; for (char32_t cp : u32Text) { FT_Load_Char(mFtFace, cp , FT_LOAD_RENDER); FT_GlyphSlot slot mFtFace-gt;glyph; int ascent slot-gt;bitmap_top; int descent slot-gt;bitmap.rows - slot-gt;bitmap_top; if (ascent gt; maxAscent) maxAscent ascent; if (descent gt; maxDescent) maxDescent descent; } int maxHeight maxAscent maxDescent; if (totalWidth 0 || maxHeight 0) { *outWidth *outHeight 0; return nullptr; } //分配 RGBA 内存全透明 unsigned char* rgba (unsigned char*)calloc(totalWidth * maxHeight, 4); //字符的起始位置因为一个一个拼接字符所以需要记录当前拼接字符的起始位置 int penX 0; //字符拼接 for (char32_t cp : u32Text) { if (FT_Load_Char(mFtFace, cp, FT_LOAD_RENDER)) continue; FT_GlyphSlot slot mFtFace-gt;glyph; FT_Bitmapamp; bitmap slot-gt;bitmap; int top slot-gt;bitmap_top; int left slot-gt;bitmap_left; //一个一个像素点进行遍历拷贝到rgba中 for (int y 0; y lt; (int)bitmap.rows; y) { for (int x 0; x lt; (int)bitmap.width; x) { unsigned char alpha bitmap.buffer[y * bitmap.width x]; if (alpha 0) continue; int dstX penX left x; int dstY (maxAscent - top) y; if (dstX lt; 0 || dstX gt; totalWidth || dstY lt; 0 || dstY gt; maxHeight) continue; int idx (dstY * totalWidth dstX) * 4; rgba[idx 0] 255; // R rgba[idx 1] 255; // G rgba[idx 2] 255; // B rgba[idx 3] alpha; // A } } //更新下一个字符的起始坐标 penX static_castlt;intgt;(slot-gt;advance.x gt;gt; 6); } *outWidth totalWidth; *outHeight maxHeight; return rgba; }四、释放void RenderManager::release() { if (mFtFace) FT_Done_Face(mFtFace); if (mFtLibrary) FT_Done_FreeType(mFtLibrary); if (mFontData) free(mFontData); }五、结语这篇文章是我使用opengl渲染文字水印时记录本来以为可以很快速的写完但是在记录的过程中发现一些细节并不是了解得那么的清楚所以就稍慢了一些重新对一些细节进行了了解。写那么多错误示例是在记录思考过程中一步一步解决问题的思路希望可以帮到你。
返回列表