Index: Improve stacking of related files #926 #1823

This commit is contained in:
Michael Mayer 2022-01-03 17:29:43 +01:00
parent 3515d9f266
commit 1cde378a76
8 changed files with 210 additions and 51 deletions

View file

@ -490,7 +490,14 @@ func (m *File) HasColorProfile(profile colors.Profile) bool {
// SetColorProfile sets the ICC color profile name such as "Display P3".
func (m *File) SetColorProfile(name string) {
m.FileColorProfile = SanitizeTypeCaseSensitive(name)
if name = SanitizeTypeCaseSensitive(name); name != "" {
m.FileColorProfile = SanitizeTypeCaseSensitive(name)
}
}
// ResetColorProfile removes the ICC color profile name.
func (m *File) ResetColorProfile() {
m.FileColorProfile = ""
}
// AddFaces adds face markers to the file.

View file

@ -644,6 +644,12 @@ func TestFile_SetColorProfile(t *testing.T) {
m.SetColorProfile("")
assert.Equal(t, "Display P3", m.ColorProfile())
assert.False(t, m.HasColorProfile(colors.Default))
assert.True(t, m.HasColorProfile(colors.ProfileDisplayP3))
m.ResetColorProfile()
assert.Equal(t, "", m.ColorProfile())
assert.True(t, m.HasColorProfile(colors.Default))
assert.False(t, m.HasColorProfile(colors.ProfileDisplayP3))

View file

@ -138,22 +138,35 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
fileStacked = true
}
// Stack file based on matching location and time metadata?
if o.Stack && photoQuery.Error != nil && Config().Settings().StackMeta() && m.MetaData().HasTimeAndPlace() {
metaData = m.MetaData()
photoQuery = entity.UnscopedDb().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ? AND taken_src = 'meta' AND camera_serial = ?", metaData.Lat, metaData.Lng, metaData.TakenAt, metaData.CameraSerial)
// Find existing photo stack?
if o.Stack {
// Matching location and time metadata?
if photoQuery.Error != nil && Config().Settings().StackMeta() && m.MetaData().HasTimeAndPlace() {
metaData = m.MetaData()
photoQuery = entity.UnscopedDb().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ? AND taken_src = 'meta' AND camera_serial = ?", metaData.Lat, metaData.Lng, metaData.TakenAt, metaData.CameraSerial)
if photoQuery.Error == nil {
fileStacked = true
if photoQuery.Error == nil {
fileStacked = true
}
}
}
// Stack file based on the same unique ID?
if o.Stack && photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() {
photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", m.MetaData().DocumentID)
// Same unique ID?
if photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() {
photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", sanitize.Log(m.MetaData().DocumentID))
if photoQuery.Error == nil {
fileStacked = true
if photoQuery.Error == nil {
fileStacked = true
}
}
// Related file?
if photoQuery.Error != nil {
photoQuery = entity.UnscopedDb().First(&photo, "id IN (SELECT photo_id FROM files WHERE file_sidecar = 0 AND file_missing = 0 AND file_name = LIKE ?)", fs.StripKnownExt(fileName)+".%")
if photoQuery.Error == nil {
log.Debugf("index: %s belongs to %s", sanitize.Log(m.BaseName()), sanitize.Log(filepath.Join(photo.PhotoPath, photo.PhotoName)))
fileStacked = true
}
}
}
} else {
@ -353,6 +366,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
details.SetArtist(metaData.Artist, entity.SrcXmp)
details.SetCopyright(metaData.Copyright, entity.SrcXmp)
} else {
log.Warn(err.Error())
file.FileError = err.Error()
}
case m.IsRaw(), m.IsHEIF(), m.IsImageOther():
@ -400,10 +414,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.SetExposure(m.FocalLength(), m.FNumber(), m.Iso(), m.Exposure(), entity.SrcMeta)
}
if photo.TypeSrc == entity.SrcAuto {
// Update photo type only if not manually modified.
if m.IsRaw() && photo.PhotoType == entity.TypeImage {
// Update photo type if an image and not manually modified.
if photo.TypeSrc == entity.SrcAuto && photo.PhotoType == entity.TypeImage {
if m.IsRaw() {
photo.PhotoType = entity.TypeRaw
} else if m.IsLive() {
photo.PhotoType = entity.TypeLive
}
}
case m.IsVideo():
@ -555,10 +571,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.UpdateDateFields()
// Panorama?
if file.Panorama() {
photo.PhotoPanorama = true
}
// Set remaining file properties.
file.FileSidecar = m.IsSidecar()
file.FileVideo = m.IsVideo()
file.FileType = string(m.FileType())
@ -566,6 +584,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FileOrientation = m.Orientation()
file.ModTime = modTime.Unix()
// Detect ICC color profile for JPEGs if still unknown at this point.
if file.FileColorProfile == "" && file.FileType == string(fs.FormatJpeg) {
file.SetColorProfile(m.ColorProfile())
}
// Update existing photo?
if photoExists || photo.HasID() {
if err := photo.Save(); err != nil {
log.Errorf("index: %s in %s (update existing photo)", err, logName)

View file

@ -4,6 +4,8 @@ import (
"fmt"
"path/filepath"
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/sanitize"
)
@ -69,7 +71,7 @@ func IndexMain(related *RelatedFiles, ind *Index, opt IndexOptions) (result Inde
return result
}
// IndexMain indexes a group of related files and returns the result.
// IndexRelated indexes a group of related files and returns the result.
func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result IndexResult) {
done := make(map[string]bool)
sizeLimit := ind.conf.OriginalsLimit()
@ -79,9 +81,15 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
if result.Failed() {
log.Warn(result.Err)
return result
} else if !result.Success() || result.Stacked() {
// Skip related files if main file was stacked or indexing was not completely successful.
} else if !result.Success() {
// Skip related files if indexing was not completely successful.
return result
} else if result.Stacked() && len(related.Files) > 1 && related.Main != nil {
// Show info if main file was stacked and has additional related files.
fileType := string(related.Main.FileType())
relatedFiles := len(related.Files) - 1
mainLogName := sanitize.Log(related.Main.RelName(ind.originalsPath()))
log.Infof("index: stacked main %s file %s has %s", fileType, mainLogName, english.Plural(relatedFiles, "related file", "related files"))
}
done[related.Main.FileName()] = true

View file

@ -4,14 +4,14 @@ import (
"path/filepath"
"testing"
"github.com/photoprism/photoprism/internal/face"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/stretchr/testify/assert"
)
func TestIndexRelated(t *testing.T) {

View file

@ -14,10 +14,10 @@ import (
"sync"
"time"
"github.com/dustin/go-humanize/english"
"github.com/disintegration/imaging"
"github.com/djherbis/times"
"github.com/dustin/go-humanize/english"
"github.com/mandykoh/prism/meta/autometa"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/meta"
@ -30,23 +30,25 @@ import (
// MediaFile represents a single photo, video or sidecar file.
type MediaFile struct {
fileName string
fileRoot string
statErr error
modTime time.Time
fileSize int64
fileType fs.FileFormat
mimeType string
takenAt time.Time
takenAtSrc string
hash string
checksum string
hasJpeg bool
width int
height int
metaData meta.Data
metaDataOnce sync.Once
location *entity.Cell
fileName string
fileRoot string
statErr error
modTime time.Time
fileSize int64
fileType fs.FileFormat
mimeType string
takenAt time.Time
takenAtSrc string
hash string
checksum string
hasJpeg bool
noColorProfile bool
colorProfile string
width int
height int
metaData meta.Data
metaDataOnce sync.Once
location *entity.Cell
}
// NewMediaFile returns a new media file.
@ -297,6 +299,8 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
matches = append(matches, name)
}
isHEIF := false
for _, fileName := range matches {
f, err := NewMediaFile(fileName)
@ -315,10 +319,11 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
} else if f.IsRaw() {
result.Main = f
} else if f.IsHEIF() {
isHEIF = true
result.Main = f
} else if f.IsImageOther() {
result.Main = f
} else if f.IsVideo() {
} else if f.IsVideo() && !isHEIF {
result.Main = f
} else if result.Main != nil && f.IsJpeg() {
if result.Main.IsJpeg() && len(result.Main.FileName()) > len(f.FileName()) {
@ -737,6 +742,19 @@ func (m *MediaFile) IsPhoto() bool {
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsImageOther()
}
// IsLive returns true if this is a live photo.
func (m *MediaFile) IsLive() bool {
if m.IsHEIF() {
return fs.FormatMov.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
if m.IsVideo() {
return fs.FormatHEIF.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
return false
}
// ExifSupported returns true if parsing exif metadata is supported for the media file type.
func (m *MediaFile) ExifSupported() bool {
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsPng() || m.IsTiff()
@ -747,7 +765,7 @@ func (m *MediaFile) IsMedia() bool {
return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsHEIF() || m.IsImageOther()
}
// Jpeg returns a the JPEG version of the media file (if exists).
// Jpeg returns the JPEG version of the media file (if exists).
func (m *MediaFile) Jpeg() (*MediaFile, error) {
if m.IsJpeg() {
if !fs.FileExists(m.FileName()) {
@ -766,7 +784,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
return NewMediaFile(jpegFilename)
}
// ContainsJpeg returns true if this file has or is a jpeg media file.
// HasJpeg returns true if the file has or is a jpeg media file.
func (m *MediaFile) HasJpeg() bool {
if m.hasJpeg {
return true
@ -1060,3 +1078,43 @@ func (m *MediaFile) RemoveSidecars() (err error) {
return nil
}
// ColorProfile returns the ICC color profile name.
func (m *MediaFile) ColorProfile() string {
if !m.IsJpeg() || m.colorProfile != "" || m.noColorProfile {
return m.colorProfile
}
logName := sanitize.Log(m.BaseName())
// Open file.
fileReader, err := os.Open(m.FileName())
if err != nil {
m.noColorProfile = true
return ""
}
defer fileReader.Close()
// Read color metadata.
md, _, err := autometa.Load(fileReader)
if err != nil || md == nil {
m.noColorProfile = true
return ""
}
// Read ICC profile and convert colors if possible.
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
// Do nothing.
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
log.Debugf("media: %s has color profile %s", logName, sanitize.Log(profile))
m.colorProfile = profile
return m.colorProfile
}
log.Tracef("media: %s has no color profile", logName)
m.noColorProfile = true
return ""
}

View file

@ -2202,3 +2202,57 @@ func TestMediaFile_RemoveSidecars(t *testing.T) {
_ = os.Remove(sidecarName)
})
}
func TestMediaFile_ColorProfile(t *testing.T) {
c := config.TestConfig()
t.Run("iphone_7.json", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.json")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", mediaFile.ColorProfile())
})
t.Run("iphone_7.xmp", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.xmp")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", mediaFile.ColorProfile())
})
t.Run("iphone_7.heic", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", mediaFile.ColorProfile())
})
t.Run("canon_eos_6d.dng", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/canon_eos_6d.dng")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", mediaFile.ColorProfile())
})
t.Run("elephants.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "Adobe RGB (1998)", mediaFile.ColorProfile())
})
t.Run("/beach_wood.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/beach_wood.jpg")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "", mediaFile.ColorProfile())
})
t.Run("/peacock_blue.jpg", func(t *testing.T) {
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/peacock_blue.jpg")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "sRGB IEC61966-2.1", mediaFile.ColorProfile())
})
}

View file

@ -77,14 +77,16 @@ func OpenJpeg(fileName string, orientation int) (result image.Image, err error)
}
// Read ICC profile and convert colors if possible.
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
// Do nothing.
log.Tracef("resample: detected no color profile in %s", logName)
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
log.Debugf("resample: detected color profile %s in %s", sanitize.Log(profile), logName)
switch {
case colors.ProfileDisplayP3.Equal(profile):
img = colors.ToSRGB(img, colors.ProfileDisplayP3)
if md != nil {
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
// Do nothing.
log.Tracef("resample: %s has no color profile", logName)
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
log.Tracef("resample: %s has color profile %s", logName, sanitize.Log(profile))
switch {
case colors.ProfileDisplayP3.Equal(profile):
img = colors.ToSRGB(img, colors.ProfileDisplayP3)
}
}
}