From 138dabd0c89fe42cdfb2b01a8bb40c6075efc052 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 19 Jul 2020 16:39:43 +0200 Subject: [PATCH] Metadata: Use mime type to determine file format and exif parser #391 Signed-off-by: Michael Mayer --- go.mod | 9 +-- go.sum | 13 ++++ internal/meta/exif.go | 48 ++++++++++---- internal/meta/exif_test.go | 62 +++++++++++++----- internal/meta/testdata/exif-example.tiff | Bin 0 -> 14682 bytes internal/photoprism/index.go | 5 +- internal/photoprism/mediafile.go | 52 ++++++++++----- internal/photoprism/mediafile_test.go | 80 ++++++++++++++++++++++- internal/photoprism/metadata.go | 8 +-- pkg/fs/mime.go | 5 +- 10 files changed, 224 insertions(+), 58 deletions(-) create mode 100644 internal/meta/testdata/exif-example.tiff 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 0000000000000000000000000000000000000000..df2bfe270c5eecdfcf3b184b13b8d825ceaf31d5 GIT binary patch literal 14682 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 A{r~^~ literal 0 HcmV?d00001 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.