Add v2 release module

for/master 2.0.0
Dustin Oprea 2020-01-02 00:10:07 -05:00
parent 74567945ac
commit 903910b6a7
49 changed files with 17833 additions and 6 deletions

View File

@ -17,7 +17,7 @@ This package provides native Go functionality to parse an existing EXIF block, u
To get the project and dependencies:
```
$ go get -t github.com/dsoprea/go-exif
$ go get -t github.com/dsoprea/go-exif/v2
```
@ -26,15 +26,15 @@ $ go get -t github.com/dsoprea/go-exif
The traditional method:
```
$ go test github.com/dsoprea/go-exif
$ go test github.com/dsoprea/go-exif/v2
```
## Usage
The package provides a set of [working examples](https://godoc.org/github.com/dsoprea/go-exif#pkg-examples) and is covered by unit-tests. Please look to these for getting familiar with how to read and write EXIF.
The package provides a set of [working examples](https://godoc.org/github.com/dsoprea/go-exif/v2#pkg-examples) and is covered by unit-tests. Please look to these for getting familiar with how to read and write EXIF.
In general, this package is concerned only with parsing and encoding raw EXIF data. It does not understand specific file-formats. This package assumes you know how to extract the raw EXIF data from a file, such as a JPEG, and, if you want to update it, know then how to write it back. File-specific formats are not the concern of *go-exif*, though we provide [exif.SearchAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif#SearchAndExtractExif) and [exif.SearchFileAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif#SearchFileAndExtractExif) as brute-force search mechanisms that will help you explore the EXIF information for newer formats that you might not yet have any way to parse.
In general, this package is concerned only with parsing and encoding raw EXIF data. It does not understand specific file-formats. This package assumes you know how to extract the raw EXIF data from a file, such as a JPEG, and, if you want to update it, know then how to write it back. File-specific formats are not the concern of *go-exif*, though we provide [exif.SearchAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif/v2#SearchAndExtractExif) and [exif.SearchFileAndExtractExif](https://godoc.org/github.com/dsoprea/go-exif/v2#SearchFileAndExtractExif) as brute-force search mechanisms that will help you explore the EXIF information for newer formats that you might not yet have any way to parse.
That said, the author also provides [go-jpeg-image-structure](https://github.com/dsoprea/go-jpeg-image-structure) and [go-png-image-structure](https://github.com/dsoprea/go-png-image-structure) to support properly reading and writing JPEG and PNG images. See the [SetExif example in go-jpeg-image-structure](https://godoc.org/github.com/dsoprea/go-jpeg-image-structure#example-SegmentList-SetExif) for practical information on getting started with JPEG files.
@ -55,8 +55,7 @@ There is an "IFD mapping" and a "tag index" that must be created and passed to t
There is a reader implementation included as a runnable tool:
```
$ go get github.com/dsoprea/go-exif/exif-read-tool
$ go build -o exif-read-tool github.com/dsoprea/go-exif/exif-read-tool
$ go get github.com/dsoprea/go-exif/v2/exif-read-tool
$ exif-read-tool -filepath "<media file-path>"
```

9
v2/LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT LICENSE
Copyright 2019 Dustin Oprea
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

BIN
v2/assets/DC-008-2012_E.pdf Normal file

Binary file not shown.

Binary file not shown.

BIN
v2/assets/NDM_8901.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

BIN
v2/assets/NDM_8901.jpg.exif Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

695
v2/assets/exif_read.json Normal file

File diff suppressed because one or more lines are too long

BIN
v2/assets/gps.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@ -0,0 +1 @@
ruamel.yaml

2492
v2/assets/raw_tags/tags.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python2.7
"""
Parses the table-data from view-source:http://www.exiv2.org/tags.html
"""
import sys
import collections
import xml.etree.ElementTree as ET
import ruamel.yaml
# Prepare YAML to write hex expressions (otherwise the hex will be a string and
# quotes or a decimal and a base-10 number).
class HexInt(int):
pass
def representer(dumper, data):
return \
ruamel.yaml.ScalarNode(
'tag:yaml.org,2002:int',
'0x{:04x}'.format(data))
ruamel.yaml.add_representer(HexInt, representer)
def _write(tags):
writeable = {}
for tag in tags:
pivot = tag['fq_key'].rindex('.')
item = {
'id': HexInt(tag['id_dec']),
'name': tag['fq_key'][pivot + 1:],
'type_name': tag['type'].upper(),
}
ifdName = tag['ifd']
if ifdName == 'Image':
ifdName = 'IFD'
if ifdName == 'Photo':
ifdName = 'Exif'
# UserComment. Has invalid type "COMMENT".
if item['id'] == 0x9286 and ifdName == 'Exif':
item['type_name'] = 'UNDEFINED'
try:
writeable[ifdName].append(item)
except KeyError:
writeable[ifdName] = [item]
with open('tags.yaml', 'w') as f:
# Otherwise, the next dictionaries will look like Python dictionaries,
# whatever sense that makes.
ruamel.yaml.dump(writeable, f, default_flow_style=False)
def _main():
tree = ET.parse('tags.html')
root = tree.getroot()
labels = [
'id_hex',
'id_dec',
'ifd',
'fq_key',
'type',
'description',
]
tags = []
for node in root.iter('tr'):
values = [child.text.strip() for child in node.iter('td')]
# Skips the header row.
if not values:
continue
assert \
len(values) == len(labels), \
"Row fields count not the same as labels: {}".format(values)
tags.append(dict(zip(labels, values)))
_write(tags)
if __name__ == '__main__':
_main()

944
v2/assets/tags.yaml Normal file
View File

@ -0,0 +1,944 @@
# Notes:
#
# This file was produced from http://www.exiv2.org/tags.html, using the included
# tool, though that document appears to have some duplicates when all IDs are
# supposed to be unique (EXIF information only has IDs, not IFDs; IFDs are
# determined by our pre-existing knowledge of those tags).
#
# The webpage that we've produced this file from appears to indicate that
# ImageWidth is represented by both 0x0100 and 0x0001 depending on whether the
# encoding is RGB or YCbCr.
Exif:
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8830
name: SensitivityType
type_name: SHORT
- id: 0x8831
name: StandardOutputSensitivity
type_name: LONG
- id: 0x8832
name: RecommendedExposureIndex
type_name: LONG
- id: 0x8833
name: ISOSpeed
type_name: LONG
- id: 0x8834
name: ISOSpeedLatitudeyyy
type_name: LONG
- id: 0x8835
name: ISOSpeedLatitudezzz
type_name: LONG
- id: 0x9000
name: ExifVersion
type_name: UNDEFINED
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9004
name: DateTimeDigitized
type_name: ASCII
- id: 0x9101
name: ComponentsConfiguration
type_name: UNDEFINED
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: RATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x9214
name: SubjectArea
type_name: SHORT
- id: 0x927c
name: MakerNote
type_name: UNDEFINED
- id: 0x9286
name: UserComment
type_name: UNDEFINED
- id: 0x9290
name: SubSecTime
type_name: ASCII
- id: 0x9291
name: SubSecTimeOriginal
type_name: ASCII
- id: 0x9292
name: SubSecTimeDigitized
type_name: ASCII
- id: 0xa000
name: FlashpixVersion
type_name: UNDEFINED
- id: 0xa001
name: ColorSpace
type_name: SHORT
- id: 0xa002
name: PixelXDimension
type_name: LONG
- id: 0xa003
name: PixelYDimension
type_name: LONG
- id: 0xa004
name: RelatedSoundFile
type_name: ASCII
- id: 0xa005
name: InteroperabilityTag
type_name: LONG
- id: 0xa20b
name: FlashEnergy
type_name: RATIONAL
- id: 0xa20c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0xa20e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0xa20f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0xa210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0xa214
name: SubjectLocation
type_name: SHORT
- id: 0xa215
name: ExposureIndex
type_name: RATIONAL
- id: 0xa217
name: SensingMethod
type_name: SHORT
- id: 0xa300
name: FileSource
type_name: UNDEFINED
- id: 0xa301
name: SceneType
type_name: UNDEFINED
- id: 0xa302
name: CFAPattern
type_name: UNDEFINED
- id: 0xa401
name: CustomRendered
type_name: SHORT
- id: 0xa402
name: ExposureMode
type_name: SHORT
- id: 0xa403
name: WhiteBalance
type_name: SHORT
- id: 0xa404
name: DigitalZoomRatio
type_name: RATIONAL
- id: 0xa405
name: FocalLengthIn35mmFilm
type_name: SHORT
- id: 0xa406
name: SceneCaptureType
type_name: SHORT
- id: 0xa407
name: GainControl
type_name: SHORT
- id: 0xa408
name: Contrast
type_name: SHORT
- id: 0xa409
name: Saturation
type_name: SHORT
- id: 0xa40a
name: Sharpness
type_name: SHORT
- id: 0xa40b
name: DeviceSettingDescription
type_name: UNDEFINED
- id: 0xa40c
name: SubjectDistanceRange
type_name: SHORT
- id: 0xa420
name: ImageUniqueID
type_name: ASCII
- id: 0xa430
name: CameraOwnerName
type_name: ASCII
- id: 0xa431
name: BodySerialNumber
type_name: ASCII
- id: 0xa432
name: LensSpecification
type_name: RATIONAL
- id: 0xa433
name: LensMake
type_name: ASCII
- id: 0xa434
name: LensModel
type_name: ASCII
- id: 0xa435
name: LensSerialNumber
type_name: ASCII
GPSInfo:
- id: 0x0000
name: GPSVersionID
type_name: BYTE
- id: 0x0001
name: GPSLatitudeRef
type_name: ASCII
- id: 0x0002
name: GPSLatitude
type_name: RATIONAL
- id: 0x0003
name: GPSLongitudeRef
type_name: ASCII
- id: 0x0004
name: GPSLongitude
type_name: RATIONAL
- id: 0x0005
name: GPSAltitudeRef
type_name: BYTE
- id: 0x0006
name: GPSAltitude
type_name: RATIONAL
- id: 0x0007
name: GPSTimeStamp
type_name: RATIONAL
- id: 0x0008
name: GPSSatellites
type_name: ASCII
- id: 0x0009
name: GPSStatus
type_name: ASCII
- id: 0x000a
name: GPSMeasureMode
type_name: ASCII
- id: 0x000b
name: GPSDOP
type_name: RATIONAL
- id: 0x000c
name: GPSSpeedRef
type_name: ASCII
- id: 0x000d
name: GPSSpeed
type_name: RATIONAL
- id: 0x000e
name: GPSTrackRef
type_name: ASCII
- id: 0x000f
name: GPSTrack
type_name: RATIONAL
- id: 0x0010
name: GPSImgDirectionRef
type_name: ASCII
- id: 0x0011
name: GPSImgDirection
type_name: RATIONAL
- id: 0x0012
name: GPSMapDatum
type_name: ASCII
- id: 0x0013
name: GPSDestLatitudeRef
type_name: ASCII
- id: 0x0014
name: GPSDestLatitude
type_name: RATIONAL
- id: 0x0015
name: GPSDestLongitudeRef
type_name: ASCII
- id: 0x0016
name: GPSDestLongitude
type_name: RATIONAL
- id: 0x0017
name: GPSDestBearingRef
type_name: ASCII
- id: 0x0018
name: GPSDestBearing
type_name: RATIONAL
- id: 0x0019
name: GPSDestDistanceRef
type_name: ASCII
- id: 0x001a
name: GPSDestDistance
type_name: RATIONAL
- id: 0x001b
name: GPSProcessingMethod
type_name: UNDEFINED
- id: 0x001c
name: GPSAreaInformation
type_name: UNDEFINED
- id: 0x001d
name: GPSDateStamp
type_name: ASCII
- id: 0x001e
name: GPSDifferential
type_name: SHORT
IFD:
- id: 0x000b
name: ProcessingSoftware
type_name: ASCII
- id: 0x00fe
name: NewSubfileType
type_name: LONG
- id: 0x00ff
name: SubfileType
type_name: SHORT
- id: 0x0100
name: ImageWidth
type_name: LONG
- id: 0x0101
name: ImageLength
type_name: LONG
- id: 0x0102
name: BitsPerSample
type_name: SHORT
- id: 0x0103
name: Compression
type_name: SHORT
- id: 0x0106
name: PhotometricInterpretation
type_name: SHORT
- id: 0x0107
name: Thresholding
type_name: SHORT
- id: 0x0108
name: CellWidth
type_name: SHORT
- id: 0x0109
name: CellLength
type_name: SHORT
- id: 0x010a
name: FillOrder
type_name: SHORT
- id: 0x010d
name: DocumentName
type_name: ASCII
- id: 0x010e
name: ImageDescription
type_name: ASCII
- id: 0x010f
name: Make
type_name: ASCII
- id: 0x0110
name: Model
type_name: ASCII
- id: 0x0111
name: StripOffsets
type_name: LONG
- id: 0x0112
name: Orientation
type_name: SHORT
- id: 0x0115
name: SamplesPerPixel
type_name: SHORT
- id: 0x0116
name: RowsPerStrip
type_name: LONG
- id: 0x0117
name: StripByteCounts
type_name: LONG
- id: 0x011a
name: XResolution
type_name: RATIONAL
- id: 0x011b
name: YResolution
type_name: RATIONAL
- id: 0x011c
name: PlanarConfiguration
type_name: SHORT
- id: 0x0122
name: GrayResponseUnit
type_name: SHORT
- id: 0x0123
name: GrayResponseCurve
type_name: SHORT
- id: 0x0124
name: T4Options
type_name: LONG
- id: 0x0125
name: T6Options
type_name: LONG
- id: 0x0128
name: ResolutionUnit
type_name: SHORT
- id: 0x0129
name: PageNumber
type_name: SHORT
- id: 0x012d
name: TransferFunction
type_name: SHORT
- id: 0x0131
name: Software
type_name: ASCII
- id: 0x0132
name: DateTime
type_name: ASCII
- id: 0x013b
name: Artist
type_name: ASCII
- id: 0x013c
name: HostComputer
type_name: ASCII
- id: 0x013d
name: Predictor
type_name: SHORT
- id: 0x013e
name: WhitePoint
type_name: RATIONAL
- id: 0x013f
name: PrimaryChromaticities
type_name: RATIONAL
- id: 0x0140
name: ColorMap
type_name: SHORT
- id: 0x0141
name: HalftoneHints
type_name: SHORT
- id: 0x0142
name: TileWidth
type_name: SHORT
- id: 0x0143
name: TileLength
type_name: SHORT
- id: 0x0144
name: TileOffsets
type_name: SHORT
- id: 0x0145
name: TileByteCounts
type_name: SHORT
- id: 0x014a
name: SubIFDs
type_name: LONG
- id: 0x014c
name: InkSet
type_name: SHORT
- id: 0x014d
name: InkNames
type_name: ASCII
- id: 0x014e
name: NumberOfInks
type_name: SHORT
- id: 0x0150
name: DotRange
type_name: BYTE
- id: 0x0151
name: TargetPrinter
type_name: ASCII
- id: 0x0152
name: ExtraSamples
type_name: SHORT
- id: 0x0153
name: SampleFormat
type_name: SHORT
- id: 0x0154
name: SMinSampleValue
type_name: SHORT
- id: 0x0155
name: SMaxSampleValue
type_name: SHORT
- id: 0x0156
name: TransferRange
type_name: SHORT
- id: 0x0157
name: ClipPath
type_name: BYTE
- id: 0x0158
name: XClipPathUnits
type_name: SSHORT
- id: 0x0159
name: YClipPathUnits
type_name: SSHORT
- id: 0x015a
name: Indexed
type_name: SHORT
- id: 0x015b
name: JPEGTables
type_name: UNDEFINED
- id: 0x015f
name: OPIProxy
type_name: SHORT
- id: 0x0200
name: JPEGProc
type_name: LONG
- id: 0x0201
name: JPEGInterchangeFormat
type_name: LONG
- id: 0x0202
name: JPEGInterchangeFormatLength
type_name: LONG
- id: 0x0203
name: JPEGRestartInterval
type_name: SHORT
- id: 0x0205
name: JPEGLosslessPredictors
type_name: SHORT
- id: 0x0206
name: JPEGPointTransforms
type_name: SHORT
- id: 0x0207
name: JPEGQTables
type_name: LONG
- id: 0x0208
name: JPEGDCTables
type_name: LONG
- id: 0x0209
name: JPEGACTables
type_name: LONG
- id: 0x0211
name: YCbCrCoefficients
type_name: RATIONAL
- id: 0x0212
name: YCbCrSubSampling
type_name: SHORT
- id: 0x0213
name: YCbCrPositioning
type_name: SHORT
- id: 0x0214
name: ReferenceBlackWhite
type_name: RATIONAL
- id: 0x02bc
name: XMLPacket
type_name: BYTE
- id: 0x4746
name: Rating
type_name: SHORT
- id: 0x4749
name: RatingPercent
type_name: SHORT
- id: 0x800d
name: ImageID
type_name: ASCII
- id: 0x828d
name: CFARepeatPatternDim
type_name: SHORT
- id: 0x828e
name: CFAPattern
type_name: BYTE
- id: 0x828f
name: BatteryLevel
type_name: RATIONAL
- id: 0x8298
name: Copyright
type_name: ASCII
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x83bb
name: IPTCNAA
type_name: LONG
- id: 0x8649
name: ImageResources
type_name: BYTE
- id: 0x8769
name: ExifTag
type_name: LONG
- id: 0x8773
name: InterColorProfile
type_name: UNDEFINED
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8825
name: GPSTag
type_name: LONG
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8829
name: Interlace
type_name: SHORT
- id: 0x882a
name: TimeZoneOffset
type_name: SSHORT
- id: 0x882b
name: SelfTimerMode
type_name: SHORT
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: SRATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x920b
name: FlashEnergy
type_name: RATIONAL
- id: 0x920c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0x920d
name: Noise
type_name: UNDEFINED
- id: 0x920e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0x920f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0x9210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0x9211
name: ImageNumber
type_name: LONG
- id: 0x9212
name: SecurityClassification
type_name: ASCII
- id: 0x9213
name: ImageHistory
type_name: ASCII
- id: 0x9214
name: SubjectLocation
type_name: SHORT
- id: 0x9215
name: ExposureIndex
type_name: RATIONAL
- id: 0x9216
name: TIFFEPStandardID
type_name: BYTE
- id: 0x9217
name: SensingMethod
type_name: SHORT
- id: 0x9c9b
name: XPTitle
type_name: BYTE
- id: 0x9c9c
name: XPComment
type_name: BYTE
- id: 0x9c9d
name: XPAuthor
type_name: BYTE
- id: 0x9c9e
name: XPKeywords
type_name: BYTE
- id: 0x9c9f
name: XPSubject
type_name: BYTE
- id: 0xc4a5
name: PrintImageMatching
type_name: UNDEFINED
- id: 0xc612
name: DNGVersion
type_name: BYTE
- id: 0xc613
name: DNGBackwardVersion
type_name: BYTE
- id: 0xc614
name: UniqueCameraModel
type_name: ASCII
- id: 0xc615
name: LocalizedCameraModel
type_name: BYTE
- id: 0xc616
name: CFAPlaneColor
type_name: BYTE
- id: 0xc617
name: CFALayout
type_name: SHORT
- id: 0xc618
name: LinearizationTable
type_name: SHORT
- id: 0xc619
name: BlackLevelRepeatDim
type_name: SHORT
- id: 0xc61a
name: BlackLevel
type_name: RATIONAL
- id: 0xc61b
name: BlackLevelDeltaH
type_name: SRATIONAL
- id: 0xc61c
name: BlackLevelDeltaV
type_name: SRATIONAL
- id: 0xc61d
name: WhiteLevel
type_name: SHORT
- id: 0xc61e
name: DefaultScale
type_name: RATIONAL
- id: 0xc61f
name: DefaultCropOrigin
type_name: SHORT
- id: 0xc620
name: DefaultCropSize
type_name: SHORT
- id: 0xc621
name: ColorMatrix1
type_name: SRATIONAL
- id: 0xc622
name: ColorMatrix2
type_name: SRATIONAL
- id: 0xc623
name: CameraCalibration1
type_name: SRATIONAL
- id: 0xc624
name: CameraCalibration2
type_name: SRATIONAL
- id: 0xc625
name: ReductionMatrix1
type_name: SRATIONAL
- id: 0xc626
name: ReductionMatrix2
type_name: SRATIONAL
- id: 0xc627
name: AnalogBalance
type_name: RATIONAL
- id: 0xc628
name: AsShotNeutral
type_name: SHORT
- id: 0xc629
name: AsShotWhiteXY
type_name: RATIONAL
- id: 0xc62a
name: BaselineExposure
type_name: SRATIONAL
- id: 0xc62b
name: BaselineNoise
type_name: RATIONAL
- id: 0xc62c
name: BaselineSharpness
type_name: RATIONAL
- id: 0xc62d
name: BayerGreenSplit
type_name: LONG
- id: 0xc62e
name: LinearResponseLimit
type_name: RATIONAL
- id: 0xc62f
name: CameraSerialNumber
type_name: ASCII
- id: 0xc630
name: LensInfo
type_name: RATIONAL
- id: 0xc631
name: ChromaBlurRadius
type_name: RATIONAL
- id: 0xc632
name: AntiAliasStrength
type_name: RATIONAL
- id: 0xc633
name: ShadowScale
type_name: SRATIONAL
- id: 0xc634
name: DNGPrivateData
type_name: BYTE
- id: 0xc635
name: MakerNoteSafety
type_name: SHORT
- id: 0xc65a
name: CalibrationIlluminant1
type_name: SHORT
- id: 0xc65b
name: CalibrationIlluminant2
type_name: SHORT
- id: 0xc65c
name: BestQualityScale
type_name: RATIONAL
- id: 0xc65d
name: RawDataUniqueID
type_name: BYTE
- id: 0xc68b
name: OriginalRawFileName
type_name: BYTE
- id: 0xc68c
name: OriginalRawFileData
type_name: UNDEFINED
- id: 0xc68d
name: ActiveArea
type_name: SHORT
- id: 0xc68e
name: MaskedAreas
type_name: SHORT
- id: 0xc68f
name: AsShotICCProfile
type_name: UNDEFINED
- id: 0xc690
name: AsShotPreProfileMatrix
type_name: SRATIONAL
- id: 0xc691
name: CurrentICCProfile
type_name: UNDEFINED
- id: 0xc692
name: CurrentPreProfileMatrix
type_name: SRATIONAL
- id: 0xc6bf
name: ColorimetricReference
type_name: SHORT
- id: 0xc6f3
name: CameraCalibrationSignature
type_name: BYTE
- id: 0xc6f4
name: ProfileCalibrationSignature
type_name: BYTE
- id: 0xc6f6
name: AsShotProfileName
type_name: BYTE
- id: 0xc6f7
name: NoiseReductionApplied
type_name: RATIONAL
- id: 0xc6f8
name: ProfileName
type_name: BYTE
- id: 0xc6f9
name: ProfileHueSatMapDims
type_name: LONG
- id: 0xc6fa
name: ProfileHueSatMapData1
type_name: FLOAT
- id: 0xc6fb
name: ProfileHueSatMapData2
type_name: FLOAT
- id: 0xc6fc
name: ProfileToneCurve
type_name: FLOAT
- id: 0xc6fd
name: ProfileEmbedPolicy
type_name: LONG
- id: 0xc6fe
name: ProfileCopyright
type_name: BYTE
- id: 0xc714
name: ForwardMatrix1
type_name: SRATIONAL
- id: 0xc715
name: ForwardMatrix2
type_name: SRATIONAL
- id: 0xc716
name: PreviewApplicationName
type_name: BYTE
- id: 0xc717
name: PreviewApplicationVersion
type_name: BYTE
- id: 0xc718
name: PreviewSettingsName
type_name: BYTE
- id: 0xc719
name: PreviewSettingsDigest
type_name: BYTE
- id: 0xc71a
name: PreviewColorSpace
type_name: LONG
- id: 0xc71b
name: PreviewDateTime
type_name: ASCII
- id: 0xc71c
name: RawImageDigest
type_name: UNDEFINED
- id: 0xc71d
name: OriginalRawFileDigest
type_name: UNDEFINED
- id: 0xc71e
name: SubTileBlockSize
type_name: LONG
- id: 0xc71f
name: RowInterleaveFactor
type_name: LONG
- id: 0xc725
name: ProfileLookTableDims
type_name: LONG
- id: 0xc726
name: ProfileLookTableData
type_name: FLOAT
- id: 0xc740
name: OpcodeList1
type_name: UNDEFINED
- id: 0xc741
name: OpcodeList2
type_name: UNDEFINED
- id: 0xc74e
name: OpcodeList3
type_name: UNDEFINED
- id: 0xc761
name: NoiseProfile
type_name: DOUBLE
Iop:
- id: 0x0001
name: InteroperabilityIndex
type_name: ASCII
- id: 0x0002
name: InteroperabilityVersion
type_name: UNDEFINED
- id: 0x1000
name: RelatedImageFileFormat
type_name: ASCII
- id: 0x1001
name: RelatedImageWidth
type_name: LONG
- id: 0x1002
name: RelatedImageLength
type_name: LONG

187
v2/common_test.go Normal file
View File

@ -0,0 +1,187 @@
package exif
import (
"os"
"path"
"reflect"
"testing"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
var (
assetsPath = ""
testImageFilepath = ""
testExifData = make([]byte, 0)
)
func getExifSimpleTestIb() *IfdBuilder {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
err = ib.AddStandard(0x000b, "asciivalue")
log.PanicIf(err)
err = ib.AddStandard(0x00ff, []uint16{0x1122})
log.PanicIf(err)
err = ib.AddStandard(0x0100, []uint32{0x33445566})
log.PanicIf(err)
err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
log.PanicIf(err)
return ib
}
func getExifSimpleTestIbBytes() []byte {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
err = ib.AddStandard(0x000b, "asciivalue")
log.PanicIf(err)
err = ib.AddStandard(0x00ff, []uint16{0x1122})
log.PanicIf(err)
err = ib.AddStandard(0x0100, []uint32{0x33445566})
log.PanicIf(err)
err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
log.PanicIf(err)
ibe := NewIfdByteEncoder()
exifData, err := ibe.EncodeToExif(ib)
log.PanicIf(err)
return exifData
}
func validateExifSimpleTestIb(exifData []byte, t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
eh, index, err := Collect(im, ti, exifData)
log.PanicIf(err)
if eh.ByteOrder != TestDefaultByteOrder {
t.Fatalf("EXIF byte-order is not correct: %v", eh.ByteOrder)
} else if eh.FirstIfdOffset != ExifDefaultFirstIfdOffset {
t.Fatalf("EXIF first IFD-offset not correct: (0x%02x)", eh.FirstIfdOffset)
}
if len(index.Ifds) != 1 {
t.Fatalf("There wasn't exactly one IFD decoded: (%d)", len(index.Ifds))
}
ifd := index.RootIfd
if ifd.ByteOrder != TestDefaultByteOrder {
t.Fatalf("IFD byte-order not correct.")
} else if ifd.IfdPath != IfdStandard {
t.Fatalf("IFD name not correct.")
} else if ifd.Index != 0 {
t.Fatalf("IFD index not zero: (%d)", ifd.Index)
} else if ifd.Offset != uint32(0x0008) {
t.Fatalf("IFD offset not correct.")
} else if len(ifd.Entries) != 4 {
t.Fatalf("IFD number of entries not correct: (%d)", len(ifd.Entries))
} else if ifd.NextIfdOffset != uint32(0) {
t.Fatalf("Next-IFD offset is non-zero.")
} else if ifd.NextIfd != nil {
t.Fatalf("Next-IFD pointer is non-nil.")
}
// Verify the values by using the actual, orginal types (this is awesome).
addressableData := exifData[ExifAddressableAreaStart:]
expected := []struct {
tagId uint16
value interface{}
}{
{tagId: 0x000b, value: "asciivalue"},
{tagId: 0x00ff, value: []uint16{0x1122}},
{tagId: 0x0100, value: []uint32{0x33445566}},
{tagId: 0x013e, value: []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}}},
}
for i, e := range ifd.Entries {
if e.TagId != expected[i].tagId {
t.Fatalf("Tag-ID for entry (%d) not correct: (0x%02x) != (0x%02x)", i, e.TagId, expected[i].tagId)
}
value, err := e.Value(addressableData, TestDefaultByteOrder)
log.PanicIf(err)
if reflect.DeepEqual(value, expected[i].value) != true {
t.Fatalf("Value for entry (%d) not correct: [%v] != [%v]", i, value, expected[i].value)
}
}
}
func init() {
// This will only be executed when we're running tests in this package and
// not when this package is being imported from a subpackage.
goPath := os.Getenv("GOPATH")
if goPath != "" {
assetsPath = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif", "assets")
} else {
// Module-enabled context.
currentWd, err := os.Getwd()
log.PanicIf(err)
assetsPath = path.Join(currentWd, "assets")
}
testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
// Load test EXIF data.
filepath := path.Join(assetsPath, "NDM_8901.jpg.exif")
var err error
testExifData, err = ioutil.ReadFile(filepath)
log.PanicIf(err)
}

10
v2/error.go Normal file
View File

@ -0,0 +1,10 @@
package exif
import (
"errors"
)
var (
ErrTagNotFound = errors.New("tag not found")
ErrTagNotStandard = errors.New("tag not a standard tag")
)

160
v2/exif-read-tool/main.go Normal file
View File

@ -0,0 +1,160 @@
// This tool dumps EXIF information from images.
//
// Example command-line:
//
// exif-read-tool -filepath <file-path>
//
// Example Output:
//
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]
// IFD=[IfdIdentity<PARENT-NAME=[] NAME=[IFD]>] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
// ...
package main
import (
"flag"
"fmt"
"os"
"encoding/json"
"io/ioutil"
"github.com/dsoprea/go-exif"
"github.com/dsoprea/go-logging"
)
var (
filepathArg = ""
printAsJsonArg = false
printLoggingArg = false
)
type IfdEntry struct {
IfdPath string `json:"ifd_path"`
FqIfdPath string `json:"fq_ifd_path"`
IfdIndex int `json:"ifd_index"`
TagId uint16 `json:"tag_id"`
TagName string `json:"tag_name"`
TagTypeId exif.TagTypePrimitive `json:"tag_type_id"`
TagTypeName string `json:"tag_type_name"`
UnitCount uint32 `json:"unit_count"`
Value interface{} `json:"value"`
ValueString string `json:"value_string"`
}
func main() {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Program error.")
os.Exit(1)
}
}()
flag.StringVar(&filepathArg, "filepath", "", "File-path of image")
flag.BoolVar(&printAsJsonArg, "json", false, "Print JSON")
flag.BoolVar(&printLoggingArg, "verbose", false, "Print logging")
flag.Parse()
if filepathArg == "" {
fmt.Printf("Please provide a file-path for an image.\n")
os.Exit(1)
}
if printLoggingArg == true {
cla := log.NewConsoleLogAdapter()
log.AddAdapter("console", cla)
}
f, err := os.Open(filepathArg)
log.PanicIf(err)
data, err := ioutil.ReadAll(f)
log.PanicIf(err)
rawExif, err := exif.SearchAndExtractExif(data)
log.PanicIf(err)
// Run the parse.
im := exif.NewIfdMappingWithStandard()
ti := exif.NewTagIndex()
entries := make([]IfdEntry, 0)
visitor := func(fqIfdPath string, ifdIndex int, tagId uint16, tagType exif.TagType, valueContext exif.ValueContext) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
log.Panic(err)
}
}()
ifdPath, err := im.StripPathPhraseIndices(fqIfdPath)
log.PanicIf(err)
it, err := ti.Get(ifdPath, tagId)
if err != nil {
if log.Is(err, exif.ErrTagNotFound) {
fmt.Printf("WARNING: Unknown tag: [%s] (%04x)\n", ifdPath, tagId)
return nil
} else {
log.Panic(err)
}
}
valueString := ""
var value interface{}
if tagType.Type() == exif.TypeUndefined {
var err error
value, err = valueContext.Undefined()
if err != nil {
if err == exif.ErrUnhandledUnknownTypedTag {
value = nil
} else {
log.Panic(err)
}
}
valueString = fmt.Sprintf("%v", value)
} else {
valueString, err = valueContext.FormatFirst()
log.PanicIf(err)
value = valueString
}
entry := IfdEntry{
IfdPath: ifdPath,
FqIfdPath: fqIfdPath,
IfdIndex: ifdIndex,
TagId: tagId,
TagName: it.Name,
TagTypeId: tagType.Type(),
TagTypeName: tagType.Name(),
UnitCount: valueContext.UnitCount(),
Value: value,
ValueString: valueString,
}
entries = append(entries, entry)
return nil
}
_, err = exif.Visit(exif.IfdStandard, im, ti, rawExif, visitor)
log.PanicIf(err)
if printAsJsonArg == true {
data, err := json.MarshalIndent(entries, "", " ")
log.PanicIf(err)
fmt.Println(string(data))
} else {
for _, entry := range entries {
fmt.Printf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]\n", entry.IfdPath, entry.TagId, entry.TagName, entry.UnitCount, entry.TagTypeName, entry.ValueString)
}
}
}

