把一张图准备好再发布
缩放到图片实际会被展示的尺寸,为高密度屏幕最多做到两倍。按版式需要的形状去裁剪,而不是让浏览器把它压变形。一直压缩到瑕疵刚刚出现,然后退回一档。元数据最后再剥离。顺序很重要:第一步之后的每一步,处理的像素都要少得多。
尺寸是第一位的
像素数随宽度的平方增长,所以尺寸上不大的差别,在数据量上就是巨大的差别。一张以 4000 像素宽存储、却显示在 800 像素栏位里的图,携带的数据大约是它所需的二十五倍,而任何质量参数都挽回不了这一点。
| 存储尺寸 | 像素数 | 相对 800px 的数据量 |
|---|---|---|
| 4000 x 2667 | 1070 万 | 25 倍 |
| 1600 x 1067 | 170 万 | 4 倍 |
| 800 x 533 | 43 万 | 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,再把其余的一切剥掉。