可变字体实例提取器

好的,为你准备一份代码介绍博客文章。这篇文章会解释你的工具做什么,如何使用,以及它背后的技术原理。


告别选择困难:将可变字体(Variable Font)“冻结”为静态实例的利器

可变字体(Variable Font)是现代网页和应用设计中的一股革新力量,它允许在一个字体文件中存储无限多的样式变体,通过调整“轴”(如字重 wght、字宽 wdth)来动态生成字体效果。这极大地减少了字体文件的大小,并赋予了设计师前所未有的自由。

然而,这种灵活性并非在所有场景下都适用。在一些老旧系统、特定的排版软件,或者需要精确控制字体文件且不希望有额外“动态”特性的环境中,我们仍然需要传统的静态字体(Static Font)

这就引出了一个问题:我们能否从一个可变字体中,提取出它所有预定义的命名实例(Named Instances),并将它们“固化”为独立的静态字体文件呢?

答案是肯定的!今天,我将向大家介绍一个用 C++ 开发的小工具,它能够精准地完成这项任务。

工具简介:FreezeType (或者叫 Var2Static)

我为这个工具起名为 FreezeType,寓意是把可变字体这种“流动”的形态,“冻结”成一个个静态、独立的字体文件。

它能做什么?

FreezeType 工具的核心功能是读取一个可变字体文件(如 .ttf.otf),然后:

  1. 识别所有命名实例: 可变字体通常会内嵌一些设计师预定义的“快照”,比如“Regular”、“Bold”、“Light Condensed”等。这些被称为命名实例。
  2. 精确提取轴坐标: 对于每个命名实例,工具会从字体内部获取其对应的所有轴(例如字重 wght = 700,字宽 wdth = 100)的精确数值。
  3. 生成独立的静态字体文件: 它会将这些轴坐标“固化”到字体轮廓数据中,移除所有可变字体相关的元数据,生成一个完全独立的静态 .ttf 文件。
  4. 智能命名输出文件: 生成的静态字体文件将采用“字体家族名-样式名_轴缩写+数值.ttf”的命名格式,例如 NotoSansSC-Bold_w700_wd100.ttf,让你一目了然地知道每个文件的具体样式。
为什么需要它?
  • 兼容性: 为不支持可变字体的旧系统或软件提供兼容性方案。
  • 性能优化: 在某些特定场景下,静态字体可能比可变字体渲染更快,或者集成更容易。
  • 精确控制: 开发者可以精确地分发和管理每个特定的字体样式。
  • 备份与归档: 将可变字体的每一个重要样式作为独立的静态文件进行备份和归档。

技术揭秘:HarfBuzz Subsetter 与 FreeType2 的强强联合

FreezeType 的强大功能,离不开两个开源字体处理库:

  1. FreeType2:
  • 作用: 用于加载字体文件,解析其结构,特别是读取可变字体中的 fvar (Font Variations) 表,以获取所有**命名实例(Named Instances)**的定义及其对应的轴(wght, wdth 等)坐标。
  • 核心 API: FT_New_Face, FT_Get_MM_Var 等。
  1. HarfBuzz Subsetter (hb-subset):
  • 作用: 这是实现“静态化”的核心引擎。它是一个功能强大的字体子集化(subsetting)工具,但它最关键的能力之一是字体轴的“固定”(Axis Pinning)

  • 核心 API: hb_subset_input_pin_axis_location。通过这个函数,我们告诉 HarfBuzz 将某个轴固定到特定的数值。HarfBuzz 会自动执行以下复杂操作:

  • 将字体轮廓(glyfCFF2 表)的变异数据(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;
}
代码下载链接: https://pan.quark.cn/s/b80bd6ed2d38 USB Type-C 协议作为USB接口的最新一代标准,致力于提供更高速的数据传输速率、更强的电源传输性能以及更灵活的连接选择。官方技术文档全面解释了该协议的各个细节,为开发者和工程师提供了系统的技术参考。以下列出该协议的一些主要技术要点: 1. **双向连接特性**:Type-C 最突出的优势在于其可逆性设计,用户可以随意正反方向插入接口,从而避免了传统USB接口常见的插接错误问题。 2. **数据传输性能**:Type-C 兼容USB 3.1规范,其最高数据传输速率可达到10 Gbps(SuperSpeed USB 10标准),同时保持对USB 3.0(5 Gbps)和USB 2.0(480 Mbps)的向下兼容性。 3. **电力供应能力**:Type-C 支持USB Power Delivery (PD) 协议,其最大供电功率可达到100W,显著超越了以往的USB接口规格,足以满足笔记本电脑等高功耗设备的使用需求。PD协议通过动态协商电源供需关系,确保设备在安全的前提下高效用电。 4. **BC1.2充电标准**:Type-C 还支持Battery Charging 1.2 (BC1.2) 标准,能够为移动设备提供快速充电服务,最大电流输出可达1.5A或3A,有效提升了充电效率。 5. **EMarker芯片功能**:在Type-C线缆中,E-Marker芯片扮演着核心角色,它负责存储并传递线缆的技术参数,如数据传输速率、最大电压等级和电流容量,从而保证设备与线缆之间的精准通信。 6. **连接器结构及引脚配置**:Type-C连接器包含24个引脚,涵盖电源线路、数据...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁金金_chihiro_修行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值