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"
|
"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
30
exif.go
|
@ -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
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/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=
|
||||||
|
|
183
ifd_enumerate.go
183
ifd_enumerate.go
|
@ -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) {
|
||||||
|
|
|
@ -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
23
tags.go
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue