修复Twitter等网站在Chrome的字体显示问题

system-ui

Twitter的默认字体栈是

font-family: system-ui, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;

这里别的都好说,问题就出在system-ui这个。

system-ui这个generic font的设计思路很美好:自动调用系统字体,给用户最原生的体验。这个思路用在移动平台上、甚至Mac OS都没有大问题,但是在Windows上问题就很多。

Windows简中默认的字体是微软雅黑,英文则是Segoe UI。当然,Segoe UI是没有CJK字符的,所以会fallback到其他字体。

微软雅黑最大的问题是中文字符还算不错,但是英文和假名都巨丑无比。所以,一旦调用了雅黑来显示日文、英文内容,立刻变得不堪入目。

对比:

雅黑

微软雅黑

segoeui

Meiryo

所以,一旦system-ui导致调用了雅黑来显示非中文内容,效果立刻变得很差。

由于system-ui的这个问题,有许多主流网站已经刻意将system-ui从其字体栈中删除,例如GitHubBootstrap等等。不知道为什么Twitter目前还没有跟进(而且没记错的话,Twitter是换了新版界面之后才专门加上了system-ui)。这里有个CSSWG的相关讨论(发帖人是Chrome/Google团队的CJK字体专家Koji Ishii)。

Chrome对于system-ui的处理

不过既然标题专门提到了“Chrome”,就知道这事情没有这么简单。

确实,system-ui的behavior在Chrome和Firefox是不一致的。其核心区别在于,如何处理lang参数。

很久以前在S1发过一贴专门讲不同浏览器不同语言的fallback机制(待更新),不过这里不展开。具体到这个问题,就是Firefox面对system-ui,仍然会根据语言来选择在选项里设置的字体,而Chrome会原封不动的直接套用系统默认字体——对于简体中文Win来说,就是微软雅黑。

回到Twitter上的话,Twitter其实有个很优秀的功能就是对于每条tweet会自动分析语言,然后给那个div套上lang='xx'的选项。所以,大部分日文推是已经加了lang=ja了的(如果没加这个问题会变得更复杂)。对于Firefox,面对system-ui,看到lang=ja他会先选择你设置的日文默认无衬线字体——没记错的话默认是Meiryo——显示效果就比较美观。而Chrome里则会无脑显示为微软雅黑。

用这个HTML可以很快速地对比两者。

可以看到,对于指定了“Segoe UI”的场合,由于Segoe UI无法显示CJK字符,会fallback到其他字体。这里,无论是Firefox还是Chrome都会根据lang来选择合适的fallback字体(ja=Meiryo、zh-cn=雅黑。en的话,由于我浏览器语言优先级是zh>ja,也是雅黑)。

对于指定了system-ui的场合,Firefox的逻辑是和其他西文字体完全一样的,对于CJK字符会根据语言选择fallback字体。唯一的区别在于这里对于lang=en的场合他是选用了有衬线字体TNR,因为Firefox字体设置里对于Latin的默认比例字体是衬线而不是无衬线。不过一般网站的CSS都会在字体最后指定sans-serif,所以实践中不会见到。

而Chrome那边如上文所述,会直接选用雅黑。由于雅黑字符集全,自然所有字符都雅黑了。假名巨丑,中日不同形的字符也会变成中文字形(这里的“将”)。另外,Chrome对于lang=en汉字会fallback到宋体而不是微软雅黑也是个多年以来的quirk了,我倒是不是特别不喜欢宋体所以不是太有所谓。

另外,强调一遍以上的都是在简中系统测试的。如果你的系统是英文,那默认字体则是Segoe UI,所以是从小(字符集)往大(字符集)fallback,不会受到雅黑的污染。

Workaround

在Chrome 78之前,我是使用了一个全局的Stylus rule来对付system-ui的:

@font-face {
    font-family: system-ui;
    src: local("Segoe UI");
}

