2021-08-12 20:03:24 +01:00
package exif
import (
"fmt"
2022-01-23 13:41:31 +00:00
"io"
2021-08-12 20:03:24 +01:00
"math"
"github.com/dsoprea/go-logging"
2022-01-23 13:41:31 +00:00
"github.com/dsoprea/go-utility/v2/filesystem"
2021-08-12 20:03:24 +01:00
2022-01-23 13:41:31 +00:00
"github.com/dsoprea/go-exif/v3/common"
"github.com/dsoprea/go-exif/v3/undefined"
2021-08-12 20:03:24 +01:00
)
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.
2022-01-23 13:41:31 +00:00
func GetFlatExifData ( exifData [ ] byte , so * ScanOptions ) ( exifTags [ ] ExifTag , med * MiscellaneousExifData , err error ) {
2021-08-12 20:03:24 +01:00
defer func ( ) {
if state := recover ( ) ; state != nil {
err = log . Wrap ( state . ( error ) )
}
} ( )
2022-01-23 13:41:31 +00:00
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 ( )
2021-08-12 20:03:24 +01:00
log . PanicIf ( err )
ti := NewTagIndex ( )
2022-01-23 13:41:31 +00:00
if doUniversalSearch == true {
ti . SetUniversalSearch ( true )
}
ebs := NewExifReadSeeker ( rs )
ie := NewIfdEnumerate ( im , ti , ebs , eh . ByteOrder )
2021-08-12 20:03:24 +01:00
exifTags = make ( [ ] ExifTag , 0 )
2022-01-23 13:41:31 +00:00
visitor := func ( ite * IfdTagEntry ) ( err error ) {
2021-08-12 20:03:24 +01:00
// 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
2022-01-23 13:41:31 +00:00
} 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
2021-08-12 20:03:24 +01:00
} else {
log . Panic ( err )
}
}
et := ExifTag {
2022-01-23 13:41:31 +00:00
IfdPath : ite . IfdPath ( ) ,
2021-08-12 20:03:24 +01:00
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
}
2022-01-23 13:41:31 +00:00
med , err = ie . Scan ( exifcommon . IfdStandardIfdIdentity , eh . FirstIfdOffset , visitor , nil )
2021-08-12 20:03:24 +01:00
log . PanicIf ( err )
2022-01-23 13:41:31 +00:00
return exifTags , med , nil
2021-08-12 20:03:24 +01:00
}
// 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
}