sharp默认转JPG颜色没错,ICC和EXIF却全没了

上传链路里收到 iPhone 的 HEIC,后端用 sharp 转成 JPG。这一步最先被盯上的往往是颜色,可四种写法挨个跑下来,颜色反倒是最省心的部分。sharp 0.34.5 默认输出的 JPG 颜色没问题,文件里的 ICC 配置文件却是 0 字节。EXIF 也一起没了。换成 keepIccProfile、keepMetadata、withMetadata 三种写法,留下来的东西各不一样。其中两种会把 GPS 定位原样带进输出文件。我经历过一次数据合规整改,被法务盯了三个月。比起色差,我更在意图片里还剩下什么。下面先把四种写法的输出逐项列出来,再说各自放在哪种场景。

测试环境是一台 Apple M4 的 Mac 和 sharp 0.34.5。每种写法只跑一次。样张是用 sips 从一张合成图编出来的 HEIC,尺寸 2016×1512。文件里嵌着 536 字节的 Display P3 配置文件。画面里有 12 个色块。8 个是只有 P3 才装得下的高饱和色,另外 4 个落在 sRGB 范围内。EXIF 里写了拍摄时间。GPS 是写进去的一个太平洋海面上的虚构点。它不是 iPhone 直出的原片,真机常见的 HDR、10 bit 和 nclx 色彩描述都没覆盖到。色差一律用 CIEDE2000 算。

预编译的sharp读得出HEIC却解不开

第一个坑出在转码之前。对 HEIC 直接调 metadata() 能读出格式和尺寸,536 字节的 ICC 和 EXIF 也都在,看上去像是支持的。接着调 jpeg() 就报错了,信息是 Support for this compression format has not been built in。npm 装下来的预编译包只带了 AVIF 解码,HEVC 编码的 HEIC 它解不了。metadata() 跑通并不等于能转。上传接口要是拿它探测格式,会把一批转不了的文件放进来。这次我先用 Mac 自带的 sips 把 HEIC 解成 PNG 再交给 sharp。P3 配置文件和 EXIF 在 PNG 里都还在。服务器上没有 sips 就得另找一条解码路,那条路我这次没测。

解码放到浏览器里也是一条路。我用的是图映 ImgIng imging.cn/ 的格式转换做对照,在电脑浏览器上一次转一张。这一次 3 次转换都没看到上传请求。转出来的 JPG 带着 Display P3 配置文件、拍摄时间和 GPS,色块色差都在 0.31 以内。它内部怎么解码的我不清楚。我只看了输出文件。定位在这条路上同样留了下来。后端那几种写法也得按这个前提去看。

四种写法留下的配置文件和EXIF不一样

演示脚本做的事很简单。先对 HEIC 读一次元数据、试一次转码,输出的头两行就是这两步打出来的。接着拿 sips 解出来的那张 PNG 分别走默认、keepIccProfile() 和 withMetadata() 三条路,都按质量 90 存成 JPG。存完再用 metadata() 把每个文件读回来,打印 ICC 的字节数和有没有 EXIF。下面是完整输出。

yaml 复制代码
$ node sharp_demo.js
heic metadata: heif 2016 1512 icc 536
heic decode: heif: Error while loading plugin: Support for this compression format has not been built in (11.6003)
default icc bytes: 0 exif: false
keepIccProfile icc bytes: 536 exif: false
withMetadata icc bytes: 480 exif: true

keepMetadata() 不在这段演示里,实测表里它的输出带 P3 配置文件,EXIF 和 GPS 也都在。四种合起来是下面这张表。

写法 像素 输出的 ICC 拍摄时间和 GPS 颜色
默认 转成 sRGB 没有 都丢 对(P3 高饱和色被裁剪)
keepIccProfile() 保持 P3 Display P3(536 字节) 都丢 对
withMetadata() 转成 sRGB sRGB(480 字节) 都保留 对(P3 高饱和色被裁剪)
keepMetadata() 保持 P3 Display P3 都保留 对

默认输出颜色是对的只是没标色彩空间

