1
Fork 0

Use exiftool for parsing exif data

Use exiftool to parse exif metadata.

Signed-off-by: Kjeldgaard <Kjeldgaard@users.noreply.github.com>
This commit is contained in:
Kjeldgaard 2021-03-20 00:26:05 +01:00
parent ba16fc1caa
commit 1f9f6ebf9a
10 changed files with 238 additions and 214 deletions

View File

@ -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 \

View File

@ -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

View File

@ -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=

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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")
}

View File

@ -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) {