但是在Chrome 79之后,你无法再使用@font-face来重新定义保留关键字了,说是这样才符合标准(那个标准我读了几遍都读不出这层意味,专门去CSSWG问了下,不过那边的专家都说确实不应该能覆盖。另外,Firefox从来就不支持用@font-face覆盖generic keyword。)

顺便一提,我还替换了几个在Windows字体渲染效果极差的字体:

@font-face {
font-family: "M PLUS 1p";
src: local("Meiryo");
}
@font-face {
font-family: "mplus-1p-regular";
src: local("Meiryo");
}
@font-face {
font-family: "M PLUS 1p";
font-weight: bold;
src: local("Meiryo Bold");
}

既然现在不行了,那只好手动替换Twitter了:

@-moz-document domain("twitter.com") {
* {
font-family: "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif !important;
}
}

DVD的名义时长和实际时长的不一致

之前提过,我发现直接播放DVD(ISO)和先remux或者压制之后的视频的名义时长不一样。

两者的区别很小,但是正好是一个1000/1001——比如手头这个视频,直接播放DVD显示01:20:03,但是压制后就显示01:20:08。此现象可以在几乎所有播放器重现(我测试了:Pot,MPC-BE,SMPlayer(mpv核心))。

我之前默认分开播放两者时,会分别真的遵照这个不同的时长来播放,从而得出了DVD会实际按照24/30fps播放、而压制之后会按照23.976/29.97fps的速度来播放的结论。但是真的如此吗?还是要实测才知道。

我用的测试方法如下:

  1. 用同一个播放器播放两个视频,手动不停暂停调整到保证两者的播放in sync或者基本 in sync。观察两者的时间区别(如果有)。
  2. 找一个比较好计算的时间点开启一个秒表(我用的手机)作为第三参照。
  3. 放置视频播放至少40分钟以上。再次对比视频实际内容和时间轴。

今天要写的是测试1,使用MPC-BE(我对Pot的标准程度始终无法信任…)。

首先我们打开上述视频,然后截个图记录。上图是直接播放DVD,下图是我自己rip的版本。

001.png

首先可以看到,两者的总时长如上所述,错了5秒。我从30分钟左右开始,调整到两者视频内容基本一致(最多错4-5帧,也就是+-200ms以内)。这里可以看到,从我选的这个时间点两者的时间已经有所不同了,但是相差只有1-2s左右。另外秒表我用的手机,这里没有拍进来,我是从上面的视频的00:31:00开始计时。

经过大约40多分钟的放置play,我们来看看结果。

首先是视频内容:两者依然基本是in sync的,虽然可能稍微差了那么一点点,但是最多也只有几百ms的区别。这证明,两个视频的实际时间/播放速度是一致的,否定了上面说的“播放fps不同”的假说。那么名义时间呢?显然时间不可能还保持只有1s的差距,否则最后结束时间差的那5秒哪里去了!

事实上,正是如此——但是出乎意料的是,这两个视频居然都没有和我的秒表一致!

 

003

可以看到,本来我的秒表是和上面的视频(DVD)的秒位应该是一致的,但是现在上面的要慢高达2秒;而下面的视频(rip)则比比秒表慢了2秒左右(一开始就慢1秒多,所以间隔基本没变)。

两个视频的名义时间的差距,算上最开始就慢了1秒多,相当于在43.5分钟内又多偏移了3秒左右,倒是基本符合1001/1000差:

2612.7*1001/1000-2612.7~=2.613
2612.7-2612.7*1000/1001~=2.610

(正算反算都是2.61秒左右)

也就是说,虽然视频实际播放的速度一致,但是显示的名义时间却至少有一个不是准确的,甚至两个都不准(不说死是因为我无法确认我手机秒表的准确性)。

硬要选一个的话,下面的视频的名义时间比较接近我的秒表。下面的视频是rip过的MKV,每帧的timecode是写死的可以提取出来,最次也可以根据帧数反推——如果播放名义时间符合实际时间,证明确实是23.976fps在播放,而不是24。

