在嵌入式Linux图形应用开发中,字体显示是一个绕不开的话题。本文基于FreeType字体引擎,详细讲解如何在LCD屏幕的指定位置显示一行汉字字符串,并重点解析字符基线、坐标系转换以及字符串外框计算等关键问题。

1 LCD 坐标系与 FreeType 坐标系的差异

1.1 LCD 使用的是“屏幕坐标系”

  • 原点 (0,0)左上角
  • X轴:向右
  • Y轴:向下

1.2 FreeType使用的是“笛卡尔坐标系”

  • 原点 (0,0)左下角
  • X轴:向右
  • Y轴:向上

        假设LCD垂直分辨率为V,两种坐标系之间的转换关系为:

LCD坐标(x,y) <---> 笛卡尔坐标(x,V-y)

👉 在使用 FreeType 显示字体前,必须完成坐标系转换,否则显示位置一定是错的。

2 为什么“字体大小”不是绝对值?

        在FreeType中,我们通常会这样设置字体大小:

FT_Set_Pixel_Sizes(face, 0, font_size);

⚠️ 注意:font_size只是一个“期望高度”,而不是强制像素高度。

例如字符串:

小程同学https://gitcode.com/xiaocheng/gitcode_knowledge
  • 汉字
  • 英文字母
  • 数字
  • 符号(.

如果所有字符都强制等高等宽,字形比例会被破坏,显示效果会非常难看。

👉 每个字符的实际宽度、高度、上下延展范围都是不同的

3 FreeType 中最重要的三个概念

        FreeType在绘制一行文字时,引入了几个非常重要的概念:

3.1 基线(Baseline)

  • 基线是字符排版的参考线,所有字符都基于同一条基线绘制
  • 字符可以:
    • 向上超出基线(如大写字母)
    • 向下超出基线(如 gy

3.2 原点(Origin / Pen)

        FreeType使用一个“笔尖(pen)”来决定字符的位置:

pen_next = pen_current + advance
  • 每个字符都有一个advance
  • 用于决定下一个字符的原点位置
  • advance的单位为1/64像素

2.3 字符外框(Bounding Box)

        每个字符都有一个独立的外框:

xMin, xMax, yMin, yMax

⚠️ 关键点:

  • 外框 不一定以原点为中心
  • 不同字符的外框大小完全不同

4 FreeType 中的几个关键数据结构

4.1 FT_Library(FreeType 库句柄)

FT_Library library;
FT_Init_FreeType(&library);

4.2 FT_Face(字体文件)

        一个FT_Face对应一个字体文件:

FT_New_Face(library, font_file, 0, &face);

4.3 FT_GlyphSlot(插槽)

FT_GlyphSlot slot = face->glyph;
  • 保存字符的位图、度量信息
  • 每次加载字符都会覆盖它

4.4 FT_Glyph 与 FT_BBox

FT_Get_Glyph(slot, &glyph);
FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_TRUNCATE, &bbox);
  • FT_Glyph:字符的几何描述
  • FT_BBox:字符的外接矩形(边界框)

5 如何计算“一整行文字”的外框?

5.1 核心思想

  • 每个字符都有自己的外框
  • 一行文字的外框 = 所有字符外框的并集

5.2 实现步骤

  1. pen 从 (0, 0) 开始(基线)
  2. 对每个字符:
    1. 设置变换(平移到 pen)
    2. 加载字符
    3. 获取字符外框
  3. 更新整行的最小 / 最大边界
  4. 根据 advance 移动 pen

5.3 关键代码

/**
 * @brief  计算一行字符串在 FreeType 坐标系中的整体外接矩形
 * @param  face   FreeType 字体对象
 * @param  wstr   宽字符字符串(Unicode)
 * @param  abbox  返回的一行文字整体外框
 * @return 0 成功,-1 失败
 */
int compute_string_bbox(FT_Face face, wchar_t *wstr, FT_BBox *abbox)
{
    int i;
    int error;

    /* 整行文字的外框 */
    FT_BBox bbox;

    /* 单个字符的外框 */
    FT_BBox glyph_bbox;

    /* pen:字符原点(基线上的位置),单位是 1/64 像素 */
    FT_Vector pen;

    /* glyph:用于获取字符的几何信息 */
    FT_Glyph glyph;

    /* glyph slot:字符位图和度量信息都保存在这里 */
    FT_GlyphSlot slot = face->glyph;

    /* 初始化整体外框
     * 使用一个很大的正负值,方便后续取 min / max
     */
    bbox.xMin = bbox.yMin =  32000;
    bbox.xMax = bbox.yMax = -32000;

    /* 指定第一个字符的原点为 (0, 0) */
    pen.x = 0;
    pen.y = 0;

    /* 依次处理字符串中的每一个字符 */
    for (i = 0; i < wcslen(wstr); i++)
    {
        /* 设置字体变换矩阵
         * 这里只使用平移(pen),不做旋转和缩放
         */
        FT_Set_Transform(face, 0, &pen);

        /* 加载字符并生成位图
         * 注意:每次都会覆盖 face->glyph 中的内容
         */
        error = FT_Load_Char(face, wstr[i], FT_LOAD_RENDER);
        if (error)
        {
            printf("FT_Load_Char error\n");
            return -1;
        }

        /* 从 glyph slot 中取出 glyph
         * glyph 保存了字符的几何轮廓信息
         */
        error = FT_Get_Glyph(face->glyph, &glyph);
        if (error)
        {
            printf("FT_Get_Glyph error\n");
            return -1;
        }

        /* 获取该字符的外接矩形(bounding box)
         * 得到的是字符相对于当前 pen 的外框
         */
        FT_Glyph_Get_CBox(glyph,
                          FT_GLYPH_BBOX_TRUNCATE,
                          &glyph_bbox);

        /* 更新整行文字的外框 */
        if (glyph_bbox.xMin < bbox.xMin)
            bbox.xMin = glyph_bbox.xMin;

        if (glyph_bbox.yMin < bbox.yMin)
            bbox.yMin = glyph_bbox.yMin;

        if (glyph_bbox.xMax > bbox.xMax)
            bbox.xMax = glyph_bbox.xMax;

        if (glyph_bbox.yMax > bbox.yMax)
            bbox.yMax = glyph_bbox.yMax;

        /* 根据 advance 移动到下一个字符的原点
         * advance 的单位同样是 1/64 像素
         */
        pen.x += slot->advance.x;
        pen.y += slot->advance.y;
    }

    /* 返回整行字符串的外框 */
    *abbox = bbox;

    return 0;
}

6 在指定 (x,y) 位置显示一行文字

6.1 思路总结

  1. 先计算整行文字的外框
  2. 已知:pen=(0,0)时,外框左上角是(x', y')
  3. 若要外框左上角对齐到(x,y)
pen.x = x - x'
pen.y = y - y'

⚠️ 注意:FreeType中pen的单位是 1/64 像素

6.2 显示函数核心代码

/**
 * @brief  在 LCD 的指定位置显示一行文字
 * @param  face   FreeType 字体对象
 * @param  wstr   宽字符字符串(Unicode)
 * @param  lcd_x  LCD 坐标系中的 X 坐标(左上角)
 * @param  lcd_y  LCD 坐标系中的 Y 坐标(左上角)
 * @return 0 成功,-1 失败
 */
int display_string(FT_Face face, wchar_t *wstr, int lcd_x, int lcd_y)
{
    int i;
    int error;

    /* 整行文字的外框 */
    FT_BBox bbox;

    /* pen:字符原点(基线上的位置) */
    FT_Vector pen;

    /* glyph slot:保存当前字符的位图和度量信息 */
    FT_GlyphSlot slot = face->glyph;

    /* 将 LCD 坐标转换为 FreeType 使用的笛卡尔坐标系
     * LCD 原点在左上角
     * FreeType 原点在左下角
     */
    int x = lcd_x;
    int y = var.yres - lcd_y;

    /* 第一步:计算整行文字的外接矩形 */
    compute_string_bbox(face, wstr, &bbox);

    /* 第二步:反推第一个字符的原点(pen)
     *
     * 当 pen = (0,0) 时,文字外框左上角是 (bbox.xMin, bbox.yMax)
     * 现在希望文字外框左上角对齐到 (x, y)
     */
    pen.x = (x - bbox.xMin) * 64;   /* FreeType 使用 1/64 像素 */
    pen.y = (y - bbox.yMax) * 64;

    /* 第三步:逐个字符加载并绘制 */
    for (i = 0; i < wcslen(wstr); i++)
    {
        /* 设置当前字符的变换(只平移,不旋转) */
        FT_Set_Transform(face, 0, &pen);

        /* 加载并生成当前字符的位图 */
        error = FT_Load_Char(face, wstr[i], FT_LOAD_RENDER);
        if (error)
        {
            printf("FT_Load_Char error\n");
            return -1;
        }

        /* 将字符位图绘制到 LCD 上
         * 注意:draw_bitmap 使用的是 LCD 坐标系
         */
        draw_bitmap(&slot->bitmap,
                    slot->bitmap_left,
                    var.yres - slot->bitmap_top);

        /* 移动到下一个字符的原点 */
        pen.x += slot->advance.x;
        pen.y += slot->advance.y;
    }

    return 0;
}

7 实验与运行结果

7.1 编译

arm-buildroot-linux-gnueabihf-gcc -o show_line show_line.c -lfreetype

7.2 运行

./show_line ./simsun.ttc 10 200 80

参数说明:

  • 10:  LCD X 坐标
  • 200:LCD Y 坐标
  • 80:字体大小

7.3 显示效果

        LCD 上成功显示:

小程同学https://gitcode.com/xiaocheng/gitcode_knowledge

文字左上角严格对齐到指定 (x, y) 坐标,字符间距与基线完全正确。

8 总结

        本文重点解决了 FreeType 初学者最容易踩坑的三个问题:

  • ✅ 坐标系不一致
  • ✅ 字符大小不统一
  • ✅ 一行文字整体定位困难

👉 如果本文对你有帮助,欢迎点赞、收藏、关注 小程同学>o< 🌱

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