toolgarden.xyz
EN
浏览器工具开发字体裁剪fonteditor-coreWeb 字体前端工程

怎么实现在线字体裁剪?基于 fonteditor-core 的浏览器方案

从 TTF/WOFF 解析、Unicode 去重、字体表裁剪到预览和下载,完整实现一个无需上传文件的在线字体子集工具。

ToolGarden 推荐的工具优先在浏览器本地运行,文件和文本不必上传到服务器,适合更注重安全隐私的日常处理。

发布于 2026年7月22日约 11 分钟阅读作者 ToolGarden

中文、日文和韩文字体往往包含成千上万个字形,但一个落地页可能只用到一句标题和几个按钮。字体裁剪就是删除未使用字形,只让浏览器下载页面真正需要的字符。

一个可用的在线字体裁剪工具,并不是给字体库外面套一个文件上传框。它还要正确处理 Unicode、判断字体格式、解压和压缩 WOFF、报告缺失字形、隔离预览字体、释放 Object URL,并限制内存占用。下面这套方案来自 fonteditor-core 与 fflate 的浏览器本地实现。

字体裁剪到底裁掉了什么?

字体文件不仅包含字形轮廓,还包含字符映射、度量、命名、hinting、kerning 等表。子集化会保留目标 Unicode 码点对应的字形,并重写相关字体表。它和普通压缩不是一回事:ZIP 或 Brotli 只能压缩传输字节,不能从解码后的字体中删除几千个没用到的汉字。

收益最明显的场景通常是 CJK 大字库、图标字体、活动专题字体和只用于品牌标题的字体。如果拉丁字体本来就很小,或者必须保留 kerning、hinting 等表,体积下降可能没有想象中大。

输入字体场景字符来源常见输出
营销页标题字体最终标题和按钮文案单页使用的小体积 WOFF
中文 UI 字体产品文案与允许的标点按语言拆分的 WOFF
图标字体当前前端包真正使用的图标裁剪后的图标字体
文档内嵌字体从目标文档提取的字符便于携带的 TTF 子集

先设计浏览器本地处理链路

页面只负责选文件、设置选项、显示进度、预览和错误;工具函数接收 File 与明确参数,返回判别联合结果。把字体解析从 React 组件中移出去后,失败分支更容易测试,转换逻辑也不会被 UI state 绑死。

普通字体裁剪没有上传文件和字符文本的必要。File.arrayBuffer 在当前标签页读取用户选择的文件,fonteditor-core 完成解析与重写,结果再包装成 Blob 供预览和下载。

  • 读取完整文件前先检查字节大小和格式。
  • 同时参考 MIME 与扩展名,因为浏览器经常不给字体文件填写 MIME。
  • 返回 unsupported_input、empty_chars、parse_failed、no_glyphs 等明确错误码。
  • 设置最大输入体积,避免误选超大字体集合导致标签页内存耗尽。

按 Unicode 码点收集字符,不要直接 split

JavaScript 字符串底层使用 UTF-16。基本多文种平面之外的字符可能由一对代理项组成,text.split('') 会把一个字符拆成两个无效单元。应该使用 for...of 或 Array.from 遍历,再调用 codePointAt(0),最后用 Set 对数字码点去重。

C0、C1 控制字符可以忽略,但不要粗暴删除所有空白;普通空格本身也是字体需要保留的字形。源字体不存在的字符也应明确列出,不能只靠浏览器回退字体制造“看起来都支持”的假象。

用 fonteditor-core 生成字体子集

fonteditor-core 可以解析 TTF、WOFF,按 Unicode 码点选择字形,再写出 TTF 或 WOFF。WOFF 的字体表使用压缩数据,因此需要用 fflate 提供 inflate 与 deflate 适配器。解析阶段和写出阶段的 hinting、kerning 参数应保持一致。

import { createFont } from 'fonteditor-core';
import { deflateSync, inflateSync } from 'fflate';

const codePoints = [...new Set(
  Array.from(text, char => char.codePointAt(0)!)
)];

const input = await file.arrayBuffer();
const font = createFont(input, {
  type: 'ttf',
  subset: codePoints,
  hinting: false,
  kerning: false,
  inflate: data => Array.from(inflateSync(new Uint8Array(data))),
});

const output = font.write({
  type: 'woff',
  deflate: data => Array.from(deflateSync(new Uint8Array(data))),
});
  • 网页字体如果不依赖小字号屏显优化,可默认关闭 hinting 以减小体积。
  • 排版质量敏感时保留 kerning,但要用真实字体对比体积差异。
  • 输出文件名保持可预测,例如 brand.subset.woff。
  • WOFF2 是另一项能力;这条 fonteditor-core 链路输出的是 WOFF,不是 WOFF2。

验证字符覆盖率,而不是只判断写出成功

文件成功生成,不代表请求字符全部存在。应读取结果字形上的 Unicode 映射,与请求码点逐一比对,分别返回已包含字符和缺失字符。这样用户看到的是可操作的检测结果,而不是一块可能偷偷使用系统回退字体的预览区。

界面可以展示原始体积、输出体积、节省字节、节省比例、请求字符数、命中字符数和字形总数。对本来很小的字体,或者转换为开销不同的格式,输出甚至可能变大,因此不要承诺“裁剪后一定更小”。

用临时字体预览,并正确释放资源

生成结果不必上传也能预览。把 ArrayBuffer 包装为 Blob,创建 Object URL,再通过 FontFace 或动态 @font-face 加载,并给每次结果分配唯一 font-family。预览内容最好只使用确认已经包含的字符,避免系统回退字体掩盖缺字。

const blob = new Blob([output], { type: 'font/woff' });
const url = URL.createObjectURL(blob);

const face = new FontFace('SubsetPreview', `url(${url})`);
await face.load();
document.fonts.add(face);

// Revoke the previous URL when regenerating or leaving the page.
URL.revokeObjectURL(url);
  • 重新生成前先 revoke 上一个 Object URL。
  • 预览不再使用时,从 document.fonts 中移除对应 FontFace。
  • 每份结果使用唯一字体名,避免浏览器缓存造成预览错乱。
  • 下载完成后立即移除临时 a 标签。

上线前必须考虑的字体边界

可变字体、彩色 Emoji、OpenType 布局规则、连字和复杂文字塑形,都比“一个码点对应一个字形”复杂。某段文本可能依赖没有直接 Unicode 映射的替代字形。正式上线前应使用目标语言、目标浏览器和真实字体授权做验证。

对于静态网站,构建阶段使用成熟字体工具通常更容易复现。在线裁剪适合探索、一次性素材、内部工作流和不方便上传的字体文件,但不能取代字体授权与排版质量检查。

总结

可靠的在线字体裁剪需要同时做好 Unicode 遍历、类型化校验、真实字形覆盖检测、字体表选项和 Blob 生命周期管理。把解析写进独立工具层,把缺字明确展示出来,并用复杂字体做真实测试,才能让它从演示代码变成可用工具。

常见问题

Q.字体裁剪和字体压缩是一回事吗?

不是。裁剪会删除未使用字形并重写字体表;压缩只是更高效地编码剩余字节。WOFF 子集通常同时获得两层收益:先减少字形,再压缩字体表。

Q.为什么输出字体的字形数比输入字符数多?

字体通常必须保留 .notdef 等基础字形,解析器也可能保留字符映射或内部关系依赖的字形。因此字形总数不需要和去重字符数完全相等。

Q.这个浏览器方案能输出 WOFF2 吗?

本文的 fonteditor-core 加 fflate 链路只能输出 TTF 或 WOFF。WOFF2 需要单独编码器,常见方案是 WebAssembly 或构建阶段的 fonttools,并在浏览器兼容性和字体授权验证后再开放。

Q.裁剪字体后,字体授权会变化吗?

不会。技术处理不会自动赋予再分发权。有些字体允许网页嵌入或修改,有些会限制使用方式。托管、修改或分发字体子集前必须检查源字体许可证。

Q.为什么 Emoji 或复杂文字可能裁剪不完整?

部分字体依赖彩色字体表、可变字体表、连字或文字塑形规则,并不是简单的一码点一字形。复杂文字应使用真实词句验证,生产环境需要时应改用支持塑形关系的字体流水线。