ifd: Implemented recursion. Refactored IFD seeking.

- tags.yaml: Uncommented previously-unhandleable tags.
pull/3/head
Dustin Oprea 2018-04-15 23:13:25 -04:00
parent 34fb63841d
commit 172013e0f0
7 changed files with 406 additions and 312 deletions

View File

@ -71,10 +71,9 @@ GPSInfo:
name: GPSDateStamp name: GPSDateStamp
- id: 0x001e - id: 0x001e
name: GPSDifferential name: GPSDifferential
Image: IFD:
# Conflicts with GPSDOP and may not even be official. - id: 0x000b
#- id: 0x000b name: ProcessingSoftware
# name: ProcessingSoftware
- id: 0x00fe - id: 0x00fe
name: NewSubfileType name: NewSubfileType
- id: 0x00ff - id: 0x00ff
@ -483,158 +482,153 @@ Image:
name: OpcodeList3 name: OpcodeList3
- id: 0xc761 - id: 0xc761
name: NoiseProfile name: NoiseProfile
# Note sure about this. ExifTool doesn't recognize it and 0x0001 conflicts with GPSLatitudeRef. Iop:
# - id: 0x0001
# Iop: name: InteroperabilityIndex
# - id: 0x0001 - id: 0x0002
# name: InteroperabilityIndex name: InteroperabilityVersion
# - id: 0x0002 - id: 0x1000
# name: InteroperabilityVersion name: RelatedImageFileFormat
# - id: 0x1000 - id: 0x1001
# name: RelatedImageFileFormat name: RelatedImageWidth
# - id: 0x1001 - id: 0x1002
# name: RelatedImageWidth name: RelatedImageLength
# - id: 0x1002 Exif:
# name: RelatedImageLength - id: 0x829a
# name: ExposureTime
# Aren't recognized by ExifTool nor the PDF documentation (included). - id: 0x829d
# name: FNumber
# Photo: - id: 0x8822
# - id: 0x829a name: ExposureProgram
# name: ExposureTime - id: 0x8824
# - id: 0x829d name: SpectralSensitivity
# name: FNumber - id: 0x8827
# - id: 0x8822 name: ISOSpeedRatings
# name: ExposureProgram - id: 0x8828
# - id: 0x8824 name: OECF
# name: SpectralSensitivity - id: 0x8830
# - id: 0x8827 name: SensitivityType
# name: ISOSpeedRatings - id: 0x8831
# - id: 0x8828 name: StandardOutputSensitivity
# name: OECF - id: 0x8832
# - id: 0x8830 name: RecommendedExposureIndex
# name: SensitivityType - id: 0x8833
# - id: 0x8831 name: ISOSpeed
# name: StandardOutputSensitivity - id: 0x8834
# - id: 0x8832 name: ISOSpeedLatitudeyyy
# name: RecommendedExposureIndex - id: 0x8835
# - id: 0x8833 name: ISOSpeedLatitudezzz
# name: ISOSpeed - id: 0x9000
# - id: 0x8834 name: ExifVersion
# name: ISOSpeedLatitudeyyy - id: 0x9003
# - id: 0x8835 name: DateTimeOriginal
# name: ISOSpeedLatitudezzz - id: 0x9004
# - id: 0x9000 name: DateTimeDigitized
# name: ExifVersion - id: 0x9101
# - id: 0x9003 name: ComponentsConfiguration
# name: DateTimeOriginal - id: 0x9102
# - id: 0x9004 name: CompressedBitsPerPixel
# name: DateTimeDigitized - id: 0x9201
# - id: 0x9101 name: ShutterSpeedValue
# name: ComponentsConfiguration - id: 0x9202
# - id: 0x9102 name: ApertureValue
# name: CompressedBitsPerPixel - id: 0x9203
# - id: 0x9201 name: BrightnessValue
# name: ShutterSpeedValue - id: 0x9204
# - id: 0x9202 name: ExposureBiasValue
# name: ApertureValue - id: 0x9205
# - id: 0x9203 name: MaxApertureValue
# name: BrightnessValue - id: 0x9206
# - id: 0x9204 name: SubjectDistance
# name: ExposureBiasValue - id: 0x9207
# - id: 0x9205 name: MeteringMode
# name: MaxApertureValue - id: 0x9208
# - id: 0x9206 name: LightSource
# name: SubjectDistance - id: 0x9209
# - id: 0x9207 name: Flash
# name: MeteringMode - id: 0x920a
# - id: 0x9208 name: FocalLength
# name: LightSource - id: 0x9214
# - id: 0x9209 name: SubjectArea
# name: Flash - id: 0x927c
# - id: 0x920a name: MakerNote
# name: FocalLength - id: 0x9286
# - id: 0x9214 name: UserComment
# name: SubjectArea - id: 0x9290
# - id: 0x927c name: SubSecTime
# name: MakerNote - id: 0x9291
# - id: 0x9286 name: SubSecTimeOriginal
# name: UserComment - id: 0x9292
# - id: 0x9290 name: SubSecTimeDigitized
# name: SubSecTime - id: 0xa000
# - id: 0x9291 name: FlashpixVersion
# name: SubSecTimeOriginal - id: 0xa001
# - id: 0x9292 name: ColorSpace
# name: SubSecTimeDigitized - id: 0xa002
# - id: 0xa000 name: PixelXDimension
# name: FlashpixVersion - id: 0xa003
# - id: 0xa001 name: PixelYDimension
# name: ColorSpace - id: 0xa004
# - id: 0xa002 name: RelatedSoundFile
# name: PixelXDimension - id: 0xa005
# - id: 0xa003 name: InteroperabilityTag
# name: PixelYDimension - id: 0xa20b
# - id: 0xa004 name: FlashEnergy
# name: RelatedSoundFile - id: 0xa20c
# - id: 0xa005 name: SpatialFrequencyResponse
# name: InteroperabilityTag - id: 0xa20e
# - id: 0xa20b name: FocalPlaneXResolution
# name: FlashEnergy - id: 0xa20f
# - id: 0xa20c name: FocalPlaneYResolution
# name: SpatialFrequencyResponse - id: 0xa210
# - id: 0xa20e name: FocalPlaneResolutionUnit
# name: FocalPlaneXResolution - id: 0xa214
# - id: 0xa20f name: SubjectLocation
# name: FocalPlaneYResolution - id: 0xa215
# - id: 0xa210 name: ExposureIndex
# name: FocalPlaneResolutionUnit - id: 0xa217
# - id: 0xa214 name: SensingMethod
# name: SubjectLocation - id: 0xa300
# - id: 0xa215 name: FileSource
# name: ExposureIndex - id: 0xa301
# - id: 0xa217 name: SceneType
# name: SensingMethod - id: 0xa302
# - id: 0xa300 name: CFAPattern
# name: FileSource - id: 0xa401
# - id: 0xa301 name: CustomRendered
# name: SceneType - id: 0xa402
# - id: 0xa302 name: ExposureMode
# name: CFAPattern - id: 0xa403
# - id: 0xa401 name: WhiteBalance
# name: CustomRendered - id: 0xa404
# - id: 0xa402 name: DigitalZoomRatio
# name: ExposureMode - id: 0xa405
# - id: 0xa403 name: FocalLengthIn35mmFilm
# name: WhiteBalance - id: 0xa406
# - id: 0xa404 name: SceneCaptureType
# name: DigitalZoomRatio - id: 0xa407
# - id: 0xa405 name: GainControl
# name: FocalLengthIn35mmFilm - id: 0xa408
# - id: 0xa406 name: Contrast
# name: SceneCaptureType - id: 0xa409
# - id: 0xa407 name: Saturation
# name: GainControl - id: 0xa40a
# - id: 0xa408 name: Sharpness
# name: Contrast - id: 0xa40b
# - id: 0xa409 name: DeviceSettingDescription
# name: Saturation - id: 0xa40c
# - id: 0xa40a name: SubjectDistanceRange
# name: Sharpness - id: 0xa420
# - id: 0xa40b name: ImageUniqueID
# name: DeviceSettingDescription - id: 0xa430
# - id: 0xa40c name: CameraOwnerName
# name: SubjectDistanceRange - id: 0xa431
# - id: 0xa420 name: BodySerialNumber
# name: ImageUniqueID - id: 0xa432
# - id: 0xa430 name: LensSpecification
# name: CameraOwnerName - id: 0xa433
# - id: 0xa431 name: LensMake
# name: BodySerialNumber - id: 0xa434
# - id: 0xa432 name: LensModel
# name: LensSpecification - id: 0xa435
# - id: 0xa433 name: LensSerialNumber
# name: LensMake
# - id: 0xa434
# name: LensModel
# - id: 0xa435
# name: LensSerialNumber