介于两个视频的实际播放速度一致,如果认同下面视频名义时间准确(=实际播放时间)这个结论,就可以推出:对于DVD,播放器仅仅是是按照30/24的速度来计算以及显示总时长/当前时间的,播放时并不按照那个。

 

改天再用其他播放器再测试一个视频好了,这次准备使用Pot,使用电脑上的秒表软件而非手机,另外准备使用一个真·30fps的视频测试(这次测试这个视频是film的,24fps)。

 

现在我很好奇的是,如果把DVD放在物理DVD播放器接电视播放,会怎样呢?

ffmpeg静态图转视频

注意:本文部分内容之前发表在此文。但是经过补充后单独成章比较好,就移动到这边来了。

将一张图片转成视频的基本命令很简单:

ffmpeg -loop 1 -i {img} -t {dur} -vf format=yuv420p output.mp4

默认的fps是25,可以用-r指定成别的。这里重点注意-vf format=yuv420p部分,这个保证视频会被正确转换为最常见的YUV420格式,否则会使用YUV444格式。这个等效于-pix_fmt yuv420p

同理,如果需要其他格式(YUV444、YUV422之类),只需要替换成其他的pixel format即可。如果想要full range,可以使用yuvj420p

但是仅仅这样做,有两个问题。

选择的正确RGB->YUV转换矩阵

第一个问题是RGB->YUV的转换矩阵。ffmpeg默认是使用BT.601矩阵来进行这个转换的,如果你的输出视频是SD分辨率,这并没有问题。但是如果是HD视频,HD默认的标准是BT.709,所以这样就不对了。最明显的就是纯红色255, 0, 0会变成255, 25, 0。

解决办法,首先自然可以给输出结果显式加上BT.601的metadata:

-color_primaries smpte170m -color_trc smpte170m -colorspace smpte170m

(NTSC和PAL还略有不同,参见此文。)

但是,不推荐这种做法。有的滤镜/渲染器根本无视元数据。HD视频还是老老实实用BT.709就好。所以最好的办法是进行正确的BT.709转换。

ffmpeg有N个video filter可以实现这个,最常见的是scale

ffmpeg -loop 1 -i {img} -vf scale=out_color_matrix=bt709,format=yuv420p -color_primaries 1 -color_trc 1 -colorspace 1 -t {dur} output.mp4

其中-vf scale=out_color_matrix=bt709部分是把图片转换成BT.709(Rec. 709的另外一个名称)。因为我们还有个format的vf,直接把两者用逗号串起来(filter chain)。

后面的-color_primaries 1 -color_trc 1 -colorspace 1的是在元数据里标注。如果用x264编码(默认如此),也可以直接用-x264opts colormatrix=bt709。当然,如上所述,如果是HD视频可以略去不写(不推荐)。

除了-vf scale,还可以用:

  • -vf colormatrix=bt601:bt709,format=yuv420p:从命令来看,感觉实质是先转601再转709。
  • -vf colorspace=iall=bt601-6-625:all=bt709:format=yuv420p :同上。另外注意,这里的formatcolorspace这个vf的一部分(冒号分割而非逗号),而不是再调用format
  • -vf zscale=matrix=709,format=yuv420p:这是一个比较新的filter,来自zlib

几个讨论可以参见这帖这帖

swscale的颜色误差bug

我之前一直是使用上述的-vf scale来转的,但是总是发现颜色有点偏——白色(255, 255, 255)会变成略微发黄的颜色(252, 255, 252),我以为这仅仅是精度不足的缘故也没太在意。直到有一天无意发现,如果我先将输入的BMP图片另存为PNG格式,就不会发生偏色。这完全不make sense!于是我报到了ffmpeg

经过快2个月无人问津后,我忍不住去ffmpeg的emaillist发了个帖,果然引来了玉——除了一些workaround之外(下述),最重要的是有人指向了真正的bug:#979