View File

@ -0,0 +1,166 @@
package main
import (
"bytes"
"fmt"
"os"
"path"
"reflect"
"testing"
"encoding/json"
"io/ioutil"
"os/exec"
"github.com/dsoprea/go-logging"
)
var (
assetsPath = ""
appFilepath = ""
testImageFilepath = ""
)
func TestMain(t *testing.T) {
cmd := exec.Command(
"go", "run", appFilepath,
"-filepath", testImageFilepath)
b := new(bytes.Buffer)
cmd.Stdout = b
cmd.Stderr = b
err := cmd.Run()
actual := b.String()
if err != nil {
fmt.Printf(actual)
log.Panic(err)
}
expected := `IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]
IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]
IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]
IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]
IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]
IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]
IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]
IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]
IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]
IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]
IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]
IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]
IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]
IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0230]
IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]
IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[ComponentsConfiguration<ID=[YCBCR] BYTES=[1 2 3 0]>]
IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[614400/65536]
IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[262144/65536]
IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[0/1]
IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[5]
IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[16]
IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[16/1]
IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[MakerNote<TYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]>]
IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment<SIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)>]
IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]
IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]
IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]
IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[1]
IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[3840]
IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[2560]
IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[9326]
IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]
IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]
IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[3840000/1461]
IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[2560000/972]
IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[0]
IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[0]
IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[0]
IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[0]
IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]
IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]
IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[16/1...]
IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]
IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]
IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[9554]
IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 03 00 00]
IFD-PATH=[IFD] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]
IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]
IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]
`
if actual != expected {
t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actual, expected)
}
}
func TestMainJson(t *testing.T) {
cmd := exec.Command(
"go", "run", appFilepath,
"-filepath", testImageFilepath,
"-json")
b := new(bytes.Buffer)
cmd.Stdout = b
cmd.Stderr = b
err := cmd.Run()
actualRaw := b.Bytes()
if err != nil {
fmt.Printf(string(actualRaw))
log.Panic(err)
}
// Parse actual data.
actual := make([]map[string]interface{}, 0)
err = json.Unmarshal(actualRaw, &actual)
log.PanicIf(err)
// Read and parse expected data.
jsonFilepath := path.Join(assetsPath, "exif_read.json")
expectedRaw, err := ioutil.ReadFile(jsonFilepath)
log.PanicIf(err)
expected := make([]map[string]interface{}, 0)
err = json.Unmarshal(expectedRaw, &expected)
log.PanicIf(err)
if reflect.DeepEqual(actual, expected) == false {
t.Fatalf("Output not as expected:\nACTUAL:\n%s\nEXPECTED:\n%s", actualRaw, expectedRaw)
}
}
func init() {
goPath := os.Getenv("GOPATH")
moduleRoot := ""
if goPath != "" {
moduleRoot = path.Join(goPath, "src", "github.com", "dsoprea", "go-exif")
} else {
// Module-enabled context.
currentWd, err := os.Getwd()
log.PanicIf(err)
moduleRoot = path.Join(currentWd, "..")
}
assetsPath = path.Join(moduleRoot, "assets")
appFilepath = path.Join(moduleRoot, "exif-read-tool", "main.go")
testImageFilepath = path.Join(assetsPath, "NDM_8901.jpg")
}

247
v2/exif.go Normal file
View File

@ -0,0 +1,247 @@
package exif
import (
"bytes"
"errors"
"fmt"
"os"
"encoding/binary"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
const (
// ExifAddressableAreaStart is the absolute offset in the file that all
// offsets are relative to.
ExifAddressableAreaStart = uint32(0x0)
// ExifDefaultFirstIfdOffset is essentially the number of bytes in addition
// to `ExifAddressableAreaStart` that you have to move in order to escape
// the rest of the header and get to the earliest point where we can put
// stuff (which has to be the first IFD). This is the size of the header
// sequence containing the two-character byte-order, two-character fixed-
// bytes, and the four bytes describing the first-IFD offset.
ExifDefaultFirstIfdOffset = uint32(2 + 2 + 4)
)
var (
exifLogger = log.NewLogger("exif.exif")
// EncodeDefaultByteOrder is the default byte-order for encoding operations.
EncodeDefaultByteOrder = binary.BigEndian
// Default byte order for tests.
TestDefaultByteOrder = binary.BigEndian
BigEndianBoBytes = [2]byte{'M', 'M'}
LittleEndianBoBytes = [2]byte{'I', 'I'}
ByteOrderLookup = map[[2]byte]binary.ByteOrder{
BigEndianBoBytes: binary.BigEndian,
LittleEndianBoBytes: binary.LittleEndian,
}
ByteOrderLookupR = map[binary.ByteOrder][2]byte{
binary.BigEndian: BigEndianBoBytes,
binary.LittleEndian: LittleEndianBoBytes,
}
ExifFixedBytesLookup = map[binary.ByteOrder][2]byte{
binary.LittleEndian: [2]byte{0x2a, 0x00},
binary.BigEndian: [2]byte{0x00, 0x2a},
}
)
var (
ErrNoExif = errors.New("no exif data")
ErrExifHeaderError = errors.New("exif header error")
)
// SearchAndExtractExif returns a slice from the beginning of the EXIF data to
// end of the file (it's not practical to try and calculate where the data
// actually ends; it needs to be formally parsed).
func SearchAndExtractExif(data []byte) (rawExif []byte, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
// Search for the beginning of the EXIF information. The EXIF is near the
// beginning of our/most JPEGs, so this has a very low cost.
foundAt := -1
for i := 0; i < len(data); i++ {
if _, err := ParseExifHeader(data[i:]); err == nil {
foundAt = i
break
} else if log.Is(err, ErrNoExif) == false {
return nil, err
}
}
if foundAt == -1 {
return nil, ErrNoExif
}
return data[foundAt:], nil
}
// SearchFileAndExtractExif returns a slice from the beginning of the EXIF data
// to the end of the file (it's not practical to try and calculate where the
// data actually ends).
func SearchFileAndExtractExif(filepath string) (rawExif []byte, err error) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
// Open the file.
f, err := os.Open(filepath)
log.PanicIf(err)
defer f.Close()
data, err := ioutil.ReadAll(f)
log.PanicIf(err)
rawExif, err = SearchAndExtractExif(data)
log.PanicIf(err)
return rawExif, nil
}
type ExifHeader struct {
ByteOrder binary.ByteOrder
FirstIfdOffset uint32
}
func (eh ExifHeader) String() string {
return fmt.Sprintf("ExifHeader<BYTE-ORDER=[%v] FIRST-IFD-OFFSET=(0x%02x)>", eh.ByteOrder, eh.FirstIfdOffset)
}
// ParseExifHeader parses the bytes at the very top of the header.
//
// This will panic with ErrNoExif on any data errors so that we can double as
// an EXIF-detection routine.
func ParseExifHeader(data []byte) (eh ExifHeader, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Good reference:
//
// CIPA DC-008-2016; JEITA CP-3451D
// -> http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
if len(data) < 2 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (1): (%d)", len(data))
return eh, ErrNoExif
}
byteOrderBytes := [2]byte{data[0], data[1]}
byteOrder, found := ByteOrderLookup[byteOrderBytes]
if found == false {
// exifLogger.Warningf(nil, "EXIF byte-order not recognized: [%v]", byteOrderBytes)
return eh, ErrNoExif
}
if len(data) < 4 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (2): (%d)", len(data))
return eh, ErrNoExif
}
fixedBytes := [2]byte{data[2], data[3]}
expectedFixedBytes := ExifFixedBytesLookup[byteOrder]
if fixedBytes != expectedFixedBytes {
// exifLogger.Warningf(nil, "EXIF header fixed-bytes should be [%v] but are: [%v]", expectedFixedBytes, fixedBytes)
return eh, ErrNoExif
}
if len(data) < 2 {
exifLogger.Warningf(nil, "Not enough data for EXIF header (3): (%d)", len(data))
return eh, ErrNoExif
}
firstIfdOffset := byteOrder.Uint32(data[4:8])
eh = ExifHeader{
ByteOrder: byteOrder,
FirstIfdOffset: firstIfdOffset,
}
return eh, nil
}
// Visit recursively invokes a callback for every tag.
func Visit(rootIfdName string, ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte, visitor RawTagVisitor) (eh ExifHeader, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
eh, err = ParseExifHeader(exifData)
log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
err = ie.Scan(rootIfdName, eh.FirstIfdOffset, visitor, true)
log.PanicIf(err)
return eh, nil
}
// Collect recursively builds a static structure of all IFDs and tags.
func Collect(ifdMapping *IfdMapping, tagIndex *TagIndex, exifData []byte) (eh ExifHeader, index IfdIndex, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
eh, err = ParseExifHeader(exifData)
log.PanicIf(err)
ie := NewIfdEnumerate(ifdMapping, tagIndex, exifData, eh.ByteOrder)
index, err = ie.Collect(eh.FirstIfdOffset, true)
log.PanicIf(err)
return eh, index, nil
}
// BuildExifHeader constructs the bytes that go in the very beginning.
func BuildExifHeader(byteOrder binary.ByteOrder, firstIfdOffset uint32) (headerBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
b := new(bytes.Buffer)
// This is the point in the data that all offsets are relative to.
boBytes := ByteOrderLookupR[byteOrder]
_, err = b.WriteString(string(boBytes[:]))
log.PanicIf(err)
fixedBytes := ExifFixedBytesLookup[byteOrder]
_, err = b.Write(fixedBytes[:])
log.PanicIf(err)
err = binary.Write(b, byteOrder, firstIfdOffset)
log.PanicIf(err)
return b.Bytes(), nil
}

368
v2/exif_test.go Normal file
View File

@ -0,0 +1,368 @@
package exif
import (
"bytes"
"fmt"
"os"
"reflect"
"testing"
"encoding/binary"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
func TestVisit(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Exif failure.")
}
}()
ti := NewTagIndex()
// Open the file.
f, err := os.Open(testImageFilepath)
log.PanicIf(err)
defer f.Close()
data, err := ioutil.ReadAll(f)
log.PanicIf(err)
// Search for the beginning of the EXIF information. The EXIF is near the
// very beginning of our/most JPEGs, so this has a very low cost.
foundAt := -1
for i := 0; i < len(data); i++ {
if _, err := ParseExifHeader(data[i:]); err == nil {
foundAt = i
break
} else if log.Is(err, ErrNoExif) == false {
log.Panic(err)
}
}
if foundAt == -1 {
log.Panicf("EXIF start not found")
}
// Run the parse.
im := NewIfdMappingWithStandard()
tags := make([]string, 0)
visitor := func(fqIfdPath string, ifdIndex int, tagId uint16, tagType TagType, valueContext ValueContext) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
log.Panic(err)
}
}()
ifdPath, err := im.StripPathPhraseIndices(fqIfdPath)
log.PanicIf(err)
it, err := ti.Get(ifdPath, tagId)
if err != nil {
if log.Is(err, ErrTagNotFound) {
fmt.Printf("Unknown tag: [%s] (%04x)\n", ifdPath, tagId)
return nil
} else {
log.Panic(err)
}
}
valueString := ""
if tagType.Type() == TypeUndefined {
value, err := valueContext.Undefined()
if err != nil {
if err == ErrUnhandledUnknownTypedTag {
valueString = "!UNDEFINED!"
} else {
log.Panic(err)
}
}
valueString = fmt.Sprintf("%v", value)
} else {
valueString, err = valueContext.FormatFirst()
log.PanicIf(err)
}
description := fmt.Sprintf("IFD-PATH=[%s] ID=(0x%04x) NAME=[%s] COUNT=(%d) TYPE=[%s] VALUE=[%s]", ifdPath, tagId, it.Name, valueContext.UnitCount(), tagType.Name(), valueString)
tags = append(tags, description)
return nil
}
_, err = Visit(IfdStandard, im, ti, data[foundAt:], visitor)
log.PanicIf(err)
expected := []string{
"IFD-PATH=[IFD] ID=(0x010f) NAME=[Make] COUNT=(6) TYPE=[ASCII] VALUE=[Canon]",
"IFD-PATH=[IFD] ID=(0x0110) NAME=[Model] COUNT=(22) TYPE=[ASCII] VALUE=[Canon EOS 5D Mark III]",
"IFD-PATH=[IFD] ID=(0x0112) NAME=[Orientation] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD-PATH=[IFD] ID=(0x0132) NAME=[DateTime] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD-PATH=[IFD] ID=(0x013b) NAME=[Artist] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD-PATH=[IFD] ID=(0x0213) NAME=[YCbCrPositioning] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD-PATH=[IFD] ID=(0x8298) NAME=[Copyright] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD-PATH=[IFD] ID=(0x8769) NAME=[ExifTag] COUNT=(1) TYPE=[LONG] VALUE=[360]",
"IFD-PATH=[IFD/Exif] ID=(0x829a) NAME=[ExposureTime] COUNT=(1) TYPE=[RATIONAL] VALUE=[1/640]",
"IFD-PATH=[IFD/Exif] ID=(0x829d) NAME=[FNumber] COUNT=(1) TYPE=[RATIONAL] VALUE=[4/1]",
"IFD-PATH=[IFD/Exif] ID=(0x8822) NAME=[ExposureProgram] COUNT=(1) TYPE=[SHORT] VALUE=[4]",
"IFD-PATH=[IFD/Exif] ID=(0x8827) NAME=[ISOSpeedRatings] COUNT=(1) TYPE=[SHORT] VALUE=[1600]",
"IFD-PATH=[IFD/Exif] ID=(0x8830) NAME=[SensitivityType] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD-PATH=[IFD/Exif] ID=(0x8832) NAME=[RecommendedExposureIndex] COUNT=(1) TYPE=[LONG] VALUE=[1600]",
"IFD-PATH=[IFD/Exif] ID=(0x9000) NAME=[ExifVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0230]",
"IFD-PATH=[IFD/Exif] ID=(0x9003) NAME=[DateTimeOriginal] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD-PATH=[IFD/Exif] ID=(0x9004) NAME=[DateTimeDigitized] COUNT=(20) TYPE=[ASCII] VALUE=[2017:12:02 08:18:50]",
"IFD-PATH=[IFD/Exif] ID=(0x9101) NAME=[ComponentsConfiguration] COUNT=(4) TYPE=[UNDEFINED] VALUE=[ComponentsConfiguration<ID=[YCBCR] BYTES=[1 2 3 0]>]",
"IFD-PATH=[IFD/Exif] ID=(0x9201) NAME=[ShutterSpeedValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[614400/65536]",
"IFD-PATH=[IFD/Exif] ID=(0x9202) NAME=[ApertureValue] COUNT=(1) TYPE=[RATIONAL] VALUE=[262144/65536]",
"IFD-PATH=[IFD/Exif] ID=(0x9204) NAME=[ExposureBiasValue] COUNT=(1) TYPE=[SRATIONAL] VALUE=[0/1]",
"IFD-PATH=[IFD/Exif] ID=(0x9207) NAME=[MeteringMode] COUNT=(1) TYPE=[SHORT] VALUE=[5]",
"IFD-PATH=[IFD/Exif] ID=(0x9209) NAME=[Flash] COUNT=(1) TYPE=[SHORT] VALUE=[16]",
"IFD-PATH=[IFD/Exif] ID=(0x920a) NAME=[FocalLength] COUNT=(1) TYPE=[RATIONAL] VALUE=[16/1]",
"IFD-PATH=[IFD/Exif] ID=(0x927c) NAME=[MakerNote] COUNT=(8152) TYPE=[UNDEFINED] VALUE=[MakerNote<TYPE-ID=[28 00 01 00 03 00 31 00 00 00 74 05 00 00 02 00 03 00 04 00] LEN=(8152) SHA1=[d4154aa7df5474efe7ab38de2595919b9b4cc29f]>]",
"IFD-PATH=[IFD/Exif] ID=(0x9286) NAME=[UserComment] COUNT=(264) TYPE=[UNDEFINED] VALUE=[UserComment<SIZE=(256) ENCODING=[UNDEFINED] V=[0 0 0 0 0 0 0 0]... LEN=(256)>]",
"IFD-PATH=[IFD/Exif] ID=(0x9290) NAME=[SubSecTime] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD-PATH=[IFD/Exif] ID=(0x9291) NAME=[SubSecTimeOriginal] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD-PATH=[IFD/Exif] ID=(0x9292) NAME=[SubSecTimeDigitized] COUNT=(3) TYPE=[ASCII] VALUE=[00]",
"IFD-PATH=[IFD/Exif] ID=(0xa000) NAME=[FlashpixVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]",
"IFD-PATH=[IFD/Exif] ID=(0xa001) NAME=[ColorSpace] COUNT=(1) TYPE=[SHORT] VALUE=[1]",
"IFD-PATH=[IFD/Exif] ID=(0xa002) NAME=[PixelXDimension] COUNT=(1) TYPE=[SHORT] VALUE=[3840]",
"IFD-PATH=[IFD/Exif] ID=(0xa003) NAME=[PixelYDimension] COUNT=(1) TYPE=[SHORT] VALUE=[2560]",
"IFD-PATH=[IFD/Exif] ID=(0xa005) NAME=[InteroperabilityTag] COUNT=(1) TYPE=[LONG] VALUE=[9326]",
"IFD-PATH=[IFD/Exif/Iop] ID=(0x0001) NAME=[InteroperabilityIndex] COUNT=(4) TYPE=[ASCII] VALUE=[R98]",
"IFD-PATH=[IFD/Exif/Iop] ID=(0x0002) NAME=[InteroperabilityVersion] COUNT=(4) TYPE=[UNDEFINED] VALUE=[0100]",
"IFD-PATH=[IFD/Exif] ID=(0xa20e) NAME=[FocalPlaneXResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[3840000/1461]",
"IFD-PATH=[IFD/Exif] ID=(0xa20f) NAME=[FocalPlaneYResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[2560000/972]",
"IFD-PATH=[IFD/Exif] ID=(0xa210) NAME=[FocalPlaneResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
"IFD-PATH=[IFD/Exif] ID=(0xa401) NAME=[CustomRendered] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD-PATH=[IFD/Exif] ID=(0xa402) NAME=[ExposureMode] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD-PATH=[IFD/Exif] ID=(0xa403) NAME=[WhiteBalance] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD-PATH=[IFD/Exif] ID=(0xa406) NAME=[SceneCaptureType] COUNT=(1) TYPE=[SHORT] VALUE=[0]",
"IFD-PATH=[IFD/Exif] ID=(0xa430) NAME=[CameraOwnerName] COUNT=(1) TYPE=[ASCII] VALUE=[]",
"IFD-PATH=[IFD/Exif] ID=(0xa431) NAME=[BodySerialNumber] COUNT=(13) TYPE=[ASCII] VALUE=[063024020097]",
"IFD-PATH=[IFD/Exif] ID=(0xa432) NAME=[LensSpecification] COUNT=(4) TYPE=[RATIONAL] VALUE=[16/1...]",
"IFD-PATH=[IFD/Exif] ID=(0xa434) NAME=[LensModel] COUNT=(22) TYPE=[ASCII] VALUE=[EF16-35mm f/4L IS USM]",
"IFD-PATH=[IFD/Exif] ID=(0xa435) NAME=[LensSerialNumber] COUNT=(11) TYPE=[ASCII] VALUE=[2400001068]",
"IFD-PATH=[IFD] ID=(0x8825) NAME=[GPSTag] COUNT=(1) TYPE=[LONG] VALUE=[9554]",
"IFD-PATH=[IFD/GPSInfo] ID=(0x0000) NAME=[GPSVersionID] COUNT=(4) TYPE=[BYTE] VALUE=[02 03 00 00]",
"IFD-PATH=[IFD] ID=(0x0103) NAME=[Compression] COUNT=(1) TYPE=[SHORT] VALUE=[6]",
"IFD-PATH=[IFD] ID=(0x011a) NAME=[XResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD-PATH=[IFD] ID=(0x011b) NAME=[YResolution] COUNT=(1) TYPE=[RATIONAL] VALUE=[72/1]",
"IFD-PATH=[IFD] ID=(0x0128) NAME=[ResolutionUnit] COUNT=(1) TYPE=[SHORT] VALUE=[2]",
}
if reflect.DeepEqual(tags, expected) == false {
fmt.Printf("\n")
fmt.Printf("ACTUAL:\n")
fmt.Printf("\n")
for _, line := range tags {
fmt.Println(line)
}
fmt.Printf("\n")
fmt.Printf("EXPECTED:\n")
fmt.Printf("\n")
for _, line := range expected {
fmt.Println(line)
}
fmt.Printf("\n")
t.Fatalf("tags not correct.")
}
}
func TestSearchFileAndExtractExif(t *testing.T) {
// Returns a slice starting with the EXIF data and going to the end of the
// image.
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
t.Fatalf("found EXIF data not correct")
}
}
func TestSearchAndExtractExif(t *testing.T) {
imageData, err := ioutil.ReadFile(testImageFilepath)
log.PanicIf(err)
rawExif, err := SearchAndExtractExif(imageData)
log.PanicIf(err)
if bytes.Compare(rawExif[:len(testExifData)], testExifData) != 0 {
t.Fatalf("found EXIF data not correct")
}
}
func TestCollect(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Exif failure.")
}
}()
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
rootIfd := index.RootIfd
ifds := index.Ifds
tree := index.Tree
lookup := index.Lookup
if rootIfd.Offset != uint32(0x0008) {
t.Fatalf("Root-IFD not correct: (0x%04d).", rootIfd.Offset)
} else if rootIfd.Id != 0 {
t.Fatalf("Root-IFD does not have the right ID: (%d)", rootIfd.Id)
} else if tree[0] != rootIfd {
t.Fatalf("Root-IFD is not indexed properly.")
} else if len(ifds) != 5 {
t.Fatalf("The IFD list is not the right size: (%d)", len(ifds))
} else if len(tree) != 5 {
t.Fatalf("The IFD tree is not the right size: (%d)", len(tree))
} else if len(lookup) != 4 {
t.Fatalf("The IFD lookup is not the right size: (%d)", len(lookup))
}
if rootIfd.NextIfdOffset != 0x2c54 {
t.Fatalf("Root IFD does not continue correctly: (0x%04x)", rootIfd.NextIfdOffset)
} else if rootIfd.NextIfd.Offset != rootIfd.NextIfdOffset {
t.Fatalf("Root IFD neighbor object does not have the right offset: (0x%04x != 0x%04x)", rootIfd.NextIfd.Offset, rootIfd.NextIfdOffset)
} else if rootIfd.NextIfd.NextIfdOffset != 0 {
t.Fatalf("Root IFD chain not terminated correctly (1).")
} else if rootIfd.NextIfd.NextIfd != nil {
t.Fatalf("Root IFD chain not terminated correctly (2).")
}
if rootIfd.IfdPath != IfdPathStandard {
t.Fatalf("Root IFD is not labeled correctly: [%s]", rootIfd.IfdPath)
} else if rootIfd.NextIfd.IfdPath != IfdPathStandard {
t.Fatalf("Root IFD sibling is not labeled correctly: [%s]", rootIfd.IfdPath)
} else if rootIfd.Children[0].IfdPath != IfdPathStandardExif {
t.Fatalf("Root IFD child (0) is not labeled correctly: [%s]", rootIfd.Children[0].IfdPath)
} else if rootIfd.Children[1].IfdPath != IfdPathStandardGps {
t.Fatalf("Root IFD child (1) is not labeled correctly: [%s]", rootIfd.Children[1].IfdPath)
} else if rootIfd.Children[0].Children[0].IfdPath != IfdPathStandardExifIop {
t.Fatalf("Exif IFD child is not an IOP IFD: [%s]", rootIfd.Children[0].Children[0].IfdPath)
}
if lookup[IfdPathStandard][0].IfdPath != IfdPathStandard {
t.Fatalf("Lookup for standard IFD not correct.")
} else if lookup[IfdPathStandard][1].IfdPath != IfdPathStandard {
t.Fatalf("Lookup for standard IFD not correct.")
}
if lookup[IfdPathStandardExif][0].IfdPath != IfdPathStandardExif {
t.Fatalf("Lookup for EXIF IFD not correct.")
}
if lookup[IfdPathStandardGps][0].IfdPath != IfdPathStandardGps {
t.Fatalf("Lookup for GPS IFD not correct.")
}
if lookup[IfdPathStandardExifIop][0].IfdPath != IfdPathStandardExifIop {
t.Fatalf("Lookup for IOP IFD not correct.")
}
foundExif := 0
foundGps := 0
for _, ite := range lookup[IfdPathStandard][0].Entries {
if ite.ChildIfdPath == IfdPathStandardExif {
foundExif++
if ite.TagId != IfdExifId {
t.Fatalf("EXIF IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdExifId)
}
}
if ite.ChildIfdPath == IfdPathStandardGps {
foundGps++
if ite.TagId != IfdGpsId {
t.Fatalf("GPS IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdGpsId)
}
}
}
if foundExif != 1 {
t.Fatalf("Exactly one EXIF IFD tag wasn't found: (%d)", foundExif)
} else if foundGps != 1 {
t.Fatalf("Exactly one GPS IFD tag wasn't found: (%d)", foundGps)
}
foundIop := 0
for _, ite := range lookup[IfdPathStandardExif][0].Entries {
if ite.ChildIfdPath == IfdPathStandardExifIop {
foundIop++
if ite.TagId != IfdIopId {
t.Fatalf("IOP IFD tag-ID mismatch: (0x%04x) != (0x%04x)", ite.TagId, IfdIopId)
}
}
}
if foundIop != 1 {
t.Fatalf("Exactly one IOP IFD tag wasn't found: (%d)", foundIop)
}
}
func TestParseExifHeader(t *testing.T) {
eh, err := ParseExifHeader(testExifData)
log.PanicIf(err)
if eh.ByteOrder != binary.LittleEndian {
t.Fatalf("Byte-order of EXIF header not correct.")
} else if eh.FirstIfdOffset != 0x8 {
t.Fatalf("First IFD offset not correct.")
}
}
func TestExif_BuildAndParseExifHeader(t *testing.T) {
headerBytes, err := BuildExifHeader(TestDefaultByteOrder, 0x11223344)
log.PanicIf(err)
eh, err := ParseExifHeader(headerBytes)
log.PanicIf(err)
if eh.ByteOrder != TestDefaultByteOrder {
t.Fatalf("Byte-order of EXIF header not correct.")
} else if eh.FirstIfdOffset != 0x11223344 {
t.Fatalf("First IFD offset not correct.")
}
}
func ExampleBuildExifHeader() {
headerBytes, err := BuildExifHeader(TestDefaultByteOrder, 0x11223344)
log.PanicIf(err)
eh, err := ParseExifHeader(headerBytes)
log.PanicIf(err)
fmt.Printf("%v\n", eh)
// Output: ExifHeader<BYTE-ORDER=[BigEndian] FIRST-IFD-OFFSET=(0x11223344)>
}

11
v2/go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/dsoprea/go-exif/v2
go 1.13
require (
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696
github.com/go-errors/errors v1.0.1 // indirect
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
gopkg.in/yaml.v2 v2.2.7
)

14
v2/go.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696 h1:VGFnZAcLwPpt1sHlAxml+pGLZz9A2s+K/s1YNhPC91Y=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec h1:lJwO/92dFXWeXOZdoGXgptLmNLwynMSHUmU6besqtiw=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

56
v2/gps.go Normal file
View File

@ -0,0 +1,56 @@
package exif
import (
"errors"
"fmt"
"time"
"github.com/golang/geo/s2"
)
var (
ErrGpsCoordinatesNotValid = errors.New("GPS coordinates not valid")
)
type GpsDegrees struct {
Orientation byte
Degrees, Minutes, Seconds float64
}
func (d GpsDegrees) String() string {
return fmt.Sprintf("Degrees<O=[%s] D=(%g) M=(%g) S=(%g)>", string([]byte{d.Orientation}), d.Degrees, d.Minutes, d.Seconds)
}
func (d GpsDegrees) Decimal() float64 {
decimal := float64(d.Degrees) + float64(d.Minutes)/60.0 + float64(d.Seconds)/3600.0
if d.Orientation == 'S' || d.Orientation == 'W' {
return -decimal
} else {
return decimal
}
}
type GpsInfo struct {
Latitude, Longitude GpsDegrees
Altitude int
Timestamp time.Time
}
func (gi *GpsInfo) String() string {
return fmt.Sprintf("GpsInfo<LAT=(%.05f) LON=(%.05f) ALT=(%d) TIME=[%s]>", gi.Latitude.Decimal(), gi.Longitude.Decimal(), gi.Altitude, gi.Timestamp)
}
func (gi *GpsInfo) S2CellId() s2.CellID {
latitude := gi.Latitude.Decimal()
longitude := gi.Longitude.Decimal()
ll := s2.LatLngFromDegrees(latitude, longitude)
cellId := s2.CellIDFromLatLng(ll)
if cellId.IsValid() == false {
panic(ErrGpsCoordinatesNotValid)
}
return cellId
}

407
v2/ifd.go Normal file
View File

@ -0,0 +1,407 @@
package exif
import (
"errors"
"fmt"
"strings"
"github.com/dsoprea/go-logging"
)
const (
// IFD names. The paths that we referred to the IFDs with are comprised of
// these.
IfdStandard = "IFD"
IfdExif = "Exif"
IfdGps = "GPSInfo"
IfdIop = "Iop"
// Tag IDs for child IFDs.
IfdExifId = 0x8769
IfdGpsId = 0x8825
IfdIopId = 0xA005
// Just a placeholder.
IfdRootId = 0x0000
// The paths of the standard IFDs expressed in the standard IFD-mappings
// and as the group-names in the tag data.
IfdPathStandard = "IFD"
IfdPathStandardExif = "IFD/Exif"
IfdPathStandardExifIop = "IFD/Exif/Iop"
IfdPathStandardGps = "IFD/GPSInfo"
)
var (
ifdLogger = log.NewLogger("exif.ifd")
)
var (
ErrChildIfdNotMapped = errors.New("no child-IFD for that tag-ID under parent")
)
// type IfdIdentity struct {
// ParentIfdName string
// IfdName string
// }
// func (ii IfdIdentity) String() string {
// return fmt.Sprintf("IfdIdentity<PARENT-NAME=[%s] NAME=[%s]>", ii.ParentIfdName, ii.IfdName)
// }
type MappedIfd struct {
ParentTagId uint16
Placement []uint16
Path []string
Name string
TagId uint16
Children map[uint16]*MappedIfd
}
func (mi *MappedIfd) String() string {
pathPhrase := mi.PathPhrase()
return fmt.Sprintf("MappedIfd<(0x%04X) [%s] PATH=[%s]>", mi.TagId, mi.Name, pathPhrase)
}
func (mi *MappedIfd) PathPhrase() string {
return strings.Join(mi.Path, "/")
}
// IfdMapping describes all of the IFDs that we currently recognize.
type IfdMapping struct {
rootNode *MappedIfd
}
func NewIfdMapping() (ifdMapping *IfdMapping) {
rootNode := &MappedIfd{
Path: make([]string, 0),
Children: make(map[uint16]*MappedIfd),
}
return &IfdMapping{
rootNode: rootNode,
}
}
func NewIfdMappingWithStandard() (ifdMapping *IfdMapping) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.Panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
return im
}
func (im *IfdMapping) Get(parentPlacement []uint16) (childIfd *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ptr := im.rootNode
for _, tagId := range parentPlacement {
if descendantPtr, found := ptr.Children[tagId]; found == false {
log.Panicf("ifd child with tag-ID (%04x) not registered: [%s]", tagId, ptr.PathPhrase())
} else {
ptr = descendantPtr
}
}
return ptr, nil
}
func (im *IfdMapping) GetWithPath(pathPhrase string) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if pathPhrase == "" {
log.Panicf("path-phrase is empty")
}
path := strings.Split(pathPhrase, "/")
ptr := im.rootNode
for _, name := range path {
var hit *MappedIfd
for _, mi := range ptr.Children {
if mi.Name == name {
hit = mi
break
}
}
if hit == nil {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, ptr.PathPhrase())
}
ptr = hit
}
return ptr, nil
}
// GetChild is a convenience function to get the child path for a given parent
// placement and child tag-ID.
func (im *IfdMapping) GetChild(parentPathPhrase string, tagId uint16) (mi *MappedIfd, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
mi, err = im.GetWithPath(parentPathPhrase)
log.PanicIf(err)
for _, childMi := range mi.Children {
if childMi.TagId == tagId {
return childMi, nil
}
}
// Whether or not an IFD is defined in data, such an IFD is not registered
// and would be unknown.
log.Panic(ErrChildIfdNotMapped)
return nil, nil
}
type IfdTagIdAndIndex struct {
Name string
TagId uint16
Index int
}
func (itii IfdTagIdAndIndex) String() string {
return fmt.Sprintf("IfdTagIdAndIndex<NAME=[%s] ID=(%04x) INDEX=(%d)>", itii.Name, itii.TagId, itii.Index)
}
// ResolvePath takes a list of names, which can also be suffixed with indices
// (to identify the second, third, etc.. sibling IFD) and returns a list of
// tag-IDs and those indices.
//
// Example:
//
// - IFD/Exif/Iop
// - IFD0/Exif/Iop
//
// This is the only call that supports adding the numeric indices.
func (im *IfdMapping) ResolvePath(pathPhrase string) (lineage []IfdTagIdAndIndex, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
pathPhrase = strings.TrimSpace(pathPhrase)
if pathPhrase == "" {
log.Panicf("can not resolve empty path-phrase")
}
path := strings.Split(pathPhrase, "/")
lineage = make([]IfdTagIdAndIndex, len(path))
ptr := im.rootNode
empty := IfdTagIdAndIndex{}
for i, name := range path {
indexByte := name[len(name)-1]
index := 0
if indexByte >= '0' && indexByte <= '9' {
index = int(indexByte - '0')
name = name[:len(name)-1]
}
itii := IfdTagIdAndIndex{}
for _, mi := range ptr.Children {
if mi.Name != name {
continue
}
itii.Name = name
itii.TagId = mi.TagId
itii.Index = index
ptr = mi
break
}
if itii == empty {
log.Panicf("ifd child with name [%s] not registered: [%s]", name, pathPhrase)
}
lineage[i] = itii
}
return lineage, nil
}
func (im *IfdMapping) FqPathPhraseFromLineage(lineage []IfdTagIdAndIndex) (fqPathPhrase string) {
fqPathParts := make([]string, len(lineage))
for i, itii := range lineage {
if itii.Index > 0 {
fqPathParts[i] = fmt.Sprintf("%s%d", itii.Name, itii.Index)
} else {
fqPathParts[i] = itii.Name
}
}
return strings.Join(fqPathParts, "/")
}
func (im *IfdMapping) PathPhraseFromLineage(lineage []IfdTagIdAndIndex) (pathPhrase string) {
pathParts := make([]string, len(lineage))
for i, itii := range lineage {
pathParts[i] = itii.Name
}
return strings.Join(pathParts, "/")
}
// StripPathPhraseIndices returns a non-fully-qualified path-phrase (no
// indices).
func (im *IfdMapping) StripPathPhraseIndices(pathPhrase string) (strippedPathPhrase string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
lineage, err := im.ResolvePath(pathPhrase)
log.PanicIf(err)
strippedPathPhrase = im.PathPhraseFromLineage(lineage)
return strippedPathPhrase, nil
}
// Add puts the given IFD at the given position of the tree. The position of the
// tree is referred to as the placement and is represented by a set of tag-IDs,
// where the leftmost is the root tag and the tags going to the right are
// progressive descendants.
func (im *IfdMapping) Add(parentPlacement []uint16, tagId uint16, name string) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! It would be nicer to provide a list of names in the placement rather than tag-IDs.
ptr, err := im.Get(parentPlacement)
log.PanicIf(err)
path := make([]string, len(parentPlacement)+1)
if len(parentPlacement) > 0 {
copy(path, ptr.Path)
}
path[len(path)-1] = name
placement := make([]uint16, len(parentPlacement)+1)
if len(placement) > 0 {
copy(placement, ptr.Placement)
}
placement[len(placement)-1] = tagId
childIfd := &MappedIfd{
ParentTagId: ptr.TagId,
Path: path,
Placement: placement,
Name: name,
TagId: tagId,
Children: make(map[uint16]*MappedIfd),
}
if _, found := ptr.Children[tagId]; found == true {
log.Panicf("child IFD with tag-ID (%04x) already registered under IFD [%s] with tag-ID (%04x)", tagId, ptr.Name, ptr.TagId)
}
ptr.Children[tagId] = childIfd
return nil
}
func (im *IfdMapping) dumpLineages(stack []*MappedIfd, input []string) (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
currentIfd := stack[len(stack)-1]
output = input
for _, childIfd := range currentIfd.Children {
stackCopy := make([]*MappedIfd, len(stack)+1)
copy(stackCopy, stack)
stackCopy[len(stack)] = childIfd
// Add to output, but don't include the obligatory root node.
parts := make([]string, len(stackCopy)-1)
for i, mi := range stackCopy[1:] {
parts[i] = mi.Name
}
output = append(output, strings.Join(parts, "/"))
output, err = im.dumpLineages(stackCopy, output)
log.PanicIf(err)
}
return output, nil
}
func (im *IfdMapping) DumpLineages() (output []string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
stack := []*MappedIfd{im.rootNode}
output = make([]string, 0)
output, err = im.dumpLineages(stack, output)
log.PanicIf(err)
return output, nil
}
func LoadStandardIfds(im *IfdMapping) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = im.Add([]uint16{}, IfdRootId, IfdStandard)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId}, IfdExifId, IfdExif)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId, IfdExifId}, IfdIopId, IfdIop)
log.PanicIf(err)
err = im.Add([]uint16{IfdRootId}, IfdGpsId, IfdGps)
log.PanicIf(err)
return nil
}

