当前位置: 首页 > news >正文

渲染 opentype 多个字符的文本,并设置文本的渲染开始位置

文章目录

  • OpenType 可变字体渲染系统解析
    • 1. OpenType 可变字体基础
    • 2. 代码架构解析
      • 2.1 核心数据结构
      • 2.2 主渲染流程
    • 3. 关键函数解析
      • 3.1 可变字体实例处理 (`render_text_instances`)
      • 3.2 字形渲染核心 (`render_multiple_glyphs`)
    • 4. OpenType 与代码的对应关系
    • 5. 渲染过程详解
    • 6. 性能优化点
    • 7. 扩展性设计
  • 代码部分

OpenType 可变字体渲染系统解析

下面我将结合 OpenType 原理和代码实现,详细解释这个可变字体渲染系统的工作原理。

1. OpenType 可变字体基础

OpenType 可变字体通过以下核心概念实现字体变形:

  • 设计轴 (Design Axes):控制字体变形的参数(如字重、宽度等)
  • 主表 (fvar table):存储所有可变轴和命名实例的定义
  • 字形变化表 (gvar table):描述如何根据轴设置调整字形轮廓
  • 命名实例 (Named Instances):预设的轴值组合(如"Bold"、"Condensed"等)

2. 代码架构解析

2.1 核心数据结构

// FreeType 关键数据结构
FT_Library library;  // FreeType 库实例
FT_Face face;        // 字体face对象,包含所有字形信息
FT_MM_Var* mm_var;   // 可变字体元数据指针

2.2 主渲染流程

int main() {// 初始化FreeType库FT_Init_FreeType(&library);// 加载可变字体文件FT_New_Face(library, "NotoSansSC-VF_black.ttf", 0, &face);// 设置基础字号FT_Set_Pixel_Sizes(face, 0, FONT_SIZE);// 定义要渲染的Unicode码点std::vector<uint32_t> codepoints = {0x4F60, 0x597D, ...};// 渲染所有命名实例render_text_instances(face, codepoints, "output", "NotoSansSC");
}

3. 关键函数解析

3.1 可变字体实例处理 (render_text_instances)

bool render_text_instances(FT_Face face, ...) {// 检查是否为可变字体if (!FT_HAS_MULTIPLE_MASTERS(face)) return false;// 获取可变字体元数据FT_Get_MM_Var(face, &mm_var);// 保存原始轴设置FT_Get_Var_Design_Coordinates(face, mm_var->num_axis, original_coords);// 遍历所有命名实例for (FT_UInt i = 0; i < mm_var->num_namedstyles; i++) {// 切换到指定实例FT_Set_Named_Instance(face, i);// 渲染文本render_multiple_glyphs(...);// 保存为JPEGsave_as_jpeg(...);}// 恢复原始设置FT_Set_Var_Design_Coordinates(face, ...);
}

3.2 字形渲染核心 (render_multiple_glyphs)

