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
Dustin Oprea 2018-06-09 02:11:24 -04:00
parent 1ce5b771db
commit b9537b58c2
9 changed files with 383 additions and 114 deletions

BIN
assets/20180428_212312.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

0
assets/NDM_8901.jpg.exif Executable file → Normal file
View File

0
assets/NDM_8901.jpg.thumbnail Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

37
gps.go Normal file
View File

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

132
ifd.go Normal file
View File

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

View File

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

View File

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

View File

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

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