Exif: Refactor JPEG rotation based on orientation flag #1064

We now manually detect and change the rotation, the imaging
autorotation functionality was disabled for our core use-cases.

anymore.
This commit is contained in:
Michael Mayer 2021-02-21 22:53:25 +01:00
parent 1d108199ef
commit 01d4b1ee31
13 changed files with 99 additions and 52 deletions

6
go.sum
View file

@ -49,13 +49,9 @@ github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5/go.mod h1:9EXlP
github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0=
github.com/dsoprea/go-exif/v2 v2.0.0-20200807075213-089aa48c91e6 h1:jkmCBceHmez4ArDFqcIrjFhPTTIV2IlWiF/QTeubgOs=
github.com/dsoprea/go-exif/v2 v2.0.0-20200807075213-089aa48c91e6/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
github.com/dsoprea/go-exif/v2 v2.0.0-20210131231135-d154f10435cc h1:F8AmoUFkSqzbZoGrIGWpQqbh3qosJl3h8zdVusOOggQ=
github.com/dsoprea/go-exif/v2 v2.0.0-20210131231135-d154f10435cc/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20200807075213-089aa48c91e6 h1:AWLaaemM6TvO4DVwMtXibJKpWWfyw+tiZwYUiueLPzE=
github.com/dsoprea/go-exif/v3 v3.0.0-20200807075213-089aa48c91e6/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-exif/v3 v3.0.0-20210131231135-d154f10435cc h1:WlJC9DefVe1OZKM04jD7jInkZ9Oyou+K6cpYOVPXq0o=
github.com/dsoprea/go-exif/v3 v3.0.0-20210131231135-d154f10435cc/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200717090456-b3d9dcddffd1 h1:R/EEzpxqQxeEcJ/z0EFTI1U6XsuOnepyp5o1uZg5c2E=
github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200717090456-b3d9dcddffd1/go.mod h1:UwRKreeVikXn5OarSnt4OqovcEjsIgZVuc5svj7G5w4=
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4=
@ -126,8 +122,6 @@ github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXg
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210108004804-a63082ebfb66 h1:wNA26/2ftrz6nI4dbIim6OSKtLlNdjpNiwFB+l/yqtQ=
github.com/golang/geo v0.0.0-20210108004804-a63082ebfb66/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

View file

@ -102,7 +102,7 @@ func AlbumCover(router *gin.RouterGroup) {
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}
@ -214,7 +214,7 @@ func LabelCover(router *gin.RouterGroup) {
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}

View file

@ -111,7 +111,7 @@ func GetFolderCover(router *gin.RouterGroup) {
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}

View file

