mirror of https://github.com/stashapp/stash.git
Adjust image dimensions for exif orientation (#5188)
This commit is contained in:
parent
a3c34a51aa
commit
899ee713ab
1
go.mod
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue