From 1f9f6ebf9aa21bd2d37ebff07eb5a517928be4aa Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Sat, 20 Mar 2021 00:26:05 +0100 Subject: [PATCH 1/6] 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 cb0d2a5..2d6c3f8 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 6aa9063..fb8a77b 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 6bc5e98..a103eea 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 0094165..4a771fa 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 38aaba9..f86d552 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 15042ad..de7eb2f 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 1e3bf75..903f6e4 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 0000000..226924f --- /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 b4bf6a3..0000000 --- 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 906d29b..9548eaa 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) { From 7b5305579b0302bbf4a06e55069d685460b200ec Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Tue, 23 Mar 2021 21:36:03 +0100 Subject: [PATCH 2/6] Extract metadata as integers Extract all metada as integers fomr exiftool. Signed-off-by: Kjeldgaard --- api/go.mod | 3 +- api/go.sum | 2 + api/graphql/generated.go | 8 +- api/graphql/models/media_exif.go | 2 +- api/graphql/schema.graphql | 4 +- api/scanner/exif/exif.go | 12 +- api/scanner/exif/exif_parser_external.go | 51 ++---- api/scanner/exif/exif_parser_internal.go | 191 ++++++++++++++++++++++ ui/src/components/sidebar/MediaSidebar.js | 16 +- 9 files changed, 242 insertions(+), 47 deletions(-) create mode 100644 api/scanner/exif/exif_parser_internal.go diff --git a/api/go.mod b/api/go.mod index fb8a77b..7d5ebbe 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,7 +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/barasher/go-exiftool v1.3.2 // indirect github.com/disintegration/imaging v1.6.2 github.com/go-sql-driver/mysql v1.5.0 github.com/gorilla/handlers v1.5.1 @@ -13,6 +13,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/h2non/filetype v1.1.1 github.com/joho/godotenv v1.3.0 + github.com/kjeldgaard/go-exiftool v1.3.3 github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f github.com/vektah/gqlparser/v2 v2.1.0 diff --git a/api/go.sum b/api/go.sum index a103eea..f17d24d 100644 --- a/api/go.sum +++ b/api/go.sum @@ -117,6 +117,8 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kjeldgaard/go-exiftool v1.3.3 h1:tDlkcHtTuduu7UgopqGS/ooh1tmEgtIhlBjmTXzjxbg= +github.com/kjeldgaard/go-exiftool v1.3.3/go.mod h1:Nmi1VkXBbr48Kq5ZJjo29RD+a3PdWVGlL+8bSssxFbU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 4a771fa..3f937ad 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 - "A formated description of the mode for adjusting the exposure of the image" - exposureProgram: String + "An index describing the mode for adjusting the exposure of the image" + exposureProgram: Int } type VideoMetadata { @@ -4616,9 +4616,9 @@ func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, fiel if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*int64) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalOInt2ᚖint64(ctx, field.Selections, res) } func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) { diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go index f86d552..3b15fe5 100644 --- a/api/graphql/models/media_exif.go +++ b/api/graphql/models/media_exif.go @@ -16,7 +16,7 @@ type MediaEXIF struct { FocalLength *float64 Flash *string Orientation *int64 - ExposureProgram *string + ExposureProgram *int64 GPSLatitude *float64 GPSLongitude *float64 } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index de7eb2f..15042ad 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 - "A formated description of the mode for adjusting the exposure of the image" - exposureProgram: String + "An index describing the mode for adjusting the exposure of the image" + exposureProgram: Int } type VideoMetadata { diff --git a/api/scanner/exif/exif.go b/api/scanner/exif/exif.go index 903f6e4..be2a026 100644 --- a/api/scanner/exif/exif.go +++ b/api/scanner/exif/exif.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "gorm.io/gorm" + "github.com/kjeldgaard/go-exiftool" "github.com/photoview/photoview/api/graphql/models" ) @@ -31,7 +32,16 @@ func SaveEXIF(tx *gorm.DB, media *models.Media) (*models.MediaEXIF, error) { } } - var parser exifParser = &externalExifParser{} + // Decide between internal or external Exif parser + et, err := exiftool.NewExiftool() + et.Close() + var parser exifParser + if err != nil { + parser = &internalExifParser{} + } else { + parser = &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 index 226924f..3111171 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -6,7 +6,7 @@ import ( "strconv" "time" - "github.com/barasher/go-exiftool" + "github.com/kjeldgaard/go-exiftool" "github.com/photoview/photoview/api/graphql/models" ) @@ -14,7 +14,10 @@ type externalExifParser struct{} func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { // Init ExifTool - et, err := exiftool.NewExiftool() + + et2 := exiftool.Charset("-n") + + et, err := .NewExiftool(et2) if err != nil { log.Printf("Error initializing ExifTool: %s\n", err) return nil, err @@ -60,7 +63,6 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. if err == nil { newExif.DateShot = &dateTime } - } // Get exposure time @@ -88,7 +90,7 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. focalLen, err := fileInfo.GetString("FocalLength") if err == nil { log.Printf("Focal length: %s", focalLen) - reg, err := regexp.Compile("[0-9.]+") + reg, _ := regexp.Compile("[0-9.]+") focalLenStr := reg.FindString(focalLen) focalLenFloat, err := strconv.ParseFloat(focalLenStr, 64) if err == nil { @@ -104,44 +106,19 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. } // Get orientation - orientation, err := fileInfo.GetString("Orientation") - log.Printf("Orientation: %s", orientation) + orientation, err := fileInfo.GetInt("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 - } - } - } - } + log.Printf("Orientation: %d", orientation) + newExif.Orientation = &orientation } // Get exposure program - expProgram, err := fileInfo.GetString("ExposureProgram") + expProgram, err := fileInfo.GetStrings("ExposureProgram") if err == nil { - log.Printf("Exposure Program: %s", expProgram) - newExif.ExposureProgram = &expProgram + for _, value := range expProgram { + log.Printf("%s", value) + } + //log.Printf("Exposure Program: %d", expProgram) } // GPS coordinates - longitude diff --git a/api/scanner/exif/exif_parser_internal.go b/api/scanner/exif/exif_parser_internal.go new file mode 100644 index 0000000..654ec5f --- /dev/null +++ b/api/scanner/exif/exif_parser_internal.go @@ -0,0 +1,191 @@ +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 { + iso64 := int64(iso) + newExif.Iso = &iso64 + } + } + + 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 { + orientation64 := int64(*orientation) + newExif.Orientation = &orientation64 + } + + exposureProgram, err := p.readIntegerTag(exifTags, exif.ExposureProgram, media) + if err == nil { + exposureProgram64 := int64(*exposureProgram) + newExif.ExposureProgram = &exposureProgram64 + } + + 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 9548eaa..321569b 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -134,6 +134,20 @@ const exifNameLookup = { flash: 'Flash', } +// From https://exiftool.org/TagNames/EXIF.html +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 ', + 9: 'Bulb', +} + const SidebarContent = ({ media, hidePreview }) => { let exifItems = [] @@ -152,7 +166,7 @@ const SidebarContent = ({ media, hidePreview }) => { exif.dateShot = new Date(exif.dateShot).toLocaleString() if (exif.exposureProgram) { - exif.exposureProgram = `${exif.exposureProgram}` + exif.exposureProgram = exposurePrograms[exif.exposureProgram] } if (exif.aperture) { From 1193222f92d8097d8f463f0ca298918064650449 Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Sun, 28 Mar 2021 23:52:51 +0200 Subject: [PATCH 3/6] Update external exif parser method Exiftool meta data parsed as intergers and not strings. Keep internal exif parsing functionality. Signed-off-by: Kjeldgaard --- api/graphql/generated.go | 6 +- api/graphql/models/media_exif.go | 2 +- api/graphql/schema.graphql | 2 +- api/scanner/exif/exif_parser_external.go | 72 ++++------------------- api/scanner/exif/exif_parser_internal.go | 5 +- ui/src/components/sidebar/MediaSidebar.js | 51 +++++++++++++++- 6 files changed, 71 insertions(+), 67 deletions(-) diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 3f937ad..e452da2 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1889,7 +1889,7 @@ type MediaEXIF { "The focal length of the lens, when the image was taken" focalLength: Float "A formatted description of the flash settings, when the image was taken" - flash: String + flash: Int "An index describing the mode for adjusting the exposure of the image" exposureProgram: Int } @@ -4584,9 +4584,9 @@ func (ec *executionContext) _MediaEXIF_flash(ctx context.Context, field graphql. if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*int64) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalOInt2ᚖint64(ctx, field.Selections, res) } func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go index 3b15fe5..6b75ab6 100644 --- a/api/graphql/models/media_exif.go +++ b/api/graphql/models/media_exif.go @@ -14,7 +14,7 @@ type MediaEXIF struct { Aperture *float64 Iso *int64 FocalLength *float64 - Flash *string + Flash *int64 Orientation *int64 ExposureProgram *int64 GPSLatitude *float64 diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 15042ad..ff91f54 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -312,7 +312,7 @@ type MediaEXIF { "The focal length of the lens, when the image was taken" focalLength: Float "A formatted description of the flash settings, when the image was taken" - flash: String + flash: Int "An index describing the mode for adjusting the exposure of the image" exposureProgram: Int } diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index 3111171..5378cd8 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -14,10 +14,8 @@ type externalExifParser struct{} func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models.MediaEXIF, returnErr error) { // Init ExifTool - - et2 := exiftool.Charset("-n") - - et, err := .NewExiftool(et2) + extraInitArgs := []string{"-n"} + et, err := exiftool.NewExiftool(exiftool.AddInitArgs(extraInitArgs)) if err != nil { log.Printf("Error initializing ExifTool: %s\n", err) return nil, err @@ -99,9 +97,9 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. } // Get flash info - flash, err := fileInfo.GetString("Flash") + flash, err := fileInfo.GetInt("Flash") if err == nil { - log.Printf("Flash: %s", flash) + log.Printf("Flash: %d", flash) newExif.Flash = &flash } @@ -113,71 +111,27 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. } // Get exposure program - expProgram, err := fileInfo.GetStrings("ExposureProgram") + expProgram, err := fileInfo.GetInt("ExposureProgram") if err == nil { - for _, value := range expProgram { - log.Printf("%s", value) - } - //log.Printf("Exposure Program: %d", expProgram) + log.Printf("Exposure Program: %d", expProgram) + newExif.ExposureProgram = &expProgram } // GPS coordinates - longitude - longitudeRaw, err := fileInfo.GetString("GPSLongitude") + longitudeRaw, err := fileInfo.GetFloat("GPSLongitude") if err == nil { - log.Printf("GPS longitude: %s", longitudeRaw) - value, err := ConvertCoodinateToFloat(longitudeRaw) - if err == nil { - newExif.GPSLongitude = &value - } + log.Printf("GPS longitude: %f", longitudeRaw) + newExif.GPSLongitude = &longitudeRaw } // GPS coordinates - latitude - latitudeRaw, err := fileInfo.GetString("GPSLatitude") + latitudeRaw, err := fileInfo.GetFloat("GPSLatitude") if err == nil { - log.Printf("GPS latitude: %s", latitudeRaw) - value, err := ConvertCoodinateToFloat(latitudeRaw) - if err == nil { - newExif.GPSLatitude = &value - } + log.Printf("GPS latitude: %f", latitudeRaw) + newExif.GPSLatitude = &latitudeRaw } } 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 index 654ec5f..baacf57 100644 --- a/api/scanner/exif/exif_parser_internal.go +++ b/api/scanner/exif/exif_parser_internal.go @@ -106,9 +106,10 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. } } - flash, err := exifTags.Flash() + flash, err := p.readIntegerTag(exifTags, exif.Flash, media) if err == nil { - newExif.Flash = &flash + flash64 := int64(*flash) + newExif.Flash = &flash64 } orientation, err := p.readIntegerTag(exifTags, exif.Orientation, media) diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 321569b..7914391 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -148,6 +148,49 @@ const exposurePrograms = { 9: 'Bulb', } +// From https://exiftool.org/TagNames/EXIF.html#Flash +const flash = { + 0x0: 'No Flash', + 0x1: 'Fired', + 0x5: 'Fired, Return not detected', + 0x7: 'Fired, Return detected', + 0x8: 'On, Did not fire', + 0x9: 'On, Fired', + 0xd: 'On, Return not detected', + 0xf: 'On, Return detected', + 0x10: 'Off, Did not fire', + 0x14: 'Off, Did not fire, Return not detected', + 0x18: 'Auto, Did not fire', + 0x19: 'Auto, Fired', + 0x1d: 'Auto, Fired, Return not detected', + 0x1f: 'Auto, Fired, Return detected', + 0x20: 'No flash function', + 0x30: 'Off, No flash function', + 0x41: 'Fired, Red-eye reduction', + 0x45: 'Fired, Red-eye reduction, Return not detected', + 0x47: 'Fired, Red-eye reduction, Return detected', + 0x49: 'On, Red-eye reduction', + 0x4d: 'On, Red-eye reduction, Return not detected', + 0x4f: 'On, Red-eye reduction, Return detected', + 0x50: 'Off, Red-eye reduction', + 0x58: 'Auto, Did not fire, Red-eye reduction', + 0x59: 'Auto, Fired, Red-eye reduction', + 0x5d: 'Auto, Fired, Red-eye reduction, Return not detected', + 0x5f: 'Auto, Fired, Red-eye reduction, Return detected', +} + +// From https://exiftool.org/TagNames/EXIF.html +const orientation = { + 1: 'Horizontal (normal)', + 2: 'Mirror horizontal', + 3: 'Rotate 180', + 4: 'Mirror vertical', + 5: 'Mirror horizontal and rotate 270 CW', + 6: 'Rotate 90 CW', + 7: 'Mirror horizontal and rotate 90 CW', + 8: 'Rotate 270 CW', +} + const SidebarContent = ({ media, hidePreview }) => { let exifItems = [] @@ -165,7 +208,9 @@ const SidebarContent = ({ media, hidePreview }) => { ) exif.dateShot = new Date(exif.dateShot).toLocaleString() - if (exif.exposureProgram) { + + if (exposurePrograms.hasOwnProperty(exif.exposureProgram)) + { exif.exposureProgram = exposurePrograms[exif.exposureProgram] } @@ -177,6 +222,10 @@ const SidebarContent = ({ media, hidePreview }) => { exif.focalLength = `${exif.focalLength}mm` } + if (flash.hasOwnProperty(exif.flash)) { + exif.flash = flash[exif.flash] + } + exifItems = exifKeys.map(key => ( )) From da664c77663bbc20dfea4abb293c91eaf01a783d Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Mon, 29 Mar 2021 11:57:57 +0200 Subject: [PATCH 4/6] Use strict equal when comparing exif values This fixes a problem where flash values of 0 (no flash) would not show up in the side bar --- ui/src/components/sidebar/MediaSidebar.js | 89 ++++++++++++----------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index 7914391..becdfac 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -150,53 +150,53 @@ const exposurePrograms = { // From https://exiftool.org/TagNames/EXIF.html#Flash const flash = { - 0x0: 'No Flash', - 0x1: 'Fired', - 0x5: 'Fired, Return not detected', - 0x7: 'Fired, Return detected', - 0x8: 'On, Did not fire', - 0x9: 'On, Fired', - 0xd: 'On, Return not detected', - 0xf: 'On, Return detected', - 0x10: 'Off, Did not fire', - 0x14: 'Off, Did not fire, Return not detected', - 0x18: 'Auto, Did not fire', - 0x19: 'Auto, Fired', - 0x1d: 'Auto, Fired, Return not detected', - 0x1f: 'Auto, Fired, Return detected', - 0x20: 'No flash function', - 0x30: 'Off, No flash function', - 0x41: 'Fired, Red-eye reduction', - 0x45: 'Fired, Red-eye reduction, Return not detected', - 0x47: 'Fired, Red-eye reduction, Return detected', - 0x49: 'On, Red-eye reduction', - 0x4d: 'On, Red-eye reduction, Return not detected', - 0x4f: 'On, Red-eye reduction, Return detected', - 0x50: 'Off, Red-eye reduction', - 0x58: 'Auto, Did not fire, Red-eye reduction', - 0x59: 'Auto, Fired, Red-eye reduction', - 0x5d: 'Auto, Fired, Red-eye reduction, Return not detected', - 0x5f: 'Auto, Fired, Red-eye reduction, Return detected', + 0x0: 'No Flash', + 0x1: 'Fired', + 0x5: 'Fired, Return not detected', + 0x7: 'Fired, Return detected', + 0x8: 'On, Did not fire', + 0x9: 'On, Fired', + 0xd: 'On, Return not detected', + 0xf: 'On, Return detected', + 0x10: 'Off, Did not fire', + 0x14: 'Off, Did not fire, Return not detected', + 0x18: 'Auto, Did not fire', + 0x19: 'Auto, Fired', + 0x1d: 'Auto, Fired, Return not detected', + 0x1f: 'Auto, Fired, Return detected', + 0x20: 'No flash function', + 0x30: 'Off, No flash function', + 0x41: 'Fired, Red-eye reduction', + 0x45: 'Fired, Red-eye reduction, Return not detected', + 0x47: 'Fired, Red-eye reduction, Return detected', + 0x49: 'On, Red-eye reduction', + 0x4d: 'On, Red-eye reduction, Return not detected', + 0x4f: 'On, Red-eye reduction, Return detected', + 0x50: 'Off, Red-eye reduction', + 0x58: 'Auto, Did not fire, Red-eye reduction', + 0x59: 'Auto, Fired, Red-eye reduction', + 0x5d: 'Auto, Fired, Red-eye reduction, Return not detected', + 0x5f: 'Auto, Fired, Red-eye reduction, Return detected', } // From https://exiftool.org/TagNames/EXIF.html -const orientation = { - 1: 'Horizontal (normal)', - 2: 'Mirror horizontal', - 3: 'Rotate 180', - 4: 'Mirror vertical', - 5: 'Mirror horizontal and rotate 270 CW', - 6: 'Rotate 90 CW', - 7: 'Mirror horizontal and rotate 90 CW', - 8: 'Rotate 270 CW', -} +// const orientation = { +// 1: 'Horizontal (normal)', +// 2: 'Mirror horizontal', +// 3: 'Rotate 180', +// 4: 'Mirror vertical', +// 5: 'Mirror horizontal and rotate 270 CW', +// 6: 'Rotate 90 CW', +// 7: 'Mirror horizontal and rotate 90 CW', +// 8: 'Rotate 270 CW', +// } const SidebarContent = ({ media, hidePreview }) => { let exifItems = [] if (media && media.exif) { let exifKeys = Object.keys(exifNameLookup).filter( - x => !!media.exif[x] && x != '__typename' + x => media.exif[x] !== undefined && x != '__typename' ) let exif = exifKeys.reduce( @@ -209,20 +209,23 @@ const SidebarContent = ({ media, hidePreview }) => { exif.dateShot = new Date(exif.dateShot).toLocaleString() - if (exposurePrograms.hasOwnProperty(exif.exposureProgram)) - { + if ( + exif.exposureProgram !== undefined && + exif.exposureProgram !== 0 && + exposurePrograms[exif.exposureProgram] + ) { exif.exposureProgram = exposurePrograms[exif.exposureProgram] } - if (exif.aperture) { + if (exif.aperture !== undefined) { exif.aperture = `f/${exif.aperture}` } - if (exif.focalLength) { + if (exif.focalLength !== undefined) { exif.focalLength = `${exif.focalLength}mm` } - if (flash.hasOwnProperty(exif.flash)) { + if (exif.flash !== undefined && flash[exif.flash]) { exif.flash = flash[exif.flash] } From f946500eaaf9500ff58a98c6d8500f535f018f06 Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Wed, 31 Mar 2021 23:35:14 +0200 Subject: [PATCH 5/6] Use barasher/go-exiftool Use barasher/go-exiftool again. Exposure is not saved as float and not a string. Signed-off-by: Kjeldgaard --- api/go.mod | 3 +-- api/go.sum | 11 +++++++---- api/graphql/generated.go | 6 +++--- api/graphql/models/media_exif.go | 2 +- api/graphql/schema.graphql | 2 +- api/scanner/exif/exif.go | 2 +- api/scanner/exif/exif_parser_external.go | 9 ++++----- api/scanner/exif/exif_parser_internal.go | 8 +++++--- 8 files changed, 23 insertions(+), 20 deletions(-) diff --git a/api/go.mod b/api/go.mod index 3cc631d..1bcac1f 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,7 +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 // indirect + github.com/barasher/go-exiftool v1.4.0 github.com/disintegration/imaging v1.6.2 github.com/go-sql-driver/mysql v1.5.0 github.com/gorilla/handlers v1.5.1 @@ -13,7 +13,6 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/h2non/filetype v1.1.1 github.com/joho/godotenv v1.3.0 - github.com/kjeldgaard/go-exiftool v1.3.3 github.com/pkg/errors v0.9.1 github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f github.com/strukturag/libheif v1.11.0 diff --git a/api/go.sum b/api/go.sum index 03d6dc5..3c7e767 100644 --- a/api/go.sum +++ b/api/go.sum @@ -10,12 +10,13 @@ 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/barasher/go-exiftool v1.4.0 h1:fTQsz1lWS15rRl13aFRjENt0gUXC+ZVBo/vICacn98A= +github.com/barasher/go-exiftool v1.4.0/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= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -106,8 +107,6 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kjeldgaard/go-exiftool v1.3.3 h1:tDlkcHtTuduu7UgopqGS/ooh1tmEgtIhlBjmTXzjxbg= -github.com/kjeldgaard/go-exiftool v1.3.3/go.mod h1:Nmi1VkXBbr48Kq5ZJjo29RD+a3PdWVGlL+8bSssxFbU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -146,6 +145,7 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f h1:8P2MkG70G76gnZBOPGwmMIgwBb/rESQuwsJ7K8ds4NE= github.com/sabhiram/go-gitignore v0.0.0-20201211210132-54b8a0bf510f/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -156,6 +156,7 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -174,6 +175,7 @@ github.com/strukturag/libheif v1.3.2 h1:NXft6czPSJiBeGc6hchNgczIY9aRPxsIcNIlVlDm github.com/strukturag/libheif v1.3.2/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= github.com/strukturag/libheif v1.11.0 h1:HaWu5re98INSXNq7C8o5AwLcv2qD8+U7a+jVCpGWemI= github.com/strukturag/libheif v1.11.0/go.mod h1:E/PNRlmVtrtj9j2AvBZlrO4dsBDu6KfwDZn7X1Ce8Ks= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.1.0 h1:uiKJ+T5HMGGQM2kRKQ8Pxw8+Zq9qhhZhz/lieYvCMns= @@ -237,6 +239,7 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM= golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/api/graphql/generated.go b/api/graphql/generated.go index e452da2..73d5467 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -1881,7 +1881,7 @@ type MediaEXIF { lens: String dateShot: Time "The exposure time of the image" - exposure: String + exposure: Float "The aperature stops of the image" aperture: Float "The ISO setting of the image" @@ -4456,9 +4456,9 @@ func (ec *executionContext) _MediaEXIF_exposure(ctx context.Context, field graph if resTmp == nil { return graphql.Null } - res := resTmp.(*string) + res := resTmp.(*float64) fc.Result = res - return ec.marshalOString2ᚖstring(ctx, field.Selections, res) + return ec.marshalOFloat2ᚖfloat64(ctx, field.Selections, res) } func (ec *executionContext) _MediaEXIF_aperture(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) { diff --git a/api/graphql/models/media_exif.go b/api/graphql/models/media_exif.go index 6b75ab6..dec9d67 100644 --- a/api/graphql/models/media_exif.go +++ b/api/graphql/models/media_exif.go @@ -10,7 +10,7 @@ type MediaEXIF struct { Maker *string Lens *string DateShot *time.Time - Exposure *string + Exposure *float64 Aperture *float64 Iso *int64 FocalLength *float64 diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index ff91f54..817fb0d 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -304,7 +304,7 @@ type MediaEXIF { lens: String dateShot: Time "The exposure time of the image" - exposure: String + exposure: Float "The aperature stops of the image" aperture: Float "The ISO setting of the image" diff --git a/api/scanner/exif/exif.go b/api/scanner/exif/exif.go index be2a026..85f9eea 100644 --- a/api/scanner/exif/exif.go +++ b/api/scanner/exif/exif.go @@ -6,7 +6,7 @@ import ( "github.com/pkg/errors" "gorm.io/gorm" - "github.com/kjeldgaard/go-exiftool" + "github.com/barasher/go-exiftool" "github.com/photoview/photoview/api/graphql/models" ) diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index 5378cd8..b8caaeb 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -6,16 +6,15 @@ import ( "strconv" "time" - "github.com/kjeldgaard/go-exiftool" + "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 - extraInitArgs := []string{"-n"} - et, err := exiftool.NewExiftool(exiftool.AddInitArgs(extraInitArgs)) + // ExifTool - No print conversion mode + et, err := exiftool.NewExiftool(exiftool.NoPrintConversion()) if err != nil { log.Printf("Error initializing ExifTool: %s\n", err) return nil, err @@ -64,7 +63,7 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. } // Get exposure time - exposureTime, err := fileInfo.GetString("ExposureTime") + exposureTime, err := fileInfo.GetFloat("ExposureTime") if err == nil { log.Printf("Exposure time: %s", exposureTime) newExif.Exposure = &exposureTime diff --git a/api/scanner/exif/exif_parser_internal.go b/api/scanner/exif/exif_parser_internal.go index baacf57..59523a3 100644 --- a/api/scanner/exif/exif_parser_internal.go +++ b/api/scanner/exif/exif_parser_internal.go @@ -58,10 +58,12 @@ func (p *internalExifParser) ParseExif(media *models.Media) (returnExif *models. newExif.DateShot = &date } - exposure, err := p.readRationalTag(exifTags, exif.ExposureTime, media) + exposure, err := exifTags.Get(exif.ExposureTime) if err == nil { - exposureStr := exposure.RatString() - newExif.Exposure = &exposureStr + exposureFloat, err := exposure.Float(0) + if err == nil { + newExif.Exposure = &exposureFloat + } } apertureRat, err := p.readRationalTag(exifTags, exif.FNumber, media) From 3bde799fb3c781d14117e7c0329343358b52f749 Mon Sep 17 00:00:00 2001 From: Kjeldgaard Date: Wed, 31 Mar 2021 23:43:44 +0200 Subject: [PATCH 6/6] Fix log bug Fix log bug, exposure time is now float and not string. Signed-off-by: Kjeldgaard --- api/scanner/exif/exif_parser_external.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/scanner/exif/exif_parser_external.go b/api/scanner/exif/exif_parser_external.go index b8caaeb..6a47e32 100644 --- a/api/scanner/exif/exif_parser_external.go +++ b/api/scanner/exif/exif_parser_external.go @@ -65,7 +65,7 @@ func (p *externalExifParser) ParseExif(media *models.Media) (returnExif *models. // Get exposure time exposureTime, err := fileInfo.GetFloat("ExposureTime") if err == nil { - log.Printf("Exposure time: %s", exposureTime) + log.Printf("Exposure time: %f", exposureTime) newExif.Exposure = &exposureTime }