feat: 增加 EXIF 旋转图片自动转正
- 解析 JPEG EXIF Orientation,方向 5/6/7/8 的竖拍横存图不再误入横屏目录 - 配置 ffmpeg 时用 ffmpeg 物理转正像素并保留原 EXIF;无 ffmpeg 时按显示方向归类 - 新增 ffmpeg.path / organize.rotateImages 配置项 - 补充 EXIF 解析与旋转幂等测试
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// buildJpegWithOrientation 拼一个带 Exif 方向标记的最小 JPEG:
|
||||
// SOI + APP1(Exif) + EOI,APP1 里只放 TIFF 头 + IFD0 一条 Orientation。
|
||||
func buildJpegWithOrientation(t *testing.T, orientation uint16, bigEndian bool) []byte {
|
||||
t.Helper()
|
||||
var order binary.ByteOrder = binary.LittleEndian
|
||||
if bigEndian {
|
||||
order = binary.BigEndian
|
||||
}
|
||||
bom := []byte("II")
|
||||
if bigEndian {
|
||||
bom = []byte("MM")
|
||||
}
|
||||
// TIFF 头(8) + IFD0 计数(2) + 一条目(12)
|
||||
tiff := make([]byte, 0, 8+2+12)
|
||||
tiff = append(tiff, bom...)
|
||||
magic := make([]byte, 2)
|
||||
order.PutUint16(magic, 42)
|
||||
tiff = append(tiff, magic...)
|
||||
ifdOff := make([]byte, 4)
|
||||
order.PutUint32(ifdOff, 8)
|
||||
tiff = append(tiff, ifdOff...)
|
||||
count := make([]byte, 2)
|
||||
order.PutUint16(count, 1)
|
||||
tiff = append(tiff, count...)
|
||||
// 一条目:tag=0x0112 type=SHORT count=1 value=orientation
|
||||
entry := make([]byte, 12)
|
||||
order.PutUint16(entry[0:2], 0x0112)
|
||||
order.PutUint16(entry[2:4], 3) // SHORT
|
||||
order.PutUint32(entry[4:8], 1) // count
|
||||
order.PutUint16(entry[8:10], orientation)
|
||||
tiff = append(tiff, entry...)
|
||||
|
||||
app1Data := append([]byte("Exif\x00\x00"), tiff...)
|
||||
segLen := len(app1Data) + 2
|
||||
out := []byte{0xFF, 0xD8} // SOI
|
||||
out = append(out, 0xFF, 0xE1)
|
||||
lenB := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(lenB, uint16(segLen))
|
||||
out = append(out, lenB...)
|
||||
out = append(out, app1Data...)
|
||||
out = append(out, 0xFF, 0xD9) // EOI
|
||||
return out
|
||||
}
|
||||
|
||||
func TestReadExifOrientation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cases := []struct {
|
||||
name string
|
||||
ori uint16
|
||||
bigEnd bool
|
||||
want int
|
||||
}{
|
||||
{"le6", 6, false, 6},
|
||||
{"be8", 8, true, 8},
|
||||
{"le1", 1, false, 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
p := filepath.Join(dir, c.name+".jpg")
|
||||
if err := os.WriteFile(p, buildJpegWithOrientation(t, c.ori, c.bigEnd), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := ReadExifOrientation(p); got != c.want {
|
||||
t.Fatalf("ReadExifOrientation = %d, want %d", got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 非 JPEG 返回 0
|
||||
p := filepath.Join(dir, "notjpeg.txt")
|
||||
if err := os.WriteFile(p, []byte("hello"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := ReadExifOrientation(p); got != 0 {
|
||||
t.Fatalf("non-jpeg ReadExifOrientation = %d, want 0", got)
|
||||
}
|
||||
// 不存在的文件返回 0
|
||||
if got := ReadExifOrientation(filepath.Join(dir, "missing.jpg")); got != 0 {
|
||||
t.Fatalf("missing ReadExifOrientation = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedsDimensionSwap(t *testing.T) {
|
||||
cases := map[int]bool{
|
||||
1: false, 2: false, 3: false, 4: false,
|
||||
5: true, 6: true, 7: true, 8: true,
|
||||
0: false, 9: false,
|
||||
}
|
||||
for ori, want := range cases {
|
||||
if got := NeedsDimensionSwap(ori); got != want {
|
||||
t.Fatalf("NeedsDimensionSwap(%d) = %v, want %v", ori, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user