最近换了新手机。前日在Twitter发手机截图的时候,一眼就注意到颜色不对(饱和度过低)。颜色不对的原因很显然:内嵌的色彩空间ICC profile没有正确处理。最近几年的手机基本都是广色域(Display P3为多),对于截图比较常见的处理方式是自动在截图的图片里加一个Display P3的ICC。iOS已经是这样很久,我之前的OnePlus手机并没有这么处理(而且我之前的手机截图直接就是JPEG,所以也不会有本文所述的问题),换了这个新手机才第一次在安卓端遇到。
对于使用非标准/默认sRGB的图片的处理方式大概有两种,一种是保留ICC,一种是转换为sRGB。前者最优但是向后兼容性有限,后者兼容性强。但是错误的方式是不转换(即保持像素点的原始RGB值)但是却丢掉ICC。这个具体的前文已经说过了就不赘述。显然,Twitter这里就犯了这样的错误。
但是这里有两点很难理解:第一,我用iPad上传截图从没遇到过这样的问题。前面已经说过iOS的截图也是用了Display P3的ICC。第二,我感觉Twitter的Media Team不应该这么菜。
推特的图片处理概述
既然说到2,那就得从当年Twitter对于图片上传处理的改版说起。推特原来是对于上传一切图片都二压的。从2019年12月起,改成了JPEG不二压(除非超大,见下述)。元数据自然是strip的,但是ICC profile保留。这里可以参见当年Media Team的dev发的推(注:此人已经跳槽到Meta。否则这次的问题还想at他反馈下呢)。PNG的话,则是一般都二压,除非:1) 转出来的JPEG比PNG还大、2) 图片特别小、3) 是palette(即所谓PNG8)、4) 有透明度等特殊情况。
这里有个日本网友总结的:

这个改动是非常好的,尤其是会对ICC进行保留这点。所以我想当然地认为,如果你上传PNG且被二压了,那么至少ICC是会保留的。结果居然不会?!
测试
OK,那为什么iPad就可以?这时候我脑子里大概已经确定了推特给了iOS特殊待遇,毕竟这种事情已经屡见不鲜了。不过到底是如何给的特殊待遇?我们得详细测试下。那么启动一个小号,开始大量上传测试。用Photshop制作一个Adobe RGB的PNG上传,丢失ICC,符合预测。用web上传一张iPad截图,咦,颜色怎么是正常的?看来特殊待遇不是在app端?那让我们把图片剪裁一下,再上传——咦,怎么又丢失ICC了?!难道我动到了什么元数据?不服,我用Exiftool把原始文件的metadata一字不漏的复制过来:
exiftool -TagsFromFile input.png "-all:all>all:all" output.png
怎么还是丢失?到这里我已经开始抓头发了。
不要急,让我们仔细对比一下两张图片的所有的技术细节区别——唔,看起来iPad的截图还挺可怕的,是用了16bit/ch(一般是8),而且有alpha通道[*]。转成8bit/ch的再上传,果然ICC又丢失了!难道是Twitter对于8bit和16bit的处理不同?那我们自己造一张16bit的图总可以了吧!咦,怎么还是丢失了,这……
[*] PNG支持alpha通道(约等于透明度)。但是注意,许多软件,例如Photoshop,在保存的时候,如果检测到你的alpha channel是全1(即全opaque,没有任何像素有任何透明度)的时候,会自动删除alpha通道。可以使用magick的
magick convert input.png -alpha on output.png
来强行开启alpha通道。
我反复地把不同图片的内容进行复制粘贴、缩小放大来上传,结果就是有时候行有时候不行。在我已经开始怀疑推特使用了什么heuristics来分析图片内容、又或者有奇怪的cache的时候,突然想到:咦,同样一张图8bit会丢失16bit不会,但是我自己制作的比较小的16bit却又不会,难道……和文件大小有关?想到上面规则里有的“JPEG不能超过5MB否则二压”,我于是制作了2张 PNG + display P3的图,一张小于5M 一张大于5M,然后上传……
……但是还没完。我们把这两张图下下来对比一下。记得要下载原图,也就是
咦,似乎还有点奇怪?两张图的技术参数是完全一样的,都是85% quality的4:2:0的JPEG,符合上面二压的说法。但是两者其实都没有保留Display P3的ICC:正确的那张图,是正确转换到了sRGB,而且加了sRGB的ICC:

