mirror of
https://github.com/dsoprea/go-exif.git
synced 2025-05-31 11:41:57 +00:00
We now keep track of the further offset that we have accessed (though not necessarily the furthest offset of data that is unparseable by us). It sometimes supports debugging efforts to have a ballpark idea of how much EXIF data there is, rather than just "from the signature to the end of the file" or however big the EXIF container is in the enclosing image format. The latter can be buggy or ambiguous.
1336 lines
33 KiB
Go
1336 lines
33 KiB
Go
package exif
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"encoding/binary"
|
|
|
|
"github.com/dsoprea/go-logging"
|
|
|
|
"github.com/dsoprea/go-exif/v2/common"
|
|
"github.com/dsoprea/go-exif/v2/undefined"
|
|
)
|
|
|
|
var (
|
|
ifdEnumerateLogger = log.NewLogger("exif.ifd_enumerate")
|
|
)
|
|
|
|
var (
|
|
// ErrNoThumbnail means that no thumbnail was found.
|
|
ErrNoThumbnail = errors.New("no thumbnail")
|
|
|
|
// ErrNoGpsTags means that no GPS info was found.
|
|
ErrNoGpsTags = errors.New("no gps tags")
|
|
|
|
// ErrTagTypeNotValid means that the tag-type is not valid.
|
|
ErrTagTypeNotValid = errors.New("tag type invalid")
|
|
|
|
// ErrOffsetInvalid means that the file offset is not valid.
|
|
ErrOffsetInvalid = errors.New("file offset invalid")
|
|
)
|
|
|
|
var (
|
|
ValidGpsVersions = [][4]byte{
|
|
// 2.0.0.0 appears to have a very similar format to 2.2.0.0, so enabling
|
|
// it under that assumption.
|
|
//
|
|
// IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[114]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 00 00 00]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0001) NAME=[GPSLatitudeRef] COUNT=(2) TYPE=[ASCII] VALUE=[S]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0002) NAME=[GPSLatitude] COUNT=(3) TYPE=[RATIONAL] VALUE=[38/1...]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0003) NAME=[GPSLongitudeRef] COUNT=(2) TYPE=[ASCII] VALUE=[E]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0004) NAME=[GPSLongitude] COUNT=(3) TYPE=[RATIONAL] VALUE=[144/1...]
|
|
// IFD-PATH=[IFD/GPSInfo] ID=(0x0012) NAME=[GPSMapDatum] COUNT=(7) TYPE=[ASCII] VALUE=[WGS-84]
|
|
//
|
|
{2, 0, 0, 0},
|
|
|
|
{2, 2, 0, 0},
|
|
|
|
// Suddenly appeared at the default in 2.31: https://home.jeita.or.jp/tsc/std-pdf/CP-3451D.pdf
|
|
//
|
|
// Note that the presence of 2.3.0.0 doesn't seem to guarantee
|
|
// coordinates. In some cases, we seen just the following:
|
|
//
|
|
// GPS Tag Version |2.3.0.0
|
|
// GPS Receiver Status |V
|
|
// Geodetic Survey Data|WGS-84
|
|
// GPS Differential Cor|0
|
|
//
|
|
{2, 3, 0, 0},
|
|
}
|
|
)
|
|
|
|
// byteParser knows how to decode an IFD and all of the tags it
|
|
// describes.
|
|
//
|
|
// The IFDs and the actual values can float throughout the EXIF block, but the
|
|
// IFD itself is just a minor header followed by a set of repeating,
|
|
// statically-sized records. So, the tags (though notnecessarily their values)
|
|
// are fairly simple to enumerate.
|
|
type byteParser struct {
|
|
byteOrder binary.ByteOrder
|
|
addressableData []byte
|
|
ifdOffset uint32
|
|
buffer *bytes.Buffer
|
|
currentOffset uint32
|
|
}
|
|
|
|
func newByteParser(addressableData []byte, byteOrder binary.ByteOrder, ifdOffset uint32) (bp *byteParser, err error) {
|
|
if ifdOffset >= uint32(len(addressableData)) {
|
|
return nil, ErrOffsetInvalid
|
|
}
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
bp = &byteParser{
|
|
addressableData: addressableData,
|
|
byteOrder: byteOrder,
|
|
buffer: bytes.NewBuffer(addressableData[ifdOffset:]),
|
|
currentOffset: ifdOffset,
|
|
}
|
|
|
|
return bp, nil
|
|
}
|
|
|
|
// getUint16 reads a uint16 and advances both our current and our current
|
|
// accumulator (which allows us to know how far to seek to the beginning of the
|
|
// next IFD when it's time to jump).
|
|
func (bp *byteParser) getUint16() (value uint16, raw []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
needBytes := 2
|
|
offset := 0
|
|
raw = make([]byte, needBytes)
|
|
|
|
for offset < needBytes {
|
|
n, err := bp.buffer.Read(raw[offset:])
|
|
log.PanicIf(err)
|
|
|
|
offset += n
|
|
}
|
|
|
|
value = bp.byteOrder.Uint16(raw)
|
|
|
|
bp.currentOffset += uint32(needBytes)
|
|
|
|
return value, raw, nil
|
|
}
|
|
|
|
// getUint32 reads a uint32 and advances both our current and our current
|
|
// accumulator (which allows us to know how far to seek to the beginning of the
|
|
// next IFD when it's time to jump).
|
|
func (bp *byteParser) getUint32() (value uint32, raw []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
needBytes := 4
|
|
offset := 0
|
|
raw = make([]byte, needBytes)
|
|
|
|
for offset < needBytes {
|
|
n, err := bp.buffer.Read(raw[offset:])
|
|
log.PanicIf(err)
|
|
|
|
offset += n
|
|
}
|
|
|
|
value = bp.byteOrder.Uint32(raw)
|
|
|
|
bp.currentOffset += uint32(needBytes)
|
|
|
|
return value, raw, nil
|
|
}
|
|
|
|
// CurrentOffset returns the starting offset but the number of bytes that we
|
|
// have parsed. This is arithmetic-based tracking, not a seek(0) operation.
|
|
func (bp *byteParser) CurrentOffset() uint32 {
|
|
return bp.currentOffset
|
|
}
|
|
|
|
type IfdEnumerate struct {
|
|
exifData []byte
|
|
buffer *bytes.Buffer
|
|
byteOrder binary.ByteOrder
|
|
tagIndex *TagIndex
|
|
ifdMapping *IfdMapping
|
|
furthestOffset uint32
|
|
}
|
|
|
|
func NewIfdEnumerate(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, byteOrder binary.ByteOrder) *IfdEnumerate {
|
|
return &IfdEnumerate{
|
|
exifData: exifData,
|
|
buffer: bytes.NewBuffer(exifData),
|
|
byteOrder: byteOrder,
|
|
ifdMapping: ifdMapping,
|
|
tagIndex: tagIndex,
|
|
}
|
|
}
|
|
|
|
func (ie *IfdEnumerate) getByteParser(ifdOffset uint32) (bp *byteParser, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
bp, err =
|
|
newByteParser(
|
|
ie.exifData[ExifAddressableAreaStart:],
|
|
ie.byteOrder,
|
|
ifdOffset)
|
|
|
|
if err != nil {
|
|
if err == ErrOffsetInvalid {
|
|
return nil, err
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
return bp, nil
|
|
}
|
|
|
|
func (ie *IfdEnumerate) parseTag(fqIfdPath string, tagPosition int, bp *byteParser) (ite *IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
tagId, _, err := bp.getUint16()
|
|
log.PanicIf(err)
|
|
|
|
tagTypeRaw, _, err := bp.getUint16()
|
|
log.PanicIf(err)
|
|
|
|
tagType := exifcommon.TagTypePrimitive(tagTypeRaw)
|
|
|
|
unitCount, _, err := bp.getUint32()
|
|
log.PanicIf(err)
|
|
|
|
valueOffset, rawValueOffset, err := bp.getUint32()
|
|
log.PanicIf(err)
|
|
|
|
if tagType.IsValid() == false {
|
|
ite = &IfdTagEntry{
|
|
tagType: tagType,
|
|
}
|
|
|
|
log.Panic(ErrTagTypeNotValid)
|
|
}
|
|
|
|
ifdPath, err := ie.ifdMapping.StripPathPhraseIndices(fqIfdPath)
|
|
log.PanicIf(err)
|
|
|
|
ite = newIfdTagEntry(
|
|
ifdPath,
|
|
tagId,
|
|
tagPosition,
|
|
tagType,
|
|
unitCount,
|
|
valueOffset,
|
|
rawValueOffset,
|
|
ie.exifData[ExifAddressableAreaStart:],
|
|
ie.byteOrder)
|
|
|
|
// If it's an IFD but not a standard one, it'll just be seen as a LONG
|
|
// (the standard IFD tag type), later, unless we skip it because it's
|
|
// [likely] not even in the standard list of known tags.
|
|
mi, err := ie.ifdMapping.GetChild(ifdPath, tagId)
|
|
if err == nil {
|
|
ite.SetChildIfd(
|
|
fmt.Sprintf("%s/%s", fqIfdPath, mi.Name),
|
|
mi.PathPhrase(),
|
|
mi.Name)
|
|
|
|
// We also need to set `tag.ChildFqIfdPath` but can't do it here
|
|
// because we don't have the IFD index.
|
|
} else if log.Is(err, ErrChildIfdNotMapped) == false {
|
|
log.Panic(err)
|
|
}
|
|
|
|
return ite, nil
|
|
}
|
|
|
|
// TagVisitorFn is called for each tag when enumerating through the EXIF.
|
|
type TagVisitorFn func(fqIfdPath string, ifdIndex int, ite *IfdTagEntry) (err error)
|
|
|
|
// ParseIfd decodes the IFD block that we're currently sitting on the first
|
|
// byte of.
|
|
func (ie *IfdEnumerate) ParseIfd(fqIfdPath string, ifdIndex int, bp *byteParser, visitor TagVisitorFn, doDescend bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
tagCount, _, err := bp.getUint16()
|
|
log.PanicIf(err)
|
|
|
|
ifdEnumerateLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount)
|
|
|
|
entries = make([]*IfdTagEntry, 0)
|
|
|
|
var enumeratorThumbnailOffset *IfdTagEntry
|
|
var enumeratorThumbnailSize *IfdTagEntry
|
|
|
|
for i := 0; i < int(tagCount); i++ {
|
|
ite, err := ie.parseTag(fqIfdPath, i, bp)
|
|
if err != nil {
|
|
if log.Is(err, ErrTagTypeNotValid) == true {
|
|
ifdEnumerateLogger.Warningf(nil, "Tag in IFD [%s] at position (%d) has invalid type (%d) and will be skipped.", fqIfdPath, i, ite.tagType)
|
|
continue
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
tagId := ite.TagId()
|
|
if tagId == ThumbnailOffsetTagId {
|
|
enumeratorThumbnailOffset = ite
|
|
|
|
continue
|
|
} else if tagId == ThumbnailSizeTagId {
|
|
enumeratorThumbnailSize = ite
|
|
continue
|
|
}
|
|
|
|
if visitor != nil {
|
|
err := visitor(fqIfdPath, ifdIndex, ite)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
if ite.TagType() != exifcommon.TypeUndefined {
|
|
// If this tag's value is an offset, bump our max-offset value to
|
|
// what that offset is plus however large that value is.
|
|
|
|
vc := ite.getValueContext()
|
|
|
|
farOffset, err := vc.GetFarOffset()
|
|
if err == nil {
|
|
candidateOffset := farOffset + uint32(vc.SizeInBytes())
|
|
if candidateOffset > ie.furthestOffset {
|
|
ie.furthestOffset = candidateOffset
|
|
}
|
|
} else if err != exifcommon.ErrNotFarValue {
|
|
log.PanicIf(err)
|
|
}
|
|
}
|
|
|
|
// If it's an IFD but not a standard one, it'll just be seen as a LONG
|
|
// (the standard IFD tag type), later, unless we skip it because it's
|
|
// [likely] not even in the standard list of known tags.
|
|
if ite.ChildIfdPath() != "" {
|
|
if doDescend == true {
|
|
ifdEnumerateLogger.Debugf(nil, "Descending to IFD [%s].", ite.ChildIfdPath())
|
|
|
|
err := ie.scan(ite.ChildFqIfdPath(), ite.getValueOffset(), visitor)
|
|
log.PanicIf(err)
|
|
}
|
|
}
|
|
|
|
entries = append(entries, ite)
|
|
}
|
|
|
|
if enumeratorThumbnailOffset != nil && enumeratorThumbnailSize != nil {
|
|
thumbnailData, err = ie.parseThumbnail(enumeratorThumbnailOffset, enumeratorThumbnailSize)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
nextIfdOffset, _, err = bp.getUint32()
|
|
log.PanicIf(err)
|
|
|
|
ifdEnumerateLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset)
|
|
|
|
return nextIfdOffset, entries, thumbnailData, nil
|
|
}
|
|
|
|
func (ie *IfdEnumerate) parseThumbnail(offsetIte, lengthIte *IfdTagEntry) (thumbnailData []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
vRaw, err := lengthIte.Value()
|
|
log.PanicIf(err)
|
|
|
|
vList := vRaw.([]uint32)
|
|
if len(vList) != 1 {
|
|
log.Panicf("not exactly one long: (%d)", len(vList))
|
|
}
|
|
|
|
length := vList[0]
|
|
|
|
// The tag is official a LONG type, but it's actually an offset to a blob of bytes.
|
|
offsetIte.updateTagType(exifcommon.TypeByte)
|
|
offsetIte.updateUnitCount(length)
|
|
|
|
thumbnailData, err = offsetIte.GetRawBytes()
|
|
log.PanicIf(err)
|
|
|
|
return thumbnailData, nil
|
|
}
|
|
|
|
// Scan enumerates the different IFD blocks.
|
|
func (ie *IfdEnumerate) scan(fqIfdName string, ifdOffset uint32, visitor TagVisitorFn) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
for ifdIndex := 0; ; ifdIndex++ {
|
|
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (0x%04x) (scan).", fqIfdName, ifdIndex, ifdOffset)
|
|
|
|
bp, err := ie.getByteParser(ifdOffset)
|
|
if err != nil {
|
|
if err == ErrOffsetInvalid {
|
|
ifdEnumerateLogger.Errorf(nil, nil, "IFD [%s] (%d) at offset (0x%04x) is unreachable. Terminating scan.", fqIfdName, ifdIndex, ifdOffset)
|
|
break
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
nextIfdOffset, _, _, err := ie.ParseIfd(fqIfdName, ifdIndex, bp, visitor, true)
|
|
log.PanicIf(err)
|
|
|
|
currentOffset := bp.CurrentOffset()
|
|
if currentOffset > ie.furthestOffset {
|
|
ie.furthestOffset = currentOffset
|
|
}
|
|
|
|
if nextIfdOffset == 0 {
|
|
break
|
|
}
|
|
|
|
ifdOffset = nextIfdOffset
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Scan enumerates the different EXIF blocks (called IFDs). `rootIfdName` will
|
|
// be "IFD" in the TIFF standard.
|
|
func (ie *IfdEnumerate) Scan(rootIfdName string, ifdOffset uint32, visitor TagVisitorFn) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
err = ie.scan(rootIfdName, ifdOffset, visitor)
|
|
log.PanicIf(err)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Ifd represents a single parsed IFD.
|
|
type Ifd struct {
|
|
|
|
// TODO(dustin): !! Why are all of these exported? Stop doing this in the next release.
|
|
// TODO(dustin): Add NextIfd().
|
|
|
|
ByteOrder binary.ByteOrder
|
|
|
|
// Name is the name of the IFD (the rightmost name in the path, sans any
|
|
// indices).
|
|
Name string
|
|
|
|
// IfdPath is a simple IFD path (e.g. IFD/GPSInfo). No indices.
|
|
IfdPath string
|
|
|
|
// FqIfdPath is a fully-qualified IFD path (e.g. IFD0/GPSInfo0). With
|
|
// indices.
|
|
FqIfdPath string
|
|
|
|
TagId uint16
|
|
|
|
Id int
|
|
|
|
ParentIfd *Ifd
|
|
|
|
// ParentTagIndex is our tag position in the parent IFD, if we had a parent
|
|
// (if `ParentIfd` is not nil and we weren't an IFD referenced as a sibling
|
|
// instead of as a child).
|
|
ParentTagIndex int
|
|
|
|
// Name string
|
|
Index int
|
|
Offset uint32
|
|
|
|
Entries []*IfdTagEntry
|
|
EntriesByTagId map[uint16][]*IfdTagEntry
|
|
|
|
Children []*Ifd
|
|
|
|
ChildIfdIndex map[string]*Ifd
|
|
|
|
NextIfdOffset uint32
|
|
NextIfd *Ifd
|
|
|
|
thumbnailData []byte
|
|
|
|
ifdMapping *IfdMapping
|
|
tagIndex *TagIndex
|
|
}
|
|
|
|
func (ifd *Ifd) ChildWithIfdPath(ifdPath string) (childIfd *Ifd, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
for _, childIfd := range ifd.Children {
|
|
if childIfd.IfdPath == ifdPath {
|
|
return childIfd, nil
|
|
}
|
|
}
|
|
|
|
log.Panic(ErrTagNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
// FindTagWithId returns a list of tags (usually just zero or one) that match
|
|
// the given tag ID. This is efficient.
|
|
func (ifd *Ifd) FindTagWithId(tagId uint16) (results []*IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
results, found := ifd.EntriesByTagId[tagId]
|
|
if found != true {
|
|
log.Panic(ErrTagNotFound)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// FindTagWithName returns a list of tags (usually just zero or one) that match
|
|
// the given tag name. This is not efficient (though the labor is trivial).
|
|
func (ifd *Ifd) FindTagWithName(tagName string) (results []*IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
it, err := ifd.tagIndex.GetWithName(ifd.IfdPath, tagName)
|
|
if log.Is(err, ErrTagNotFound) == true {
|
|
log.Panic(ErrTagNotStandard)
|
|
} else if err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
results = make([]*IfdTagEntry, 0)
|
|
for _, ite := range ifd.Entries {
|
|
if ite.TagId() == it.Id {
|
|
results = append(results, ite)
|
|
}
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
log.Panic(ErrTagNotFound)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (ifd *Ifd) String() string {
|
|
parentOffset := uint32(0)
|
|
if ifd.ParentIfd != nil {
|
|
parentOffset = ifd.ParentIfd.Offset
|
|
}
|
|
|
|
return fmt.Sprintf("Ifd<ID=(%d) IFD-PATH=[%s] INDEX=(%d) COUNT=(%d) OFF=(0x%04x) CHILDREN=(%d) PARENT=(0x%04x) NEXT-IFD=(0x%04x)>", ifd.Id, ifd.IfdPath, ifd.Index, len(ifd.Entries), ifd.Offset, len(ifd.Children), parentOffset, ifd.NextIfdOffset)
|
|
}
|
|
|
|
func (ifd *Ifd) Thumbnail() (data []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
if ifd.thumbnailData == nil {
|
|
log.Panic(ErrNoThumbnail)
|
|
}
|
|
|
|
return ifd.thumbnailData, nil
|
|
}
|
|
|
|
// dumpTags recursively builds a list of tags from an IFD.
|
|
func (ifd *Ifd) dumpTags(tags []*IfdTagEntry) []*IfdTagEntry {
|
|
if tags == nil {
|
|
tags = make([]*IfdTagEntry, 0)
|
|
}
|
|
|
|
// Now, print the tags while also descending to child-IFDS as we encounter them.
|
|
|
|
ifdsFoundCount := 0
|
|
|
|
for _, ite := range ifd.Entries {
|
|
tags = append(tags, ite)
|
|
|
|
childIfdPath := ite.ChildIfdPath()
|
|
if childIfdPath != "" {
|
|
ifdsFoundCount++
|
|
|
|
childIfd, found := ifd.ChildIfdIndex[childIfdPath]
|
|
if found != true {
|
|
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
|
|
}
|
|
|
|
tags = childIfd.dumpTags(tags)
|
|
}
|
|
}
|
|
|
|
if len(ifd.Children) != ifdsFoundCount {
|
|
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
|
|
}
|
|
|
|
if ifd.NextIfd != nil {
|
|
tags = ifd.NextIfd.dumpTags(tags)
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
// DumpTags prints the IFD hierarchy.
|
|
func (ifd *Ifd) DumpTags() []*IfdTagEntry {
|
|
return ifd.dumpTags(nil)
|
|
}
|
|
|
|
func (ifd *Ifd) printTagTree(populateValues bool, index, level int, nextLink bool) {
|
|
indent := strings.Repeat(" ", level*2)
|
|
|
|
prefix := " "
|
|
if nextLink {
|
|
prefix = ">"
|
|
}
|
|
|
|
fmt.Printf("%s%sIFD: %s\n", indent, prefix, ifd)
|
|
|
|
// Now, print the tags while also descending to child-IFDS as we encounter them.
|
|
|
|
ifdsFoundCount := 0
|
|
|
|
for _, ite := range ifd.Entries {
|
|
if ite.ChildIfdPath() != "" {
|
|
fmt.Printf("%s - TAG: %s\n", indent, ite)
|
|
} else {
|
|
it, err := ifd.tagIndex.Get(ifd.IfdPath, ite.TagId())
|
|
|
|
tagName := ""
|
|
if err == nil {
|
|
tagName = it.Name
|
|
}
|
|
|
|
var valuePhrase string
|
|
if populateValues == true {
|
|
var err error
|
|
|
|
valuePhrase, err = ite.Format()
|
|
if err != nil {
|
|
if log.Is(err, exifcommon.ErrUnhandledUndefinedTypedTag) == true {
|
|
ifdEnumerateLogger.Warningf(nil, "Skipping non-standard undefined tag: [%s] (%04x)", ifd.IfdPath, ite.TagId())
|
|
continue
|
|
} else if err == exifundefined.ErrUnparseableValue {
|
|
ifdEnumerateLogger.Warningf(nil, "Skipping unparseable undefined tag: [%s] (%04x) [%s]", ifd.IfdPath, ite.TagId(), it.Name)
|
|
continue
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
} else {
|
|
valuePhrase = "!UNRESOLVED"
|
|
}
|
|
|
|
fmt.Printf("%s - TAG: %s NAME=[%s] VALUE=[%v]\n", indent, ite, tagName, valuePhrase)
|
|
}
|
|
|
|
childIfdPath := ite.ChildIfdPath()
|
|
if childIfdPath != "" {
|
|
ifdsFoundCount++
|
|
|
|
childIfd, found := ifd.ChildIfdIndex[childIfdPath]
|
|
if found != true {
|
|
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
|
|
}
|
|
|
|
childIfd.printTagTree(populateValues, 0, level+1, false)
|
|
}
|
|
}
|
|
|
|
if len(ifd.Children) != ifdsFoundCount {
|
|
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
|
|
}
|
|
|
|
if ifd.NextIfd != nil {
|
|
ifd.NextIfd.printTagTree(populateValues, index+1, level, true)
|
|
}
|
|
}
|
|
|
|
// PrintTagTree prints the IFD hierarchy.
|
|
func (ifd *Ifd) PrintTagTree(populateValues bool) {
|
|
ifd.printTagTree(populateValues, 0, 0, false)
|
|
}
|
|
|
|
func (ifd *Ifd) printIfdTree(level int, nextLink bool) {
|
|
indent := strings.Repeat(" ", level*2)
|
|
|
|
prefix := " "
|
|
if nextLink {
|
|
prefix = ">"
|
|
}
|
|
|
|
fmt.Printf("%s%s%s\n", indent, prefix, ifd)
|
|
|
|
// Now, print the tags while also descending to child-IFDS as we encounter them.
|
|
|
|
ifdsFoundCount := 0
|
|
|
|
for _, ite := range ifd.Entries {
|
|
childIfdPath := ite.ChildIfdPath()
|
|
if childIfdPath != "" {
|
|
ifdsFoundCount++
|
|
|
|
childIfd, found := ifd.ChildIfdIndex[childIfdPath]
|
|
if found != true {
|
|
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
|
|
}
|
|
|
|
childIfd.printIfdTree(level+1, false)
|
|
}
|
|
}
|
|
|
|
if len(ifd.Children) != ifdsFoundCount {
|
|
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
|
|
}
|
|
|
|
if ifd.NextIfd != nil {
|
|
ifd.NextIfd.printIfdTree(level, true)
|
|
}
|
|
}
|
|
|
|
// PrintIfdTree prints the IFD hierarchy.
|
|
func (ifd *Ifd) PrintIfdTree() {
|
|
ifd.printIfdTree(0, false)
|
|
}
|
|
|
|
func (ifd *Ifd) dumpTree(tagsDump []string, level int) []string {
|
|
if tagsDump == nil {
|
|
tagsDump = make([]string, 0)
|
|
}
|
|
|
|
indent := strings.Repeat(" ", level*2)
|
|
|
|
var ifdPhrase string
|
|
if ifd.ParentIfd != nil {
|
|
ifdPhrase = fmt.Sprintf("[%s]->[%s]:(%d)", ifd.ParentIfd.IfdPath, ifd.IfdPath, ifd.Index)
|
|
} else {
|
|
ifdPhrase = fmt.Sprintf("[ROOT]->[%s]:(%d)", ifd.IfdPath, ifd.Index)
|
|
}
|
|
|
|
startBlurb := fmt.Sprintf("%s> IFD %s TOP", indent, ifdPhrase)
|
|
tagsDump = append(tagsDump, startBlurb)
|
|
|
|
ifdsFoundCount := 0
|
|
for _, ite := range ifd.Entries {
|
|
tagsDump = append(tagsDump, fmt.Sprintf("%s - (0x%04x)", indent, ite.TagId()))
|
|
|
|
childIfdPath := ite.ChildIfdPath()
|
|
if childIfdPath != "" {
|
|
ifdsFoundCount++
|
|
|
|
childIfd, found := ifd.ChildIfdIndex[childIfdPath]
|
|
if found != true {
|
|
log.Panicf("alien child IFD referenced by a tag: [%s]", childIfdPath)
|
|
}
|
|
|
|
tagsDump = childIfd.dumpTree(tagsDump, level+1)
|
|
}
|
|
}
|
|
|
|
if len(ifd.Children) != ifdsFoundCount {
|
|
log.Panicf("have one or more dangling child IFDs: (%d) != (%d)", len(ifd.Children), ifdsFoundCount)
|
|
}
|
|
|
|
finishBlurb := fmt.Sprintf("%s< IFD %s BOTTOM", indent, ifdPhrase)
|
|
tagsDump = append(tagsDump, finishBlurb)
|
|
|
|
if ifd.NextIfd != nil {
|
|
siblingBlurb := fmt.Sprintf("%s* LINKING TO SIBLING IFD [%s]:(%d)", indent, ifd.NextIfd.IfdPath, ifd.NextIfd.Index)
|
|
tagsDump = append(tagsDump, siblingBlurb)
|
|
|
|
tagsDump = ifd.NextIfd.dumpTree(tagsDump, level)
|
|
}
|
|
|
|
return tagsDump
|
|
}
|
|
|
|
// DumpTree returns a list of strings describing the IFD hierarchy.
|
|
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.IfdPath != exifcommon.IfdPathStandardGps {
|
|
log.Panicf("GPS can only be read on GPS IFD: [%s] != [%s]", ifd.IfdPath, exifcommon.IfdPathStandardGps)
|
|
}
|
|
|
|
if tags, found := ifd.EntriesByTagId[TagGpsVersionId]; found == false {
|
|
// We've seen this. We'll just have to default to assuming we're in a
|
|
// 2.2.0.0 format.
|
|
ifdEnumerateLogger.Warningf(nil, "No GPS version tag (0x%04x) found.", TagGpsVersionId)
|
|
} else {
|
|
versionBytes, err := tags[0].GetRawBytes()
|
|
log.PanicIf(err)
|
|
|
|
hit := false
|
|
for _, acceptedGpsVersion := range ValidGpsVersions {
|
|
if bytes.Compare(versionBytes, acceptedGpsVersion[:]) == 0 {
|
|
hit = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hit != true {
|
|
ifdEnumerateLogger.Warningf(nil, "GPS version not supported: %v", versionBytes)
|
|
log.Panic(ErrNoGpsTags)
|
|
}
|
|
}
|
|
|
|
tags, found := ifd.EntriesByTagId[TagLatitudeId]
|
|
if found == false {
|
|
ifdEnumerateLogger.Warningf(nil, "latitude not found")
|
|
log.Panic(ErrNoGpsTags)
|
|
}
|
|
|
|
latitudeValue, err := tags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
// 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 := tags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
tags, found = ifd.EntriesByTagId[TagLongitudeId]
|
|
if found == false {
|
|
ifdEnumerateLogger.Warningf(nil, "longitude not found")
|
|
log.Panic(ErrNoGpsTags)
|
|
}
|
|
|
|
longitudeValue, err := tags[0].Value()
|
|
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 := tags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
// Parse location.
|
|
|
|
latitudeRaw := latitudeValue.([]exifcommon.Rational)
|
|
|
|
gi.Latitude, err = NewGpsDegreesFromRationals(latitudeRefValue.(string), latitudeRaw)
|
|
log.PanicIf(err)
|
|
|
|
longitudeRaw := longitudeValue.([]exifcommon.Rational)
|
|
|
|
gi.Longitude, err = NewGpsDegreesFromRationals(longitudeRefValue.(string), longitudeRaw)
|
|
log.PanicIf(err)
|
|
|
|
// Parse altitude.
|
|
|
|
altitudeTags, foundAltitude := ifd.EntriesByTagId[TagAltitudeId]
|
|
altitudeRefTags, foundAltitudeRef := ifd.EntriesByTagId[TagAltitudeRefId]
|
|
|
|
if foundAltitude == true && foundAltitudeRef == true {
|
|
altitudeValue, err := altitudeTags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
altitudeRefValue, err := altitudeRefTags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
altitudeRaw := altitudeValue.([]exifcommon.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 := datestampTags[0].Value()
|
|
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 := timestampTags[0].Value()
|
|
log.PanicIf(err)
|
|
|
|
timestampRaw := timestampValue.([]exifcommon.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 ParsedTagVisitor func(*Ifd, *IfdTagEntry) error
|
|
|
|
func (ifd *Ifd) EnumerateTagsRecursively(visitor ParsedTagVisitor) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
for ptr := ifd; ptr != nil; ptr = ptr.NextIfd {
|
|
for _, ite := range ifd.Entries {
|
|
childIfdPath := ite.ChildIfdPath()
|
|
if childIfdPath != "" {
|
|
childIfd := ifd.ChildIfdIndex[childIfdPath]
|
|
|
|
err := childIfd.EnumerateTagsRecursively(visitor)
|
|
log.PanicIf(err)
|
|
} else {
|
|
err := visitor(ifd, ite)
|
|
log.PanicIf(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type QueuedIfd struct {
|
|
Name string
|
|
IfdPath string
|
|
FqIfdPath string
|
|
|
|
TagId uint16
|
|
|
|
Index int
|
|
Offset uint32
|
|
Parent *Ifd
|
|
|
|
// ParentTagIndex is our tag position in the parent IFD, if we had a parent
|
|
// (if `ParentIfd` is not nil and we weren't an IFD referenced as a sibling
|
|
// instead of as a child).
|
|
ParentTagIndex int
|
|
}
|
|
|
|
type IfdIndex struct {
|
|
RootIfd *Ifd
|
|
Ifds []*Ifd
|
|
Tree map[int]*Ifd
|
|
Lookup map[string][]*Ifd
|
|
}
|
|
|
|
// Scan enumerates the different EXIF blocks (called IFDs).
|
|
func (ie *IfdEnumerate) Collect(rootIfdOffset uint32) (index IfdIndex, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
tree := make(map[int]*Ifd)
|
|
ifds := make([]*Ifd, 0)
|
|
lookup := make(map[string][]*Ifd)
|
|
|
|
queue := []QueuedIfd{
|
|
{
|
|
Name: exifcommon.IfdStandard,
|
|
IfdPath: exifcommon.IfdStandard,
|
|
FqIfdPath: exifcommon.IfdStandard,
|
|
|
|
TagId: 0xffff,
|
|
|
|
Index: 0,
|
|
Offset: rootIfdOffset,
|
|
},
|
|
}
|
|
|
|
edges := make(map[uint32]*Ifd)
|
|
|
|
for {
|
|
if len(queue) == 0 {
|
|
break
|
|
}
|
|
|
|
qi := queue[0]
|
|
|
|
name := qi.Name
|
|
ifdPath := qi.IfdPath
|
|
fqIfdPath := qi.FqIfdPath
|
|
|
|
currentIndex := qi.Index
|
|
offset := qi.Offset
|
|
parentIfd := qi.Parent
|
|
|
|
queue = queue[1:]
|
|
|
|
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (0x%04x) (Collect).", ifdPath, currentIndex, offset)
|
|
|
|
bp, err := ie.getByteParser(offset)
|
|
if err != nil {
|
|
if err == ErrOffsetInvalid {
|
|
return index, err
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
nextIfdOffset, entries, thumbnailData, err := ie.ParseIfd(fqIfdPath, currentIndex, bp, nil, false)
|
|
log.PanicIf(err)
|
|
|
|
currentOffset := bp.CurrentOffset()
|
|
if currentOffset > ie.furthestOffset {
|
|
ie.furthestOffset = currentOffset
|
|
}
|
|
|
|
id := len(ifds)
|
|
|
|
entriesByTagId := make(map[uint16][]*IfdTagEntry)
|
|
for _, ite := range entries {
|
|
tagId := ite.TagId()
|
|
|
|
tags, found := entriesByTagId[tagId]
|
|
if found == false {
|
|
tags = make([]*IfdTagEntry, 0)
|
|
}
|
|
|
|
entriesByTagId[tagId] = append(tags, ite)
|
|
}
|
|
|
|
ifd := &Ifd{
|
|
ByteOrder: ie.byteOrder,
|
|
|
|
Name: name,
|
|
IfdPath: ifdPath,
|
|
FqIfdPath: fqIfdPath,
|
|
|
|
TagId: qi.TagId,
|
|
|
|
Id: id,
|
|
|
|
ParentIfd: parentIfd,
|
|
ParentTagIndex: qi.ParentTagIndex,
|
|
|
|
Index: currentIndex,
|
|
Offset: offset,
|
|
Entries: entries,
|
|
EntriesByTagId: entriesByTagId,
|
|
|
|
// This is populated as each child is processed.
|
|
Children: make([]*Ifd, 0),
|
|
|
|
NextIfdOffset: nextIfdOffset,
|
|
thumbnailData: thumbnailData,
|
|
|
|
ifdMapping: ie.ifdMapping,
|
|
tagIndex: ie.tagIndex,
|
|
}
|
|
|
|
// Add ourselves to a big list of IFDs.
|
|
ifds = append(ifds, ifd)
|
|
|
|
// Install ourselves into a by-id lookup table (keys are unique).
|
|
tree[id] = ifd
|
|
|
|
// Install into by-name buckets.
|
|
|
|
if list_, found := lookup[ifdPath]; found == true {
|
|
lookup[ifdPath] = append(list_, ifd)
|
|
} else {
|
|
list_ = make([]*Ifd, 1)
|
|
list_[0] = ifd
|
|
|
|
lookup[ifdPath] = list_
|
|
}
|
|
|
|
// Add a link from the previous IFD in the chain to us.
|
|
if previousIfd, found := edges[offset]; found == true {
|
|
previousIfd.NextIfd = ifd
|
|
}
|
|
|
|
// Attach as a child to our parent (where we appeared as a tag in
|
|
// that IFD).
|
|
if parentIfd != nil {
|
|
parentIfd.Children = append(parentIfd.Children, ifd)
|
|
}
|
|
|
|
// Determine if any of our entries is a child IFD and queue it.
|
|
for i, ite := range entries {
|
|
if ite.ChildIfdPath() == "" {
|
|
continue
|
|
}
|
|
|
|
qi := QueuedIfd{
|
|
Name: ite.ChildIfdName(),
|
|
IfdPath: ite.ChildIfdPath(),
|
|
FqIfdPath: ite.ChildFqIfdPath(),
|
|
TagId: ite.TagId(),
|
|
|
|
Index: 0,
|
|
Offset: ite.getValueOffset(),
|
|
Parent: ifd,
|
|
ParentTagIndex: i,
|
|
}
|
|
|
|
queue = append(queue, qi)
|
|
}
|
|
|
|
// If there's another IFD in the chain.
|
|
if nextIfdOffset != 0 {
|
|
// Allow the next link to know what the previous link was.
|
|
edges[nextIfdOffset] = ifd
|
|
|
|
siblingIndex := currentIndex + 1
|
|
|
|
var fqIfdPath string
|
|
if parentIfd != nil {
|
|
fqIfdPath = fmt.Sprintf("%s/%s%d", parentIfd.FqIfdPath, name, siblingIndex)
|
|
} else {
|
|
fqIfdPath = fmt.Sprintf("%s%d", name, siblingIndex)
|
|
}
|
|
|
|
qi := QueuedIfd{
|
|
Name: name,
|
|
IfdPath: ifdPath,
|
|
FqIfdPath: fqIfdPath,
|
|
TagId: 0xffff,
|
|
Index: siblingIndex,
|
|
Offset: nextIfdOffset,
|
|
}
|
|
|
|
queue = append(queue, qi)
|
|
}
|
|
}
|
|
|
|
index.RootIfd = tree[0]
|
|
index.Ifds = ifds
|
|
index.Tree = tree
|
|
index.Lookup = lookup
|
|
|
|
err = ie.setChildrenIndex(index.RootIfd)
|
|
log.PanicIf(err)
|
|
|
|
return index, nil
|
|
}
|
|
|
|
func (ie *IfdEnumerate) setChildrenIndex(ifd *Ifd) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
childIfdIndex := make(map[string]*Ifd)
|
|
for _, childIfd := range ifd.Children {
|
|
childIfdIndex[childIfd.IfdPath] = childIfd
|
|
}
|
|
|
|
ifd.ChildIfdIndex = childIfdIndex
|
|
|
|
for _, childIfd := range ifd.Children {
|
|
err := ie.setChildrenIndex(childIfd)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FurthestOffset returns the furthest offset visited in the EXIF blob. This
|
|
// *does not* account for the locations of any undefined tags since we always
|
|
// evaluate the furthest offset, whether or not the user wants to know it.
|
|
//
|
|
// We are not willing to incur the cost of actually parsing those tags just to
|
|
// know their length when there are still undefined tags that are out there
|
|
// that we still won't have any idea how to parse, thus making this an
|
|
// approximation regardless of how clever we get.
|
|
func (ie *IfdEnumerate) FurthestOffset() uint32 {
|
|
|
|
// TODO(dustin): Add test
|
|
|
|
return ie.furthestOffset
|
|
}
|
|
|
|
// ParseOneIfd is a hack to use an IE to parse a raw IFD block. Can be used for
|
|
// testing.
|
|
func ParseOneIfd(ifdMapping *IfdMapping, tagIndex *TagIndex, fqIfdPath, ifdPath string, byteOrder binary.ByteOrder, ifdBlock []byte, visitor TagVisitorFn) (nextIfdOffset uint32, entries []*IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder)
|
|
|
|
bp, err := newByteParser(ifdBlock, byteOrder, 0)
|
|
if err != nil {
|
|
if err == ErrOffsetInvalid {
|
|
return 0, nil, err
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
nextIfdOffset, entries, _, err = ie.ParseIfd(fqIfdPath, 0, bp, visitor, true)
|
|
log.PanicIf(err)
|
|
|
|
return nextIfdOffset, entries, nil
|
|
}
|
|
|
|
// ParseOneTag is a hack to use an IE to parse a raw tag block.
|
|
func ParseOneTag(ifdMapping *IfdMapping, tagIndex *TagIndex, fqIfdPath, ifdPath string, byteOrder binary.ByteOrder, tagBlock []byte) (tag *IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
ie := NewIfdEnumerate(ifdMapping, tagIndex, make([]byte, 0), byteOrder)
|
|
|
|
bp, err := newByteParser(tagBlock, byteOrder, 0)
|
|
if err != nil {
|
|
if err == ErrOffsetInvalid {
|
|
return nil, err
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
tag, err = ie.parseTag(fqIfdPath, 0, bp)
|
|
log.PanicIf(err)
|
|
|
|
return tag, nil
|
|
}
|
|
|
|
func FindIfdFromRootIfd(rootIfd *Ifd, ifdPath string) (ifd *Ifd, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): !! Add test.
|
|
|
|
lineage, err := rootIfd.ifdMapping.ResolvePath(ifdPath)
|
|
log.PanicIf(err)
|
|
|
|
// Confirm the first IFD is our root IFD type, and then prune it because
|
|
// from then on we'll be searching down through our children.
|
|
|
|
if len(lineage) == 0 {
|
|
log.Panicf("IFD path must be non-empty.")
|
|
} else if lineage[0].Name != exifcommon.IfdStandard {
|
|
log.Panicf("First IFD path item must be [%s].", exifcommon.IfdStandard)
|
|
}
|
|
|
|
desiredRootIndex := lineage[0].Index
|
|
lineage = lineage[1:]
|
|
|
|
// TODO(dustin): !! This is a poorly conceived fix that just doubles the work we already have to do below, which then interacts badly with the indices not being properly represented in the IFD-phrase.
|
|
// TODO(dustin): !! <-- However, we're not sure whether we shouldn't store a secondary IFD-path with the indices. Some IFDs may not necessarily restrict which IFD indices they can be a child of (only the IFD itself matters). Validation should be delegated to the caller.
|
|
thisIfd := rootIfd
|
|
for currentRootIndex := 0; currentRootIndex < desiredRootIndex; currentRootIndex++ {
|
|
if thisIfd.NextIfd == nil {
|
|
log.Panicf("Root-IFD index (%d) does not exist in the data.", currentRootIndex)
|
|
}
|
|
|
|
thisIfd = thisIfd.NextIfd
|
|
}
|
|
|
|
for i, itii := range lineage {
|
|
var hit *Ifd
|
|
for _, childIfd := range thisIfd.Children {
|
|
if childIfd.TagId == itii.TagId {
|
|
hit = childIfd
|
|
break
|
|
}
|
|
}
|
|
|
|
// If we didn't find the child, add it.
|
|
if hit == nil {
|
|
log.Panicf("IFD [%s] in [%s] not found: %s", itii.Name, ifdPath, thisIfd.Children)
|
|
}
|
|
|
|
thisIfd = hit
|
|
|
|
// If we didn't find the sibling, add it.
|
|
for i = 0; i < itii.Index; i++ {
|
|
if thisIfd.NextIfd == nil {
|
|
log.Panicf("IFD [%s] does not have (%d) occurrences/siblings\n", thisIfd.IfdPath, itii.Index)
|
|
}
|
|
|
|
thisIfd = thisIfd.NextIfd
|
|
}
|
|
}
|
|
|
|
return thisIfd, nil
|
|
}
|