From b10c607f3a8f98615f63692d6d8b30e26a9cd0c2 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 24 Feb 2020 23:30:08 +0100 Subject: [PATCH] Add exif parsing --- api/database/migrations/0002_photo.up.sql | 7 +- api/go.mod | 5 +- api/go.sum | 16 ++ api/gqlgen.yml | 4 + api/graphql/generated.go | 197 ++++++++++------- api/graphql/models/generated.go | 25 --- api/graphql/models/photo_exif.go | 39 ++++ api/graphql/resolvers/photo.go | 6 +- api/graphql/schema.graphql | 7 +- api/scanner/exif.go | 248 ++++++++++++++++++++++ api/scanner/photo_scanner.go | 6 + api/scanner/process_photo.go | 8 +- 12 files changed, 453 insertions(+), 115 deletions(-) create mode 100644 api/graphql/models/photo_exif.go create mode 100644 api/scanner/exif.go diff --git a/api/database/migrations/0002_photo.up.sql b/api/database/migrations/0002_photo.up.sql index 15c8dcb5..5d27a984 100644 --- a/api/database/migrations/0002_photo.up.sql +++ b/api/database/migrations/0002_photo.up.sql @@ -3,13 +3,14 @@ CREATE TABLE IF NOT EXISTS photo_exif ( camera varchar(256), maker varchar(256), lens varchar(256), - dateShot timestamp, - file_size_bytes bigint, + dateShot timestamp NULL, exposure varchar(256), - aperature float, + aperture float, iso int(6), focal_length float, flash varchar(256), + orientation int(1), + exposure_program int(1), PRIMARY KEY (exif_id) ); diff --git a/api/go.mod b/api/go.mod index 8b73d491..695f711e 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,8 +5,6 @@ go 1.13 require ( github.com/99designs/gqlgen v0.10.2 github.com/fatih/color v1.9.0 - github.com/go-chi/chi v3.3.2+incompatible - github.com/go-chi/cors v1.0.0 github.com/go-sql-driver/mysql v1.5.0 github.com/golang-migrate/migrate v3.5.4+incompatible github.com/gorilla/mux v1.7.4 @@ -16,10 +14,9 @@ require ( github.com/lib/pq v1.3.0 github.com/nf/cr2 v0.0.0-20180623103828-4699471a17ed github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 - github.com/rs/cors v1.6.0 - github.com/sirupsen/logrus v1.4.2 github.com/vektah/gqlparser v1.2.0 github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 + github.com/xor-gate/goexif2 v1.1.0 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/image v0.0.0-20200119044424-58c23975cae1 ) diff --git a/api/go.sum b/api/go.sum index 03aa30f3..a7678391 100644 --- a/api/go.sum +++ b/api/go.sum @@ -6,17 +6,27 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dsoprea/go-exif v0.0.0-20200126052615-bd04addaf40f h1:yIPu74TXwq1dxn9edEn4p1si4DW/c/h7sgEJ/zMZNcg= +github.com/dsoprea/go-exif v0.0.0-20200126052615-bd04addaf40f/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= +github.com/dsoprea/go-exif/v2 v2.0.0-20200126052615-bd04addaf40f h1:LJ4lH4r8MgKEC5HeSNRw81lfVOlAM0xELOOvf712H0o= +github.com/dsoprea/go-exif/v2 v2.0.0-20200126052615-bd04addaf40f/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= +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/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0= github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXgptLmNLwynMSHUmU6besqtiw= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -76,11 +86,15 @@ github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF github.com/viktorstrate/photoview v0.0.0-20200119220544-691e4c7dc433 h1:n6jGnDctC9HI7B1rnc5ATQPYaaxQaJtS8bI6oI0QV34= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= +github.com/xor-gate/goexif2 v1.1.0 h1:OvTZ5iEvsDhRWFjV5xY3wT7uHFna28nSSP7ucau+cXQ= +github.com/xor-gate/goexif2 v1.1.0/go.mod h1:eRjn3VSkAwpNpxEx/CGmd0zg0JFGL3akrSMxnJ581AY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -96,5 +110,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 67267481..d0222856 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -20,12 +20,16 @@ resolver: autobind: [] models: + ID: + model: github.com/99designs/gqlgen/graphql.IntID User: model: github.com/viktorstrate/photoview/api/graphql/models.User Photo: model: github.com/viktorstrate/photoview/api/graphql/models.Photo PhotoURL: model: github.com/viktorstrate/photoview/api/graphql/models.PhotoURL + PhotoEXIF: + model: github.com/viktorstrate/photoview/api/graphql/models.PhotoEXIF Album: model: github.com/viktorstrate/photoview/api/graphql/models.Album ShareToken: diff --git a/api/graphql/generated.go b/api/graphql/generated.go index f2de4228..4bfb0c2b 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -113,17 +113,18 @@ type ComplexityRoot struct { } PhotoExif struct { - Aperture func(childComplexity int) int - Camera func(childComplexity int) int - DateShot func(childComplexity int) int - Exposure func(childComplexity int) int - FileSize func(childComplexity int) int - Flash func(childComplexity int) int - FocalLength func(childComplexity int) int - Iso func(childComplexity int) int - Lens func(childComplexity int) int - Maker func(childComplexity int) int - Photo func(childComplexity int) int + Aperture func(childComplexity int) int + Camera func(childComplexity int) int + DateShot func(childComplexity int) int + Exposure func(childComplexity int) int + ExposureProgram func(childComplexity int) int + Flash func(childComplexity int) int + FocalLength func(childComplexity int) int + ID func(childComplexity int) int + Iso func(childComplexity int) int + Lens func(childComplexity int) int + Maker func(childComplexity int) int + Photo func(childComplexity int) int } PhotoURL struct { @@ -201,7 +202,7 @@ type PhotoResolver interface { Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) HighRes(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) Album(ctx context.Context, obj *models.Photo) (*models.Album, error) - Exif(ctx context.Context, obj *models.Photo) (*models.PhotoExif, error) + Exif(ctx context.Context, obj *models.Photo) (*models.PhotoEXIF, error) Shares(ctx context.Context, obj *models.Photo) ([]*models.ShareToken, error) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) } @@ -629,12 +630,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PhotoExif.Exposure(childComplexity), true - case "PhotoEXIF.fileSize": - if e.complexity.PhotoExif.FileSize == nil { + case "PhotoEXIF.exposureProgram": + if e.complexity.PhotoExif.ExposureProgram == nil { break } - return e.complexity.PhotoExif.FileSize(childComplexity), true + return e.complexity.PhotoExif.ExposureProgram(childComplexity), true case "PhotoEXIF.flash": if e.complexity.PhotoExif.Flash == nil { @@ -650,6 +651,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.PhotoExif.FocalLength(childComplexity), true + case "PhotoEXIF.id": + if e.complexity.PhotoExif.ID == nil { + break + } + + return e.complexity.PhotoExif.ID(childComplexity), true + case "PhotoEXIF.iso": if e.complexity.PhotoExif.Iso == nil { break @@ -1192,6 +1200,7 @@ type Photo { "EXIF metadata from the camera" type PhotoEXIF { + id: Int! photo: Photo "The model name of the camera" camera: String @@ -1200,8 +1209,6 @@ type PhotoEXIF { "The name of the lens" lens: String dateShot: Time - "The formatted filesize of the image" - fileSize: String "The exposure time of the image" exposure: String "The aperature stops of the image" @@ -1209,9 +1216,11 @@ type PhotoEXIF { "The ISO setting of the image" iso: Int "The focal length of the lens, when the image was taken" - focalLength: String + focalLength: Float "A formatted description of the flash settings, when the image was taken" flash: String + "An index describing the mode for adjusting the exposure of the image" + exposureProgram: Int } `}, ) @@ -3162,10 +3171,10 @@ func (ec *executionContext) _Photo_exif(ctx context.Context, field graphql.Colle if resTmp == nil { return graphql.Null } - res := resTmp.(*models.PhotoExif) + res := resTmp.(*models.PhotoEXIF) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec.marshalOPhotoEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoExif(ctx, field.Selections, res) + return ec.marshalOPhotoEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoEXIF(ctx, field.Selections, res) } func (ec *executionContext) _Photo_shares(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) { @@ -3390,7 +3399,7 @@ func (ec *executionContext) _PhotoDownload_url(ctx context.Context, field graphq return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_photo(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_id(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3403,13 +3412,50 @@ func (ec *executionContext) _PhotoEXIF_photo(ctx context.Context, field graphql. Object: "PhotoEXIF", Field: field, Args: nil, - IsMethod: false, + IsMethod: true, } ctx = graphql.WithResolverContext(ctx, rctx) ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Photo, nil + return obj.ID(), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !ec.HasError(rctx) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) _PhotoEXIF_photo(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + ec.Tracer.EndFieldExecution(ctx) + }() + rctx := &graphql.ResolverContext{ + Object: "PhotoEXIF", + Field: field, + Args: nil, + IsMethod: true, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Photo(), nil }) if err != nil { ec.Error(ctx, err) @@ -3424,7 +3470,7 @@ func (ec *executionContext) _PhotoEXIF_photo(ctx context.Context, field graphql. return ec.marshalOPhoto2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhoto(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_camera(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_camera(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3458,7 +3504,7 @@ func (ec *executionContext) _PhotoEXIF_camera(ctx context.Context, field graphql return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_maker(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_maker(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3492,7 +3538,7 @@ func (ec *executionContext) _PhotoEXIF_maker(ctx context.Context, field graphql. return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_lens(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_lens(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3526,7 +3572,7 @@ func (ec *executionContext) _PhotoEXIF_lens(ctx context.Context, field graphql.C return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_dateShot(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_dateShot(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3560,41 +3606,7 @@ func (ec *executionContext) _PhotoEXIF_dateShot(ctx context.Context, field graph return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_fileSize(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { - ctx = ec.Tracer.StartFieldExecution(ctx, field) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - ec.Tracer.EndFieldExecution(ctx) - }() - rctx := &graphql.ResolverContext{ - Object: "PhotoEXIF", - Field: field, - Args: nil, - IsMethod: false, - } - ctx = graphql.WithResolverContext(ctx, rctx) - ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.FileSize, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*string) - rctx.Result = res - ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) -} - -func (ec *executionContext) _PhotoEXIF_exposure(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_exposure(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3628,7 +3640,7 @@ func (ec *executionContext) _PhotoEXIF_exposure(ctx context.Context, field graph return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_aperture(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_aperture(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3662,7 +3674,7 @@ func (ec *executionContext) _PhotoEXIF_aperture(ctx context.Context, field graph return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_iso(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_iso(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3696,7 +3708,7 @@ func (ec *executionContext) _PhotoEXIF_iso(ctx context.Context, field graphql.Co return ec.marshalOInt2ᚖint(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_focalLength(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_focalLength(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3724,13 +3736,13 @@ func (ec *executionContext) _PhotoEXIF_focalLength(ctx context.Context, field gr if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*float64) rctx.Result = res ctx = ec.Tracer.StartFieldChildExecution(ctx) - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res) } -func (ec *executionContext) _PhotoEXIF_flash(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) { +func (ec *executionContext) _PhotoEXIF_flash(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { if r := recover(); r != nil { @@ -3764,6 +3776,40 @@ func (ec *executionContext) _PhotoEXIF_flash(ctx context.Context, field graphql. return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _PhotoEXIF_exposureProgram(ctx context.Context, field graphql.CollectedField, obj *models.PhotoEXIF) (ret graphql.Marshaler) { + ctx = ec.Tracer.StartFieldExecution(ctx, field) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + ec.Tracer.EndFieldExecution(ctx) + }() + rctx := &graphql.ResolverContext{ + Object: "PhotoEXIF", + Field: field, + Args: nil, + IsMethod: false, + } + ctx = graphql.WithResolverContext(ctx, rctx) + ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ExposureProgram, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + rctx.Result = res + ctx = ec.Tracer.StartFieldChildExecution(ctx) + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + func (ec *executionContext) _PhotoURL_url(ctx context.Context, field graphql.CollectedField, obj *models.PhotoURL) (ret graphql.Marshaler) { ctx = ec.Tracer.StartFieldExecution(ctx, field) defer func() { @@ -6531,7 +6577,7 @@ func (ec *executionContext) _PhotoDownload(ctx context.Context, sel ast.Selectio var photoEXIFImplementors = []string{"PhotoEXIF"} -func (ec *executionContext) _PhotoEXIF(ctx context.Context, sel ast.SelectionSet, obj *models.PhotoExif) graphql.Marshaler { +func (ec *executionContext) _PhotoEXIF(ctx context.Context, sel ast.SelectionSet, obj *models.PhotoEXIF) graphql.Marshaler { fields := graphql.CollectFields(ec.RequestContext, sel, photoEXIFImplementors) out := graphql.NewFieldSet(fields) @@ -6540,6 +6586,11 @@ func (ec *executionContext) _PhotoEXIF(ctx context.Context, sel ast.SelectionSet switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("PhotoEXIF") + case "id": + out.Values[i] = ec._PhotoEXIF_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } case "photo": out.Values[i] = ec._PhotoEXIF_photo(ctx, field, obj) case "camera": @@ -6550,8 +6601,6 @@ func (ec *executionContext) _PhotoEXIF(ctx context.Context, sel ast.SelectionSet out.Values[i] = ec._PhotoEXIF_lens(ctx, field, obj) case "dateShot": out.Values[i] = ec._PhotoEXIF_dateShot(ctx, field, obj) - case "fileSize": - out.Values[i] = ec._PhotoEXIF_fileSize(ctx, field, obj) case "exposure": out.Values[i] = ec._PhotoEXIF_exposure(ctx, field, obj) case "aperture": @@ -6562,6 +6611,8 @@ func (ec *executionContext) _PhotoEXIF(ctx context.Context, sel ast.SelectionSet out.Values[i] = ec._PhotoEXIF_focalLength(ctx, field, obj) case "flash": out.Values[i] = ec._PhotoEXIF_flash(ctx, field, obj) + case "exposureProgram": + out.Values[i] = ec._PhotoEXIF_exposureProgram(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -7932,11 +7983,11 @@ func (ec *executionContext) marshalOPhoto2ᚖgithubᚗcomᚋviktorstrateᚋphoto return ec._Photo(ctx, sel, v) } -func (ec *executionContext) marshalOPhotoEXIF2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoExif(ctx context.Context, sel ast.SelectionSet, v models.PhotoExif) graphql.Marshaler { +func (ec *executionContext) marshalOPhotoEXIF2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoEXIF(ctx context.Context, sel ast.SelectionSet, v models.PhotoEXIF) graphql.Marshaler { return ec._PhotoEXIF(ctx, sel, &v) } -func (ec *executionContext) marshalOPhotoEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoExif(ctx context.Context, sel ast.SelectionSet, v *models.PhotoExif) graphql.Marshaler { +func (ec *executionContext) marshalOPhotoEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoEXIF(ctx context.Context, sel ast.SelectionSet, v *models.PhotoEXIF) graphql.Marshaler { if v == nil { return graphql.Null } diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index 2f54781e..c806d17b 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "strconv" - "time" ) type AuthorizeResult struct { @@ -39,30 +38,6 @@ type PhotoDownload struct { URL string `json:"url"` } -// EXIF metadata from the camera -type PhotoExif struct { - Photo *Photo `json:"photo"` - // The model name of the camera - Camera *string `json:"camera"` - // The maker of the camera - Maker *string `json:"maker"` - // The name of the lens - Lens *string `json:"lens"` - DateShot *time.Time `json:"dateShot"` - // The formatted filesize of the image - FileSize *string `json:"fileSize"` - // The exposure time of the image - Exposure *string `json:"exposure"` - // The aperature stops of the image - Aperture *float64 `json:"aperture"` - // The ISO setting of the image - Iso *int `json:"iso"` - // The focal length of the lens, when the image was taken - FocalLength *string `json:"focalLength"` - // A formatted description of the flash settings, when the image was taken - Flash *string `json:"flash"` -} - type ScannerResult struct { Finished bool `json:"finished"` Success bool `json:"success"` diff --git a/api/graphql/models/photo_exif.go b/api/graphql/models/photo_exif.go new file mode 100644 index 00000000..bd40ffd4 --- /dev/null +++ b/api/graphql/models/photo_exif.go @@ -0,0 +1,39 @@ +package models + +import ( + "database/sql" + "time" +) + +type PhotoEXIF struct { + ExifID int + Camera *string + Maker *string + Lens *string + DateShot *time.Time + Exposure *string + Aperture *float64 + Iso *int + FocalLength *float64 + Flash *string + Orientation *int + ExposureProgram *int +} + +func (exif *PhotoEXIF) Photo() *Photo { + panic("not implemented") +} + +func (exif *PhotoEXIF) ID() int { + return exif.ExifID +} + +func NewPhotoExifFromRow(row *sql.Row) (*PhotoEXIF, error) { + exif := PhotoEXIF{} + + if err := row.Scan(&exif.ExifID, &exif.Camera, &exif.Maker, &exif.Lens, &exif.DateShot, &exif.Exposure, &exif.Aperture, &exif.Iso, &exif.FocalLength, &exif.Flash, &exif.Orientation, &exif.ExposureProgram); err != nil { + return nil, err + } + + return &exif, nil +} diff --git a/api/graphql/resolvers/photo.go b/api/graphql/resolvers/photo.go index 31238701..73ef2682 100644 --- a/api/graphql/resolvers/photo.go +++ b/api/graphql/resolvers/photo.go @@ -146,7 +146,7 @@ func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.A panic("not implemented") } -func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoExif, error) { - log.Println("Photo: EXIF not implemented") - return nil, nil +func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoEXIF, error) { + row := r.Database.QueryRow("SELECT photo_exif.* FROM photo NATURAL JOIN photo_exif WHERE photo.photo_id = ?", obj.PhotoID) + return models.NewPhotoExifFromRow(row) } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index e3b88f4d..f73236cb 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -202,6 +202,7 @@ type Photo { "EXIF metadata from the camera" type PhotoEXIF { + id: Int! photo: Photo "The model name of the camera" camera: String @@ -210,8 +211,6 @@ type PhotoEXIF { "The name of the lens" lens: String dateShot: Time - "The formatted filesize of the image" - fileSize: String "The exposure time of the image" exposure: String "The aperature stops of the image" @@ -219,7 +218,9 @@ type PhotoEXIF { "The ISO setting of the image" iso: Int "The focal length of the lens, when the image was taken" - focalLength: String + focalLength: Float "A formatted description of the flash settings, when the image was taken" flash: String + "An index describing the mode for adjusting the exposure of the image" + exposureProgram: Int } diff --git a/api/scanner/exif.go b/api/scanner/exif.go new file mode 100644 index 00000000..9e852757 --- /dev/null +++ b/api/scanner/exif.go @@ -0,0 +1,248 @@ +package scanner + +import ( + "database/sql" + "errors" + "log" + "math/big" + "os" + + "github.com/viktorstrate/photoview/api/graphql/models" + "github.com/xor-gate/goexif2/exif" + "github.com/xor-gate/goexif2/mknote" +) + +func ScanEXIF(tx *sql.Tx, photo *models.Photo) (*models.PhotoEXIF, error) { + + log.Printf("Scanning for EXIF") + + { + // Check if EXIF data already exists + if photo.ExifId != nil { + row := tx.QueryRow("SELECT * FROM photo_exif WHERE exif_id = ?", photo.ExifId) + return models.NewPhotoExifFromRow(row) + } + + row := tx.QueryRow("SELECT photo_exif.* FROM photo, photo_exif WHERE photo.exif_id = photo_exif.exif_id AND photo.photo_id = ?", photo.PhotoID) + exifData, err := models.NewPhotoExifFromRow(row) + if err != nil && err != sql.ErrNoRows { + return nil, err + } else if exifData != nil { + return exifData, nil + } + } + + photoFile, err := os.Open(photo.Path) + if err != nil { + return nil, err + } + + exif.RegisterParsers(mknote.All...) + + exifTags, err := exif.Decode(photoFile) + if err != nil { + return nil, err + } + + // log.Printf("EXIF DATA FOR %s\n%s\n", photo.Title, exifTags.String()) + + valueNames := make([]string, 0) + exifValues := make([]interface{}, 0) + + model, err := readStringTag(exifTags, exif.Model, photo) + if err == nil { + valueNames = append(valueNames, "camera") + exifValues = append(exifValues, model) + } + + maker, err := readStringTag(exifTags, exif.Make, photo) + if err == nil { + valueNames = append(valueNames, "maker") + exifValues = append(exifValues, maker) + } + + lens, err := readStringTag(exifTags, exif.LensModel, photo) + if err == nil { + valueNames = append(valueNames, "lens") + exifValues = append(exifValues, lens) + } + + date, err := exifTags.DateTime() + if err == nil { + valueNames = append(valueNames, "dateShot") + exifValues = append(exifValues, date) + } + + exposure, err := readRationalTag(exifTags, exif.ExposureTime, photo) + if err == nil { + valueNames = append(valueNames, "exposure") + exifValues = append(exifValues, exposure.RatString()) + } + + apertureRat, err := readRationalTag(exifTags, exif.FNumber, photo) + if err == nil { + aperture, _ := apertureRat.Float32() + valueNames = append(valueNames, "aperture") + exifValues = append(exifValues, aperture) + } + + isoTag, err := exifTags.Get(exif.ISOSpeedRatings) + if err != nil { + log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %s\n", photo.Title) + } else { + iso, err := isoTag.Int(0) + if err != nil { + log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %s\n", photo.Title) + } else { + valueNames = append(valueNames, "iso") + exifValues = append(exifValues, iso) + } + } + + focalLengthRat, err := readRationalTag(exifTags, exif.FocalLength, photo) + if err == nil { + focalLength, _ := focalLengthRat.Float32() + valueNames = append(valueNames, "focal_length") + exifValues = append(exifValues, focalLength) + } else { + // For some photos, the focal length cannot be read as a rational value, + // but is instead the second value read as an integer + tag, err := exifTags.Get(exif.FocalLength) + if err == nil { + focalLength, err := tag.Int(1) + if err != nil { + log.Printf("WARN: Could not parse EXIF FocalLength as integer: %s\n%s\n", photo.Title, err) + } else { + valueNames = append(valueNames, "focal_length") + exifValues = append(exifValues, focalLength) + } + } + } + + flash, err := exifTags.Flash() + if err == nil { + valueNames = append(valueNames, "flash") + exifValues = append(exifValues, flash) + } + + orientation, err := readIntegerTag(exifTags, exif.Orientation, photo) + if err == nil { + valueNames = append(valueNames, "orientation") + exifValues = append(exifValues, *orientation) + } + + exposureProgram, err := readIntegerTag(exifTags, exif.ExposureProgram, photo) + if err == nil { + valueNames = append(valueNames, "exposure_program") + exifValues = append(exifValues, *exposureProgram) + } + + if len(valueNames) == 0 { + return nil, nil + } + + prepareQuestions := "" + for range valueNames { + prepareQuestions += "?," + } + prepareQuestions = prepareQuestions[0 : len(prepareQuestions)-1] + + columns := "" + for _, name := range valueNames { + columns += name + "," + } + columns = columns[0 : len(columns)-1] + + // Insert into database + result, err := tx.Exec("INSERT INTO photo_exif ("+columns+") VALUES ("+prepareQuestions+")", exifValues...) + if err != nil { + return nil, err + } + + exifID, err := result.LastInsertId() + if err != nil { + return nil, err + } + + // Link exif to photo in database + result, err = tx.Exec("UPDATE photo SET exif_id = ? WHERE photo_id = ?", exifID, photo.PhotoID) + if err != nil { + return nil, err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, err + } + + if rowsAffected == 0 { + return nil, errors.New("Linking exif to photo in database failed: 0 rows affected") + } + + // Return newly created exif row + row := tx.QueryRow("SELECT * FROM photo_exif WHERE exif_id = ?", exifID) + return models.NewPhotoExifFromRow(row) +} + +func readStringTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*string, error) { + tag, err := tags.Get(name) + if err != nil { + log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title) + return nil, err + } + + if tag != nil { + value, err := tag.StringVal() + if err != nil { + log.Printf("WARN: Could not parse %s from EXIF as string: %s\n", name, photo.Title) + return nil, err + } + + return &value, nil + } + + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title) + return nil, errors.New("exif tag returned null") +} + +func readRationalTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*big.Rat, error) { + tag, err := tags.Get(name) + if err != nil { + log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title) + return nil, err + } + + if tag != nil { + value, err := tag.Rat(0) + if err != nil { + log.Printf("WARN: Could not parse %s from EXIF as rational: %s\n%s\n", name, photo.Title, err) + return nil, err + } + + return value, nil + } + + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title) + return nil, errors.New("exif tag returned null") +} + +func readIntegerTag(tags *exif.Exif, name exif.FieldName, photo *models.Photo) (*int, error) { + tag, err := tags.Get(name) + if err != nil { + log.Printf("WARN: Could not read %s from EXIF: %s\n", name, photo.Title) + return nil, err + } + + if tag != nil { + value, err := tag.Int(0) + if err != nil { + log.Printf("WARN: Could not parse %s from EXIF as integer: %s\n%s\n", name, photo.Title, err) + return nil, err + } + + return &value, nil + } + + log.Printf("WARN: EXIF tag %s returned null: %s\n", name, photo.Title) + return nil, errors.New("exif tag returned null") +} diff --git a/api/scanner/photo_scanner.go b/api/scanner/photo_scanner.go index 078f0071..27d7260a 100644 --- a/api/scanner/photo_scanner.go +++ b/api/scanner/photo_scanner.go @@ -41,6 +41,12 @@ func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string) return err } + _, err = ScanEXIF(tx, photo) + if err != nil { + log.Printf("ERROR: ScanEXIF for %s: %s\n", photoName, err) + return err + } + if err := ProcessPhoto(tx, photo, content_type); err != nil { return err } diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go index fb7acd2c..61399284 100644 --- a/api/scanner/process_photo.go +++ b/api/scanner/process_photo.go @@ -49,7 +49,7 @@ func ProcessPhoto(tx *sql.Tx, photo *models.Photo, content_type *string) error { log.Printf("Processing photo: %s\n", photo.Path) - imageData := processImageData{ + imageData := ProcessImageData{ photoPath: photo.Path, } @@ -249,13 +249,13 @@ func encodeImageJPEG(photoPath string, photoImage image.Image, jpegOptions *jpeg return nil } -type processImageData struct { +type ProcessImageData struct { photoPath string _photoImage image.Image _thumbnailImage image.Image } -func (img *processImageData) PhotoImage() (image.Image, error) { +func (img *ProcessImageData) PhotoImage() (image.Image, error) { if img._photoImage != nil { return img._photoImage, nil } @@ -276,7 +276,7 @@ func (img *processImageData) PhotoImage() (image.Image, error) { return img._photoImage, nil } -func (img *processImageData) ThumbnailImage() (image.Image, error) { +func (img *ProcessImageData) ThumbnailImage() (image.Image, error) { photoImage, err := img.PhotoImage() if err != nil { return nil, err