238 lines
6.7 KiB
Go
238 lines
6.7 KiB
Go
|
package exif
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math"
|
||
|
|
||
|
"github.com/dsoprea/go-logging"
|
||
|
"github.com/dsoprea/go-utility/v2/filesystem"
|
||
|
|
||
|
"github.com/dsoprea/go-exif/v3/common"
|
||
|
"github.com/dsoprea/go-exif/v3/undefined"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
utilityLogger = log.NewLogger("exif.utility")
|
||
|
)
|
||
|
|
||
|
// ExifTag is one simple representation of a tag in a flat list of all of them.
|
||
|
type ExifTag struct {
|
||
|
// IfdPath is the fully-qualified IFD path (even though it is not named as
|
||
|
// such).
|
||
|
IfdPath string `json:"ifd_path"`
|
||
|
|
||
|
// TagId is the tag-ID.
|
||
|
TagId uint16 `json:"id"`
|
||
|
|
||
|
// TagName is the tag-name. This is never empty.
|
||
|
TagName string `json:"name"`
|
||
|
|
||
|
// UnitCount is the recorded number of units constution of the value.
|
||
|
UnitCount uint32 `json:"unit_count"`
|
||
|
|
||
|
// TagTypeId is the type-ID.
|
||
|
TagTypeId exifcommon.TagTypePrimitive `json:"type_id"`
|
||
|
|
||
|
// TagTypeName is the type name.
|
||
|
TagTypeName string `json:"type_name"`
|
||
|
|
||
|
// Value is the decoded value.
|
||
|
Value interface{} `json:"value"`
|
||
|
|
||
|
// ValueBytes is the raw, encoded value.
|
||
|
ValueBytes []byte `json:"value_bytes"`
|
||
|
|
||
|
// Formatted is the human representation of the first value (tag values are
|
||
|
// always an array).
|
||
|
FormattedFirst string `json:"formatted_first"`
|
||
|
|
||
|
// Formatted is the human representation of the complete value.
|
||
|
Formatted string `json:"formatted"`
|
||
|
|
||
|
// ChildIfdPath is the name of the child IFD this tag represents (if it
|
||
|
// represents any). Otherwise, this is empty.
|
||
|
ChildIfdPath string `json:"child_ifd_path"`
|
||
|
}
|
||
|
|
||
|
// String returns a string representation.
|
||
|
func (et ExifTag) String() string {
|
||
|
return fmt.Sprintf(
|
||
|
"ExifTag<"+
|
||
|
"IFD-PATH=[%s] "+
|
||
|
"TAG-ID=(0x%02x) "+
|
||
|
"TAG-NAME=[%s] "+
|
||
|
"TAG-TYPE=[%s] "+
|
||
|
"VALUE=[%v] "+
|
||
|
"VALUE-BYTES=(%d) "+
|
||
|
"CHILD-IFD-PATH=[%s]",
|
||
|
et.IfdPath, et.TagId, et.TagName, et.TagTypeName, et.FormattedFirst,
|
||
|
len(et.ValueBytes), et.ChildIfdPath)
|
||
|
}
|
||
|
|
||
|
// GetFlatExifData returns a simple, flat representation of all tags.
|
||
|
func GetFlatExifData(exifData []byte, so *ScanOptions) (exifTags []ExifTag, med *MiscellaneousExifData, err error) {
|
||
|
defer func() {
|
||
|
if state := recover(); state != nil {
|
||
|
err = log.Wrap(state.(error))
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
sb := rifs.NewSeekableBufferWithBytes(exifData)
|
||
|
|
||
|
exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(sb, so, false)
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
return exifTags, med, nil
|
||
|
}
|
||
|
|
||
|
// RELEASE(dustin): GetFlatExifDataUniversalSearch is a kludge to allow univeral tag searching in a backwards-compatible manner. For the next release, undo this and simply add the flag to GetFlatExifData.
|
||
|
|
||
|
// GetFlatExifDataUniversalSearch returns a simple, flat representation of all
|
||
|
// tags.
|
||
|
func GetFlatExifDataUniversalSearch(exifData []byte, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) {
|
||
|
defer func() {
|
||
|
if state := recover(); state != nil {
|
||
|
err = log.Wrap(state.(error))
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
sb := rifs.NewSeekableBufferWithBytes(exifData)
|
||
|
|
||
|
exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(sb, so, doUniversalSearch)
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
return exifTags, med, nil
|
||
|
}
|
||
|
|
||
|
// RELEASE(dustin): GetFlatExifDataUniversalSearchWithReadSeeker is a kludge to allow using a ReadSeeker in a backwards-compatible manner. For the next release, drop this and refactor GetFlatExifDataUniversalSearch to take a ReadSeeker.
|
||
|
|
||
|
// GetFlatExifDataUniversalSearchWithReadSeeker returns a simple, flat
|
||
|
// representation of all tags given a ReadSeeker.
|
||
|
func GetFlatExifDataUniversalSearchWithReadSeeker(rs io.ReadSeeker, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) {
|
||
|
defer func() {
|
||
|
if state := recover(); state != nil {
|
||
|
err = log.Wrap(state.(error))
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
exifTags, med, err = getFlatExifDataUniversalSearchWithReadSeeker(rs, so, doUniversalSearch)
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
return exifTags, med, nil
|
||
|
}
|
||
|
|
||
|
// getFlatExifDataUniversalSearchWithReadSeeker returns a simple, flat
|
||
|
// representation of all tags given a ReadSeeker.
|
||
|
func getFlatExifDataUniversalSearchWithReadSeeker(rs io.ReadSeeker, so *ScanOptions, doUniversalSearch bool) (exifTags []ExifTag, med *MiscellaneousExifData, err error) {
|
||
|
defer func() {
|
||
|
if state := recover(); state != nil {
|
||
|
err = log.Wrap(state.(error))
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
headerData := make([]byte, ExifSignatureLength)
|
||
|
if _, err = io.ReadFull(rs, headerData); err != nil {
|
||
|
if err == io.EOF {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
log.Panic(err)
|
||
|
}
|
||
|
|
||
|
eh, err := ParseExifHeader(headerData)
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
im, err := exifcommon.NewIfdMappingWithStandard()
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
ti := NewTagIndex()
|
||
|
|
||
|
if doUniversalSearch == true {
|
||
|
ti.SetUniversalSearch(true)
|
||
|
}
|
||
|
|
||
|
ebs := NewExifReadSeeker(rs)
|
||
|
ie := NewIfdEnumerate(im, ti, ebs, eh.ByteOrder)
|
||
|
|
||
|
exifTags = make([]ExifTag, 0)
|
||
|
|
||
|
visitor := func(ite *IfdTagEntry) (err error) {
|
||
|
// This encodes down to base64. Since this an example tool and we do not
|
||
|
// expect to ever decode the output, we are not worried about
|
||
|
// specifically base64-encoding it in order to have a measure of
|
||
|
// control.
|
||
|
valueBytes, err := ite.GetRawBytes()
|
||
|
if err != nil {
|
||
|
if err == exifundefined.ErrUnparseableValue {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
log.Panic(err)
|
||
|
}
|
||
|
|
||
|
value, err := ite.Value()
|
||
|
if err != nil {
|
||
|
if err == exifcommon.ErrUnhandledUndefinedTypedTag {
|
||
|
value = exifundefined.UnparseableUnknownTagValuePlaceholder
|
||
|
} else if log.Is(err, exifcommon.ErrParseFail) == true {
|
||
|
utilityLogger.Warningf(nil,
|
||
|
"Could not parse value for tag [%s] (%04x) [%s].",
|
||
|
ite.IfdPath(), ite.TagId(), ite.TagName())
|
||
|
|
||
|
return nil
|
||
|
} else {
|
||
|
log.Panic(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
et := ExifTag{
|
||
|
IfdPath: ite.IfdPath(),
|
||
|
TagId: ite.TagId(),
|
||
|
TagName: ite.TagName(),
|
||
|
UnitCount: ite.UnitCount(),
|
||
|
TagTypeId: ite.TagType(),
|
||
|
TagTypeName: ite.TagType().String(),
|
||
|
Value: value,
|
||
|
ValueBytes: valueBytes,
|
||
|
ChildIfdPath: ite.ChildIfdPath(),
|
||
|
}
|
||
|
|
||
|
et.Formatted, err = ite.Format()
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
et.FormattedFirst, err = ite.FormatFirst()
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
exifTags = append(exifTags, et)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
med, err = ie.Scan(exifcommon.IfdStandardIfdIdentity, eh.FirstIfdOffset, visitor, nil)
|
||
|
log.PanicIf(err)
|
||
|
|
||
|
return exifTags, med, nil
|
||
|
}
|
||
|
|
||
|
// GpsDegreesEquals returns true if the two `GpsDegrees` are identical.
|
||
|
func GpsDegreesEquals(gi1, gi2 GpsDegrees) bool {
|
||
|
if gi2.Orientation != gi1.Orientation {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
degreesRightBound := math.Nextafter(gi1.Degrees, gi1.Degrees+1)
|
||
|
minutesRightBound := math.Nextafter(gi1.Minutes, gi1.Minutes+1)
|
||
|
secondsRightBound := math.Nextafter(gi1.Seconds, gi1.Seconds+1)
|
||
|
|
||
|
if gi2.Degrees < gi1.Degrees || gi2.Degrees >= degreesRightBound {
|
||
|
return false
|
||
|
} else if gi2.Minutes < gi1.Minutes || gi2.Minutes >= minutesRightBound {
|
||
|
return false
|
||
|
} else if gi2.Seconds < gi1.Seconds || gi2.Seconds >= secondsRightBound {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|