1265
v2/ifd_builder.go Normal file

File diff suppressed because it is too large Load Diff

530
v2/ifd_builder_encode.go Normal file
View File

@ -0,0 +1,530 @@
package exif
import (
"bytes"
"fmt"
"strings"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
const (
// Tag-ID + Tag-Type + Unit-Count + Value/Offset.
IfdTagEntrySize = uint32(2 + 2 + 4 + 4)
)
type ByteWriter struct {
b *bytes.Buffer
byteOrder binary.ByteOrder
}
func NewByteWriter(b *bytes.Buffer, byteOrder binary.ByteOrder) (bw *ByteWriter) {
return &ByteWriter{
b: b,
byteOrder: byteOrder,
}
}
func (bw ByteWriter) writeAsBytes(value interface{}) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = binary.Write(bw.b, bw.byteOrder, value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteUint32(value uint32) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = bw.writeAsBytes(value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteUint16(value uint16) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
err = bw.writeAsBytes(value)
log.PanicIf(err)
return nil
}
func (bw ByteWriter) WriteFourBytes(value []byte) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
len_ := len(value)
if len_ != 4 {
log.Panicf("value is not four-bytes: (%d)", len_)
}
_, err = bw.b.Write(value)
log.PanicIf(err)
return nil
}
// ifdOffsetIterator keeps track of where the next IFD should be written by
// keeping track of where the offsets start, the data that has been added, and
// bumping the offset *when* the data is added.
type ifdDataAllocator struct {
offset uint32
b bytes.Buffer
}
func newIfdDataAllocator(ifdDataAddressableOffset uint32) *ifdDataAllocator {
return &ifdDataAllocator{
offset: ifdDataAddressableOffset,
}
}
func (ida *ifdDataAllocator) Allocate(value []byte) (offset uint32, err error) {
_, err = ida.b.Write(value)
log.PanicIf(err)
offset = ida.offset
ida.offset += uint32(len(value))
return offset, nil
}
func (ida *ifdDataAllocator) NextOffset() uint32 {
return ida.offset
}
func (ida *ifdDataAllocator) Bytes() []byte {
return ida.b.Bytes()
}
// IfdByteEncoder converts an IB to raw bytes (for writing) while also figuring
// out all of the allocations and indirection that is required for extended
// data.
type IfdByteEncoder struct {
// journal holds a list of actions taken while encoding.
journal [][3]string
}
func NewIfdByteEncoder() (ibe *IfdByteEncoder) {
return &IfdByteEncoder{
journal: make([][3]string, 0),
}
}
func (ibe *IfdByteEncoder) Journal() [][3]string {
return ibe.journal
}
func (ibe *IfdByteEncoder) TableSize(entryCount int) uint32 {
// Tag-Count + (Entry-Size * Entry-Count) + Next-IFD-Offset.
return uint32(2) + (IfdTagEntrySize * uint32(entryCount)) + uint32(4)
}
func (ibe *IfdByteEncoder) pushToJournal(where, direction, format string, args ...interface{}) {
event := [3]string{
direction,
where,
fmt.Sprintf(format, args...),
}
ibe.journal = append(ibe.journal, event)
}
// PrintJournal prints a hierarchical representation of the steps taken during
// encoding.
func (ibe *IfdByteEncoder) PrintJournal() {
maxWhereLength := 0
for _, event := range ibe.journal {
where := event[1]
len_ := len(where)
if len_ > maxWhereLength {
maxWhereLength = len_
}
}
level := 0
for i, event := range ibe.journal {
direction := event[0]
where := event[1]
message := event[2]
if direction != ">" && direction != "<" && direction != "-" {
log.Panicf("journal operation not valid: [%s]", direction)
}
if direction == "<" {
if level <= 0 {
log.Panicf("journal operations unbalanced (too many closes)")
}
level--
}
indent := strings.Repeat(" ", level)
fmt.Printf("%3d %s%s %s: %s\n", i, indent, direction, where, message)
if direction == ">" {
level++
}
}
if level != 0 {
log.Panicf("journal operations unbalanced (too many opens)")
}
}
// encodeTagToBytes encodes the given tag to a byte stream. If
// `nextIfdOffsetToWrite` is more than (0), recurse into child IFDs
// (`nextIfdOffsetToWrite` is required in order for them to know where the its
// IFD data will be written, in order for them to know the offset of where
// their allocated-data block will start, which follows right behind).
func (ibe *IfdByteEncoder) encodeTagToBytes(ib *IfdBuilder, bt *BuilderTag, bw *ByteWriter, ida *ifdDataAllocator, nextIfdOffsetToWrite uint32) (childIfdBlock []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Write tag-ID.
err = bw.WriteUint16(bt.tagId)
log.PanicIf(err)
// Works for both values and child IFDs (which have an official size of
// LONG).
err = bw.WriteUint16(uint16(bt.typeId))
log.PanicIf(err)
// Write unit-count.
if bt.value.IsBytes() == true {
effectiveType := bt.typeId
if bt.typeId == TypeUndefined {
effectiveType = TypeByte
}
// It's a non-unknown value.Calculate the count of values of
// the type that we're writing and the raw bytes for the whole list.
typeSize := uint32(effectiveType.Size())
valueBytes := bt.value.Bytes()
len_ := len(valueBytes)
unitCount := uint32(len_) / typeSize
if _, found := tagsWithoutAlignment[bt.tagId]; found == false {
remainder := uint32(len_) % typeSize
if remainder > 0 {
log.Panicf("tag (0x%04x) value of (%d) bytes not evenly divisible by type-size (%d)", bt.tagId, len_, typeSize)
}
}
err = bw.WriteUint32(unitCount)
log.PanicIf(err)
// Write four-byte value/offset.
if len_ > 4 {
offset, err := ida.Allocate(valueBytes)
log.PanicIf(err)
err = bw.WriteUint32(offset)
log.PanicIf(err)
} else {
fourBytes := make([]byte, 4)
copy(fourBytes, valueBytes)
err = bw.WriteFourBytes(fourBytes)
log.PanicIf(err)
}
} else {
if bt.value.IsIb() == false {
log.Panicf("tag value is not a byte-slice but also not a child IB: %v", bt)
}
// Write unit-count (one LONG representing one offset).
err = bw.WriteUint32(1)
log.PanicIf(err)
if nextIfdOffsetToWrite > 0 {
var err error
ibe.pushToJournal("encodeTagToBytes", ">", "[%s]->[%s]", ib.ifdPath, bt.value.Ib().ifdPath)
// Create the block of IFD data and everything it requires.
childIfdBlock, err = ibe.encodeAndAttachIfd(bt.value.Ib(), nextIfdOffsetToWrite)
log.PanicIf(err)
ibe.pushToJournal("encodeTagToBytes", "<", "[%s]->[%s]", bt.value.Ib().ifdPath, ib.ifdPath)
// Use the next-IFD offset for it. The IFD will actually get
// attached after we return.
err = bw.WriteUint32(nextIfdOffsetToWrite)
log.PanicIf(err)
} else {
// No child-IFDs are to be allocated. Finish the entry with a NULL
// pointer.
ibe.pushToJournal("encodeTagToBytes", "-", "*Not* descending to child: [%s]", bt.value.Ib().ifdPath)
err = bw.WriteUint32(0)
log.PanicIf(err)
}
}
return childIfdBlock, nil
}
// encodeIfdToBytes encodes the given IB to a byte-slice. We are given the
// offset at which this IFD will be written. This method is used called both to
// pre-determine how big the table is going to be (so that we can calculate the
// address to allocate data at) as well as to write the final table.
//
// It is necessary to fully realize the table in order to predetermine its size
// because it is not enough to know the size of the table: If there are child
// IFDs, we will not be able to allocate them without first knowing how much
// data we need to allocate for the current IFD.
func (ibe *IfdByteEncoder) encodeIfdToBytes(ib *IfdBuilder, ifdAddressableOffset uint32, nextIfdOffsetToWrite uint32, setNextIb bool) (data []byte, tableSize uint32, dataSize uint32, childIfdSizes []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ibe.pushToJournal("encodeIfdToBytes", ">", "%s", ib)
tableSize = ibe.TableSize(len(ib.tags))
b := new(bytes.Buffer)
bw := NewByteWriter(b, ib.byteOrder)
// Write tag count.
err = bw.WriteUint16(uint16(len(ib.tags)))
log.PanicIf(err)
ida := newIfdDataAllocator(ifdAddressableOffset)
childIfdBlocks := make([][]byte, 0)
// Write raw bytes for each tag entry. Allocate larger data to be referred
// to in the follow-up data-block as required. Any "unknown"-byte tags that
// we can't parse will not be present here (using AddTagsFromExisting(), at
// least).
for _, bt := range ib.tags {
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if childIfdBlock != nil {
// We aren't allowed to have non-nil child IFDs if we're just
// sizing things up.
if nextIfdOffsetToWrite == 0 {
log.Panicf("no IFD offset provided for child-IFDs; no new child-IFDs permitted")
}
nextIfdOffsetToWrite += uint32(len(childIfdBlock))
childIfdBlocks = append(childIfdBlocks, childIfdBlock)
}
}
dataBytes := ida.Bytes()
dataSize = uint32(len(dataBytes))
childIfdSizes = make([]uint32, len(childIfdBlocks))
childIfdsTotalSize := uint32(0)
for i, childIfdBlock := range childIfdBlocks {
len_ := uint32(len(childIfdBlock))
childIfdSizes[i] = len_
childIfdsTotalSize += len_
}
// N the link from this IFD to the next IFD that will be written in the
// next cycle.
if setNextIb == true {
// Write address of next IFD in chain. This will be the original
// allocation offset plus the size of everything we have allocated for
// this IFD and its child-IFDs.
//
// It is critical that this number is stepped properly. We experienced
// an issue whereby it first looked like we were duplicating the IFD and
// then that we were duplicating the tags in the wrong IFD, and then
// finally we determined that the next-IFD offset for the first IFD was
// accidentally pointing back to the EXIF IFD, so we were visiting it
// twice when visiting through the tags after decoding. It was an
// expensive bug to find.
ibe.pushToJournal("encodeIfdToBytes", "-", "Setting 'next' IFD to (0x%08x).", nextIfdOffsetToWrite)
err := bw.WriteUint32(nextIfdOffsetToWrite)
log.PanicIf(err)
} else {
err := bw.WriteUint32(0)
log.PanicIf(err)
}
_, err = b.Write(dataBytes)
log.PanicIf(err)
// Append any child IFD blocks after our table and data blocks. These IFDs
// were equipped with the appropriate offset information so it's expected
// that all offsets referred to by these will be correct.
//
// Note that child-IFDs are append after the current IFD and before the
// next IFD, as opposed to the root IFDs, which are chained together but
// will be interrupted by these child-IFDs (which is expected, per the
// standard).
for _, childIfdBlock := range childIfdBlocks {
_, err = b.Write(childIfdBlock)
log.PanicIf(err)
}
ibe.pushToJournal("encodeIfdToBytes", "<", "%s", ib)
return b.Bytes(), tableSize, dataSize, childIfdSizes, nil
}
// encodeAndAttachIfd is a reentrant function that processes the IFD chain.
func (ibe *IfdByteEncoder) encodeAndAttachIfd(ib *IfdBuilder, ifdAddressableOffset uint32) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ibe.pushToJournal("encodeAndAttachIfd", ">", "%s", ib)
b := new(bytes.Buffer)
i := 0
for thisIb := ib; thisIb != nil; thisIb = thisIb.nextIb {
// Do a dry-run in order to pre-determine its size requirement.
ibe.pushToJournal("encodeAndAttachIfd", ">", "Beginning encoding process: (%d) [%s]", i, thisIb.ifdPath)
ibe.pushToJournal("encodeAndAttachIfd", ">", "Calculating size: (%d) [%s]", i, thisIb.ifdPath)
_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, 0, false)
log.PanicIf(err)
ibe.pushToJournal("encodeAndAttachIfd", "<", "Finished calculating size: (%d) [%s]", i, thisIb.ifdPath)
ifdAddressableOffset += tableSize
nextIfdOffsetToWrite := ifdAddressableOffset + allocatedDataSize
ibe.pushToJournal("encodeAndAttachIfd", ">", "Next IFD will be written at offset (0x%08x)", nextIfdOffsetToWrite)
// Write our IFD as well as any child-IFDs (now that we know the offset
// where new IFDs and their data will be allocated).
setNextIb := thisIb.nextIb != nil
ibe.pushToJournal("encodeAndAttachIfd", ">", "Encoding starting: (%d) [%s] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, thisIb.ifdPath, nextIfdOffsetToWrite)
tableAndAllocated, effectiveTableSize, effectiveAllocatedDataSize, childIfdSizes, err :=
ibe.encodeIfdToBytes(thisIb, ifdAddressableOffset, nextIfdOffsetToWrite, setNextIb)
log.PanicIf(err)
if effectiveTableSize != tableSize {
log.Panicf("written table size does not match the pre-calculated table size: (%d) != (%d) %s", effectiveTableSize, tableSize, ib)
} else if effectiveAllocatedDataSize != allocatedDataSize {
log.Panicf("written allocated-data size does not match the pre-calculated allocated-data size: (%d) != (%d) %s", effectiveAllocatedDataSize, allocatedDataSize, ib)
}
ibe.pushToJournal("encodeAndAttachIfd", "<", "Encoding done: (%d) [%s]", i, thisIb.ifdPath)
totalChildIfdSize := uint32(0)
for _, childIfdSize := range childIfdSizes {
totalChildIfdSize += childIfdSize
}
if len(tableAndAllocated) != int(tableSize+allocatedDataSize+totalChildIfdSize) {
log.Panicf("IFD table and data is not a consistent size: (%d) != (%d)", len(tableAndAllocated), tableSize+allocatedDataSize+totalChildIfdSize)
}
// TODO(dustin): We might want to verify the original tableAndAllocated length, too.
_, err = b.Write(tableAndAllocated)
log.PanicIf(err)
// Advance past what we've allocated, thus far.
ifdAddressableOffset += allocatedDataSize + totalChildIfdSize
ibe.pushToJournal("encodeAndAttachIfd", "<", "Finishing encoding process: (%d) [%s] [FINAL:] NEXT-IFD-OFFSET-TO-WRITE=(0x%08x)", i, ib.ifdPath, nextIfdOffsetToWrite)
i++
}
ibe.pushToJournal("encodeAndAttachIfd", "<", "%s", ib)
return b.Bytes(), nil
}
// EncodeToExifPayload is the base encoding step that transcribes the entire IB
// structure to its on-disk layout.
func (ibe *IfdByteEncoder) EncodeToExifPayload(ib *IfdBuilder) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
data, err = ibe.encodeAndAttachIfd(ib, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
return data, nil
}
// EncodeToExif calls EncodeToExifPayload and then packages the result into a
// complete EXIF block.
func (ibe *IfdByteEncoder) EncodeToExif(ib *IfdBuilder) (data []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
encodedIfds, err := ibe.EncodeToExifPayload(ib)
log.PanicIf(err)
// Wrap the IFD in a formal EXIF block.
b := new(bytes.Buffer)
headerBytes, err := BuildExifHeader(ib.byteOrder, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
_, err = b.Write(headerBytes)
log.PanicIf(err)
_, err = b.Write(encodedIfds)
log.PanicIf(err)
return b.Bytes(), nil
}

View File

@ -0,0 +1,906 @@
package exif
import (
"bytes"
"fmt"
"strings"
"testing"
"github.com/dsoprea/go-logging"
)
func Test_ByteWriter_writeAsBytes_uint8(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.writeAsBytes(uint8(0x12))
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x12}) != 0 {
t.Fatalf("uint8 not encoded correctly.")
}
}
func Test_ByteWriter_writeAsBytes_uint16(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.writeAsBytes(uint16(0x1234))
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x12, 0x34}) != 0 {
t.Fatalf("uint16 not encoded correctly.")
}
}
func Test_ByteWriter_writeAsBytes_uint32(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.writeAsBytes(uint32(0x12345678))
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x12, 0x34, 0x56, 0x78}) != 0 {
t.Fatalf("uint32 not encoded correctly.")
}
}
func Test_ByteWriter_WriteUint16(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.WriteUint16(uint16(0x1234))
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x12, 0x34}) != 0 {
t.Fatalf("uint16 not encoded correctly (as bytes).")
}
}
func Test_ByteWriter_WriteUint32(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.WriteUint32(uint32(0x12345678))
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x12, 0x34, 0x56, 0x78}) != 0 {
t.Fatalf("uint32 not encoded correctly (as bytes).")
}
}
func Test_ByteWriter_WriteFourBytes(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.WriteFourBytes([]byte{0x11, 0x22, 0x33, 0x44})
log.PanicIf(err)
if bytes.Compare(b.Bytes(), []byte{0x11, 0x22, 0x33, 0x44}) != 0 {
t.Fatalf("four-bytes not encoded correctly.")
}
}
func Test_ByteWriter_WriteFourBytes_TooMany(t *testing.T) {
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
err := bw.WriteFourBytes([]byte{0x11, 0x22, 0x33, 0x44, 0x55})
if err == nil {
t.Fatalf("expected error for not exactly four-bytes")
} else if err.Error() != "value is not four-bytes: (5)" {
t.Fatalf("wrong error for not exactly four bytes: %v", err)
}
}
func Test_IfdDataAllocator_Allocate_InitialOffset1(t *testing.T) {
addressableOffset := uint32(0)
ida := newIfdDataAllocator(addressableOffset)
if ida.NextOffset() != addressableOffset {
t.Fatalf("initial offset not correct: (%d) != (%d)", ida.NextOffset(), addressableOffset)
} else if len(ida.Bytes()) != 0 {
t.Fatalf("initial buffer not empty")
}
data := []byte{0x1, 0x2, 0x3}
offset, err := ida.Allocate(data)
log.PanicIf(err)
expected := uint32(addressableOffset + 0)
if offset != expected {
t.Fatalf("offset not bumped correctly (2): (%d) != (%d)", offset, expected)
} else if ida.NextOffset() != offset+uint32(3) {
t.Fatalf("position counter not advanced properly")
} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3}) != 0 {
t.Fatalf("buffer not correct after write (1)")
}
data = []byte{0x4, 0x5, 0x6}
offset, err = ida.Allocate(data)
log.PanicIf(err)
expected = uint32(addressableOffset + 3)
if offset != expected {
t.Fatalf("offset not bumped correctly (3): (%d) != (%d)", offset, expected)
} else if ida.NextOffset() != offset+uint32(3) {
t.Fatalf("position counter not advanced properly")
} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}) != 0 {
t.Fatalf("buffer not correct after write (2)")
}
}
func Test_IfdDataAllocator_Allocate_InitialOffset2(t *testing.T) {
addressableOffset := uint32(10)
ida := newIfdDataAllocator(addressableOffset)
if ida.NextOffset() != addressableOffset {
t.Fatalf("initial offset not correct: (%d) != (%d)", ida.NextOffset(), addressableOffset)
} else if len(ida.Bytes()) != 0 {
t.Fatalf("initial buffer not empty")
}
data := []byte{0x1, 0x2, 0x3}
offset, err := ida.Allocate(data)
log.PanicIf(err)
expected := uint32(addressableOffset + 0)
if offset != expected {
t.Fatalf("offset not bumped correctly (2): (%d) != (%d)", offset, expected)
} else if ida.NextOffset() != offset+uint32(3) {
t.Fatalf("position counter not advanced properly")
} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3}) != 0 {
t.Fatalf("buffer not correct after write (1)")
}
data = []byte{0x4, 0x5, 0x6}
offset, err = ida.Allocate(data)
log.PanicIf(err)
expected = uint32(addressableOffset + 3)
if offset != expected {
t.Fatalf("offset not bumped correctly (3): (%d) != (%d)", offset, expected)
} else if ida.NextOffset() != offset+uint32(3) {
t.Fatalf("position counter not advanced properly")
} else if bytes.Compare(ida.Bytes(), []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6}) != 0 {
t.Fatalf("buffer not correct after write (2)")
}
}
func Test_IfdByteEncoder__Arithmetic(t *testing.T) {
ibe := NewIfdByteEncoder()
if (ibe.TableSize(1) - ibe.TableSize(0)) != IfdTagEntrySize {
t.Fatalf("table-size/entry-size not consistent (1)")
} else if (ibe.TableSize(11) - ibe.TableSize(10)) != IfdTagEntrySize {
t.Fatalf("table-size/entry-size not consistent (2)")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_bytes_embedded1(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
ibe := NewIfdByteEncoder()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
it, err := ti.Get(ib.ifdPath, uint16(0x0000))
log.PanicIf(err)
bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12)})
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated")
} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x12, 0x00, 0x00, 0x00}) != 0 {
t.Fatalf("encoded tag-entry bytes not correct")
} else if ida.NextOffset() != addressableOffset {
t.Fatalf("allocation was done but not expected")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_bytes_embedded2(t *testing.T) {
ibe := NewIfdByteEncoder()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
it, err := ti.Get(ib.ifdPath, uint16(0x0000))
log.PanicIf(err)
bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78)})
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated")
} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x12, 0x34, 0x56, 0x78}) != 0 {
t.Fatalf("encoded tag-entry bytes not correct")
} else if ida.NextOffset() != addressableOffset {
t.Fatalf("allocation was done but not expected")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_bytes_allocated(t *testing.T) {
ibe := NewIfdByteEncoder()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
it, err := ti.Get(ib.ifdPath, uint16(0x0000))
log.PanicIf(err)
bt := NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0x12), uint8(0x34), uint8(0x56), uint8(0x78), uint8(0x9a)})
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated (1)")
} else if bytes.Compare(b.Bytes(), []byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x34}) != 0 {
t.Fatalf("encoded tag-entry bytes not correct (1)")
} else if ida.NextOffset() != addressableOffset+uint32(5) {
t.Fatalf("allocation offset not expected (1)")
} else if bytes.Compare(ida.Bytes(), []byte{0x12, 0x34, 0x56, 0x78, 0x9A}) != 0 {
t.Fatalf("allocated data not correct (1)")
}
// Test that another allocation encodes to the new offset.
bt = NewStandardBuilderTag(IfdPathStandardGps, it, TestDefaultByteOrder, []uint8{uint8(0xbc), uint8(0xde), uint8(0xf0), uint8(0x12), uint8(0x34)})
childIfdBlock, err = ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated (2)")
} else if bytes.Compare(b.Bytes(), []byte{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x34, // Tag 1
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x12, 0x39, // Tag 2
}) != 0 {
t.Fatalf("encoded tag-entry bytes not correct (2)")
} else if ida.NextOffset() != addressableOffset+uint32(10) {
t.Fatalf("allocation offset not expected (2)")
} else if bytes.Compare(ida.Bytes(), []byte{
0x12, 0x34, 0x56, 0x78, 0x9A,
0xbc, 0xde, 0xf0, 0x12, 0x34,
}) != 0 {
t.Fatalf("allocated data not correct (2)")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_childIfd__withoutAllocate(t *testing.T) {
ibe := NewIfdByteEncoder()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
bt := NewChildIfdBuilderTag(IfdPathStandard, IfdExifId, tagValue)
nextIfdOffsetToWrite := uint32(0)
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if childIfdBlock != nil {
t.Fatalf("no child-IFDs were expected to be allocated")
} else if bytes.Compare(b.Bytes(), []byte{0x87, 0x69, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}) != 0 {
t.Fatalf("encoded tag-entry with child-IFD not correct")
} else if ida.NextOffset() != addressableOffset {
t.Fatalf("allocation offset not expected")
}
}
func Test_IfdByteEncoder_encodeTagToBytes_childIfd__withAllocate(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
// Create a child IFD (represented by an IB instance) that we can allocate
// space for and then attach to a tag (which would normally be an entry,
// then, in a higher IFD).
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
childIbTestTag := &BuilderTag{
ifdPath: IfdPathStandardExif,
tagId: 0x8822,
typeId: TypeShort,
value: NewIfdBuilderTagValueFromBytes([]byte{0x12, 0x34}),
}
childIb.Add(childIbTestTag)
// Formally compose the tag that refers to it.
tagValue := NewIfdBuilderTagValueFromIfdBuilder(childIb)
bt := NewChildIfdBuilderTag(IfdPathStandard, IfdExifId, tagValue)
// Encode the tag. Since we've actually provided an offset at which we can
// allocate data, the child-IFD will automatically be encoded, allocated,
// and installed into the allocated-data block (which will follow the IFD
// block/table in the file).
ibe := NewIfdByteEncoder()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
// addressableOffset is the offset of where large data can be allocated
// (which follows the IFD table/block). Large, in that it can't be stored
// in the table itself. Just used for arithmetic. This is just where the
// data for the current IFD can be written. It's not absolute for the EXIF
// data in general.
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
// This is the offset of where the next IFD can be written in the EXIF byte
// stream. Just used for arithmetic.
nextIfdOffsetToWrite := uint32(2000)
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, nextIfdOffsetToWrite)
log.PanicIf(err)
if ida.NextOffset() != addressableOffset {
t.Fatalf("IDA offset changed but no allocations where expected: (0x%02x)", ida.NextOffset())
}
tagBytes := b.Bytes()
if len(tagBytes) != 12 {
t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
} else if len(childIfdBlock) != 18 {
t.Fatalf("Child IFD is not the right size: (%d)", len(childIfdBlock))
}
iteV, err := ParseOneTag(im, ti, fmt.Sprintf("%s%d", IfdPathStandard, 0), IfdPathStandard, TestDefaultByteOrder, tagBytes, false)
log.PanicIf(err)
if iteV.TagId != IfdExifId {
t.Fatalf("IFD first tag-ID not correct: (0x%02x)", iteV.TagId)
} else if iteV.TagIndex != 0 {
t.Fatalf("IFD first tag index not correct: (%d)", iteV.TagIndex)
} else if iteV.TagType != TypeLong {
t.Fatalf("IFD first tag type not correct: (%d)", iteV.TagType)
} else if iteV.UnitCount != 1 {
t.Fatalf("IFD first tag unit-count not correct: (%d)", iteV.UnitCount)
} else if iteV.ValueOffset != nextIfdOffsetToWrite {
t.Fatalf("IFD's child-IFD offset (as offset) is not correct: (%d) != (%d)", iteV.ValueOffset, nextIfdOffsetToWrite)
} else if bytes.Compare(iteV.RawValueOffset, []byte{0x0, 0x0, 0x07, 0xd0}) != 0 {
t.Fatalf("IFD's child-IFD offset (as raw bytes) is not correct: [%x]", iteV.RawValueOffset)
} else if iteV.ChildIfdPath != IfdPathStandardExif {
t.Fatalf("IFD first tag IFD-name name not correct: [%s]", iteV.ChildIfdPath)
} else if iteV.IfdPath != IfdPathStandard {
t.Fatalf("IFD first tag parent IFD not correct: %v", iteV.IfdPath)
}
// Validate the child's raw IFD bytes.
childNextIfdOffset, childEntries, err := ParseOneIfd(im, ti, "IFD0/Exif0", "IFD/Exif", TestDefaultByteOrder, childIfdBlock, nil, false)
log.PanicIf(err)
if childNextIfdOffset != uint32(0) {
t.Fatalf("Child IFD: Next IFD offset should be (0): (0x%08x)", childNextIfdOffset)
} else if len(childEntries) != 1 {
t.Fatalf("Child IFD: Expected exactly one entry: (%d)", len(childEntries))
}
ite := childEntries[0]
if ite.TagId != 0x8822 {
t.Fatalf("Child IFD first tag-ID not correct: (0x%02x)", ite.TagId)
} else if ite.TagIndex != 0 {
t.Fatalf("Child IFD first tag index not correct: (%d)", ite.TagIndex)
} else if ite.TagType != TypeShort {
t.Fatalf("Child IFD first tag type not correct: (%d)", ite.TagType)
} else if ite.UnitCount != 1 {
t.Fatalf("Child IFD first tag unit-count not correct: (%d)", ite.UnitCount)
} else if ite.ValueOffset != 0x12340000 {
t.Fatalf("Child IFD first tag value value (as offset) not correct: (0x%02x)", ite.ValueOffset)
} else if bytes.Compare(ite.RawValueOffset, []byte{0x12, 0x34, 0x0, 0x0}) != 0 {
t.Fatalf("Child IFD first tag value value (as raw bytes) not correct: [%v]", ite.RawValueOffset)
} else if ite.ChildIfdPath != "" {
t.Fatalf("Child IFD first tag IFD-name name not empty: [%s]", ite.ChildIfdPath)
} else if ite.IfdPath != IfdPathStandardExif {
t.Fatalf("Child IFD first tag parent IFD not correct: %v", ite.IfdPath)
}
}
func Test_IfdByteEncoder_encodeTagToBytes_simpleTag_allocate(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
// Encode the tag. Since we've actually provided an offset at which we can
// allocate data, the child-IFD will automatically be encoded, allocated,
// and installed into the allocated-data block (which will follow the IFD
// block/table in the file).
ibe := NewIfdByteEncoder()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
it, err := ib.tagIndex.Get(ib.ifdPath, uint16(0x000b))
log.PanicIf(err)
valueString := "testvalue"
bt := NewStandardBuilderTag(IfdPathStandard, it, TestDefaultByteOrder, valueString)
b := new(bytes.Buffer)
bw := NewByteWriter(b, TestDefaultByteOrder)
// addressableOffset is the offset of where large data can be allocated
// (which follows the IFD table/block). Large, in that it can't be stored
// in the table itself. Just used for arithmetic. This is just where the
// data for the current IFD can be written. It's not absolute for the EXIF
// data in general.
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
childIfdBlock, err := ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
if ida.NextOffset() == addressableOffset {
t.Fatalf("IDA offset did not change even though there should've been an allocation.")
}
tagBytes := b.Bytes()
if len(tagBytes) != 12 {
t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
} else if len(childIfdBlock) != 0 {
t.Fatalf("Child IFD not have been allocated.")
}
ite, err := ParseOneTag(im, ti, fmt.Sprintf("%s%d", IfdPathStandard, 0), IfdPathStandard, TestDefaultByteOrder, tagBytes, false)
log.PanicIf(err)
if ite.TagId != 0x000b {
t.Fatalf("Tag-ID not correct: (0x%02x)", ite.TagId)
} else if ite.TagIndex != 0 {
t.Fatalf("Tag index not correct: (%d)", ite.TagIndex)
} else if ite.TagType != TypeAscii {
t.Fatalf("Tag type not correct: (%d)", ite.TagType)
} else if ite.UnitCount != (uint32(len(valueString) + 1)) {
t.Fatalf("Tag unit-count not correct: (%d)", ite.UnitCount)
} else if ite.ValueOffset != addressableOffset {
t.Fatalf("Tag's value (as offset) is not correct: (%d) != (%d)", ite.ValueOffset, addressableOffset)
} else if bytes.Compare(ite.RawValueOffset, []byte{0x0, 0x0, 0x12, 0x34}) != 0 {
t.Fatalf("Tag's value (as raw bytes) is not correct: [%x]", ite.RawValueOffset)
} else if ite.ChildIfdPath != "" {
t.Fatalf("Tag's IFD-name should be empty: [%s]", ite.ChildIfdPath)
} else if ite.IfdPath != IfdPathStandard {
t.Fatalf("Tag's parent IFD is not correct: %v", ite.IfdPath)
}
expectedBuffer := bytes.NewBufferString(valueString)
expectedBuffer.Write([]byte{0x0})
expectedBytes := expectedBuffer.Bytes()
allocatedBytes := ida.Bytes()
if bytes.Compare(allocatedBytes, expectedBytes) != 0 {
t.Fatalf("Allocated bytes not correct: %v != %v", allocatedBytes, expectedBytes)
}
}
func Test_IfdByteEncoder_encodeIfdToBytes_simple(t *testing.T) {
ib := getExifSimpleTestIb()
// Write the byte stream.
ibe := NewIfdByteEncoder()
// addressableOffset is the offset of where large data can be allocated
// (which follows the IFD table/block). Large, in that it can't be stored
// in the table itself. Just used for arithmetic. This is just where the
// data for the current IFD can be written. It's not absolute for the EXIF
// data in general.
addressableOffset := uint32(0x1234)
tableAndAllocated, tableSize, allocatedDataSize, childIfdSizes, err := ibe.encodeIfdToBytes(ib, addressableOffset, uint32(0), false)
log.PanicIf(err)
expectedTableSize := ibe.TableSize(4)
if tableSize != expectedTableSize {
t.Fatalf("Table-size not the right size: (%d) != (%d)", tableSize, expectedTableSize)
} else if len(childIfdSizes) != 0 {
t.Fatalf("One or more child IFDs were allocated but shouldn't have been: (%d)", len(childIfdSizes))
}
// The ASCII value plus the rational size.
expectedAllocatedSize := 11 + 8
if int(allocatedDataSize) != expectedAllocatedSize {
t.Fatalf("Allocated data size not correct: (%d)", allocatedDataSize)
}
expectedIfdAndDataBytes := []byte{
// IFD table block.
// - Tag count
0x00, 0x04,
// - Tags
0x00, 0x0b, 0x00, 0x02, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x12, 0x34,
0x00, 0xff, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x11, 0x22, 0x00, 0x00,
0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x33, 0x44, 0x55, 0x66,
0x01, 0x3e, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x12, 0x3f,
// - Next IFD offset
0x00, 0x00, 0x00, 0x00,
// IFD data block.
// - The one ASCII value
0x61, 0x73, 0x63, 0x69, 0x69, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x00,
// - The one rational value
0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
}
if bytes.Compare(tableAndAllocated, expectedIfdAndDataBytes) != 0 {
t.Fatalf("IFD table and allocated data not correct: %v", DumpBytesClauseToString(tableAndAllocated))
}
}
func Test_IfdByteEncoder_encodeIfdToBytes_fullExif(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
ib := getExifSimpleTestIb()
// Encode the IFD to a byte stream.
ibe := NewIfdByteEncoder()
// Run a simulation just to figure out the sizes.
_, tableSize, allocatedDataSize, _, err := ibe.encodeIfdToBytes(ib, uint32(0), uint32(0), false)
log.PanicIf(err)
addressableOffset := ExifDefaultFirstIfdOffset + tableSize
nextIfdOffsetToWrite := addressableOffset + allocatedDataSize
// Run the final encode now that we can correctly assign the offsets.
tableAndAllocated, _, _, _, err := ibe.encodeIfdToBytes(ib, addressableOffset, uint32(nextIfdOffsetToWrite), false)
log.PanicIf(err)
if len(tableAndAllocated) != (int(tableSize) + int(allocatedDataSize)) {
t.Fatalf("Table-and-data size doesn't match what was expected: (%d) != (%d + %d)", len(tableAndAllocated), tableSize, allocatedDataSize)
}
// Wrap the IFD in a formal EXIF block.
b := new(bytes.Buffer)
headerBytes, err := BuildExifHeader(TestDefaultByteOrder, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
_, err = b.Write(headerBytes)
log.PanicIf(err)
_, err = b.Write(tableAndAllocated)
log.PanicIf(err)
// Now, try parsing it as EXIF data, making sure to resolve (read:
// dereference) the values (which will include the allocated ones).
exifData := b.Bytes()
validateExifSimpleTestIb(exifData, t)
}
func Test_IfdByteEncoder_EncodeToExifPayload(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
ib := getExifSimpleTestIb()
// Encode the IFD to a byte stream.
ibe := NewIfdByteEncoder()
encodedIfds, err := ibe.EncodeToExifPayload(ib)
log.PanicIf(err)
// Wrap the IFD in a formal EXIF block.
b := new(bytes.Buffer)
headerBytes, err := BuildExifHeader(TestDefaultByteOrder, ExifDefaultFirstIfdOffset)
log.PanicIf(err)
_, err = b.Write(headerBytes)
log.PanicIf(err)
_, err = b.Write(encodedIfds)
log.PanicIf(err)
// Now, try parsing it as EXIF data, making sure to resolve (read:
// dereference) the values (which will include the allocated ones).
exifData := b.Bytes()
validateExifSimpleTestIb(exifData, t)
}
func Test_IfdByteEncoder_EncodeToExif(t *testing.T) {
ib := getExifSimpleTestIb()
// TODO(dustin): Do a child-IFD allocation in addition to the tag allocations.
ibe := NewIfdByteEncoder()
exifData, err := ibe.EncodeToExif(ib)
log.PanicIf(err)
validateExifSimpleTestIb(exifData, t)
}
func Test_IfdByteEncoder_EncodeToExif_WithChildAndSibling(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
panic(err)
}
}()
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
err = ib.AddStandard(0x000b, "asciivalue")
log.PanicIf(err)
err = ib.AddStandard(0x00ff, []uint16{0x1122})
log.PanicIf(err)
// Add a child IB right in the middle.
childIb := NewIfdBuilder(im, ti, IfdPathStandardExif, TestDefaultByteOrder)
err = childIb.AddStandardWithName("ISOSpeedRatings", []uint16{0x1122})
log.PanicIf(err)
err = childIb.AddStandardWithName("ISOSpeed", []uint32{0x33445566})
log.PanicIf(err)
err = ib.AddChildIb(childIb)
log.PanicIf(err)
err = ib.AddStandard(0x0100, []uint32{0x33445566})
log.PanicIf(err)
// Add another child IB, just to ensure a little more punishment and make
// sure we're managing our allocation offsets correctly.
childIb2 := NewIfdBuilder(im, ti, IfdPathStandardGps, TestDefaultByteOrder)
err = childIb2.AddStandardWithName("GPSAltitudeRef", []uint8{0x11, 0x22})
log.PanicIf(err)
err = ib.AddChildIb(childIb2)
log.PanicIf(err)
err = ib.AddStandard(0x013e, []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
log.PanicIf(err)
// Link to another IB (sibling relationship). The root/standard IFD may
// occur twice in some JPEGs (for thumbnail or FlashPix images).
nextIb := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
err = nextIb.AddStandard(0x0101, []uint32{0x11223344})
log.PanicIf(err)
err = nextIb.AddStandard(0x0102, []uint16{0x5566})
log.PanicIf(err)
ib.SetNextIb(nextIb)
// Encode.
ibe := NewIfdByteEncoder()
exifData, err := ibe.EncodeToExif(ib)
log.PanicIf(err)
// Parse.
_, index, err := Collect(im, ti, exifData)
log.PanicIf(err)
tagsDump := index.RootIfd.DumpTree()
actual := strings.Join(tagsDump, "\n")
expected :=
`> IFD [ROOT]->[IFD]:(0) TOP
- (0x000b)
- (0x00ff)
- (0x8769)
> IFD [IFD]->[IFD/Exif]:(0) TOP
- (0x8827)
- (0x8833)
< IFD [IFD]->[IFD/Exif]:(0) BOTTOM
- (0x0100)
- (0x8825)
> IFD [IFD]->[IFD/GPSInfo]:(0) TOP
- (0x0005)
< IFD [IFD]->[IFD/GPSInfo]:(0) BOTTOM
- (0x013e)
< IFD [ROOT]->[IFD]:(0) BOTTOM
* LINKING TO SIBLING IFD [IFD]:(1)
> IFD [ROOT]->[IFD]:(1) TOP
- (0x0101)
- (0x0102)
< IFD [ROOT]->[IFD]:(1) BOTTOM`
if actual != expected {
fmt.Printf("\n")
fmt.Printf("Actual:\n")
fmt.Printf("\n")
fmt.Printf("%s\n", actual)
fmt.Printf("\n")
fmt.Printf("Expected:\n")
fmt.Printf("\n")
fmt.Printf("%s\n", expected)
fmt.Printf("\n")
t.Fatalf("IFD hierarchy not correct.")
}
}
func ExampleIfdByteEncoder_EncodeToExif() {
// Construct an IFD.
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ib := NewIfdBuilder(im, ti, IfdPathStandard, TestDefaultByteOrder)
err = ib.AddStandardWithName("ProcessingSoftware", "asciivalue")
log.PanicIf(err)
err = ib.AddStandardWithName("DotRange", []uint8{0x11})
log.PanicIf(err)
err = ib.AddStandardWithName("SubfileType", []uint16{0x2233})
log.PanicIf(err)
err = ib.AddStandardWithName("ImageWidth", []uint32{0x44556677})
log.PanicIf(err)
err = ib.AddStandardWithName("WhitePoint", []Rational{{Numerator: 0x11112222, Denominator: 0x33334444}})
log.PanicIf(err)
err = ib.AddStandardWithName("ShutterSpeedValue", []SignedRational{{Numerator: 0x11112222, Denominator: 0x33334444}})
log.PanicIf(err)
// Encode it.
ibe := NewIfdByteEncoder()
exifData, err := ibe.EncodeToExif(ib)
log.PanicIf(err)
// Parse it so we can see it.
_, index, err := Collect(im, ti, exifData)
log.PanicIf(err)
// addressableData is the byte-slice where the allocated data can be
// resolved (where position 0x0 will correlate with offset 0x0).
addressableData := exifData[ExifAddressableAreaStart:]
for i, e := range index.RootIfd.Entries {
value, err := e.Value(addressableData, TestDefaultByteOrder)
log.PanicIf(err)
fmt.Printf("%d: %s [%v]\n", i, e, value)
}
// Output:
//
// 0: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x000b) TAG-TYPE=[ASCII] UNIT-COUNT=(11)> [asciivalue]
// 1: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0150) TAG-TYPE=[BYTE] UNIT-COUNT=(1)> [[17]]
// 2: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x00ff) TAG-TYPE=[SHORT] UNIT-COUNT=(1)> [[8755]]
// 3: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0100) TAG-TYPE=[LONG] UNIT-COUNT=(1)> [[1146447479]]
// 4: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x013e) TAG-TYPE=[RATIONAL] UNIT-COUNT=(1)> [[{286335522 858997828}]]
// 5: IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x9201) TAG-TYPE=[SRATIONAL] UNIT-COUNT=(1)> [[{286335522 858997828}]]
}