void render_multiple_glyphs(...) {for (auto codepoint : codepoints) {// 加载字形信息(不渲染)FT_Load_Char(face, codepoint, FT_LOAD_NO_BITMAP);// 计算位置并渲染render_glyph_at_position(...);// 根据advance移动笔位置current_x += (face->glyph->advance.x >> 6) / (float)img_width;}
}

4. OpenType 与代码的对应关系

OpenType 概念代码实现说明
设计轴 (Design Axes)mm_var->axis[]通过FT_Get_MM_Var获取所有轴定义
命名实例 (Instances)mm_var->namedstyle[]每个实例包含预设的轴坐标和名称
字形轮廓调整FT_Set_Named_Instance触发FreeType内部自动处理gvar表的插值计算
字符映射 (cmap表)FT_Get_Char_Index将Unicode码点转换为字形索引
字形度量 (hmtx表)face->glyph->metrics包含advance、bearing等关键布局信息

5. 渲染过程详解

  1. 字形加载

    FT_Load_Char(face, codepoint, FT_LOAD_RENDER | FT_LOAD_NO_HINTING);
    
    • FreeType 根据当前实例设置自动调整字形轮廓
    • FT_LOAD_NO_HINTING 禁用提示以获得更准确的中文渲染
  2. 位置计算

    int x_pos = (img_width * start_x_percent) + face->glyph->bitmap_left;
    int y_pos = (img_height * start_y_percent) - face->glyph->bitmap_top;
    
    • 考虑字形的bearing(起始偏移)
    • 使用百分比位置便于不同尺寸的布局
  3. 位图渲染

    for (int y = 0; y < bitmap.rows; y++) {for (int x = 0; x < bitmap.width; x++) {// Alpha混合计算image[pos] = static_cast<unsigned char>(r * alpha + image[pos] * (1 - alpha));}
    }
    
    • 处理抗锯齿边缘的alpha通道
    • 支持前景色与背景色的混合

6. 性能优化点

  1. 字形缓存

    • 可添加缓存机制避免重复加载常用字形
    • 对静态文本预渲染
  2. 并行渲染

    • 不同实例的渲染可以并行处理
    • 使用线程池加速批量导出
  3. 内存管理

    • 重用图像缓冲区
    • 预分配足够的内存

7. 扩展性设计

  1. 多字体回退

    if (glyph_index == 0) {// 尝试回退字体
    }
    
  2. 动态轴控制

    FT_Set_Var_Design_Coordinates(face, num_axis, custom_coords);
    
  3. 复杂文本布局

    • 可集成Harfbuzz进行高级排版
    • 支持双向文本和组合字符

这个系统充分利用了OpenType可变字体的特性,通过FreeType API实现了高质量的文本渲染,特别优化了对中文等复杂文字的支持。

代码部分

// FreeType相关头文件
#include <ft2build.h>
#include FT_FREETYPE_H         // FreeType核心库
#include FT_MULTIPLE_MASTERS_H // 可变字体支持
#include FT_TRUETYPE_TAGS_H    // TrueType标签定义
#include FT_SFNT_NAMES_H       // SFNT名称表支持
#include FT_TYPE1_TABLES_H     // Type1字体表支持// JPEG处理库
#include <jpeglib.h>           // libjpeg库
#include <turbojpeg.h>         // TurboJPEG库// C++标准库
#include <vector>              // 向量容器
#include <string>              // 字符串处理
#include <iostream>            // 输入输出流
#include <algorithm>           // 算法函数
#include <iomanip>             // IO流格式化
#include <sstream>             // 字符串流
#include <cstdint>             // 标准整数类型// 渲染配置常量
const int IMAGE_WIDTH = 4800;   // 输出图像宽度(像素)
const int IMAGE_HEIGHT = 1200;  // 输出图像高度(像素)
const int FONT_SIZE = 300;      // 字体大小(像素)
const int JPEG_QUALITY = 95;    // JPEG压缩质量(0-100)
const int CHAR_SPACING = 50;    // 字符间距(像素)// 创建空白RGB图像函数
std::vector<unsigned char> create_blank_image(int width, int height,unsigned char r = 255,    // 红色分量默认值unsigned char g = 255,    // 绿色分量默认值unsigned char b = 255) {  // 蓝色分量默认值// 创建存储图像数据的向量(宽度×高度×3通道)std::vector<unsigned char> image(width * height * 3);// 填充白色背景for (int i = 0; i < width * height; i++) {image[i * 3] = r;      // 设置红色通道image[i * 3 + 1] = g;  // 设置绿色通道image[i * 3 + 2] = b; // 设置蓝色通道}return image;
}// 渲染单个字符到指定位置
void render_glyph_at_position(FT_Face face,           // FreeType字体face对象char32_t char_code,        // Unicode码点std::vector<unsigned char>& image, // 图像数据引用int img_width, int img_height,     // 图像尺寸float start_x_percent,     // 水平起始位置百分比(0.0-1.0)float start_y_percent,     // 垂直起始位置百分比(0.0-1.0)unsigned char r = 0,       // 文本颜色-红unsigned char g = 0,       // 文本颜色-绿unsigned char b = 0) {     // 文本颜色-蓝// 加载并渲染字符字形if (FT_Load_Char(face, char_code, FT_LOAD_RENDER | FT_LOAD_NO_BITMAP)) {std::cerr << "无法加载字符 U+" << std::hex << char_code << std::dec << std::endl;return;}// 获取字形位图FT_Bitmap& bitmap = face->glyph->bitmap;// 计算渲染位置(考虑字形的bearing)int x_pos = static_cast<int>(img_width * start_x_percent) + face->glyph->bitmap_left;int y_pos = static_cast<int>(img_height * start_y_percent) - face->glyph->bitmap_top;// 边界检查(确保不超出图像范围)x_pos = (std::max)(0, std::min(x_pos, (int)(img_width - bitmap.width)));y_pos = (std::max)(0, std::min(y_pos, (int)(img_height - bitmap.rows)));// 将字形数据渲染到图像for (int y = 0; y < bitmap.rows; y++) {for (int x = 0; x < bitmap.width; x++) {// 检查像素是否在图像范围内if (x + x_pos < img_width && y + y_pos < img_height) {// 获取当前像素的alpha值unsigned char val = bitmap.buffer[y * bitmap.pitch + x];int pos = ((y + y_pos) * img_width + (x + x_pos)) * 3;// 如果像素不透明则进行混合if (val > 0) {float alpha = val / 255.0f;  // 归一化alpha值// 混合前景色和背景色image[pos] = static_cast<unsigned char>(image[pos] * (1 - alpha) + r * alpha);image[pos + 1] = static_cast<unsigned char>(image[pos + 1] * (1 - alpha) + g * alpha);image[pos + 2] = static_cast<unsigned char>(image[pos + 2] * (1 - alpha) + b * alpha);}}}}
}// 批量渲染多个字符(自动计算位置)
void render_multiple_glyphs(FT_Face face,const std::vector<uint32_t>& codepoints, // Unicode码点数组std::vector<unsigned char>& image,      // 图像数据引用int img_width, int img_height,          // 图像尺寸float start_x_percent,         // 起始水平位置百分比float start_y_percent,         // 起始垂直位置百分比float advance_scale = 1.0f) {  // 字符间距缩放因子float current_x = start_x_percent;  // 当前水平位置// 遍历所有码点for (auto codepoint : codepoints) {// 预加载字符获取advance信息(不渲染)if (FT_Load_Char(face, codepoint, FT_LOAD_NO_BITMAP)) continue;// 渲染当前字符到当前位置render_glyph_at_position(face, codepoint, image, img_width, img_height,current_x, start_y_percent);// 计算下一个字符的位置(基于advance值)current_x += (face->glyph->advance.x >> 6) / (float)img_width * advance_scale;}
}// 保存图像为JPEG格式
bool save_as_jpeg(const std::string& filename,  // 输出文件名const std::vector<unsigned char>& image,    // 图像数据int width, int height) {                   // 图像尺寸// 初始化JPEG压缩器tjhandle jpegCompressor = tjInitCompress();if (!jpegCompressor) {std::cerr << "TurboJPEG初始化失败: " << tjGetErrorStr() << std::endl;return false;}unsigned char* jpegBuf = nullptr;  // JPEG缓冲区指针unsigned long jpegSize = 0;        // JPEG数据大小// 执行JPEG压缩if (tjCompress2(jpegCompressor, image.data(), width, 0, height, TJPF_RGB,&jpegBuf, &jpegSize, TJSAMP_444, JPEG_QUALITY,TJFLAG_ACCURATEDCT) != 0) {std::cerr << "JPEG压缩失败: " << tjGetErrorStr() << std::endl;tjDestroy(jpegCompressor);return false;}// 打开输出文件FILE* file = nullptr;if (fopen_s(&file, filename.c_str(), "wb") != 0 || !file) {std::cerr << "无法创建文件 " << filename << std::endl;tjFree(jpegBuf);tjDestroy(jpegCompressor);return false;}// 写入文件并检查是否成功bool success = fwrite(jpegBuf, 1, jpegSize, file) == jpegSize;fclose(file);tjFree(jpegBuf);tjDestroy(jpegCompressor);if (!success) {std::cerr << "写入文件失败: " << filename << std::endl;}return success;
}// 渲染可变字体的所有命名实例
bool render_text_instances(FT_Face face,const std::vector<uint32_t>& codepoints,  // Unicode码点数组const std::string& output_dir,    // 输出目录const std::string& base_filename, // 基础文件名float start_x_percent = 0.1f,    // 水平起始位置float start_y_percent = 0.5f) {  // 垂直起始位置// 检查是否为可变字体if (!FT_HAS_MULTIPLE_MASTERS(face)) {std::cerr << "字体不是可变字体" << std::endl;return false;}// 获取可变字体信息FT_MM_Var* mm_var = nullptr;if (FT_Get_MM_Var(face, &mm_var)) {std::cerr << "无法获取可变字体信息" << std::endl;return false;}// 保存原始设计坐标(用于恢复)FT_Fixed* original_coords = new FT_Fixed[mm_var->num_axis];FT_Get_Var_Design_Coordinates(face, mm_var->num_axis, original_coords);bool all_success = true;  // 跟踪所有实例是否成功渲染// 遍历所有命名实例for (FT_UInt i = 0; i < mm_var->num_namedstyles; i++) {// 设置当前命名实例if (FT_Set_Named_Instance(face, i)) {std::cerr << "无法设置命名实例 " << i << std::endl;all_success = false;continue;}// 获取实例名称(从SFNT名称表)std::string instance_name = "Instance_" + std::to_string(i);FT_SfntName name_info;if (FT_Get_Sfnt_Name(face, mm_var->namedstyle[i].strid, &name_info) == 0) {instance_name = std::string(reinterpret_cast<const char*>(name_info.string),name_info.string_len);// 清理名称中的非法字符(用于文件名)std::replace_if(instance_name.begin(), instance_name.end(),[](char c) { return !isalnum(c) && c != '_' && c != '-'; }, '_');}// 创建空白图像auto image = create_blank_image(IMAGE_WIDTH, IMAGE_HEIGHT);// 渲染所有字符到图像render_multiple_glyphs(face, codepoints, image, IMAGE_WIDTH, IMAGE_HEIGHT,start_x_percent, start_y_percent);// 生成输出文件名std::ostringstream filename;filename << output_dir << "\\" << base_filename << "_" << i << "_"<< instance_name << ".jpg";// 保存JPEG文件if (!save_as_jpeg(filename.str(), image, IMAGE_WIDTH, IMAGE_HEIGHT)) {all_success = false;}else {std::cout << "已保存: " << filename.str() << std::endl;}}// 恢复原始设计坐标FT_Set_Var_Design_Coordinates(face, mm_var->num_axis, original_coords);delete[] original_coords;FT_Done_MM_Var(face->glyph->library, mm_var);return all_success;
}// 主函数
int main() {// 初始化FreeType库FT_Library library;if (FT_Init_FreeType(&library)) {std::cerr << "无法初始化FreeType库" << std::endl;return 1;}// 加载字体文件FT_Face face;const std::string font_path = "D:/NotoSansSC-VF_black.ttf";if (FT_New_Face(library, font_path.c_str(), 0, &face)) {std::cerr << "无法加载字体文件 " << font_path << std::endl;FT_Done_FreeType(library);return 1;}// 设置字体大小FT_Set_Pixel_Sizes(face, 0, FONT_SIZE);// 定义要渲染的文本:"你好世界2025"的Unicode码点std::vector<uint32_t> codepoints = {0x4F60,  // 你0x597D,  // 好0x4E16,  // 世0x754C,  // 界0x0032,  // 20x0030,  // 00x0032,  // 20x0035   // 5};// 渲染所有命名实例(从图像20%宽度、40%高度处开始)if (!render_text_instances(face, codepoints, "D:/font_output", "NotoSansSC", 0.2f, 0.4f)) {std::cerr << "渲染过程中出现错误" << std::endl;}// 清理资源FT_Done_Face(face);FT_Done_FreeType(library);return 0;
}
http://www.dtcms.com/a/331295.html

相关文章:

  • Warm-Flow 1.8.0 重大更新
  • Lua 脚本在 Redis 中的应用
  • vivo Pulsar 万亿级消息处理实践(4)-Ansible运维部署
  • 河南萌新联赛2025第(五)场:信息工程大学补题
  • 飞书文档定时自动同步至百炼知识库
  • ESP32 I2S音频总线学习笔记(六):DIY蓝牙音箱教程
  • CVPR 2025 | 北大团队SLAM3R:单目RGB长视频实时重建,精度效率双杀!
  • 在mysql> 下怎么运行 .sql脚本
  • C#WPF实战出真汁00--项目介绍
  • 极速开发新体验_Vite构建工具详解
  • 使用YOLOv13进行钢板表面缺陷检测
  • Python之Django使用技巧(附视频教程)
  • 云手机都具有哪些特点?
  • Ollama如何分别使用2张H100GPU和4张A100部署GPT-OSS-120B全指南:硬件配置与负载均衡实战
  • Linux命令大全-zip命令
  • 嵌入式学习(day27)多任务进程
  • 接口测试与常用接口测试工具详解
  • CMake message()使用指南
  • SpringMVC(详细版从入门到精通)未完
  • 微前端-解决MicroApp微前端内存泄露问题
  • python bokeh
  • 为什么在函数内部,有时无法访问外部的变量?
  • 从0-1学习Java(三)快速了解字符串、数组、“==“与equals比较
  • 基于STM32的Air780短信发送
  • 【每天一个知识点】生物的数字孪生
  • C++模板特化、分离编译
  • 力扣-295.数据流的中位数
  • InfiniBand 与 RoCE 协议介绍
  • 激光雷达与可见光相机的图像融合
  • C++ vector越界问题完全解决方案:从基础防护到现代C++新特性