View File

@ -69,7 +69,7 @@ func (e *Exif) Parse(data []byte, visitor TagVisitor) (err error) {
ifd := NewIfd(data, byteOrder) ifd := NewIfd(data, byteOrder)
err = ifd.Scan(visitor, firstIfdOffset) err = ifd.Scan(IfdStandard, firstIfdOffset, visitor)
log.PanicIf(err) log.PanicIf(err)
return nil return nil

View File

@ -34,6 +34,13 @@ func TestIsExif_False(t *testing.T) {
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Exif failure.")
}
}()
// Open the file. // Open the file.
filepath := path.Join(assetsPath, "NDM_8901.jpg") filepath := path.Join(assetsPath, "NDM_8901.jpg")
@ -67,7 +74,7 @@ func TestParse(t *testing.T) {
ti := NewTagIndex() ti := NewTagIndex()
tags := make([]string, 0) tags := make([]string, 0)
visitor := func(tagId uint16, tagType TagType, valueContext ValueContext) (err error) { visitor := func(indexedIfdName string, tagId uint16, tagType TagType, valueContext ValueContext) (err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
@ -75,19 +82,26 @@ func TestParse(t *testing.T) {
} }
}() }()
it, err := ti.GetWithTagId(tagId) it, err := ti.Get(indexedIfdName, tagId)
if err != nil { if err != nil {
if err == ErrTagNotFound { if log.Is(err, ErrTagNotFound) {
fmt.Printf("Unknown tag: [%s] (%04x)\n", indexedIfdName, tagId)
return nil return nil
} else { } else {
log.Panic(err) log.Panic(err)
} }
} }
valueString, err := tagType.ValueString(valueContext, true) // TODO(dustin): Finish case-specific parsing of known undefined values.
log.PanicIf(err) valueString := ""
if tagType.Type() == TypeUndefined {
valueString = "!UNDEFINED!"
} else {
valueString, err = tagType.ValueString(valueContext, true)
log.PanicIf(err)
}
description := fmt.Sprintf("ID=(0x%04x) NAME=[%s] IFD=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", tagId, it.Name, it.Ifd, valueContext.UnitCount, tagType.Name(), valueString) description := fmt.Sprintf("IFD=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", indexedIfdName, tagId, it.Name, valueContext.UnitCount, tagType.Name(), valueString)
tags = append(tags, description) tags = append(tags, description)
return nil return nil
@ -97,24 +111,65 @@ func TestParse(t *testing.T) {
log.PanicIf(err) log.PanicIf(err)
expected := []string { expected := []string {
"ID=(0x010f) NAME=[Make] IFD=[Image] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]", "IFD=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]",
"ID=(0x0110) NAME=[Model] IFD=[Image] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]", "IFD=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]",
"ID=(0x0112) NAME=[Orientation] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[1]", "IFD=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", "IFD=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", "IFD=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", "IFD=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x0132) NAME=[DateTime] IFD=[Image] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]", "IFD=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"ID=(0x013b) NAME=[Artist] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]", "IFD=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"ID=(0x0213) NAME=[YCbCrPositioning] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", "IFD=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x8298) NAME=[Copyright] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]", "IFD=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"ID=(0x8769) NAME=[ExifTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[360]", "IFD=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]",
"ID=(0x8825) NAME=[GPSTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[9554]", "IFD=[Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]",
"ID=(0x0103) NAME=[Compression] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[6]", "IFD=[Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]",
"ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", "IFD=[Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]",
"ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]", "IFD=[Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]",
"ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]", "IFD=[Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x0201) NAME=[JPEGInterchangeFormat] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[11444]", "IFD=[Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]",
"ID=(0x0202) NAME=[JPEGInterchangeFormatLength] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[21491]", "IFD=[Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD=[Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD=[Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[614400/65536]",
"IFD=[Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[262144/65536]",
"IFD=[Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[0/1]",
"IFD=[Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[5]",
"IFD=[Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[16]",
"IFD=[Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[16/1]",
"IFD=[Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD=[Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD=[Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD=[Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"IFD=[Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[3840]",
"IFD=[Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[2560]",
"IFD=[Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[9326]",
"IFD=[Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]",
"IFD=[Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[!UNDEFINED!]",
"IFD=[Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[3840000/1461]",
"IFD=[Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[2560000/972]",
"IFD=[Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD=[Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD=[Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD=[Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD=[Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD=[Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD=[Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]",
"IFD=[Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[16/1]",
"IFD=[Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]",
"IFD=[Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]",
"IFD=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[9554]",
"IFD=[GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[2]",
"IFD=[IFD] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]",
"IFD=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD=[IFD] ID=(0x0201) NAME=[JPEGInterchangeFormat] COUNT=(1) TYPE=[LONG] VALUE=[11444]",
"IFD=[IFD] ID=(0x0202) NAME=[JPEGInterchangeFormatLength] COUNT=(1) TYPE=[LONG] VALUE=[21491]",
} }
if reflect.DeepEqual(tags, expected) == false { if reflect.DeepEqual(tags, expected) == false {

230
ifd.go
View File

@ -2,7 +2,6 @@ package exif
import ( import (
"bytes" "bytes"
"io"
"encoding/binary" "encoding/binary"
@ -14,6 +13,77 @@ var (
) )
// IfdTagEnumerator knows how to decode an IFD and all of the tags it
// describes. Note that the IFDs and the actual values floating throughout the
// whole EXIF block, but the IFD itself has just a minor header and a set of
// repeating, statically-sized records. So, the tags (though not their values)
// are fairly simple to enumerate.
type IfdTagEnumerator struct {
byteOrder IfdByteOrder
rawExif []byte
ifdOffset uint32
buffer *bytes.Buffer
}
func NewIfdTagEnumerator(rawExif []byte, byteOrder IfdByteOrder, ifdOffset uint32) (ite *IfdTagEnumerator) {
ite = &IfdTagEnumerator{
rawExif: rawExif,
byteOrder: byteOrder,
buffer: bytes.NewBuffer(rawExif[ifdOffset:]),
}
return ite
}
// 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))
}
}()
raw = make([]byte, 2)
_, err = ife.buffer.Read(raw)
log.PanicIf(err)
if ife.byteOrder.IsLittleEndian() == true {
value = binary.LittleEndian.Uint16(raw)
} else {
value = binary.BigEndian.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))
}
}()
raw = make([]byte, 4)
_, err = ife.buffer.Read(raw)
log.PanicIf(err)
if ife.byteOrder.IsLittleEndian() == true {
value = binary.LittleEndian.Uint32(raw)
} else {
value = binary.BigEndian.Uint32(raw)
}
return value, raw, nil
}
type Ifd struct { type Ifd struct {
data []byte data []byte
buffer *bytes.Buffer buffer *bytes.Buffer
@ -31,70 +101,6 @@ func NewIfd(data []byte, byteOrder IfdByteOrder) *Ifd {
} }
} }
// read is a wrapper around the built-in reader that applies which endianness
// we are.
func (ifd *Ifd) read(r io.Reader, into interface{}) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if ifd.byteOrder.IsLittleEndian() == true {
err := binary.Read(r, binary.LittleEndian, into)
log.PanicIf(err)
} else {
err := binary.Read(r, binary.BigEndian, into)
log.PanicIf(err)
}
return 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 (ifd *Ifd) getUint16() (value uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = ifd.read(ifd.buffer, &value)
log.PanicIf(err)
ifd.currentOffset += 2
return value, 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 (ifd *Ifd) getUint32() (value uint32, raw []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
raw = make([]byte, 4)
_, err = ifd.buffer.Read(raw)
log.PanicIf(err)
ifd.currentOffset += 4
if ifd.byteOrder.IsBigEndian() {
value = binary.BigEndian.Uint32(raw)
} else {
value = binary.LittleEndian.Uint32(raw)
}
return value, raw, nil
}
// ValueContext describes all of the parameters required to find and extract // ValueContext describes all of the parameters required to find and extract
// the actual tag value. // the actual tag value.
type ValueContext struct { type ValueContext struct {
@ -104,44 +110,65 @@ type ValueContext struct {
RawExif []byte RawExif []byte
} }
func (ifd *Ifd) getTagEnumerator(ifdOffset uint32) (ite *IfdTagEnumerator) {
ite = NewIfdTagEnumerator(
ifd.data[ifd.ifdTopOffset:],
ifd.byteOrder,
ifdOffset)
return ite
}
// TagVisitor is an optional callback that can get hit for every tag we parse // TagVisitor is an optional callback that can get hit for every tag we parse
// through. `rawExif` is the byte array startign after the EXIF header (where // through. `rawExif` is the byte array startign after the EXIF header (where
// the offsets of all IFDs and values are calculated from). // the offsets of all IFDs and values are calculated from).
type TagVisitor func(tagId uint16, tagType TagType, valueContext ValueContext) (err error) type TagVisitor func(indexedIfdName string, tagId uint16, tagType TagType, valueContext ValueContext) (err error)
// parseCurrentIfd decodes the IFD block that we're currently sitting on the // parseIfd decodes the IFD block that we're currently sitting on the first
// first byte of. // byte of.
func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err error) { func (ifd *Ifd) parseIfd(ifdName string, ifdIndex int, ifdOffset uint32, visitor TagVisitor) (nextIfdOffset uint32, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
ifdLogger.Debugf(nil, "Parsing IFD [%s] (%d) at offset (%04x).", ifdName, ifdIndex, ifdOffset)
tagCount, err := ifd.getUint16() // Return the name of the IFD as its known in our tag-index. We should skip
// over the current IFD if this is empty (which means we don't recognize/
// understand the IFD and, therefore, don't know the tags that are valid for
// it). Note that we could leave ignoring the tags as a responsibility for
// the visitor, but then it'd be easy for people to integrate that logic and
// not realize that they needed to specially handle an empty IFD name until
// they happened upon some obscure media one day and suddenly have issue if
// they unwittingly write something that breaks in that situation.
indexedIfdName := IfdName(ifdName, ifdIndex)
if indexedIfdName == "" {
ifdLogger.Debugf(nil, "IFD not known and will not be visited: [%s] (%d)", ifdName, ifdIndex)
}
ite := ifd.getTagEnumerator(ifdOffset)
tagCount, _, err := ite.getUint16()
log.PanicIf(err) log.PanicIf(err)
ifdLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount) ifdLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount)
for i := uint16(0); i < tagCount; i++ { for i := uint16(0); i < tagCount; i++ {
tagId, _, err := ite.getUint16()
// TODO(dustin): !! 0x8769 tag-IDs are child IFDs. We need to be able to recurse.
tagId, err := ifd.getUint16()
log.PanicIf(err) log.PanicIf(err)
tagType, err := ifd.getUint16() tagType, _, err := ite.getUint16()
log.PanicIf(err) log.PanicIf(err)
unitCount, _, err := ifd.getUint32() unitCount, _, err := ite.getUint32()
log.PanicIf(err) log.PanicIf(err)
valueOffset, rawValueOffset, err := ifd.getUint32() valueOffset, rawValueOffset, err := ite.getUint32()
log.PanicIf(err) log.PanicIf(err)
if visitor != nil { if visitor != nil && indexedIfdName != "" {
tt := NewTagType(tagType, ifd.byteOrder) tt := NewTagType(tagType, ifd.byteOrder)
vc := ValueContext{ vc := ValueContext{
@ -151,12 +178,20 @@ func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err e
RawExif: ifd.data[ifd.ifdTopOffset:], RawExif: ifd.data[ifd.ifdTopOffset:],
} }
err := visitor(tagId, tt, vc) err := visitor(indexedIfdName, tagId, tt, vc)
log.PanicIf(err)
}
childIfdName, isIfd := IsIfdTag(tagId)
if isIfd == true {
ifdLogger.Debugf(nil, "Descending to IFD [%s].", childIfdName)
err := ifd.Scan(childIfdName, valueOffset, visitor)
log.PanicIf(err) log.PanicIf(err)
} }
} }
nextIfdOffset, _, err = ifd.getUint32() nextIfdOffset, _, err = ite.getUint32()
log.PanicIf(err) log.PanicIf(err)
ifdLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset) ifdLogger.Debugf(nil, "Next IFD at offset: (%08x)", nextIfdOffset)
@ -164,50 +199,23 @@ func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err e
return nextIfdOffset, nil return nextIfdOffset, nil
} }
// forwardToIfd jumps to the beginning of an IFD block that starts on or after
// the current position.
func (ifd *Ifd) forwardToIfd(ifdOffset uint32) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ifdLogger.Debugf(nil, "Forwarding to IFD. TOP-OFFSET=(%d) IFD-OFFSET=(%d)", ifd.ifdTopOffset, ifdOffset)
nextOffset := ifd.ifdTopOffset + ifdOffset
// We're assuming the guarantee that the next IFD will follow the
// current one. So, figure out how far it is from our current position.
delta := nextOffset - ifd.currentOffset
ifd.buffer.Next(int(delta))
ifd.currentOffset = nextOffset
return nil
}
// Scan enumerates the different EXIF blocks (called IFDs). // Scan enumerates the different EXIF blocks (called IFDs).
func (ifd *Ifd) Scan(visitor TagVisitor, firstIfdOffset uint32) (err error) { func (ifd *Ifd) Scan(ifdName string, ifdOffset uint32, visitor TagVisitor) (err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
err = ifd.forwardToIfd(firstIfdOffset) for ifdIndex := 0;; ifdIndex++ {
log.PanicIf(err) nextIfdOffset, err := ifd.parseIfd(ifdName, ifdIndex, ifdOffset, visitor)
for {
nextIfdOffset, err := ifd.parseCurrentIfd(visitor)
log.PanicIf(err) log.PanicIf(err)
if nextIfdOffset == 0 { if nextIfdOffset == 0 {
break break
} }
err = ifd.forwardToIfd(nextIfdOffset) ifdOffset = nextIfdOffset
log.PanicIf(err)
} }
return nil return nil

57
tags.go
View File

@ -10,8 +10,21 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
const (
IfdStandard = "IFD"
IfdExif = "Exif"
IfdGps = "GPSInfo"
IfdIop = "Iop"
)
var ( var (
tagDataFilepath = "" tagDataFilepath = ""
IfdTags = map[uint16]string {
0x8769: IfdExif,
0x8825: IfdGps,
0xA005: IfdIop,
}
) )
var ( var (
@ -52,7 +65,6 @@ func (it IndexedTag) Is(id uint16) bool {
type TagIndex struct { type TagIndex struct {
tagsByIfd map[string]map[uint16]*IndexedTag tagsByIfd map[string]map[uint16]*IndexedTag
tagsById map[uint16]*IndexedTag
} }
func NewTagIndex() *TagIndex { func NewTagIndex() *TagIndex {
@ -87,7 +99,6 @@ func (ti *TagIndex) load() (err error) {
// Load structure. // Load structure.
tagsById := make(map[uint16]*IndexedTag)
tagsByIfd := make(map[string]map[uint16]*IndexedTag) tagsByIfd := make(map[string]map[uint16]*IndexedTag)
count := 0 count := 0
@ -102,25 +113,22 @@ func (ti *TagIndex) load() (err error) {
Name: tagName, Name: tagName,
} }
if _, found := tagsById[tagId]; found == true {
log.Panicf("tag-ID defined more than once: (%02x)", tagId)
}
tagsById[tagId] = tag
family, found := tagsByIfd[ifdName] family, found := tagsByIfd[ifdName]
if found == false { if found == false {
family = make(map[uint16]*IndexedTag) family = make(map[uint16]*IndexedTag)
tagsByIfd[ifdName] = family tagsByIfd[ifdName] = family
} }
if _, found := family[tagId]; found == true {
log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", ifdName, tagId)
}
family[tagId] = tag family[tagId] = tag
count++ count++
} }
} }
ti.tagsById = tagsById
ti.tagsByIfd = tagsByIfd ti.tagsByIfd = tagsByIfd
tagsLogger.Debugf(nil, "(%d) tags loaded.", count) tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
@ -128,14 +136,19 @@ func (ti *TagIndex) load() (err error) {
return nil return nil
} }
func (ti *TagIndex) GetWithTagId(id uint16) (it *IndexedTag, err error) { func (ti *TagIndex) Get(ifdName string, id uint16) (it *IndexedTag, err error) {
defer func() { defer func() {
if state := recover(); state != nil { if state := recover(); state != nil {
err = log.Wrap(state.(error)) err = log.Wrap(state.(error))
} }
}() }()
it, found := ti.tagsById[id] family, found := ti.tagsByIfd[ifdName]
if found == false {
log.Panic(ErrTagNotFound)
}
it, found = family[id]
if found == false { if found == false {
log.Panic(ErrTagNotFound) log.Panic(ErrTagNotFound)
} }
@ -143,6 +156,28 @@ func (ti *TagIndex) GetWithTagId(id uint16) (it *IndexedTag, err error) {
return it, nil return it, nil
} }
// GetIfdName returns the known index name for the tags that are expected/
// allowed for the IFD. If there's an error, returns "". If returns "", the IFD
// should be skipped.
func IfdName(ifdName string, ifdIndex int) string {
// There's an IFD0 and IFD1, but the others must be unique.
if ifdName == IfdStandard && ifdIndex > 1 {
tagsLogger.Errorf(nil, "The 'IFD' IFD can not occur more than twice: [%s]. Ignoring IFD.", ifdName)
return ""
} else if ifdName != IfdStandard && ifdIndex > 0 {
tagsLogger.Errorf(nil, "Only the 'IFD' IFD can be repeated: [%s]. Ignoring IFD.", ifdName)
return ""
}
return ifdName
}
// IsIfdTag returns true if the given tag points to a child IFD block.
func IsIfdTag(tagId uint16) (name string, found bool) {
name, found = IfdTags[tagId]
return name, found
}
func init() { func init() {
goPath := os.Getenv("GOPATH") goPath := os.Getenv("GOPATH")
if goPath == "" { if goPath == "" {

View File

@ -6,13 +6,15 @@ import (
"github.com/dsoprea/go-logging" "github.com/dsoprea/go-logging"
) )
func TestGetWithTagId(t *testing.T) { func TestGet(t *testing.T) {
ti := NewTagIndex() ti := NewTagIndex()
it, err := ti.GetWithTagId(0x10f) indexedIfdName := IfdName(IfdStandard, 0)
it, err := ti.Get(indexedIfdName, 0x10f)
log.PanicIf(err) log.PanicIf(err)
if it.Is(0x10f) == false || it.IsName("Image", "Make") == false { if it.Is(0x10f) == false || it.IsName("IFD", "Make") == false {
t.Fatalf("tag info not correct") t.Fatalf("tag info not correct")
} }
} }

View File

@ -16,7 +16,7 @@ const (
TypeShort = uint16(3) TypeShort = uint16(3)
TypeLong = uint16(4) TypeLong = uint16(4)
TypeRational = uint16(5) TypeRational = uint16(5)
TypeUndefined = uint16(6) TypeUndefined = uint16(7)
TypeSignedLong = uint16(9) TypeSignedLong = uint16(9)
TypeSignedRational = uint16(10) TypeSignedRational = uint16(10)
) )
@ -586,7 +586,7 @@ func (tt TagType) ValueString(valueContext ValueContext, justFirst bool) (value
} else { } else {
return "", nil return "", nil
} }
} else if tt.Type() == TypeRational { } else if tt.Type() == TypeSignedRational {
raw, err := tt.ReadSignedRationalValues(valueContext) raw, err := tt.ReadSignedRationalValues(valueContext)
log.PanicIf(err) log.PanicIf(err)
@ -603,7 +603,7 @@ func (tt TagType) ValueString(valueContext ValueContext, justFirst bool) (value
return "", nil return "", nil
} }
} else { } else {
log.Panicf("value of type [%s] is unparseable", tt) log.Panicf("value of type (%d) [%s] is unparseable", tt.Type(), tt)
// Never called. // Never called.
return "", nil return "", nil