mirror of https://github.com/dsoprea/go-exif.git
Working on parsing timestamps
parent
d21ac8e2de
commit
cfe60a0cce
Binary file not shown.
Binary file not shown.
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
log "github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -163,17 +163,10 @@ func init() {
|
|||
// This will only be executed when we're running tests in this package and
|
||||
// not when this package is being imported from a subpackage.
|
||||
|
||||
goPath := os.Getenv("GOPATH")
|
||||
if goPath != "" {
|
||||
assetsPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
|
||||
} else {
|
||||
// Module-enabled context.
|
||||
currentWd, err := os.Getwd()
|
||||
log.PanicIf(err)
|
||||
|
||||
currentWd, err := os.Getwd()
|
||||
log.PanicIf(err)
|
||||
|
||||
assetsPath = path.Join(currentWd, "assets")
|
||||
}
|
||||
assetsPath = path.Join(currentWd, "assets")
|
||||
|
||||
testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
|
||||
|
||||
|
@ -181,7 +174,6 @@ func init() {
|
|||
|
||||
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
|
||||
|
||||
var err error
|
||||
testExifData, err = ioutil.ReadFile(filepath)
|
||||
log.PanicIf(err)
|
||||
}
|
||||
|
|
30
exif.go
30
exif.go
|
@ -5,11 +5,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
log "github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -245,3 +246,30 @@ func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerB
|
|||
|
||||
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
1
go.sum
|
@ -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/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=
|
||||
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/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
183
ifd_enumerate.go
183
ifd_enumerate.go
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
log "github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -997,6 +997,187 @@ func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
|
|||
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
|
||||
|
||||
func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) {
|
||||
|
|
|
@ -6,11 +6,12 @@ import (
|
|||
"path"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
log "github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
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]>
|
||||
}
|
||||
|
||||
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() {
|
||||
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
|
||||
log.PanicIf(err)
|
||||
|
|
23
tags.go
23
tags.go
|
@ -3,17 +3,34 @@ package exif
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
log "github.com/dsoprea/go-logging"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
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
|
||||
ThumbnailSizeTagId = 0x0202
|
||||
|
||||
// Exif
|
||||
// GPS Tags ////////////////////////////////
|
||||
// https://exiftool.org/TagNames/GPS.html //
|
||||
////////////////////////////////////////////
|
||||
|
||||
TagVersionId = 0x0000
|
||||
|
||||
|
|
Loading…
Reference in New Issue