mirror of https://github.com/dsoprea/go-exif.git
385 lines
9.3 KiB
Go
385 lines
9.3 KiB
Go
package exif
|
|
|
|
import (
|
|
"os"
|
|
"path"
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
"github.com/dsoprea/go-logging"
|
|
)
|
|
|
|
const (
|
|
IfdStandard = "IFD"
|
|
|
|
IfdExif = "Exif"
|
|
IfdGps = "GPSInfo"
|
|
IfdIop = "Iop"
|
|
|
|
IfdExifId = 0x8769
|
|
IfdGpsId = 0x8825
|
|
IfdIopId = 0xA005
|
|
)
|
|
|
|
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,
|
|
}
|
|
)
|
|
|
|
var (
|
|
tagsLogger = log.NewLogger("exif.tags")
|
|
)
|
|
|
|
|
|
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.
|
|
|
|
type encodedTag struct {
|
|
// id is signed, here, because YAML doesn't have enough information to
|
|
// support unsigned.
|
|
Id int `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
TypeName string `yaml:"type_name"`
|
|
}
|
|
|
|
|
|
// Indexing structures.
|
|
|
|
type IndexedTag struct {
|
|
Id uint16
|
|
Name string
|
|
Ifd string
|
|
Type uint16
|
|
}
|
|
|
|
func (it IndexedTag) String() string {
|
|
return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.Ifd)
|
|
}
|
|
|
|
func (it IndexedTag) IsName(ifd, name string) bool {
|
|
return it.Name == name && it.Ifd == ifd
|
|
}
|
|
|
|
func (it IndexedTag) Is(ifd string, id uint16) bool {
|
|
return it.Id == id && it.Ifd == ifd
|
|
}
|
|
|
|
type TagIndex struct {
|
|
tagsByIfd map[string]map[uint16]*IndexedTag
|
|
tagsByIfdR map[string]map[string]*IndexedTag
|
|
}
|
|
|
|
func NewTagIndex() *TagIndex {
|
|
ti := new(TagIndex)
|
|
|
|
err := ti.load()
|
|
log.PanicIf(err)
|
|
|
|
return ti
|
|
}
|
|
|
|
func (ti *TagIndex) load() (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
|
|
// Read static data.
|
|
|
|
// TODO(dustin): !! Load this from init() so we're not doing it every time.
|
|
f, err := os.Open(tagDataFilepath)
|
|
log.PanicIf(err)
|
|
|
|
d := yaml.NewDecoder(f)
|
|
|
|
encodedIfds := make(map[string][]encodedTag)
|
|
|
|
err = d.Decode(encodedIfds)
|
|
log.PanicIf(err)
|
|
|
|
|
|
// Load structure.
|
|
|
|
tagsByIfd := make(map[string]map[uint16]*IndexedTag)
|
|
tagsByIfdR := make(map[string]map[string]*IndexedTag)
|
|
|
|
count := 0
|
|
for ifdName, tags := range encodedIfds {
|
|
for _, tagInfo := range tags {
|
|
tagId := uint16(tagInfo.Id)
|
|
tagName := tagInfo.Name
|
|
tagTypeName := tagInfo.TypeName
|
|
|
|
// TODO(dustin): !! Non-standard types, but found in real data. Ignore for right now.
|
|
if tagTypeName == "SSHORT" || tagTypeName == "FLOAT" || tagTypeName == "DOUBLE" {
|
|
continue
|
|
}
|
|
|
|
tagTypeId, found := TypeNamesR[tagTypeName]
|
|
if found == false {
|
|
log.Panicf("type [%s] for [%s] not valid", tagTypeName, tagName)
|
|
continue
|
|
}
|
|
|
|
tag := &IndexedTag{
|
|
Ifd: ifdName,
|
|
Id: tagId,
|
|
Name: tagName,
|
|
Type: tagTypeId,
|
|
}
|
|
|
|
|
|
// Store by ID.
|
|
|
|
family, found := tagsByIfd[ifdName]
|
|
if found == false {
|
|
family = make(map[uint16]*IndexedTag)
|
|
tagsByIfd[ifdName] = family
|
|
}
|
|
|
|
if _, found := family[tagId]; found == true {
|
|
log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", ifdName, tagId)
|
|
}
|
|
|
|
family[tagId] = tag
|
|
|
|
|
|
// Store by name.
|
|
|
|
familyR, found := tagsByIfdR[ifdName]
|
|
if found == false {
|
|
familyR = make(map[string]*IndexedTag)
|
|
tagsByIfdR[ifdName] = familyR
|
|
}
|
|
|
|
if _, found := familyR[tagName]; found == true {
|
|
log.Panicf("tag-name defined more than once for IFD [%s]: (%s)", ifdName, tagName)
|
|
}
|
|
|
|
familyR[tagName] = tag
|
|
|
|
count++
|
|
}
|
|
}
|
|
|
|
ti.tagsByIfd = tagsByIfd
|
|
ti.tagsByIfdR = tagsByIfdR
|
|
|
|
tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get returns information about the non-IFD tag.
|
|
func (ti *TagIndex) Get(ii IfdIdentity, id uint16) (it *IndexedTag, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
family, found := ti.tagsByIfd[ii.IfdName]
|
|
if found == false {
|
|
log.Panic(ErrTagNotFound)
|
|
}
|
|
|
|
it, found = family[id]
|
|
if found == false {
|
|
log.Panic(ErrTagNotFound)
|
|
}
|
|
|
|
return it, nil
|
|
}
|
|
|
|
// Get returns information about the non-IFD tag.
|
|
func (ti *TagIndex) GetWithName(ii IfdIdentity, name string) (it *IndexedTag, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
it, found := ti.tagsByIfdR[ii.IfdName][name]
|
|
if found != true {
|
|
log.Panic(ErrTagNotFound)
|
|
}
|
|
|
|
return it, nil
|
|
}
|
|
|
|
// IfdTagWithId returns true if the given tag points to a child IFD block.
|
|
func IfdTagNameWithIdOrFail(parentIfdName string, tagId uint16) string {
|
|
name, found := IfdTagNameWithId(parentIfdName, tagId)
|
|
if found == false {
|
|
log.Panicf("tag-ID (0x%02x) under parent IFD [%s] not associated with a child IFD", tagId, parentIfdName)
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
// IfdTagWithId returns true if the given tag points to a child IFD block.
|
|
|
|
// TODO(dustin): !! Rewrite to take an IfdIdentity, instead. We shouldn't expect that IFD names are globally unique.
|
|
|
|
func IfdTagNameWithId(parentIfdName string, tagId uint16) (name string, found bool) {
|
|
if tags, found := IfdTagNames[parentIfdName]; found == true {
|
|
if name, found = tags[tagId]; found == true {
|
|
return name, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// IfdTagIdWithIdentity returns true if the given tag points to a child IFD
|
|
// block.
|
|
func IfdTagIdWithIdentity(ii IfdIdentity) (tagId uint16, found bool) {
|
|
if tags, found := IfdTagIds[ii.ParentIfdName]; found == true {
|
|
if tagId, found = tags[ii.IfdName]; found == true {
|
|
return tagId, true
|
|
}
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
func IfdTagIdWithIdentityOrFail(ii IfdIdentity) (tagId uint16) {
|
|
if tags, found := IfdTagIds[ii.ParentIfdName]; found == true {
|
|
if tagId, found = tags[ii.IfdName]; found == true {
|
|
if tagId == 0 {
|
|
// This IFD is not the type that can be linked to from a tag.
|
|
log.Panicf("not a child IFD")
|
|
}
|
|
|
|
return tagId
|
|
}
|
|
}
|
|
|
|
log.Panicf("no tag for invalid IFD identity")
|
|
return 0
|
|
}
|
|
|
|
func IfdIdWithIdentity(ii IfdIdentity) int {
|
|
id, _ := IfdIds[ii]
|
|
return id
|
|
}
|
|
|
|
func IfdIdWithIdentityOrFail(ii IfdIdentity) int {
|
|
id, _ := IfdIds[ii]
|
|
if id == 0 {
|
|
log.Panicf("IFD not valid: %v", ii)
|
|
}
|
|
|
|
return id
|
|
}
|
|
|
|
func IfdIdOrFail(parentIfdName, ifdName string) (ii IfdIdentity, id int) {
|
|
ii, id = IfdId(parentIfdName, ifdName)
|
|
if id == 0 {
|
|
log.Panicf("IFD is not valid: [%s] [%s]", parentIfdName, ifdName)
|
|
}
|
|
|
|
return ii, id
|
|
}
|
|
|
|
func IfdId(parentIfdName, ifdName string) (ii IfdIdentity, id int) {
|
|
ii = IfdIdentity{
|
|
ParentIfdName: parentIfdName,
|
|
IfdName: ifdName,
|
|
}
|
|
|
|
id, found := IfdIds[ii]
|
|
if found != true {
|
|
return IfdIdentity{}, 0
|
|
}
|
|
|
|
return ii, id
|
|
}
|
|
|
|
func init() {
|
|
goPath := os.Getenv("GOPATH")
|
|
if goPath == "" {
|
|
log.Panicf("GOPATH is empty")
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|