Cube 开发踩坑实录(一):为啥我的图片是歪的
前言
精弘存储立方(简称 Cube)是我们内部自研的对象存储中间件,支持对传统文件系统和 AWS S3 进行连接和操作,为我们其他项目提供了高效统一的对象存储服务。在此之外,我们也根据实际业务需求提供了许多针对图片的特殊功能,例如 Webp 转码存储和缩略图功能等。
然而在开发这些图片功能的过程中,踩过的坑真是数不胜数,正好写两篇博客分享一下这些有趣的问题和最终的解决方法。
发现问题
在论坛上线测试服之后,一位用户在发布广东美食相关的帖子时,所拍摄的图片方向完全错掉了,用户需要旋转自己的脖子才能看到正确的图像。
在经过一轮测试后,我们发现这个问题只会出现在苹果设备拍摄的照片上,而在其他设备上则没有问题。
一开始我只以为是苹果拍摄时的方向策略有问题,所以并没有太放在心上,毕竟图片上传的逻辑就是原样传上去然后送到 Cube 进行转码存储,不应该会在这上面出问题。
直到技术面试那天我和毛衣闲聊时,偶然提到图片元数据这个点,才想起问题出在哪:
苹果系统拍照后会把图像朝向信息存储在 EXIF 元数据中,而 Golang 官方的 image
库进行解码时只会读取像素数据,并不会根据元数据进行相应的转换处理。
尝试保留元数据
发现问题后,我脑袋中立刻想到的第一个解决方法就是在编码图片时保留 EXIF 元数据。
然而经过一番尝试后,我发现这条路行不通。
首先,Cube 中使用 github.com/chai2010/webp
库进行 Webp 编码,而这个库只对 libwebp
的基础编码接口进行了 Golang 封装,并没有暴露 EXIF 的写入接口,这意味着我们只能自己提取元数据,然后手动封装成 EXIF chunk
并插入到 RIFF
容器中。
其次,保留元数据也不利于后续的图像处理,例如在对图片进行缩略图生成时,又要重新进行一遍上述操作,来把元数据封装到 JPEG 缩略图中。
所以最终我还是选择了另一个简单粗暴的方案:直接在上传图片时就对图片进行方向修正,然后再进行编码。
方向修正
方向修正的实现也比较简单,先读取 EXIF 元数据,然后根据 Orientation
标签的值对图像进行旋转处理即可。
读取元数据可以用 github.com/rwcarlsen/goexif/exif
这个库,将文件读取为 io.Reader
后传入解码器即可。
x, err := exif.Decode(exifData)
if err != nil {
return nil, err
}
// 读取 Orientation 标签的值
tag, err := x.Get(exif.Orientation)
if err != nil {
return nil, err
}
// 转换为整数
orientation, err := tag.Int(0)
if err != nil {
return nil, err
}
旋转图片的操作可以用 Golang 官方的 image
库进行,不过需要手动映射像素,比较麻烦。
我们可以使用 github.com/disintegration/imaging
这个库,它封装了 FlipH
、FlipV
、Rotate180
等常用的旋转操作。
以下是根据不同 Orientation
值进行旋转的代码:
// applyOrientation 根据 EXIF Orientation 调整图像方向
func applyOrientation(img image.Image, orientation int) image.Image {
switch orientation {
case 1: // 正常
return img
case 2: // 水平翻转
return imaging.FlipH(img)
case 3: // 旋转 180°
return imaging.Rotate180(img)
case 4: // 垂直翻转
return imaging.FlipV(img)
case 5: // 顺时针 90° + 水平翻转
return imaging.FlipH(imaging.Rotate270(img))
case 6: // 顺时针 90°
return imaging.Rotate270(img)
case 7: // 顺时针 90° + 垂直翻转
return imaging.FlipV(imaging.Rotate270(img))
case 8: // 逆时针 90°
return imaging.Rotate90(img)
default:
return img
}
}
最后在执行 webp.Encode
函数前调用 applyOrientation
函数即可。
坐现成的轮椅
既然已经引入了 imaging
这个库,那它还能帮我们简化更多操作吗?
当然可以,其实 imaging
库内置的解码函数已经提供了自动处理方向的选项,可以直接替换掉 image
库的解码函数。
// Before
img, _, err := image.Decode(reader)
if err != nil {
return nil, err
}
// 此处省略读取 orientation 的代码
img = applyOrientation(img, orientation)
// After
img, err := imaging.Decode(reader, imaging.AutoOrientation(true))
if err != nil {
return nil, err
}
此外,缩略图的自动缩放逻辑也能直接用现成的 imaging.Fit
替代。
// Before
func resizeIfNeeded(img image.Image, targetLongSide int) image.Image {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
longSide, shortSide := max(w, h), min(w, h)
if longSide <= targetLongSide {
return img
}
scale := float64(targetLongSide) / float64(longSide)
targetShort := int(float64(shortSide) * scale)
var tw, th int
if w > h {
tw, th = targetLongSide, targetShort
} else {
tw, th = targetShort, targetLongSide
}
dst := image.NewRGBA(image.Rect(0, 0, tw, th))
draw.Lanczos.Scale(dst, dst.Rect, img, b, draw.Over, nil)
return dst
}
// After
func resizeIfNeeded(img image.Image, targetLongSide int) image.Image {
return imaging.Fit(img, targetLongSide, targetLongSide, imaging.Lanczos)
}
由此可见,image
库自带的处理函数功能精简,只提供了最基础的操作和绘图函数,而 imaging
库则提供了完善的封装和更多高级功能,简直是超级轮椅般的存在。
问题解决
将修复后的版本同步到测试服后,iPhone 用户拍的照片终于方向正常了。