mirror of
https://github.com/dsoprea/go-exif.git
synced 2025-04-27 13:12:39 +00:00
- These are absolute representations of where an IFD is positioned with respect to the other IFDs. There is a more-specific, "fully-qualified" form of the IFD-path that allows you to express indices in order to refer to specific siblings. - Eliminates issues with IFDs at different levels potentially having the same name (if that's what is required with a certain dataset/ datasource). - There is a specific IFD registry that controls the heirarchy of IFDs that we recognize and the tags associated with the child IFDs. This allows custom hierarchies replacing even the TIFF specification for which are expected in an image (so we can read other types of images or any potential IFD hierarchy, even non-image ones). - IFD and IB instances embed the IFD-path and FQ IFD-path that they were found or built for.
408 lines
8.9 KiB
Go
408 lines
8.9 KiB
Go
package exif
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/dsoprea/go-logging"
|
|
)
|
|
|
|
const (
|
|
// IFD names. The paths that we referred to the IFDs with are comprised of
|
|
// these.
|
|
|
|
IfdStandard = "IFD"
|
|
IfdExif = "Exif"
|
|
IfdGps = "GPSInfo"
|
|
IfdIop = "Iop"
|
|
|
|
// Tag IDs for child IFDs.
|
|
|
|
IfdExifId = 0x8769
|
|
IfdGpsId = 0x8825
|
|
IfdIopId = 0xA005
|
|
|
|
// Just a placeholder.
|
|
|
|
IfdRootId = 0x0000
|
|
|
|
// The paths of the standard IFDs expressed in the standard IFD-mappings
|
|
// and as the group-names in the tag data.
|
|
|
|
IfdPathStandard = "IFD"
|
|
IfdPathStandardExif = "IFD/Exif"
|
|
IfdPathStandardExifIop = "IFD/Exif/Iop"
|
|
IfdPathStandardGps = "IFD/GPSInfo"
|
|
)
|
|
|
|
var (
|
|
ifdLogger = log.NewLogger("exif.ifd")
|
|
)
|
|
|
|
var (
|
|
ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
|
|
)
|
|
|
|
// 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)
|
|
// }
|
|
|
|
type MappedIfd struct {
|
|
ParentTagId uint16
|
|
Placement []uint16
|
|
Path []string
|
|
|
|
Name string
|
|
TagId uint16
|
|
Children map[uint16]*MappedIfd
|
|
}
|
|
|
|
func (mi *MappedIfd) String() string {
|
|
pathPhrase := mi.PathPhrase()
|
|
return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
|
|
}
|
|
|
|
func (mi *MappedIfd) PathPhrase() string {
|
|
return strings.Join(mi.Path, "/")
|
|
}
|
|
|
|
// IfdMapping describes all of the IFDs that we currently recognize.
|
|
type IfdMapping struct {
|
|
rootNode *MappedIfd
|
|
}
|
|
|
|
func NewIfdMapping() (ifdMapping *IfdMapping) {
|
|
rootNode := &MappedIfd{
|
|
Path: make([]string, 0),
|
|
Children: make(map[uint16]*MappedIfd),
|
|
}
|
|
|
|
return &IfdMapping{
|
|
rootNode: rootNode,
|
|
}
|
|
}
|
|
|
|
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err := log.Wrap(state.(error))
|
|
log.Panic(err)
|
|
}
|
|
}()
|
|
|
|
im := NewIfdMapping()
|
|
|
|
err := LoadStandardIfds(im)
|
|
log.PanicIf(err)
|
|
|
|
return im
|
|
}
|
|
|
|
func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ptr := im.rootNode
|
|
for _, tagId := range parentPlacement {
|
|
if descendantPtr, found := ptr.Children[tagId]; found == false {
|
|
log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
|
|
} else {
|
|
ptr = descendantPtr
|
|
}
|
|
}
|
|
|
|
return ptr, nil
|
|
}
|
|
|
|
func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
if pathPhrase == "" {
|
|
log.Panicf("path-phrase is empty")
|
|
}
|
|
|
|
path := strings.Split(pathPhrase, "/")
|
|
ptr := im.rootNode
|
|
|
|
for _, name := range path {
|
|
var hit *MappedIfd
|
|
for _, mi := range ptr.Children {
|
|
if mi.Name == name {
|
|
hit = mi
|
|
break
|
|
}
|
|
}
|
|
|
|
if hit == nil {
|
|
log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
|
|
}
|
|
|
|
ptr = hit
|
|
}
|
|
|
|
return ptr, nil
|
|
}
|
|
|
|
// GetChild is a convenience function to get the child path for a given parent
|
|
// placement and child tag-ID.
|
|
func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
mi, err = im.GetWithPath(parentPathPhrase)
|
|
log.PanicIf(err)
|
|
|
|
for _, childMi := range mi.Children {
|
|
if childMi.TagId == tagId {
|
|
return childMi, nil
|
|
}
|
|
}
|
|
|
|
// Whether or not an IFD is defined in data, such an IFD is not registered
|
|
// and would be unknown.
|
|
log.Panic(ErrChildIfdNotMapped)
|
|
return nil, nil
|
|
}
|
|
|
|
type IfdTagIdAndIndex struct {
|
|
Name string
|
|
TagId uint16
|
|
Index int
|
|
}
|
|
|
|
func (itii IfdTagIdAndIndex) String() string {
|
|
return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
|
|
}
|
|
|
|
// ResolvePath takes a list of names, which can also be suffixed with indices
|
|
// (to identify the second, third, etc.. sibling IFD) and returns a list of
|
|
// tag-IDs and those indices.
|
|
//
|
|
// Example:
|
|
//
|
|
// - IFD/Exif/Iop
|
|
// - IFD0/Exif/Iop
|
|
//
|
|
// This is the only call that supports adding the numeric indices.
|
|
func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
pathPhrase = strings.TrimSpace(pathPhrase)
|
|
|
|
if pathPhrase == "" {
|
|
log.Panicf("can not resolve empty path-phrase")
|
|
}
|
|
|
|
path := strings.Split(pathPhrase, "/")
|
|
lineage = make([]IfdTagIdAndIndex, len(path))
|
|
|
|
ptr := im.rootNode
|
|
empty := IfdTagIdAndIndex{}
|
|
for i, name := range path {
|
|
indexByte := name[len(name)-1]
|
|
index := 0
|
|
if indexByte >= '0' && indexByte <= '9' {
|
|
index = int(indexByte - '0')
|
|
name = name[:len(name)-1]
|
|
}
|
|
|
|
itii := IfdTagIdAndIndex{}
|
|
for _, mi := range ptr.Children {
|
|
if mi.Name != name {
|
|
continue
|
|
}
|
|
|
|
itii.Name = name
|
|
itii.TagId = mi.TagId
|
|
itii.Index = index
|
|
|
|
ptr = mi
|
|
|
|
break
|
|
}
|
|
|
|
if itii == empty {
|
|
log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
|
|
}
|
|
|
|
lineage[i] = itii
|
|
}
|
|
|
|
return lineage, nil
|
|
}
|
|
|
|
func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
|
|
fqPathParts := make([]string, len(lineage))
|
|
for i, itii := range lineage {
|
|
if itii.Index > 0 {
|
|
fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
|
|
} else {
|
|
fqPathParts[i] = itii.Name
|
|
}
|
|
}
|
|
|
|
return strings.Join(fqPathParts, "/")
|
|
}
|
|
|
|
func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
|
|
pathParts := make([]string, len(lineage))
|
|
for i, itii := range lineage {
|
|
pathParts[i] = itii.Name
|
|
}
|
|
|
|
return strings.Join(pathParts, "/")
|
|
}
|
|
|
|
// StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
|
|
// indices).
|
|
func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
lineage, err := im.ResolvePath(pathPhrase)
|
|
log.PanicIf(err)
|
|
|
|
strippedPathPhrase = im.PathPhraseFromLineage(lineage)
|
|
return strippedPathPhrase, nil
|
|
}
|
|
|
|
// Add puts the given IFD at the given position of the tree. The position of the
|
|
// tree is referred to as the placement and is represented by a set of tag-IDs,
|
|
// where the leftmost is the root tag and the tags going to the right are
|
|
// progressive descendants.
|
|
func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
|
|
|
|
ptr, err := im.Get(parentPlacement)
|
|
log.PanicIf(err)
|
|
|
|
path := make([]string, len(parentPlacement)+1)
|
|
if len(parentPlacement) > 0 {
|
|
copy(path, ptr.Path)
|
|
}
|
|
|
|
path[len(path)-1] = name
|
|
|
|
placement := make([]uint16, len(parentPlacement)+1)
|
|
if len(placement) > 0 {
|
|
copy(placement, ptr.Placement)
|
|
}
|
|
|
|
placement[len(placement)-1] = tagId
|
|
|
|
childIfd := &MappedIfd{
|
|
ParentTagId: ptr.TagId,
|
|
Path: path,
|
|
Placement: placement,
|
|
Name: name,
|
|
TagId: tagId,
|
|
Children: make(map[uint16]*MappedIfd),
|
|
}
|
|
|
|
if _, found := ptr.Children[tagId]; found == true {
|
|
log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
|
|
}
|
|
|
|
ptr.Children[tagId] = childIfd
|
|
|
|
return nil
|
|
}
|
|
|
|
func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
currentIfd := stack[len(stack)-1]
|
|
|
|
output = input
|
|
for _, childIfd := range currentIfd.Children {
|
|
stackCopy := make([]*MappedIfd, len(stack)+1)
|
|
|
|
copy(stackCopy, stack)
|
|
stackCopy[len(stack)] = childIfd
|
|
|
|
// Add to output, but don't include the obligatory root node.
|
|
parts := make([]string, len(stackCopy)-1)
|
|
for i, mi := range stackCopy[1:] {
|
|
parts[i] = mi.Name
|
|
}
|
|
|
|
output = append(output, strings.Join(parts, "/"))
|
|
|
|
output, err = im.dumpLineages(stackCopy, output)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func (im *IfdMapping) DumpLineages() (output []string, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
stack := []*MappedIfd{im.rootNode}
|
|
output = make([]string, 0)
|
|
|
|
output, err = im.dumpLineages(stack, output)
|
|
log.PanicIf(err)
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func LoadStandardIfds(im *IfdMapping) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
err = im.Add([]uint16{}, IfdRootId, IfdStandard)
|
|
log.PanicIf(err)
|
|
|
|
err = im.Add([]uint16{IfdRootId}, IfdExifId, IfdExif)
|
|
log.PanicIf(err)
|
|
|
|
err = im.Add([]uint16{IfdRootId, IfdExifId}, IfdIopId, IfdIop)
|
|
log.PanicIf(err)
|
|
|
|
err = im.Add([]uint16{IfdRootId}, IfdGpsId, IfdGps)
|
|
log.PanicIf(err)
|
|
|
|
return nil
|
|
}
|