diff --git a/assets/tags.yaml b/assets/tags.yaml index 416111f..0bc7fbd 100644 --- a/assets/tags.yaml +++ b/assets/tags.yaml @@ -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 diff --git a/exif.go b/exif.go index f73a5cb..c3554c6 100644 --- a/exif.go +++ b/exif.go @@ -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 diff --git a/exif_test.go b/exif_test.go index cd12551..e1696c5 100644 --- a/exif_test.go +++ b/exif_test.go @@ -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 { diff --git a/ifd.go b/ifd.go index 11a5dfb..340bf87 100644 --- a/ifd.go +++ b/ifd.go @@ -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 diff --git a/tags.go b/tags.go index ad99be6..aa4de8a 100644 --- a/tags.go +++ b/tags.go @@ -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 == "" { diff --git a/tags_test.go b/tags_test.go index 340fc5d..7207f3e 100644 --- a/tags_test.go +++ b/tags_test.go @@ -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") } } diff --git a/type.go b/type.go index 340308b..38999a6 100644 --- a/type.go +++ b/type.go @@ -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