paoding run 写出 records.jsonl,每行一个 JSON 对象。第一行是头部,之后每行描述目标字体中的一个字。
记录里没有字形轮廓,也没有参照数据(Make Me a Hanzi)的几何。它们通过字形名和轮廓序号指向目标字体,只额外记录切线和带标签的点,坐标都用字体自己的单位。要得到轮廓,用记录和你手上的那份字体重建(paoding.record.realize_record)。
{"kind": "header", "format": 1, "tool": "paoding 0.1.0.dev0",
"font": {"file": "SourceHanSansSC-Regular.otf", "sha256": "f1d86111…"},
"reference": [{"file": "dictionary.txt", "sha256": "…"}, {"file": "graphics.txt", "sha256": "…"}],
"params": {"level": 1, "ppem": null, "threshold": 0.85, "overrides": ["paoding:structure.toml"]}}记录只对哈希值所指的那个字体文件有效:不同版本的字体,轮廓序号和坐标都会变。overrides 列出生效的覆盖文件,paoding: 开头的是工具自带的那份。
部件用它在 IDS 树中的路径来指称,以点分隔:
"":整个字;"0":第一个顶层部件;"1.0":第二个顶层部件的第一个子部件。
例如 照 ⿱⿰日召灬 中,"0" 是 昭,"0.1" 是 召。level 表示切分的深度,第 1 层是顶层拆分。
ids 就是路径所指的那棵树。MMAH 没有的字,参照由部件拼成,ids 来自 BabelStone 的 IDS 表,记录带 "composed": true。Make Me a Hanzi 只把一个字拆一两层(照 是 ⿱昭灬);第 1 层以下,本身也是字的部件会按它自己的条目展开(昭 是 ⿰日召),所以在第 3 层,照 是 ⿱⿰日⿱刀口灬。
展开到基础部件为止。以下几类算基础部件,不再下拆:
- MMAH 标为象形的(日、木、口);
- 部件相互穿插的(
⿻); - 含有单笔部件或未知部件(
?)的。
结构覆盖可以改变展开在哪里停下,自带的修正就把 言、貝、欠、斤、卑 定为基础部件。一个部件的笔画如果和它自己的条目对不上(笔画数不同,或形状差太多),也保持整体。MMAH 的部件全部未知的字(如 門 是 ⿰??)整字不拆,ids 为 ?。
nodes 列出该层实际出现的部件,按叠放顺序、自下而上排列(默认为 IDS 顺序,可用覆盖改)。如果某一笔的参照路径比所切的层级短,它保留那个较短的节点。
{"char": "申", "codepoint": "U+7533", "glyph": "cid27077", "level": 1,
"ids": "⿻日丨", "nodes": ["0", "1"],
"status": "auto", "confidence": 0.95,
"units": [
{"contours": [0, 1, 2, 3, 4],
"cuts": [[[538.7, 195.0], [537.9, 267.0]], …],
"faces": [{"at": [715.6, 422.8], "nodes": ["0"], "flat": "0"},
{"at": [497.6, 227.3], "nodes": ["0", "1"], "flat": "1", "rule": "crossing"},
{"at": [497.0, 75.8], "nodes": ["1"], "flat": "1"}, …]}
],
"metrics": {…}}单元是字形墨迹中连通的一块:一条外轮廓连同它的孔洞。contours 列出组成它的原始轮廓序号(即通过 glyphSet[glyph].draw 由画笔看到的顺序)。轮廓互相重叠的字体同样适用:重叠的轮廓归入同一个单元。
一个单元要么整个属于某个部件:
{"contours": [3], "node": "1"} // 由覆盖强制指定时另加 "override": true按参照本该切开、但同一字体在别的字里把同样形状的单元整个交给同一个部件时,单元也整个归它,like 列出最多三个这样的字(见原理):
{"contours": [4], "node": "0", "like": "院隆陈"}要么被切开。cuts 是以字体单位表示的折线,每条都从单元轮廓的一侧穿到另一侧,把单元分成若干面;faces 的每一项给包含其点 at 的那个面加标签:
nodes:在叠加视图中包含这个面的所有部件;flat:在拍平视图中拥有这个面的那一个部件;rule(仅共有的面):flat为什么胜出:through:它的笔画穿过交接处,另一笔止于此;crossing:两笔都穿过,叠放在上层的部件胜出;corner:两笔都不穿过,笔画伸进这个面更深的部件胜出,相同时取先写的笔画;override:由覆盖指定。
部件与别的部件相接、按参照切开的字,若同一字体在别的字里把这个部件画成不与别的部件相接的样子,切线可能改由这个“模板”决定(见原理)。记录顶层的 templates 给出这样的部件和模板取自的字:
"templates": {"0": "訂"}- 拍平视图:每个面交给它的
flat部件。各部件恰好铺满字形,没有重叠。适合平面着色、面积统计,或任何每个点只画一次的用途。 - 叠加视图:每个面交给它的全部
nodes。每个部件自身完整,可以单独展示(动画、高亮、按部件检索的缩略图),部件在交接处会重叠。按叠放顺序不透明地绘制叠加视图的各部件,看起来和拍平视图一样,只是through或corner的面若归下层部件所有,会有差别。
在 T 形交接处,叠加视图会把止于此的那一笔延伸进它所接的笔画,直到参照笔画结束的位置。目标字形本身无法告诉我们这一段该有多深,所以这个深度只是近似值。
- 单元轮廓:它的
contours的并集,去掉重叠。 - 把轮廓展平,加上切线(两端各延长几个单位,确保穿过轮廓),再做多边形化,得到单元的各个面。
- 用落在面内的
faces点给每个面加标签(没有则取最近的点)。 - 要得到精确的曲线:用每个面(外扩几个单位、再减去其他所有面)去裁剪单元轮廓。切口是直线段,其余部分保留字体原有的曲线。
paoding.record.realize_record(record, font) 完成以上步骤,返回 {"overlay": {node: Path}, "flat": {node: Path}}(skia-pathops 路径,字体单位)。
{"char": "好", "glyph": "U+597D", …,
"pixels": {"left": 0, "top": 14,
"rows": ["...a............",
"...a....bbbbbb..",
…],
"legend": {"a": {"nodes": ["0"], "flat": "0"},
"b": {"nodes": ["1"], "flat": "1"}}}}rows 从上到下覆盖字形的位图;left 和 top 给出第 0 列、第 0 行相对原点和基线的位置(以像素为单位,y 向上)。. 表示无墨迹;字母指明该像素的叠加集合和拍平归属,交接处的像素可以对应一个 nodes 含多个部件的字母。
status 在 confidence 达到阈值时为 auto,否则为 low;覆盖可以把它设为 verified 或 exclude。confidence 取最弱的部件的分数,结构是推断而来时再打折扣。每个部件的分数是以下两项中较低的一项:
median_in:部件的参照中线(去掉两端、伸进别的单元的那一小段,以及落在已整个归别的部件的like单元上的部分)有多少落在或贴近它的叠加墨迹上。配准跑偏时会偏低。位图字体允许中线偏离墨迹半个像素。purity:部件的拍平墨迹,其骨架有多少不明显更靠近别的部件的笔画(同样不算上面去掉的那些中线段)。墨迹分错部件时会偏低。位图字体的每个像素都分给最近的笔画,这项指标按构造恒为 1,所以改为:部件的横竖像素串(三个像素及以上)中,没有被别的部件截断的比例。
另有两项仅供参考:skel_cov 和 iou。两者都受参照(楷体)与目标字体之间风格差异的限制。
扣分规则:
- 有交叉(crossing)或转角(corner)交接时 ×0.95;
- 有笔画的归属是推断的(既不来自 MMAH 的匹配,也不来自覆盖,包括拼合参照的全部笔画)时 ×0.95;
- 拆法中有部件分不到笔画时 ×0.5;
- 有部件最终没有墨迹时直接为 0;
- 结构检查标出可疑位置(见下)时 ×0.85。
矢量字体的记录可能带有 flags,列出切分结果与目标字形自身笔画结构不符的位置:
"flags": [{"kind": "split", "at": [96.0, 210.5], "nodes": ["0", "1"]}]检查在目标墨迹的骨架上做。骨架被交点(笔画相接、相交处)、拐角(急转处,一笔结束、下一笔开始)和收腰(墨迹骤然变窄处,两笔首尾相碰)分成若干段;离这些点稍远的一段,是一笔或沿同一方向连下去的几笔,应当只有一个主人。
split:这样的一段被两个部件分了,从一笔中间切开(nodes按占有长度排列);vote:这一段的墨迹归了一个部件,但沿着它走的参照中线几乎都是另一个部件的(nodes:墨迹的主人、中线的主人);stray:某个部件在叠加视图中有一小块孤立的墨迹,伸进了别的部件的笔画(拍平视图里这块归别的部件),和它自己的其余墨迹不相连(nodes:这个部件)。
at 是字体单位下的一个点。这些只是提示,不改变切分本身。
TOML 格式见 paoding.overrides:
[structure."<字>"]:对所有字体修正拆法或笔画归属;[glyph."<字>"]:只对某一套字体设定叠放顺序、按轮廓序号整体指定单元归属、按点指定共有面的拍平归属,以及审核状态。
记录中各部分的来源和发布时的注意事项,见数据来源与许可。