Backend: Rename MediaFile.Exif() to MetaData() #172

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-01-07 18:13:53 +01:00
parent c147eee30f
commit b21ad9bece
11 changed files with 69 additions and 348 deletions

View file

@ -7,6 +7,7 @@ import (
"trimmer.io/go-xmp/xmp"
)
// Data represents image meta data.
type Data struct {
UUID string
TakenAt time.Time

View file

@ -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 {

View file

@ -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 {

View file

@ -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")

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}