Working on parsing timestamps

pull/76/head
Logan Shire 2022-11-05 19:59:02 -04:00
parent d21ac8e2de
commit cfe60a0cce
8 changed files with 268 additions and 18 deletions

BIN
assets/Display_P3.heic Normal file

Binary file not shown.

BIN
assets/My_Display_P3.heic Normal file

Binary file not shown.

View File

@ -8,7 +8,7 @@ import (
"io/ioutil" "io/ioutil"
"github.com/dsoprea/go-logging" log "github.com/dsoprea/go-logging"
) )
var ( var (
@ -163,17 +163,10 @@ func init() {
// This will only be executed when we're running tests in this package and // This will only be executed when we're running tests in this package and
// not when this package is being imported from a subpackage. // not when this package is being imported from a subpackage.
goPath := os.Getenv("GOPATH") currentWd, err := os.Getwd()
if goPath != "" { log.PanicIf(err)
assetsPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
} else {
// Module-enabled context.
currentWd, err := os.Getwd() assetsPath = path.Join(currentWd, "assets")
log.PanicIf(err)
assetsPath = path.Join(currentWd, "assets")
}
testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg") testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
@ -181,7 +174,6 @@ func init() {
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif") filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
var err error
testExifData, err = ioutil.ReadFile(filepath) testExifData, err = ioutil.ReadFile(filepath)
log.PanicIf(err) log.PanicIf(err)
} }

30
exif.go
View File

@ -5,11 +5,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"time"
"encoding/binary" "encoding/binary"
"io/ioutil" "io/ioutil"
"github.com/dsoprea/go-logging" log "github.com/dsoprea/go-logging"
) )
const ( const (
@ -245,3 +246,30 @@ func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerB
return b.Bytes(), nil return b.Bytes(), nil
} }
type ExifInfo struct {
// The time at which the image was captured.
DateTimeOriginal time.Time
HasDateTimeOriginal bool
// The offset from UTC
OffsetTimeOriginal time.Duration
HasOffsetTimeOriginal bool
}
// This function converts the DateTimeOriginal value to UTC if the ExifInfo has an OffsetTimeOriginal value.
// By default we can make no assumptions about the time zone the original date time was captured in.
// We only know the time in UTC if the exif info provides the OffsetTimeOriginal value.
//
// E.g. New York is typically 5h earlier than UTC. During DST New York is only 4h earlier than UTC.
// Given a date time original value of 2022:11:05 17:38:25, and a OffsetTimeOriginal value of -04:00,
// we need to add 4*60*60 seconds to the time in New York to get the time in UTC.
// This finally gives us a time in UTC of 2022:11:05 21:38:25.
func (ei *ExifInfo) DateTimeOriginalInUTC() time.Time {
return ei.DateTimeOriginal.Add(-1 * ei.OffsetTimeOriginal).In(time.UTC)
}
// func (ei *ExifInfo) String() string {
// return fmt.Sprintf("ExifInfo<LAT=(%.05f) LON=(%.05f) ALT=(%d) TIME=[%s]>", gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp)
// }

1
go.sum
View File

@ -9,6 +9,7 @@ golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8ou
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -11,7 +11,7 @@ import (
"encoding/binary" "encoding/binary"
"github.com/dsoprea/go-logging" log "github.com/dsoprea/go-logging"
) )
var ( var (
@ -997,6 +997,187 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
return gi, nil return gi, nil
} }
// ExifInfo parses and consolidates the Exif info. This can only be called on the Exif IFD.
func (ifd *Ifd) ExifInfo() (ei *ExifInfo, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Also add functionality to update the GPS info.
ei = new(ExifInfo)
if ifd.IfdPath != IfdPathStandardExif {
log.Panicf("Exif can only be read on Exif IFD: [%s] != [%s]", ifd.IfdPath, IfdPathStandardExif)
}
// Parse Original Time Tags
dateTimeOriginalTags, foundDateTimeOriginal := ifd.EntriesByTagId[ExifDateTimeOriginal]
if foundDateTimeOriginal {
// Parse the DateTimeOriginal tag:
dateTimeOriginalValue, err := ifd.TagValue(dateTimeOriginalTags[0])
log.PanicIf(err)
dateTimeOriginal, err := time.Parse("2006:1:2 15:04:05", dateTimeOriginalValue.(string))
log.PanicIf(err)
ei.DateTimeOriginal = dateTimeOriginal
ei.HasDateTimeOriginal = true
// Parse the SubSecTimeOriginal tag if we have one:
subSecTimeOriginalTags, foundSubSecTimeOriginal := ifd.EntriesByTagId[ExifSubSecTimeOriginal]
if foundSubSecTimeOriginal {
subSecTimeOriginalValue, err := ifd.TagValue(subSecTimeOriginalTags[0])
log.PanicIf(err)
// The sub-sec time is an integer number of milliseconds formatted as a string.
subSecTimeOriginal, err := strconv.ParseInt(subSecTimeOriginalValue.(string), 10, 64)
log.PanicIf(err)
subSecTimeOriginalDuration := time.Millisecond * time.Duration(subSecTimeOriginal)
dateTimeOriginalWithSubSeconds := ei.DateTimeOriginal.Add(subSecTimeOriginalDuration)
ei.DateTimeOriginal = dateTimeOriginalWithSubSeconds
}
// Parse the OffsetTimeOriginal tag if we have one:
offsetTimeOriginalTags, foundOffsetTimeOriginal := ifd.EntriesByTagId[ExifOffsetTimeOriginal]
if foundOffsetTimeOriginal {
offsetTimeOriginalValue, err := ifd.TagValue(offsetTimeOriginalTags[0])
log.PanicIf(err)
// The offset time is formatted like a time zone but it includes daylight savings time if applicable.
// As such we don't want to treat it as an actual time zone.
// Rather, we want to parse it as a time zone but then convert it to a duration relative to UTC.
offsetTimeOriginal, err := time.Parse("-07:00", offsetTimeOriginalValue.(string))
log.PanicIf(err)
_, offsetInt := offsetTimeOriginal.Zone()
ei.OffsetTimeOriginal = time.Second * time.Duration(offsetInt)
ei.HasOffsetTimeOriginal = true
}
// dateParts := strings.Split(datestampValue.(string), ":")
// year, err1 := strconv.ParseUint(dateParts[0], 10, 16)
// month, err2 := strconv.ParseUint(dateParts[1], 10, 8)
// day, err3 := strconv.ParseUint(dateParts[2], 10, 8)
// if err1 == nil && err2 == nil && err3 == nil {
// timestampValue, err := ifd.TagValue(timestampTags[0])
// log.PanicIf(err)
// timestampRaw := timestampValue.([]Rational)
// hour := int(timestampRaw[0].Numerator / timestampRaw[0].Denominator)
// minute := int(timestampRaw[1].Numerator / timestampRaw[1].Denominator)
// second := int(timestampRaw[2].Numerator / timestampRaw[2].Denominator)
// gi.Timestamp = time.Date(int(year), time.Month(month), int(day), hour, minute, second, 0, time.UTC)
// }
}
// Look for whether North or South.
// tags, found = ifd.EntriesByTagId[TagLatitudeRefId]
// if found == false {
// ifdEnumerateLogger.Warningf(nil, "latitude-ref not found")
// log.Panic(ErrNoGpsTags)
// }
// latitudeRefValue, err := ifd.TagValue(tags[0])
// log.PanicIf(err)
// tags, found = ifd.EntriesByTagId[TagLongitudeId]
// if found == false {
// ifdEnumerateLogger.Warningf(nil, "longitude not found")
// log.Panic(ErrNoGpsTags)
// }
// longitudeValue, err := ifd.TagValue(tags[0])
// log.PanicIf(err)
// Look for whether West or East.
// tags, found = ifd.EntriesByTagId[TagLongitudeRefId]
// if found == false {
// ifdEnumerateLogger.Warningf(nil, "longitude-ref not found")
// log.Panic(ErrNoGpsTags)
// }
// longitudeRefValue, err := ifd.TagValue(tags[0])
// log.PanicIf(err)
// Parse location.
// latitudeRaw := latitudeValue.([]Rational)
// ei.Latitude = GpsDegrees{
// Orientation: latitudeRefValue.(string)[0],
// Degrees: float64(latitudeRaw[0].Numerator) / float64(latitudeRaw[0].Denominator),
// Minutes: float64(latitudeRaw[1].Numerator) / float64(latitudeRaw[1].Denominator),
// Seconds: float64(latitudeRaw[2].Numerator) / float64(latitudeRaw[2].Denominator),
// }
// longitudeRaw := longitudeValue.([]Rational)
// ei.Longitude = GpsDegrees{
// Orientation: longitudeRefValue.(string)[0],
// Degrees: float64(longitudeRaw[0].Numerator) / float64(longitudeRaw[0].Denominator),
// Minutes: float64(longitudeRaw[1].Numerator) / float64(longitudeRaw[1].Denominator),
// Seconds: float64(longitudeRaw[2].Numerator) / float64(longitudeRaw[2].Denominator),
// }
// Parse altitude.
// altitudeTags, foundAltitude := ifd.EntriesByTagId[TagAltitudeId]
// altitudeRefTags, foundAltitudeRef := ifd.EntriesByTagId[TagAltitudeRefId]
// if foundAltitude == true && foundAltitudeRef == true {
// altitudeValue, err := ifd.TagValue(altitudeTags[0])
// log.PanicIf(err)
// altitudeRefValue, err := ifd.TagValue(altitudeRefTags[0])
// log.PanicIf(err)
// altitudeRaw := altitudeValue.([]Rational)
// altitude := int(altitudeRaw[0].Numerator / altitudeRaw[0].Denominator)
// if altitudeRefValue.([]byte)[0] == 1 {
// altitude *= -1
// }
// ei.Altitude = altitude
// }
// Parse time.
// timestampTags, foundTimestamp := ifd.EntriesByTagId[TagTimestampId]
// datestampTags, foundDatestamp := ifd.EntriesByTagId[TagDatestampId]
// if foundTimestamp == true && foundDatestamp == true {
// datestampValue, err := ifd.TagValue(datestampTags[0])
// log.PanicIf(err)
// dateParts := strings.Split(datestampValue.(string), ":")
// year, err1 := strconv.ParseUint(dateParts[0], 10, 16)
// month, err2 := strconv.ParseUint(dateParts[1], 10, 8)
// day, err3 := strconv.ParseUint(dateParts[2], 10, 8)
// if err1 == nil && err2 == nil && err3 == nil {
// timestampValue, err := ifd.TagValue(timestampTags[0])
// log.PanicIf(err)
// timestampRaw := timestampValue.([]Rational)
// hour := int(timestampRaw[0].Numerator / timestampRaw[0].Denominator)
// minute := int(timestampRaw[1].Numerator / timestampRaw[1].Denominator)
// second := int(timestampRaw[2].Numerator / timestampRaw[2].Denominator)
// gi.Timestamp = time.Date(int(year), time.Month(month), int(day), hour, minute, second, 0, time.UTC)
// }
// }
return ei, nil
}
type ParsedTagVisitor func(*Ifd, *IfdTagEntry) error type ParsedTagVisitor func(*Ifd, *IfdTagEntry) error
func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) { func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) {

View File

@ -6,11 +6,12 @@ import (
"path" "path"
"reflect" "reflect"
"testing" "testing"
"time"
"encoding/binary" "encoding/binary"
"io/ioutil" "io/ioutil"
"github.com/dsoprea/go-logging" log "github.com/dsoprea/go-logging"
) )
func TestIfdTagEntry_ValueBytes(t *testing.T) { func TestIfdTagEntry_ValueBytes(t *testing.T) {
@ -494,6 +495,36 @@ func ExampleIfd_GpsInfo() {
// GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]> // GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]>
} }
func ExampleIfd_ExifInfo() {
filepath := path.Join(assetsPath, "My_Display_P3.heic")
rawExif, err := SearchFileAndExtractExif(filepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd, err := index.RootIfd.ChildWithIfdPath(IfdPathStandardExif)
log.PanicIf(err)
ei, err := ifd.ExifInfo()
log.PanicIf(err)
loc, _ := time.LoadLocation("America/New_York")
utc := ei.DateTimeOriginalInUTC()
fmt.Printf("%v\n", utc.In(loc).Format(time.Kitchen))
// Output:
// GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]>
}
func ExampleIfd_FindTagWithName() { func ExampleIfd_FindTagWithName() {
rawExif, err := SearchFileAndExtractExif(testImageFilepath) rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err) log.PanicIf(err)

23
tags.go
View File

@ -3,17 +3,34 @@ package exif
import ( import (
"fmt" "fmt"
"github.com/dsoprea/go-logging" log "github.com/dsoprea/go-logging"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
const ( const (
// IFD1
// Exif Tags ///////////////////////////////
// https://exiftool.org/TagNames/EXIF.html //
////////////////////////////////////////////
// The date and time when the original image data was generated.
ExifDateTimeOriginal = 0x9003
// A tag used to record fractions of seconds for the DateTimeOriginal tag.
ExifSubSecTimeOriginal = 0x9291
// Time difference from Universal Time Coordinated including daylight saving time of DateTimeOriginal tag.
// May be omitted even if DateTimeOriginal is included if the camera doesn't have the capability to determine this.
ExifOffsetTimeOriginal = 0x9011
// Backwards-Compatible Exif Tags ///////////
// https://exiftool.org/TagNames/EXIF.html //
/////////////////////////////////////////////
ThumbnailOffsetTagId = 0x0201 ThumbnailOffsetTagId = 0x0201
ThumbnailSizeTagId = 0x0202 ThumbnailSizeTagId = 0x0202
// Exif // GPS Tags ////////////////////////////////
// https://exiftool.org/TagNames/GPS.html //
////////////////////////////////////////////
TagVersionId = 0x0000 TagVersionId = 0x0000