错误的那张则是和原来一样,是直接丢掉了原来的ICC,也没有内嵌新的ICC。
赶紧掏出iPad确认了下,果然是不一样的——用iPad上传,是保留了原始的ICC而非转换:

果然,iPad客户端还是有特殊待遇的。那么我们再多试几次,发现如下现象:
如果用iPad客户端上传,所有的PNG二压都会保留ICC——即使是小于5MB的。而且也不限于iPad截图,我把我自己造的AdobeRGB的图、以及Android截图用iPad上传,全都是正常保留ICC:
与之相反,Android就惨咯:用Android上传任意PNG都会丢失ICC,哪怕是在web端可以正确处理的>5MB的图:
那么如果上传会被二压的超大的JPEG,会怎么样呢?我以为会和PNG一样,结果还稍有不同:
iPad – 保留ICC; Android – 保留ICC; Web – 转换ICC为sRGB
区别在于,Android这次正常了。
总结一下:
| Input | iPad app | Android app | Web |
| PNG, <5MB | Keep ICC | Lose ICC | Lose ICC |
| PNG, >5MB | Keep ICC | Lose ICC | Convert ICC to sRGB |
| Large JPEG | Keep ICC | Keep ICC | Convert ICC to sRGB |
使用Twitter内置的裁剪功能的结果
另外一个有趣之处是使用Twitter内置的crop功能时的结果。我分别拿各种组合试了下,不赘述直接贴结果:
iPad:如果原图是sRGB或者无ICC,会产生一个无ICC的图片;如果原图是非sRGB(Display P3、Adobe RGB),会一律转换为Display P3(含ICC)
Android:保留原图ICC。注意这里profile并不是直接copy,而是会用一套谷歌的equivalent的ICC,比如如果原图是苹果的Display P3:

会变成谷歌的:

如果原图是Adobe RGB:

同样会变成谷歌家的:

但是颜色都是一样的/对的。
另外一个小插曲:
用 Android Twitter打开修图时,会看到在filter等功能时图片的预览明显颜色是错误的(丢失ICC)(左),但是在crop的页面却正确(右):

如果试图打开那张10M的iPad截图,就更草了,filter直接无法处理(应该是凡是用了iPad这个color profile的都会有问题:我把这图用PS改小了点照样不行)(左),但是同样,crop是工作的(右):

估计是调用了不同的系统接口吧。
Web的话,则是无脑(无论图片大小、格式)调用上面提到过的PNG>5MB时的编码器,产出(正确转换的)sRGB的图(含ICC)。
总结如下:
| Input | iPad app | Android app | Web |
| sRGB or no ICC | No ICC | sRGB ICC | sRGB ICC |
| Other ICCs | Convert to Display P3 | Use Google equivalent ICC | Convert to sRGB |
结语
从上面的一些现象可以大概猜到,很多东西应该也不是Twitter自己写的,而是调用了系统的图形接口,所以这个问题也不能完全甩锅给Twitter;但是至少我们知道,如果他们用心,至少完全是可以做到正确转换/保留ICC的,所以在安卓端上传PNG会完全丢失ICC的问题还是要推特背锅。
这里不得不顺便提一个很让人frustrated的问题:对于这种大型服务,通常完全没有任何顺畅反馈的通道。比如Twitter的客服,基本只会处理账号和内容相关的问题,凡是任何技术问题,反馈就如同对牛弹琴。Twitter另有面向dev的渠道,但是严格只回答和API相关的问题——如果只是“使用”上的技术问题,他们是不管的。如果没有在Twitter上班的朋友,几乎没有任何办法传达到相关人士耳中。之前有人劝我,别白费功夫了,他们不care;但是我想反馈也不是单纯为了帮他们改善产品,而是这种问题切实地影响到了我个人的用户体验。(耸肩)















