文章目录
好的,为你准备一份代码介绍博客文章。这篇文章会解释你的工具做什么,如何使用,以及它背后的技术原理。
告别选择困难:将可变字体(Variable Font)“冻结”为静态实例的利器
可变字体(Variable Font)是现代网页和应用设计中的一股革新力量,它允许在一个字体文件中存储无限多的样式变体,通过调整“轴”(如字重 wght、字宽 wdth)来动态生成字体效果。这极大地减少了字体文件的大小,并赋予了设计师前所未有的自由。
然而,这种灵活性并非在所有场景下都适用。在一些老旧系统、特定的排版软件,或者需要精确控制字体文件且不希望有额外“动态”特性的环境中,我们仍然需要传统的静态字体(Static Font)。
这就引出了一个问题:我们能否从一个可变字体中,提取出它所有预定义的命名实例(Named Instances),并将它们“固化”为独立的静态字体文件呢?
答案是肯定的!今天,我将向大家介绍一个用 C++ 开发的小工具,它能够精准地完成这项任务。
工具简介:FreezeType (或者叫 Var2Static)
我为这个工具起名为 FreezeType,寓意是把可变字体这种“流动”的形态,“冻结”成一个个静态、独立的字体文件。
它能做什么?
FreezeType 工具的核心功能是读取一个可变字体文件(如 .ttf 或 .otf),然后:
- 识别所有命名实例: 可变字体通常会内嵌一些设计师预定义的“快照”,比如“Regular”、“Bold”、“Light Condensed”等。这些被称为命名实例。
- 精确提取轴坐标: 对于每个命名实例,工具会从字体内部获取其对应的所有轴(例如字重
wght= 700,字宽wdth= 100)的精确数值。 - 生成独立的静态字体文件: 它会将这些轴坐标“固化”到字体轮廓数据中,移除所有可变字体相关的元数据,生成一个完全独立的静态
.ttf文件。 - 智能命名输出文件: 生成的静态字体文件将采用“
字体家族名-样式名_轴缩写+数值.ttf”的命名格式,例如NotoSansSC-Bold_w700_wd100.ttf,让你一目了然地知道每个文件的具体样式。
为什么需要它?
- 兼容性: 为不支持可变字体的旧系统或软件提供兼容性方案。
- 性能优化: 在某些特定场景下,静态字体可能比可变字体渲染更快,或者集成更容易。
- 精确控制: 开发者可以精确地分发和管理每个特定的字体样式。
- 备份与归档: 将可变字体的每一个重要样式作为独立的静态文件进行备份和归档。
技术揭秘:HarfBuzz Subsetter 与 FreeType2 的强强联合
FreezeType 的强大功能,离不开两个开源字体处理库:
- FreeType2:
- 作用: 用于加载字体文件,解析其结构,特别是读取可变字体中的
fvar(Font Variations) 表,以获取所有**命名实例(Named Instances)**的定义及其对应的轴(wght,wdth等)坐标。 - 核心 API:
FT_New_Face,FT_Get_MM_Var等。
- HarfBuzz Subsetter (hb-subset):
-
作用: 这是实现“静态化”的核心引擎。它是一个功能强大的字体子集化(subsetting)工具,但它最关键的能力之一是字体轴的“固定”(Axis Pinning)。
-
核心 API:
hb_subset_input_pin_axis_location。通过这个函数,我们告诉 HarfBuzz 将某个轴固定到特定的数值。HarfBuzz 会自动执行以下复杂操作: -
将字体轮廓(
glyf或CFF2表)的变异数据(deltas)应用到基本轮廓上。 -
移除所有可变字体相关的表(如
fvar,gvar,avar,STAT)。 -
修正字体度量(如
hmtx表),确保字距在静态化后依然准确。 -
构建一个完全符合 OpenType 规范的静态字体文件。
-
优势: 使用
hb-subset进行轴固定比手动解析和修改字体表要安全、高效且不易出错。它处理了大量的底层细节,确保生成字体文件的完整性和正确性。
代码概览(简化版)
为了便于理解,我们来看一下核心逻辑的简化版。完整的代码可以在文末获取。
// 1. 获取可变字体实例信息
std::map<int, std::set<std::string>> get_variable_font_instances(FT_Face face);
// 2. 静态化核心函数
std::vector<uint8_t> staticize_instance(
const std::vector<uint8_t>& font_data,
FT_Face face,
int instance_index)
{
// ... 从 FreeType 获取实例的轴坐标 ...
// 创建 HarfBuzz 环境
hb_blob_t* original_blob = hb_blob_create(font_data.data(), font_data.size(), HB_MEMORY_MODE_READONLY, nullptr, nullptr);
hb_face_t* original_hb_face = hb_face_create(original_blob, 0);
hb_subset_input_t* subset_input = hb_subset_input_create_or_fail();
// 核心步骤:将实例的轴坐标“固定”到 HarfBuzz 的子集输入中
for (each_axis_in_instance) {
hb_subset_input_pin_axis_location(subset_input, original_hb_face, axis_tag, axis_value);
}
// 确保保留所有字形
hb_set_t* glyph_set = hb_subset_input_glyph_set(subset_input);
hb_set_invert(glyph_set);
// 执行子集化(静态化)操作
hb_face_t* static_hb_face = hb_subset_or_fail(original_hb_face, subset_input);
// 从生成的 HarfBuzz face 中提取二进制数据
std::vector<uint8_t> result_font_data;
if (static_hb_face) {
hb_blob_t* result_blob = hb_face_reference_blob(static_hb_face);
// ... 拷贝 blob 数据到 result_font_data ...
hb_blob_destroy(result_blob);
}
// ... 清理 HarfBuzz 和 FreeType 资源 ...
return result_font_data;
}
// 3. 主函数循环所有实例并调用 staticize_instance
int main() {
// ... 初始化 FreeType, 加载可变字体文件 ...
// ... 获取字体家族名 ...
// ... 读取原始字体文件到内存 ...
// 遍历每一个命名实例
for (const auto& [index, names] : get_variable_font_instances(face)) {
// ... 从 FreeType 的 mm_var 结构中获取每个轴的数值 (如 wght=700) ...
// ... 组合文件名 (如 NotoSansSC-Bold_w700.ttf) ...
std::vector<uint8_t> static_font = staticize_instance(original_font_data, face, index);
if (!static_font.empty()) {
// ... 保存 static_font 到文件 ...
}
}
// ... 清理 FreeType 资源 ...
}
如何编译和运行?
要编译此工具,你需要安装 FreeType2 和 HarfBuzz 库(包括 harfbuzz-subset 模块)。在 Linux/macOS 上通常可以通过包管理器安装,在 Windows 上则需要手动编译或使用预编译库。
编译示例 (使用 g++):
g++ your_code.cpp -o FreezeType -lfreetype -lharfbuzz -lharfbuzz-subset -I/path/to/freetype/include -I/path/to/harfbuzz/include -L/path/to/freetype/lib -L/path/to/harfBuzz/lib
运行:
./FreezeType
程序将自动读取你指定的字体文件,并生成一系列静态字体文件在当前目录下。
结语
FreezeType 提供了一个将可变字体的灵活性转化为静态字体确定性的有效方案。它利用了 HarfBuzz 强大的底层字体处理能力,为在不同环境下使用字体提供了更多可能性。
希望这个工具能帮助你在字体处理的道路上更进一步!如果你有任何疑问或改进建议,欢迎交流。
podofo 实现参考
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_MULTIPLE_MASTERS_H
#include FT_SFNT_NAMES_H
#include FT_TRUETYPE_TAGS_H
#include FT_TRUETYPE_IDS_H
#include <hb.h>
#include <hb-subset.h>
#include <map>
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <set>
#include <fstream>
#include <iterator>
#include <sstream>
#include <iomanip>
#include <windows.h>
// --- 编码转换工具 ---
std::string utf16_to_utf8(const wchar_t* utf16_str, size_t length) {
if (!utf16_str || length == 0) return "";
int utf8_len = WideCharToMultiByte(CP_UTF8, 0, utf16_str, static_cast<int>(length), nullptr, 0, nullptr, nullptr);
if (utf8_len <= 0) return "";
std::string utf8_str(utf8_len, 0);
WideCharToMultiByte(CP_UTF8, 0, utf16_str, static_cast<int>(length), &utf8_str[0], utf8_len, nullptr, nullptr);
utf8_str.erase(std::remove(utf8_str.begin(), utf8_str.end(), '\0'), utf8_str.end());
return utf8_str;
}
std::wstring utf16be_to_utf16(const uint8_t* src, size_t len) {
if (len % 2 != 0) return L"";
std::wstring result;
result.reserve(len / 2);
for (size_t i = 0; i < len; i += 2) {
wchar_t c = (src[i] << 8) | src[i + 1];
result.push_back(c);
}
return result;
}
std::string decode_sfnt_name(const FT_SfntName& name) {
if (name.string_len == 0) return "";
if (name.platform_id == TT_PLATFORM_MACINTOSH && name.encoding_id == TT_MAC_ID_ROMAN) {
std::string result(reinterpret_cast<const char*>(name.string), name.string_len);
result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
return result;
}
else if (name.platform_id == TT_PLATFORM_MICROSOFT || name.platform_id == TT_PLATFORM_APPLE_UNICODE) {
std::wstring utf16_str = utf16be_to_utf16(name.string, name.string_len);
return utf16_to_utf8(utf16_str.c_str(), utf16_str.length());
}
std::string result(reinterpret_cast<const char*>(name.string), name.string_len);
result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
return result;
}
// --- 文件名清理工具 ---
std::string sanitize_filename(std::string name) {
std::string illegal = "/\\?%*:|\"<> ";
for (char c : illegal) {
std::replace(name.begin(), name.end(), c, '_');
}
return name;
}
// --- 核心功能函数 ---
/**
* 获取可变字体的命名实例映射
*/
std::map<int, std::set<std::string>> get_variable_font_instances(FT_Face face) {
std::map<int, std::set<std::string>> instance_map;
if (!face || !FT_HAS_MULTIPLE_MASTERS(face)) return instance_map;
FT_MM_Var* mm_var = nullptr;
if (FT_Get_MM_Var(face, &mm_var) != 0) return instance_map;
for (FT_UInt inst_idx = 0; inst_idx < mm_var->num_namedstyles; ++inst_idx) {
FT_Var_Named_Style& instance = mm_var->namedstyle[inst_idx];
std::set<std::string> names;
FT_SfntName name_rec;
if (FT_Get_Sfnt_Name(face, instance.strid, &name_rec) == 0) {
std::string decoded = decode_sfnt_name(name_rec);
if (!decoded.empty()) names.insert(decoded);
}
if (names.empty()) names.insert("Instance_" + std::to_string(inst_idx));
instance_map[inst_idx] = names;
}
FT_Done_MM_Var(face->glyph->library, mm_var);
return instance_map;
}
/**
* 静态化核心逻辑:使用 hb_subset_input_pin_axis_location
*/
std::vector<uint8_t> staticize_instance(const std::vector<uint8_t>& font_data, FT_Face face, int instance_index) {
FT_MM_Var* mm_var = nullptr;
if (FT_Get_MM_Var(face, &mm_var) != 0) return {};
hb_blob_t* blob = hb_blob_create((const char*)font_data.data(), (unsigned int)font_data.size(), HB_MEMORY_MODE_READONLY, nullptr, nullptr);
hb_face_t* hb_face_orig = hb_face_create(blob, 0);
hb_subset_input_t* input = hb_subset_input_create_or_fail();
if (input && hb_face_orig) {
// Pin (固定) 每一个轴到实例定义的坐标
for (FT_UInt i = 0; i < mm_var->num_axis; i++) {
hb_tag_t tag = mm_var->axis[i].tag;
float value = mm_var->namedstyle[instance_index].coords[i] / 65536.0f;
hb_subset_input_pin_axis_location(input, hb_face_orig, tag, value);
}
// 保留所有字形
hb_set_t* glyphs = hb_subset_input_glyph_set(input);
hb_set_invert(glyphs);
hb_face_t* subset_face = hb_subset_or_fail(hb_face_orig, input);
if (subset_face) {
hb_blob_t* res_blob = hb_face_reference_blob(subset_face);
unsigned int len;
const char* data = hb_blob_get_data(res_blob, &len);
std::vector<uint8_t> result(data, data + len);
hb_blob_destroy(res_blob);
hb_face_destroy(subset_face);
// 清理并返回
hb_subset_input_destroy(input);
hb_face_destroy(hb_face_orig);
hb_blob_destroy(blob);
FT_Done_MM_Var(face->glyph->library, mm_var);
return result;
}
}
if (input) hb_subset_input_destroy(input);
if (hb_face_orig) hb_face_destroy(hb_face_orig);
hb_blob_destroy(blob);
FT_Done_MM_Var(face->glyph->library, mm_var);
return {};
}
// --- 主函数 ---
int main() {
FT_Library library;
FT_Face face;
if (FT_Init_FreeType(&library)) return 1;
const char* font_path = "D:/NotoSansSC-VariableFont_wght.ttf"; // 请确保路径正确
if (FT_New_Face(library, font_path, 0, &face)) {
std::cerr << "无法加载字体" << std::endl;
FT_Done_FreeType(library);
return 1;
}
// 读取原始二进制数据
std::ifstream font_file(font_path, std::ios::binary);
std::vector<uint8_t> font_data((std::istreambuf_iterator<char>(font_file)), std::istreambuf_iterator<char>());
font_file.close();
FT_MM_Var* mm_var = nullptr;
FT_Get_MM_Var(face, &mm_var);
auto instances = get_variable_font_instances(face);
std::string family = face->family_name ? face->family_name : "Font";
std::cout << "找到 " << instances.size() << " 个实例,准备导出...\n" << std::endl;
for (const auto& [index, names] : instances) {
std::string style_name = names.empty() ? "Style" : *names.begin();
// 构造包含轴数值的详细信息
std::stringstream axis_ss;
for (FT_UInt j = 0; j < mm_var->num_axis; j++) {
float val = mm_var->namedstyle[index].coords[j] / 65536.0f;
char tag[5] = { (char)(mm_var->axis[j].tag >> 24), (char)(mm_var->axis[j].tag >> 16),
(char)(mm_var->axis[j].tag >> 8), (char)(mm_var->axis[j].tag), 0 };
std::string t(tag);
// 缩写映射提升可读性
if (t == "wght") t = "w";
else if (t == "wdth") t = "wd";
axis_ss << "_" << t << (int)val;
}
std::string final_filename = sanitize_filename(family + "-" + style_name + axis_ss.str()) + ".ttf";
std::cout << "正在处理: " << style_name << " (" << axis_ss.str() << ")... ";
std::vector<uint8_t> output = staticize_instance(font_data, face, index);
if (!output.empty()) {
std::ofstream out_f(final_filename, std::ios::binary);
out_f.write((char*)output.data(), output.size());
std::cout << "[成功] -> " << final_filename << std::endl;
} else {
std::cout << "[失败]" << std::endl;
}
}
FT_Done_MM_Var(face->glyph->library, mm_var);
FT_Done_Face(face);
FT_Done_FreeType(library);
return 0;
}
960

被折叠的 条评论
为什么被折叠?



