覆盖率是怎么算的

最后更新 2026-11-02

EN中文
本页目录

「这个字体覆盖了哪些字符」在本站是现算的,不需要重新解析字体、不需要联网、也不需要重跑同步。原因很简单:需要的两份数据都已经在手上,而且都是纯数据。

两个输入

  1. 字体的码位区间(cmapRanges):分析字体时从 cmap 表里抄出来的区间,压成 [起, 止, 起, 止, …] 的扁平数组存进档案。CJK 字体大约几百段;
  2. 区块表:Unicode 官方 Blocks.txt 解析出来的三百多个区块,外加每个区块的「已分配」数,服务端缓存着。

一个在浏览器(本机字体档案)或 API 响应里,一个在 /api/unicode/blocks。两边都是数组与数字,遍历一遍就出结果。

分子:区间与区块求交

对每个区块,数「有多少个码位同时落在字体区间里、又落在这个区块里」。

实现是一趟归并:区块与区间都是升序的,所以用一个游标在区间数组上往前走,每个区块只从上次停下的地方继续看。整体复杂度是「区块数 + 区间数」,不是「区块数 × 区间数」。

(这段代码在 shared/src/fonts/coverage.ts,前端与服务端用的是同一份。)

两个分母

数字 分母 用在哪
covered / assigned 区块里已分配的码位数 显示在区块行上
ratio 区块的总大小 进度条的宽度

为什么分开,见 区块与「已分配」:一个 CJK 区块两万多码位里大量是未分配,用总大小当分母,条会显得无关紧要;而用已分配当分母,才能看出「这个字体到底漏了多少该有的字」。

三个限制(如实说明)

  1. 「映射到非 0 字形」就算覆盖。分析时已经把映射到 .notdef(0 号)的码位滤掉了,但轮廓为空的字形仍然会被算进去——它确实有映射,也确实是那个字形,只是画不出东西。所以「覆盖率 100%」不等于「每个字都好看」。
  2. 变体序列不算。cmap 里的变体选择符(U+FE0E / U+FE0F 那类)不在码位区间里,所以「emoji 的彩色版本」这种能力看不出来。
  3. 只探测、没解析的本机字体没有覆盖率。这类条目的 cmapRanges 是 null,页面拿不到区间,只能显示「未分析」。

为什么不用重跑

如果覆盖率要在服务端算好存下来,就得为每个字体 × 每个区块存一行,数据量随字体数线性增长,而算法本身是一次遍历。现算更简单,也不会因为 Unicode 更新区块表而让旧数据对不上。

代价是每次打开页面都要算一遍——实测 353 个区块 × 几百个区间,在浏览器里是毫秒级的事。

延伸阅读

  • shared/src/fonts/coverage.ts(算法与注释)
  • Unicode 官方:Blocks.txt、extracted/DerivedName.txt