Adjust image dimensions for exif orientation (#5188)

This commit is contained in:
WithoutPants 2024-09-03 16:32:29 +10:00 committed by GitHub
parent a3c34a51aa
commit 899ee713ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 123 additions and 23 deletions

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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
}
}

View File

@ -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
@ -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 {