原创文章 · 图片与色彩

手机拍的广色域(Display P3)照片,为什么在网页里处理后颜色变淡?怎么保住?

Jinlab · 2026年10月1日 · 基于真实开发与真机测试整理

很多人遇到过:iPhone拍的一张红花、一片绿叶,放进某个网页工具里裁一下、转个方向再存出来,颜色明显没那么鲜艳了。这不是错觉,也不是压缩画质造成的。本文从"颜色到底存在文件的什么地方"讲起,说清楚变淡的两种原因,再介绍我们在浏览器里保住原始颜色的做法,以及实测数据。

先弄清楚:什么是广色域和色彩配置

屏幕和照片能表示的颜色范围叫"色域"。网页和大多数图片默认用的是sRGB,这是一个很老、也很通用的标准。近几年的iPhone拍照默认使用Display P3,它比sRGB范围更大,能表示更饱和的红色、绿色和橙色。所以叫"广色域"。

关键在于:一张图片里每个像素存的只是三个数字,比如(255,0,0)。这三个数字本身没有颜色含义,要靠文件里附带的"色彩配置"(ICC配置文件,常简称ICC)来解释——同样是(255,0,0),按sRGB解释是普通的红,按Display P3解释是更浓、更艳的红。iPhone拍出的照片会在文件里嵌一份Display P3的ICC,支持色彩管理的看图软件和浏览器读到它,才能把颜色显示对。

理解了这一点,"颜色变淡"的原因就很好解释了:要么数字被改了,要么解释数字的那份说明书丢了。

颜色变淡的两种原因

原因一:处理前先被转换成了sRGB

浏览器在解码图片时会做色彩管理:把图片的颜色换算到它内部工作的色彩空间里,这样显示出来是对的。很多网页图片工具用Canvas(网页里的画布)来做旋转、裁剪、缩放,而常见的写法拿到的是已经被换算成sRGB的像素。sRGB装不下P3里最饱和的那部分颜色,它们会被"压"到sRGB的边界上。

这种情况下,你在屏幕上看到的预览是"正确的sRGB版本",导出的也是一张标准sRGB图。普通颜色几乎没差别,但花瓣、霓虹灯、鲜艳的衣服这类高饱和区域就会变平、变淡,而且这是不可逆的——原图里多出来的那部分颜色信息已经没了。

原因二:数字没变,但色彩配置丢了

另一种情况恰好相反:工具设法拿到了原始像素数字,处理完写成新文件时却没有把原图的ICC带上,或者编码器自己塞进了一份sRGB的ICC。于是原本按P3解释的数字,被看图软件按sRGB去解释。P3里的(255,0,0)被当成sRGB的(255,0,0)显示,整张图都会偏淡,看起来像蒙了一层灰。

这两种原因的结果看起来差不多,但修法完全不同:第一种要避免换算,第二种要把ICC原样写回去。很多工具只解决了其中一个。

不同浏览器的行为并不一致

你可能会想:浏览器能不能直接给我原始数字?部分浏览器可以。我们在桌面版Chromium上实测,用display-p3色彩空间的Canvas,加上createImageBitmap的colorSpaceConversion: 'none'选项,能拿到与文件原始解码值逐个相同的数字,导出时也会嵌入P3的ICC。

但不能指望所有环境都这样。2026年9月我们在一台Redmi Note 12 Turbo(Android 15)上测试:Chrome、小米浏览器、微信内置浏览器的原生解码都能保留P3,而夸克浏览器会先把P3压成sRGB。另外,Linux桌面版WebKit会把P3压成sRGB,可真机iPhone(iOS 26.6)上的原生解码却保留了P3——因为解码和色彩管理走的是各平台自己的图形库,同一个内核在不同系统上表现不同。结论只能以真机为准。

经验:和色彩、解码有关的结论,至少要分别在Chromium和WebKit内核、并区分"读入"和"导出"两个方向实测。只测一个浏览器、一个方向,很容易得出错误结论。

我们的做法:预览交给浏览器,导出时重新读原始数字

既然不能依赖浏览器的原生解码,最稳妥的办法是绕开它,但只绕开"会改颜色"的那一步。我们最终采用的是一条混合路径:

  1. 预览和编辑用浏览器色彩管理后的图。这样屏幕上看到的颜色是对的,编辑器不会把P3数字当成sRGB显示。
  2. 导出时,用WebAssembly(WASM)解码器重新读一遍原文件,拿到原始数字。我们用的是开源项目jSquash里的mozjpeg解码器,压缩后约63KB,只有遇到广色域照片时才加载。PNG和WebP也有对应的解码器。
  3. 把原始数字放进普通的Canvas,做旋转、裁剪、缩放。在同一个色彩空间里,这些操作只是搬运和插值数字,不会做颜色换算。预览和导出共用同一个编辑函数,保证两边结果一致。
  4. 用Canvas编码成JPEG、PNG或WebP,去掉编码器自带的色彩块,再把原图的ICC原样写回。实测Chrome用Canvas编码JPEG时会自带一份sRGB的ICC,必须先删掉,否则会和写回的P3配置冲突。