2110
v2/ifd_builder_test.go Normal file

File diff suppressed because it is too large Load Diff

1356
v2/ifd_enumerate.go Normal file

File diff suppressed because it is too large Load Diff

532
v2/ifd_enumerate_test.go Normal file
View File

@ -0,0 +1,532 @@
package exif
import (
"bytes"
"fmt"
"path"
"reflect"
"testing"
"encoding/binary"
"io/ioutil"
"github.com/dsoprea/go-logging"
)
func TestIfdTagEntry_ValueBytes(t *testing.T) {
byteOrder := binary.BigEndian
ve := NewValueEncoder(byteOrder)
original := []byte("original text")
ed, err := ve.encodeBytes(original)
log.PanicIf(err)
// Now, pass the raw encoded value as if it was the entire addressable area
// and provide an offset of 0 as if it was a real block of data and this
// value happened to be recorded at the beginning.
ite := IfdTagEntry{
TagType: TypeByte,
UnitCount: uint32(len(original)),
ValueOffset: 0,
}
decodedBytes, err := ite.ValueBytes(ed.Encoded, byteOrder)
log.PanicIf(err)
if bytes.Compare(decodedBytes, original) != 0 {
t.Fatalf("Bytes not decoded correctly.")
}
}
func TestIfdTagEntry_ValueBytes_RealData(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
}
}()
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
eh, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
var ite *IfdTagEntry
for _, thisIte := range index.RootIfd.Entries {
if thisIte.TagId == 0x0110 {
ite = thisIte
break
}
}
if ite == nil {
t.Fatalf("Tag not found.")
}
addressableData := rawExif[ExifAddressableAreaStart:]
decodedBytes, err := ite.ValueBytes(addressableData, eh.ByteOrder)
log.PanicIf(err)
expected := []byte("Canon EOS 5D Mark III")
expected = append(expected, 0)
if len(decodedBytes) != int(ite.UnitCount) {
t.Fatalf("Decoded bytes not the right count.")
} else if bytes.Compare(decodedBytes, expected) != 0 {
t.Fatalf("Decoded bytes not correct.")
}
}
func TestIfd_FindTagWithId_Hit(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
results, err := ifd.FindTagWithId(0x011b)
if len(results) != 1 {
t.Fatalf("Exactly one result was not found: (%d)", len(results))
} else if results[0].TagId != 0x011b {
t.Fatalf("The result was not expected: %v", results[0])
}
}
func TestIfd_FindTagWithId_Miss(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
_, err = ifd.FindTagWithId(0xffff)
if err == nil {
t.Fatalf("Expected error for not-found tag.")
} else if log.Is(err, ErrTagNotFound) == false {
log.Panic(err)
}
}
func TestIfd_FindTagWithName_Hit(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
results, err := ifd.FindTagWithName("YResolution")
if len(results) != 1 {
t.Fatalf("Exactly one result was not found: (%d)", len(results))
} else if results[0].TagId != 0x011b {
t.Fatalf("The result was not expected: %v", results[0])
}
}
func TestIfd_FindTagWithName_Miss(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
_, err = ifd.FindTagWithName("PlanarConfiguration")
if err == nil {
t.Fatalf("Expected error for not-found tag.")
} else if log.Is(err, ErrTagNotFound) == false {
log.Panic(err)
}
}
func TestIfd_FindTagWithName_NonStandard(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
_, err = ifd.FindTagWithName("GeorgeNotAtHome")
if err == nil {
t.Fatalf("Expected error for not-found tag.")
} else if log.Is(err, ErrTagNotStandard) == false {
log.Panic(err)
}
}
func TestIfd_Thumbnail(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
if ifd.NextIfd == nil {
t.Fatalf("There is no IFD1.")
}
// The thumbnail is in IFD1 (The second root IFD).
actual, err := ifd.NextIfd.Thumbnail()
log.PanicIf(err)
expectedFilepath := path.Join(assetsPath, "NDM_8901.jpg.thumbnail")
expected, err := ioutil.ReadFile(expectedFilepath)
log.PanicIf(err)
if bytes.Compare(actual, expected) != 0 {
t.Fatalf("thumbnail not correct")
}
}
func TestIfd_GpsInfo(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
}
}()
filepath := path.Join(assetsPath, "gps.jpg")
rawExif, err := SearchFileAndExtractExif(filepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd, err := index.RootIfd.ChildWithIfdPath(IfdPathStandardGps)
log.PanicIf(err)
gi, err := ifd.GpsInfo()
log.PanicIf(err)
if gi.Latitude.Orientation != 'N' || gi.Latitude.Degrees != 26 || gi.Latitude.Minutes != 35 || gi.Latitude.Seconds != 12 {
t.Fatalf("latitude not correct")
} else if gi.Longitude.Orientation != 'W' || gi.Longitude.Degrees != 80 || gi.Longitude.Minutes != 3 || gi.Longitude.Seconds != 13 {
t.Fatalf("longitude not correct")
} else if gi.Altitude != 0 {
t.Fatalf("altitude not correct")
} else if gi.Timestamp.Unix() != 1524964977 {
t.Fatalf("timestamp not correct")
} else if gi.Altitude != 0 {
t.Fatalf("altitude not correct")
}
}
func TestIfd_EnumerateTagsRecursively(t *testing.T) {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
collected := make([][2]interface{}, 0)
cb := func(ifd *Ifd, ite *IfdTagEntry) error {
item := [2]interface{}{
ifd.IfdPath,
int(ite.TagId),
}
collected = append(collected, item)
return nil
}
err = index.RootIfd.EnumerateTagsRecursively(cb)
log.PanicIf(err)
expected := [][2]interface{}{
[2]interface{}{"IFD", 0x010f},
[2]interface{}{"IFD", 0x0110},
[2]interface{}{"IFD", 0x0112},
[2]interface{}{"IFD", 0x011a},
[2]interface{}{"IFD", 0x011b},
[2]interface{}{"IFD", 0x0128},
[2]interface{}{"IFD", 0x0132},
[2]interface{}{"IFD", 0x013b},
[2]interface{}{"IFD", 0x0213},
[2]interface{}{"IFD", 0x8298},
[2]interface{}{"IFD/Exif", 0x829a},
[2]interface{}{"IFD/Exif", 0x829d},
[2]interface{}{"IFD/Exif", 0x8822},
[2]interface{}{"IFD/Exif", 0x8827},
[2]interface{}{"IFD/Exif", 0x8830},
[2]interface{}{"IFD/Exif", 0x8832},
[2]interface{}{"IFD/Exif", 0x9000},
[2]interface{}{"IFD/Exif", 0x9003},
[2]interface{}{"IFD/Exif", 0x9004},
[2]interface{}{"IFD/Exif", 0x9101},
[2]interface{}{"IFD/Exif", 0x9201},
[2]interface{}{"IFD/Exif", 0x9202},
[2]interface{}{"IFD/Exif", 0x9204},
[2]interface{}{"IFD/Exif", 0x9207},
[2]interface{}{"IFD/Exif", 0x9209},
[2]interface{}{"IFD/Exif", 0x920a},
[2]interface{}{"IFD/Exif", 0x927c},
[2]interface{}{"IFD/Exif", 0x9286},
[2]interface{}{"IFD/Exif", 0x9290},
[2]interface{}{"IFD/Exif", 0x9291},
[2]interface{}{"IFD/Exif", 0x9292},
[2]interface{}{"IFD/Exif", 0xa000},
[2]interface{}{"IFD/Exif", 0xa001},
[2]interface{}{"IFD/Exif", 0xa002},
[2]interface{}{"IFD/Exif", 0xa003},
[2]interface{}{"IFD/Exif/Iop", 0x0001},
[2]interface{}{"IFD/Exif/Iop", 0x0002},
[2]interface{}{"IFD/Exif", 0xa20e},
[2]interface{}{"IFD/Exif", 0xa20f},
[2]interface{}{"IFD/Exif", 0xa210},
[2]interface{}{"IFD/Exif", 0xa401},
[2]interface{}{"IFD/Exif", 0xa402},
[2]interface{}{"IFD/Exif", 0xa403},
[2]interface{}{"IFD/Exif", 0xa406},
[2]interface{}{"IFD/Exif", 0xa430},
[2]interface{}{"IFD/Exif", 0xa431},
[2]interface{}{"IFD/Exif", 0xa432},
[2]interface{}{"IFD/Exif", 0xa434},
[2]interface{}{"IFD/Exif", 0xa435},
[2]interface{}{"IFD/GPSInfo", 0x0000},
[2]interface{}{"IFD", 0x010f},
[2]interface{}{"IFD", 0x0110},
[2]interface{}{"IFD", 0x0112},
[2]interface{}{"IFD", 0x011a},
[2]interface{}{"IFD", 0x011b},
[2]interface{}{"IFD", 0x0128},
[2]interface{}{"IFD", 0x0132},
[2]interface{}{"IFD", 0x013b},
[2]interface{}{"IFD", 0x0213},
[2]interface{}{"IFD", 0x8298},
[2]interface{}{"IFD/Exif", 0x829a},
[2]interface{}{"IFD/Exif", 0x829d},
[2]interface{}{"IFD/Exif", 0x8822},
[2]interface{}{"IFD/Exif", 0x8827},
[2]interface{}{"IFD/Exif", 0x8830},
[2]interface{}{"IFD/Exif", 0x8832},
[2]interface{}{"IFD/Exif", 0x9000},
[2]interface{}{"IFD/Exif", 0x9003},
[2]interface{}{"IFD/Exif", 0x9004},
[2]interface{}{"IFD/Exif", 0x9101},
[2]interface{}{"IFD/Exif", 0x9201},
[2]interface{}{"IFD/Exif", 0x9202},
[2]interface{}{"IFD/Exif", 0x9204},
[2]interface{}{"IFD/Exif", 0x9207},
[2]interface{}{"IFD/Exif", 0x9209},
[2]interface{}{"IFD/Exif", 0x920a},
[2]interface{}{"IFD/Exif", 0x927c},
[2]interface{}{"IFD/Exif", 0x9286},
[2]interface{}{"IFD/Exif", 0x9290},
[2]interface{}{"IFD/Exif", 0x9291},
[2]interface{}{"IFD/Exif", 0x9292},
[2]interface{}{"IFD/Exif", 0xa000},
[2]interface{}{"IFD/Exif", 0xa001},
[2]interface{}{"IFD/Exif", 0xa002},
[2]interface{}{"IFD/Exif", 0xa003},
[2]interface{}{"IFD/Exif/Iop", 0x0001},
[2]interface{}{"IFD/Exif/Iop", 0x0002},
[2]interface{}{"IFD/Exif", 0xa20e},
[2]interface{}{"IFD/Exif", 0xa20f},
[2]interface{}{"IFD/Exif", 0xa210},
[2]interface{}{"IFD/Exif", 0xa401},
[2]interface{}{"IFD/Exif", 0xa402},
[2]interface{}{"IFD/Exif", 0xa403},
[2]interface{}{"IFD/Exif", 0xa406},
[2]interface{}{"IFD/Exif", 0xa430},
[2]interface{}{"IFD/Exif", 0xa431},
[2]interface{}{"IFD/Exif", 0xa432},
[2]interface{}{"IFD/Exif", 0xa434},
[2]interface{}{"IFD/Exif", 0xa435},
[2]interface{}{"IFD/GPSInfo", 0x0000},
}
if reflect.DeepEqual(collected, expected) != true {
fmt.Printf("ACTUAL:\n")
fmt.Printf("\n")
for _, item := range collected {
fmt.Printf("[2]interface{} { \"%s\", 0x%04x },\n", item[0], item[1])
}
fmt.Printf("\n")
fmt.Printf("EXPECTED:\n")
fmt.Printf("\n")
for _, item := range expected {
fmt.Printf("[2]interface{} { \"%s\", 0x%04x },\n", item[0], item[1])
}
fmt.Printf("\n")
t.Fatalf("tags not visited correctly")
}
}
func ExampleIfd_EnumerateTagsRecursively() {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
cb := func(ifd *Ifd, ite *IfdTagEntry) error {
// Something useful.
return nil
}
err = index.RootIfd.EnumerateTagsRecursively(cb)
log.PanicIf(err)
// Output:
}
func ExampleIfd_GpsInfo() {
filepath := path.Join(assetsPath, "gps.jpg")
rawExif, err := SearchFileAndExtractExif(filepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd, err := index.RootIfd.ChildWithIfdPath(IfdPathStandardGps)
log.PanicIf(err)
gi, err := ifd.GpsInfo()
log.PanicIf(err)
fmt.Printf("%s\n", gi)
// Output:
// GpsInfo<LAT=(26.58667) LON=(-80.05361) ALT=(0) TIME=[2018-04-29 01:22:57 +0000 UTC]>
}
func ExampleIfd_FindTagWithName() {
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
tagName := "Model"
// We know the tag we want is on IFD0 (the first/root IFD).
results, err := index.RootIfd.FindTagWithName(tagName)
log.PanicIf(err)
// This should never happen.
if len(results) != 1 {
log.Panicf("there wasn't exactly one result")
}
ite := results[0]
valueRaw, err := index.RootIfd.TagValue(ite)
log.PanicIf(err)
value := valueRaw.(string)
fmt.Println(value)
// Output:
// Canon EOS 5D Mark III
}

233
v2/ifd_tag_entry.go Normal file
View File

@ -0,0 +1,233 @@
package exif
import (
"fmt"
"reflect"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
iteLogger = log.NewLogger("exif.ifd_tag_entry")
)
type IfdTagEntry struct {
TagId uint16
TagIndex int
TagType TagTypePrimitive
UnitCount uint32
ValueOffset uint32
RawValueOffset []byte
// ChildIfdName is the right most atom in the IFD-path. We need this to
// construct the fully-qualified IFD-path.
ChildIfdName string
// ChildIfdPath is the IFD-path of the child if this tag represents a child
// IFD.
ChildIfdPath string
// ChildFqIfdPath is the IFD-path of the child if this tag represents a
// child IFD. Includes indices.
ChildFqIfdPath string
// TODO(dustin): !! IB's host the child-IBs directly in the tag, but that's not the case here. Refactor to accomodate it for a consistent experience.
// IfdPath is the IFD that this tag belongs to.
IfdPath string
// TODO(dustin): !! We now parse and read the value immediately. Update the rest of the logic to use this and get rid of all of the staggered and different resolution mechanisms.
value []byte
isUnhandledUnknown bool
}
func (ite *IfdTagEntry) String() string {
return fmt.Sprintf("IfdTagEntry<TAG-IFD-PATH=[%s] TAG-ID=(0x%04x) TAG-TYPE=[%s] UNIT-COUNT=(%d)>", ite.IfdPath, ite.TagId, TypeNames[ite.TagType], ite.UnitCount)
}
// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
//
// func (ite *IfdTagEntry) IfdPath() string {
// return ite.IfdPath
// }
// TODO(dustin): TODO(dustin): Stop exporting IfdPath and TagId.
//
// func (ite *IfdTagEntry) TagId() uint16 {
// return ite.TagId
// }
// ValueString renders a string from whatever the value in this tag is.
func (ite *IfdTagEntry) ValueString(addressableData []byte, byteOrder binary.ByteOrder) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
if ite.TagType == TypeUndefined {
valueRaw, err := valueContext.Undefined()
log.PanicIf(err)
value = fmt.Sprintf("%v", valueRaw)
} else {
value, err = valueContext.Format()
log.PanicIf(err)
}
return value, nil
}
// ValueBytes renders a specific list of bytes from the value in this tag.
func (ite *IfdTagEntry) ValueBytes(addressableData []byte, byteOrder binary.ByteOrder) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Return the exact bytes of the unknown-type value. Returning a string
// (`ValueString`) is easy because we can just pass everything to
// `Sprintf()`. Returning the raw, typed value (`Value`) is easy
// (obviously). However, here, in order to produce the list of bytes, we
// need to coerce whatever `Undefined()` returns.
if ite.TagType == TypeUndefined {
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
value, err := valueContext.Undefined()
log.PanicIf(err)
switch value.(type) {
case []byte:
return value.([]byte), nil
case TagUnknownType_UnknownValue:
b := []byte(value.(TagUnknownType_UnknownValue))
return b, nil
case string:
return []byte(value.(string)), nil
case UnknownTagValue:
valueBytes, err := value.(UnknownTagValue).ValueBytes()
log.PanicIf(err)
return valueBytes, nil
default:
// TODO(dustin): !! Finish translating the rest of the types (make reusable and replace into other similar implementations?)
log.Panicf("can not produce bytes for unknown-type tag (0x%04x) (2): [%s]", ite.TagId, reflect.TypeOf(value))
}
}
originalType := NewTagType(ite.TagType, byteOrder)
byteCount := uint32(originalType.Type().Size()) * ite.UnitCount
tt := NewTagType(TypeByte, byteOrder)
if tt.valueIsEmbedded(byteCount) == true {
iteLogger.Debugf(nil, "Reading BYTE value (ITE; embedded).")
// In this case, the bytes normally used for the offset are actually
// data.
value, err = tt.ParseBytes(ite.RawValueOffset, byteCount)
log.PanicIf(err)
} else {
iteLogger.Debugf(nil, "Reading BYTE value (ITE; at offset).")
value, err = tt.ParseBytes(addressableData[ite.ValueOffset:], byteCount)
log.PanicIf(err)
}
return value, nil
}
// Value returns the specific, parsed, typed value from the tag.
func (ite *IfdTagEntry) Value(addressableData []byte, byteOrder binary.ByteOrder) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
valueContext :=
newValueContextFromTag(
ite,
addressableData,
byteOrder)
if ite.TagType == TypeUndefined {
value, err = valueContext.Undefined()
log.PanicIf(err)
} else {
tt := NewTagType(ite.TagType, byteOrder)
value, err = tt.Resolve(valueContext)
log.PanicIf(err)
}
return value, nil
}
// IfdTagEntryValueResolver instances know how to resolve the values for any
// tag for a particular EXIF block.
type IfdTagEntryValueResolver struct {
addressableData []byte
byteOrder binary.ByteOrder
}
func NewIfdTagEntryValueResolver(exifData []byte, byteOrder binary.ByteOrder) (itevr *IfdTagEntryValueResolver) {
return &IfdTagEntryValueResolver{
addressableData: exifData[ExifAddressableAreaStart:],
byteOrder: byteOrder,
}
}
// ValueBytes will resolve embedded or allocated data from the tag and return the raw bytes.
func (itevr *IfdTagEntryValueResolver) ValueBytes(ite *IfdTagEntry) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).readRawEncoded()` instead of this method.
valueContext := newValueContextFromTag(
ite,
itevr.addressableData,
itevr.byteOrder)
rawBytes, err := valueContext.readRawEncoded()
log.PanicIf(err)
return rawBytes, nil
}
func (itevr *IfdTagEntryValueResolver) Value(ite *IfdTagEntry) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// OBSOLETE(dustin): This is now redundant. Use `(*ValueContext).Values()` instead of this method.
valueContext := newValueContextFromTag(
ite,
itevr.addressableData,
itevr.byteOrder)
values, err := valueContext.Values()
log.PanicIf(err)
return values, nil
}

210
v2/ifd_tag_entry_test.go Normal file
View File

@ -0,0 +1,210 @@
package exif
import (
"bytes"
"testing"
"github.com/dsoprea/go-logging"
)
func TestIfdTagEntry_ValueString_Allocated(t *testing.T) {
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 6,
ValueOffset: 0x0,
RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
IfdPath: IfdPathStandard,
}
data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
value, err := ite.ValueString(data, TestDefaultByteOrder)
log.PanicIf(err)
expected := "11 22 33 44 55 66"
if value != expected {
t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
}
}
func TestIfdTagEntry_ValueString_Embedded(t *testing.T) {
data := []byte{0x11, 0x22, 0x33, 0x44}
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 4,
ValueOffset: 0,
RawValueOffset: data,
IfdPath: IfdPathStandard,
}
value, err := ite.ValueString(nil, TestDefaultByteOrder)
log.PanicIf(err)
expected := "11 22 33 44"
if value != expected {
t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
}
}
func TestIfdTagEntry_ValueString_Unknown(t *testing.T) {
data := []uint8{'0', '2', '3', '0'}
ite := IfdTagEntry{
TagId: 0x9000,
TagIndex: 0,
TagType: TypeUndefined,
UnitCount: 4,
ValueOffset: 0x0,
RawValueOffset: data,
IfdPath: IfdPathStandardExif,
}
value, err := ite.ValueString(nil, TestDefaultByteOrder)
log.PanicIf(err)
expected := "0230"
if value != expected {
t.Fatalf("Value not expected: [%s] != [%s]", value, expected)
}
}
func TestIfdTagEntry_ValueBytes_Allocated(t *testing.T) {
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 6,
ValueOffset: 0x0,
RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
IfdPath: IfdPathStandard,
}
data := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
value, err := ite.ValueBytes(data, TestDefaultByteOrder)
log.PanicIf(err)
if bytes.Compare(value, data) != 0 {
t.Fatalf("Value not expected: [%s] != [%s]", value, data)
}
}
func TestIfdTagEntry_ValueBytes_Embedded(t *testing.T) {
data := []byte{0x11, 0x22, 0x33, 0x44}
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 4,
ValueOffset: 0x0,
RawValueOffset: data,
IfdPath: IfdPathStandard,
}
value, err := ite.ValueBytes(nil, TestDefaultByteOrder)
log.PanicIf(err)
if bytes.Compare(value, data) != 0 {
t.Fatalf("Value not expected: [%s] != [%s]", value, data)
}
}
func TestIfdTagEntry_Value_Normal(t *testing.T) {
data := []byte{0x11, 0x22, 0x33, 0x44}
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 4,
ValueOffset: 0x0,
RawValueOffset: data,
IfdPath: IfdPathStandard,
}
value, err := ite.Value(nil, TestDefaultByteOrder)
log.PanicIf(err)
if bytes.Compare(value.([]byte), data) != 0 {
t.Fatalf("Value not expected: [%s] != [%s]", value, data)
}
}
func TestIfdTagEntry_Value_Unknown(t *testing.T) {
data := []uint8{'0', '2', '3', '0'}
ite := IfdTagEntry{
TagId: 0x9000,
TagIndex: 0,
TagType: TypeUndefined,
UnitCount: 4,
ValueOffset: 0x0,
RawValueOffset: data,
IfdPath: IfdPathStandardExif,
}
value, err := ite.Value(nil, TestDefaultByteOrder)
log.PanicIf(err)
gs := value.(TagUnknownType_GeneralString)
vb, err := gs.ValueBytes()
log.PanicIf(err)
if bytes.Compare(vb, data) != 0 {
t.Fatalf("Value not expected: [%s] != [%s]", value, data)
}
}
func TestIfdTagEntry_String(t *testing.T) {
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: 6,
ValueOffset: 0x0,
RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
IfdPath: IfdPathStandard,
}
expected := "IfdTagEntry<TAG-IFD-PATH=[IFD] TAG-ID=(0x0001) TAG-TYPE=[BYTE] UNIT-COUNT=(6)>"
if ite.String() != expected {
t.Fatalf("string representation not expected: [%s] != [%s]", ite.String(), expected)
}
}
func TestIfdTagEntryValueResolver_ValueBytes(t *testing.T) {
allocatedData := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
ite := IfdTagEntry{
TagId: 0x1,
TagIndex: 0,
TagType: TypeByte,
UnitCount: uint32(len(allocatedData)),
ValueOffset: 0x8,
RawValueOffset: []byte{0x0, 0x0, 0x0, 0x0},
IfdPath: IfdPathStandard,
}
headerBytes, err := BuildExifHeader(TestDefaultByteOrder, uint32(0))
log.PanicIf(err)
exifData := make([]byte, len(headerBytes)+len(allocatedData))
copy(exifData[0:], headerBytes)
copy(exifData[len(headerBytes):], allocatedData)
itevr := NewIfdTagEntryValueResolver(exifData, TestDefaultByteOrder)
value, err := itevr.ValueBytes(&ite)
log.PanicIf(err)
if bytes.Compare(value, allocatedData) != 0 {
t.Fatalf("bytes not expected: %v != %v", value, allocatedData)
}
}

264
v2/ifd_test.go Normal file
View File

@ -0,0 +1,264 @@
package exif
import (
"fmt"
"reflect"
"sort"
"testing"
"github.com/dsoprea/go-logging"
)
func TestIfdMapping_Add(t *testing.T) {
im := NewIfdMapping()
err := im.Add([]uint16{}, 0x1111, "ifd0")
log.PanicIf(err)
err = im.Add([]uint16{0x1111}, 0x4444, "ifd00")
log.PanicIf(err)
err = im.Add([]uint16{0x1111, 0x4444}, 0x5555, "ifd000")
log.PanicIf(err)
err = im.Add([]uint16{}, 0x2222, "ifd1")
log.PanicIf(err)
err = im.Add([]uint16{}, 0x3333, "ifd2")
log.PanicIf(err)
lineages, err := im.DumpLineages()
log.PanicIf(err)
sort.Strings(lineages)
expected := []string{
"ifd0",
"ifd0/ifd00",
"ifd0/ifd00/ifd000",
"ifd1",
"ifd2",
}
if reflect.DeepEqual(lineages, expected) != true {
fmt.Printf("Actual:\n")
fmt.Printf("\n")
for i, line := range lineages {
fmt.Printf("(%d) %s\n", i, line)
}
fmt.Printf("\n")
fmt.Printf("Expected:\n")
fmt.Printf("\n")
for i, line := range expected {
fmt.Printf("(%d) %s\n", i, line)
}
t.Fatalf("IFD-mapping dump not correct.")
}
}
func TestIfdMapping_LoadStandardIfds(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineages, err := im.DumpLineages()
log.PanicIf(err)
sort.Strings(lineages)
expected := []string{
"IFD",
"IFD/Exif",
"IFD/Exif/Iop",
"IFD/GPSInfo",
}
if reflect.DeepEqual(lineages, expected) != true {
fmt.Printf("Actual:\n")
fmt.Printf("\n")
for i, line := range lineages {
fmt.Printf("(%d) %s\n", i, line)
}
fmt.Printf("\n")
fmt.Printf("Expected:\n")
fmt.Printf("\n")
for i, line := range expected {
fmt.Printf("(%d) %s\n", i, line)
}
t.Fatalf("IFD-mapping dump not correct.")
}
}
func TestIfdMapping_Get(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
mi, err := im.Get([]uint16{IfdRootId, IfdExifId, IfdIopId})
log.PanicIf(err)
if mi.ParentTagId != IfdExifId {
t.Fatalf("Parent tag-ID not correct")
} else if mi.TagId != IfdIopId {
t.Fatalf("Tag-ID not correct")
} else if mi.Name != "Iop" {
t.Fatalf("name not correct")
} else if mi.PathPhrase() != "IFD/Exif/Iop" {
t.Fatalf("path not correct")
}
}
func TestIfdMapping_GetWithPath(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
mi, err := im.GetWithPath("IFD/Exif/Iop")
log.PanicIf(err)
if mi.ParentTagId != IfdExifId {
t.Fatalf("Parent tag-ID not correct")
} else if mi.TagId != IfdIopId {
t.Fatalf("Tag-ID not correct")
} else if mi.Name != "Iop" {
t.Fatalf("name not correct")
} else if mi.PathPhrase() != "IFD/Exif/Iop" {
t.Fatalf("path not correct")
}
}
func TestIfdMapping_ResolvePath__Regular(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineage, err := im.ResolvePath("IFD/Exif/Iop")
log.PanicIf(err)
expected := []IfdTagIdAndIndex{
IfdTagIdAndIndex{Name: "IFD", TagId: 0, Index: 0},
IfdTagIdAndIndex{Name: "Exif", TagId: 0x8769, Index: 0},
IfdTagIdAndIndex{Name: "Iop", TagId: 0xa005, Index: 0},
}
if reflect.DeepEqual(lineage, expected) != true {
t.Fatalf("Lineage not correct.")
}
}
func TestIfdMapping_ResolvePath__WithIndices(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
lineage, err := im.ResolvePath("IFD/Exif1/Iop")
log.PanicIf(err)
expected := []IfdTagIdAndIndex{
IfdTagIdAndIndex{Name: "IFD", TagId: 0, Index: 0},
IfdTagIdAndIndex{Name: "Exif", TagId: 0x8769, Index: 1},
IfdTagIdAndIndex{Name: "Iop", TagId: 0xa005, Index: 0},
}
if reflect.DeepEqual(lineage, expected) != true {
t.Fatalf("Lineage not correct.")
}
}
func TestIfdMapping_ResolvePath__Miss(t *testing.T) {
im := NewIfdMapping()
err := LoadStandardIfds(im)
log.PanicIf(err)
_, err = im.ResolvePath("IFD/Exif/Invalid")
if err == nil {
t.Fatalf("Expected failure for invalid IFD path.")
} else if err.Error() != "ifd child with name [Invalid] not registered: [IFD/Exif/Invalid]" {
log.Panic(err)
}
}
func TestIfdMapping_FqPathPhraseFromLineage(t *testing.T) {
lineage := []IfdTagIdAndIndex{
IfdTagIdAndIndex{Name: "IFD", Index: 0},
IfdTagIdAndIndex{Name: "Exif", Index: 1},
IfdTagIdAndIndex{Name: "Iop", Index: 0},
}
im := NewIfdMapping()
fqPathPhrase := im.FqPathPhraseFromLineage(lineage)
if fqPathPhrase != "IFD/Exif1/Iop" {
t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
}
}
func TestIfdMapping_PathPhraseFromLineage(t *testing.T) {
lineage := []IfdTagIdAndIndex{
IfdTagIdAndIndex{Name: "IFD", Index: 0},
IfdTagIdAndIndex{Name: "Exif", Index: 1},
IfdTagIdAndIndex{Name: "Iop", Index: 0},
}
im := NewIfdMapping()
fqPathPhrase := im.PathPhraseFromLineage(lineage)
if fqPathPhrase != "IFD/Exif/Iop" {
t.Fatalf("path-phrase not correct: [%s]", fqPathPhrase)
}
}
func TestIfdMapping_NewIfdMappingWithStandard(t *testing.T) {
imWith := NewIfdMappingWithStandard()
imWithout := NewIfdMapping()
err := LoadStandardIfds(imWithout)
outputWith, err := imWith.DumpLineages()
log.PanicIf(err)
sort.Strings(outputWith)
outputWithout, err := imWithout.DumpLineages()
log.PanicIf(err)
sort.Strings(outputWithout)
if reflect.DeepEqual(outputWith, outputWithout) != true {
fmt.Printf("WITH:\n")
fmt.Printf("\n")
for _, line := range outputWith {
fmt.Printf("%s\n", line)
}
fmt.Printf("\n")
fmt.Printf("WITHOUT:\n")
fmt.Printf("\n")
for _, line := range outputWithout {
fmt.Printf("%s\n", line)
}
fmt.Printf("\n")
t.Fatalf("Standard IFDs not loaded correctly.")
}
}

190
v2/parser.go Normal file
View File

@ -0,0 +1,190 @@
package exif
import (
"bytes"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type Parser struct {
}
func (p *Parser) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeByte.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = []uint8(data[:count])
return value, nil
}
// ParseAscii returns a string and auto-strips the trailing NUL character.
func (p *Parser) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
if len(data) == 0 || data[count-1] != 0 {
s := string(data[:count])
typeLogger.Warningf(nil, "ascii not terminated with nul as expected: [%v]", s)
return s, nil
} else {
// Auto-strip the NUL from the end. It serves no purpose outside of
// encoding semantics.
return string(data[:count-1]), nil
}
}
// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
// character.
func (p *Parser) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeAscii.Size() * count) {
log.Panic(ErrNotEnoughData)
}
return string(data[:count]), nil
}
func (p *Parser) ParseShorts(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeShort.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]uint16, count)
for i := 0; i < count; i++ {
value[i] = byteOrder.Uint16(data[i*2:])
}
return value, nil
}
func (p *Parser) ParseLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeLong.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]uint32, count)
for i := 0; i < count; i++ {
value[i] = byteOrder.Uint32(data[i*4:])
}
return value, nil
}
func (p *Parser) ParseRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeRational.Size() * count) {
log.Panic(ErrNotEnoughData)
}
value = make([]Rational, count)
for i := 0; i < count; i++ {
value[i].Numerator = byteOrder.Uint32(data[i*8:])
value[i].Denominator = byteOrder.Uint32(data[i*8+4:])
}
return value, nil
}
func (p *Parser) ParseSignedLongs(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeSignedLong.Size() * count) {
log.Panic(ErrNotEnoughData)
}
b := bytes.NewBuffer(data)
value = make([]int32, count)
for i := 0; i < count; i++ {
err := binary.Read(b, byteOrder, &value[i])
log.PanicIf(err)
}
return value, nil
}
func (p *Parser) ParseSignedRationals(data []byte, unitCount uint32, byteOrder binary.ByteOrder) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
count := int(unitCount)
if len(data) < (TypeSignedRational.Size() * count) {
log.Panic(ErrNotEnoughData)
}
b := bytes.NewBuffer(data)
value = make([]SignedRational, count)
for i := 0; i < count; i++ {
err = binary.Read(b, byteOrder, &value[i].Numerator)
log.PanicIf(err)
err = binary.Read(b, byteOrder, &value[i].Denominator)
log.PanicIf(err)
}
return value, nil
}

8
v2/readme.go Normal file
View File

@ -0,0 +1,8 @@
// exif parses raw EXIF information given a block of raw EXIF data. It can also
// construct new EXIF information, and provides tools for doing so. This package
// is not involved with the parsing of particular file-formats.
//
// The EXIF data must first be extracted and then provided to us. Conversely,
// when constructing new EXIF data, the caller is responsible for packaging
// this in whichever format they require.
package exif

397
v2/tag_type.go Normal file
View File

@ -0,0 +1,397 @@
package exif
// NOTE(dustin): Most of this file encapsulates deprecated functionality and awaits being dumped in a future release.
import (
"fmt"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type TagType struct {
tagType TagTypePrimitive
name string
byteOrder binary.ByteOrder
}
func NewTagType(tagType TagTypePrimitive, byteOrder binary.ByteOrder) TagType {
name, found := TypeNames[tagType]
if found == false {
log.Panicf("tag-type not valid: 0x%04x", tagType)
}
return TagType{
tagType: tagType,
name: name,
byteOrder: byteOrder,
}
}
func (tt TagType) String() string {
return fmt.Sprintf("TagType<NAME=[%s]>", tt.name)
}
func (tt TagType) Name() string {
return tt.name
}
func (tt TagType) Type() TagTypePrimitive {
return tt.tagType
}
func (tt TagType) ByteOrder() binary.ByteOrder {
return tt.byteOrder
}
func (tt TagType) Size() int {
// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
return tt.Type().Size()
}
// valueIsEmbedded will return a boolean indicating whether the value should be
// found directly within the IFD entry or an offset to somewhere else.
func (tt TagType) valueIsEmbedded(unitCount uint32) bool {
return (tt.tagType.Size() * int(unitCount)) <= 4
}
func (tt TagType) readRawEncoded(valueContext ValueContext) (rawBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
unitSizeRaw := uint32(tt.tagType.Size())
if tt.valueIsEmbedded(valueContext.UnitCount()) == true {
byteLength := unitSizeRaw * valueContext.UnitCount()
return valueContext.RawValueOffset()[:byteLength], nil
} else {
return valueContext.AddressableData()[valueContext.ValueOffset() : valueContext.ValueOffset()+valueContext.UnitCount()*unitSizeRaw], nil
}
}
func (tt TagType) ParseBytes(data []byte, unitCount uint32) (value []uint8, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseBytes()` should be used.
value, err = parser.ParseBytes(data, unitCount)
log.PanicIf(err)
return value, nil
}
// ParseAscii returns a string and auto-strips the trailing NUL character.
func (tt TagType) ParseAscii(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseAscii()` should be used.
value, err = parser.ParseAscii(data, unitCount)
log.PanicIf(err)
return value, nil
}
// ParseAsciiNoNul returns a string without any consideration for a trailing NUL
// character.
func (tt TagType) ParseAsciiNoNul(data []byte, unitCount uint32) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseAsciiNoNul()` should be used.
value, err = parser.ParseAsciiNoNul(data, unitCount)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseShorts(data []byte, unitCount uint32) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseShorts()` should be used.
value, err = parser.ParseShorts(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseLongs(data []byte, unitCount uint32) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseLongs()` should be used.
value, err = parser.ParseLongs(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseRationals(data []byte, unitCount uint32) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseRationals()` should be used.
value, err = parser.ParseRationals(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseSignedLongs(data []byte, unitCount uint32) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseSignedLongs()` should be used.
value, err = parser.ParseSignedLongs(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ParseSignedRationals(data []byte, unitCount uint32) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(*Parser).ParseSignedRationals()` should be used.
value, err = parser.ParseSignedRationals(data, unitCount, tt.byteOrder)
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadByteValues(valueContext ValueContext) (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadBytes()` should be used.
value, err = valueContext.ReadBytes()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadAsciiValue(valueContext ValueContext) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadAscii()` should be used.
value, err = valueContext.ReadAscii()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadAsciiNoNulValue(valueContext ValueContext) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadAsciiNoNul()` should be used.
value, err = valueContext.ReadAsciiNoNul()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadShortValues(valueContext ValueContext) (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadShorts()` should be used.
value, err = valueContext.ReadShorts()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadLongValues(valueContext ValueContext) (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadLongs()` should be used.
value, err = valueContext.ReadLongs()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadRationalValues(valueContext ValueContext) (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadRationals()` should be used.
value, err = valueContext.ReadRationals()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadSignedLongValues(valueContext ValueContext) (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadSignedLongs()` should be used.
value, err = valueContext.ReadSignedLongs()
log.PanicIf(err)
return value, nil
}
func (tt TagType) ReadSignedRationalValues(valueContext ValueContext) (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).ReadSignedRationals()` should be used.
value, err = valueContext.ReadSignedRationals()
log.PanicIf(err)
return value, nil
}
// ResolveAsString resolves the given value and returns a flat string.
//
// Where the type is not ASCII, `justFirst` indicates whether to just stringify
// the first item in the slice (or return an empty string if the slice is
// empty).
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (tt TagType) ResolveAsString(valueContext ValueContext, justFirst bool) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if justFirst == true {
value, err = valueContext.FormatFirst()
log.PanicIf(err)
} else {
value, err = valueContext.Format()
log.PanicIf(err)
}
return value, nil
}
// Resolve knows how to resolve the given value.
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (tt TagType) Resolve(valueContext *ValueContext) (values interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `(ValueContext).Values()` should be used.
values, err = valueContext.Values()
log.PanicIf(err)
return values, nil
}
// Encode knows how to encode the given value to a byte slice.
func (tt TagType) Encode(value interface{}) (encoded []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ve := NewValueEncoder(tt.byteOrder)
ed, err := ve.EncodeWithType(tt, value)
log.PanicIf(err)
return ed.Encoded, err
}
func (tt TagType) FromString(valueString string) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// DEPRECATED(dustin): `EncodeStringToBytes()` should be used.
value, err = EncodeStringToBytes(tt.tagType, valueString)
log.PanicIf(err)
return value, nil
}

229
v2/tags.go Normal file
View File

@ -0,0 +1,229 @@
package exif
import (
"fmt"
"github.com/dsoprea/go-logging"
"gopkg.in/yaml.v2"
)
const (
// IFD1
ThumbnailOffsetTagId = 0x0201
ThumbnailSizeTagId = 0x0202
// Exif
TagVersionId = 0x0000
TagLatitudeId = 0x0002
TagLatitudeRefId = 0x0001
TagLongitudeId = 0x0004
TagLongitudeRefId = 0x0003
TagTimestampId = 0x0007
TagDatestampId = 0x001d
TagAltitudeId = 0x0006
TagAltitudeRefId = 0x0005
)
var (
// tagsWithoutAlignment is a tag-lookup for tags whose value size won't
// necessarily be a multiple of its tag-type.
tagsWithoutAlignment = map[uint16]struct{}{
// The thumbnail offset is stored as a long, but its data is a binary
// blob (not a slice of longs).
ThumbnailOffsetTagId: struct{}{},
}
)
var (
tagsLogger = log.NewLogger("exif.tags")
)
// File structures.
type encodedTag struct {
// id is signed, here, because YAML doesn't have enough information to
// support unsigned.
Id int `yaml:"id"`
Name string `yaml:"name"`
TypeName string `yaml:"type_name"`
}
// Indexing structures.
type IndexedTag struct {
Id uint16
Name string
IfdPath string
Type TagTypePrimitive
}
func (it *IndexedTag) String() string {
return fmt.Sprintf("TAG<ID=(0x%04x) NAME=[%s] IFD=[%s]>", it.Id, it.Name, it.IfdPath)
}
func (it *IndexedTag) IsName(ifdPath, name string) bool {
return it.Name == name && it.IfdPath == ifdPath
}
func (it *IndexedTag) Is(ifdPath string, id uint16) bool {
return it.Id == id && it.IfdPath == ifdPath
}
type TagIndex struct {
tagsByIfd map[string]map[uint16]*IndexedTag
tagsByIfdR map[string]map[string]*IndexedTag
}
func NewTagIndex() *TagIndex {
ti := new(TagIndex)
ti.tagsByIfd = make(map[string]map[uint16]*IndexedTag)
ti.tagsByIfdR = make(map[string]map[string]*IndexedTag)
return ti
}
func (ti *TagIndex) Add(it *IndexedTag) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Store by ID.
family, found := ti.tagsByIfd[it.IfdPath]
if found == false {
family = make(map[uint16]*IndexedTag)
ti.tagsByIfd[it.IfdPath] = family
}
if _, found := family[it.Id]; found == true {
log.Panicf("tag-ID defined more than once for IFD [%s]: (%02x)", it.IfdPath, it.Id)
}
family[it.Id] = it
// Store by name.
familyR, found := ti.tagsByIfdR[it.IfdPath]
if found == false {
familyR = make(map[string]*IndexedTag)
ti.tagsByIfdR[it.IfdPath] = familyR
}
if _, found := familyR[it.Name]; found == true {
log.Panicf("tag-name defined more than once for IFD [%s]: (%s)", it.IfdPath, it.Name)
}
familyR[it.Name] = it
return nil
}
// Get returns information about the non-IFD tag.
func (ti *TagIndex) Get(ifdPath string, id uint16) (it *IndexedTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if len(ti.tagsByIfd) == 0 {
err := LoadStandardTags(ti)
log.PanicIf(err)
}
family, found := ti.tagsByIfd[ifdPath]
if found == false {
log.Panic(ErrTagNotFound)
}
it, found = family[id]
if found == false {
log.Panic(ErrTagNotFound)
}
return it, nil
}
// Get returns information about the non-IFD tag.
func (ti *TagIndex) GetWithName(ifdPath string, name string) (it *IndexedTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if len(ti.tagsByIfdR) == 0 {
err := LoadStandardTags(ti)
log.PanicIf(err)
}
it, found := ti.tagsByIfdR[ifdPath][name]
if found != true {
log.Panic(ErrTagNotFound)
}
return it, nil
}
// LoadStandardTags registers the tags that all devices/applications should
// support.
func LoadStandardTags(ti *TagIndex) (err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// Read static data.
encodedIfds := make(map[string][]encodedTag)
err = yaml.Unmarshal([]byte(tagsYaml), encodedIfds)
log.PanicIf(err)
// Load structure.
count := 0
for ifdPath, tags := range encodedIfds {
for _, tagInfo := range tags {
tagId := uint16(tagInfo.Id)
tagName := tagInfo.Name
tagTypeName := tagInfo.TypeName
// TODO(dustin): !! Non-standard types, but found in real data. Ignore for right now.
if tagTypeName == "SSHORT" || tagTypeName == "FLOAT" || tagTypeName == "DOUBLE" {
continue
}
tagTypeId, found := TypeNamesR[tagTypeName]
if found == false {
log.Panicf("type [%s] for [%s] not valid", tagTypeName, tagName)
continue
}
it := &IndexedTag{
IfdPath: ifdPath,
Id: tagId,
Name: tagName,
Type: tagTypeId,
}
err = ti.Add(it)
log.PanicIf(err)
count++
}
}
tagsLogger.Debugf(nil, "(%d) tags loaded.", count)
return nil
}

951
v2/tags_data.go Normal file
View File

@ -0,0 +1,951 @@
package exif
var (
// From assets/tags.yaml . Needs to be here so it's embedded in the binary.
tagsYaml = `
# Notes:
#
# This file was produced from http://www.exiv2.org/tags.html, using the included
# tool, though that document appears to have some duplicates when all IDs are
# supposed to be unique (EXIF information only has IDs, not IFDs; IFDs are
# determined by our pre-existing knowledge of those tags).
#
# The webpage that we've produced this file from appears to indicate that
# ImageWidth is represented by both 0x0100 and 0x0001 depending on whether the
# encoding is RGB or YCbCr.
IFD/Exif:
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8830
name: SensitivityType
type_name: SHORT
- id: 0x8831
name: StandardOutputSensitivity
type_name: LONG
- id: 0x8832
name: RecommendedExposureIndex
type_name: LONG
- id: 0x8833
name: ISOSpeed
type_name: LONG
- id: 0x8834
name: ISOSpeedLatitudeyyy
type_name: LONG
- id: 0x8835
name: ISOSpeedLatitudezzz
type_name: LONG
- id: 0x9000
name: ExifVersion
type_name: UNDEFINED
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9004
name: DateTimeDigitized
type_name: ASCII
- id: 0x9101
name: ComponentsConfiguration
type_name: UNDEFINED
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: RATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x9214
name: SubjectArea
type_name: SHORT
- id: 0x927c
name: MakerNote
type_name: UNDEFINED
- id: 0x9286
name: UserComment
type_name: UNDEFINED
- id: 0x9290
name: SubSecTime
type_name: ASCII
- id: 0x9291
name: SubSecTimeOriginal
type_name: ASCII
- id: 0x9292
name: SubSecTimeDigitized
type_name: ASCII
- id: 0xa000
name: FlashpixVersion
type_name: UNDEFINED
- id: 0xa001
name: ColorSpace
type_name: SHORT
- id: 0xa002
name: PixelXDimension
type_name: LONG
- id: 0xa003
name: PixelYDimension
type_name: LONG
- id: 0xa004
name: RelatedSoundFile
type_name: ASCII
- id: 0xa005
name: InteroperabilityTag
type_name: LONG
- id: 0xa20b
name: FlashEnergy
type_name: RATIONAL
- id: 0xa20c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0xa20e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0xa20f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0xa210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0xa214
name: SubjectLocation
type_name: SHORT
- id: 0xa215
name: ExposureIndex
type_name: RATIONAL
- id: 0xa217
name: SensingMethod
type_name: SHORT
- id: 0xa300
name: FileSource
type_name: UNDEFINED
- id: 0xa301
name: SceneType
type_name: UNDEFINED
- id: 0xa302
name: CFAPattern
type_name: UNDEFINED
- id: 0xa401
name: CustomRendered
type_name: SHORT
- id: 0xa402
name: ExposureMode
type_name: SHORT
- id: 0xa403
name: WhiteBalance
type_name: SHORT
- id: 0xa404
name: DigitalZoomRatio
type_name: RATIONAL
- id: 0xa405
name: FocalLengthIn35mmFilm
type_name: SHORT
- id: 0xa406
name: SceneCaptureType
type_name: SHORT
- id: 0xa407
name: GainControl
type_name: SHORT
- id: 0xa408
name: Contrast
type_name: SHORT
- id: 0xa409
name: Saturation
type_name: SHORT
- id: 0xa40a
name: Sharpness
type_name: SHORT
- id: 0xa40b
name: DeviceSettingDescription
type_name: UNDEFINED
- id: 0xa40c
name: SubjectDistanceRange
type_name: SHORT
- id: 0xa420
name: ImageUniqueID
type_name: ASCII
- id: 0xa430
name: CameraOwnerName
type_name: ASCII
- id: 0xa431
name: BodySerialNumber
type_name: ASCII
- id: 0xa432
name: LensSpecification
type_name: RATIONAL
- id: 0xa433
name: LensMake
type_name: ASCII
- id: 0xa434
name: LensModel
type_name: ASCII
- id: 0xa435
name: LensSerialNumber
type_name: ASCII
IFD/GPSInfo:
- id: 0x0000
name: GPSVersionID
type_name: BYTE
- id: 0x0001
name: GPSLatitudeRef
type_name: ASCII
- id: 0x0002
name: GPSLatitude
type_name: RATIONAL
- id: 0x0003
name: GPSLongitudeRef
type_name: ASCII
- id: 0x0004
name: GPSLongitude
type_name: RATIONAL
- id: 0x0005
name: GPSAltitudeRef
type_name: BYTE
- id: 0x0006
name: GPSAltitude
type_name: RATIONAL
- id: 0x0007
name: GPSTimeStamp
type_name: RATIONAL
- id: 0x0008
name: GPSSatellites
type_name: ASCII
- id: 0x0009
name: GPSStatus
type_name: ASCII
- id: 0x000a
name: GPSMeasureMode
type_name: ASCII
- id: 0x000b
name: GPSDOP
type_name: RATIONAL
- id: 0x000c
name: GPSSpeedRef
type_name: ASCII
- id: 0x000d
name: GPSSpeed
type_name: RATIONAL
- id: 0x000e
name: GPSTrackRef
type_name: ASCII
- id: 0x000f
name: GPSTrack
type_name: RATIONAL
- id: 0x0010
name: GPSImgDirectionRef
type_name: ASCII
- id: 0x0011
name: GPSImgDirection
type_name: RATIONAL
- id: 0x0012
name: GPSMapDatum
type_name: ASCII
- id: 0x0013
name: GPSDestLatitudeRef
type_name: ASCII
- id: 0x0014
name: GPSDestLatitude
type_name: RATIONAL
- id: 0x0015
name: GPSDestLongitudeRef
type_name: ASCII
- id: 0x0016
name: GPSDestLongitude
type_name: RATIONAL
- id: 0x0017
name: GPSDestBearingRef
type_name: ASCII
- id: 0x0018
name: GPSDestBearing
type_name: RATIONAL
- id: 0x0019
name: GPSDestDistanceRef
type_name: ASCII
- id: 0x001a
name: GPSDestDistance
type_name: RATIONAL
- id: 0x001b
name: GPSProcessingMethod
type_name: UNDEFINED
- id: 0x001c
name: GPSAreaInformation
type_name: UNDEFINED
- id: 0x001d
name: GPSDateStamp
type_name: ASCII
- id: 0x001e
name: GPSDifferential
type_name: SHORT
IFD:
- id: 0x000b
name: ProcessingSoftware
type_name: ASCII
- id: 0x00fe
name: NewSubfileType
type_name: LONG
- id: 0x00ff
name: SubfileType
type_name: SHORT
- id: 0x0100
name: ImageWidth
type_name: LONG
- id: 0x0101
name: ImageLength
type_name: LONG
- id: 0x0102
name: BitsPerSample
type_name: SHORT
- id: 0x0103
name: Compression
type_name: SHORT
- id: 0x0106
name: PhotometricInterpretation
type_name: SHORT
- id: 0x0107
name: Thresholding
type_name: SHORT
- id: 0x0108
name: CellWidth
type_name: SHORT
- id: 0x0109
name: CellLength
type_name: SHORT
- id: 0x010a
name: FillOrder
type_name: SHORT
- id: 0x010d
name: DocumentName
type_name: ASCII
- id: 0x010e
name: ImageDescription
type_name: ASCII
- id: 0x010f
name: Make
type_name: ASCII
- id: 0x0110
name: Model
type_name: ASCII
- id: 0x0111
name: StripOffsets
type_name: LONG
- id: 0x0112
name: Orientation
type_name: SHORT
- id: 0x0115
name: SamplesPerPixel
type_name: SHORT
- id: 0x0116
name: RowsPerStrip
type_name: LONG
- id: 0x0117
name: StripByteCounts
type_name: LONG
- id: 0x011a
name: XResolution
type_name: RATIONAL
- id: 0x011b
name: YResolution
type_name: RATIONAL
- id: 0x011c
name: PlanarConfiguration
type_name: SHORT
- id: 0x0122
name: GrayResponseUnit
type_name: SHORT
- id: 0x0123
name: GrayResponseCurve
type_name: SHORT
- id: 0x0124
name: T4Options
type_name: LONG
- id: 0x0125
name: T6Options
type_name: LONG
- id: 0x0128
name: ResolutionUnit
type_name: SHORT
- id: 0x0129
name: PageNumber
type_name: SHORT
- id: 0x012d
name: TransferFunction
type_name: SHORT
- id: 0x0131
name: Software
type_name: ASCII
- id: 0x0132
name: DateTime
type_name: ASCII
- id: 0x013b
name: Artist
type_name: ASCII
- id: 0x013c
name: HostComputer
type_name: ASCII
- id: 0x013d
name: Predictor
type_name: SHORT
- id: 0x013e
name: WhitePoint
type_name: RATIONAL
- id: 0x013f
name: PrimaryChromaticities
type_name: RATIONAL
- id: 0x0140
name: ColorMap
type_name: SHORT
- id: 0x0141
name: HalftoneHints
type_name: SHORT
- id: 0x0142
name: TileWidth
type_name: SHORT
- id: 0x0143
name: TileLength
type_name: SHORT
- id: 0x0144
name: TileOffsets
type_name: SHORT
- id: 0x0145
name: TileByteCounts
type_name: SHORT
- id: 0x014a
name: SubIFDs
type_name: LONG
- id: 0x014c
name: InkSet
type_name: SHORT
- id: 0x014d
name: InkNames
type_name: ASCII
- id: 0x014e
name: NumberOfInks
type_name: SHORT
- id: 0x0150
name: DotRange
type_name: BYTE
- id: 0x0151
name: TargetPrinter
type_name: ASCII
- id: 0x0152
name: ExtraSamples
type_name: SHORT
- id: 0x0153
name: SampleFormat
type_name: SHORT
- id: 0x0154
name: SMinSampleValue
type_name: SHORT
- id: 0x0155
name: SMaxSampleValue
type_name: SHORT
- id: 0x0156
name: TransferRange
type_name: SHORT
- id: 0x0157
name: ClipPath
type_name: BYTE
- id: 0x0158
name: XClipPathUnits
type_name: SSHORT
- id: 0x0159
name: YClipPathUnits
type_name: SSHORT
- id: 0x015a
name: Indexed
type_name: SHORT
- id: 0x015b
name: JPEGTables
type_name: UNDEFINED
- id: 0x015f
name: OPIProxy
type_name: SHORT
- id: 0x0200
name: JPEGProc
type_name: LONG
- id: 0x0201
name: JPEGInterchangeFormat
type_name: LONG
- id: 0x0202
name: JPEGInterchangeFormatLength
type_name: LONG
- id: 0x0203
name: JPEGRestartInterval
type_name: SHORT
- id: 0x0205
name: JPEGLosslessPredictors
type_name: SHORT
- id: 0x0206
name: JPEGPointTransforms
type_name: SHORT
- id: 0x0207
name: JPEGQTables
type_name: LONG
- id: 0x0208
name: JPEGDCTables
type_name: LONG
- id: 0x0209
name: JPEGACTables
type_name: LONG
- id: 0x0211
name: YCbCrCoefficients
type_name: RATIONAL
- id: 0x0212
name: YCbCrSubSampling
type_name: SHORT
- id: 0x0213
name: YCbCrPositioning
type_name: SHORT
- id: 0x0214
name: ReferenceBlackWhite
type_name: RATIONAL
- id: 0x02bc
name: XMLPacket
type_name: BYTE
- id: 0x4746
name: Rating
type_name: SHORT
- id: 0x4749
name: RatingPercent
type_name: SHORT
- id: 0x800d
name: ImageID
type_name: ASCII
- id: 0x828d
name: CFARepeatPatternDim
type_name: SHORT
- id: 0x828e
name: CFAPattern
type_name: BYTE
- id: 0x828f
name: BatteryLevel
type_name: RATIONAL
- id: 0x8298
name: Copyright
type_name: ASCII
- id: 0x829a
name: ExposureTime
type_name: RATIONAL
- id: 0x829d
name: FNumber
type_name: RATIONAL
- id: 0x83bb
name: IPTCNAA
type_name: LONG
- id: 0x8649
name: ImageResources
type_name: BYTE
- id: 0x8769
name: ExifTag
type_name: LONG
- id: 0x8773
name: InterColorProfile
type_name: UNDEFINED
- id: 0x8822
name: ExposureProgram
type_name: SHORT
- id: 0x8824
name: SpectralSensitivity
type_name: ASCII
- id: 0x8825
name: GPSTag
type_name: LONG
- id: 0x8827
name: ISOSpeedRatings
type_name: SHORT
- id: 0x8828
name: OECF
type_name: UNDEFINED
- id: 0x8829
name: Interlace
type_name: SHORT
- id: 0x882a
name: TimeZoneOffset
type_name: SSHORT
- id: 0x882b
name: SelfTimerMode
type_name: SHORT
- id: 0x9003
name: DateTimeOriginal
type_name: ASCII
- id: 0x9102
name: CompressedBitsPerPixel
type_name: RATIONAL
- id: 0x9201
name: ShutterSpeedValue
type_name: SRATIONAL
- id: 0x9202
name: ApertureValue
type_name: RATIONAL
- id: 0x9203
name: BrightnessValue
type_name: SRATIONAL
- id: 0x9204
name: ExposureBiasValue
type_name: SRATIONAL
- id: 0x9205
name: MaxApertureValue
type_name: RATIONAL
- id: 0x9206
name: SubjectDistance
type_name: SRATIONAL
- id: 0x9207
name: MeteringMode
type_name: SHORT
- id: 0x9208
name: LightSource
type_name: SHORT
- id: 0x9209
name: Flash
type_name: SHORT
- id: 0x920a
name: FocalLength
type_name: RATIONAL
- id: 0x920b
name: FlashEnergy
type_name: RATIONAL
- id: 0x920c
name: SpatialFrequencyResponse
type_name: UNDEFINED
- id: 0x920d
name: Noise
type_name: UNDEFINED
- id: 0x920e
name: FocalPlaneXResolution
type_name: RATIONAL
- id: 0x920f
name: FocalPlaneYResolution
type_name: RATIONAL
- id: 0x9210
name: FocalPlaneResolutionUnit
type_name: SHORT
- id: 0x9211
name: ImageNumber
type_name: LONG
- id: 0x9212
name: SecurityClassification
type_name: ASCII
- id: 0x9213
name: ImageHistory
type_name: ASCII
- id: 0x9214
name: SubjectLocation
type_name: SHORT
- id: 0x9215
name: ExposureIndex
type_name: RATIONAL
- id: 0x9216
name: TIFFEPStandardID
type_name: BYTE
- id: 0x9217
name: SensingMethod
type_name: SHORT
- id: 0x9c9b
name: XPTitle
type_name: BYTE
- id: 0x9c9c
name: XPComment
type_name: BYTE
- id: 0x9c9d
name: XPAuthor
type_name: BYTE
- id: 0x9c9e
name: XPKeywords
type_name: BYTE
- id: 0x9c9f
name: XPSubject
type_name: BYTE
- id: 0xc4a5
name: PrintImageMatching
type_name: UNDEFINED
- id: 0xc612
name: DNGVersion
type_name: BYTE
- id: 0xc613
name: DNGBackwardVersion
type_name: BYTE
- id: 0xc614
name: UniqueCameraModel
type_name: ASCII
- id: 0xc615
name: LocalizedCameraModel
type_name: BYTE
- id: 0xc616
name: CFAPlaneColor
type_name: BYTE
- id: 0xc617
name: CFALayout
type_name: SHORT
- id: 0xc618
name: LinearizationTable
type_name: SHORT
- id: 0xc619
name: BlackLevelRepeatDim
type_name: SHORT
- id: 0xc61a
name: BlackLevel
type_name: RATIONAL
- id: 0xc61b
name: BlackLevelDeltaH
type_name: SRATIONAL
- id: 0xc61c
name: BlackLevelDeltaV
type_name: SRATIONAL
- id: 0xc61d
name: WhiteLevel
type_name: SHORT
- id: 0xc61e
name: DefaultScale
type_name: RATIONAL
- id: 0xc61f
name: DefaultCropOrigin
type_name: SHORT
- id: 0xc620
name: DefaultCropSize
type_name: SHORT
- id: 0xc621
name: ColorMatrix1
type_name: SRATIONAL
- id: 0xc622
name: ColorMatrix2
type_name: SRATIONAL
- id: 0xc623
name: CameraCalibration1
type_name: SRATIONAL
- id: 0xc624
name: CameraCalibration2
type_name: SRATIONAL
- id: 0xc625
name: ReductionMatrix1
type_name: SRATIONAL
- id: 0xc626
name: ReductionMatrix2
type_name: SRATIONAL
- id: 0xc627
name: AnalogBalance
type_name: RATIONAL
- id: 0xc628
name: AsShotNeutral
type_name: SHORT
- id: 0xc629
name: AsShotWhiteXY
type_name: RATIONAL
- id: 0xc62a
name: BaselineExposure
type_name: SRATIONAL
- id: 0xc62b
name: BaselineNoise
type_name: RATIONAL
- id: 0xc62c
name: BaselineSharpness
type_name: RATIONAL
- id: 0xc62d
name: BayerGreenSplit
type_name: LONG
- id: 0xc62e
name: LinearResponseLimit
type_name: RATIONAL
- id: 0xc62f
name: CameraSerialNumber
type_name: ASCII
- id: 0xc630
name: LensInfo
type_name: RATIONAL
- id: 0xc631
name: ChromaBlurRadius
type_name: RATIONAL
- id: 0xc632
name: AntiAliasStrength
type_name: RATIONAL
- id: 0xc633
name: ShadowScale
type_name: SRATIONAL
- id: 0xc634
name: DNGPrivateData
type_name: BYTE
- id: 0xc635
name: MakerNoteSafety
type_name: SHORT
- id: 0xc65a
name: CalibrationIlluminant1
type_name: SHORT
- id: 0xc65b
name: CalibrationIlluminant2
type_name: SHORT
- id: 0xc65c
name: BestQualityScale
type_name: RATIONAL
- id: 0xc65d
name: RawDataUniqueID
type_name: BYTE
- id: 0xc68b
name: OriginalRawFileName
type_name: BYTE
- id: 0xc68c
name: OriginalRawFileData
type_name: UNDEFINED
- id: 0xc68d
name: ActiveArea
type_name: SHORT
- id: 0xc68e
name: MaskedAreas
type_name: SHORT
- id: 0xc68f
name: AsShotICCProfile
type_name: UNDEFINED
- id: 0xc690
name: AsShotPreProfileMatrix
type_name: SRATIONAL
- id: 0xc691
name: CurrentICCProfile
type_name: UNDEFINED
- id: 0xc692
name: CurrentPreProfileMatrix
type_name: SRATIONAL
- id: 0xc6bf
name: ColorimetricReference
type_name: SHORT
- id: 0xc6f3
name: CameraCalibrationSignature
type_name: BYTE
- id: 0xc6f4
name: ProfileCalibrationSignature
type_name: BYTE
- id: 0xc6f6
name: AsShotProfileName
type_name: BYTE
- id: 0xc6f7
name: NoiseReductionApplied
type_name: RATIONAL
- id: 0xc6f8
name: ProfileName
type_name: BYTE
- id: 0xc6f9
name: ProfileHueSatMapDims
type_name: LONG
- id: 0xc6fa
name: ProfileHueSatMapData1
type_name: FLOAT
- id: 0xc6fb
name: ProfileHueSatMapData2
type_name: FLOAT
- id: 0xc6fc
name: ProfileToneCurve
type_name: FLOAT
- id: 0xc6fd
name: ProfileEmbedPolicy
type_name: LONG
- id: 0xc6fe
name: ProfileCopyright
type_name: BYTE
- id: 0xc714
name: ForwardMatrix1
type_name: SRATIONAL
- id: 0xc715
name: ForwardMatrix2
type_name: SRATIONAL
- id: 0xc716
name: PreviewApplicationName
type_name: BYTE
- id: 0xc717
name: PreviewApplicationVersion
type_name: BYTE
- id: 0xc718
name: PreviewSettingsName
type_name: BYTE
- id: 0xc719
name: PreviewSettingsDigest
type_name: BYTE
- id: 0xc71a
name: PreviewColorSpace
type_name: LONG
- id: 0xc71b
name: PreviewDateTime
type_name: ASCII
- id: 0xc71c
name: RawImageDigest
type_name: UNDEFINED
- id: 0xc71d
name: OriginalRawFileDigest
type_name: UNDEFINED
- id: 0xc71e
name: SubTileBlockSize
type_name: LONG
- id: 0xc71f
name: RowInterleaveFactor
type_name: LONG
- id: 0xc725
name: ProfileLookTableDims
type_name: LONG
- id: 0xc726
name: ProfileLookTableData
type_name: FLOAT
- id: 0xc740
name: OpcodeList1
type_name: UNDEFINED
- id: 0xc741
name: OpcodeList2
type_name: UNDEFINED
- id: 0xc74e
name: OpcodeList3
type_name: UNDEFINED
- id: 0xc761
name: NoiseProfile
type_name: DOUBLE
IFD/Exif/Iop:
- id: 0x0001
name: InteroperabilityIndex
type_name: ASCII
- id: 0x0002
name: InteroperabilityVersion
type_name: UNDEFINED
- id: 0x1000
name: RelatedImageFileFormat
type_name: ASCII
- id: 0x1001
name: RelatedImageWidth
type_name: LONG
- id: 0x1002
name: RelatedImageLength
type_name: LONG
`
)

29
v2/tags_test.go Normal file
View File

@ -0,0 +1,29 @@
package exif
import (
"testing"
"github.com/dsoprea/go-logging"
)
func TestGet(t *testing.T) {
ti := NewTagIndex()
it, err := ti.Get(IfdPathStandard, 0x10f)
log.PanicIf(err)
if it.Is(IfdPathStandard, 0x10f) == false || it.IsName(IfdPathStandard, "Make") == false {
t.Fatalf("tag info not correct")
}
}
func TestGetWithName(t *testing.T) {
ti := NewTagIndex()
it, err := ti.GetWithName(IfdPathStandard, "Make")
log.PanicIf(err)
if it.Is(IfdPathStandard, 0x10f) == false || it.Is(IfdPathStandard, 0x10f) == false {
t.Fatalf("tag info not correct")
}
}

417
v2/tags_undefined.go Normal file
View File

@ -0,0 +1,417 @@
package exif
import (
"bytes"
"fmt"
"strings"
"crypto/sha1"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
const (
UnparseableUnknownTagValuePlaceholder = "!UNKNOWN"
)
// TODO(dustin): Rename "unknown" in symbol names to "undefined" in the next release.
//
// See https://github.com/dsoprea/go-exif/issues/27 .
const (
TagUnknownType_9298_UserComment_Encoding_ASCII = iota
TagUnknownType_9298_UserComment_Encoding_JIS = iota
TagUnknownType_9298_UserComment_Encoding_UNICODE = iota
TagUnknownType_9298_UserComment_Encoding_UNDEFINED = iota
)
const (
TagUnknownType_9101_ComponentsConfiguration_Channel_Y = 0x1
TagUnknownType_9101_ComponentsConfiguration_Channel_Cb = 0x2
TagUnknownType_9101_ComponentsConfiguration_Channel_Cr = 0x3
TagUnknownType_9101_ComponentsConfiguration_Channel_R = 0x4
TagUnknownType_9101_ComponentsConfiguration_Channel_G = 0x5
TagUnknownType_9101_ComponentsConfiguration_Channel_B = 0x6
)
const (
TagUnknownType_9101_ComponentsConfiguration_OTHER = iota
TagUnknownType_9101_ComponentsConfiguration_RGB = iota
TagUnknownType_9101_ComponentsConfiguration_YCBCR = iota
)
var (
TagUnknownType_9298_UserComment_Encoding_Names = map[int]string{
TagUnknownType_9298_UserComment_Encoding_ASCII: "ASCII",
TagUnknownType_9298_UserComment_Encoding_JIS: "JIS",
TagUnknownType_9298_UserComment_Encoding_UNICODE: "UNICODE",
TagUnknownType_9298_UserComment_Encoding_UNDEFINED: "UNDEFINED",
}
TagUnknownType_9298_UserComment_Encodings = map[int][]byte{
TagUnknownType_9298_UserComment_Encoding_ASCII: []byte{'A', 'S', 'C', 'I', 'I', 0, 0, 0},
TagUnknownType_9298_UserComment_Encoding_JIS: []byte{'J', 'I', 'S', 0, 0, 0, 0, 0},
TagUnknownType_9298_UserComment_Encoding_UNICODE: []byte{'U', 'n', 'i', 'c', 'o', 'd', 'e', 0},
TagUnknownType_9298_UserComment_Encoding_UNDEFINED: []byte{0, 0, 0, 0, 0, 0, 0, 0},
}
TagUnknownType_9101_ComponentsConfiguration_Names = map[int]string{
TagUnknownType_9101_ComponentsConfiguration_OTHER: "OTHER",
TagUnknownType_9101_ComponentsConfiguration_RGB: "RGB",
TagUnknownType_9101_ComponentsConfiguration_YCBCR: "YCBCR",
}
TagUnknownType_9101_ComponentsConfiguration_Configurations = map[int][]byte{
TagUnknownType_9101_ComponentsConfiguration_RGB: []byte{
TagUnknownType_9101_ComponentsConfiguration_Channel_R,
TagUnknownType_9101_ComponentsConfiguration_Channel_G,
TagUnknownType_9101_ComponentsConfiguration_Channel_B,
0,
},
TagUnknownType_9101_ComponentsConfiguration_YCBCR: []byte{
TagUnknownType_9101_ComponentsConfiguration_Channel_Y,
TagUnknownType_9101_ComponentsConfiguration_Channel_Cb,
TagUnknownType_9101_ComponentsConfiguration_Channel_Cr,
0,
},
}
)
// TODO(dustin): Rename `UnknownTagValue` to `UndefinedTagValue`.
type UnknownTagValue interface {
ValueBytes() ([]byte, error)
}
// TODO(dustin): Rename `TagUnknownType_GeneralString` to `TagUnknownType_GeneralString`.
type TagUnknownType_GeneralString string
func (gs TagUnknownType_GeneralString) ValueBytes() (value []byte, err error) {
return []byte(gs), nil
}
// TODO(dustin): Rename `TagUnknownType_9298_UserComment` to `TagUndefinedType_9298_UserComment`.
type TagUnknownType_9298_UserComment struct {
EncodingType int
EncodingBytes []byte
}
func (uc TagUnknownType_9298_UserComment) String() string {
var valuePhrase string
if len(uc.EncodingBytes) <= 8 {
valuePhrase = fmt.Sprintf("%v", uc.EncodingBytes)
} else {
valuePhrase = fmt.Sprintf("%v...", uc.EncodingBytes[:8])
}
return fmt.Sprintf("UserComment<SIZE=(%d) ENCODING=[%s] V=%v LEN=(%d)>", len(uc.EncodingBytes), TagUnknownType_9298_UserComment_Encoding_Names[uc.EncodingType], valuePhrase, len(uc.EncodingBytes))
}
func (uc TagUnknownType_9298_UserComment) ValueBytes() (value []byte, err error) {
encodingTypeBytes, found := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
if found == false {
log.Panicf("encoding-type not valid for unknown-type tag 9298 (UserComment): (%d)", uc.EncodingType)
}
value = make([]byte, len(uc.EncodingBytes)+8)
copy(value[:8], encodingTypeBytes)
copy(value[8:], uc.EncodingBytes)
return value, nil
}
// TODO(dustin): Rename `TagUnknownType_927C_MakerNote` to `TagUndefinedType_927C_MakerNote`.
type TagUnknownType_927C_MakerNote struct {
MakerNoteType []byte
MakerNoteBytes []byte
}
func (mn TagUnknownType_927C_MakerNote) String() string {
parts := make([]string, 20)
for i, c := range mn.MakerNoteType {
parts[i] = fmt.Sprintf("%02x", c)
}
h := sha1.New()
_, err := h.Write(mn.MakerNoteBytes)
log.PanicIf(err)
digest := h.Sum(nil)
return fmt.Sprintf("MakerNote<TYPE-ID=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(mn.MakerNoteBytes), digest)
}
func (uc TagUnknownType_927C_MakerNote) ValueBytes() (value []byte, err error) {
return uc.MakerNoteBytes, nil
}
// TODO(dustin): Rename `TagUnknownType_9101_ComponentsConfiguration` to `TagUndefinedType_9101_ComponentsConfiguration`.
type TagUnknownType_9101_ComponentsConfiguration struct {
ConfigurationId int
ConfigurationBytes []byte
}
func (cc TagUnknownType_9101_ComponentsConfiguration) String() string {
return fmt.Sprintf("ComponentsConfiguration<ID=[%s] BYTES=%v>", TagUnknownType_9101_ComponentsConfiguration_Names[cc.ConfigurationId], cc.ConfigurationBytes)
}
func (uc TagUnknownType_9101_ComponentsConfiguration) ValueBytes() (value []byte, err error) {
return uc.ConfigurationBytes, nil
}
// TODO(dustin): Rename `EncodeUnknown_9286` to `EncodeUndefined_9286`.
func EncodeUnknown_9286(uc TagUnknownType_9298_UserComment) (encoded []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
b := new(bytes.Buffer)
encodingTypeBytes := TagUnknownType_9298_UserComment_Encodings[uc.EncodingType]
_, err = b.Write(encodingTypeBytes)
log.PanicIf(err)
_, err = b.Write(uc.EncodingBytes)
log.PanicIf(err)
return b.Bytes(), nil
}
type EncodeableUndefinedValue struct {
IfdPath string
TagId uint16
Parameters interface{}
}
func EncodeUndefined(ifdPath string, tagId uint16, value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Finish implementing these.
if ifdPath == IfdPathStandardExif {
if tagId == 0x9286 {
encoded, err := EncodeUnknown_9286(value.(TagUnknownType_9298_UserComment))
log.PanicIf(err)
ed.Type = TypeUndefined
ed.Encoded = encoded
ed.UnitCount = uint32(len(encoded))
return ed, nil
}
}
log.Panicf("undefined value not encodable: %s (0x%02x)", ifdPath, tagId)
// Never called.
return EncodedData{}, nil
}
// TODO(dustin): Rename `TagUnknownType_UnknownValue` to `TagUndefinedType_UnknownValue`.
type TagUnknownType_UnknownValue []byte
func (tutuv TagUnknownType_UnknownValue) String() string {
parts := make([]string, len(tutuv))
for i, c := range tutuv {
parts[i] = fmt.Sprintf("%02x", c)
}
h := sha1.New()
_, err := h.Write(tutuv)
log.PanicIf(err)
digest := h.Sum(nil)
return fmt.Sprintf("Unknown<DATA=[%s] LEN=(%d) SHA1=[%020x]>", strings.Join(parts, " "), len(tutuv), digest)
}
// UndefinedValue knows how to resolve the value for most unknown-type tags.
func UndefinedValue(ifdPath string, tagId uint16, valueContext interface{}, byteOrder binary.ByteOrder) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): Stop exporting this. Use `(*ValueContext).Undefined()`.
var valueContextPtr *ValueContext
if vc, ok := valueContext.(*ValueContext); ok == true {
// Legacy usage.
valueContextPtr = vc
} else {
// Standard usage.
valueContextValue := valueContext.(ValueContext)
valueContextPtr = &valueContextValue
}
typeLogger.Debugf(nil, "UndefinedValue: IFD-PATH=[%s] TAG-ID=(0x%02x)", ifdPath, tagId)
if ifdPath == IfdPathStandardExif {
if tagId == 0x9000 {
// ExifVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0xa000 {
// FlashpixVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0x9286 {
// UserComment
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
unknownUc := TagUnknownType_9298_UserComment{
EncodingType: TagUnknownType_9298_UserComment_Encoding_UNDEFINED,
EncodingBytes: []byte{},
}
encoding := valueBytes[:8]
for encodingIndex, encodingBytes := range TagUnknownType_9298_UserComment_Encodings {
if bytes.Compare(encoding, encodingBytes) == 0 {
uc := TagUnknownType_9298_UserComment{
EncodingType: encodingIndex,
EncodingBytes: valueBytes[8:],
}
return uc, nil
}
}
typeLogger.Warningf(nil, "User-comment encoding not valid. Returning 'unknown' type (the default).")
return unknownUc, nil
} else if tagId == 0x927c {
// MakerNote
// TODO(dustin): !! This is the Wild Wild West. This very well might be a child IFD, but any and all OEM's define their own formats. If we're going to be writing changes and this is complete EXIF (which may not have the first eight bytes), it might be fine. However, if these are just IFDs they'll be relative to the main EXIF, this will invalidate the MakerNote data for IFDs and any other implementations that use offsets unless we can interpret them all. It be best to return to this later and just exclude this from being written for now, though means a loss of a wealth of image metadata.
// -> We can also just blindly try to interpret as an IFD and just validate that it's looks good (maybe it will even have a 'next ifd' pointer that we can validate is 0x0).
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
// TODO(dustin): Doesn't work, but here as an example.
// ie := NewIfdEnumerate(valueBytes, byteOrder)
// // TODO(dustin): !! Validate types (might have proprietary types, but it might be worth splitting the list between valid and not valid; maybe fail if a certain proportion are invalid, or maybe aren't less then a certain small integer)?
// ii, err := ie.Collect(0x0)
// for _, entry := range ii.RootIfd.Entries {
// fmt.Printf("ENTRY: 0x%02x %d\n", entry.TagId, entry.TagType)
// }
mn := TagUnknownType_927C_MakerNote{
MakerNoteType: valueBytes[:20],
// MakerNoteBytes has the whole length of bytes. There's always
// the chance that the first 20 bytes includes actual data.
MakerNoteBytes: valueBytes,
}
return mn, nil
} else if tagId == 0x9101 {
// ComponentsConfiguration
valueContextPtr.SetUnknownValueType(TypeByte)
valueBytes, err := valueContextPtr.ReadBytes()
log.PanicIf(err)
for configurationId, configurationBytes := range TagUnknownType_9101_ComponentsConfiguration_Configurations {
if bytes.Compare(valueBytes, configurationBytes) == 0 {
cc := TagUnknownType_9101_ComponentsConfiguration{
ConfigurationId: configurationId,
ConfigurationBytes: valueBytes,
}
return cc, nil
}
}
cc := TagUnknownType_9101_ComponentsConfiguration{
ConfigurationId: TagUnknownType_9101_ComponentsConfiguration_OTHER,
ConfigurationBytes: valueBytes,
}
return cc, nil
}
} else if ifdPath == IfdPathStandardGps {
if tagId == 0x001c {
// GPSAreaInformation
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
} else if tagId == 0x001b {
// GPSProcessingMethod
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
}
} else if ifdPath == IfdPathStandardExifIop {
if tagId == 0x0002 {
// InteropVersion
valueContextPtr.SetUnknownValueType(TypeAsciiNoNul)
valueString, err := valueContextPtr.ReadAsciiNoNul()
log.PanicIf(err)
return TagUnknownType_GeneralString(valueString), nil
}
}
// TODO(dustin): !! Still need to do:
//
// complex: 0xa302, 0xa20c, 0x8828
// long: 0xa301, 0xa300
//
// 0xa40b is device-specific and unhandled.
//
// See https://github.com/dsoprea/go-exif/issues/26.
// We have no choice but to return the error. We have no way of knowing how
// much data there is without already knowing what data-type this tag is.
return nil, ErrUnhandledUnknownTypedTag
}

88
v2/tags_undefined_test.go Normal file
View File

@ -0,0 +1,88 @@
package exif
import (
"bytes"
"testing"
"github.com/dsoprea/go-logging"
)
func TestUndefinedValue_ExifVersion(t *testing.T) {
byteOrder := TestDefaultByteOrder
fqIfdPath := "IFD0/Exif0"
ifdPath := "IFD/Exif"
// Create our unknown-type tag's value using the fact that we know it's a
// non-null-terminated string.
ve := NewValueEncoder(byteOrder)
tt := NewTagType(TypeAsciiNoNul, byteOrder)
valueString := "0230"
ed, err := ve.EncodeWithType(tt, valueString)
log.PanicIf(err)
// Create the tag using the official "unknown" type now that we already
// have the bytes.
encodedValue := NewIfdBuilderTagValueFromBytes(ed.Encoded)
bt := &BuilderTag{
ifdPath: ifdPath,
tagId: 0x9000,
typeId: TypeUndefined,
value: encodedValue,
}
// Stage the build.
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
ibe := NewIfdByteEncoder()
ib := NewIfdBuilder(im, ti, ifdPath, byteOrder)
b := new(bytes.Buffer)
bw := NewByteWriter(b, byteOrder)
addressableOffset := uint32(0x1234)
ida := newIfdDataAllocator(addressableOffset)
// Encode.
_, err = ibe.encodeTagToBytes(ib, bt, bw, ida, uint32(0))
log.PanicIf(err)
tagBytes := b.Bytes()
if len(tagBytes) != 12 {
t.Fatalf("Tag not encoded to the right number of bytes: (%d)", len(tagBytes))
}
ite, err := ParseOneTag(im, ti, fqIfdPath, ifdPath, byteOrder, tagBytes, false)
log.PanicIf(err)
if ite.TagId != 0x9000 {
t.Fatalf("Tag-ID not correct: (0x%02x)", ite.TagId)
} else if ite.TagIndex != 0 {
t.Fatalf("Tag index not correct: (%d)", ite.TagIndex)
} else if ite.TagType != TypeUndefined {
t.Fatalf("Tag type not correct: (%d)", ite.TagType)
} else if ite.UnitCount != (uint32(len(valueString))) {
t.Fatalf("Tag unit-count not correct: (%d)", ite.UnitCount)
} else if bytes.Compare(ite.RawValueOffset, []byte{'0', '2', '3', '0'}) != 0 {
t.Fatalf("Tag's value (as raw bytes) is not correct: [%x]", ite.RawValueOffset)
} else if ite.ChildIfdPath != "" {
t.Fatalf("Tag's child IFD-path should be empty: [%s]", ite.ChildIfdPath)
} else if ite.IfdPath != ifdPath {
t.Fatalf("Tag's parent IFD is not correct: %v", ite.IfdPath)
}
}
// TODO(dustin): !! Add tests for remaining, well-defined unknown
// TODO(dustin): !! Test what happens with unhandled unknown-type tags (though it should never get to this point in the normal workflow).

310
v2/type.go Normal file
View File

@ -0,0 +1,310 @@
package exif
import (
"errors"
"fmt"
"strconv"
"strings"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
type TagTypePrimitive uint16
func (typeType TagTypePrimitive) String() string {
return TypeNames[typeType]
}
func (tagType TagTypePrimitive) Size() int {
if tagType == TypeByte {
return 1
} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
return 1
} else if tagType == TypeShort {
return 2
} else if tagType == TypeLong {
return 4
} else if tagType == TypeRational {
return 8
} else if tagType == TypeSignedLong {
return 4
} else if tagType == TypeSignedRational {
return 8
} else {
log.Panicf("can not determine tag-value size for type (%d): [%s]", tagType, TypeNames[tagType])
// Never called.
return 0
}
}
const (
TypeByte TagTypePrimitive = 1
TypeAscii TagTypePrimitive = 2
TypeShort TagTypePrimitive = 3
TypeLong TagTypePrimitive = 4
TypeRational TagTypePrimitive = 5
TypeUndefined TagTypePrimitive = 7
TypeSignedLong TagTypePrimitive = 9
TypeSignedRational TagTypePrimitive = 10
// TypeAsciiNoNul is just a pseudo-type, for our own purposes.
TypeAsciiNoNul TagTypePrimitive = 0xf0
)
var (
typeLogger = log.NewLogger("exif.type")
)
var (
// TODO(dustin): Rename TypeNames() to typeNames() and add getter.
TypeNames = map[TagTypePrimitive]string{
TypeByte: "BYTE",
TypeAscii: "ASCII",
TypeShort: "SHORT",
TypeLong: "LONG",
TypeRational: "RATIONAL",
TypeUndefined: "UNDEFINED",
TypeSignedLong: "SLONG",
TypeSignedRational: "SRATIONAL",
TypeAsciiNoNul: "_ASCII_NO_NUL",
}
TypeNamesR = map[string]TagTypePrimitive{}
)
var (
// ErrNotEnoughData is used when there isn't enough data to accomodate what
// we're trying to parse (sizeof(type) * unit_count).
ErrNotEnoughData = errors.New("not enough data for type")
// ErrWrongType is used when we try to parse anything other than the
// current type.
ErrWrongType = errors.New("wrong type, can not parse")
// ErrUnhandledUnknownTag is used when we try to parse a tag that's
// recorded as an "unknown" type but not a documented tag (therefore
// leaving us not knowning how to read it).
ErrUnhandledUnknownTypedTag = errors.New("not a standard unknown-typed tag")
)
type Rational struct {
Numerator uint32
Denominator uint32
}
type SignedRational struct {
Numerator int32
Denominator int32
}
func TagTypeSize(tagType TagTypePrimitive) int {
// DEPRECATED(dustin): `(TagTypePrimitive).Size()` should be used, directly.
return tagType.Size()
}
// Format returns a stringified value for the given bytes. Automatically
// calculates count based on type size.
func Format(rawBytes []byte, tagType TagTypePrimitive, justFirst bool, byteOrder binary.ByteOrder) (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): !! Add tests
typeSize := tagType.Size()
if len(rawBytes)%typeSize != 0 {
log.Panicf("byte-count (%d) does not align for [%s] type with a size of (%d) bytes", len(rawBytes), TypeNames[tagType], typeSize)
}
// unitCount is the calculated unit-count. This should equal the original
// value from the tag (pre-resolution).
unitCount := uint32(len(rawBytes) / typeSize)
// Truncate the items if it's not bytes or a string and we just want the first.
valueSuffix := ""
if justFirst == true && unitCount > 1 && tagType != TypeByte && tagType != TypeAscii && tagType != TypeAsciiNoNul {
unitCount = 1
valueSuffix = "..."
}
if tagType == TypeByte {
items, err := parser.ParseBytes(rawBytes, unitCount)
log.PanicIf(err)
return DumpBytesToString(items), nil
} else if tagType == TypeAscii {
phrase, err := parser.ParseAscii(rawBytes, unitCount)
log.PanicIf(err)
return phrase, nil
} else if tagType == TypeAsciiNoNul {
phrase, err := parser.ParseAsciiNoNul(rawBytes, unitCount)
log.PanicIf(err)
return phrase, nil
} else if tagType == TypeShort {
items, err := parser.ParseShorts(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeLong {
items, err := parser.ParseLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeRational {
items, err := parser.ParseRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
parts := make([]string, len(items))
for i, r := range items {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
}
if justFirst == true {
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", parts), nil
}
} else {
return "", nil
}
} else if tagType == TypeSignedLong {
items, err := parser.ParseSignedLongs(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", items[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", items), nil
}
} else {
return "", nil
}
} else if tagType == TypeSignedRational {
items, err := parser.ParseSignedRationals(rawBytes, unitCount, byteOrder)
log.PanicIf(err)
parts := make([]string, len(items))
for i, r := range items {
parts[i] = fmt.Sprintf("%d/%d", r.Numerator, r.Denominator)
}
if len(items) > 0 {
if justFirst == true {
return fmt.Sprintf("%v%s", parts[0], valueSuffix), nil
} else {
return fmt.Sprintf("%v", parts), nil
}
} else {
return "", nil
}
} else {
// Affects only "unknown" values, in general.
log.Panicf("value of type [%s] can not be formatted into string", tagType.String())
// Never called.
return "", nil
}
}
func EncodeStringToBytes(tagType TagTypePrimitive, valueString string) (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if tagType == TypeUndefined {
// TODO(dustin): Circle back to this.
log.Panicf("undefined-type values are not supported")
}
if tagType == TypeByte {
return []byte(valueString), nil
} else if tagType == TypeAscii || tagType == TypeAsciiNoNul {
// Whether or not we're putting an NUL on the end is only relevant for
// byte-level encoding. This function really just supports a user
// interface.
return valueString, nil
} else if tagType == TypeShort {
n, err := strconv.ParseUint(valueString, 10, 16)
log.PanicIf(err)
return uint16(n), nil
} else if tagType == TypeLong {
n, err := strconv.ParseUint(valueString, 10, 32)
log.PanicIf(err)
return uint32(n), nil
} else if tagType == TypeRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseUint(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseUint(parts[1], 10, 32)
log.PanicIf(err)
return Rational{
Numerator: uint32(numerator),
Denominator: uint32(denominator),
}, nil
} else if tagType == TypeSignedLong {
n, err := strconv.ParseInt(valueString, 10, 32)
log.PanicIf(err)
return int32(n), nil
} else if tagType == TypeSignedRational {
parts := strings.SplitN(valueString, "/", 2)
numerator, err := strconv.ParseInt(parts[0], 10, 32)
log.PanicIf(err)
denominator, err := strconv.ParseInt(parts[1], 10, 32)
log.PanicIf(err)
return SignedRational{
Numerator: int32(numerator),
Denominator: int32(denominator),
}, nil
}
log.Panicf("from-string encoding for type not supported; this shouldn't happen: [%s]", tagType.String())
return nil, nil
}
func init() {
for typeId, typeName := range TypeNames {
TypeNamesR[typeName] = typeId
}
}

262
v2/type_encode.go Normal file
View File

@ -0,0 +1,262 @@
package exif
import (
"bytes"
"reflect"
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
typeEncodeLogger = log.NewLogger("exif.type_encode")
)
// EncodedData encapsulates the compound output of an encoding operation.
type EncodedData struct {
Type TagTypePrimitive
Encoded []byte
// TODO(dustin): Is this really necessary? We might have this just to correlate to the incoming stream format (raw bytes and a unit-count both for incoming and outgoing).
UnitCount uint32
}
type ValueEncoder struct {
byteOrder binary.ByteOrder
}
func NewValueEncoder(byteOrder binary.ByteOrder) *ValueEncoder {
return &ValueEncoder{
byteOrder: byteOrder,
}
}
func (ve *ValueEncoder) encodeBytes(value []uint8) (ed EncodedData, err error) {
ed.Type = TypeByte
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(value))
return ed, nil
}
func (ve *ValueEncoder) encodeAscii(value string) (ed EncodedData, err error) {
ed.Type = TypeAscii
ed.Encoded = []byte(value)
ed.Encoded = append(ed.Encoded, 0)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
// encodeAsciiNoNul returns a string encoded as a byte-string without a trailing
// NUL byte.
//
// Note that:
//
// 1. This type can not be automatically encoded using `Encode()`. The default
// mode is to encode *with* a trailing NUL byte using `encodeAscii`. Only
// certain undefined-type tags using an unterminated ASCII string and these
// are exceptional in nature.
//
// 2. The presence of this method allows us to completely test the complimentary
// no-nul parser.
//
func (ve *ValueEncoder) encodeAsciiNoNul(value string) (ed EncodedData, err error) {
ed.Type = TypeAsciiNoNul
ed.Encoded = []byte(value)
ed.UnitCount = uint32(len(ed.Encoded))
return ed, nil
}
func (ve *ValueEncoder) encodeShorts(value []uint16) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*2)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint16(ed.Encoded[i*2:(i+1)*2], value[i])
}
ed.Type = TypeShort
return ed, nil
}
func (ve *ValueEncoder) encodeLongs(value []uint32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*4)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*4:(i+1)*4], value[i])
}
ed.Type = TypeLong
return ed, nil
}
func (ve *ValueEncoder) encodeRationals(value []Rational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
ed.Encoded = make([]byte, ed.UnitCount*8)
for i := uint32(0); i < ed.UnitCount; i++ {
ve.byteOrder.PutUint32(ed.Encoded[i*8+0:i*8+4], value[i].Numerator)
ve.byteOrder.PutUint32(ed.Encoded[i*8+4:i*8+8], value[i].Denominator)
}
ed.Type = TypeRational
return ed, nil
}
func (ve *ValueEncoder) encodeSignedLongs(value []int32) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i])
log.PanicIf(err)
}
ed.Type = TypeSignedLong
ed.Encoded = b.Bytes()
return ed, nil
}
func (ve *ValueEncoder) encodeSignedRationals(value []SignedRational) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
ed.UnitCount = uint32(len(value))
b := bytes.NewBuffer(make([]byte, 0, 8*ed.UnitCount))
for i := uint32(0); i < ed.UnitCount; i++ {
err := binary.Write(b, ve.byteOrder, value[i].Numerator)
log.PanicIf(err)
err = binary.Write(b, ve.byteOrder, value[i].Denominator)
log.PanicIf(err)
}
ed.Type = TypeSignedRational
ed.Encoded = b.Bytes()
return ed, nil
}
// Encode returns bytes for the given value, infering type from the actual
// value. This does not support `TypeAsciiNoNull` (all strings are encoded as
// `TypeAscii`).
func (ve *ValueEncoder) Encode(value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): This is redundant with EncodeWithType. Refactor one to use the other.
switch value.(type) {
case []byte:
ed, err = ve.encodeBytes(value.([]byte))
log.PanicIf(err)
case string:
ed, err = ve.encodeAscii(value.(string))
log.PanicIf(err)
case []uint16:
ed, err = ve.encodeShorts(value.([]uint16))
log.PanicIf(err)
case []uint32:
ed, err = ve.encodeLongs(value.([]uint32))
log.PanicIf(err)
case []Rational:
ed, err = ve.encodeRationals(value.([]Rational))
log.PanicIf(err)
case []int32:
ed, err = ve.encodeSignedLongs(value.([]int32))
log.PanicIf(err)
case []SignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational))
log.PanicIf(err)
default:
log.Panicf("value not encodable: [%s] [%v]", reflect.TypeOf(value), value)
}
return ed, nil
}
// EncodeWithType returns bytes for the given value, using the given `TagType`
// value to determine how to encode. This supports `TypeAsciiNoNul`.
func (ve *ValueEncoder) EncodeWithType(tt TagType, value interface{}) (ed EncodedData, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
// TODO(dustin): This is redundant with Encode. Refactor one to use the other.
switch tt.Type() {
case TypeByte:
ed, err = ve.encodeBytes(value.([]byte))
log.PanicIf(err)
case TypeAscii:
ed, err = ve.encodeAscii(value.(string))
log.PanicIf(err)
case TypeAsciiNoNul:
ed, err = ve.encodeAsciiNoNul(value.(string))
log.PanicIf(err)
case TypeShort:
ed, err = ve.encodeShorts(value.([]uint16))
log.PanicIf(err)
case TypeLong:
ed, err = ve.encodeLongs(value.([]uint32))
log.PanicIf(err)
case TypeRational:
ed, err = ve.encodeRationals(value.([]Rational))
log.PanicIf(err)
case TypeSignedLong:
ed, err = ve.encodeSignedLongs(value.([]int32))
log.PanicIf(err)
case TypeSignedRational:
ed, err = ve.encodeSignedRationals(value.([]SignedRational))
log.PanicIf(err)
default:
log.Panicf("value not encodable (with type): %v [%v]", tt, value)
}
return ed, nil
}

566
v2/type_encode_test.go Normal file
View File

@ -0,0 +1,566 @@
package exif
import (
"testing"
"reflect"
"github.com/dsoprea/go-logging"
)
func TestByteCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []byte("original text")
ed, err := ve.encodeBytes(original)
log.PanicIf(err)
if ed.Type != TypeByte {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseBytes(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestAsciiCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.encodeAscii(original)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
expected = append(expected, 0)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 14 {
t.Fatalf("Unit-count not correct.")
}
// Check that the string was recovered correctly and with the trailing NUL
// character autostripped.
tt := NewTagType(TypeAscii, byteOrder)
recovered, err := tt.ParseAscii(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestAsciiNoNulCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.encodeAsciiNoNul(original)
log.PanicIf(err)
if ed.Type != TypeAsciiNoNul {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
// Check that the string was recovered correctly and with the trailing NUL
// character ignored (because not expected in the context of that type).
tt := NewTagType(TypeAsciiNoNul, byteOrder)
recovered, err := tt.ParseAsciiNoNul(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, string(expected)) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestShortCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint16 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.encodeShorts(original)
log.PanicIf(err)
if ed.Type != TypeShort {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x11,
0x00, 0x22,
0x00, 0x33,
0x00, 0x44,
0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseShorts(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestLongCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.encodeLongs(original)
log.PanicIf(err)
if ed.Type != TypeLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseLongs(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestRationalCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []Rational {
Rational{
Numerator: 0x11,
Denominator: 0x22,
},
Rational{
Numerator: 0x33,
Denominator: 0x44,
},
Rational{
Numerator: 0x55,
Denominator: 0x66,
},
Rational{
Numerator: 0x77,
Denominator: 0x88,
},
Rational{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.encodeRationals(original)
log.PanicIf(err)
if ed.Type != TypeRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseRationals(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestSignedLongCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []int32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.encodeSignedLongs(original)
log.PanicIf(err)
if ed.Type != TypeSignedLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseSignedLongs(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestSignedRationalCycle(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []SignedRational {
SignedRational{
Numerator: 0x11,
Denominator: 0x22,
},
SignedRational{
Numerator: 0x33,
Denominator: 0x44,
},
SignedRational{
Numerator: 0x55,
Denominator: 0x66,
},
SignedRational{
Numerator: 0x77,
Denominator: 0x88,
},
SignedRational{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.encodeSignedRationals(original)
log.PanicIf(err)
if ed.Type != TypeSignedRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
tt := NewTagType(ed.Type, byteOrder)
recovered, err := tt.ParseSignedRationals(ed.Encoded, ed.UnitCount)
if reflect.DeepEqual(recovered, original) != true {
t.Fatalf("Value not recovered correctly.")
}
}
func TestEncode_Byte(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []byte("original text")
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeByte {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 13 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_Ascii(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := "original text"
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeAscii {
t.Fatalf("IFD type not expected.")
}
expected := []byte(original)
expected = append(expected, 0)
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 14 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_Short(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint16 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeShort {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x11,
0x00, 0x22,
0x00, 0x33,
0x00, 0x44,
0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_Long(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []uint32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_Rational(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []Rational {
Rational{
Numerator: 0x11,
Denominator: 0x22,
},
Rational{
Numerator: 0x33,
Denominator: 0x44,
},
Rational{
Numerator: 0x55,
Denominator: 0x66,
},
Rational{
Numerator: 0x77,
Denominator: 0x88,
},
Rational{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_SignedLong(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []int32 { 0x11, 0x22, 0x33, 0x44, 0x55 }
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeSignedLong {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}
func TestEncode_SignedRational(t *testing.T) {
byteOrder := TestDefaultByteOrder
ve := NewValueEncoder(byteOrder)
original := []SignedRational {
SignedRational{
Numerator: 0x11,
Denominator: 0x22,
},
SignedRational{
Numerator: 0x33,
Denominator: 0x44,
},
SignedRational{
Numerator: 0x55,
Denominator: 0x66,
},
SignedRational{
Numerator: 0x77,
Denominator: 0x88,
},
SignedRational{
Numerator: 0x99,
Denominator: 0x00,
},
}
ed, err := ve.Encode(original)
log.PanicIf(err)
if ed.Type != TypeSignedRational {
t.Fatalf("IFD type not expected.")
}
expected := []byte {
0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x22,
0x00, 0x00, 0x00, 0x33,
0x00, 0x00, 0x00, 0x44,
0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x66,
0x00, 0x00, 0x00, 0x77,
0x00, 0x00, 0x00, 0x88,
0x00, 0x00, 0x00, 0x99,
0x00, 0x00, 0x00, 0x00,
}
if reflect.DeepEqual(ed.Encoded, expected) != true {
t.Fatalf("Data not encoded correctly.")
} else if ed.UnitCount != 5 {
t.Fatalf("Unit-count not correct.")
}
}

297
v2/type_test.go Normal file
View File

@ -0,0 +1,297 @@
package exif
import (
"testing"
"bytes"
"fmt"
"reflect"
"github.com/dsoprea/go-logging"
)
func TestTagType_EncodeDecode_Byte(t *testing.T) {
tt := NewTagType(TypeByte, TestDefaultByteOrder)
data := []byte { 0x11, 0x22, 0x33, 0x44, 0x55 }
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, data) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseBytes(encoded, uint32(len(data)))
log.PanicIf(err)
if bytes.Compare(restored, data) != 0 {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_Ascii(t *testing.T) {
tt := NewTagType(TypeAscii, TestDefaultByteOrder)
data := "hello"
encoded, err := tt.Encode(data)
log.PanicIf(err)
if string(encoded) != fmt.Sprintf("%s\000", data) {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseAscii(encoded, uint32(len(data)))
log.PanicIf(err)
if restored != data {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_Shorts(t *testing.T) {
tt := NewTagType(TypeShort, TestDefaultByteOrder)
data := []uint16 { 0x11, 0x22, 0x33 }
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, []byte { 0x00, 0x11, 0x00, 0x22, 0x00, 0x33 }) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseShorts(encoded, uint32(len(data)))
log.PanicIf(err)
if reflect.DeepEqual(restored, data) != true {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_Long(t *testing.T) {
tt := NewTagType(TypeLong, TestDefaultByteOrder)
data := []uint32 { 0x11, 0x22, 0x33 }
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33 }) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseLongs(encoded, uint32(len(data)))
log.PanicIf(err)
if reflect.DeepEqual(restored, data) != true {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_Rational(t *testing.T) {
tt := NewTagType(TypeRational, TestDefaultByteOrder)
data := []Rational {
Rational{ Numerator: 0x11, Denominator: 0x22 },
Rational{ Numerator: 0x33, Denominator: 0x44 },
}
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44 }) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseRationals(encoded, uint32(len(data)))
log.PanicIf(err)
if reflect.DeepEqual(restored, data) != true {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_SignedLong(t *testing.T) {
tt := NewTagType(TypeSignedLong, TestDefaultByteOrder)
data := []int32 { 0x11, 0x22, 0x33 }
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33 }) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseSignedLongs(encoded, uint32(len(data)))
log.PanicIf(err)
if reflect.DeepEqual(restored, data) != true {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_SignedRational(t *testing.T) {
tt := NewTagType(TypeSignedRational, TestDefaultByteOrder)
data := []SignedRational {
SignedRational{ Numerator: 0x11, Denominator: 0x22 },
SignedRational{ Numerator: 0x33, Denominator: 0x44 },
}
encoded, err := tt.Encode(data)
log.PanicIf(err)
if bytes.Compare(encoded, []byte { 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x44 }) != 0 {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseSignedRationals(encoded, uint32(len(data)))
log.PanicIf(err)
if reflect.DeepEqual(restored, data) != true {
t.Fatalf("Data not decoded correctly.")
}
}
func TestTagType_EncodeDecode_AsciiNoNul(t *testing.T) {
tt := NewTagType(TypeAsciiNoNul, TestDefaultByteOrder)
data := "hello"
encoded, err := tt.Encode(data)
log.PanicIf(err)
if string(encoded) != data {
t.Fatalf("Data not encoded correctly.")
}
restored, err := tt.ParseAsciiNoNul(encoded, uint32(len(data)))
log.PanicIf(err)
if restored != data {
t.Fatalf("Data not decoded correctly.")
}
}
// TODO(dustin): Add tests for TypeUndefined.
func TestTagType_FromString_Undefined(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
log.Panic(err)
}
}()
tt := NewTagType(TypeUndefined, TestDefaultByteOrder)
_, err := tt.FromString("")
if err == nil {
t.Fatalf("no error for undefined-type")
} else if err.Error() != "undefined-type values are not supported" {
fmt.Printf("[%s]\n", err.Error())
log.Panic(err)
}
}
func TestTagType_FromString_Byte(t *testing.T) {
tt := NewTagType(TypeByte, TestDefaultByteOrder)
value, err := tt.FromString("abc")
log.PanicIf(err)
if reflect.DeepEqual(value, []byte { 'a', 'b', 'c' }) != true {
t.Fatalf("byte value not correct")
}
}
func TestTagType_FromString_Ascii(t *testing.T) {
tt := NewTagType(TypeAscii, TestDefaultByteOrder)
value, err := tt.FromString("abc")
log.PanicIf(err)
if reflect.DeepEqual(value, "abc") != true {
t.Fatalf("ASCII value not correct: [%s]", value)
}
}
func TestTagType_FromString_Short(t *testing.T) {
tt := NewTagType(TypeShort, TestDefaultByteOrder)
value, err := tt.FromString("55")
log.PanicIf(err)
if reflect.DeepEqual(value, uint16(55)) != true {
t.Fatalf("short value not correct")
}
}
func TestTagType_FromString_Long(t *testing.T) {
tt := NewTagType(TypeLong, TestDefaultByteOrder)
value, err := tt.FromString("66000")
log.PanicIf(err)
if reflect.DeepEqual(value, uint32(66000)) != true {
t.Fatalf("long value not correct")
}
}
func TestTagType_FromString_Rational(t *testing.T) {
tt := NewTagType(TypeRational, TestDefaultByteOrder)
value, err := tt.FromString("12/34")
log.PanicIf(err)
expected := Rational{
Numerator: 12,
Denominator: 34,
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("rational value not correct")
}
}
func TestTagType_FromString_SignedLong(t *testing.T) {
tt := NewTagType(TypeSignedLong, TestDefaultByteOrder)
value, err := tt.FromString("-66000")
log.PanicIf(err)
if reflect.DeepEqual(value, int32(-66000)) != true {
t.Fatalf("signed-long value not correct")
}
}
func TestTagType_FromString_SignedRational(t *testing.T) {
tt := NewTagType(TypeSignedRational, TestDefaultByteOrder)
value, err := tt.FromString("-12/34")
log.PanicIf(err)
expected := SignedRational{
Numerator: -12,
Denominator: 34,
}
if reflect.DeepEqual(value, expected) != true {
t.Fatalf("signd-rational value not correct")
}
}
func TestTagType_FromString_AsciiNoNul(t *testing.T) {
tt := NewTagType(TypeAsciiNoNul, TestDefaultByteOrder)
value, err := tt.FromString("abc")
log.PanicIf(err)
if reflect.DeepEqual(value, "abc") != true {
t.Fatalf("ASCII-no-nul value not correct")
}
}

222
v2/utility.go Normal file
View File

@ -0,0 +1,222 @@
package exif
import (
"bytes"
"fmt"
"strconv"
"strings"
"time"
"github.com/dsoprea/go-logging"
)
func DumpBytes(data []byte) {
fmt.Printf("DUMP: ")
for _, x := range data {
fmt.Printf("%02x ", x)
}
fmt.Printf("\n")
}
func DumpBytesClause(data []byte) {
fmt.Printf("DUMP: ")
fmt.Printf("[]byte { ")
for i, x := range data {
fmt.Printf("0x%02x", x)
if i < len(data)-1 {
fmt.Printf(", ")
}
}
fmt.Printf(" }\n")
}
func DumpBytesToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("%02x", x))
log.PanicIf(err)
if i < len(data)-1 {
_, err := b.WriteRune(' ')
log.PanicIf(err)
}
}
return b.String()
}
func DumpBytesClauseToString(data []byte) string {
b := new(bytes.Buffer)
for i, x := range data {
_, err := b.WriteString(fmt.Sprintf("0x%02x", x))
log.PanicIf(err)
if i < len(data)-1 {
_, err := b.WriteString(", ")
log.PanicIf(err)
}
}
return b.String()
}
// ParseExifFullTimestamp parses dates like "2018:11:30 13:01:49" into a UTC
// `time.Time` struct.
func ParseExifFullTimestamp(fullTimestampPhrase string) (timestamp time.Time, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
parts := strings.Split(fullTimestampPhrase, " ")
datestampValue, timestampValue := parts[0], parts[1]
dateParts := strings.Split(datestampValue, ":")
year, err := strconv.ParseUint(dateParts[0], 10, 16)
if err != nil {
log.Panicf("could not parse year")
}
month, err := strconv.ParseUint(dateParts[1], 10, 8)
if err != nil {
log.Panicf("could not parse month")
}
day, err := strconv.ParseUint(dateParts[2], 10, 8)
if err != nil {
log.Panicf("could not parse day")
}
timeParts := strings.Split(timestampValue, ":")
hour, err := strconv.ParseUint(timeParts[0], 10, 8)
if err != nil {
log.Panicf("could not parse hour")
}
minute, err := strconv.ParseUint(timeParts[1], 10, 8)
if err != nil {
log.Panicf("could not parse minute")
}
second, err := strconv.ParseUint(timeParts[2], 10, 8)
if err != nil {
log.Panicf("could not parse second")
}
timestamp = time.Date(int(year), time.Month(month), int(day), int(hour), int(minute), int(second), 0, time.UTC)
return timestamp, nil
}
// ExifFullTimestampString produces a string like "2018:11:30 13:01:49" from a
// `time.Time` struct. It will attempt to convert to UTC first.
func ExifFullTimestampString(t time.Time) (fullTimestampPhrase string) {
t = t.UTC()
return fmt.Sprintf("%04d:%02d:%02d %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
}
// ExifTag is one simple representation of a tag in a flat list of all of them.
type ExifTag struct {
IfdPath string `json:"ifd_path"`
TagId uint16 `json:"id"`
TagName string `json:"name"`
TagTypeId TagTypePrimitive `json:"type_id"`
TagTypeName string `json:"type_name"`
Value interface{} `json:"value"`
ValueBytes []byte `json:"value_bytes"`
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.Value, len(et.ValueBytes), et.ChildIfdPath)
}
// GetFlatExifData returns a simple, flat representation of all tags.
func GetFlatExifData(exifData []byte) (exifTags []ExifTag, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
im := NewIfdMappingWithStandard()
ti := NewTagIndex()
_, index, err := Collect(im, ti, exifData)
log.PanicIf(err)
q := []*Ifd{index.RootIfd}
exifTags = make([]ExifTag, 0)
for len(q) > 0 {
var ifd *Ifd
ifd, q = q[0], q[1:]
ti := NewTagIndex()
for _, ite := range ifd.Entries {
tagName := ""
it, err := ti.Get(ifd.IfdPath, ite.TagId)
if err != nil {
// If it's a non-standard tag, just leave the name blank.
if log.Is(err, ErrTagNotFound) != true {
log.PanicIf(err)
}
} else {
tagName = it.Name
}
value, err := ifd.TagValue(ite)
if err != nil {
if err == ErrUnhandledUnknownTypedTag {
value = UnparseableUnknownTagValuePlaceholder
} else {
log.Panic(err)
}
}
valueBytes, err := ifd.TagValueBytes(ite)
if err != nil && err != ErrUnhandledUnknownTypedTag {
log.Panic(err)
}
et := ExifTag{
IfdPath: ifd.IfdPath,
TagId: ite.TagId,
TagName: tagName,
TagTypeId: ite.TagType,
TagTypeName: TypeNames[ite.TagType],
Value: value,
ValueBytes: valueBytes,
ChildIfdPath: ite.ChildIfdPath,
}
exifTags = append(exifTags, et)
}
for _, childIfd := range ifd.Children {
q = append(q, childIfd)
}
if ifd.NextIfd != nil {
q = append(q, ifd.NextIfd)
}
}
return exifTags, nil
}

125
v2/utility_test.go Normal file
View File

@ -0,0 +1,125 @@
package exif
import (
"io/ioutil"
"fmt"
"os"
"testing"
"time"
"github.com/dsoprea/go-logging"
)
func TestDumpBytes(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "utilitytest")
log.PanicIf(err)
defer os.Remove(f.Name())
originalStdout := os.Stdout
os.Stdout = f
DumpBytes([]byte{0x11, 0x22})
os.Stdout = originalStdout
_, err = f.Seek(0, 0)
log.PanicIf(err)
content, err := ioutil.ReadAll(f)
log.PanicIf(err)
if string(content) != "DUMP: 11 22 \n" {
t.Fatalf("content not correct: [%s]", string(content))
}
}
func TestDumpBytesClause(t *testing.T) {
f, err := ioutil.TempFile(os.TempDir(), "utilitytest")
log.PanicIf(err)
defer os.Remove(f.Name())
originalStdout := os.Stdout
os.Stdout = f
DumpBytesClause([]byte{0x11, 0x22})
os.Stdout = originalStdout
_, err = f.Seek(0, 0)
log.PanicIf(err)
content, err := ioutil.ReadAll(f)
log.PanicIf(err)
if string(content) != "DUMP: []byte { 0x11, 0x22 }\n" {
t.Fatalf("content not correct: [%s]", string(content))
}
}
func TestDumpBytesToString(t *testing.T) {
s := DumpBytesToString([]byte{0x12, 0x34, 0x56})
if s != "12 34 56" {
t.Fatalf("result not expected")
}
}
func TestDumpBytesClauseToString(t *testing.T) {
s := DumpBytesClauseToString([]byte{0x12, 0x34, 0x56})
if s != "0x12, 0x34, 0x56" {
t.Fatalf("result not expected")
}
}
func TestParseExifFullTimestamp(t *testing.T) {
timestamp, err := ParseExifFullTimestamp("2018:11:30 13:01:49")
log.PanicIf(err)
actual := timestamp.Format(time.RFC3339)
expected := "2018-11-30T13:01:49Z"
if actual != expected {
t.Fatalf("time not formatted correctly: [%s] != [%s]", actual, expected)
}
}
func TestExifFullTimestampString(t *testing.T) {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
restoredPhrase := ExifFullTimestampString(timestamp)
if restoredPhrase != originalPhrase {
t.Fatalf("Final phrase [%s] does not equal original phrase [%s]", restoredPhrase, originalPhrase)
}
}
func ExampleParseExifFullTimestamp() {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
fmt.Printf("To Go timestamp: [%s]\n", timestamp.Format(time.RFC3339))
// Output:
// To Go timestamp: [2018-11-30T13:01:49Z]
}
func ExampleExifFullTimestampString() {
originalPhrase := "2018:11:30 13:01:49"
timestamp, err := ParseExifFullTimestamp(originalPhrase)
log.PanicIf(err)
restoredPhrase := ExifFullTimestampString(timestamp)
fmt.Printf("To EXIF timestamp: [%s]\n", restoredPhrase)
// Output:
// To EXIF timestamp: [2018:11:30 13:01:49]
}

361
v2/value_context.go Normal file
View File

@ -0,0 +1,361 @@
package exif
import (
"encoding/binary"
"github.com/dsoprea/go-logging"
)
var (
parser *Parser
)
// ValueContext describes all of the parameters required to find and extract
// the actual tag value.
type ValueContext struct {
unitCount uint32
valueOffset uint32
rawValueOffset []byte
addressableData []byte
tagType TagTypePrimitive
byteOrder binary.ByteOrder
// undefinedValueTagType is the effective type to use if this is an
// "undefined" value.
undefinedValueTagType TagTypePrimitive
ifdPath string
tagId uint16
}
func newValueContext(ifdPath string, tagId uint16, unitCount, valueOffset uint32, rawValueOffset, addressableData []byte, tagType TagTypePrimitive, byteOrder binary.ByteOrder) *ValueContext {
return &ValueContext{
unitCount: unitCount,
valueOffset: valueOffset,
rawValueOffset: rawValueOffset,
addressableData: addressableData,
tagType: tagType,
byteOrder: byteOrder,
ifdPath: ifdPath,
tagId: tagId,
}
}
func newValueContextFromTag(ite *IfdTagEntry, addressableData []byte, byteOrder binary.ByteOrder) *ValueContext {
return newValueContext(
ite.IfdPath,
ite.TagId,
ite.UnitCount,
ite.ValueOffset,
ite.RawValueOffset,
addressableData,
ite.TagType,
byteOrder)
}
func (vc *ValueContext) SetUnknownValueType(tagType TagTypePrimitive) {
vc.undefinedValueTagType = tagType
}
func (vc *ValueContext) UnitCount() uint32 {
return vc.unitCount
}
func (vc *ValueContext) ValueOffset() uint32 {
return vc.valueOffset
}
func (vc *ValueContext) RawValueOffset() []byte {
return vc.rawValueOffset
}
func (vc *ValueContext) AddressableData() []byte {
return vc.addressableData
}
// isEmbedded returns whether the value is embedded or a reference. This can't
// be precalculated since the size is not defined for all types (namely the
// "undefined" types).
func (vc *ValueContext) isEmbedded() bool {
tagType := vc.effectiveValueType()
return (tagType.Size() * int(vc.unitCount)) <= 4
}
func (vc *ValueContext) effectiveValueType() (tagType TagTypePrimitive) {
if vc.tagType == TypeUndefined {
tagType = vc.undefinedValueTagType
if tagType == 0 {
log.Panicf("undefined-value type not set")
}
} else {
tagType = vc.tagType
}
return tagType
}
func (vc *ValueContext) readRawEncoded() (rawBytes []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
tagType := vc.effectiveValueType()
unitSizeRaw := uint32(tagType.Size())
if vc.isEmbedded() == true {
byteLength := unitSizeRaw * vc.unitCount
return vc.rawValueOffset[:byteLength], nil
} else {
return vc.addressableData[vc.valueOffset : vc.valueOffset+vc.unitCount*unitSizeRaw], nil
}
}
// Format returns a string representation for the value.
//
// Where the type is not ASCII, `justFirst` indicates whether to just stringify
// the first item in the slice (or return an empty string if the slice is
// empty).
//
// Since this method lacks the information to process undefined-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (vc *ValueContext) Format() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawBytes, err := vc.readRawEncoded()
log.PanicIf(err)
phrase, err := Format(rawBytes, vc.tagType, false, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
// FormatOne is similar to `Format` but only gets and stringifies the first
// item.
func (vc *ValueContext) FormatFirst() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawBytes, err := vc.readRawEncoded()
log.PanicIf(err)
phrase, err := Format(rawBytes, vc.tagType, true, vc.byteOrder)
log.PanicIf(err)
return phrase, nil
}
func (vc *ValueContext) ReadBytes() (value []byte, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseBytes(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadAscii() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseAscii(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadAsciiNoNul() (value string, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseAsciiNoNul(rawValue, vc.unitCount)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadShorts() (value []uint16, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseShorts(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadLongs() (value []uint32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseLongs(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadRationals() (value []Rational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseRationals(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadSignedLongs() (value []int32, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseSignedLongs(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func (vc *ValueContext) ReadSignedRationals() (value []SignedRational, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
rawValue, err := vc.readRawEncoded()
log.PanicIf(err)
value, err = parser.ParseSignedRationals(rawValue, vc.unitCount, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
// Values knows how to resolve the given value. This value is always a list
// (undefined-values aside), so we're named accordingly.
//
// Since this method lacks the information to process unknown-type tags (e.g.
// byte-order, tag-ID, IFD type), it will return an error if attempted. See
// `Undefined()`.
func (vc *ValueContext) Values() (values interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
if vc.tagType == TypeByte {
values, err = vc.ReadBytes()
log.PanicIf(err)
} else if vc.tagType == TypeAscii {
values, err = vc.ReadAscii()
log.PanicIf(err)
} else if vc.tagType == TypeAsciiNoNul {
values, err = vc.ReadAsciiNoNul()
log.PanicIf(err)
} else if vc.tagType == TypeShort {
values, err = vc.ReadShorts()
log.PanicIf(err)
} else if vc.tagType == TypeLong {
values, err = vc.ReadLongs()
log.PanicIf(err)
} else if vc.tagType == TypeRational {
values, err = vc.ReadRationals()
log.PanicIf(err)
} else if vc.tagType == TypeSignedLong {
values, err = vc.ReadSignedLongs()
log.PanicIf(err)
} else if vc.tagType == TypeSignedRational {
values, err = vc.ReadSignedRationals()
log.PanicIf(err)
} else if vc.tagType == TypeUndefined {
log.Panicf("will not parse undefined-type value")
// Never called.
return nil, nil
} else {
log.Panicf("value of type [%s] is unparseable", vc.tagType)
// Never called.
return nil, nil
}
return values, nil
}
// Undefined attempts to identify and decode supported undefined-type fields.
// This is the primary, preferred interface to reading undefined values.
func (vc *ValueContext) Undefined() (value interface{}, err error) {
defer func() {
if state := recover(); state != nil {
err = log.Wrap(state.(error))
}
}()
value, err = UndefinedValue(vc.ifdPath, vc.tagId, vc, vc.byteOrder)
log.PanicIf(err)
return value, nil
}
func init() {
parser = &Parser{}
}

112
v2/value_context_test.go Normal file
View File

@ -0,0 +1,112 @@
package exif
import (
"bytes"
"testing"
"github.com/dsoprea/go-logging"
)
func TestValueContext_ReadAscii(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
}
}()
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifd := index.RootIfd
var ite *IfdTagEntry
for _, thisIte := range ifd.Entries {
if thisIte.TagId == 0x0110 {
ite = thisIte
break
}
}
if ite == nil {
t.Fatalf("Tag not found.")
}
valueContext := ifd.GetValueContext(ite)
decodedString, err := valueContext.ReadAscii()
log.PanicIf(err)
decodedBytes := []byte(decodedString)
expected := []byte("Canon EOS 5D Mark III")
if bytes.Compare(decodedBytes, expected) != 0 {
t.Fatalf("Decoded bytes not correct.")
}
}
func TestValueContext_Undefined(t *testing.T) {
defer func() {
if state := recover(); state != nil {
err := log.Wrap(state.(error))
log.PrintErrorf(err, "Test failure.")
}
}()
rawExif, err := SearchFileAndExtractExif(testImageFilepath)
log.PanicIf(err)
im := NewIfdMapping()
err = LoadStandardIfds(im)
log.PanicIf(err)
ti := NewTagIndex()
_, index, err := Collect(im, ti, rawExif)
log.PanicIf(err)
ifdExif := index.Lookup[IfdPathStandardExif][0]
var ite *IfdTagEntry
for _, thisIte := range ifdExif.Entries {
if thisIte.TagId == 0x9000 {
ite = thisIte
break
}
}
if ite == nil {
t.Fatalf("Tag not found.")
}
valueContext := ifdExif.GetValueContext(ite)
value, err := valueContext.Undefined()
log.PanicIf(err)
gs, ok := value.(TagUnknownType_GeneralString)
if ok != true {
t.Fatalf("Undefined value not processed correctly.")
}
decodedBytes, err := gs.ValueBytes()
log.PanicIf(err)
expected := []byte("0230")
if bytes.Compare(decodedBytes, expected) != 0 {
t.Fatalf("Decoded bytes not correct.")
}
}