feat: 增加 EXIF 旋转图片自动转正

- 解析 JPEG EXIF Orientation,方向 5/6/7/8 的竖拍横存图不再误入横屏目录
- 配置 ffmpeg 时用 ffmpeg 物理转正像素并保留原 EXIF;无 ffmpeg 时按显示方向归类
- 新增 ffmpeg.path / organize.rotateImages 配置项
- 补充 EXIF 解析与旋转幂等测试
This commit is contained in:
2025-05-19 21:15:58 +08:00
parent 0c96a7bb63
commit 66faed75a2
7 changed files with 459 additions and 6 deletions
+41 -1
View File
@@ -56,6 +56,7 @@ const (
type Organizer struct {
pickerDir string // 整理后的文件都放到这个根目录下
ffprobePath string // ffprobe 程序路径;为空表示没有可用解码器
ffmpegPath string // ffmpeg 程序路径;为空表示没有转正 EXIF 旋转图片的能力
store *store.Store // 数据库;为 nil 表示不记录媒体信息
mover *mover.Mover // 负责实际搬文件的家伙
buckets map[string][]string // 目标目录 → 里面该放哪些文件(只记源路径)
@@ -65,10 +66,11 @@ type Organizer struct {
}
// New 创建整理器。store 可以为 nil——数据库打不开时,整理照常进行,只是不记入库信息。
func New(pickerDir, ffprobePath string, st *store.Store) *Organizer {
func New(pickerDir, ffprobePath, ffmpegPath string, st *store.Store) *Organizer {
return &Organizer{
pickerDir: pickerDir,
ffprobePath: ffprobePath,
ffmpegPath: ffmpegPath,
store: st,
mover: &mover.Mover{},
buckets: make(map[string][]string),
@@ -295,6 +297,10 @@ func (o *Organizer) probeImage(f scanner.File) imageProbeResult {
return r
}
r.res = res
// 先处理 EXIF 旋转:带旋转标记的图,要么用 ffmpeg 把像素转正(文件变了),
// 要么退而求其次按"显示方向"归类(宽高对调)。
// 必须在算 MD5 / 分析明暗之前做——转正会改文件内容,得让 MD5 对得上最终文件。
o.correctImageRotation(f, r.res)
// 算 MD5(顺便拿指纹,可能省掉全量计算)
r.md5Str, r.headMd5, r.tailMd5, r.size = o.calcFileInfo(f)
// 分析明暗(GIF 不做,动画图逐帧分析没意义)
@@ -309,6 +315,40 @@ func (o *Organizer) probeImage(f scanner.File) imageProbeResult {
return r
}
// correctImageRotation 处理图片的 EXIF 旋转标记,保证分类方向正确。
//
// 手机/相机竖拍的图,文件里常横着存像素、再打一个 EXIF 旋转标记(Orientation=5~8),
// 查看器会自动转正显示,但我们读到的宽高是"横着存"的,会把竖图错判成横图。
//
// 两种处理方式(按优先级):
// 1. 配置了 ffmpeg 且允许转正:用 ffmpeg 把像素真正转正(烧进旋转),文件内容会变;
// 2. 没有 ffmpeg:不改文件,只把宽高对调,按"显示方向"归类。
//
// 只有方向 5/6/7/8(90°/270° 旋转)才需要宽高对调;2/3/4 是镜像或 180°,横竖不变。
func (o *Organizer) correctImageRotation(f scanner.File, res *probe.ProbeResult) {
ori := probe.ReadExifOrientation(f.Path)
if ori == 0 {
return // 没有旋转标记,不用管
}
// 有 ffmpeg 且允许转正:真把图转过来
if o.ffmpegPath != "" {
if err := probe.RotateImage(o.ffmpegPath, f.Path); err != nil {
fmt.Printf("=== 图片转正失败(按显示方向归类): %s, 错误: %v \n", f.Path, err)
} else {
// 转正后文件变了,重新读一遍尺寸,保证分类和实际文件一致
if res2, err := probe.ProbeImage(f.Path, o.ffprobePath); err == nil {
res.Width, res.Height = res2.Width, res2.Height
}
fmt.Printf("=== 已转正图片: %s (Orientation=%d)\n", f.Path, ori)
return
}
}
// 没有 ffmpeg(或转正失败):退而求其次,按显示方向对调宽高归类
if probe.NeedsDimensionSwap(ori) {
res.Width, res.Height = res.Height, res.Width
}
}
// ProcessImages 处理全部图片:先并行体检,再按顺序一个个归类。
func (o *Organizer) ProcessImages(images []scanner.File) {
// 统计结果,最后打一行总结给用户看