Metadata: Use mime type to determine file format and exif parser #391

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-07-19 16:39:43 +02:00
parent 0023fdb1e2
commit 138dabd0c8
10 changed files with 224 additions and 58 deletions

9
go.mod
View file

@ -6,14 +6,15 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/djherbis/times v1.2.0 github.com/djherbis/times v1.2.0
github.com/dsoprea/go-exif/v2 v2.0.0-20200717063959-46b1a0cd1772 // indirect github.com/dsoprea/go-exif/v2 v2.0.0-20200717071058-9393e7afd446 // indirect
github.com/dsoprea/go-exif/v3 v3.0.0-20200717063959-46b1a0cd1772 github.com/dsoprea/go-exif/v3 v3.0.0-20200717071058-9393e7afd446
github.com/dsoprea/go-heic-exif-extractor v0.0.0-20200520190950-3ae4ff88a0d1 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-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-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // 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-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/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3

13
go.sum
View file

@ -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-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 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-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 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-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 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-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 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-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 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8=
github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= 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 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-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 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-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d h1:F/7L5wr/fP/SKeO5HuMlNEX9Ipyx2MbH2rV9G4zJRpk= 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-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 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-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 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-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 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-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 h1:ojqYA1mU6LuRm8XzrVOvyfb000y59cbUcu6Wt8sFSAs=
github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= 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 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 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= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=

View file

@ -3,7 +3,6 @@ package meta
import ( import (
"fmt" "fmt"
"math" "math"
"path"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"strconv" "strconv"
@ -15,6 +14,8 @@ import (
heicexif "github.com/dsoprea/go-heic-exif-extractor" heicexif "github.com/dsoprea/go-heic-exif-extractor"
"github.com/dsoprea/go-jpeg-image-structure" "github.com/dsoprea/go-jpeg-image-structure"
"github.com/dsoprea/go-png-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" "github.com/photoprism/photoprism/pkg/txt"
"gopkg.in/ugjka/go-tz.v2/tz" "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. // Exif parses an image file for Exif meta data and returns as Data struct.
func Exif(fileName string) (data Data, err error) { func Exif(fileName string, fileType fs.FileType) (data Data, err error) {
err = data.Exif(fileName) err = data.Exif(fileName, fileType)
return data, err return data, err
} }
// Exif parses an image file for Exif meta data and returns as Data struct. // 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() { defer func() {
if e := recover(); e != nil { if e := recover(); e != nil {
err = fmt.Errorf("metadata: %s in %s (exif panic)\nstack: %s", e, txt.Quote(filepath.Base(fileName)), debug.Stack()) 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 var parsed bool
logName := txt.Quote(filepath.Base(fileName)) logName := txt.Quote(filepath.Base(fileName))
ext := strings.ToLower(path.Ext(fileName))
if ext == ".jpg" || ext == ".jpeg" { if fileType == fs.TypeJpeg {
jmp := jpegstructure.NewJpegMediaParser() jpegMp := jpegstructure.NewJpegMediaParser()
sl, err := jmp.ParseFile(fileName) sl, err := jpegMp.ParseFile(fileName)
if err != nil { if err != nil {
return err return err
@ -81,10 +81,10 @@ func (data *Data) Exif(fileName string) (err error) {
} else { } else {
parsed = true parsed = true
} }
} else if ext == ".png" { } else if fileType == fs.TypePng {
pmp := pngstructure.NewPngMediaParser() pngMp := pngstructure.NewPngMediaParser()
cs, err := pmp.ParseFile(fileName) cs, err := pngMp.ParseFile(fileName)
if err != nil { if err != nil {
return err return err
@ -101,10 +101,10 @@ func (data *Data) Exif(fileName string) (err error) {
} else { } else {
parsed = true parsed = true
} }
} else if ext == ".heic" { } else if fileType == fs.TypeHEIF {
hmp := heicexif.NewHeicExifMediaParser() heicMp := heicexif.NewHeicExifMediaParser()
cs, err := hmp.ParseFile(fileName) cs, err := heicMp.ParseFile(fileName)
if err != nil { if err != nil {
return err return err
@ -121,6 +121,26 @@ func (data *Data) Exif(fileName string) (err error) {
} else { } else {
parsed = true 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 { if !parsed {

View file

@ -3,12 +3,13 @@ package meta
import ( import (
"testing" "testing"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestExif(t *testing.T) { func TestExif(t *testing.T) {
t.Run("photoshop.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -37,7 +38,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("ladybug.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -69,7 +70,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("gopro_hd2.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -98,7 +99,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("tweethog.png", func(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 { if err == nil {
t.Fatal("err should NOT be 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) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -129,7 +130,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("gps-2000.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -157,7 +158,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("image-2011.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -192,7 +193,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("ship.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -213,7 +214,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("no-exif-data.jpg", func(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 { if err == nil {
t.Fatal("err should NOT be 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) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -234,7 +235,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("orientation.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -262,13 +263,13 @@ func TestExif(t *testing.T) {
}) })
t.Run("gopher-preview.jpg", func(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)") 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) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -289,7 +290,7 @@ func TestExif(t *testing.T) {
}) })
t.Run("panorama360.jpg", func(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -315,7 +316,38 @@ func TestExif(t *testing.T) {
assert.Equal(t, "", data.CameraOwner) assert.Equal(t, "", data.CameraOwner)
assert.Equal(t, "", data.CameraSerial) assert.Equal(t, "", data.CameraSerial)
assert.Equal(t, 6, data.FocalLength) 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) assert.Equal(t, "", data.Projection)
}) })
} }

BIN
internal/meta/testdata/exif-example.tiff vendored Normal file

Binary file not shown.

View file

@ -166,6 +166,10 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
var files MediaFiles var files MediaFiles
for _, f := range related.Files { for _, f := range related.Files {
if ind.files.Ignore(f.RelName(originalsPath), f.ModTime(), opt.Rescan) {
return nil
}
if done[f.FileName()].Processed() { if done[f.FileName()].Processed() {
continue continue
} }
@ -173,7 +177,6 @@ func (ind *Index) Start(opt IndexOptions) fs.Done {
files = append(files, f) files = append(files, f)
filesIndexed++ filesIndexed++
done[f.FileName()] = fs.Processed done[f.FileName()] = fs.Processed
ind.files.Add(f.RelName(originalsPath), f.ModTime())
} }
filesIndexed++ filesIndexed++

View file

@ -574,6 +574,21 @@ func (m *MediaFile) IsJpeg() bool {
return m.MimeType() == fs.MimeTypeJpeg 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. // IsJson return true if this media file is a json sidecar file.
func (m *MediaFile) IsJson() bool { func (m *MediaFile) IsJson() bool {
return m.HasFileType(fs.TypeJson) return m.HasFileType(fs.TypeJson)
@ -581,11 +596,18 @@ func (m *MediaFile) IsJson() bool {
// FileType returns the file type (jpg, gif, tiff,...). // FileType returns the file type (jpg, gif, tiff,...).
func (m *MediaFile) FileType() fs.FileType { func (m *MediaFile) FileType() fs.FileType {
if m.IsJpeg() { switch {
case m.IsJpeg():
return fs.TypeJpeg 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,...). // MediaType returns the media type (video, image, raw, sidecar,...).
@ -607,11 +629,6 @@ func (m *MediaFile) IsRaw() bool {
return m.HasFileType(fs.TypeRaw) 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. // IsTiff returns true if this is a TIFF file.
func (m *MediaFile) IsTiff() bool { func (m *MediaFile) IsTiff() bool {
return m.HasFileType(fs.TypeTiff) 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. // IsImageOther returns true if this is a PNG, GIF, BMP or TIFF file.
func (m *MediaFile) IsImageOther() bool { func (m *MediaFile) IsImageOther() bool {
switch m.FileType() { switch {
case fs.TypeBitmap: case m.IsPng(), m.IsGif(), m.IsTiff(), m.IsBitmap():
return true
case fs.TypeGif:
return true
case fs.TypePng:
return true
case fs.TypeTiff:
return true return true
default: default:
return false return false
@ -663,6 +674,11 @@ func (m *MediaFile) IsPhoto() bool {
return m.IsJpeg() || m.IsRaw() || m.IsHEIF() || m.IsImageOther() 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). // IsMedia returns true if this is a media file (photo or video, not sidecar or other).
func (m *MediaFile) IsMedia() bool { func (m *MediaFile) IsMedia() bool {
return m.IsJpeg() || m.IsVideo() || m.IsRaw() || m.IsHEIF() || m.IsImageOther() 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 { func (m *MediaFile) decodeDimensions() error {
if !m.IsMedia() { 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 var width, height int
@ -719,7 +735,7 @@ func (m *MediaFile) decodeDimensions() error {
height = data.Height height = data.Height
} }
if m.IsJpeg() { if m.IsJpeg() || m.IsPng() || m.IsGif() {
file, err := os.Open(m.FileName()) file, err := os.Open(m.FileName())
if err != nil || file == nil { if err != nil || file == nil {

View file

@ -1098,9 +1098,13 @@ func TestMediaFile_IsPng(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png") mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/tweethog.png")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, fs.TypePng, mediaFile.FileType())
assert.Equal(t, "image/png", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsPng()) assert.Equal(t, true, mediaFile.IsPng())
}) })
} }
@ -1113,6 +1117,8 @@ func TestMediaFile_IsTiff(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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()) assert.Equal(t, false, mediaFile.IsTiff())
}) })
t.Run("/purple.tiff", func(t *testing.T) { t.Run("/purple.tiff", func(t *testing.T) {
@ -1122,6 +1128,19 @@ func TestMediaFile_IsTiff(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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()) assert.Equal(t, true, mediaFile.IsTiff())
}) })
} }
@ -1161,6 +1180,9 @@ func TestMediaFile_IsImageOther(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) 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()) assert.Equal(t, true, mediaFile.IsImageOther())
}) })
t.Run("/preloader.gif", func(t *testing.T) { t.Run("/preloader.gif", func(t *testing.T) {
@ -1170,6 +1192,9 @@ func TestMediaFile_IsImageOther(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, fs.TypeGif, mediaFile.FileType())
assert.Equal(t, "image/gif", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsImageOther()) assert.Equal(t, true, mediaFile.IsImageOther())
}) })
} }
@ -1409,8 +1434,9 @@ func TestMediaFile_decodeDimension(t *testing.T) {
decodeErr := mediaFile.decodeDimensions() 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) { t.Run("clock_purple.jpg", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
@ -1424,6 +1450,7 @@ func TestMediaFile_decodeDimension(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
}) })
t.Run("iphone_7.heic", func(t *testing.T) { t.Run("iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()
@ -1437,6 +1464,57 @@ func TestMediaFile_decodeDimension(t *testing.T) {
t.Fatal(err) 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) { func TestMediaFile_Width(t *testing.T) {

View file

@ -1,7 +1,7 @@
package photoprism package photoprism
import ( import (
"errors" "fmt"
"path/filepath" "path/filepath"
"github.com/photoprism/photoprism/internal/meta" "github.com/photoprism/photoprism/internal/meta"
@ -14,10 +14,10 @@ func (m *MediaFile) MetaData() (result meta.Data) {
m.metaDataOnce.Do(func() { m.metaDataOnce.Do(func() {
var err error var err error
if m.IsPhoto() { if m.ExifSupported() {
err = m.metaData.Exif(m.FileName()) err = m.metaData.Exif(m.FileName(), m.FileType())
} else { } 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"). // Parse JSON sidecar file names as Google Photos uses them ("img_1234.jpg.json").

View file

@ -6,7 +6,10 @@ import (
) )
const ( 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. // MimeType returns the mime type of a file, empty string if unknown.