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

View File

@ -34,6 +34,13 @@ func TestIsExif_False(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.
filepath := path.Join(assetsPath, "NDM_8901.jpg")
@ -67,7 +74,7 @@ func TestParse(t *testing.T) {
ti := NewTagIndex()
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() {
if state := recover(); state != nil {
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 == ErrTagNotFound {
if log.Is(err, ErrTagNotFound) {
fmt.Printf("Unknown tag: [%s] (%04x)\n", indexedIfdName, tagId)
return nil
} else {
log.Panic(err)
}
}
valueString, err := tagType.ValueString(valueContext, true)
log.PanicIf(err)
// TODO(dustin): Finish case-specific parsing of known undefined values.
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)
return nil
@ -97,24 +111,65 @@ func TestParse(t *testing.T) {
log.PanicIf(err)
expected := []string {
"ID=(0x010f) NAME=[Make] IFD=[Image] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]",
"ID=(0x0110) NAME=[Model] IFD=[Image] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]",
"ID=(0x0112) NAME=[Orientation] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x0132) NAME=[DateTime] IFD=[Image] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"ID=(0x013b) NAME=[Artist] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"ID=(0x0213) NAME=[YCbCrPositioning] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x8298) NAME=[Copyright] IFD=[Image] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"ID=(0x8769) NAME=[ExifTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[360]",
"ID=(0x8825) NAME=[GPSTag] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[9554]",
"ID=(0x0103) NAME=[Compression] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[6]",
"ID=(0x011a) NAME=[XResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x011b) NAME=[YResolution] IFD=[Image] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"ID=(0x0128) NAME=[ResolutionUnit] IFD=[Image] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"ID=(0x0201) NAME=[JPEGInterchangeFormat] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[11444]",
"ID=(0x0202) NAME=[JPEGInterchangeFormatLength] IFD=[Image] COUNT=(1) TYPE=[LONG] VALUE=[21491]",
"IFD=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]",
"IFD=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]",
"IFD=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"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=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]",
"IFD=[Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]",
"IFD=[Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]",
"IFD=[Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]",
"IFD=[Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]",
"IFD=[Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD=[Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]",
"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 {

230
ifd.go
View File

@ -2,7 +2,6 @@ package exif
import (
"bytes"
"io"
"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 {
data []byte
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
// the actual tag value.
type ValueContext struct {
@ -104,44 +110,65 @@ type ValueContext struct {
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
// through. `rawExif` is the byte array startign after the EXIF header (where
// 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
// first byte of.
func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err error) {
// parseIfd decodes the IFD block that we're currently sitting on the first
// byte of.
func (ifd *Ifd) parseIfd(ifdName string, ifdIndex int, ifdOffset uint32, visitor TagVisitor) (nextIfdOffset uint32, err error) {
defer func() {
if state := recover(); state != nil {
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)
ifdLogger.Debugf(nil, "Current IFD tag-count: (%d)", tagCount)
for i := uint16(0); i < tagCount; i++ {
// TODO(dustin): !! 0x8769 tag-IDs are child IFDs. We need to be able to recurse.
tagId, err := ifd.getUint16()
tagId, _, err := ite.getUint16()
log.PanicIf(err)
tagType, err := ifd.getUint16()
tagType, _, err := ite.getUint16()
log.PanicIf(err)
unitCount, _, err := ifd.getUint32()
unitCount, _, err := ite.getUint32()
log.PanicIf(err)
valueOffset, rawValueOffset, err := ifd.getUint32()
valueOffset, rawValueOffset, err := ite.getUint32()
log.PanicIf(err)
if visitor != nil {
if visitor != nil && indexedIfdName != "" {
tt := NewTagType(tagType, ifd.byteOrder)
vc := ValueContext{
@ -151,12 +178,20 @@ func (ifd *Ifd) parseCurrentIfd(visitor TagVisitor) (nextIfdOffset uint32, err e
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)
}
}
nextIfdOffset, _, err = ifd.getUint32()
nextIfdOffset, _, err = ite.getUint32()
log.PanicIf(err)
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
}
// 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).
func (ifd *Ifd) Scan(visitor TagVisitor, firstIfdOffset uint32) (err error) {
func (ifd *Ifd) Scan(ifdName string, ifdOffset uint32, visitor TagVisitor) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = ifd.forwardToIfd(firstIfdOffset)
log.PanicIf(err)
for {
nextIfdOffset, err := ifd.parseCurrentIfd(visitor)
for ifdIndex := 0;; ifdIndex++ {
nextIfdOffset, err := ifd.parseIfd(ifdName, ifdIndex, ifdOffset, visitor)
log.PanicIf(err)
if nextIfdOffset == 0 {
break
}
err = ifd.forwardToIfd(nextIfdOffset)
log.PanicIf(err)
ifdOffset = nextIfdOffset
}
return nil

57
tags.go
View File

@ -10,8 +10,21 @@ import (
"github.com/dsoprea/go-logging"
)
const (
IfdStandard = "IFD"
IfdExif = "Exif"
IfdGps = "GPSInfo"
IfdIop = "Iop"
)
var (
tagDataFilepath = ""
IfdTags = map[uint16]string {
0x8769: IfdExif,
0x8825: IfdGps,
0xA005: IfdIop,
}
)
var (
@ -52,7 +65,6 @@ func (it IndexedTag) Is(id uint16) bool {
type TagIndex struct {
tagsByIfd map[string]map[uint16]*IndexedTag
tagsById map[uint16]*IndexedTag
}
func NewTagIndex() *TagIndex {
@ -87,7 +99,6 @@ func (ti *TagIndex) load() (err error) {
// Load structure.
tagsById := make(map[uint16]*IndexedTag)
tagsByIfd := make(map[string]map[uint16]*IndexedTag)
count := 0
@ -102,25 +113,22 @@ func (ti *TagIndex) load() (err error) {
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]
if found == false {
family = make(map[uint16]*IndexedTag)
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
count++
}
}
ti.tagsById = tagsById
ti.tagsByIfd = tagsByIfd
tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
@ -128,14 +136,19 @@ func (ti *TagIndex) load() (err error) {
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() {
if state := recover(); state != nil {
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 {
log.Panic(ErrTagNotFound)
}
@ -143,6 +156,28 @@ func (ti *TagIndex) GetWithTagId(id uint16) (it *IndexedTag, err error) {
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() {
goPath := os.Getenv("GOPATH")
if goPath == "" {

View File

@ -6,13 +6,15 @@ import (
"github.com/dsoprea/go-logging"
)
func TestGetWithTagId(t *testing.T) {
func TestGet(t *testing.T) {
ti := NewTagIndex()
it, err := ti.GetWithTagId(0x10f)
indexedIfdName := IfdName(IfdStandard, 0)
it, err := ti.Get(indexedIfdName, 0x10f)
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")
}
}

View File

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