Sign in

    mbt-fontsub

    纯 MoonBit 的 TrueType 字体子集化引擎:字形闭包、cmap/loca/hmtx 重建、可验证的体积压缩,附 CLI。

    font
    truetype
    sfnt
    subset
    pdf
    typography
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    10 hours ago
    Downloads
    14

    Dependencies

    #mbt-fontsub

    纯 MoonBit 实现的 TrueType 字体子集化引擎:给定字体和文本,输出只包含所需字形、 但结构与度量完全合法的子集字体。面向 PDF / 文档生成、电子书打包与 Web 字体裁剪。

    #为什么需要它

    中文字体通常有 1–3 万个字形、体积 10–20 MB。文档生成时如果整份嵌入,产物会大一个数量级。 现有 MoonBit 字体包(mizchi/font、moon_skrifa、harfbuzz、freetype 等) 都只做解析、度量或排版,没有一个能裁剪字体。mbt-fontsub 补的是这一环。

    实测:

    样本原始子集后字符 / 字形外部校验
    VT323(拉丁)153,116 B8,964 B12 / 13GDI+ 逐像素差异 0
    DengXian(中文)16,324,168 B16,804 B27 / 29GDI+ 逐像素差异 0
    SimSun(TTC 第 1 面)18,070,492 B10,320 B14 / 15本工具回读 + 校验和

    #安装与使用

    moon add Mr-Houjie/mbt-fontsub

    # 查看字体信息(含 TTC 面数) moon run cmd/main -- info tests/fixtures/VT323-Regular.ttf # 按文本子集化 moon run cmd/main -- subset tests/fixtures/VT323-Regular.ttf \ --text "Hello, MoonBit!" -o out.ttf # TTC 集合:指定面 moon run cmd/main -- subset C:/Windows/Fonts/simsun.ttc --face 1 \ --text "你好,世界" -o out.ttf # 只做自检,不写文件 moon run cmd/main -- verify tests/fixtures/VT323-Regular.ttf --text "Hello"

    输出示例:

    已写出 : out.ttf 体积 : 153116 → 8964 字节(省 94.15%) 字符 / 字形 : 12 / 13(原 587) 自检 : 通过

    #库 API

    ///|
    let font = @fontsub.read_font("tests/fixtures/VT323-Regular.ttf")
    let text = "Hello, MoonBit!"
    let chars : Array[Int] = []
    for ch in text {
    chars.push(ch.to_int())
    }
    let subset = @fontsub.subset_by_chars(font, chars)
    let report = @fontsub.verify_subset(font, subset, chars)
    assert_true(report.ok)

    API说明
    parse_font(Bytes) -> Font解析单体 TrueType,TTC 默认第 0 面
    parse_font_face(Bytes, Int) -> Font解析指定面
    ttc_face_count(Bytes) -> IntTTC 面数,单体字体返回 1
    read_font / read_font_face读文件并解析
    subset_by_text / subset_by_chars默认策略子集化
    subset_by_text_with / subset_by_chars_with带 SubsetOptions 的子集化
    verify_subset / verify_subset_with回读自检
    Font::glyph_of / has_glyph / advance_of查询映射与度量

    #扩展点

    SubsetOptions 提供三个入口,不需要改库代码:

    ///|
    let options = @fontsub.SubsetOptions::default()
    .drop_table("name") // 额外丢弃某张表
    .keep_table("GSUB") // 强制保留某张表
    .with_collector(fn(_font, _glyph) { [] }) // 为字形补充依赖(例如 GSUB 闭包)
    .with_validator(fn(_original, _subset, _chars) { ["自定义规则"] })
    let subset = @fontsub.subset_by_text_with(font, text, options)

    • 表策略:keep_table / drop_table 控制结果里保留哪些表;
    • 字形收集器:对每个已选字形调用,返回的字形继续参与闭包与编号重排;
    • 验证器:在内建检查之后追加自定义验收规则,失败信息进入 VerifyReport。

    #子集做了什么

    1. 始终保留 .notdef(glyph 0);
    2. 字符经 cmap 映射到的字形;
    3. 复合字形递归引用到的全部组件(闭包),并改写组件编号;
    4. 重写 glyf / loca / hmtx / cmap,修正 head / maxp / hhea / OS/2, post 降为格式 3;
    5. 按 OpenType 规范重新计算逐表校验和与 head.checkSumAdjustment。

    #能力边界(v0.1)

    支持:单体 TrueType 与 TTC 集合、glyf 轮廓、cmap format 4 / 12、复合字形、 BMP 与辅助平面字符。

    明确识别但暂不处理,遇到时返回 FontUnsupported(不是误判为「不是字体」):

    • CFF / CFF2 轮廓(OTTO)
    • WOFF / WOFF2 容器
    • 可变字体实例化(gvar)
    • GSUB 连字闭包(默认丢弃 GSUB/GPOS/GDEF;可用 keep_table + collector 自行处理)

    #自检与跨库验证

    moon test # 单元测试 moon run cmd/verify # 独立自检(Windows 下 moon test 嵌套进程受限时使用) moon run cmd/verify -- corpus/fonts/*.ttf # 对语料逐字体巡检

    verify_subset 会断言:字符覆盖、前进宽度、字形非空、复合组件范围、unitsPerEm、 逐表校验和、整字体校验和(0xB1B0AFBA)、体积变化与结果确定性。

    跨库对拍(需要 fonttools):

    moon run cmd/main -- subset tests/fixtures/VT323-Regular.ttf \ --text "Hello, MoonBit! 0123" -o /tmp/subset.ttf python tools/crosscheck.py tests/fixtures/VT323-Regular.ttf \ /tmp/subset.ttf "Hello, MoonBit! 0123"

    crosscheck.py 用 fontTools 以 checkChecksums=2 严格读取子集字体, 并逐字符比较 cmap、前进宽度与展开后的轮廓坐标。

    #语料

    大字体不进仓库,只提交清单(URL + SHA-256 + 许可证):

    python tools/fetch_corpus.py moon run cmd/verify -- corpus/fonts/PressStart2P-Regular.ttf corpus/fonts/ZCOOLXiaoWei-Regular.ttf

    #许可证

    MIT。测试样本 tests/fixtures/VT323-Regular.ttf 来自 Google Fonts, 按 SIL Open Font License 1.1 分发,详见 tests/fixtures/OFL.txt。

    FontError

    pub suberror FontError {
    FontTruncated
    FontNotSfnt
    FontUnsupported(String)
    FontMissingTable(String)
    FontBadTable(String)
    FontIo(String)
    }

    Font

    pub(all) struct Font {
    bytes : Bytes
    tables : Map[String, TableRecord]
    units_per_em : Int
    num_glyphs : Int
    index_to_loc_format : Int
    loca : Array[Int]
    advances : Array[Int]
    lsbs : Array[Int]
    num_h_metrics : Int
    ascender : Int
    descender : Int
    cmap : Map[Int, Int]
    cmap_formats : Array[Int]
    }

    Font::advance_of

    fn Font::advance_of(self : Font, glyph : Int) -> Int?

    字形的前进宽度;越界返回 None。

    Font::glyph_count

    fn Font::glyph_count(self : Font) -> Int

    字形总数(含 .notdef)。

    Font::glyph_length

    fn Font::glyph_length(self : Font, glyph : Int) -> Int?

    字形数据长度(loca 差值);用于判断空字形与复合字形。

    Font::glyph_of

    fn Font::glyph_of(self : Font, ch : Char) -> Int?

    字符到字形的映射;未收录时返回 None,映射到 .notdef 时返回 Some(0)。

    Font::has_glyph

    fn Font::has_glyph(self : Font, ch : Char) -> Bool

    该字符在字体中是否有真实字形(不是 .notdef)。

    Font::is_composite

    fn Font::is_composite(self : Font, glyph : Int) -> Bool raise FontError

    是否为复合字形(numberOfContours < 0)。

    Font::table_tags

    fn Font::table_tags(self : Font) -> Array[String]

    字体包含的表标签,按字典序排列。

    Font::units

    fn Font::units(self : Font) -> Int

    每 em 的单位数(常见值 1000 或 2048)。

    SubsetCase

    pub(all) struct SubsetCase {
    name : String
    text : String
    }

    SubsetOptions

    pub(all) struct SubsetOptions {
    keep_optional : Array[String]
    drop_extra : Array[String]
    collector : (Font, Int) -> Array[Int]?
    validators : Array[(Font, Font, Array[Int]) -> Array[String]]
    }

    SubsetOptions::default

    fn SubsetOptions::default() -> SubsetOptions

    SubsetOptions::drop_table

    fn SubsetOptions::drop_table(self : SubsetOptions, tag : String) -> SubsetOptions

    额外丢弃某张表。

    SubsetOptions::keep_table

    fn SubsetOptions::keep_table(self : SubsetOptions, tag : String) -> SubsetOptions

    强制保留某张表。适用于 GSUB 这类需要调用方自行处理字形编号的表。

    SubsetOptions::keeps_table

    fn SubsetOptions::keeps_table(self : SubsetOptions, tag : String) -> Bool

    tag 是否应出现在子集结果中。

    SubsetOptions::with_collector

    fn SubsetOptions::with_collector(self : SubsetOptions, collector : (Font, Int) -> Array[Int]) -> SubsetOptions

    注册自定义字形收集器。

    收集器会对每个已选字形调用一次,返回的字形会继续参与闭包与编号重排。 这为 GSUB 连字、数学字形变体等「字符之外的字形依赖」提供了扩展入口。

    SubsetOptions::with_validator

    fn SubsetOptions::with_validator(self : SubsetOptions, validator : (Font, Font, Array[Int]) -> Array[String]) -> SubsetOptions

    注册自定义验证器。

    TableRecord

    pub(all) struct TableRecord {
    tag : String
    checksum : UInt
    offset : Int
    length : Int
    }

    VerifyReport

    pub(all) struct VerifyReport {
    ok : Bool
    chars_checked : Int
    glyphs : Int
    original_size : Int
    subset_size : Int
    savings_percent : Double
    failures : Array[String]
    }

    cjk_case

    fn cjk_case() -> SubsetCase

    中文字符集,供带 CJK 字形的语料使用;字体缺字时不会强行断言。

    default_cases

    fn default_cases() -> Array[SubsetCase]

    default_optional_tables

    fn default_optional_tables() -> Array[String]

    默认保留的可选表(必需表始终重建)。

    font_error_message

    fn font_error_message(error : FontError) -> String

    hex

    fn hex(code : Int) -> String

    码点的十六进制表示,用于稳定的错误信息。

    parse_font

    fn parse_font(bytes : Bytes) -> Font raise FontError

    解析单体 TrueType 字体;如果是 TTC 集合,默认取第 0 面。

    明确区分「不是字体」和「是字体但本版本不支持」: CFF/CFF2(OTTO)、WOFF、WOFF2 会返回 FontUnsupported,而不是 FontNotSfnt。

    parse_font_face

    fn parse_font_face(bytes : Bytes, index : Int) -> Font raise FontError

    解析字体集合的第 index 面;单体字体只接受 index = 0。

    TTC 的表偏移是相对文件起点的绝对偏移,因此解析时保留整份字节, 只把表目录的基准点移到面偏移处。

    read_font

    fn read_font(path : String) -> Font raise FontError

    从磁盘读取并解析字体;IO 失败保留路径,便于定位。

    read_font_face

    fn read_font_face(path : String, index : Int) -> Font raise FontError

    从磁盘读取字体并解析指定面。

    subset_by_chars

    fn subset_by_chars(font : Font, chars : Array[Int]) -> Bytes raise FontError

    按字符集合做子集化,使用默认表策略。

    subset_by_chars_with

    fn subset_by_chars_with(font : Font, chars : Array[Int], options : SubsetOptions) -> Bytes raise FontError

    按字符集合做子集化,返回新的 TTF 字节。

    收录规则:
    1. 始终保留 .notdef(glyph 0);
    2. 字符经 cmap 映射到的字形;
    3. 这些字形通过复合引用到的所有组件(递归闭包)。

    未收录字符不会出现在新 cmap 中;调用方可以用 verify_subset 复核。

    subset_by_text

    fn subset_by_text(font : Font, text : String) -> Bytes raise FontError

    按文本做子集化,使用默认表策略。

    subset_by_text_with

    fn subset_by_text_with(font : Font, text : String, options : SubsetOptions) -> Bytes raise FontError

    按文本做子集化,并应用自定义策略。

    ttc_face_count

    fn ttc_face_count(bytes : Bytes) -> Int raise FontError

    TTC 集合中的字体数量;单体字体返回 1。

    verify_subset

    fn verify_subset(original : Font, subset_bytes : Bytes, chars : Array[Int]) -> VerifyReport

    用默认策略子集化并自检。

    verify_subset_with

    fn verify_subset_with(original : Font, subset_bytes : Bytes, chars : Array[Int], options : SubsetOptions) -> VerifyReport

    子集化自检;options.validators 中的规则会在内建检查之后执行。