From 1f9f6ebf9aa21bd2d37ebff07eb5a517928be4aa Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Sat, 20 Mar 2021 00:26:05 +0100 Subject: [PATCH] Use exiftool for parsing exif data Use exiftool to parse exif metadata. Signed-off-by: Kjeldgaard --- Dockerfile | 2 +- api/go.mod | 1 + api/go.sum | 2 + api/graphql/generated.go | 27 ++- api/graphql/models/media_exif.go | 6 +- api/graphql/schema.graphql | 4 +- api/scanner/exif/exif.go | 2 +- api/scanner/exif/exif_parser_external.go | 206 ++++++++++++++++++++++ api/scanner/exif/exif_parser_internal.go | 188 -------------------- ui/src/components/sidebar/MediaSidebar.js | 14 +- 10 files changed, 238 insertions(+), 214 deletions(-) create mode 100644 api/scanner/exif/exif_parser_external.go delete mode 100644 api/scanner/exif/exif_parser_internal.go diff --git a/Dockerfile b/Dockerfile index cb0d2a58..2d6c3f85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,7 +73,7 @@ COPY api/data /app/data RUN apt-get update \ # Required dependencies - && apt-get install -y curl gpg libdlib19 ffmpeg + && apt-get install -y curl gpg libdlib19 ffmpeg exiftool # Install Darktable if building for a supported architecture RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ] || [ "${TARGETPLATFORM}" = "linux/arm64" ]; then \ diff --git a/api/go.mod b/api/go.mod index 6aa9063b..fb8a77b3 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/99designs/gqlgen v0.13.0 github.com/Kagami/go-face v0.0.0-20200825065730-3dd2d74dccfb + github.com/barasher/go-exiftool v1.3.2 github.com/disintegration/imaging v1.6.2 github.com/go-sql-driver/mysql v1.5.0 github.com/gorilla/handlers v1.5.1 diff --git a/api/go.sum b/api/go.sum index 6bc5e982..a103eead 100644 --- a/api/go.sum +++ b/api/go.sum @@ -11,6 +11,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/barasher/go-exiftool v1.3.2 h1:yWUIGOsM6PLbbHxe84ASTo/eyORMTyMH/5Qv1yBcC7s= +github.com/barasher/go-exiftool v1.3.2/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 0094165e..4a771fa5 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1890,8 +1890,8 @@ type MediaEXIF { 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 + "A formated description of the mode for adjusting the exposure of the image" + exposureProgram: String } type VideoMetadata { @@ -4520,9 +4520,9 @@ func (ec *executionContext) _MediaEXIF_iso(ctx context.Context, field graphql.Co if resTmp == nil { return graphql.Null } - res := resTmp.(*int) + res := resTmp.(*int64) fc.Result = res - return ec.marshalOInt2ᚖint(ctx, field.Selections, res) + return ec.marshalOInt2ᚖint64(ctx, field.Selections, res) } func (ec *executionContext) _MediaEXIF_focalLength(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { @@ -4616,9 +4616,9 @@ func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, fiel if resTmp == nil { return graphql.Null } - res := resTmp.(*int) + res := resTmp.(*string) fc.Result = res - return ec.marshalOInt2ᚖint(ctx, field.Selections, res) + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { @@ -11788,6 +11788,21 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele return graphql.MarshalInt(*v) } +func (ec *executionContext) unmarshalOInt2ᚖint64(ctx context.Context, v interface{}) (*int64, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt64(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint64(ctx context.Context, sel ast.SelectionSet, v *int64) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return graphql.MarshalInt64(*v) +} + func (ec *executionContext) marshalOMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v *models.Media) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go index 38aaba9d..f86d552a 100644 --- a/api/graphql/models/media_exif.go +++ b/api/graphql/models/media_exif.go @@ -12,11 +12,11 @@ type MediaEXIF struct { DateShot *time.Time Exposure *string Aperture *float64 - Iso *int + Iso *int64 FocalLength *float64 Flash *string - Orientation *int - ExposureProgram *int + Orientation *int64 + ExposureProgram *string GPSLatitude *float64 GPSLongitude *float64 } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 15042adf..de7eb2f0 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -313,8 +313,8 @@ type MediaEXIF { 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 + "A formated description of the mode for adjusting the exposure of the image" + exposureProgram: String } type VideoMetadata { diff --git a/api/scanner/exif/exif.go b/api/scanner/exif/exif.go index 1e3bf75e..903f6e4f 100644 --- a/api/scanner/exif/exif.go +++ b/api/scanner/exif/exif.go @@ -31,7 +31,7 @@ func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) { } } - var parser exifParser = &internalExifParser{} + var parser exifParser = &externalExifParser{} exif, err := parser.ParseExif(media) if err != nil { diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go new file mode 100644 index 00000000..226924fb --- /dev/null +++ b/api/scanner/exif/exif_parser_external.go @@ -0,0 +1,206 @@ +package exif + +import ( + "log" + "regexp" + "strconv" + "time" + + "github.com/barasher/go-exiftool" + "github.com/photoview/photoview/api/graphql/models" +) + +type externalExifParser struct{} + +func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { + // Init ExifTool + et, err := exiftool.NewExiftool() + if err != nil { + log.Printf("Error initializing ExifTool: %s\n", err) + return nil, err + } + defer et.Close() + + fileInfos := et.ExtractMetadata(media.Path) + newExif := models.MediaEXIF{} + + for _, fileInfo := range fileInfos { + if fileInfo.Err != nil { + log.Printf("Fileinfo error\n") + continue + } + + // Get camera model + model, err := fileInfo.GetString("Model") + if err == nil { + log.Printf("Camera model: %v", model) + newExif.Camera = &model + } + + // Get Camera make + make, err := fileInfo.GetString("Make") + if err == nil { + log.Printf("Camera make: %v", make) + newExif.Maker = &make + } + + // Get lens + lens, err := fileInfo.GetString("LensModel") + if err == nil { + log.Printf("Lens: %v", lens) + newExif.Lens = &lens + } + + //Get time of photo + date, err := fileInfo.GetString("DateTimeOriginal") + if err == nil { + log.Printf("Date shot: %s", date) + layout := "2006:01:02 15:04:05" + dateTime, err := time.Parse(layout, date) + if err == nil { + newExif.DateShot = &dateTime + } + + } + + // Get exposure time + exposureTime, err := fileInfo.GetString("ExposureTime") + if err == nil { + log.Printf("Exposure time: %s", exposureTime) + newExif.Exposure = &exposureTime + } + + // Get aperture + aperture, err := fileInfo.GetFloat("Aperture") + if err == nil { + log.Printf("Aperture: %f", aperture) + newExif.Aperture = &aperture + } + + // Get ISO + iso, err := fileInfo.GetInt("ISO") + if err == nil { + log.Printf("ISO: %d", iso) + newExif.Iso = &iso + } + + // Get focal length + focalLen, err := fileInfo.GetString("FocalLength") + if err == nil { + log.Printf("Focal length: %s", focalLen) + reg, err := regexp.Compile("[0-9.]+") + focalLenStr := reg.FindString(focalLen) + focalLenFloat, err := strconv.ParseFloat(focalLenStr, 64) + if err == nil { + newExif.FocalLength = &focalLenFloat + } + } + + // Get flash info + flash, err := fileInfo.GetString("Flash") + if err == nil { + log.Printf("Flash: %s", flash) + newExif.Flash = &flash + } + + // Get orientation + orientation, err := fileInfo.GetString("Orientation") + log.Printf("Orientation: %s", orientation) + if err == nil { + if orientation == "Horizontal (normal)" { + var orientationInt int64 = 0 + newExif.Orientation = &orientationInt + } else { + reg, err := regexp.Compile("CCW") + if err == nil { + if reg.MatchString(orientation) { + //Counter clockwise + reg, err := regexp.Compile("[0-9.]+") + orientationStr := reg.FindString(orientation) + orientationInt, err := strconv.ParseInt(orientationStr, 10, 64) + if err == nil { + log.Printf("Orientation: %d", orientationInt) + newExif.Orientation = &orientationInt + } + } else { + // Clockwise + reg, err := regexp.Compile("[0-9.]+") + orientationStr := reg.FindString(orientation) + orientationInt, err := strconv.ParseInt(orientationStr, 10, 64) + if err == nil { + orientationInt = orientationInt * -1 + log.Printf("Orientation: %d", orientationInt) + newExif.Orientation = &orientationInt + } + } + } + } + } + + // Get exposure program + expProgram, err := fileInfo.GetString("ExposureProgram") + if err == nil { + log.Printf("Exposure Program: %s", expProgram) + newExif.ExposureProgram = &expProgram + } + + // GPS coordinates - longitude + longitudeRaw, err := fileInfo.GetString("GPSLongitude") + if err == nil { + log.Printf("GPS longitude: %s", longitudeRaw) + value, err := ConvertCoodinateToFloat(longitudeRaw) + if err == nil { + newExif.GPSLongitude = &value + } + } + + // GPS coordinates - latitude + latitudeRaw, err := fileInfo.GetString("GPSLatitude") + if err == nil { + log.Printf("GPS latitude: %s", latitudeRaw) + value, err := ConvertCoodinateToFloat(latitudeRaw) + if err == nil { + newExif.GPSLatitude = &value + } + } + } + + returnExif = &newExif + return +} + +func ConvertCoodinateToFloat(coordinate string) (value float64, err error) { + reg, err := regexp.Compile("[0-9.]+") + if err != nil { + return 0, err + } + + coordinateStr := reg.FindAllString(coordinate, -1) + log.Printf("GPS: %s length: %d\n", coordinateStr, len(coordinateStr)) + if len(coordinateStr) != 3 { + return 0, err + } + + deg, err := strconv.ParseFloat(coordinateStr[0], 64) + if err != nil { + return 0, err + } + + minute, err := strconv.ParseFloat(coordinateStr[1], 64) + if err != nil { + return 0, err + } + + second, err := strconv.ParseFloat(coordinateStr[2], 64) + if err != nil { + return 0, err + } + + var multiplier float64 = 1 + if deg < 0 { + multiplier = -1 + } + + value = (deg + minute / 60 + second / 3600) * multiplier + return value, nil +} diff --git a/api/scanner/exif/exif_parser_internal.go b/api/scanner/exif/exif_parser_internal.go deleted file mode 100644 index b4bf6a37..00000000 --- a/api/scanner/exif/exif_parser_internal.go +++ /dev/null @@ -1,188 +0,0 @@ -package exif - -import ( - "fmt" - "log" - "math/big" - "os" - - "github.com/photoview/photoview/api/graphql/models" - "github.com/pkg/errors" - "github.com/xor-gate/goexif2/exif" - "github.com/xor-gate/goexif2/mknote" -) - -// internalExifParser is an exif parser that parses the media without the use of external tools -type internalExifParser struct{} - -func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { - photoFile, err := os.Open(media.Path) - if err != nil { - return nil, err - } - - exif.RegisterParsers(mknote.All...) - - // Recover if exif.Decode panics - defer func() { - if err := recover(); err != nil { - log.Printf("Recovered from panic: Exif decoding: %s\n", err) - returnErr = errors.New(fmt.Sprintf("Exif decoding panicked: %s\n", err)) - } - }() - - exifTags, err := exif.Decode(photoFile) - if err != nil { - return nil, errors.Wrap(err, "Could not decode EXIF") - } - - newExif := models.MediaEXIF{} - - model, err := p.readStringTag(exifTags, exif.Model, media) - if err == nil { - newExif.Camera = model - } - - maker, err := p.readStringTag(exifTags, exif.Make, media) - if err == nil { - newExif.Maker = maker - } - - lens, err := p.readStringTag(exifTags, exif.LensModel, media) - if err == nil { - newExif.Lens = lens - } - - date, err := exifTags.DateTime() - if err == nil { - newExif.DateShot = &date - } - - exposure, err := p.readRationalTag(exifTags, exif.ExposureTime, media) - if err == nil { - exposureStr := exposure.RatString() - newExif.Exposure = &exposureStr - } - - apertureRat, err := p.readRationalTag(exifTags, exif.FNumber, media) - if err == nil { - aperture, _ := apertureRat.Float64() - newExif.Aperture = &aperture - } - - isoTag, err := exifTags.Get(exif.ISOSpeedRatings) - if err != nil { - log.Printf("WARN: Could not read ISOSpeedRatings from EXIF: %s\n", media.Title) - } else { - iso, err := isoTag.Int(0) - if err != nil { - log.Printf("WARN: Could not parse EXIF ISOSpeedRatings as integer: %s\n", media.Title) - } else { - newExif.Iso = &iso - } - } - - focalLengthTag, err := exifTags.Get(exif.FocalLength) - if err == nil { - focalLengthRat, err := focalLengthTag.Rat(0) - if err == nil { - focalLength, _ := focalLengthRat.Float64() - newExif.FocalLength = &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 - - if err == nil { - focalLength, err := focalLengthTag.Int(1) - if err != nil { - log.Printf("WARN: Could not parse EXIF FocalLength as rational or integer: %s\n%s\n", media.Title, err) - } else { - focalLenFloat := float64(focalLength) - newExif.FocalLength = &focalLenFloat - } - } - } - } - - flash, err := exifTags.Flash() - if err == nil { - newExif.Flash = &flash - } - - orientation, err := p.readIntegerTag(exifTags, exif.Orientation, media) - if err == nil { - newExif.Orientation = orientation - } - - exposureProgram, err := p.readIntegerTag(exifTags, exif.ExposureProgram, media) - if err == nil { - newExif.ExposureProgram = exposureProgram - } - - lat, long, err := exifTags.LatLong() - if err == nil { - newExif.GPSLatitude = &lat - newExif.GPSLongitude = &long - } - - returnExif = &newExif - return -} - -func (p *internalExifParser) readStringTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*string, error) { - tag, err := tags.Get(name) - if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) - } - - if tag != nil { - value, err := tag.StringVal() - if err != nil { - return nil, errors.Wrapf(err, "could not parse %s from EXIF as string: %s", name, media.Title) - } - - return &value, nil - } - - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) - return nil, errors.New("exif tag returned null") -} - -func (p *internalExifParser) readRationalTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*big.Rat, error) { - tag, err := tags.Get(name) - if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) - } - - if tag != nil { - value, err := tag.Rat(0) - if err != nil { - return nil, errors.Wrapf(err, "could not parse %s from EXIF as rational: %s", name, media.Title) - } - - return value, nil - } - - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) - return nil, errors.New("exif tag returned null") -} - -func (p *internalExifParser) readIntegerTag(tags *exif.Exif, name exif.FieldName, media *models.Media) (*int, error) { - tag, err := tags.Get(name) - if err != nil { - return nil, errors.Wrapf(err, "could not read %s from EXIF: %s", name, media.Title) - } - - if tag != nil { - value, err := tag.Int(0) - if err != nil { - return nil, errors.Wrapf(err, "Could not parse %s from EXIF as integer: %s", name, media.Title) - } - - return &value, nil - } - - log.Printf("WARN: EXIF tag %s returned null: %s\n", name, media.Title) - return nil, errors.New("exif tag returned null") -} diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 906d29b5..9548eaa8 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -134,18 +134,6 @@ const exifNameLookup = { flash: 'Flash', } -const exposurePrograms = { - 0: 'Not defined', - 1: 'Manual', - 2: 'Normal program', - 3: 'Aperture priority', - 4: 'Shutter priority', - 5: 'Creative program', - 6: 'Action program', - 7: 'Portrait mode', - 8: 'Landscape mode ', -} - const SidebarContent = ({ media, hidePreview }) => { let exifItems = [] @@ -164,7 +152,7 @@ const SidebarContent = ({ media, hidePreview }) => { exif.dateShot = new Date(exif.dateShot).toLocaleString() if (exif.exposureProgram) { - exif.exposureProgram = exposurePrograms[exif.exposureProgram] + exif.exposureProgram = `${exif.exposureProgram}` } if (exif.aperture) {