diff --git a/go.mod b/go.mod index 942722b45..cabc07d81 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,15 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/disintegration/imaging v1.6.2 github.com/djherbis/times v1.2.0 - github.com/dsoprea/go-exif/v2 v2.0.0-20200717063959-46b1a0cd1772 // indirect - github.com/dsoprea/go-exif/v3 v3.0.0-20200717063959-46b1a0cd1772 - github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200520190950-3ae4ff88a0d1 + github.com/dsoprea/go-exif/v2 v2.0.0-20200717071058-9393e7afd446 // indirect + github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446 + github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200717090456-b3d9dcddffd1 github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect - github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200615034914-d40a386309d2 + github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200717085400-dd2ba56ee6b8 github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect github.com/dsoprea/go-png-image-structure v0.0.0-20200615034826-4cfc78940228 + github.com/dsoprea/go-tiff-image-structure v0.0.0-20200717073440-8ac81ec8b423 github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect github.com/dustin/go-humanize v1.0.0 github.com/gin-gonic/gin v1.6.3 diff --git a/go.sum b/go.sum index eda4dac43..6ebbde3f7 100644 --- a/go.sum +++ b/go.sum @@ -48,16 +48,25 @@ github.com/dsoprea/go-exif/v2 v2.0.0-20200520183328-015129a9efd5/go.mod h1:9EXlP github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= github.com/dsoprea/go-exif/v2 v2.0.0-20200717063959-46b1a0cd1772 h1:M49UNOTa5sLju107lAoMsm93B/fHD02vWIoskmXMBm8= github.com/dsoprea/go-exif/v2 v2.0.0-20200717063959-46b1a0cd1772/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= +github.com/dsoprea/go-exif/v2 v2.0.0-20200717071058-9393e7afd446 h1:ruDG+2wFz+k/mDNy8x1UqWEItWNLXpvGlLv05+TlZt4= +github.com/dsoprea/go-exif/v2 v2.0.0-20200717071058-9393e7afd446/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= +github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= github.com/dsoprea/go-exif/v3 v3.0.0-20200717063959-46b1a0cd1772 h1:l/wfrK3wEH7sYpJe+Y8ZdFJW3AmsDgPoAQq2RLgKPSQ= github.com/dsoprea/go-exif/v3 v3.0.0-20200717063959-46b1a0cd1772/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= +github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446 h1:96yylb+JH415u6V7ykNtnEBLaZUwS1S31TnAezcvnNE= +github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200520190950-3ae4ff88a0d1 h1:8Tbo+OYgg7i2G3fltmpWq1if1e752aMX7Zv/sNWWJUk= github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200520190950-3ae4ff88a0d1/go.mod h1:UwRKreeVikXn5OarSnt4OqovcEjsIgZVuc5svj7G5w4= +github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200717090456-b3d9dcddffd1 h1:R/EEzpxqQxeEcJ/z0EFTI1U6XsuOnepyp5o1uZg5c2E= +github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200717090456-b3d9dcddffd1/go.mod h1:UwRKreeVikXn5OarSnt4OqovcEjsIgZVuc5svj7G5w4= github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4= github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200615034914-d40a386309d2 h1:8HmMqu64P4ZDGtcVwZDfmS4xuLXYjf2iery8teY7d9c= github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200615034914-d40a386309d2/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200717085400-dd2ba56ee6b8 h1:cXCR9FOOkTEZ3t+asmy3lLv2AKYAah2igfx7WnNnVMc= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20200717085400-dd2ba56ee6b8/go.mod h1:ZoOP3yUG0HD1T4IUjIFsz/2OAB2yB4YX6NSm4K+uJRg= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk= @@ -70,12 +79,16 @@ github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= github.com/dsoprea/go-png-image-structure v0.0.0-20200615034826-4cfc78940228 h1:GKAdOrszPH3mQ44eRg2kw9zBW0hi2L78ZNjkTx+cte0= github.com/dsoprea/go-png-image-structure v0.0.0-20200615034826-4cfc78940228/go.mod h1:aDYQkL/5gfRNZkoxiLTSWU4Y8/gV/4MVsy/MU9uwTak= +github.com/dsoprea/go-tiff-image-structure v0.0.0-20200717073440-8ac81ec8b423 h1:aIXEGtyKFKqeNW2rc4cx3J2TLxQ9F5fwWPSbq6p6Fq8= +github.com/dsoprea/go-tiff-image-structure v0.0.0-20200717073440-8ac81ec8b423/go.mod h1:we+M+yrq8ifsA33a7C7p8E1ztBbdDYjMIC8RMm8KPL8= github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176 h1:CfXezFYb2STGOd1+n1HshvE191zVx+QX3A1nML5xxME= github.com/dsoprea/go-utility v0.0.0-20200512094054-1abbbc781176/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU= github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e h1:ojqYA1mU6LuRm8XzrVOvyfb000y59cbUcu6Wt8sFSAs= github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= +github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E= +github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= diff --git a/internal/meta/exif.go b/internal/meta/exif.go index d73898ecc..f9d3e30b4 100644 --- a/internal/meta/exif.go +++ b/internal/meta/exif.go @@ -3,7 +3,6 @@ package meta import ( "fmt" "math" - "path" "path/filepath" "runtime/debug" "strconv" @@ -15,6 +14,8 @@ import ( heicexif "github.com/dsoprea/go-heic-exif-extractor" "github.com/dsoprea/go-jpeg-image-structure" "github.com/dsoprea/go-png-image-structure" + "github.com/dsoprea/go-tiff-image-structure" + "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" "gopkg.in/ugjka/go-tz.v2/tz" ) @@ -38,14 +39,14 @@ func ValidDateTime(s string) bool { } // Exif parses an image file for Exif meta data and returns as Data struct. -func Exif(fileName string) (data Data, err error) { - err = data.Exif(fileName) +func Exif(fileName string, fileType fs.FileType) (data Data, err error) { + err = data.Exif(fileName, fileType) return data, err } // Exif parses an image file for Exif meta data and returns as Data struct. -func (data *Data) Exif(fileName string) (err error) { +func (data *Data) Exif(fileName string, fileType fs.FileType) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("metadata: %s in %s (exif panic)\nstack: %s", e, txt.Quote(filepath.Base(fileName)), debug.Stack()) @@ -57,12 +58,11 @@ func (data *Data) Exif(fileName string) (err error) { var parsed bool logName := txt.Quote(filepath.Base(fileName)) - ext := strings.ToLower(path.Ext(fileName)) - if ext == ".jpg" || ext == ".jpeg" { - jmp := jpegstructure.NewJpegMediaParser() + if fileType == fs.TypeJpeg { + jpegMp := jpegstructure.NewJpegMediaParser() - sl, err := jmp.ParseFile(fileName) + sl, err := jpegMp.ParseFile(fileName) if err != nil { return err @@ -81,10 +81,10 @@ func (data *Data) Exif(fileName string) (err error) { } else { parsed = true } - } else if ext == ".png" { - pmp := pngstructure.NewPngMediaParser() + } else if fileType == fs.TypePng { + pngMp := pngstructure.NewPngMediaParser() - cs, err := pmp.ParseFile(fileName) + cs, err := pngMp.ParseFile(fileName) if err != nil { return err @@ -101,10 +101,10 @@ func (data *Data) Exif(fileName string) (err error) { } else { parsed = true } - } else if ext == ".heic" { - hmp := heicexif.NewHeicExifMediaParser() + } else if fileType == fs.TypeHEIF { + heicMp := heicexif.NewHeicExifMediaParser() - cs, err := hmp.ParseFile(fileName) + cs, err := heicMp.ParseFile(fileName) if err != nil { return err @@ -121,6 +121,26 @@ func (data *Data) Exif(fileName string) (err error) { } else { parsed = true } + } else if fileType == fs.TypeTiff { + tiffMp := tiffstructure.NewTiffMediaParser() + + cs, err := tiffMp.ParseFile(fileName) + + if err != nil { + return err + } + + _, rawExif, err = cs.Exif() + + if err != nil { + if err.Error() == "file does not have EXIF" { + return fmt.Errorf("metadata: no exif header in %s (parse tiff)", logName) + } else { + log.Warnf("metadata: %s in %s (parse tiff)", err, logName) + } + } else { + parsed = true + } } if !parsed { diff --git a/internal/meta/exif_test.go b/internal/meta/exif_test.go index c4eb670a9..f38b4de83 100644 --- a/internal/meta/exif_test.go +++ b/internal/meta/exif_test.go @@ -3,12 +3,13 @@ package meta import ( "testing" + "github.com/photoprism/photoprism/pkg/fs" "github.com/stretchr/testify/assert" ) func TestExif(t *testing.T) { t.Run("photoshop.jpg", func(t *testing.T) { - data, err := Exif("testdata/photoshop.jpg") + data, err := Exif("testdata/photoshop.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -37,7 +38,7 @@ func TestExif(t *testing.T) { }) t.Run("ladybug.jpg", func(t *testing.T) { - data, err := Exif("testdata/ladybug.jpg") + data, err := Exif("testdata/ladybug.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -69,7 +70,7 @@ func TestExif(t *testing.T) { }) t.Run("gopro_hd2.jpg", func(t *testing.T) { - data, err := Exif("testdata/gopro_hd2.jpg") + data, err := Exif("testdata/gopro_hd2.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -98,7 +99,7 @@ func TestExif(t *testing.T) { }) t.Run("tweethog.png", func(t *testing.T) { - _, err := Exif("testdata/tweethog.png") + _, err := Exif("testdata/tweethog.png", fs.TypePng) if err == nil { t.Fatal("err should NOT be nil") @@ -108,7 +109,7 @@ func TestExif(t *testing.T) { }) t.Run("iphone_7.heic", func(t *testing.T) { - data, err := Exif("testdata/iphone_7.heic") + data, err := Exif("testdata/iphone_7.heic", fs.TypeHEIF) if err != nil { t.Fatal(err) } @@ -129,7 +130,7 @@ func TestExif(t *testing.T) { }) t.Run("gps-2000.jpg", func(t *testing.T) { - data, err := Exif("testdata/gps-2000.jpg") + data, err := Exif("testdata/gps-2000.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -157,7 +158,7 @@ func TestExif(t *testing.T) { }) t.Run("image-2011.jpg", func(t *testing.T) { - data, err := Exif("testdata/image-2011.jpg") + data, err := Exif("testdata/image-2011.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -192,7 +193,7 @@ func TestExif(t *testing.T) { }) t.Run("ship.jpg", func(t *testing.T) { - data, err := Exif("testdata/ship.jpg") + data, err := Exif("testdata/ship.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -213,7 +214,7 @@ func TestExif(t *testing.T) { }) t.Run("no-exif-data.jpg", func(t *testing.T) { - _, err := Exif("testdata/no-exif-data.jpg") + _, err := Exif("testdata/no-exif-data.jpg", fs.TypeJpeg) if err == nil { t.Fatal("err should NOT be nil") @@ -223,7 +224,7 @@ func TestExif(t *testing.T) { }) t.Run("screenshot.png", func(t *testing.T) { - data, err := Exif("testdata/screenshot.png") + data, err := Exif("testdata/screenshot.png", fs.TypePng) if err != nil { t.Fatal(err) @@ -234,7 +235,7 @@ func TestExif(t *testing.T) { }) t.Run("orientation.jpg", func(t *testing.T) { - data, err := Exif("testdata/orientation.jpg") + data, err := Exif("testdata/orientation.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -262,13 +263,13 @@ func TestExif(t *testing.T) { }) t.Run("gopher-preview.jpg", func(t *testing.T) { - _, err := Exif("testdata/gopher-preview.jpg") + _, err := Exif("testdata/gopher-preview.jpg", fs.TypeJpeg) assert.EqualError(t, err, "metadata: no exif header in gopher-preview.jpg (search and extract)") }) t.Run("huawei-gps-error.jpg", func(t *testing.T) { - data, err := Exif("testdata/huawei-gps-error.jpg") + data, err := Exif("testdata/huawei-gps-error.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -289,7 +290,7 @@ func TestExif(t *testing.T) { }) t.Run("panorama360.jpg", func(t *testing.T) { - data, err := Exif("testdata/panorama360.jpg") + data, err := Exif("testdata/panorama360.jpg", fs.TypeJpeg) if err != nil { t.Fatal(err) @@ -315,7 +316,38 @@ func TestExif(t *testing.T) { assert.Equal(t, "", data.CameraOwner) assert.Equal(t, "", data.CameraSerial) assert.Equal(t, 6, data.FocalLength) - assert.Equal(t, 0, int(data.Orientation)) + assert.Equal(t, 0, data.Orientation) + assert.Equal(t, "", data.Projection) + }) + + t.Run("exif-example.tiff", func(t *testing.T) { + data, err := Exif("testdata/exif-example.tiff", fs.TypeTiff) + + if err != nil { + t.Fatal(err) + } + + // t.Logf("all: %+v", data.All) + + assert.Equal(t, "", data.Artist) + assert.Equal(t, "0001-01-01T00:00:00Z", data.TakenAt.Format("2006-01-02T15:04:05Z")) + assert.Equal(t, "0001-01-01T00:00:00Z", data.TakenAtLocal.Format("2006-01-02T15:04:05Z")) + assert.Equal(t, "", data.Title) + assert.Equal(t, "", data.Keywords) + assert.Equal(t, "", data.Description) + assert.Equal(t, "", data.Copyright) + assert.Equal(t, 43, data.Height) + assert.Equal(t, 65, data.Width) + assert.Equal(t, float32(0), data.Lat) + assert.Equal(t, float32(0), data.Lng) + assert.Equal(t, 0, data.Altitude) + assert.Equal(t, "", data.Exposure) + assert.Equal(t, "", data.CameraMake) + assert.Equal(t, "", data.CameraModel) + assert.Equal(t, "", data.CameraOwner) + assert.Equal(t, "", data.CameraSerial) + assert.Equal(t, 0, data.FocalLength) + assert.Equal(t, 1, data.Orientation) assert.Equal(t, "", data.Projection) }) } diff --git a/internal/meta/testdata/exif-example.tiff b/internal/meta/testdata/exif-example.tiff new file mode 100644 index 000000000..df2bfe270 Binary files /dev/null and b/internal/meta/testdata/exif-example.tiff differ diff --git a/internal/photoprism/index.go b/internal/photoprism/index.go index 90a777cbb..59b47a46f 100644 --- a/internal/photoprism/index.go +++ b/internal/photoprism/index.go @@ -166,6 +166,10 @@ func (ind *Index) Start(opt IndexOptions) fs.Done { var files MediaFiles for _, f := range related.Files { + if ind.files.Ignore(f.RelName(originalsPath), f.ModTime(), opt.Rescan) { + return nil + } + if done[f.FileName()].Processed() { continue } @@ -173,7 +177,6 @@ func (ind *Index) Start(opt IndexOptions) fs.Done { files = append(files, f) filesIndexed++ done[f.FileName()] = fs.Processed - ind.files.Add(f.RelName(originalsPath), f.ModTime()) } filesIndexed++ diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index 0bcbd27a8..70f400b59 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -574,6 +574,21 @@ func (m *MediaFile) IsJpeg() bool { return m.MimeType() == fs.MimeTypeJpeg } +// IsPng returns true if this is a PNG file. +func (m *MediaFile) IsPng() bool { + return m.MimeType() == fs.MimeTypePng +} + +// IsGif returns true if this is a GIF file. +func (m *MediaFile) IsGif() bool { + return m.MimeType() == fs.MimeTypeGif +} + +// IsBitmap returns true if this is a bitmap file. +func (m *MediaFile) IsBitmap() bool { + return m.MimeType() == fs.MimeTypeBitmap +} + // IsJson return true if this media file is a json sidecar file. func (m *MediaFile) IsJson() bool { return m.HasFileType(fs.TypeJson) @@ -581,11 +596,18 @@ func (m *MediaFile) IsJson() bool { // FileType returns the file type (jpg, gif, tiff,...). func (m *MediaFile) FileType() fs.FileType { - if m.IsJpeg() { + switch { + case m.IsJpeg(): return fs.TypeJpeg + case m.IsPng(): + return fs.TypePng + case m.IsGif(): + return fs.TypeGif + case m.IsBitmap(): + return fs.TypeBitmap + default: + return fs.GetFileType(m.fileName) } - - return fs.GetFileType(m.fileName) } // MediaType returns the media type (video, image, raw, sidecar,...). @@ -607,11 +629,6 @@ func (m *MediaFile) IsRaw() bool { return m.HasFileType(fs.TypeRaw) } -// IsPng returns true if this is a PNG file. -func (m *MediaFile) IsPng() bool { - return m.HasFileType(fs.TypePng) -} - // IsTiff returns true if this is a TIFF file. func (m *MediaFile) IsTiff() bool { return m.HasFileType(fs.TypeTiff) @@ -619,14 +636,8 @@ func (m *MediaFile) IsTiff() bool { // IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file. func (m *MediaFile) IsImageOther() bool { - switch m.FileType() { - case fs.TypeBitmap: - return true - case fs.TypeGif: - return true - case fs.TypePng: - return true - case fs.TypeTiff: + switch { + case m.IsPng(), m.IsGif(), m.IsTiff(), m.IsBitmap(): return true default: return false @@ -663,6 +674,11 @@ func (m *MediaFile) IsPhoto() bool { return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsImageOther() } +// 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() +} + // IsMedia returns true if this is a media file (photo or video, not sidecar or other). func (m *MediaFile) IsMedia() bool { return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsHEIF() || m.IsImageOther() @@ -707,7 +723,7 @@ func (m *MediaFile) HasJson() bool { func (m *MediaFile) decodeDimensions() error { if !m.IsMedia() { - return fmt.Errorf("not a photo: %s", m.FileName()) + return fmt.Errorf("failed decoding dimensions for %s", txt.Quote(m.BaseName())) } var width, height int @@ -719,7 +735,7 @@ func (m *MediaFile) decodeDimensions() error { height = data.Height } - if m.IsJpeg() { + if m.IsJpeg() || m.IsPng() || m.IsGif() { file, err := os.Open(m.FileName()) if err != nil || file == nil { diff --git a/internal/photoprism/mediafile_test.go b/internal/photoprism/mediafile_test.go index 84bc04196..0be0f3673 100644 --- a/internal/photoprism/mediafile_test.go +++ b/internal/photoprism/mediafile_test.go @@ -1098,9 +1098,13 @@ func TestMediaFile_IsPng(t *testing.T) { conf := config.TestConfig() mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png") + if err != nil { t.Fatal(err) } + + assert.Equal(t, fs.TypePng, mediaFile.FileType()) + assert.Equal(t, "image/png", mediaFile.MimeType()) assert.Equal(t, true, mediaFile.IsPng()) }) } @@ -1113,6 +1117,8 @@ func TestMediaFile_IsTiff(t *testing.T) { if err != nil { t.Fatal(err) } + assert.Equal(t, fs.TypeJson, mediaFile.FileType()) + assert.Equal(t, "text/plain; charset=utf-8", mediaFile.MimeType()) assert.Equal(t, false, mediaFile.IsTiff()) }) t.Run("/purple.tiff", func(t *testing.T) { @@ -1122,6 +1128,19 @@ func TestMediaFile_IsTiff(t *testing.T) { if err != nil { t.Fatal(err) } + assert.Equal(t, fs.TypeTiff, mediaFile.FileType()) + assert.Equal(t, "application/octet-stream", mediaFile.MimeType()) + assert.Equal(t, true, mediaFile.IsTiff()) + }) + t.Run("/example.tiff", func(t *testing.T) { + conf := config.TestConfig() + + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/example.tif") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, fs.TypeTiff, mediaFile.FileType()) + assert.Equal(t, "application/octet-stream", mediaFile.MimeType()) assert.Equal(t, true, mediaFile.IsTiff()) }) } @@ -1161,6 +1180,9 @@ func TestMediaFile_IsImageOther(t *testing.T) { if err != nil { t.Fatal(err) } + assert.Equal(t, fs.TypeBitmap, mediaFile.FileType()) + assert.Equal(t, "image/bmp", mediaFile.MimeType()) + assert.Equal(t, true, mediaFile.IsBitmap()) assert.Equal(t, true, mediaFile.IsImageOther()) }) t.Run("/preloader.gif", func(t *testing.T) { @@ -1170,6 +1192,9 @@ func TestMediaFile_IsImageOther(t *testing.T) { if err != nil { t.Fatal(err) } + + assert.Equal(t, fs.TypeGif, mediaFile.FileType()) + assert.Equal(t, "image/gif", mediaFile.MimeType()) assert.Equal(t, true, mediaFile.IsImageOther()) }) } @@ -1409,8 +1434,9 @@ func TestMediaFile_decodeDimension(t *testing.T) { decodeErr := mediaFile.decodeDimensions() - assert.EqualError(t, decodeErr, "not a photo: "+conf.ExamplesPath()+"/Random.docx") + assert.EqualError(t, decodeErr, "failed decoding dimensions for Random.docx") }) + t.Run("clock_purple.jpg", func(t *testing.T) { conf := config.TestConfig() @@ -1424,6 +1450,7 @@ func TestMediaFile_decodeDimension(t *testing.T) { t.Fatal(err) } }) + t.Run("iphone_7.heic", func(t *testing.T) { conf := config.TestConfig() @@ -1437,6 +1464,57 @@ func TestMediaFile_decodeDimension(t *testing.T) { t.Fatal(err) } }) + + t.Run("example.png", func(t *testing.T) { + conf := config.TestConfig() + + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/example.png") + + if err != nil { + t.Fatal(err) + } + + if err := mediaFile.decodeDimensions(); err != nil { + t.Fatal(err) + } + + assert.Equal(t, 100, mediaFile.Width()) + assert.Equal(t, 67, mediaFile.Height()) + }) + + t.Run("example.gif", func(t *testing.T) { + conf := config.TestConfig() + + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/example.gif") + + if err != nil { + t.Fatal(err) + } + + if err := mediaFile.decodeDimensions(); err != nil { + t.Fatal(err) + } + + assert.Equal(t, 100, mediaFile.Width()) + assert.Equal(t, 67, mediaFile.Height()) + }) + + t.Run("blue-go-video.mp4", func(t *testing.T) { + conf := config.TestConfig() + + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4") + + if err != nil { + t.Fatal(err) + } + + if err := mediaFile.decodeDimensions(); err != nil { + t.Fatal(err) + } + + assert.Equal(t, 1920, mediaFile.Width()) + assert.Equal(t, 1080, mediaFile.Height()) + }) } func TestMediaFile_Width(t *testing.T) { diff --git a/internal/photoprism/metadata.go b/internal/photoprism/metadata.go index afcdd45c0..2f9c199a6 100644 --- a/internal/photoprism/metadata.go +++ b/internal/photoprism/metadata.go @@ -1,7 +1,7 @@ package photoprism import ( - "errors" + "fmt" "path/filepath" "github.com/photoprism/photoprism/internal/meta" @@ -14,10 +14,10 @@ func (m *MediaFile) MetaData() (result meta.Data) { m.metaDataOnce.Do(func() { var err error - if m.IsPhoto() { - err = m.metaData.Exif(m.FileName()) + if m.ExifSupported() { + err = m.metaData.Exif(m.FileName(), m.FileType()) } else { - err = errors.New("not a photo") + 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"). diff --git a/pkg/fs/mime.go b/pkg/fs/mime.go index b677b53c0..8adcda989 100644 --- a/pkg/fs/mime.go +++ b/pkg/fs/mime.go @@ -6,7 +6,10 @@ import ( ) const ( - MimeTypeJpeg = "image/jpeg" + MimeTypeJpeg = "image/jpeg" + MimeTypePng = "image/png" + MimeTypeGif = "image/gif" + MimeTypeBitmap = "image/bmp" ) // MimeType returns the mime type of a file, empty string if unknown.