简单来说,swscale这个库(libswscale)——包括scale滤镜——有一个奇怪的bug:从bgr24转换为YUV会有色差,但是rgb24就不会(两者应可以无损转换)。上面BMP和PNG结果不同也是因为PNG用的是rgb24的pixel format,而BMP是bgr24。

既然知道了问题所在,我们只需要先转换一次即可,把上面的vf前再串一个format

-vf format=rgb24,scale=out_color_matrix=bt709,format=yuv420p

就可以啦!那么上面提到的其他几种转换滤镜,有没有同样的问题呢?

  • colormatrix:同样的bug,这个filter应该也是基于libswscale。
  • colorspace:如果你使用上述的方式,使用colorspace内置的format参数来转换成yuv420p而不是串一个format vf,可以避免这个bug。
  • zscale:无此bug。

另外,这个bug还可以通过添加scale的flag,accurate_rnd(精确rounding)来修复(这里还加上了另外一个增加精度的flag,full_chroma_int,不过这里accurate_rnd其实就够):

-vf scale=out_color_matrix=bt709:flags=full_chroma_int+accurate_rnd,format=yuv420p

当然,这并不是说这个bug仅仅是精度的问题:否则无法解释为什么rgb24就无问题。

另外,colormatrix之类的vf虽然没有flags参数,但是你可以增加-sws_flags accurate_rnd,也可以修复问题。

ユメシンデレラ (麻倉もも) 封面的情况

短文,算是上文的简单实践。主要是这次有个细枝末节的问题,写给care的人看。

首先先讲推荐使用的版本1:

颜色正确的sRGB版本。可以在官网discography(仅限限定和Anime盘,通常盘见下述)下载600px的。图像直链地址(右键复制):

或者在Sony Music Shop下载:

三个版本都有,但是只有500px,而且Anime盘下面多了个copyright标记。所以除了通常盘,还是用上面的吧。

(顺便一提,一般而言,索尼Music Shop的颜色都是比较靠谱的。官网是这次恰好是正确的,之前经常错)。

Sony Music Shop还附赠一张尺寸不算小的公式照:

注意有内嵌的Adobe RGB ICC和部分EXIF。拍摄日期可以看到是19年6月25日。

camera

至于在CD内容宣布的info帖子里的图,则是你的老朋友——Adobe RGB直接丢ICC而不是正确转换成sRGB的版本。自然不推荐,如果一定要使用,手动下载修改ICC为Adobe RGB,并使用兼容的图像软件查看(显示效果应该与上一组一致)。

贴个范例,注意和上图对比,明显肤色饱和不足:

顺便一提,荒乙的官网也是用的版本1(正确的sRGB),不过对封面进行了剪切,真人的上下切掉一点,Anime盘左右切了点。

这次案例的奇怪之处在于,存在一个版本2。

基本所有的数字平台(自然只有通常盘),包括mora、iTunes等等,以及官方discography(仅限通常盘)都是用了一个单独的的版本(下图为官方discography的600px版,可以在mora下载的音频文件中提取1400px的版本、以及在iTunes下载3000px的版本(但是明显是upscaled的,所以毫无意义)。

这个版本和上面的版本无论正确的还是错误的都不一样,主要区别在于Red通道下沉,因此明度要低不少、但是对比度更高,比如蓝色更深沉。和版本1差的也不是一次简单的sRGB<->AdobeRGB转换。

我暂时搞不清楚是因为其他什么别的色域转换导致,还是干脆就是不同的调色版本。不过如果要从2手调到1倒也不难,level里总体gamma搞个1.12、然后R通道单独加1.08左右gamma就能实现。

其实这个版本2,三种都有,我在ナタリー的访谈中找到了全套,800px:

注意,要点击查看大图(比如第一张是p_lim.jpg)。

外面的小图:

文件名是pc_item_lim.jpg,反而是和上面版本1一样的版本,这就更诡异了。