mirror of https://github.com/dsoprea/go-exif.git
1253 lines
31 KiB
Go
1253 lines
31 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("exifjpeg.ifd")
|
|
)
|
|
|
|
var (
|
|
ErrNoThumbnail = errors.New("no thumbnail")
|
|
ErrNoGpsTags = errors.New("no gps tags")
|
|
ErrTagTypeNotValid = errors.New("tag type invalid")
|
|
)
|
|
|
|
var (
|
|
ValidGpsVersions = [][4]byte{
|
|
{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},
|
|
}
|
|
)
|
|
|
|
// IfdTagEnumerator 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 IfdTagEnumerator struct {
|
|
byteOrder binary.ByteOrder
|
|
addressableData []byte
|
|
ifdOffset uint32
|
|
buffer *bytes.Buffer
|
|
}
|
|
|
|
func NewIfdTagEnumerator(addressableData []byte, byteOrder binary.ByteOrder, ifdOffset uint32) (enumerator *IfdTagEnumerator) {
|
|
enumerator = &IfdTagEnumerator{
|
|
addressableData: addressableData,
|
|
byteOrder: byteOrder,
|
|
buffer: bytes.NewBuffer(addressableData[ifdOffset:]),
|
|
}
|
|
|
|
return enumerator
|
|
}
|
|
|
|
// 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 (ife *IfdTagEnumerator) getUint16() (value uint16, raw []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
needBytes := 2
|
|
offset := 0
|
|
raw = make([]byte, needBytes)
|
|
|
|
for offset < needBytes {
|
|
n, err := ife.buffer.Read(raw[offset:])
|
|
log.PanicIf(err)
|
|
|
|
offset += n
|
|
}
|
|
|
|
value = ife.byteOrder.Uint16(raw)
|
|
|
|
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 (ife *IfdTagEnumerator) getUint32() (value uint32, raw []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
needBytes := 4
|
|
offset := 0
|
|
raw = make([]byte, needBytes)
|
|
|
|
for offset < needBytes {
|
|
n, err := ife.buffer.Read(raw[offset:])
|
|
log.PanicIf(err)
|
|
|
|
offset += n
|
|
}
|
|
|
|
value = ife.byteOrder.Uint32(raw)
|
|
|
|
return value, raw, nil
|
|
}
|
|
|
|
type IfdEnumerate struct {
|
|
exifData []byte
|
|
buffer *bytes.Buffer
|
|
byteOrder binary.ByteOrder
|
|
currentOffset uint32
|
|
tagIndex *TagIndex
|
|
ifdMapping *IfdMapping
|
|
}
|
|
|
|
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) getTagEnumerator(ifdOffset uint32) (enumerator *IfdTagEnumerator) {
|
|
enumerator = NewIfdTagEnumerator(
|
|
ie.exifData[ExifAddressableAreaStart:],
|
|
ie.byteOrder,
|
|
ifdOffset)
|
|
|
|
return enumerator
|
|
}
|
|
|
|
func (ie *IfdEnumerate) parseTag(fqIfdPath string, tagPosition int, enumerator *IfdTagEnumerator) (ite *IfdTagEntry, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
tagId, _, err := enumerator.getUint16()
|
|
log.PanicIf(err)
|
|
|
|
tagTypeRaw, _, err := enumerator.getUint16()
|
|
log.PanicIf(err)
|
|
|
|
tagType := exifcommon.TagTypePrimitive(tagTypeRaw)
|
|
|
|
unitCount, _, err := enumerator.getUint32()
|
|
log.PanicIf(err)
|
|
|
|
valueOffset, rawValueOffset, err := enumerator.getUint32()
|
|
log.PanicIf(err)
|
|
|
|
if tagType.IsValid() == false {
|
|
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
|
|
}
|
|
|
|
// RawTagWalk is an optional callback that can get hit for every tag we parse
|
|
// through. `addressableData` is the byte array startign after the EXIF header
|
|
// (where the offsets of all IFDs and values are calculated from).
|
|
//
|
|
// This was reimplemented as an interface to allow for simpler change management
|
|
// in the future.
|
|
type RawTagWalk interface {
|
|
Visit(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, enumerator *IfdTagEnumerator, visitor RawTagWalk, doDescend bool) (nextIfdOffset uint32, entries []*IfdTagEntry, thumbnailData []byte, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
tagCount, _, err := enumerator.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, enumerator)
|
|
if err != nil {
|
|
if log.Is(err, ErrTagTypeNotValid) == true {
|
|
ifdEnumerateLogger.Warningf(nil, "Tag in IFD [%s] at position (%d) has invalid type and will be skipped.", fqIfdPath, i)
|
|
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.Visit(fqIfdPath, ifdIndex, ite)
|
|
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.valueOffset_(), visitor)
|
|
log.PanicIf(err)
|
|
}
|
|
}
|
|
|
|
entries = append(entries, ite)
|
|
}
|
|
|
|
if enumeratorThumbnailOffset != nil && enumeratorThumbnailSize != nil {
|
|
thumbnailData, err = ie.parseThumbnail(enumeratorThumbnailOffset, enumeratorThumbnailSize)
|
|
log.PanicIf(err)
|
|
}
|
|
|
|
nextIfdOffset, _, err = enumerator.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 EXIF's IFD blocks.
|
|
func (ie *IfdEnumerate) scan(fqIfdName string, ifdOffset uint32, visitor RawTagWalk) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
for ifdIndex := 0; ; ifdIndex++ {
|
|
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", fqIfdName, ifdIndex, ifdOffset)
|
|
enumerator := ie.getTagEnumerator(ifdOffset)
|
|
|
|
nextIfdOffset, _, _, err := ie.ParseIfd(fqIfdName, ifdIndex, enumerator, visitor, true)
|
|
log.PanicIf(err)
|
|
|
|
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 RawTagWalk) (err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
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().
|
|
|
|
// This is just for convenience, just so that we can easily get the values
|
|
// and not involve other projects in semantics that they won't otherwise
|
|
// need to know.
|
|
addressableData []byte
|
|
|
|
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)
|
|
}
|
|
|
|
// FormatValue returns a stringified value for any well-defined tag value as
|
|
// well as supported undefined-tag values.
|
|
func (ifd *Ifd) FormatValue(ite *IfdTagEntry) (valuePhrase string, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
value, err := ite.Value()
|
|
if err != nil {
|
|
if err == exifcommon.ErrUnhandledUnknownTypedTag {
|
|
return exifundefined.UnparseableUnknownTagValuePlaceholder, nil
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
valuePhrase, err = exifcommon.FormatFromType(value, false)
|
|
log.PanicIf(err)
|
|
|
|
return valuePhrase, nil
|
|
}
|
|
|
|
// Value returns the value for the given tag.
|
|
func (ifd *Ifd) Value(ite *IfdTagEntry) (value interface{}, err error) {
|
|
defer func() {
|
|
if state := recover(); state != nil {
|
|
err = log.Wrap(state.(error))
|
|
}
|
|
}()
|
|
|
|
value, err = ite.Value()
|
|
if err != nil {
|
|
if err == exifcommon.ErrUnhandledUnknownTypedTag {
|
|
return nil, err
|
|
}
|
|
|
|
log.Panic(err)
|
|
}
|
|
|
|
return value, 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 = ifd.FormatValue(ite)
|
|
log.PanicIf(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))
|
|
}
|
|
}()
|
|
|
|
// TODO(dustin): !! Also add functionality to update the GPS info.
|
|
|
|
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 := ifd.Value(tags[0])
|
|
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 := ifd.Value(tags[0])
|
|
log.PanicIf(err)
|
|
|
|
tags, found = ifd.EntriesByTagId[TagLongitudeId]
|
|
if found == false {
|
|
ifdEnumerateLogger.Warningf(nil, "longitude not found")
|
|
log.Panic(ErrNoGpsTags)
|
|
}
|
|
|
|
longitudeValue, err := ifd.Value(tags[0])
|
|
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 := ifd.Value(tags[0])
|
|
log.PanicIf(err)
|
|
|
|
// Parse location.
|
|
|
|
latitudeRaw := latitudeValue.([]exifcommon.Rational)
|
|
|
|
gi.Latitude = GpsDegrees{
|
|
Orientation: latitudeRefValue.(string)[0],
|
|
Degrees: float64(latitudeRaw[0].Numerator) / float64(latitudeRaw[0].Denominator),
|
|
Minutes: float64(latitudeRaw[1].Numerator) / float64(latitudeRaw[1].Denominator),
|
|
Seconds: float64(latitudeRaw[2].Numerator) / float64(latitudeRaw[2].Denominator),
|
|
}
|
|
|
|
longitudeRaw := longitudeValue.([]exifcommon.Rational)
|
|
|
|
gi.Longitude = GpsDegrees{
|
|
Orientation: longitudeRefValue.(string)[0],
|
|
Degrees: float64(longitudeRaw[0].Numerator) / float64(longitudeRaw[0].Denominator),
|
|
Minutes: float64(longitudeRaw[1].Numerator) / float64(longitudeRaw[1].Denominator),
|
|
Seconds: 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.Value(altitudeTags[0])
|
|
log.PanicIf(err)
|
|
|
|
altitudeRefValue, err := ifd.Value(altitudeRefTags[0])
|
|
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 := ifd.Value(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.Value(timestampTags[0])
|
|
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
|
|
|
|
index := qi.Index
|
|
offset := qi.Offset
|
|
parentIfd := qi.Parent
|
|
|
|
queue = queue[1:]
|
|
|
|
ifdEnumerateLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ifdPath, index, offset)
|
|
enumerator := ie.getTagEnumerator(offset)
|
|
|
|
nextIfdOffset, entries, thumbnailData, err := ie.ParseIfd(fqIfdPath, index, enumerator, nil, false)
|
|
log.PanicIf(err)
|
|
|
|
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{
|
|
addressableData: ie.exifData[ExifAddressableAreaStart:],
|
|
|
|
ByteOrder: ie.byteOrder,
|
|
|
|
Name: name,
|
|
IfdPath: ifdPath,
|
|
FqIfdPath: fqIfdPath,
|
|
|
|
TagId: qi.TagId,
|
|
|
|
Id: id,
|
|
|
|
ParentIfd: parentIfd,
|
|
ParentTagIndex: qi.ParentTagIndex,
|
|
|
|
Index: index,
|
|
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.valueOffset_(),
|
|
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 := index + 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
|
|
}
|
|
|
|
// 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 RawTagWalk) (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)
|
|
enumerator := NewIfdTagEnumerator(ifdBlock, byteOrder, 0)
|
|
|
|
nextIfdOffset, entries, _, err = ie.ParseIfd(fqIfdPath, 0, enumerator, 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)
|
|
enumerator := NewIfdTagEnumerator(tagBlock, byteOrder, 0)
|
|
|
|
tag, err = ie.parseTag(fqIfdPath, 0, enumerator)
|
|
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
|
|
}
|