- 设计系统
【免费下载链接】roboto-2
The Roboto family of fonts
导读
Fontcrunch 是随 Roboto 字体项目一起发布的 TrueType 字体轮廓精简工具(一个simplify命令),由 Raph Levien(Google)编写。它的核心使命是:用最少的 TrueType 点(on-curve / off-curve points)重建一条与原字形轮廓视觉几乎一致的样条曲线,从而显著减小字体文件体积。本文以 third_party/fontcrunch/README.md 为骨架,结合仓库内 fontcrunch.py、quadopt.cc 等源码,完整讲解 Fontcrunch 的中间文件格式、gen → make → pack 三段式工作流、penalty 精度参数,以及它被整合进 Roboto 顶层构建流程(make crunch)的实际用法。读完本文,你将能独立复现一条 "提取轮廓段 → 并行优化 → 重组字体并可视化质检" 的完整字体瘦身流水线。
一、Fontcrunch 是什么:定位与设计动机
Fontcrunch 是 Roboto 字体项目工具链中用于TrueType 样条优化的专用工具。TrueType 字形轮廓由二次贝塞尔曲线(quadratic bezier)组成,其点数直接决定字体内glyf表的存储大小。Fontcrunch 的优化思路是:
- 对每条轮廓曲线,寻找一个点数最少的二次贝塞尔分段序列;
- 使该序列与原曲线在距离与切线角度两个维度上足够接近,保持视觉一致;
- 以 "足够小" 的误差换取 "尽量少" 的点数,最终达到文件体积瘦身的效果。
该工具一个值得一提的设计亮点,也是 README 明确标注的特性,是把两个 off-curve 点之间通过插值得到的 on-curve 点视为"免费"的(counting on-curve points interpolated between two off-curve points as "free"):
在 TrueType 中,若一段二次贝塞尔的两个相邻控制点都是 off-curve 点,则它们之间隐含一个 on-curve 点(即两段曲线的连接点,由中点插值自动导出,无需显式存储)。Fontcrunch 在评分时对这一类点不额外增加 penalty,从而可以比常规简化方案多省出可观的文件字节。
这一特性贯穿于 Python 与 C++ 两侧的评分逻辑中,详见下文第四节与第六节。
二、依赖、许可证与本地化改造
2.1 依赖关系
Fontcrunch 有两个层面的依赖:
- 直接依赖:
fonttools(用于读写 TTF 文件、遍历glyf表),本仓库 requirements.txt 中锁定版本为fonttools==3.13.0; - 遗留依赖:Spiro 0.01 中的曲线数学库(
fromcubic、tocubic、pcorn等),位于仓库 third_party/spiro/curves 目录下。在 fontcrunch.py 的开头可以看到:
sys.path.append( os.path.join(os.path.dirname(__file__), os.pardir, 'spiro', 'curves')) from fontTools import ttLib from fontTools.ttLib.tables import _g_l_y_f import fromcubic import tocubic import pcorn即:把上一级目录spiro/curves加入sys.path,再从其中导入三次/二次贝塞尔转换工具(fromcubic、tocubic)与分段 clothoid(欧拉螺线)曲线表示(pcorn)。
2.2 许可证
- Fontcrunch 本身以Apache License v2发布(见 third_party/fontcrunch/LICENSE);
- Spiro 曲线代码为GNU GPL v2 或更高版本;
- Spiro 曲线受美国专利保护("Spiro curves are subject to a US patent")。
这一点在引入和使用该工具时需特别注意:Apache v2 的代码可以自由使用,但 Spiro 相关的曲线算法存在 GPL 与专利双重约束,商业分发前应做合规评估。
2.3 仓库内的本地化改造
README.third_party 记录了从上游 fontbakery 引入时做的六处本地修改,有助于理解当前仓库版本与上游的差异:
- 仅从 fontbakery 引入
fontcrunch目录; .bz/.bzopt文件被重命名为.bez/.bezopt,避免与 bzip 压缩文件混淆(这一点也解释了 README 中"populate them with lots of files with .bz extension"与当前仓库 Makefile 使用*.bez的差异——仓库里实际运行的是重命名后的.bez体系);- 修改 Python 路径,使
fontcrunch.py能在 Noto 的构建环境中运行; fontcrunch.py改为按 fonttools 推荐的方式加载字形;- 强制 gcc 以 C++11 模式编译,保证
quadopt可构建; make clean扩展为同时删除临时目录。
三、中间文件格式与分段规则
3.1 目录结构与.bez文件
README 描述了完整的中间数据组织方式:
创建 256 个名为
00..ff的目录,并在其中填充大量带.bez扩展名的文件。每个文件是字体中切割出的一段非平凡(nontrivial)二次贝塞尔段,每行一条贝塞尔曲线,格式为x0 y0 x1 y1 x2 y2。直线用两端点的中点(x1, y1)表示。
这与 fontcrunch.py 中的seg_fn实现完全对应:
USE_SUBDIRS = True def seg_fn(segstr): fn = md5.new(segstr).hexdigest()[:16] if USE_SUBDIRS: dirname = fn[:2] if not os.path.exists(dirname): os.mkdir(dirname) fn = dirname + '/' + fn[2:] fn += '.bez' return fn要点:
- 每个段文件的文件名取段内容字符串的 MD5 前 16 位,因此内容完全相同的段(在不同字形、不同字体中反复出现)会被自动去重、只保存一份,这是该方案天然具备的跨字体复用能力;
- 文件写入
xx/两级目录(xx为 MD5 前两位),即 README 所说的 256 个目录00..ff; - 文件内容逐行存储一段二次贝塞尔:
x0 y0 x1 y1 x2 y2; - 直线不单独存储为两个端点,而是把控制点
(x1, y1)写成两个端点的中点,读取方据此识别(见read_bzs中if bz[1] == lerppt(0.5, bz[0], bz[2])的判断逻辑)。
3.2 什么是"非平凡段"
gen阶段并非把整个字形轮廓原样导出,而是先把轮廓切分为多个段,仅导出其中"非平凡"的部分(见 fontcrunch.py 的gen_segs):
if bk1 != (bk0 + 1) % len(sp) or len(sp[bk0]) != 2: segstr = seg_to_string(sp, bk0, bk1) fn = seg_fn(segstr) file(fn, 'w').write(segstr)即:单段、且本身就是直线的轮廓部分不需要优化,直接跳过;只有需要多条贝塞尔才能表达的段才进入.bez优化池。段的切分依据由 segment_sp 完成,它检测两类断点:
- 方向变化点(
x/y方向分量符号翻转处); - 角度突变点(相邻两段夹角超过约 0.02 弧度,即约 1.15°,或出现退化控制点)。
四、三段式工作流:gen → make → pack
README 给出的完整流程是三条命令,配合一个精度参数调整,即可完成一次字体瘦身:
4.1 第一步:生成段文件(gen)
python fontcrunch.py gen yourfont.ttf该命令遍历字体glyf表中所有字形(generate),对每个非复合字形调用gen_segs,把轮廓段切分、去重后写入xx/*.bez文件(见第三节)。glyph_to_bzs(fontcrunch.py)负责把 fonttools 读取的glyf坐标/标志位还原为二次贝塞尔序列,其间会:
- 通过
lerppt(0.5, ...)把两个连续 off-curve 点之间的隐含 on-curve 点补算出来; - 把退化的二次曲线(控制点与端点重合)当作直线处理。
4.2 第二步:并行优化(make)
make -j16 # 或根据你机器的核数选择并行度这一步驱动 third_party/fontcrunch/Makefile:
SRC = $(wildcard */*.bez) OPT = $(patsubst %.bez, %.bezopt, $(SRC)) dummy: $(OPT) quadopt: quadopt.cc $(CXX) $< -std=c++0x -O3 -Werror -Wall -o $@ %.bezopt: %.bez quadopt ./quadopt $< $@- 首先把 quadopt.cc 编译为 C++ 可执行文件(
-O3优化、-std=c++0x即 C++11 模式); - 然后对
*/*.bez中的每一个段文件运行./quadopt in out,产出对应的.bezopt优化结果; - 由于段文件彼此独立,
make天然支持-j并行;README 给出的经验数据是"在较快的机器上约每秒处理 5 个字形,视轮廓复杂度而定"(即约 0.2 秒/字形,复杂轮廓更慢)。
4.3 第三步:重组字体与质量预览(pack)
python fontcrunch.py pack yourfont.ttf > /tmp/outlines.ps newfont.ttf注意 README 原始命令中的写法,实际含义是:将字体输出重定向到 PostScript 预览文件,同时把重组后的字体保存为newfont.ttf。对应的 repack 与main入口(fontcrunch.py):
def main(argv): if argv[1] == 'gen': generate(sys.argv[2]) elif argv[1] == 'pack': repack(sys.argv[2], sys.argv[3] if len(argv) >= 3 else None)repack对每个非复合字形执行repack_glyph(fontcrunch.py):
- 重新切分轮廓段;
- 对每个非平凡段,按其
seg_fn的 MD5 找到对应的.bezopt优化结果并读取; - 通过 bzs_to_glyph 把优化后的贝塞尔序列写回
glyf(把 off-curve 控制点标记为 flag 0、on-curve 点标记为 flag 1,并重新生成endPtsOfContours); - 调用
plot_tt(newbzs, bzs, style='redblack')生成 PostScript 对比预览(详见下一节)。
这里有一个重要的前提:pack依赖gen生成的 MD5 命名体系,因此必须在同一目录下先gen后make,再pack,三者共享同一套00..ff目录。make clean(见 Makefile 第 28-32 行)会删除quadopt、所有.bez/.bezopt与空目录,用于重置中间状态。
五、精度控制:penalty 参数
README 明确指出精度控制方式:
你可以通过修改代码中的 "penalty" 来控制精度(理想情况下它应该是一个参数)。
在 fontcrunch.py 中:
penalty = 0.05penalty 代表"每多出一个点"所需付出的代价(即点数项的权重),它参与所有候选解的评分(MiniState.combine):
def combine(self, score, bz): newscore = self.score + score + penalty * (len(bz) - 1) if len(bz) == 3 and len(self.sp): lastbz = self.sp[-1] if len(lastbz) == 3: if lerppt(0.5, lastbz[1], bz[1]) == bz[0]: newscore -= penalty return MiniState(newscore, self.sp + [bz])评分由两部分构成:
- 拟合误差项:当前候选贝塞尔与原始曲线在弧长上累积的距离/角度偏差(由
measure_bz用四阶 Runge-Kutta 数值积分计算,见 tocubic.py 的measure_bz_rk4); - 点数惩罚项:
penalty * (len(bz) - 1),每引入一个新控制点(off-curve)就加一份 penalty; - "免费点"折扣:当新段的控制点
bz[1]恰好落在前一段控制点lastbz[1]与自身端点bz[0]的中点插值上时(lerppt(0.5, lastbz[1], bz[1]) == bz[0]),即该 on-curve 点可由两个 off-curve 点自动导出、无需显式存储,于是把刚加的 penalty 减回去——这正是 README 所述"免费的插值 on-curve 点"在代码中的具体实现。
调参建议(基于代码语义的推断):penalty 越大,算法越倾向于少用点(文件更小、精度更低);penalty 越小,算法越倾向于保留更多点(更贴近原始轮廓)。默认0.05是作者在文件体积与视觉质量之间取的平衡值,实际调优时应配合第五节的可视化对比逐字形检查。
六、C++ 优化器 quadopt:核心算法原理
make阶段真正执行优化的程序是 quadopt.cc。它的输入输出与.bez格式完全一致(x0 y0 x1 y1 x2 y2,每行一段二次贝塞尔),内部流程如下。
6.1 弧长参数化(Thetas)
Thetas::init(quadopt.cc)先把输入的贝塞尔序列重采样为按弧长均匀分布的采样点(位置xy与单位切向dir),使后续所有拟合工作都在弧长坐标s下进行。每条贝塞尔的弧长本身由Quad::arclen通过四阶 Runge-Kutta 积分(rk4<1>,见 quadopt.cc)求得,每段内部再细分为 100 步采样。
6.2 断点提取与半像素网格(findBreaks)
findBreaks(quadopt.cc)在曲线弧长上以 1/10 弧长为步长扫描,找到所有"舍入后位置发生变化"的点作为候选断点。关键在于HALF_STEP宏(quadopt.cc):
#define HALF_STEP 1 ... #if HALF_STEP Point p(.5 * std::round(2 * origp.x), .5 * std::round(2 * origp.y)); #else Point p = round(origp); #endif即新轮廓的端点被约束在0.5 单位(半像素)网格上——TrueType 提示(hinting)对整数网格点最友好,半像素网格在"允许更多候选位置"与"保持点坐标规整"之间取了折中。README 提到的"直线用中点表示"在这里也再次体现:isLine()判断p[1] == lerp(0.5, p[0], p[2])。
6.3 候选生成:直线与二次贝塞尔
tryLineQuad(quadopt.cc)为每对断点生成两类候选:
- 直线:仅当两端点均为整数坐标时才允许(对 hinting 友好);
- 二次贝塞尔:通过两端点切线方向求交点(
intersect),把交点舍入到整数作为控制点p[1],并要求满足okForHalf(起点在整数网格上,或该点可由相邻 off-curve 点免费导出)。
6.4 动态规划评分
optimize(quadopt.cc)是整个算法的核心,采用动态规划(Viterbi 式的前向搜索):
- 先用"整段直线或整段二次曲线"的粗解试探,若
score <= 3 * penalty直接收工(对应 Python 侧optimize_run的早期退出,fontcrunch.py); - 否则枚举所有"两段二次曲线"的组合,若
score <= 4 * penalty收工(quadopt.cc); - 仍不满意则进行完整的多段动态规划:
states[i]保存"优化到第 i 个断点的最优状态",Statelet::combine(quadopt.cc)负责状态转移,其中再次实现"免费点"折扣:
double pmul = 2; if (newq.isLine()) { pmul = 1; } else if (newprev != 0 && !newprev->q.isLine() && lerp(0.5, newprev->q.p[1], newq.p[1]) == newq.p[0]) { pmul = 1; } score = (newprev == 0 ? 0 : newprev->score) + penalty * pmul + newscore;可见:直线点 penalty 系数为 1;普通 off-curve 点系数为 2;可由两个 off-curve 点免费导出的 on-curve 点系数降为 1,与 Python 侧逻辑一一对应。最后沿prev指针回溯得到最优分段序列并逆序输出(std::reverse)。
拟合误差项由MeasureFunctor(quadopt.cc)计算:按弧长比例把候选曲线的每点映射回原始曲线,累积距离误差(dist2,即平方距离)与切线角度误差,分别乘以dist_factor = .005与angle_factor = 5后相加(quadopt.cc)。角度误差采用叉积近似(小角度下即弧度值),并用 RK4 沿弧长积分。
6.5 与 Python 侧的关系
Python 版 optimize_run 与 C++ 版optimize是同一算法的两种实现:Python 版保留更多可读性,且引入了"早期剪枝"(当某段误差超过10 * penalty且连续 20 次时提前终止内层扫描,见 fontcrunch.py);C++ 版则是生产环境实际使用的加速实现。二者共享同样的"免费点"与 penalty 语义。
七、质量检查:PostScript 轮廓对比预览
pack阶段会同时输出一段 PostScript(默认重定向到/tmp/outlines.ps),用于人工核对优化结果与原始轮廓的重合度。plot_tt(fontcrunch.py)支持两种对比样式:
redcyan:原始轮廓画成青色(cyan),优化后轮廓画成红色(red),叠加套印(setoverprint),偏差处会呈现明显的颜色分离;redblack:原始轮廓青色、新轮廓黑色,适合黑白打印检查。
在repack_glyph中默认使用redblack(fontcrunch.py)。渲染方式是把 TrueType 二次贝塞尔升阶为三次贝塞尔后逐段输出 PostScript 路径(raise_to_cubic、plot_bzs,见 fontcrunch.py),控制点也以实心/空心圆点标出,便于检查"免费点"是否被正确利用。用任意 PostScript 查看器(如gv)或转成 PDF 即可逐字形检查简化质量。
八、在 Roboto 构建流水线中的真实用法
Fontcrunch 并非孤立的第三方工具,而是被直接接入了本仓库顶层 Makefile 的crunch目标:
crunch: mkdir -p out/crunched cd third_party/fontcrunch && \ for source in ../../out/RobotoTTF/*.ttf ../../out/RobotoCondensedTTF/*.ttf; do \ python fontcrunch.py gen $$source; \ done && \ $(MAKE) -j8 && \ for source in ../../out/RobotoTTF/*.ttf ../../out/RobotoCondensedTTF/*.ttf; do \ python fontcrunch.py pack $$source ../../out/crunched/$$(basename $$source) >/dev/null; \ done从中可以读出 Roboto 项目实际使用的完整参数:
- 输入字体:
out/RobotoTTF/*.ttf(常规 Roboto 各字重)与out/RobotoCondensedTTF/*.ttf(Condensed 字重)全部参与精简; - 并行度:实际构建采用
make -j8(README 示例为-j16,按机器核数自定); - 输出:精简后的字体统一写入
out/crunched/目录,同名保存; pack阶段的重定向从> /dev/null丢弃了 PostScript 预览——说明在正式构建中预览被刻意关闭,仅保留优化后的字体文件;若要质检,可参考 README 的写法单独重定向到/tmp/outlines.ps。
精简后的out/crunched/*.ttf会继续流向后续目标(如android目标的touchup_for_android.py与 subset 处理,见 Makefile),可见 Fontcrunch 处于 Roboto 发布管线中"去 hint 字体 → 精简 → 平台化处理"的中间环节。
九、操作要点与注意事项小结
- 三段必须按序执行:
gen建立 MD5 段库 →make产出.bezopt→pack回写字形;跳过任一步骤会导致段文件缺失或 MD5 不匹配; - 工作目录约定:三步骤都应在
third_party/fontcrunch目录内执行,段文件落在该目录下的00..ff子目录中(顶层crunch目标已处理好cd); - 精度调整:修改 fontcrunch.py 中的
penalty = 0.05(README 注明"理想情况下应做成命令行参数",当前版本为硬编码);C++ 侧另有dist_factor/angle_factor(quadopt.cc)控制距离与角度误差的权重; - 运行环境:本仓库依赖 Python 2 与
fonttools==3.13.0(见 requirements.txt),README 顶层文档亦注明"currently requires Python 2, not 3";C++ 侧需支持 C++11 的编译器; - 输出检查:用
pack ... > /tmp/outlines.ps newfont.ttf的写法保留 PostScript 预览,逐字形对比原轮廓与新轮廓; - 合规提醒:Fontcrunch 本体为 Apache v2,但其依赖的 Spiro 曲线代码为 GPL v2+ 且涉及美国专利,商用前需评估。
十、扩展阅读指引
- 优化器实现:fontcrunch.py(Python 版评分、分段、MD5 去重、TTF 读写)与 quadopt.cc(C++ 版动态规划、弧长参数化、半像素网格);
- 构建规则:third_party/fontcrunch/Makefile(
.bez→.bezopt规则与clean行为); - 本地化改动记录:README.third_party(
.bz→.bez重命名、Python 路径与 C++11 修改等六项说明); - 许可证:third_party/fontcrunch/LICENSE(Apache v2);
- 曲线数学依赖:third_party/spiro/curves/tocubic.py(弧长 RK4 积分、
measure_bz_rk4)、third_party/spiro/curves/pcorn.py(分段 clothoid 曲线表示); - 集成实例:顶层 Makefile 的
crunch目标,展示 Roboto 全字重字体的真实精简流程。
- 设计系统
【免费下载链接】roboto-2
The Roboto family of fonts
相关推荐
Seedance 2.0 Masthead 构建链:密封式字形轮廓工具链与主题化 SVG 双主题渲染
Seedance 2.0 Masthead 构建链:密封式字形轮廓工具链与主题化 SVG 双主题渲染 导读 docs/MASTHEAD_BUILD.md 描述的
AI 技能AI 评测提示工程人工智能媒体生成在 macOS 上构建 Ceph:Homebrew 依赖、LLVM 工具链与 CMake 精简配置实战
在 macOS 上构建 Ceph:Homebrew 依赖、LLVM 工具链与 CMake 精简配置实战 导读 Ceph 是分布式对象、块与文件存储平台,其官方构
存储分布式文件系统对象存储后端高可用Lynx LLVH 解析:Lepus IR 中精简 LLVM 工具子集的集成与构建原理
Lynx LLVH 解析:Lepus IR 中精简 LLVM 工具子集的集成与构建原理 LLVH(Lepus LLVM Helpers)是 Lynx 开源仓库中
跨平台移动开发前端桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考