mirror of https://github.com/dsoprea/go-exif.git
ifd_enumerate: Now parse the GPS info.
- Moved some IFD functionality out to its own file. - Tweaked the permissions on some assets.pull/3/head
parent
1ce5b771db
commit
b9537b58c2
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,37 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
type GpsDegrees struct {
|
||||
Orientation byte
|
||||
Degrees, Minutes, Seconds int
|
||||
}
|
||||
|
||||
func (d GpsDegrees) String() string {
|
||||
return fmt.Sprintf("Degrees<O=[%s] D=(%d) M=(%d) S=(%d)>", string([]byte { d.Orientation }), d.Degrees, d.Minutes, d.Seconds)
|
||||
}
|
||||
|
||||
func (d GpsDegrees) Decimal() float64 {
|
||||
decimal := float64(d.Degrees) + float64(d.Minutes) / 60.0 + float64(d.Seconds) / 3600.0
|
||||
|
||||
if d.Orientation == 'S' || d.Orientation == 'W' {
|
||||
return -decimal
|
||||
} else {
|
||||
return decimal
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type GpsInfo struct {
|
||||
Latitude, Longitude GpsDegrees
|
||||
Altitude int
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (gi GpsInfo) String() string {
|
||||
return fmt.Sprintf("GpsInfo<LAT=(%.05f) LON=(%.05f) ALT=(%d) TIME=[%s]>", gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dsoprea/go-logging"
|
||||
)
|
||||
|
||||
const (
|
||||
IfdStandard = "IFD"
|
||||
|
||||
IfdExif = "Exif"
|
||||
IfdGps = "GPSInfo"
|
||||
IfdIop = "Iop"
|
||||
|
||||
IfdExifId = 0x8769
|
||||
IfdGpsId = 0x8825
|
||||
IfdIopId = 0xA005
|
||||
)
|
||||
|
||||
type IfdNameAndIndex struct {
|
||||
Ii IfdIdentity
|
||||
Index int
|
||||
}
|
||||
|
||||
var (
|
||||
// TODO(dustin): !! Get rid of this in favor of one of the two lookups, just below.
|
||||
validIfds = []string {
|
||||
IfdStandard,
|
||||
IfdExif,
|
||||
IfdGps,
|
||||
IfdIop,
|
||||
}
|
||||
|
||||
// A lookup for IFDs by their parents.
|
||||
// TODO(dustin): !! We should switch to indexing by their unique integer IDs (defined below) rather than exposing ourselves to non-unique IFD names (even if we *do* manage the naming ourselves).
|
||||
IfdTagIds = map[string]map[string]uint16 {
|
||||
"": map[string]uint16 {
|
||||
// A root IFD type. Not allowed to be a child (tag-based) IFD.
|
||||
IfdStandard: 0x0,
|
||||
},
|
||||
|
||||
IfdStandard: map[string]uint16 {
|
||||
IfdExif: IfdExifId,
|
||||
IfdGps: IfdGpsId,
|
||||
},
|
||||
|
||||
IfdExif: map[string]uint16 {
|
||||
IfdIop: IfdIopId,
|
||||
},
|
||||
}
|
||||
|
||||
// IfdTagNames contains the tag ID-to-name mappings and is populated by
|
||||
// init().
|
||||
IfdTagNames = map[string]map[uint16]string {}
|
||||
|
||||
// IFD Identities. These are often how we refer to IFDs, from call to call.
|
||||
|
||||
// The NULL-type instance for search misses and empty responses.
|
||||
ZeroIi = IfdIdentity{}
|
||||
|
||||
RootIi = IfdIdentity{ IfdName: IfdStandard }
|
||||
ExifIi = IfdIdentity{ ParentIfdName: IfdStandard, IfdName: IfdExif }
|
||||
GpsIi = IfdIdentity{ ParentIfdName: IfdStandard, IfdName: IfdGps }
|
||||
ExifIopIi = IfdIdentity{ ParentIfdName: IfdExif, IfdName: IfdIop }
|
||||
|
||||
// Produce a list of unique IDs for each IFD that we can pass around (so we
|
||||
// don't always have to be comparing parent and child names).
|
||||
//
|
||||
// For lack of need, this is just static.
|
||||
//
|
||||
// (0) is reserved for not-found/miss responses.
|
||||
IfdIds = map[IfdIdentity]int {
|
||||
RootIi: 1,
|
||||
ExifIi: 2,
|
||||
GpsIi: 3,
|
||||
ExifIopIi: 4,
|
||||
}
|
||||
|
||||
IfdDesignations = map[string]IfdNameAndIndex {
|
||||
"ifd0": { RootIi, 0 },
|
||||
"ifd1": { RootIi, 1 },
|
||||
"exif": { ExifIi, 0 },
|
||||
"gps": { GpsIi, 0 },
|
||||
"iop": { ExifIopIi, 0 },
|
||||
}
|
||||
|
||||
IfdDesignationsR = make(map[IfdNameAndIndex]string)
|
||||
)
|
||||
|
||||
var (
|
||||
ifdLogger = log.NewLogger("exif.ifd")
|
||||
)
|
||||
|
||||
func IfdDesignation(ii IfdIdentity, index int) string {
|
||||
if ii == RootIi {
|
||||
return fmt.Sprintf("%s%d", ii.IfdName, index)
|
||||
} else {
|
||||
return ii.IfdName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type IfdIdentity struct {
|
||||
ParentIfdName string
|
||||
IfdName string
|
||||
}
|
||||
|
||||
func (ii IfdIdentity) String() string {
|
||||
return fmt.Sprintf("IfdIdentity<PARENT-NAME=[%s] NAME=[%s]>", ii.ParentIfdName, ii.IfdName)
|
||||
}
|
||||
|
||||
func (ii IfdIdentity) Id() int {
|
||||
return IfdIdWithIdentityOrFail(ii)
|
||||
}
|
||||
|
||||
|
||||
func init() {
|
||||
for ifdName, tags := range IfdTagIds {
|
||||
tagsR := make(map[uint16]string)
|
||||
|
||||
for tagName, tagId := range tags {
|
||||
tagsR[tagId] = tagName
|
||||
}
|
||||
|
||||
IfdTagNames[ifdName] = tagsR
|
||||
}
|
||||
|
||||
for designation, ni := range IfdDesignations {
|
||||
IfdDesignationsR[ni] = designation
|
||||
}
|
||||
}
|
136
ifd_enumerate.go
136
ifd_enumerate.go
|
@ -6,6 +6,8 @@ import (
|
|||
"strings"
|
||||
"errors"
|
||||
"reflect"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"encoding/binary"
|
||||
|
||||
|
@ -14,8 +16,11 @@ import (
|
|||
|
||||
var (
|
||||
ifdEnumerateLogger = log.NewLogger("exifjpeg.ifd")
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoThumbnail = errors.New("no thumbnail")
|
||||
ErrNoGpsTags = errors.New("no gps tags")
|
||||
)
|
||||
|
||||
|
||||
|
@ -796,6 +801,137 @@ func (ifd *Ifd) DumpTree() []string {
|
|||
return ifd.dumpTree(nil, 0)
|
||||
}
|
||||
|
||||
// GpsInfo parses and consolidates the GPS info. This can only be called on the
|
||||
// GPS IFD.
|
||||
func (ifd *Ifd) GpsInfo() (gi *GpsInfo, err error) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err = log.Wrap(state.(error))
|
||||
}
|
||||
}()
|
||||
|
||||
gi = new(GpsInfo)
|
||||
|
||||
if ifd.Ii != GpsIi {
|
||||
log.Panicf("GPS can only be read on GPS IFD: [%s] != [%s]", ifd.Ii, GpsIi)
|
||||
}
|
||||
|
||||
if tags, found := ifd.EntriesByTagId[TagVersionId]; found == false {
|
||||
log.Panic(ErrNoGpsTags)
|
||||
} else if bytes.Compare(tags[0].value, []byte { 2, 2, 0, 0}) != 0 {
|
||||
log.Panic(ErrNoGpsTags)
|
||||
}
|
||||
|
||||
|
||||
tags, found := ifd.EntriesByTagId[TagLatitudeId]
|
||||
if found == false {
|
||||
log.Panicf("latitude not found")
|
||||
}
|
||||
|
||||
latitudeValue, err := ifd.TagValue(tags[0])
|
||||
log.PanicIf(err)
|
||||
|
||||
tags, found = ifd.EntriesByTagId[TagLatitudeRefId]
|
||||
if found == false {
|
||||
log.Panicf("latitude-ref not found")
|
||||
}
|
||||
|
||||
latitudeRefValue, err := ifd.TagValue(tags[0])
|
||||
log.PanicIf(err)
|
||||
|
||||
tags, found = ifd.EntriesByTagId[TagLongitudeId]
|
||||
if found == false {
|
||||
log.Panicf("longitude not found")
|
||||
}
|
||||
|
||||
longitudeValue, err := ifd.TagValue(tags[0])
|
||||
log.PanicIf(err)
|
||||
|
||||
tags, found = ifd.EntriesByTagId[TagLongitudeRefId]
|
||||
if found == false {
|
||||
log.Panicf("longitude-ref not found")
|
||||
}
|
||||
|
||||
longitudeRefValue, err := ifd.TagValue(tags[0])
|
||||
log.PanicIf(err)
|
||||
|
||||
|
||||
// Parse location.
|
||||
|
||||
latitudeRaw := latitudeValue.([]Rational)
|
||||
|
||||
gi.Latitude = GpsDegrees{
|
||||
Orientation: latitudeRefValue.(string)[0],
|
||||
Degrees: int(float64(latitudeRaw[0].Numerator) / float64(latitudeRaw[0].Denominator)),
|
||||
Minutes: int(float64(latitudeRaw[1].Numerator) / float64(latitudeRaw[1].Denominator)),
|
||||
Seconds: int(float64(latitudeRaw[2].Numerator) / float64(latitudeRaw[2].Denominator)),
|
||||
}
|
||||
|
||||
longitudeRaw := longitudeValue.([]Rational)
|
||||
|
||||
gi.Longitude = GpsDegrees{
|
||||
Orientation: longitudeRefValue.(string)[0],
|
||||
Degrees: int(float64(longitudeRaw[0].Numerator) / float64(longitudeRaw[0].Denominator)),
|
||||
Minutes: int(float64(longitudeRaw[1].Numerator) / float64(longitudeRaw[1].Denominator)),
|
||||
Seconds: int(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
|
||||
}
|
||||
|
||||
gi.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 gi, nil
|
||||
}
|
||||
|
||||
|
||||
type QueuedIfd struct {
|
||||
Ii IfdIdentity
|
||||
TagId uint16
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"path"
|
||||
"testing"
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
|
@ -291,3 +292,62 @@ func Test_Ifd_Thumbnail(t *testing.T) {
|
|||
t.Fatalf("thumbnail not correct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfd_GpsInfo(t *testing.T) {
|
||||
defer func() {
|
||||
if state := recover(); state != nil {
|
||||
err := log.Wrap(state.(error))
|
||||
log.PrintErrorf(err, "Test failure.")
|
||||
}
|
||||
}()
|
||||
|
||||
filepath := path.Join(assetsPath, "20180428_212312.jpg")
|
||||
|
||||
rawExif, err := SearchFileAndExtractExif(filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
_, index, err := Collect(rawExif)
|
||||
log.PanicIf(err)
|
||||
|
||||
ifd, err := index.RootIfd.ChildWithIfdIdentity(GpsIi)
|
||||
log.PanicIf(err)
|
||||
|
||||
gi, err := ifd.GpsInfo()
|
||||
log.PanicIf(err)
|
||||
|
||||
if gi.Latitude.Orientation != 'N' || gi.Latitude.Degrees != 26 || gi.Latitude.Minutes != 35 || gi.Latitude.Seconds != 12 {
|
||||
t.Fatalf("latitude not correct")
|
||||
} else if gi.Longitude.Orientation != 'W' || gi.Longitude.Degrees != 80 || gi.Longitude.Minutes != 3 || gi.Longitude.Seconds != 13 {
|
||||
t.Fatalf("longitude not correct")
|
||||
} else if gi.Altitude != 0 {
|
||||
t.Fatalf("altitude not correct")
|
||||
} else if gi.Timestamp.Unix() != 1524964977 {
|
||||
t.Fatalf("timestamp not correct")
|
||||
} else if gi.Altitude != 0 {
|
||||
t.Fatalf("altitude not correct")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func ExampleIfd_GpsInfo() {
|
||||
filepath := path.Join(assetsPath, "20180428_212312.jpg")
|
||||
|
||||
rawExif, err := SearchFileAndExtractExif(filepath)
|
||||
log.PanicIf(err)
|
||||
|
||||
_, index, err := Collect(rawExif)
|
||||
log.PanicIf(err)
|
||||
|
||||
ifd, err := index.RootIfd.ChildWithIfdIdentity(GpsIi)
|
||||
log.PanicIf(err)
|
||||
|
||||
gi, err := ifd.GpsInfo()
|
||||
log.PanicIf(err)
|
||||
|
||||
fmt.Printf("%s\n", gi)
|
||||
|
||||
// Output:
|
||||
// GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]>
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ type IfdTagEntry struct {
|
|||
// IfdName is the IFD that this tag belongs to.
|
||||
Ii IfdIdentity
|
||||
|
||||
// TODO(dustin): !! We now parse and read the value immediately. Update the rest of the logic to use this and get rid of all ofthe staggered and different resolution mechanisms.
|
||||
|
||||
// TODO(dustin): !! We now parse and read the value immediately. Update the rest of the logic to use this and get rid of all of the staggered and different resolution mechanisms.
|
||||
value []byte
|
||||
isUnhandledUnknown bool
|
||||
}
|
||||
|
|
129
tags.go
129
tags.go
|
@ -10,81 +10,30 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
IfdStandard = "IFD"
|
||||
|
||||
IfdExif = "Exif"
|
||||
IfdGps = "GPSInfo"
|
||||
IfdIop = "Iop"
|
||||
|
||||
IfdExifId = 0x8769
|
||||
IfdGpsId = 0x8825
|
||||
IfdIopId = 0xA005
|
||||
// IFD1
|
||||
|
||||
ThumbnailOffsetTagId = 0x0201
|
||||
ThumbnailSizeTagId = 0x0202
|
||||
)
|
||||
|
||||
type IfdNameAndIndex struct {
|
||||
Ii IfdIdentity
|
||||
Index int
|
||||
}
|
||||
// Exif
|
||||
|
||||
TagVersionId = 0x0000
|
||||
|
||||
TagLatitudeId = 0x0002
|
||||
TagLatitudeRefId = 0x0001
|
||||
TagLongitudeId = 0x0004
|
||||
TagLongitudeRefId = 0x0003
|
||||
|
||||
TagTimestampId = 0x0007
|
||||
TagDatestampId = 0x001d
|
||||
|
||||
TagAltitudeId = 0x0006
|
||||
TagAltitudeRefId = 0x0005
|
||||
)
|
||||
|
||||
var (
|
||||
tagDataFilepath = ""
|
||||
|
||||
// TODO(dustin): !! Get rid of this in favor of one of the two lookups, just below.
|
||||
validIfds = []string {
|
||||
IfdStandard,
|
||||
IfdExif,
|
||||
IfdGps,
|
||||
IfdIop,
|
||||
}
|
||||
|
||||
// A lookup for IFDs by their parents.
|
||||
// TODO(dustin): !! We should switch to indexing by their unique integer IDs (defined below) rather than exposing ourselves to non-unique IFD names (even if we *do* manage the naming ourselves).
|
||||
IfdTagIds = map[string]map[string]uint16 {
|
||||
"": map[string]uint16 {
|
||||
// A root IFD type. Not allowed to be a child (tag-based) IFD.
|
||||
IfdStandard: 0x0,
|
||||
},
|
||||
|
||||
IfdStandard: map[string]uint16 {
|
||||
IfdExif: IfdExifId,
|
||||
IfdGps: IfdGpsId,
|
||||
},
|
||||
|
||||
IfdExif: map[string]uint16 {
|
||||
IfdIop: IfdIopId,
|
||||
},
|
||||
}
|
||||
|
||||
// IfdTagNames contains the tag ID-to-name mappings and is populated by
|
||||
// init().
|
||||
IfdTagNames = map[string]map[uint16]string {}
|
||||
|
||||
// IFD Identities. These are often how we refer to IFDs, from call to call.
|
||||
|
||||
// The NULL-type instance for search misses and empty responses.
|
||||
ZeroIi = IfdIdentity{}
|
||||
|
||||
RootIi = IfdIdentity{ IfdName: IfdStandard }
|
||||
ExifIi = IfdIdentity{ ParentIfdName: IfdStandard, IfdName: IfdExif }
|
||||
GpsIi = IfdIdentity{ ParentIfdName: IfdStandard, IfdName: IfdGps }
|
||||
ExifIopIi = IfdIdentity{ ParentIfdName: IfdExif, IfdName: IfdIop }
|
||||
|
||||
// Produce a list of unique IDs for each IFD that we can pass around (so we
|
||||
// don't always have to be comparing parent and child names).
|
||||
//
|
||||
// For lack of need, this is just static.
|
||||
//
|
||||
// (0) is reserved for not-found/miss responses.
|
||||
IfdIds = map[IfdIdentity]int {
|
||||
RootIi: 1,
|
||||
ExifIi: 2,
|
||||
GpsIi: 3,
|
||||
ExifIopIi: 4,
|
||||
}
|
||||
|
||||
// tagsWithoutAlignment is a tag-lookup for tags whose value size won't
|
||||
// necessarily be a multiple of its tag-type.
|
||||
tagsWithoutAlignment = map[uint16]struct{} {
|
||||
|
@ -94,44 +43,12 @@ var (
|
|||
}
|
||||
|
||||
tagIndex *TagIndex
|
||||
|
||||
IfdDesignations = map[string]IfdNameAndIndex {
|
||||
"ifd0": { RootIi, 0 },
|
||||
"ifd1": { RootIi, 1 },
|
||||
"exif": { ExifIi, 0 },
|
||||
"gps": { GpsIi, 0 },
|
||||
"iop": { ExifIopIi, 0 },
|
||||
}
|
||||
|
||||
IfdDesignationsR = make(map[IfdNameAndIndex]string)
|
||||
)
|
||||
|
||||
var (
|
||||
tagsLogger = log.NewLogger("exif.tags")
|
||||
)
|
||||
|
||||
func IfdDesignation(ii IfdIdentity, index int) string {
|
||||
if ii == RootIi {
|
||||
return fmt.Sprintf("%s%d", ii.IfdName, index)
|
||||
} else {
|
||||
return ii.IfdName
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type IfdIdentity struct {
|
||||
ParentIfdName string
|
||||
IfdName string
|
||||
}
|
||||
|
||||
func (ii IfdIdentity) String() string {
|
||||
return fmt.Sprintf("IfdIdentity<PARENT-NAME=[%s] NAME=[%s]>", ii.ParentIfdName, ii.IfdName)
|
||||
}
|
||||
|
||||
func (ii IfdIdentity) Id() int {
|
||||
return IfdIdWithIdentityOrFail(ii)
|
||||
}
|
||||
|
||||
|
||||
// File structures.
|
||||
|
||||
|
@ -410,19 +327,5 @@ func init() {
|
|||
assetsPath := path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
|
||||
tagDataFilepath = path.Join(assetsPath, "tags.yaml")
|
||||
|
||||
for ifdName, tags := range IfdTagIds {
|
||||
tagsR := make(map[uint16]string)
|
||||
|
||||
for tagName, tagId := range tags {
|
||||
tagsR[tagId] = tagName
|
||||
}
|
||||
|
||||
IfdTagNames[ifdName] = tagsR
|
||||
}
|
||||
|
||||
for designation, ni := range IfdDesignations {
|
||||
IfdDesignationsR[ni] = designation
|
||||
}
|
||||
|
||||
tagIndex = NewTagIndex()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue