Workaround for bad image rotation in Exif headers #637

This commit is contained in:
Michael Mayer 2020-12-12 13:05:58 +01:00
parent 03d3dd0f02
commit 73a00efae8
14 changed files with 146 additions and 72 deletions

View file

@ -90,6 +90,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
return
}
var files photoprism.MediaFiles
unstackSingle := false
if unstackFile.BasePrefix(false) == stackPhoto.PhotoName {
@ -99,10 +100,23 @@ func PhotoUnstack(router *gin.RouterGroup) {
return
}
destName := fmt.Sprintf("%s.%s%s", unstackFile.AbsPrefix(false), unstackFile.Checksum(), unstackFile.Extension())
if err := unstackFile.Move(destName); err != nil {
log.Errorf("photo: can't rename %s to %s (unstack)", txt.Quote(unstackFile.BaseName()), txt.Quote(filepath.Base(destName)))
AbortUnexpected(c)
return
}
files = append(files, unstackFile)
unstackSingle = true
} else {
files = related.Files
}
newPhoto := entity.NewPhoto(true)
newPhoto.PhotoPath = unstackFile.RootRelPath()
newPhoto.PhotoName = unstackFile.BasePrefix(false)
if err := newPhoto.Create(); err != nil {
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(baseName))
@ -110,32 +124,13 @@ func PhotoUnstack(router *gin.RouterGroup) {
return
}
for _, r := range related.Files {
isMain := related.Main.FileName() == r.FileName()
relFileName := r.FileName()
for _, r := range files {
relName := r.RootRelName()
relRoot := r.Root()
if unstackSingle {
if unstackFile.FileName() != r.FileName() {
continue
}
destName := fmt.Sprintf("%s.%s%s", r.AbsPrefix(false), r.Checksum(), r.Extension())
if err := r.Move(destName); err != nil {
log.Errorf("photo: can't rename %s to %s (unstack)", txt.Quote(r.BaseName()), txt.Quote(filepath.Base(destName)))
AbortUnexpected(c)
return
}
unstackFile = r
if isMain {
related.Main = r
}
} else if unstackFile.FileName() == r.FileName() {
unstackFile = r
relName = file.FileName
relRoot = file.FileRoot
}
if err := entity.UnscopedDb().Exec(`UPDATE files
@ -152,8 +147,10 @@ func PhotoUnstack(router *gin.RouterGroup) {
}
// Revert file rename.
if err := r.Move(relFileName); err != nil {
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(r.BaseName()))
if unstackSingle {
if err := r.Move(photoprism.FileName(relRoot, relName)); err != nil {
log.Errorf("photo: %s (unstack %s)", err.Error(), txt.Quote(r.BaseName()))
}
}
AbortSaveFailed(c)

View file

@ -58,5 +58,5 @@ func (m *Photo) LoadFromYaml(fileName string) error {
// YamlFileName returns the YAML backup file name.
func (m *Photo) YamlFileName(originalsPath, sidecarPath string) string {
return fs.FileName(filepath.Join(originalsPath, m.PhotoPath, m.PhotoName), sidecarPath, originalsPath, fs.YamlExt, false)
return fs.FileName(filepath.Join(originalsPath, m.PhotoPath, m.PhotoName), sidecarPath, originalsPath, fs.YamlExt)
}

View file

@ -127,7 +127,7 @@ func (c *Convert) ToJson(mf *MediaFile) (*MediaFile, error) {
return nil, fmt.Errorf("convert: can't create json sidecar file for %s in read only mode", txt.Quote(mf.BaseName()))
}
jsonName = fs.FileName(mf.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), ".json", false)
jsonName = fs.FileName(mf.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), ".json")
fileName := mf.RelName(c.conf.OriginalsPath())
@ -200,7 +200,7 @@ func (c *Convert) JpegConvertCommand(mf *MediaFile, jpegName string, xmpName str
return nil, useMutex, fmt.Errorf("convert: no converter found for %s", txt.Quote(mf.BaseName()))
}
} else if mf.IsVideo() {
result = exec.Command(c.conf.FFmpegBin(), "-i", mf.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
result = exec.Command(c.conf.FFmpegBin(), "-y", "-i", mf.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)
} else if mf.IsHEIF() {
result = exec.Command(c.conf.HeifConvertBin(), mf.FileName(), jpegName)
} else {
@ -232,7 +232,7 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", image.RelName(c.conf.OriginalsPath()))
}
jpegName = fs.FileName(image.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt, false)
jpegName = fs.FileName(image.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
fileName := image.RelName(c.conf.OriginalsPath())
log.Debugf("convert: %s -> %s", fileName, filepath.Base(jpegName))
@ -320,7 +320,7 @@ func (c *Convert) ToAvc1(video *MediaFile) (*MediaFile, error) {
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", video.RelName(c.conf.OriginalsPath()))
}
avcName = fs.FileName(video.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt, false)
avcName = fs.FileName(video.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt)
fileName := video.RelName(c.conf.OriginalsPath())
log.Debugf("convert: %s -> %s", fileName, filepath.Base(avcName))

View file

@ -29,7 +29,7 @@ func TestConvert_ToJpeg(t *testing.T) {
t.Run("gopher-video.mp4", func(t *testing.T) {
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.jpg")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.mp4.jpg")
_ = os.Remove(outputName)
@ -81,7 +81,7 @@ func TestConvert_ToJpeg(t *testing.T) {
assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel)
rawFilename := filepath.Join(conf.ImportPath(), "raw", "IMG_2567.CR2")
jpgFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/IMG_2567.jpg")
jpgFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/IMG_2567.CR2.jpg")
t.Logf("Testing RAW to JPEG convert with %s", rawFilename)
@ -119,7 +119,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Run("gopher-video.mp4", func(t *testing.T) {
fileName := filepath.Join(conf.ExamplesPath(), "gopher-video.mp4")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.json")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "gopher-video.mp4.json")
_ = os.Remove(outputName)
@ -154,7 +154,7 @@ func TestConvert_ToJson(t *testing.T) {
t.Run("IMG_4120.JPG", func(t *testing.T) {
fileName := filepath.Join(conf.ExamplesPath(), "IMG_4120.JPG")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "IMG_4120.json")
outputName := filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "IMG_4120.JPG.json")
_ = os.Remove(outputName)
@ -232,7 +232,7 @@ func TestConvert_Start(t *testing.T) {
t.Fatal(err)
}
jpegFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/canon_eos_6d.jpg")
jpegFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "raw/canon_eos_6d.dng.jpg")
assert.True(t, fs.FileExists(jpegFilename), "Jpeg file was not found - is Darktable installed?")
@ -248,7 +248,7 @@ func TestConvert_Start(t *testing.T) {
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel, "UpdateCamera model should be Canon EOS M10")
existingJpegFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "/raw/IMG_2567.jpg")
existingJpegFilename := filepath.Join(conf.SidecarPath(), conf.ImportPath(), "/raw/IMG_2567.CR2.jpg")
oldHash := fs.Hash(existingJpegFilename)

View file

@ -87,7 +87,7 @@ func ImportWorker(jobs <-chan ImportJob) {
continue
}
if !f.HasJpeg() {
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(destinationMainFilename, imp.originalsPath())))
continue
@ -105,7 +105,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
}
if imp.conf.SidecarJson() && !f.HasJson() {
if imp.conf.SidecarJson() && f.IsMedia() && !f.HasJson() {
if jsonFile, err := imp.convert.ToJson(f); err != nil {
log.Errorf("import: %s in %s (create json sidecar)", err.Error(), txt.Quote(f.BaseName()))
} else {
@ -167,6 +167,14 @@ func ImportWorker(jobs <-chan ImportJob) {
continue
}
if ind.conf.SidecarJson() && f.IsMedia() && !f.HasJson() {
if jsonFile, err := ind.convert.ToJson(f); err != nil {
log.Errorf("import: failed creating json sidecar for %s (%s)", txt.Quote(f.BaseName()), err.Error())
} else {
log.Debugf("import: %s created", txt.Quote(jsonFile.BaseName()))
}
}
res := ind.MediaFile(f, indexOpt, "")
if res.Indexed() && f.IsJpeg() {

View file

@ -95,6 +95,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
stripSequence := Config().Settings().StackSequences() && !o.Single
fileRoot, fileBase, filePath, fileName := m.PathNameInfo(stripSequence)
fullBase := m.BasePrefix(false)
logName := txt.Quote(fileName)
fileSize, modTime, err := m.Stat()
@ -173,8 +174,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
// Look for existing photo if file wasn't indexed yet...
if !fileExists {
fullBase := m.BasePrefix(false)
if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fullBase); photoQuery.Error == nil || fileBase == fullBase || o.Single {
// Skip next query.
} else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ? AND photo_single = 0", filePath, fileBase); photoQuery.Error == nil {
@ -251,7 +250,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
photo.PhotoPath = filePath
photo.PhotoName = fileBase
if o.Single || photo.PhotoSingle || !stripSequence {
photo.PhotoName = fullBase
} else {
photo.PhotoName = fileBase
}
file.FileError = ""
// Flag first JPEG as primary file for this photo.

View file

@ -26,7 +26,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
return result
}
if opt.Convert && !f.HasJpeg() {
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())
result.Status = IndexFailed
@ -46,7 +46,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
}
}
if ind.conf.SidecarJson() && !f.HasJson() {
if ind.conf.SidecarJson() && f.IsMedia() && !f.HasJson() {
if jsonFile, err := ind.convert.ToJson(f); err != nil {
log.Errorf("index: failed creating json sidecar for %s (%s)", txt.Quote(f.BaseName()), err.Error())
} else {
@ -102,6 +102,14 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
continue
}
if ind.conf.SidecarJson() && f.IsMedia() && !f.HasJson() {
if jsonFile, err := ind.convert.ToJson(f); err != nil {
log.Errorf("index: failed creating json sidecar for %s (%s)", txt.Quote(f.BaseName()), err.Error())
} else {
log.Debugf("index: %s created", txt.Quote(jsonFile.BaseName()))
}
}
res := ind.MediaFile(f, opt, "")
if res.Indexed() && f.IsJpeg() {

View file

@ -459,7 +459,7 @@ func (m *MediaFile) RootPath() string {
// RootRelPath returns the relative path and automatically detects the root path.
func (m *MediaFile) RootRelPath() string {
return m.RelName(m.RootPath())
return m.RelPath(m.RootPath())
}
// RelPrefix returns the relative path and file name prefix.

View file

@ -20,20 +20,19 @@ func (m *MediaFile) MetaData() (result meta.Data) {
err = fmt.Errorf("exif not supported: %s", txt.Quote(m.BaseName()))
}
// Parse JSON sidecar file names as Google Photos uses them ("img_1234.jpg.json").
if m.JsonName() != "" {
if err := m.metaData.JSON(m.JsonName(), m.BaseName()); err != nil {
log.Debug(err)
}
}
// Parse regular JSON sidecar files ("img_1234.json").
if jsonFile := fs.TypeJson.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); jsonFile == "" {
if jsonFiles := fs.TypeJson.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
log.Debugf("media: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
} else if jsonErr := m.metaData.JSON(jsonFile, m.BaseName()); jsonErr != nil {
log.Debug(jsonErr)
} else {
err = nil
for _, jsonFile := range jsonFiles {
jsonErr := m.metaData.JSON(jsonFile, m.BaseName())
if jsonErr != nil {
log.Debug(jsonErr)
} else {
err = nil
}
}
}
if err != nil {

View file

@ -266,7 +266,7 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
assert.Equal(t, false, jpegInfo.Flash)
assert.Equal(t, "", jpegInfo.Description)
if err := os.Remove(filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "iphone_7.jpg")); err != nil {
if err := os.Remove(filepath.Join(conf.SidecarPath(), conf.ExamplesPath(), "iphone_7.heic.jpg")); err != nil {
t.Error(err)
}
}

View file

@ -208,9 +208,10 @@ func GetFileType(fileName string) FileType {
// FindFirst searches a list of directories for the first file with the same base name and a given type.
func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
fileBase := BasePrefix(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBase)
fileBaseUpper := strings.ToUpper(fileBase)
fileBase := filepath.Base(fileName)
fileBasePrefix := BasePrefix(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBasePrefix)
fileBaseUpper := strings.ToUpper(fileBasePrefix)
fileDir := filepath.Dir(fileName)
search := append([]string{fileDir}, dirs...)
@ -237,6 +238,10 @@ func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stri
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(filepath.Join(dir, fileBasePrefix) + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
if info, err := os.Stat(filepath.Join(dir, fileBaseLower) + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
@ -250,6 +255,55 @@ func (t FileType) FindFirst(fileName string, dirs []string, baseDir string, stri
return ""
}
// FindAll searches a list of directories for files with the same base name and a given type.
func (t FileType) FindAll(fileName string, dirs []string, baseDir string, stripSequence bool) (results []string) {
fileBase := filepath.Base(fileName)
fileBasePrefix := BasePrefix(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBasePrefix)
fileBaseUpper := strings.ToUpper(fileBasePrefix)
fileDir := filepath.Dir(fileName)
search := append([]string{fileDir}, dirs...)
for _, ext := range TypeExt[t] {
lastDir := ""
for _, dir := range search {
if dir == "" || dir == lastDir {
continue
}
lastDir = dir
if dir != fileDir {
if filepath.IsAbs(dir) {
dir = filepath.Join(dir, RelName(fileDir, baseDir))
} else {
dir = filepath.Join(fileDir, dir)
}
}
if info, err := os.Stat(filepath.Join(dir, fileBase) + ext); err == nil && info.Mode().IsRegular() {
results = append(results, filepath.Join(dir, info.Name()))
}
if info, err := os.Stat(filepath.Join(dir, fileBasePrefix) + ext); err == nil && info.Mode().IsRegular() {
results = append(results, filepath.Join(dir, info.Name()))
}
if info, err := os.Stat(filepath.Join(dir, fileBaseLower) + ext); err == nil && info.Mode().IsRegular() {
results = append(results, filepath.Join(dir, info.Name()))
}
if info, err := os.Stat(filepath.Join(dir, fileBaseUpper) + ext); err == nil && info.Mode().IsRegular() {
results = append(results, filepath.Join(dir, info.Name()))
}
}
}
return results
}
// NormalizedExt returns the file extension without dot and in lowercase.
func NormalizedExt(fileName string) string {
if dot := strings.LastIndex(fileName, "."); dot != -1 && len(fileName[dot+1:]) >= 1 {

View file

@ -8,9 +8,8 @@ import (
)
// FileName returns the a relative filename with the same base and a given extension in a directory.
func FileName(fileName, dirName, baseDir, fileExt string, stripSequence bool) string {
func FileName(fileName, dirName, baseDir, fileExt string) string {
fileDir := filepath.Dir(fileName)
baseName := BasePrefix(fileName, stripSequence)
if dirName == "" || dirName == "." {
dirName = fileDir
@ -27,7 +26,7 @@ func FileName(fileName, dirName, baseDir, fileExt string, stripSequence bool) st
return ""
}
result := filepath.Join(dirName, baseName) + fileExt
result := filepath.Join(dirName, filepath.Base(fileName)) + fileExt
return result
}

View file

@ -34,21 +34,21 @@ func TestRel(t *testing.T) {
func TestFileName(t *testing.T) {
t.Run("Test copy 3.jpg", func(t *testing.T) {
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp", true)
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp")
assert.Equal(t, "testdata/.photoprism/Test.xmp", result)
assert.Equal(t, "testdata/.photoprism/Test (4).jpg.xmp", result)
})
t.Run("Test (3).jpg", func(t *testing.T) {
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp", false)
result := FileName("testdata/Test (4).jpg", ".photoprism", Abs("testdata"), ".xmp")
assert.Equal(t, "testdata/.photoprism/Test (4).xmp", result)
assert.Equal(t, "testdata/.photoprism/Test (4).jpg.xmp", result)
})
t.Run("FOO.XMP", func(t *testing.T) {
result := FileName("testdata/FOO.XMP", ".photoprism/sub", Abs("testdata"), ".jpeg", true)
result := FileName("testdata/FOO.XMP", ".photoprism/sub", Abs("testdata"), ".jpeg")
assert.Equal(t, "testdata/.photoprism/sub/FOO.jpeg", result)
assert.Equal(t, "testdata/.photoprism/sub/FOO.XMP.jpeg", result)
})
t.Run("Test copy 3.jpg", func(t *testing.T) {
@ -56,14 +56,14 @@ func TestFileName(t *testing.T) {
// t.Logf("TEMP DIR, ABS NAME: %s, %s", tempDir, Abs("testdata/Test (4).jpg"))
result := FileName(Abs("testdata/Test (4).jpg"), tempDir, Abs("testdata"), ".xmp", true)
result := FileName(Abs("testdata/Test (4).jpg"), tempDir, Abs("testdata"), ".xmp")
assert.Equal(t, tempDir+"/Test.xmp", result)
assert.Equal(t, tempDir+"/Test (4).jpg.xmp", result)
})
t.Run("empty dir", func(t *testing.T) {
result := FileName("testdata/FOO.XMP", "", Abs("testdata"), ".jpeg", true)
result := FileName("testdata/FOO.XMP", "", Abs("testdata"), ".jpeg")
assert.Equal(t, "testdata/FOO.jpeg", result)
assert.Equal(t, "testdata/FOO.XMP.jpeg", result)
})
}

View file

@ -4,9 +4,13 @@ if [[ -z $DOCKER_PASSWORD ]] || [[ -z $DOCKER_USERNAME ]]; then
docker login
fi
if [[ -z $1 ]] || [[ -z $2 ]]; then
if [[ -z $1 ]] && [[ -z $2 ]]; then
echo "Please provide a container image name and version" 1>&2
exit 1
elif [[ $1 ]] && [[ -z $2 ]]; then
echo "Pushing 'photoprism/$1:latest' to Docker hub...";
docker push photoprism/$1:latest
echo "Done"
else
echo "Pushing 'photoprism/$1:$2' to Docker hub...";
docker push photoprism/$1:latest