@ -139,7 +139,7 @@ func GetThumb(router *gin.RouterGroup) {
var thumbnail string
if conf.ThumbUncached() || thumbType.OnDemand() {
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err = thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
} else {
thumbnail, err = thumb.FromCache(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
}

View file

@ -98,7 +98,7 @@ func SharePreview(router *gin.RouterGroup) {
return
}
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
if err != nil {
log.Error(err)
@ -128,7 +128,7 @@ func SharePreview(router *gin.RouterGroup) {
return
}
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err := thumb.FromFile(fileName, f.FileHash, conf.ThumbPath(), thumbType.Width, thumbType.Height, f.FileOrientation, thumbType.Options...)
if err != nil {
log.Error(err)

View file

@ -118,6 +118,14 @@ func ImportWorker(jobs <-chan ImportJob) {
continue
}
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
log.Debugf("import: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("import: %s created", filepath.Base(jsonName))
}
}
if f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := imp.convert.ToJpeg(f); err != nil {
log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), txt.Quote(fs.RelName(destMainFileName, imp.originalsPath())))
@ -136,14 +144,6 @@ func ImportWorker(jobs <-chan ImportJob) {
}
}
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
log.Debugf("import: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("import: %s created", filepath.Base(jsonName))
}
}
related, err := f.RelatedFiles(imp.conf.Settings().StackSequences())
if err != nil {

View file

@ -27,6 +27,14 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
return result
}
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("index: %s created", filepath.Base(jsonName))
}
}
if opt.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", txt.Quote(f.BaseName()), err.Error())
@ -47,14 +55,6 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
}
}
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("index: %s created", filepath.Base(jsonName))
}
}
result = ind.MediaFile(f, opt, "")
if result.Indexed() && f.IsJpeg() {
@ -108,6 +108,14 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
continue
}
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("index: %s created", filepath.Base(jsonName))
}
}
if opt.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := ind.convert.ToJpeg(f); err != nil {
result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", txt.Quote(f.BaseName()), err.Error())
@ -128,14 +136,6 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
}
}
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
log.Debugf("index: %s in %s (extract metadata)", txt.Quote(err.Error()), txt.Quote(f.BaseName()))
} else {
log.Debugf("index: %s created", filepath.Base(jsonName))
}
}
res := ind.MediaFile(f, opt, "")
if res.Indexed() && f.IsJpeg() {

View file

@ -877,7 +877,7 @@ func (m *MediaFile) Megapixels() int {
return int(math.Round(float64(m.Width()*m.Height()) / 1000000))
}
// Orientation returns the orientation of a MediaFile.
// Orientation returns the Exif orientation of the media file.
func (m *MediaFile) Orientation() int {
if data := m.MetaData(); data.Error == nil {
return data.Orientation
@ -895,7 +895,7 @@ func (m *MediaFile) Thumbnail(path string, typeName string) (filename string, er
return "", fmt.Errorf("media: invalid type %s", typeName)
}
thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, thumbType.Width, thumbType.Height, thumbType.Options...)
thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, thumbType.Width, thumbType.Height, m.Orientation(), thumbType.Options...)
if err != nil {
err = fmt.Errorf("media: failed creating thumbnail for %s (%s)", txt.Quote(m.BaseName()), err)
@ -914,7 +914,7 @@ func (m *MediaFile) Resample(path string, typeName string) (img image.Image, err
return nil, err
}
return imaging.Open(filename, imaging.AutoOrientation(true))
return imaging.Open(filename)
}
// ResampleDefault pre-renders default thumbnails.
@ -957,13 +957,15 @@ func (m *MediaFile) ResampleDefault(thumbPath string, force bool) (err error) {
}
if originalImg == nil {
img, err := imaging.Open(m.FileName(), imaging.AutoOrientation(true))
img, err := imaging.Open(m.FileName())
if err != nil {
log.Errorf("media: %s in %s", err.Error(), txt.Quote(m.BaseName()))
return err
}
img = thumb.Rotate(img, m.Orientation())
originalImg = img
}

View file

@ -113,23 +113,23 @@ func TestThumb_FromFile(t *testing.T) {
}
t.Run("valid parameter", func(t *testing.T) {
fileModel := &entity.File{
file := &entity.File{
FileName: conf.ExamplesPath() + "/elephants.jpg",
FileHash: "1234568889",
}
thumbnail, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
thumbnail, err := thumb.FromFile(file.FileName, file.FileHash, thumbsPath, 224, 224, file.FileOrientation)
assert.Nil(t, err)
assert.FileExists(t, thumbnail)
})
t.Run("hash too short", func(t *testing.T) {
fileModel := &entity.File{
file := &entity.File{
FileName: conf.ExamplesPath() + "/elephants.jpg",
FileHash: "123",
}
_, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
_, err := thumb.FromFile(file.FileName, file.FileHash, thumbsPath, 224, 224, file.FileOrientation)
if err == nil {
t.Fatal("err should NOT be nil")
@ -138,12 +138,12 @@ func TestThumb_FromFile(t *testing.T) {
assert.Equal(t, "resample: file hash is empty or too short (123)", err.Error())
})
t.Run("filename too short", func(t *testing.T) {
fileModel := &entity.File{
file := &entity.File{
FileName: "xxx",
FileHash: "12367890",
}
_, err := thumb.FromFile(fileModel.FileName, fileModel.FileHash, thumbsPath, 224, 224)
_, err := thumb.FromFile(file.FileName, file.FileHash, thumbsPath, 224, 224, file.FileOrientation)
if err == nil {
t.FailNow()
}

View file

@ -124,7 +124,7 @@ func FromCache(imageFilename, hash, thumbPath string, width, height int, opts ..
return "", ErrThumbNotCached
}
func FromFile(imageFilename, hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
func FromFile(imageFilename, hash, thumbPath string, width, height, orientation int, opts ...ResampleOption) (fileName string, err error) {
if fileName, err := FromCache(imageFilename, hash, thumbPath, width, height, opts...); err == nil {
return fileName, err
} else if err != ErrThumbNotCached {
@ -138,13 +138,17 @@ func FromFile(imageFilename, hash, thumbPath string, width, height int, opts ...
return "", err
}
img, err := imaging.Open(imageFilename, imaging.AutoOrientation(true))
img, err := imaging.Open(imageFilename)
if err != nil {
log.Errorf("resample: %s in %s", err, txt.Quote(filepath.Base(imageFilename)))
return "", err
}
if orientation > 1 {
img = Rotate(img, orientation)
}
if _, err := Create(img, fileName, width, height, opts...); err != nil {
return "", err
}

View file

@ -222,7 +222,7 @@ func TestFromFile(t *testing.T) {
assert.FileExists(t, src)
fileName, err := FromFile(src, "123456789098765432", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...)
fileName, err := FromFile(src, "123456789098765432", "testdata", colorThumb.Width, colorThumb.Height, OrientationNormal, colorThumb.Options...)
if err != nil {
t.Fatal(err)
@ -239,7 +239,7 @@ func TestFromFile(t *testing.T) {
assert.NoFileExists(t, src)
fileName, err := FromFile(src, "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...)
fileName, err := FromFile(src, "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, OrientationNormal, colorThumb.Options...)
assert.Equal(t, "", fileName)
assert.Error(t, err)
@ -247,7 +247,7 @@ func TestFromFile(t *testing.T) {
t.Run("empty filename", func(t *testing.T) {
colorThumb := Types["colors"]
fileName, err := FromFile("", "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, colorThumb.Options...)
fileName, err := FromFile("", "193456789098765432", "testdata", colorThumb.Width, colorThumb.Height, OrientationNormal, colorThumb.Options...)
if err == nil {
t.Fatal("error expected")

47
internal/thumb/rotate.go Normal file
View file

@ -0,0 +1,47 @@
package thumb
import (
"image"
"github.com/disintegration/imaging"
)
const (
OrientationUnspecified int = 0
OrientationNormal = 1
OrientationFlipH = 2
OrientationRotate180 = 3
OrientationFlipV = 4
OrientationTranspose = 5
OrientationRotate270 = 6
OrientationTransverse = 7
OrientationRotate90 = 8
)
// Rotate rotates an image based on the Exif orientation.
func Rotate(img image.Image, o int) image.Image {
switch o {
case OrientationUnspecified:
// Do nothing.
case OrientationNormal:
// Do nothing.
case OrientationFlipH:
img = imaging.FlipH(img)
case OrientationFlipV:
img = imaging.FlipV(img)
case OrientationRotate90:
img = imaging.Rotate90(img)
case OrientationRotate180:
img = imaging.Rotate180(img)
case OrientationRotate270:
img = imaging.Rotate270(img)
case OrientationTranspose:
img = imaging.Transpose(img)
case OrientationTransverse:
img = imaging.Transverse(img)
default:
log.Debugf("rotate: invalid orientation %d", o)
}
return img
}

View file

@ -104,7 +104,7 @@ func (worker *Share) Start() (err error) {
continue
}
srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbPath(), thumbType.Width, thumbType.Height, thumbType.Options...)
srcFileName, err = thumb.FromFile(srcFileName, file.File.FileHash, worker.conf.ThumbPath(), thumbType.Width, thumbType.Height, file.File.FileOrientation, thumbType.Options...)
if err != nil {
worker.logError(err)