Skip to content

Commit

Permalink
Adjust image dimensions for exif orientation (#5188)
Browse files Browse the repository at this point in the history
  • Loading branch information
WithoutPants authored Sep 3, 2024
1 parent a3c34a5 commit 899ee71
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 23 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cast v1.6.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,8 @@ github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiS
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
Expand Down
75 changes: 75 additions & 0 deletions pkg/file/image/orientation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package image

import (
"errors"
"fmt"
"io"

"github.com/rwcarlsen/goexif/exif"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)

func adjustForOrientation(fs models.FS, path string, f *models.ImageFile) {
isFlipped, err := areDimensionsFlipped(fs, path)
if err != nil {
logger.Warnf("Error determining image orientation for %s: %v", path, err)
// isFlipped is false by default
}

if isFlipped {
f.Width, f.Height = f.Height, f.Width
}
}

// areDimensionsFlipped returns true if the image dimensions are flipped.
// This is determined by the EXIF orientation tag.
func areDimensionsFlipped(fs models.FS, path string) (bool, error) {
r, err := fs.Open(path)
if err != nil {
return false, fmt.Errorf("reading image file %q: %w", path, err)
}
defer r.Close()

x, err := exif.Decode(r)
if err != nil {
if errors.Is(err, io.EOF) {
// no exif data
return false, nil
}

return false, fmt.Errorf("decoding exif data: %w", err)
}

o, err := x.Get(exif.Orientation)
if err != nil {
// assume not present
return false, nil
}

oo, err := o.Int(0)
if err != nil {
return false, fmt.Errorf("decoding orientation: %w", err)
}

return isOrientationDimensionsFlipped(oo), nil
}

// isOrientationDimensionsFlipped returns true if the image orientation is flipped based on the input orientation EXIF value.
// From https://sirv.com/help/articles/rotate-photos-to-be-upright/
// 1 = 0 degrees: the correct orientation, no adjustment is required.
// 2 = 0 degrees, mirrored: image has been flipped back-to-front.
// 3 = 180 degrees: image is upside down.
// 4 = 180 degrees, mirrored: image has been flipped back-to-front and is upside down.
// 5 = 90 degrees: image has been flipped back-to-front and is on its side.
// 6 = 90 degrees, mirrored: image is on its side.
// 7 = 270 degrees: image has been flipped back-to-front and is on its far side.
// 8 = 270 degrees, mirrored: image is on its far side.
func isOrientationDimensionsFlipped(o int) bool {
switch o {
case 5, 6, 7, 8:
return true
default:
return false
}
}
68 changes: 45 additions & 23 deletions pkg/file/image/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,36 +25,17 @@ type Decorator struct {
func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) (models.File, error) {
base := f.Base()

decorateFallback := func() (models.File, error) {
r, err := fs.Open(base.Path)
if err != nil {
return f, fmt.Errorf("reading image file %q: %w", base.Path, err)
}
defer r.Close()

c, format, err := image.DecodeConfig(r)
if err != nil {
return f, fmt.Errorf("decoding image file %q: %w", base.Path, err)
}
return &models.ImageFile{
BaseFile: base,
Format: format,
Width: c.Width,
Height: c.Height,
}, nil
}

// ignore clips in non-OsFS filesystems as ffprobe cannot read them
// TODO - copy to temp file if not an OsFS
if _, isOs := fs.(*file.OsFS); !isOs {
logger.Debugf("assuming ImageFile for non-OsFS file %q", base.Path)
return decorateFallback()
return decorateFallback(fs, f)
}

probe, err := d.FFProbe.NewVideoFile(base.Path)
if err != nil {
logger.Warnf("File %q could not be read with ffprobe: %s, assuming ImageFile", base.Path, err)
return decorateFallback()
return decorateFallback(fs, f)
}

// Fallback to catch non-animated avif images that FFProbe detects as video files
Expand All @@ -79,12 +60,53 @@ func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) (
return videoFileDecorator.Decorate(ctx, fs, f)
}

return &models.ImageFile{
ret := &models.ImageFile{
BaseFile: base,
Format: probe.VideoCodec,
Width: probe.Width,
Height: probe.Height,
}, nil
}

adjustForOrientation(fs, base.Path, ret)

return ret, nil
}

func decodeConfig(fs models.FS, path string) (config image.Config, format string, err error) {
r, err := fs.Open(path)
if err != nil {
err = fmt.Errorf("reading image file %q: %w", path, err)
return
}
defer r.Close()

config, format, err = image.DecodeConfig(r)
if err != nil {
err = fmt.Errorf("decoding image file %q: %w", path, err)
return
}
return
}

func decorateFallback(fs models.FS, f models.File) (models.File, error) {
base := f.Base()
path := base.Path

c, format, err := decodeConfig(fs, path)
if err != nil {
return f, err
}

ret := &models.ImageFile{
BaseFile: base,
Format: format,
Width: c.Width,
Height: c.Height,
}

adjustForOrientation(fs, path, ret)

return ret, nil
}

func (d *Decorator) IsMissingMetadata(ctx context.Context, fs models.FS, f models.File) bool {
Expand Down

0 comments on commit 899ee71

Please sign in to comment.