EXIF 自动方向修正、正交旋转与任意角度旋转
在 libvips 中,“旋转图片”根据角度与元数据的不同,对应三套完全不同的底层算子。混用它们不仅会浪费 CPU,还可能导致方向被重复旋转两次,或者出现意料之外的黑角背景。


三类旋转算子的职责边界
Section titled “三类旋转算子的职责边界”| 旋转场景 | 底层算子 | 是否插值 | 输出画布尺寸 | 典型用途 |
|---|---|---|---|---|
| 按 EXIF Orientation 自动扶正 | vips_autorot |
否(精确像素重排) | 90°/270° 时宽高互换 | 手机拍摄照片上传后的方向归一化 |
| 90° / 180° / 270° 正交旋转 | vips_rot |
否(精确像素重排) | 90°/270° 时宽高互换 | 用户手动点击“向左/向右旋转 90°” |
| 任意角度旋转與缩放 | vips_similarity / vips_affine |
是(双线性/双三次插值) | 外接矩形扩大,四角填充背景色 | 倾斜校正、创意排版、透视/仿射矩阵变换 |
1. 按 EXIF Orientation 自动扶正(AutoRotate / autoOrient)
Section titled “1. 按 EXIF Orientation 自动扶正(AutoRotate / autoOrient)”智能手机相机通常固定以传感器原生方向写入像素,再把拍摄时的握持角度记录在 EXIF Orientation 标签(取值 1..8)中。如果在裁剪或加水印前没有先扶正方向,水印就会被印在错误的角落。
调用 AutoRotate(sharp 中为 .autoOrient() 或无参 .rotate())会根据 EXIF 标签完成翻转/90°旋转,并自动移除或重置 EXIF Orientation 标签为 1,防止浏览器在展示时再次旋转。
package main
import ( "fmt" "os"
"github.com/davidbyttow/govips/v2/vips")
func main() { if err := vips.Startup(nil); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer vips.Shutdown()
image, err := vips.NewImageFromFile("input.jpg") if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } defer image.Close()
// 根据 EXIF Orientation 自动扶正像素并重置 Orientation 标签 if err := image.AutoRotate(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) }
data, _, err := image.ExportWebp(vips.NewWebpExportParams()) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if err := os.WriteFile("autorotated.webp", data, 0o644); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) }}import sharp from 'sharp';
// autoOrient()(或无参数的 rotate())根据 EXIF Orientation 扶正图像并重置标签await sharp('input.jpg') .autoOrient() .webp({ quality: 82 }) .toFile('autorotated.webp');import pyvips
image = pyvips.Image.new_from_file("input.jpg")upright = image.autorot()upright.write_to_file("autorotated.webp", Q=82)#!/usr/bin/env bashset -euo pipefail
vips autorot input.jpg 'autorotated.webp[Q=82]'2. 正交 90° / 180° / 270° 旋转(Rotate / rot)
Section titled “2. 正交 90° / 180° / 270° 旋转(Rotate / rot)”当旋转角度严格为 90° 的整数倍时,应使用正交旋转算子。它直接重排像素坐标,不经过插值重采样,因此不会引入任何模糊,也不会产生四角背景区域:
// 支持 vips.Angle0、vips.Angle90、vips.Angle180、vips.Angle270(顺时针)if err := image.Rotate(vips.Angle90); err != nil { return err}// 传入 90、180 或 270 时,sharp 自动走快速正交旋转路径await sharp('input.jpg') .rotate(90) .webp({ quality: 82 }) .toFile('rot90.webp');import pyvips
image = pyvips.Image.new_from_file("input.jpg")# 支持 "d0", "d90", "d180", "d270" 或便捷方法 rot90() / rot180() / rot270()rotated = image.rot("d90")rotated.write_to_file("rot90.webp", Q=82)vips rot input.jpg 'rot90.webp[Q=82]' d903. 任意角度旋转与 2×2 仿射变换(Similarity / Affine)
Section titled “3. 任意角度旋转与 2×2 仿射变换(Similarity / Affine)”对于非 90° 倍数的任意角度(如本页顶部展示的顺时针 30°),libvips 使用相似变换(vips_similarity)计算外接画布并对像素插值。原图 1938 × 1292 旋转 30° 后,外接矩形扩展为 2324 × 2088,新露出的四个三角形角落由 background 参数填充(若未指定,默认填充黑色 [0, 0, 0])。
如果需要同时执行剪切(Shear)或非等比仿射矩阵变换(2×2 矩阵 [a, b, c, d]),sharp、pyvips 与 vips CLI 暴露了 affine 接口,而 govips v2.19.0 公开层仅暴露了等比缩放加旋转的 Similarity(与二维坐标查找表重映射 Mapim):
// Similarity(scale, angle, backgroundColor, idx, idy, odx, ody)bg := &vips.ColorRGBA{R: 29, G: 45, B: 53, A: 255}if err := image.Similarity(1.0, 30.0, bg, 0, 0, 0, 0); err != nil { return err}// 1. 任意角度旋转并指定填充背景色await sharp('input.jpg') .rotate(30, { background: { r: 29, g: 45, b: 53 } }) .webp({ quality: 82 }) .toFile('rotate-30.webp');
// 2. 通用 2×2 仿射矩阵变换 (例如水平微倾 shear)await sharp('input.jpg') .affine([1, 0.15, 0, 1], { background: '#1d2d35' }) .webp({ quality: 82 }) .toFile('sheared.webp');import pyvips
image = pyvips.Image.new_from_file("input.jpg")
# 1. 任意角度旋转rotated = image.similarity(scale=1.0, angle=30.0, background=[29, 45, 53])rotated.write_to_file("rotate-30.webp", Q=82)
# 2. 通用 2×2 仿射矩阵 [a, b, c, d]sheared = image.affine([1.0, 0.15, 0.0, 1.0], background=[29, 45, 53])sheared.write_to_file("sheared.webp", Q=82)# 1. 任意角度旋转vips similarity input.jpg 'rotate-30.webp[Q=82]' --angle 30 --background 29,45,53
# 2. 通用 2×2 仿射矩阵变换vips affine input.jpg 'sheared.webp[Q=82]' "1 0.15 0 1" --background 29,45,53