From b21ad9bece593e366e36f7098ff6df5ac0f27802 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 7 Jan 2020 18:13:53 +0100 Subject: [PATCH] Backend: Rename MediaFile.Exif() to MetaData() #172 Signed-off-by: Michael Mayer --- internal/meta/data.go | 1 + internal/meta/exif.go | 2 +- internal/meta/xmp.go | 2 +- internal/photoprism/convert_test.go | 12 +- internal/photoprism/exif.go | 276 ------------------ internal/photoprism/index_mediafile.go | 29 +- internal/photoprism/location.go | 2 +- internal/photoprism/mediafile.go | 31 +- internal/photoprism/metadata.go | 11 + .../{exif_test.go => metadata_test.go} | 31 +- internal/photoprism/timezone.go | 20 +- 11 files changed, 69 insertions(+), 348 deletions(-) delete mode 100644 internal/photoprism/exif.go create mode 100644 internal/photoprism/metadata.go rename internal/photoprism/{exif_test.go => metadata_test.go} (89%) diff --git a/internal/meta/data.go b/internal/meta/data.go index 9ddaa7669..b1855b5af 100644 --- a/internal/meta/data.go +++ b/internal/meta/data.go @@ -7,6 +7,7 @@ import ( "trimmer.io/go-xmp/xmp" ) +// Data represents image meta data. type Data struct { UUID string TakenAt time.Time diff --git a/internal/meta/exif.go b/internal/meta/exif.go index fd5b59e70..42d994080 100644 --- a/internal/meta/exif.go +++ b/internal/meta/exif.go @@ -14,7 +14,7 @@ import ( "trimmer.io/go-xmp/xmp" ) -// Exif parses an image file and returns its Exif data. +// Exif parses an image file for Exif meta data and returns as Data struct. func Exif(filename string) (data Data, err error) { defer func() { if e := recover(); e != nil { diff --git a/internal/meta/xmp.go b/internal/meta/xmp.go index a858f7b7b..b44a3c249 100644 --- a/internal/meta/xmp.go +++ b/internal/meta/xmp.go @@ -12,7 +12,7 @@ import ( "trimmer.io/go-xmp/xmp" ) -// Exif returns parses an XMP and returns its data. +// XMP parses an XMP file and returns a Data struct. func XMP(filename string) (data Data, err error) { defer func() { if e := recover(); e != nil { diff --git a/internal/photoprism/convert_test.go b/internal/photoprism/convert_test.go index f429b3280..3cbb0a4e3 100644 --- a/internal/photoprism/convert_test.go +++ b/internal/photoprism/convert_test.go @@ -42,7 +42,7 @@ func TestConvert_ToJpeg(t *testing.T) { assert.Empty(t, err, "ToJpeg() failed") - infoJpeg, err := imageJpeg.Exif() + infoJpeg, err := imageJpeg.MetaData() assert.Nilf(t, err, "UpdateExif() failed for "+imageJpeg.Filename()) @@ -52,8 +52,6 @@ func TestConvert_ToJpeg(t *testing.T) { assert.Equal(t, jpegFilename, imageJpeg.filename) - assert.False(t, infoJpeg == nil || err != nil, "Could not read UpdateExif data of JPEG image") - assert.Equal(t, "Canon EOS 7D", infoJpeg.CameraModel) rawFilename := conf.ImportPath() + "/raw/IMG_2567.CR2" @@ -70,9 +68,7 @@ func TestConvert_ToJpeg(t *testing.T) { assert.NotEqual(t, rawFilename, imageRaw.filename) - infoRaw, err := imageRaw.Exif() - - assert.False(t, infoRaw == nil || err != nil, "Could not read UpdateExif data of RAW image") + infoRaw, err := imageRaw.MetaData() assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel) } @@ -100,9 +96,7 @@ func TestConvert_Path(t *testing.T) { assert.Equal(t, jpegFilename, image.filename, "FileName must be the same") - infoRaw, err := image.Exif() - - assert.False(t, infoRaw == nil || err != nil, "Could not read UpdateExif data of RAW image") + infoRaw, err := image.MetaData() assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel, "UpdateCamera model should be Canon EOS M10") diff --git a/internal/photoprism/exif.go b/internal/photoprism/exif.go deleted file mode 100644 index b6ec33719..000000000 --- a/internal/photoprism/exif.go +++ /dev/null @@ -1,276 +0,0 @@ -package photoprism - -import ( - "errors" - "fmt" - "math" - "strconv" - "strings" - "time" - - "github.com/dsoprea/go-exif" - "gopkg.in/ugjka/go-tz.v2/tz" -) - -// Exif represents MediaFile metadata. -type Exif struct { - UUID string - TakenAt time.Time - TakenAtLocal time.Time - TimeZone string - Artist string - CameraMake string - CameraModel string - Description string - LensMake string - LensModel string - Flash bool - FocalLength int - Exposure string - Aperture float64 - FNumber float64 - Iso int - Lat float64 - Lng float64 - Altitude int - Width int - Height int - Orientation int - All map[string]string -} - -var im *exif.IfdMapping - -func IfdMapping() *exif.IfdMapping { - if im != nil { - return im - } - - im = exif.NewIfdMapping() - - if err := exif.LoadStandardIfds(im); err != nil { - log.Errorf("could not parse exif config: %s", err.Error()) - } - - return im -} - -// Exif returns exif meta data of a media file. -func (m *MediaFile) Exif() (result *Exif, err error) { - defer func() { - if e := recover(); e != nil { - result = m.exifData - err = fmt.Errorf("error while parsing exif data: %s", e) - } - }() - - if m == nil { - return nil, errors.New("can't parse exif data: file instance is nil") - } - - if m.exifData != nil { - return m.exifData, nil - } - - if !m.IsJpeg() && !m.IsRaw() && !m.IsHEIF() { - return nil, errors.New(fmt.Sprintf("media file not compatible with exif: \"%s\"", m.Filename())) - } - - m.exifData = &Exif{} - - rawExif, err := exif.SearchFileAndExtractExif(m.Filename()) - - if err != nil { - return m.exifData, err - } - - ti := exif.NewTagIndex() - - tags := make(map[string]string) - im := IfdMapping() - - visitor := func(fqIfdPath string, ifdIndex int, tagId uint16, tagType exif.TagType, valueContext exif.ValueContext) (err error) { - ifdPath, err := im.StripPathPhraseIndices(fqIfdPath) - - if err != nil { - return nil - } - - it, err := ti.Get(ifdPath, tagId) - - if err != nil { - return nil - } - - valueString := "" - - if tagType.Type() != exif.TypeUndefined { - valueString, err = tagType.ResolveAsString(valueContext, true) - - if err != nil { - log.Error(err) - - return nil - } - - if it.Name != "" && valueString != "" { - tags[it.Name] = valueString - } - } - - return nil - } - - _, err = exif.Visit(exif.IfdStandard, im, ti, rawExif, visitor) - - if err != nil { - return m.exifData, err - } - - if value, ok := tags["Artist"]; ok { - m.exifData.Artist = strings.Replace(value, "\"", "", -1) - } - - if value, ok := tags["Model"]; ok { - m.exifData.CameraModel = strings.Replace(value, "\"", "", -1) - } - - if value, ok := tags["Make"]; ok { - m.exifData.CameraMake = strings.Replace(value, "\"", "", -1) - } - - if value, ok := tags["LensMake"]; ok { - m.exifData.LensMake = strings.Replace(value, "\"", "", -1) - } - - if value, ok := tags["LensModel"]; ok { - m.exifData.LensModel = strings.Replace(value, "\"", "", -1) - } - - if value, ok := tags["ExposureTime"]; ok { - m.exifData.Exposure = value - } - - if value, ok := tags["FNumber"]; ok { - values := strings.Split(value, "/") - - if len(values) == 2 && values[1] != "0" && values[1] != "" { - number, _ := strconv.ParseFloat(values[0], 64) - denom, _ := strconv.ParseFloat(values[1], 64) - - m.exifData.FNumber = math.Round((number/denom)*1000) / 1000 - } - } - - if value, ok := tags["ApertureValue"]; ok { - values := strings.Split(value, "/") - - if len(values) == 2 && values[1] != "0" && values[1] != "" { - number, _ := strconv.ParseFloat(values[0], 64) - denom, _ := strconv.ParseFloat(values[1], 64) - - m.exifData.Aperture = math.Round((number/denom)*1000) / 1000 - } - } - - if value, ok := tags["FocalLengthIn35mmFilm"]; ok { - if i, err := strconv.Atoi(value); err == nil { - m.exifData.FocalLength = i - } - } else if value, ok := tags["FocalLength"]; ok { - values := strings.Split(value, "/") - - if len(values) == 2 && values[1] != "0" && values[1] != "" { - number, _ := strconv.ParseFloat(values[0], 64) - denom, _ := strconv.ParseFloat(values[1], 64) - - m.exifData.FocalLength = int(math.Round((number/denom)*1000) / 1000) - } - } - - if value, ok := tags["ISOSpeedRatings"]; ok { - if i, err := strconv.Atoi(value); err == nil { - m.exifData.Iso = i - } - } - - if value, ok := tags["ImageUniqueID"]; ok { - m.exifData.UUID = value - } - - if value, ok := tags["ImageWidth"]; ok { - if i, err := strconv.Atoi(value); err == nil { - m.exifData.Width = i - } - } - - if value, ok := tags["ImageLength"]; ok { - if i, err := strconv.Atoi(value); err == nil { - m.exifData.Width = i - } - } - - if value, ok := tags["Orientation"]; ok { - if i, err := strconv.Atoi(value); err == nil { - m.exifData.Orientation = i - } - } else { - m.exifData.Orientation = 1 - } - - _, index, err := exif.Collect(im, ti, rawExif) - - if err != nil { - return m.exifData, err - } - - if ifd, err := index.RootIfd.ChildWithIfdPath(exif.IfdPathStandardGps); err == nil { - if gi, err := ifd.GpsInfo(); err == nil { - m.exifData.Lat = gi.Latitude.Decimal() - m.exifData.Lng = gi.Longitude.Decimal() - m.exifData.Altitude = gi.Altitude - } - } - - if m.exifData.Lat != 0 && m.exifData.Lng != 0 { - zones, err := tz.GetZone(tz.Point{ - Lat: m.exifData.Lat, - Lon: m.exifData.Lng, - }) - - if err != nil { - m.exifData.TimeZone = "UTC" - } - - m.exifData.TimeZone = zones[0] - } - - if value, ok := tags["DateTimeOriginal"]; ok { - m.exifData.TakenAtLocal, _ = time.Parse("2006:01:02 15:04:05", value) - - loc, err := time.LoadLocation(m.exifData.TimeZone) - - if err != nil { - m.exifData.TakenAt = m.exifData.TakenAtLocal - log.Warnf("no location for timezone: %s", err.Error()) - } else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", value, loc); err == nil { - m.exifData.TakenAt = tl.UTC() - } else { - log.Warnf("could parse time: %s", err.Error()) - } - } - - if value, ok := tags["Flash"]; ok { - if i, err := strconv.Atoi(value); err == nil && i&1 == 1 { - m.exifData.Flash = true - } - } - - if value, ok := tags["ImageDescription"]; ok { - m.exifData.Description = strings.Replace(value, "\"", "", -1) - } - - m.exifData.All = tags - - return m.exifData, nil -} diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 1abae4365..f7772402b 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -11,6 +11,7 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" + "github.com/photoprism/photoprism/internal/meta" "github.com/photoprism/photoprism/internal/txt" ) @@ -28,7 +29,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult { var photo entity.Photo var file, primaryFile entity.File - var exifData *Exif + var metaData meta.Data var photoQuery, fileQuery *gorm.DB var keywords []string var isNSFW bool @@ -55,8 +56,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult { photoQuery = ind.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase) if photoQuery.Error != nil && m.HasTimeAndPlace() { - exifData, _ = m.Exif() - photoQuery = ind.db.Unscoped().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ?", exifData.Lat, exifData.Lng, exifData.TakenAt) + metaData, _ = m.MetaData() + photoQuery = ind.db.Unscoped().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ?", metaData.Lat, metaData.Lng, metaData.TakenAt) } } else { photoQuery = ind.db.Unscoped().First(&photo, "id = ?", file.PhotoID) @@ -95,19 +96,19 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions) IndexResult { if fileChanged || o.UpdateExif { // Read UpdateExif data - if exifData, err := m.Exif(); err == nil { - photo.PhotoLat = exifData.Lat - photo.PhotoLng = exifData.Lng - photo.TakenAt = exifData.TakenAt - photo.TakenAtLocal = exifData.TakenAtLocal - photo.TimeZone = exifData.TimeZone - photo.PhotoAltitude = exifData.Altitude - photo.PhotoArtist = exifData.Artist + if metaData, err := m.MetaData(); err == nil { + photo.PhotoLat = metaData.Lat + photo.PhotoLng = metaData.Lng + photo.TakenAt = metaData.TakenAt + photo.TakenAtLocal = metaData.TakenAtLocal + photo.TimeZone = metaData.TimeZone + photo.PhotoAltitude = metaData.Altitude + photo.PhotoArtist = metaData.Artist - if len(exifData.UUID) > 15 { - log.Debugf("index: file uuid \"%s\"", exifData.UUID) + if len(metaData.UUID) > 15 { + log.Debugf("index: file uuid \"%s\"", metaData.UUID) - file.FileUUID = exifData.UUID + file.FileUUID = metaData.UUID } } } diff --git a/internal/photoprism/location.go b/internal/photoprism/location.go index 04041fd33..a7062a93a 100644 --- a/internal/photoprism/location.go +++ b/internal/photoprism/location.go @@ -11,7 +11,7 @@ func (m *MediaFile) Location() (*entity.Location, error) { return m.location, nil } - data, err := m.Exif() + data, err := m.MetaData() if err != nil { return nil, err diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index 159d10923..44a85ec86 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -9,11 +9,13 @@ import ( "regexp" "sort" "strings" + "sync" "time" "github.com/djherbis/times" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/file" + "github.com/photoprism/photoprism/internal/meta" ) // MediaFile represents a single photo, video or sidecar file. @@ -27,7 +29,8 @@ type MediaFile struct { perceptualHash string width int height int - exifData *Exif + once sync.Once + metaData meta.Data location *entity.Location } @@ -53,7 +56,7 @@ func (m *MediaFile) DateCreated() time.Time { m.dateCreated = time.Now() - info, err := m.Exif() + info, err := m.MetaData() if err == nil && !info.TakenAt.IsZero() && info.TakenAt.Year() > 1000 { m.dateCreated = info.TakenAt @@ -83,7 +86,7 @@ func (m *MediaFile) DateCreated() time.Time { } func (m *MediaFile) HasTimeAndPlace() bool { - exifData, err := m.Exif() + exifData, err := m.MetaData() if err != nil { return false @@ -96,7 +99,7 @@ func (m *MediaFile) HasTimeAndPlace() bool { // CameraModel returns the camera model with which the media file was created. func (m *MediaFile) CameraModel() string { - info, err := m.Exif() + info, err := m.MetaData() var result string @@ -109,7 +112,7 @@ func (m *MediaFile) CameraModel() string { // CameraMake returns the make of the camera with which the file was created. func (m *MediaFile) CameraMake() string { - info, err := m.Exif() + info, err := m.MetaData() var result string @@ -122,7 +125,7 @@ func (m *MediaFile) CameraMake() string { // LensModel returns the lens model of a media file. func (m *MediaFile) LensModel() string { - info, err := m.Exif() + info, err := m.MetaData() var result string @@ -135,7 +138,7 @@ func (m *MediaFile) LensModel() string { // LensMake returns the make of the Lens. func (m *MediaFile) LensMake() string { - info, err := m.Exif() + info, err := m.MetaData() var result string @@ -148,7 +151,7 @@ func (m *MediaFile) LensMake() string { // FocalLength return the length of the focal for a file. func (m *MediaFile) FocalLength() int { - info, err := m.Exif() + info, err := m.MetaData() var result int @@ -161,7 +164,7 @@ func (m *MediaFile) FocalLength() int { // FNumber returns the F number with which the media file was created. func (m *MediaFile) FNumber() float64 { - info, err := m.Exif() + info, err := m.MetaData() var result float64 @@ -174,7 +177,7 @@ func (m *MediaFile) FNumber() float64 { // Iso returns the iso rating as int. func (m *MediaFile) Iso() int { - info, err := m.Exif() + info, err := m.MetaData() var result int @@ -187,7 +190,7 @@ func (m *MediaFile) Iso() int { // Exposure returns the exposure time as string. func (m *MediaFile) Exposure() string { - info, err := m.Exif() + info, err := m.MetaData() var result string @@ -284,7 +287,7 @@ func (m *MediaFile) RelatedFiles() (result RelatedFiles, err error) { result.main = resultFile } else if resultFile.IsJpeg() && len(result.main.Filename()) > len(resultFile.Filename()) { result.main = resultFile - } else if resultFile.IsImageOther() { + } else if resultFile.IsImageOther() { result.main = resultFile } @@ -592,7 +595,7 @@ func (m *MediaFile) decodeDimensions() error { var width, height int - exif, err := m.Exif() + exif, err := m.MetaData() if err == nil { width = exif.Width @@ -671,7 +674,7 @@ func (m *MediaFile) AspectRatio() float64 { // Orientation returns the orientation of a MediaFile. func (m *MediaFile) Orientation() int { - if exif, err := m.Exif(); err == nil { + if exif, err := m.MetaData(); err == nil { return exif.Orientation } diff --git a/internal/photoprism/metadata.go b/internal/photoprism/metadata.go new file mode 100644 index 000000000..5e320816d --- /dev/null +++ b/internal/photoprism/metadata.go @@ -0,0 +1,11 @@ +package photoprism + +import ( + "github.com/photoprism/photoprism/internal/meta" +) + +// MetaData returns exif meta data of a media file. +func (m *MediaFile) MetaData() (result meta.Data, err error) { + m.once.Do(func() { m.metaData, err = meta.Exif(m.Filename()) }) + return m.metaData, err +} diff --git a/internal/photoprism/exif_test.go b/internal/photoprism/metadata_test.go similarity index 89% rename from internal/photoprism/exif_test.go rename to internal/photoprism/metadata_test.go index d704a498d..fddb265c4 100644 --- a/internal/photoprism/exif_test.go +++ b/internal/photoprism/metadata_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/meta" "github.com/stretchr/testify/assert" ) @@ -16,11 +17,11 @@ func TestMediaFile_Exif_JPEG(t *testing.T) { assert.Nil(t, err) - info, err := img.Exif() + info, err := img.MetaData() assert.Empty(t, err) - assert.IsType(t, &Exif{}, info) + assert.IsType(t, meta.Data{}, info) assert.Equal(t, "", info.UUID) assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", info.TakenAt.String()) @@ -40,8 +41,8 @@ func TestMediaFile_Exif_JPEG(t *testing.T) { assert.Equal(t, -33.45347, info.Lat) assert.Equal(t, 25.764645, info.Lng) assert.Equal(t, 190, info.Altitude) - assert.Equal(t, 1365, info.Width) - assert.Equal(t, 0, info.Height) + assert.Equal(t, 2048, info.Width) + assert.Equal(t, 1365, info.Height) assert.Equal(t, false, info.Flash) assert.Equal(t, "", info.Description) t.Logf("UTC: %s", info.TakenAt.String()) @@ -53,11 +54,11 @@ func TestMediaFile_Exif_JPEG(t *testing.T) { assert.Nil(t, err) - info, err := img.Exif() + info, err := img.MetaData() assert.Empty(t, err) - assert.IsType(t, &Exif{}, info) + assert.IsType(t, meta.Data{}, info) assert.Equal(t, "", info.UUID) assert.Equal(t, 1, info.Orientation) @@ -74,7 +75,7 @@ func TestMediaFile_Exif_JPEG(t *testing.T) { assert.Equal(t, 200, info.Iso) assert.Equal(t, 0, info.Altitude) assert.Equal(t, 2048, info.Width) - assert.Equal(t, 0, info.Height) + assert.Equal(t, 2048, info.Height) assert.Equal(t, true, info.Flash) assert.Equal(t, "", info.Description) t.Logf("UTC: %s", info.TakenAt.String()) @@ -93,11 +94,11 @@ func TestMediaFile_Exif_DNG(t *testing.T) { assert.Nil(t, err) - info, err := img.Exif() + info, err := img.MetaData() assert.Empty(t, err) - assert.IsType(t, &Exif{}, info) + assert.IsType(t, meta.Data{}, info) assert.Equal(t, "", info.UUID) assert.Equal(t, "2019-06-06 07:29:51 +0000 UTC", info.TakenAt.String()) @@ -114,8 +115,8 @@ func TestMediaFile_Exif_DNG(t *testing.T) { assert.Equal(t, 0.0, info.Lat) assert.Equal(t, 0.0, info.Lng) assert.Equal(t, 0, info.Altitude) - assert.Equal(t, 171, info.Width) - assert.Equal(t, 0, info.Height) + assert.Equal(t, 256, info.Width) + assert.Equal(t, 171, info.Height) assert.Equal(t, false, info.Flash) assert.Equal(t, "", info.Description) } @@ -131,9 +132,9 @@ func TestMediaFile_Exif_HEIF(t *testing.T) { assert.Nil(t, err) - info, err := img.Exif() + info, err := img.MetaData() - assert.IsType(t, &Exif{}, info) + assert.IsType(t, meta.Data{}, info) assert.Nil(t, err) @@ -143,9 +144,9 @@ func TestMediaFile_Exif_HEIF(t *testing.T) { assert.Nil(t, err) - jpegInfo, err := jpeg.Exif() + jpegInfo, err := jpeg.MetaData() - assert.IsType(t, &Exif{}, jpegInfo) + assert.IsType(t, meta.Data{}, jpegInfo) assert.Nil(t, err) diff --git a/internal/photoprism/timezone.go b/internal/photoprism/timezone.go index 9e3ac705d..d96278075 100644 --- a/internal/photoprism/timezone.go +++ b/internal/photoprism/timezone.go @@ -2,29 +2,15 @@ package photoprism import ( "errors" - - "gopkg.in/ugjka/go-tz.v2/tz" ) // TimeZone returns the time zone where the photo was taken. func (m *MediaFile) TimeZone() (string, error) { - meta, err := m.Exif() + meta, err := m.MetaData() if err != nil { - return "UTC", errors.New("no image metadata") + return "UTC", errors.New("file: unknown time zone, using UTC") } - if meta.Lat == 0 && meta.Lng == 0 { - return "UTC", errors.New("no latitude and longitude in image metadata") - } - - zones, err := tz.GetZone(tz.Point{ - Lon: meta.Lng, Lat: meta.Lat, - }) - - if err != nil { - return "UTC", errors.New("no matching zone found") - } - - return zones[0], nil + return meta.TimeZone, nil }