某天收到反馈:网页播放器里中文字幕全是方格,一个字都认不出来。

第一反应当然是编码。但翻开那批 ASS 文件——标准 UTF-8 带 BOM,拿文本编辑器打开中文清清楚楚。编码没问题。

方格的真正含义

豆腐块(tofu)不是「字符坏了」,而是字库里没有这个字的字形。字幕组的特效字幕会在样式表里指定专用字体:

Style: Sub-CN, HYQiHei 65S, 80, &H00FFFFFF, ...
Style: OPJP,   FOT-UDKakugo_Large Pr6N B, 80, ...
Style: ED_EN,  Avenir Next W1G Medium, 60, ...

网页播放器是自己在浏览器里渲染 ASS 的(libass 的 wasm 版)。它手上没有这些字体,就只能画方格。

服务器容器里装的 Noto CJK 帮不上忙——那是烧录字幕时才用得到的,直接播放走的是另一条路。

第一次修:打开回退字体

播放器端有个「回退字体」设置,默认是关的,路径也是空的。把字幕组随片附带的字体包解压进去,打开开关:

EnableFallbackFont: true
FallbackFontPath:   <字体目录>

刷新页面,大部分字正常了。但——还有少部分显示成一个小点。

第二次修:一个不写日志的上限

先排除最容易怀疑的方向:把 13 集字幕里出现的 2637 个汉字逐个拿去比对字库覆盖范围,结论是字体一个字都不缺

那问题在哪?

查接口才发现:明明放了 13 个字体,服务端只下发了 5 个

反复试验后规律浮出来——它把字体按体积从小到大排序,累加到约 20MB 就不再往下发。不报错,日志里也没有任何记录。

而这部片的字幕用了 9 种字体:

用途字体是否被下发
正片对白HYQiHei 65S❌ 被砍
staff / 字牌HYQiHei 85S❌ 被砍
日文FOT-UDKakugo
EDAvenir Next

被砍掉的那几种,播放器找不到就退回英文字体去画汉字——于是就有了那些小点。

表现是「大部分正常、少部分是点」,正好对应哪些字体侥幸挤进了名额。

两条路

一是给字体瘦身:只保留这部片实际用到的字符。用 fontTools 做子集化,13 个字体从 80MB 压到 12.7MB,全部塞得下,字形一个没动:

opt = Options()
opt.name_IDs = ["*"]          # 必须保留字体名,否则 ASS 按名字找不到它
s = Subsetter(options=opt)
s.populate(text="".join(used_chars))
s.subset(font)

二是直接把上限拿掉。代价是每次起播要先下载整包字体,首播慢几秒(浏览器会缓存)。

换番就要重跑一次子集化,嫌麻烦的话选第二条。

顺带一个坑

字体目录里的 .ttc(字体集合)会被直接无视,只认 .ttf / .otf / .woff。繁体 OP 用的那个字体正好是 ttc,一直没被下发。抽出来存成 ttf 就好:

TTFont(src, fontNumber=0).save(dst)

结论

  • 方格 ≠ 编码问题,先想字体
  • 核对办法:grep "^Style:" x.ass | awk -F, '{print $2}' | sort -u,跟服务端实际下发的字体列表比对
  • 静默丢弃是最难查的失败方式。凡是「配置生效了但结果不对」,去数一数实际生效的数量