写回ICC要按格式分别处理:JPEG放在APP2段里,较大的配置要拆成多段;PNG要写一个iCCP块并重新计算校验值;WebP要从简单格式改成扩展格式(VP8X)才能放ICCP块。判断输出是什么格式时要看实际的文件字节,而不是相信请求的格式——比如在iOS上请求编码成WebP,拿回来的可能是PNG。

为什么不全程用WASM?我们测过:解码、缩放、编码全部用WASM处理一张1200万像素的照片要约2.3秒,而"WASM只负责解码、其余交给Canvas"在桌面Chromium上只要288毫秒,在WebKit上436毫秒,快了5到8倍。真机上,Redmi手机各浏览器处理1200万到2400万像素照片在0.3到1.4秒之间,颜色都没有偏差。

手机内存是另一道坎

这条路径要在导出时多解码一次,所以不能同时常驻两份大图:按4000万像素的上限算,两份未压缩的位图就要约320MB,很多手机扛不住。我们的做法是导出时才解码,并且把解码放进一个一次性的后台线程(Web Worker),用完立刻销毁,释放WASM占用的内存。在一台4GB内存的低端安卓手机上,连续多次导出后的稳定内存占用从约531MB降到约429MB。

还有一点:解码组件加载失败时,应该明确提示用户重试,而不是悄悄导出一张sRGB版本——那样用户以为保住了颜色,其实没有。

怎么验证颜色真的保住了

肉眼对比很不可靠,我们用了两条硬标准:一是用独立的解码器读回导出文件,ICC要与原图逐字节一致;二是把导出图与"对原始数字做同样几何变换"的参考结果做全图逐像素对比。在Redmi手机上,用真实iPhone拍的1200万和2400万像素P3照片旋转后导出PNG、切九宫格,全图零差异;导出JPEG和WebP平均误差分别约0.31和0.97,而先转成sRGB的对照组是7.36。

教训:验证旋转这类几何变换一定要全图对比,不能只抽查几个点。我们早期试过一个图像库的旋转功能,抽查4个点,3个看起来对,第4个其实已经变白,却被我们当成偶然放过了;全图对比才发现约四分之一的像素变成了白色。

另一个容易踩的坑是"怎么判断一张图是不是广色域"。按配置文件的名字找"sRGB"并不可靠:小米相机写入的ICC v4配置,名字是UTF-16编码,按普通文本搜索找不到"sRGB",会把普通照片误判成广色域;反过来,Chrome的图形库(Skia)生成的Display P3配置,名字里也含有"sRGB"字样。可靠的办法是读取配置里红、绿、蓝三原色的坐标,和sRGB的标准值比较。

AI处理(比如变清晰)怎么办

AI放大、抠图这类处理通常在服务器上完成,模型一般按sRGB理解像素。我们在"变清晰"功能上的做法是:上传原始数字、不附带色彩配置,拿回结果后再把原图的ICC写回去。用一张P3测试图做2倍放大对比,与原图的平均色差(ΔE)从13.6降到4.9,其中鲜艳的红、绿、橙黄从27到37降到2到9;灰、白、黑两种做法结果一致。写回失败时,结果照常保存,同时提示颜色可能略淡。

给普通用户的建议

  • 处理完先和原图并排看最鲜艳的区域(红花、绿叶、霓虹),变淡通常一眼就能看出来。
  • 用能显示色彩配置的工具检查导出文件,确认它仍然是Display P3。例如命令行工具exiftool可以查看文件里的ICC描述。
  • 如果工具说明里写着"自动转换为sRGB",它属于第一种情况:发微信、发网页通常没问题,但想保留最鲜艳的颜色就别用它做最终导出。
  • 多张照片拼在一起时,一张图只能有一个色彩配置,统一转成sRGB是合理的折中,前提是正确换算,而不是丢掉配置。

本文的做法已用在Jinlab的图片工具pixforge里:旋转、裁剪、缩放、转格式、九宫格这些基础功能都会保留原图的广色域颜色和色彩配置。相关文章:CMYK图片在浏览器里怎么准确转成RGB。