parent
3515d9f266
commit
1cde378a76
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue