mirror of https://github.com/dsoprea/go-exif.git
parent
74567945ac
commit
903910b6a7
11
README.md
11
README.md
|
@ -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>"
|
||||
```
|
||||
|
||||
|
|
|
@ -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.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 5.3 MiB |
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
|
@ -0,0 +1 @@
|
|||
ruamel.yaml
|
File diff suppressed because it is too large
Load Diff
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package exif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTagNotFound = errors.New("tag not found")
|
||||
ErrTagNotStandard = errors.New("tag not a standard tag")
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)>
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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}]]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
`
|
||||
)
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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).
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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]
|
||||
}
|
|
@ -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{}
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue