可变字体实例提取器

可变字体实例提取器 文章目录告别选择困难将可变字体Variable Font“冻结”为静态实例的利器工具简介FreezeType (或者叫 Var2Static)它能做什么为什么需要它技术揭秘HarfBuzz Subsetter 与 FreeType2 的强强联合代码概览简化版如何编译和运行结语podofo 实现参考好的为你准备一份代码介绍博客文章。这篇文章会解释你的工具做什么如何使用以及它背后的技术原理。告别选择困难将可变字体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等坐标。核心 APIFT_New_Face,FT_Get_MM_Var等。HarfBuzz Subsetter (hb-subset)作用这是实现“静态化”的核心引擎。它是一个功能强大的字体子集化subsetting工具但它最关键的能力之一是字体轴的“固定”Axis Pinning。核心 APIhb_subset_input_pin_axis_location。通过这个函数我们告诉 HarfBuzz 将某个轴固定到特定的数值。HarfBuzz 会自动执行以下复杂操作将字体轮廓glyf或CFF2表的变异数据deltas应用到基本轮廓上。移除所有可变字体相关的表如fvar,gvar,avar,STAT。修正字体度量如hmtx表确保字距在静态化后依然准确。构建一个完全符合 OpenType 规范的静态字体文件。优势使用hb-subset进行轴固定比手动解析和修改字体表要安全、高效且不易出错。它处理了大量的底层细节确保生成字体文件的完整性和正确性。代码概览简化版为了便于理解我们来看一下核心逻辑的简化版。完整的代码可以在文末获取。// 1. 获取可变字体实例信息std::mapint,std::setstd::stringget_variable_font_instances(FT_Face face);// 2. 静态化核心函数std::vectoruint8_tstaticize_instance(conststd::vectoruint8_tfont_data,FT_Face face,intinstance_index){// ... 从 FreeType 获取实例的轴坐标 ...// 创建 HarfBuzz 环境hb_blob_t*original_blobhb_blob_create(font_data.data(),font_data.size(),HB_MEMORY_MODE_READONLY,nullptr,nullptr);hb_face_t*original_hb_facehb_face_create(original_blob,0);hb_subset_input_t*subset_inputhb_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_sethb_subset_input_glyph_set(subset_input);hb_set_invert(glyph_set);// 执行子集化静态化操作hb_face_t*static_hb_facehb_subset_or_fail(original_hb_face,subset_input);// 从生成的 HarfBuzz face 中提取二进制数据std::vectoruint8_tresult_font_data;if(static_hb_face){hb_blob_t*result_blobhb_face_reference_blob(static_hb_face);// ... 拷贝 blob 数据到 result_font_data ...hb_blob_destroy(result_blob);}// ... 清理 HarfBuzz 和 FreeType 资源 ...returnresult_font_data;}// 3. 主函数循环所有实例并调用 staticize_instanceintmain(){// ... 初始化 FreeType, 加载可变字体文件 ...// ... 获取字体家族名 ...// ... 读取原始字体文件到内存 ...// 遍历每一个命名实例for(constauto[index,names]:get_variable_font_instances(face)){// ... 从 FreeType 的 mm_var 结构中获取每个轴的数值 (如 wght700) ...// ... 组合文件名 (如 NotoSansSC-Bold_w700.ttf) ...std::vectoruint8_tstatic_fontstaticize_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-oFreezeType-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 实现参考#includeft2build.h#includeFT_FREETYPE_H#includeFT_MULTIPLE_MASTERS_H#includeFT_SFNT_NAMES_H#includeFT_TRUETYPE_TAGS_H#includeFT_TRUETYPE_IDS_H#includehb.h#includehb-subset.h#includemap#includestring#includevector#includeiostream#includealgorithm#includeset#includefstream#includeiterator#includesstream#includeiomanip#includewindows.h// --- 编码转换工具 ---std::stringutf16_to_utf8(constwchar_t*utf16_str,size_t length){if(!utf16_str||length0)return;intutf8_lenWideCharToMultiByte(CP_UTF8,0,utf16_str,static_castint(length),nullptr,0,nullptr,nullptr);if(utf8_len0)return;std::stringutf8_str(utf8_len,0);WideCharToMultiByte(CP_UTF8,0,utf16_str,static_castint(length),utf8_str[0],utf8_len,nullptr,nullptr);utf8_str.erase(std::remove(utf8_str.begin(),utf8_str.end(),\0),utf8_str.end());returnutf8_str;}std::wstringutf16be_to_utf16(constuint8_t*src,size_t len){if(len%2!0)returnL;std::wstring result;result.reserve(len/2);for(size_t i0;ilen;i2){wchar_tc(src[i]8)|src[i1];result.push_back(c);}returnresult;}std::stringdecode_sfnt_name(constFT_SfntNamename){if(name.string_len0)return;if(name.platform_idTT_PLATFORM_MACINTOSHname.encoding_idTT_MAC_ID_ROMAN){std::stringresult(reinterpret_castconstchar*(name.string),name.string_len);result.erase(std::remove(result.begin(),result.end(),\0),result.end());returnresult;}elseif(name.platform_idTT_PLATFORM_MICROSOFT||name.platform_idTT_PLATFORM_APPLE_UNICODE){std::wstring utf16_strutf16be_to_utf16(name.string,name.string_len);returnutf16_to_utf8(utf16_str.c_str(),utf16_str.length());}std::stringresult(reinterpret_castconstchar*(name.string),name.string_len);result.erase(std::remove(result.begin(),result.end(),\0),result.end());returnresult;}// --- 文件名清理工具 ---std::stringsanitize_filename(std::string name){std::string illegal/\\?%*:|\ ;for(charc:illegal){std::replace(name.begin(),name.end(),c,_);}returnname;}// --- 核心功能函数 ---/** * 获取可变字体的命名实例映射 */std::mapint,std::setstd::stringget_variable_font_instances(FT_Face face){std::mapint,std::setstd::stringinstance_map;if(!face||!FT_HAS_MULTIPLE_MASTERS(face))returninstance_map;FT_MM_Var*mm_varnullptr;if(FT_Get_MM_Var(face,mm_var)!0)returninstance_map;for(FT_UInt inst_idx0;inst_idxmm_var-num_namedstyles;inst_idx){FT_Var_Named_Styleinstancemm_var-namedstyle[inst_idx];std::setstd::stringnames;FT_SfntName name_rec;if(FT_Get_Sfnt_Name(face,instance.strid,name_rec)0){std::string decodeddecode_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);returninstance_map;}/** * 静态化核心逻辑使用 hb_subset_input_pin_axis_location */std::vectoruint8_tstaticize_instance(conststd::vectoruint8_tfont_data,FT_Face face,intinstance_index){FT_MM_Var*mm_varnullptr;if(FT_Get_MM_Var(face,mm_var)!0)return{};hb_blob_t*blobhb_blob_create((constchar*)font_data.data(),(unsignedint)font_data.size(),HB_MEMORY_MODE_READONLY,nullptr,nullptr);hb_face_t*hb_face_orighb_face_create(blob,0);hb_subset_input_t*inputhb_subset_input_create_or_fail();if(inputhb_face_orig){// Pin (固定) 每一个轴到实例定义的坐标for(FT_UInt i0;imm_var-num_axis;i){hb_tag_t tagmm_var-axis[i].tag;floatvaluemm_var-namedstyle[instance_index].coords[i]/65536.0f;hb_subset_input_pin_axis_location(input,hb_face_orig,tag,value);}// 保留所有字形hb_set_t*glyphshb_subset_input_glyph_set(input);hb_set_invert(glyphs);hb_face_t*subset_facehb_subset_or_fail(hb_face_orig,input);if(subset_face){hb_blob_t*res_blobhb_face_reference_blob(subset_face);unsignedintlen;constchar*datahb_blob_get_data(res_blob,len);std::vectoruint8_tresult(data,datalen);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);returnresult;}}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{};}// --- 主函数 ---intmain(){FT_Library library;FT_Face face;if(FT_Init_FreeType(library))return1;constchar*font_pathD:/NotoSansSC-VariableFont_wght.ttf;// 请确保路径正确if(FT_New_Face(library,font_path,0,face)){std::cerr无法加载字体std::endl;FT_Done_FreeType(library);return1;}// 读取原始二进制数据std::ifstreamfont_file(font_path,std::ios::binary);std::vectoruint8_tfont_data((std::istreambuf_iteratorchar(font_file)),std::istreambuf_iteratorchar());font_file.close();FT_MM_Var*mm_varnullptr;FT_Get_MM_Var(face,mm_var);autoinstancesget_variable_font_instances(face);std::string familyface-family_name?face-family_name:Font;std::cout找到 instances.size() 个实例准备导出...\nstd::endl;for(constauto[index,names]:instances){std::string style_namenames.empty()?Style:*names.begin();// 构造包含轴数值的详细信息std::stringstream axis_ss;for(FT_UInt j0;jmm_var-num_axis;j){floatvalmm_var-namedstyle[index].coords[j]/65536.0f;chartag[5]{(char)(mm_var-axis[j].tag24),(char)(mm_var-axis[j].tag16),(char)(mm_var-axis[j].tag8),(char)(mm_var-axis[j].tag),0};std::stringt(tag);// 缩写映射提升可读性if(twght)tw;elseif(twdth)twd;axis_ss_t(int)val;}std::string final_filenamesanitize_filename(family-style_nameaxis_ss.str()).ttf;std::cout正在处理: style_name (axis_ss.str())... ;std::vectoruint8_toutputstaticize_instance(font_data,face,index);if(!output.empty()){std::ofstreamout_f(final_filename,std::ios::binary);out_f.write((char*)output.data(),output.size());std::cout[成功] - final_filenamestd::endl;}else{std::cout[失败]std::endl;}}FT_Done_MM_Var(face-glyph-library,mm_var);FT_Done_Face(face);FT_Done_FreeType(library);return0;}