把一张图准备好再发布

在把图片放上页面之前如何确定尺寸、裁剪、重采样和压缩,包括像素比、质量参数、抠图边缘光晕、EXIF 旋转和元数据剥离。

缩放到图片实际会被展示的尺寸,为高密度屏幕最多做到两倍。按版式需要的形状去裁剪,而不是让浏览器把它压变形。一直压缩到瑕疵刚刚出现,然后退回一档。元数据最后再剥离。顺序很重要:第一步之后的每一步,处理的像素都要少得多。

尺寸是第一位的

像素数随宽度的平方增长,所以尺寸上不大的差别,在数据量上就是巨大的差别。一张以 4000 像素宽存储、却显示在 800 像素栏位里的图,携带的数据大约是它所需的二十五倍,而任何质量参数都挽回不了这一点。

存储尺寸像素数相对 800px 的数据量
4000 x 26671070 万25 倍
1600 x 1067170 万4 倍
800 x 53343 万1 倍

要选定目标宽度,就去量这张图在版式里被渲染出来的最大宽度,而不是屏幕能达到的最大宽度。三列网格里的一张卡片,可能永远只有 380 像素宽。如果一个文件要服务多个位置,就按其中最大的那个来定尺寸。

设备像素、CSS 像素,以及关于 DPI 的迷思

CSS 像素是一个版式单位,不是一个物理点。设备像素比是屏幕为每个 CSS 像素绘制多少个设备像素:较老的显示器是 1,多数现代手机和笔记本是 2,某些手机是 3。一个 800 CSS 像素宽的栏位在比例为 2 时是用 1600 个设备像素绘制的,这就是为什么按 CSS 宽度定尺寸的图片看起来发虚。

实用规则是 2 倍。按展示宽度的两倍导出,让浏览器去缩小。做到 3 倍要多花 125% 的数据,而在正常观看距离下没几个人看得出差别。

文件里存储的 DPI 和 PPI 对网页毫无意义。它们只是一个头部数字,说明这个文件应该以多大的物理尺寸打印,而浏览器完全忽略它。

printed width in inches = pixel width / PPI

3000 px / 300 ppi = 10.0 in
3000 px /  72 ppi = 41.7 in    same file, same 3000 pixels

以“300 DPI”导出既不会增加像素,也不会改变任何图像数据。

重采样做了什么

缩小意味着许多源像素合并成一个。比新网格更细的细节无法被表示,而如果重采样器只采样了很少的源像素、而不是在整个区域上做平均,那些细节就会折叠回来变成虚假的花纹:锯齿状的斜线,以及细密重复纹理上的摩尔纹,比如条纹衬衫或者带一像素边框的表格。好的缩小会把每一个有贡献的像素都平均进去,而且之后通常需要稍微锐化一点。

放大不会创造细节。插值只是在已有的像素之间猜测数值,所以结果要么发虚要么发块。机器学习的放大工具则是编造出貌似合理的细节,这和恢复细节不是一回事。

滤波器工作方式结果
最近邻复制最近的那个源像素不做混合。适合像素画和整数倍缩放,其他情况会有锯齿
双线性对周围四个像素求平均快而发虚,大幅缩小时丢细节
Lanczos在更大的邻域上做加窗 sinc 运算缩小最锐利,在极高对比的边缘会有光晕

长宽比:裁剪、加黑边还是拉伸

裁剪让主体保持正确的比例,把边缘丢掉,这是正确的默认做法。加黑边保留了整个画面,但会添上边条,还得选一个边条颜色;当什么都不能被切掉时它才值得用,比如美术作品或示意图。拉伸会造成变形,而人眼在人脸、圆形和文字上会立刻察觉。

.tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;   /* crop; contain = letterbox, fill = stretch */
}

当同一个文件要以多种形状出现时,请用安全区的思路来考虑:也就是在每一种裁剪下都能存活的那块区域。16:9 的横幅切掉上下,1:1 的方块和 4:5 的卡片切掉两侧,所以它们共同保留的是穿过中央的一条带。把主体和任何文字都放在这条带里面,否则一张横构图照片里靠近边缘的人脸会被方形裁剪砍掉脑袋。

找到合适的质量参数

质量数字不是任何东西的百分比,而且不同编码器之间的刻度也不可比。JPEG 80、WebP 80 和 AVIF 80 是三个互不相干的设置。

方法对它们都一样。用几个不同的设置导出,按它实际会被展示的尺寸去看结果,检查瑕疵最先在哪里浮现:平滑的渐变,比如天空,会出现色带和斑块;硬边缘和文字会出现振铃。找到损伤开始变得可见的位置,退回一档,停下。

同一个固定数值对每张图都是错的,因为内容决定了能丢掉多少:一张有噪点、有纹理的照片需要更多比特才能撑住,而一张平坦的插画在某个设置下就崩了,同样的设置对一张画面繁忙的照片却毫无影响。

内容起始值
按展示尺寸的照片JPEG 78 到 85,WebP 75 到 82,AVIF 50 到 62
2 倍尺寸的照片再低五到十个点
截图、纯色图形、文字无损,或者有损但取 90 以上

抠图、柔边与光晕

当图片必须摆在不止一种背景色上、或者要与其他元素重叠时,去掉背景是正确的:产品图、标志、被合成进版式里的人物。而当边界确实很细碎时,比如头发、树叶或玻璃,去背就是错的,因为一个糟糕的遮罩比一个老老实实的矩形更难看。

硬边缘是二元的,每个像素要么完全不透明、要么完全透明,这适合纯色图形,但在任何照片素材上都会出现阶梯状锯齿。柔性遮罩允许分数级的不透明度,所以一个像素可以是 40% 属于主体,这也是让头发看起来正常的唯一办法。

光晕来自边界上的那些像素。镜头虚化和抗锯齿早已把主体的颜色和原来的背景混在了一起,所以每个这样的像素都满足:

observed = alpha * foreground + (1 - alpha) * background

背景去除只计算 alpha。留在那些像素里的颜色仍然是被污染的,所以从白色影棚照里抠出来的主体会带着一圈淡淡的边:在白色页面上看不见,在深色页面上一目了然。真正的抠像还会把前景颜色解出来,这项功能通常叫作去边(defringe)或颜色净化(decontaminate colours)。如果做不到这一点,就把遮罩向内收缩零点几个像素,再重新做一次羽化。

旋转与元数据

一张图在某个程序里是正的、在另一个程序里却是横的,几乎总是 EXIF 方向标志在起作用。相机存储的是未经旋转的传感器图像,再加上一个标签说明该怎么转它。读取这个标签的软件会正确显示;忽略它的软件,包括很多图像库和缩略图生成器,会把原始像素横着显示出来。

方向值含义
1存储时就是正的
3旋转 180 度
6顺时针旋转 90 度
8逆时针旋转 90 度
2, 4, 5, 7各种镜像变体

解决办法是把旋转烘焙进像素里,再把标签设为 1 或者删掉它。以 90 度为步长旋转只是重新排列像素,不会重采样它们。要当心双重旋转:像素已经转过了,标签却还在,于是每一个遵守规范的查看器都会再转一次。

剥离元数据是一项隐私措施,不是收拾房间。EXIF、IPTC 和 XMP 数据块惯常携带着精确到几米的 GPS 坐标、拍摄时间、相机品牌、型号和序列号、所有者姓名,有时还有一张内嵌的缩略图,上面仍然是这张图被裁剪之前的样子。一张在家里拍的照片原样发出去,等于公布了家庭住址。

最后再剥离它,但有一个例外:ICC 颜色描述文件就挨着那些元数据,把它从一张广色域图片上删掉,留下的数字会被当成 sRGB 来读,表现为颜色发平或者过饱和。先转换到 sRGB,再把其余的一切剥掉。