默认写法先把像素从 P3 换算到 sRGB,再丢掉 ICC 和 EXIF。合成图里那块肤色在文件里的 P3 数值是 189、124、88,换到 sRGB 是 199、120、81。下游按 sRGB 解读正好对上。sRGB 范围内的色块色差最大 0.37。P3 高饱和色就没办法了。sRGB 装不下它们,只能裁剪。同一批样张用 Pillow 和 sips 正确转 sRGB,那几块的色差最大也还有 7.55,这不是转错。真正会让颜色变淡的是另一种情况:P3 数值原样写进 JPG 却没带 ICC,下游又当成 sRGB 显示。这时范围内的色块色差会到 2.32 到 3.33。sharp 默认不走这条路。Pillow 默认的 save() 走的就是它。

验输出时我只信 metadata() 读回来的 icc 长度。默认写法的输出拿 Mac 的 sips 去查会报 sRGB IEC61966-2.1,文件里却一个字节的 ICC 都没有,工具显示的名字靠不住。

keepIccProfile() 的输出保持 P3 并带着 536 字节的原配置文件。保 P3 这一类写法里 12 个色块色差最大 0.40。它的前提是下游每一环都认 ICC。推断:中间要是有一环把 ICC 剥掉,这张图就会落进上面那种变淡的情况。哪些 CDN 和缩略图服务会剥,我没测。

加上withMetadata后GPS也留在文件里

withMetadata() 看名字只是把元数据带上,实际做了两件事。它给输出嵌了一份 480 字节的 sRGB 配置文件,下游不用再猜色彩空间。EXIF 也整段留了下来,拍摄时间和 GPS 都在输出文件里。keepMetadata() 更彻底。P3 配置文件、EXIF 和 GPS 全保留。这次 GPS 是虚构点。换成用户手机拍的商品图或评价晒图,那就是拍摄地点的经纬度,会跟着图片出现在公开的 URL 上。合规这块我宁可多一步,按图片最后去哪儿来分。

场景 我会用的写法 理由
对外公开、下游不确定 默认 颜色对、没有 EXIF、不靠下游认 ICC
对外公开、展示链路自己管 keepIccProfile() 保 P3 也不带 EXIF
内部素材库要拍摄时间 withMetadata() 标明 sRGB、带 EXIF、不出内网
原图归档 keepMetadata() P3、EXIF、GPS 都留

浏览器那条路和 sips 也一样,图映和 sips 这次转出来的 JPG 都留着 GPS。转格式本身不会去掉定位,想去掉得专门处理。

留拍摄时间去掉GPS的写法还没测

四种写法里没有一种是保留拍摄时间、去掉 GPS 的。素材库按时间排序要用到拍摄时间,定位却不一定该留。sharp 里有没有办法只改 EXIF 里的某几个字段,我这次没试也就不下结论。服务器端的 HEIC 解码还空着。真 iPhone 原片的 nclx 色彩描述也没覆盖。

眼下能马上做的,是在转码之后加一步读 metadata(),把 icc 长度和 exif 有没有写成断言。对外的图断言 exif 为空。归档的图断言 icc 不为 0。比起肉眼看图,这一步能直接拦下两种情况:GPS 被带了出去,ICC 被悄悄丢了。

相关推荐
用户7783366132118 分钟前
SerpBase AI Overview 引用卡片:React 组件实战
前端
林小帅9 分钟前
【笔记】Prisma v7 + SQLite TS2305 及连锁 TS7006 问题
前端·javascript·后端
HUANGZHENXUAN12319 分钟前
最大子矩阵和:从 O(n⁴) 到 O(n³) 的列压缩 + Kadane
c语言·前端
叼烟扛炮2 小时前
[LangChain] 学习笔记(03):打破失忆症——对话机制与上下文管理
java·前端·javascript·笔记·学习·langchain
IT_陈寒3 小时前
SpringBoot实战技巧总结
前端·人工智能·后端
TeamDev3 小时前
使用 JxBrowser 在桌面应用中集成 Spring
java·前端·后端·spring·混合应用·jxbrowser·浏览器控件
陈随易5 小时前
Rolldown 把插件改动回传提速 26 倍
前端·后端·程序员
笃行35012 小时前
集中式与分布式,KingbaseES 的四种形态共用一套内核
前端
陈随易13 小时前
Vue 编译器换掉 source map,快了三四成
前端·后端·程序员